{expanded && (
-
+
{text}
{isStreaming && (
-
+
)}
diff --git a/ui/web/src/components/chat/tool-call-card.tsx b/ui/web/src/components/chat/tool-call-card.tsx
index 977cc1a0..6935d35b 100644
--- a/ui/web/src/components/chat/tool-call-card.tsx
+++ b/ui/web/src/components/chat/tool-call-card.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { Wrench, AlertTriangle, Loader2, ChevronDown, ChevronRight, Zap } from "lucide-react";
+import { Wrench, AlertTriangle, ChevronRight, Zap } from "lucide-react";
import type { ToolStreamEntry } from "@/types/chat";
const isSkillTool = (name: string) => name === "use_skill";
@@ -44,9 +44,7 @@ export function ToolCallCard({ entry, compact }: ToolCallCardProps) {
{canExpand && (
- expanded
- ?
- :
+
)}
@@ -88,7 +86,7 @@ function ToolIcon({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSkill
}
}
switch (phase) {
- case "calling": return
;
+ case "calling": return
;
case "completed": return
;
case "error": return
;
default: return
;
diff --git a/ui/web/src/i18n/locales/en/chat.json b/ui/web/src/i18n/locales/en/chat.json
index a8d91aaa..5db2ba2d 100644
--- a/ui/web/src/i18n/locales/en/chat.json
+++ b/ui/web/src/i18n/locales/en/chat.json
@@ -21,5 +21,9 @@
"replyingTo": "Replying to",
"error": {
"notConnected": "Not connected. Please wait for the connection to be established."
+ },
+ "selectAgent": {
+ "title": "Select an agent",
+ "description": "Choose an agent to start chatting"
}
}
diff --git a/ui/web/src/i18n/locales/vi/chat.json b/ui/web/src/i18n/locales/vi/chat.json
index 2ec92c20..fbbbb2c0 100644
--- a/ui/web/src/i18n/locales/vi/chat.json
+++ b/ui/web/src/i18n/locales/vi/chat.json
@@ -21,5 +21,9 @@
"replyingTo": "Đang trả lời",
"error": {
"notConnected": "Chưa kết nối. Vui lòng chờ kết nối được thiết lập."
+ },
+ "selectAgent": {
+ "title": "Chọn agent",
+ "description": "Chọn agent để bắt đầu trò chuyện"
}
}
diff --git a/ui/web/src/i18n/locales/zh/chat.json b/ui/web/src/i18n/locales/zh/chat.json
index 59e20741..b50c25da 100644
--- a/ui/web/src/i18n/locales/zh/chat.json
+++ b/ui/web/src/i18n/locales/zh/chat.json
@@ -21,5 +21,9 @@
"replyingTo": "回复",
"error": {
"notConnected": "未连接。请等待连接建立。"
+ },
+ "selectAgent": {
+ "title": "选择Agent",
+ "description": "选择Agent开始对话"
}
}
diff --git a/ui/web/src/index.css b/ui/web/src/index.css
index e17bb7ee..e18130be 100644
--- a/ui/web/src/index.css
+++ b/ui/web/src/index.css
@@ -173,6 +173,16 @@
.landscape-compact { padding-block: 0.25rem; }
}
+/* Wobble animation for active tool calls — gentler than spin */
+@keyframes wobble {
+ 0%, 100% { transform: rotate(0deg); }
+ 25% { transform: rotate(-12deg); }
+ 75% { transform: rotate(12deg); }
+}
+.animate-wobble {
+ animation: wobble 1s ease-in-out infinite;
+}
+
/* highlight.js: transparent bg, container handles it */
.hljs { background: transparent !important; }
diff --git a/ui/web/src/pages/chat/chat-page.tsx b/ui/web/src/pages/chat/chat-page.tsx
index 9a9c80b5..36fbf612 100644
--- a/ui/web/src/pages/chat/chat-page.tsx
+++ b/ui/web/src/pages/chat/chat-page.tsx
@@ -10,6 +10,7 @@ import { ChatThread } from "./chat-thread";
import { ChatInput, type AttachedFile } from "@/components/chat/chat-input";
import { ChatTopBar } from "@/components/chat/chat-top-bar";
import { DropZone } from "@/components/chat/drop-zone";
+import { AgentPickerPrompt } from "@/components/chat/agent-picker-prompt";
import { useChatSessions } from "./hooks/use-chat-sessions";
import { useChatMessages } from "./hooks/use-chat-messages";
import { useChatSend } from "./hooks/use-chat-send";
@@ -31,7 +32,10 @@ export function ChatPage() {
const sessionKey = urlSessionKey ?? "";
// Fallback agent ID used only when URL has no session key
- const [agentIdFallback, setAgentIdFallback] = useState("default");
+ const [agentIdFallback, setAgentIdFallback] = useState("");
+
+ // Agent is confirmed when URL has a session (agentId parsed) or user explicitly picked one
+ const agentConfirmed = !!urlSessionKey || !!agentIdFallback;
// Derive agentId from URL (source of truth), fallback to state when no session
const agentId = useMemo(() => {
@@ -255,7 +259,14 @@ export function ChatPage() {
onToggleTaskPanel={() => setTaskPanelOpen((v) => !v)}
/>
- {isOwn ? (
+ {!isOwn ? (
+
+
+ {t("readOnly")}
+
+ ) : !agentConfirmed ? (
+
+ ) : (
- ) : (
-
-
- {t("readOnly")}
-
)}
diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts
index 6043e05a..eaab7e10 100644
--- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts
+++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts
@@ -49,6 +49,13 @@ export function useChatMessages(sessionKey: string, agentId: string) {
if (sessionKey === prevKeyRef.current) return;
const wasEmpty = !prevKeyRef.current;
prevKeyRef.current = sessionKey;
+
+ // "" → newKey: part of new-chat send flow. run.started may have already
+ // captured runIdRef before this effect fires. Don't reset run state —
+ // there's no prior session state to clean up from an empty session.
+ if (wasEmpty) return;
+
+ // Session-to-session switch: full reset
setStreamText(null);
setThinkingText(null);
setToolStream([]);
@@ -57,11 +64,7 @@ export function useChatMessages(sessionKey: string, agentId: string) {
setBlockReplies([]);
setTeamTasks([]);
runIdRef.current = null;
- // Only reset when switching between existing sessions, not on "" → new key
- // (the "" → key transition is part of the send flow for new sessions).
- if (!wasEmpty) {
- expectingRunRef.current = false;
- }
+ expectingRunRef.current = false;
streamRef.current = "";
thinkingRef.current = "";
toolStreamRef.current = [];
@@ -150,11 +153,14 @@ export function useChatMessages(sessionKey: string, agentId: string) {
let cancelled = false;
if (sessionKey) {
loadHistory();
- // Restore session running state
- ws.call<{ isRunning?: boolean; activity?: RunActivity }>(Methods.CHAT_SESSION_STATUS, { sessionKey })
+ // Restore session running state (including runId for event filtering)
+ ws.call<{ isRunning?: boolean; runId?: string; activity?: RunActivity }>(Methods.CHAT_SESSION_STATUS, { sessionKey })
.then((res) => {
if (cancelled) return;
- if (res.isRunning) setIsRunning(true);
+ if (res.isRunning) {
+ setIsRunning(true);
+ if (res.runId) runIdRef.current = res.runId;
+ }
if (res.activity) {
setActivity(res.activity);
activityRef.current = res.activity;