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); + }} + > +