diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx index 7bb71eb337c..03eadf59288 100644 --- a/web/src/components/ChatSidebar.tsx +++ b/web/src/components/ChatSidebar.tsx @@ -14,11 +14,10 @@ * * 2. **Event subscriber** (/api/events?channel=…) — passive, receives * every dispatcher emit from the PTY-side `tui_gateway.entry` that - * the dashboard fanned out. This is how `tool.start/progress/ - * complete` from the agent loop reach the sidebar even though the - * PTY child runs three processes deep from us. The `channel` id - * ties this listener to the same chat tab's PTY child — see - * `ChatPage.tsx` for where the id is generated. + * the dashboard fanned out. The sidebar uses it for `session.info` + * (live chat title) and `dashboard.new_session_requested`. The + * `channel` id ties this listener to the same chat tab's PTY child — + * see `ChatPage.tsx` for where the id is generated. * * Best-effort throughout: WS failures show in the badge / banner, the * terminal pane keeps working unimpaired. @@ -31,7 +30,6 @@ import { Card } from "@nous-research/ui/ui/components/card"; import { ModelPickerDialog } from "@/components/ModelPickerDialog"; import { ModelReloadConfirm } from "@/components/ModelReloadConfirm"; import { ReasoningPicker } from "@/components/ReasoningPicker"; -import { ToolCall, type ToolEntry } from "@/components/ToolCall"; import { GatewayClient, type ConnectionState } from "@/lib/gatewayClient"; import { api, HERMES_BASE_PATH, buildWsAuthParam } from "@/lib/api"; import { titleFromSessionInfoPayload } from "@/lib/chat-title"; @@ -53,8 +51,6 @@ interface RpcEnvelope { params?: { type?: string; payload?: unknown }; } -const TOOL_LIMIT = 20; - const STATE_LABEL: Record = { idle: "idle", connecting: "connecting", @@ -81,12 +77,6 @@ interface ChatSidebarProps { className?: string; onDashboardNewSessionRequest?: () => void; onSessionTitleChange?: (title: string | null) => void; - /** - * Render the tool-call activity card. Defaults to true. The dashboard Chat - * tab sets this false so the right rail stays a thin model + session-list - * column; the model picker and its event plumbing are unaffected. - */ - showTools?: boolean; } export function ChatSidebar({ @@ -95,7 +85,6 @@ export function ChatSidebar({ className, onDashboardNewSessionRequest, onSessionTitleChange, - showTools = true, }: ChatSidebarProps) { // `version` bumps on reconnect; gw is derived so we never call setState // for it inside an effect (React 19's set-state-in-effect rule). The @@ -107,7 +96,6 @@ export function ChatSidebar({ const [state, setState] = useState("idle"); const [info, setInfo] = useState({}); - const [tools, setTools] = useState([]); const [modelOpen, setModelOpen] = useState(false); const [error, setError] = useState(null); // The badge shows config.yaml's main model (`model.default`) via @@ -163,7 +151,6 @@ export function ChatSidebar({ if (prevScopeKey.current === scopeKey) return; prevScopeKey.current = scopeKey; setError(null); - setTools([]); setVersion((v) => v + 1); }, [scopeKey]); @@ -291,74 +278,6 @@ export function ChatSidebar({ } } else if (type === "dashboard.new_session_requested") { onDashboardNewSessionRequest?.(); - } else if (type === "tool.start") { - const p = payload as - | { tool_id?: string; name?: string; context?: string } - | undefined; - const toolId = p?.tool_id; - - if (!toolId) { - return; - } - - setTools((prev) => - [ - ...prev, - { - kind: "tool" as const, - id: `tool-${toolId}-${prev.length}`, - tool_id: toolId, - name: p?.name ?? "tool", - context: p?.context, - status: "running" as const, - startedAt: Date.now(), - }, - ].slice(-TOOL_LIMIT), - ); - } else if (type === "tool.progress") { - const p = payload as - | { name?: string; preview?: string } - | undefined; - - if (!p?.name || !p.preview) { - return; - } - - setTools((prev) => - prev.map((t) => - t.status === "running" && t.name === p.name - ? { ...t, preview: p.preview } - : t, - ), - ); - } else if (type === "tool.complete") { - const p = payload as - | { - tool_id?: string; - summary?: string; - error?: string; - inline_diff?: string; - } - | undefined; - - if (!p?.tool_id) { - return; - } - - setTools((prev) => - prev.map((t) => - t.tool_id === p.tool_id - ? { - ...t, - status: p.error ? "error" : "done", - summary: p.summary, - error: p.error, - inline_diff: p.inline_diff, - completedAt: Date.now(), - } - : t, - ), - ); } }); })(); @@ -377,7 +296,6 @@ export function ChatSidebar({ const reconnect = useCallback(() => { setError(null); - setTools([]); setModelNotice(null); setPendingReloadModel(null); setVersion((v) => v + 1); @@ -472,24 +390,6 @@ export function ChatSidebar({ )} - {showTools && ( - -
- tools -
- -
- {tools.length === 0 ? ( -
- no tool calls yet -
- ) : ( - tools.map((t) => ) - )} -
-
- )} - {modelOpen && ( = { - running: "border-primary/40 bg-primary/[0.04]", - done: "border-border bg-muted/20", - error: "border-destructive/50 bg-destructive/[0.04]", -}; - -const BULLET_TONE: Record = { - running: "text-primary", - done: "text-primary/80", - error: "text-destructive", -}; - -const TICK_MS = 500; - -export function ToolCall({ tool }: { tool: ToolEntry }) { - // `open` is derived: errors default-expanded, everything else collapsed. - // `null` means "follow the default"; any explicit bool is the user's override. - // This lets a running tool flip to expanded automatically when it errors, - // without mirroring state in an effect. - const [userOverride, setUserOverride] = useState(null); - const open = userOverride ?? tool.status === "error"; - - // Tick `now` while the tool is running so the elapsed label updates live. - const [now, setNow] = useState(() => Date.now()); - useEffect(() => { - if (tool.status !== "running") return; - const id = window.setInterval(() => setNow(() => Date.now()), TICK_MS); - return () => window.clearInterval(id); - }, [tool.status]); - - // Historical tools (hydrated from session.resume) signal missing timestamps - // with `startedAt === 0`; we hide the elapsed badge for those rather than - // rendering a misleading "0ms". - const hasTimestamps = tool.startedAt > 0; - const elapsed = hasTimestamps - ? fmtElapsed((tool.completedAt ?? now) - tool.startedAt) - : null; - - const hasBody = !!( - tool.context || - tool.preview || - tool.summary || - tool.error || - tool.inline_diff - ); - - const Chevron = open ? ChevronDown : ChevronRight; - - return ( -
- setUserOverride(!open)} - disabled={!hasBody} - aria-expanded={open} - className="px-2.5 py-1.5 text-xs hover:bg-foreground/2 disabled:cursor-default" - > - {hasBody ? ( - - ) : ( - - )} - - - - {tool.name} - - - {tool.context ?? ""} - - - {tool.status === "running" && ( - - )} - {tool.status === "error" && ( - - )} - {tool.status === "done" && ( - - )} - - {elapsed && ( - - {elapsed} - - )} - - - {open && hasBody && ( -
- {tool.context &&
{tool.context}
} - - {tool.preview && tool.status === "running" && ( -
- {tool.preview} - -
- )} - - {tool.inline_diff && ( -
-
-                {colorizeDiff(tool.inline_diff)}
-              
-
- )} - - {tool.summary && ( -
- - {tool.summary} - -
- )} - - {tool.error && ( -
- - {tool.error} - -
- )} -
- )} -
- ); -} - -function Section({ - label, - children, - tone, -}: { - label: string; - children: React.ReactNode; - tone?: "error"; -}) { - return ( -
- - {label} - - -
{children}
-
- ); -} - -function fmtElapsed(ms: number): string { - const sec = Math.max(0, ms) / 1000; - if (sec < 1) return `${Math.round(ms)}ms`; - if (sec < 10) return `${sec.toFixed(1)}s`; - if (sec < 60) return `${Math.round(sec)}s`; - - const m = Math.floor(sec / 60); - const s = Math.round(sec % 60); - return s ? `${m}m ${s}s` : `${m}m`; -} - -/** Colorize unified-diff lines for the inline diff section. */ -function colorizeDiff(diff: string): React.ReactNode { - return diff.split("\n").map((line, i) => ( -
- {line || "\u00A0"} -
- )); -} - -function diffLineClass(line: string): string { - if (line.startsWith("+") && !line.startsWith("+++")) - return "text-success"; - if (line.startsWith("-") && !line.startsWith("---")) - return "text-destructive"; - if (line.startsWith("@@")) return "text-primary"; - return "text-text-secondary"; -} diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index af889dc8765..64094d0c046 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -964,7 +964,6 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { profile={scopedProfile} onDashboardNewSessionRequest={startFreshDashboardChat} onSessionTitleChange={handleSessionTitleChange} - showTools={false} /> - {/* Model picker (tools card hidden — keeps the rail thin). */} + {/* Model picker — keeps the rail thin. */}