From 85f70148a102e6c3bde91ae5fd9b7a8929829860 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 5 Jun 2026 03:02:27 -0700 Subject: [PATCH] feat(dashboard): wire cli-agent chat surface and runner glue; fix stale engine mocks (U12 completion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mount CliChatSurface in ChatView for cli-backed chat sessions (sessions carrying cliExecutorAdapterId): the message-pane + composer region is delegated to the surface (transcript/raw-terminal toggle for hybrid/native adapters, terminal-only for the generic adapter), while regular sessions keep the standard composer. The existing message list and composer JSX are captured once as render thunks and passed through, so there is no parallel message/composer UI. Add a narrow telemetry seam: TelemetryHub gains an optional onEvent tap (also settable post-construction via setEventListener) invoked with each sanitized event after routing — best-effort, a throwing listener never breaks ingest. This is the seam the CliChatSessionRunner uses to build the durable transcript from the same sanitized events the hook route already feeds the hub, without the hub becoming a general subscriber bus. Fix the stale @fusion/engine vi.mocks across dashboard tests: object-literal mocks that fully replace the module now also return listCliAdapterDescriptors (added by U15's cli-agent-settings route, evaluated at module load). Mocks that spread importOriginal/importActual already pick it up. Tests: new ChatView.cli-mount.test.tsx (cli session → CliChatSurface, regular session → normal composer, generic → terminal-only); telemetry-hub onEvent tap coverage. chat-attachment-routes, chat-cli-sessions, cli-agent-hooks-route, ChatView.cli-toggle all green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/cli-agent-hybrid-chat.md | 6 + .../dashboard/app/components/ChatView.tsx | 609 +++++++++--------- .../__tests__/ChatView.cli-mount.test.tsx | 152 +++++ packages/dashboard/app/hooks/useChat.ts | 8 + .../src/__tests__/agent-onboarding.test.ts | 1 + .../dashboard/src/__tests__/ai-refine.test.ts | 1 + .../__tests__/chat-attachment-routes.test.ts | 3 +- .../experiment-routes.finalize.test.ts | 1 + .../milestone-slice-interview.test.ts | 1 + .../src/__tests__/mission-interview.test.ts | 1 + .../__tests__/pr-metadata-generator.test.ts | 1 + .../project-pause-resume-routes.test.ts | 1 + ...utes-approval-sandbox-provisioning.test.ts | 1 + .../__tests__/routes-approval-secrets.test.ts | 1 + .../src/__tests__/routes-approval.test.ts | 1 + .../src/__tests__/routes-worktrunk.test.ts | 1 + .../__tests__/session-error-recovery.test.ts | 1 + .../session-persistence-roundtrip.test.ts | 1 + .../src/__tests__/session-reconnect.test.ts | 1 + .../__tests__/session-resume-history.test.ts | 1 + .../src/__tests__/setup-routes.test.ts | 1 + .../src/__tests__/subtask-breakdown.test.ts | 1 + .../__tests__/agent-avatar-routes.test.ts | 1 + .../routes/__tests__/custom-providers.test.ts | 1 + .../__tests__/docker-node-routes.test.ts | 1 + .../cli-agent/__tests__/telemetry-hub.test.ts | 49 ++ .../engine/src/cli-agent/telemetry-hub.ts | 41 ++ packages/engine/src/index.ts | 1 + 28 files changed, 597 insertions(+), 292 deletions(-) create mode 100644 packages/dashboard/app/components/__tests__/ChatView.cli-mount.test.tsx diff --git a/.changeset/cli-agent-hybrid-chat.md b/.changeset/cli-agent-hybrid-chat.md index b91b683597..6caf871110 100644 --- a/.changeset/cli-agent-hybrid-chat.md +++ b/.changeset/cli-agent-hybrid-chat.md @@ -12,3 +12,9 @@ authoritative session state rather than trusting a cached busy flag. The chat surface gains a transcript ↔ raw-terminal toggle (terminal owns input, composer hidden in terminal mode); generic-tier sessions render terminal-only with no toggle. New per-session `cliExecutorAdapterId` linkage on chat_sessions. + +ChatView now mounts `CliChatSurface` for cli-backed sessions (the message-pane + +composer region is delegated to it; regular sessions keep the standard composer), +and the engine `TelemetryHub` gains a narrow optional `onEvent` tap (settable via +`setEventListener`) so the chat transcript runner can observe the same sanitized +events the hook route already feeds, without the hub becoming a subscriber bus. diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index a9abfd2c9b..53ef468ecc 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -37,6 +37,7 @@ import { AgentMentionPopup } from "./AgentMentionPopup"; import { AgentAvatar } from "./AgentAvatar"; import { FileMentionPopup } from "./FileMentionPopup"; import { CreateRoomModal } from "./CreateRoomModal"; +import { CliChatSurface, type CliChatTier } from "./CliChatSurface"; import { useFileMention } from "../hooks/useFileMention"; import { useModelsCache } from "../hooks/useModelsCache"; import { useDiscoveredSkillsCache } from "../hooks/useDiscoveredSkillsCache"; @@ -2623,6 +2624,300 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView containerEl.scrollTo({ top, behavior: prefersReducedMotion ? "auto" : "smooth" }); }, []); + // ── CLI-backed chat mount (U12) ────────────────────────────────────────── + // When the active chat session selects a cli-agent executor, the message-pane + // + composer region is delegated to (transcript + raw-terminal + // toggle for hybrid/native adapters, terminal-only for the generic adapter). + // The transcript renderer and composer renderer are the EXISTING ChatView JSX + // passed through as thunks so there is no parallel message/composer UI. + const cliAdapterId = activeSession?.cliExecutorAdapterId ?? null; + const cliChatActive = Boolean(cliAdapterId); + // Generic adapter has no structured transcript → terminal-only; every other + // bundled adapter exposes a transcript and gets the toggle (the authoritative + // tier is resolved server-side; this only needs the generic vs. non-generic + // split that drives the toggle's presence). + const cliChatTier: CliChatTier = cliAdapterId === "generic" ? "generic" : "hybrid"; + // Terminal attach id: the native session linkage when known, else the chat id. + const cliTerminalSessionId = activeSession?.cliSessionFile || activeSession?.id || ""; + + // The session message pane and composer, captured once so both the normal + // provider path and the CLI-backed path (CliChatSurface thunks) render the + // exact same JSX — no parallel message/composer UI. + const renderSessionMessagesPane = () => ( +
+
+ {hasMoreMessages && messagesLoading && ( +
{t("chat.loadingOlderMessages", "Loading older messages…")}
+ )} +
+ {isStreaming ? ( + <> + {messages.map((message) => ( + + ))} +
+ {!hideAssistantIdentity && ( +
+ {activeModelProvider ? : } + {agentName} + {showAssistantModelTag && {activeModelTag}} +
+ )} + {streamingText ? ( + renderAssistantContent(streamingText, showAllAsPlain) + ) : ( +
+ {streamingThinking ? t("chat.thinkingStatus", "Thinking…") : t("chat.connectingStatus", "Connecting…")} +
+ )} + {showProviderResponseCopy && streamingText && renderCopyAction("__streaming__", streamingText, "chat-copy-response-streaming")} + {renderToolCalls(streamingToolCalls, t)} + {streamingThinking && ( +
+ {t("chat.thinking", "Thinking")} +
{linkifyFilePaths(streamingThinking)}
+
+ )} +
+ + + +
+
+ + ) : messagesLoading ? ( +
{t("chat.loadingMessages", "Loading messages...")}
+ ) : messages.length === 0 && !activeSession ? ( + renderEmptyState() + ) : messages.length === 0 && activeSession ? ( +
{t("chat.noMessagesYet", "No messages yet. Start the conversation!")}
+ ) : ( + <> + {messages.map((message) => ( + + ))} + + )} +
+
+ ); + + const renderSessionComposerPane = () => ( +
+ { + handleAttachmentFiles(event.target.files); + event.target.value = ""; + }} + /> + {showSkillMenu && ( +
+ {skillsLoading ? ( +
{t("chat.loadingSkills", "Loading skills…")}
+ ) : filteredSkills.length === 0 ? ( +
+ {skillFilter ? t("chat.noSkillsFound", "No skills found") : t("chat.noSkillsAvailable", "No skills available")} +
+ ) : ( + filteredSkills.map((skill, index) => ( + + )) + )} +
+ )} + {pendingAttachments.length > 0 && ( +
+ {pendingAttachments.map((attachment, index) => ( +
+ {attachment.previewUrl ? ( + {attachment.file.name} + ) : ( + {attachment.file.name} + )} + +
+ ))} +
+ )} +
+ +
{ + event.preventDefault(); + setIsDragOver(true); + }} + onDragLeave={() => setIsDragOver(false)} + onDrop={(event) => { + event.preventDefault(); + setIsDragOver(false); + handleAttachmentFiles(event.dataTransfer.files); + }} + > +