@bastani/atomic 0.8.22 → 0.8.23-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +17 -0
- package/dist/builtin/workflows/README.md +31 -12
- package/dist/builtin/workflows/builtin/goal.ts +139 -100
- package/dist/builtin/workflows/builtin/ralph.ts +137 -182
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +2 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +110 -13
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +2 -2
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +31 -11
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +8 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +83 -2
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts +12 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +140 -13
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/docs/workflows.md +66 -17
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-session-host.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-session-host.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentSessionEvent } from \"../../../core/agent-session.ts\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { BashResult } from \"../../../core/bash-executor.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { BashExecutionMessage } from \"../../../core/messages.ts\";\nimport {\n type Component,\n type EditorComponent,\n type EditorTheme,\n type Focusable,\n type MarkdownTheme,\n type TUI,\n Spacer,\n Text,\n matchesKey as tuiMatchesKey,\n truncateToWidth,\n visibleWidth,\n} from \"@earendil-works/pi-tui\";\nimport { SessionManager } from \"../../../core/session-manager.ts\";\nimport { CustomEditor } from \"./custom-editor.ts\";\nimport {\n LiveChatEntriesController,\n renderChatMessageEntry,\n type ChatMessageEntry,\n type ChatMessageRenderOptions,\n} from \"./chat-message-renderer.ts\";\nimport {\n ChatTranscriptComponent,\n ScrollableComponentViewport,\n type ChatTranscriptEntryLike,\n} from \"./chat-transcript.ts\";\nimport { UsageMeterComponent, FooterComponent } from \"./footer.ts\";\nimport { WorkingStatusComponent } from \"./working-status.ts\";\nimport {\n combineQueuedMessagesForEditor,\n openExternalEditorForText,\n pasteClipboardImageToEditor,\n} from \"../chat-input-actions.ts\";\nimport { pickWhimsicalWorkingMessage } from \"../whimsical-messages.ts\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst ANIMATION_FRAME_MS = 80;\nconst STREAMING_RENDER_THROTTLE_MS = 80;\nconst STREAMING_TEXT_TAIL_LINES = 240;\nconst STREAMING_TEXT_TAIL_CHARS = 16_000;\n\nexport interface ChatSessionHostStyle {\n dim(text: string): string;\n text(text: string): string;\n textMuted(text: string): string;\n accent(text: string): string;\n accentBold(text: string): string;\n rule(hex: string, text: string): string;\n cursor(): string;\n blank(width: number): string;\n editorRuleColor(\n disabled: boolean,\n agentSession: AgentSession | undefined,\n state?: { isBashMode: boolean },\n ): string;\n}\n\nexport interface ChatSessionHostBashRequest {\n command: string;\n excludeFromContext: boolean;\n onChunk: (chunk: string) => void;\n}\n\nexport interface ChatSessionHostCommands {\n ensureAttached?: () => Promise<void>;\n prompt?: (text: string) => Promise<void>;\n steer?: (text: string) => Promise<void>;\n followUp?: (text: string) => Promise<void>;\n interrupt?: () => Promise<void>;\n resume?: (message?: string) => Promise<void>;\n runBash?: (request: ChatSessionHostBashRequest) => Promise<BashResult>;\n abortBash?: () => void | Promise<void>;\n abortCompaction?: () => void | Promise<void>;\n handleSlashCommand?: (text: string) => Promise<boolean> | boolean;\n}\n\nexport interface ChatSessionHostOpts<TExtraEntry extends ChatTranscriptEntryLike = never> {\n style: ChatSessionHostStyle;\n commands?: ChatSessionHostCommands;\n requestRender?: () => void;\n getAgentSession?: () => AgentSession | undefined;\n isStreaming?: () => boolean;\n isPaused?: () => boolean;\n isDisabled?: () => boolean;\n isBashRunning?: () => boolean;\n showWarning?: (message: string) => void;\n showStatus?: (message: string) => void;\n actions?: Record<string, () => void | Promise<void>>;\n getActionKeyDisplay?: (action: string) => string;\n getMarkdownTheme?: () => MarkdownTheme;\n tui?: TUI;\n keybindings?: unknown;\n editorFactory?: (\n tui: TUI,\n theme: EditorTheme,\n keybindings: unknown,\n ) => EditorComponent;\n editorTheme: EditorTheme;\n getChatRenderSettings?: () =>\n | Partial<Omit<ChatMessageRenderOptions, \"ui\" | \"cwd\">>\n | undefined;\n getCwd?: () => string;\n footerData?: ReadonlyFooterDataProvider;\n renderExtraEntry?: (entry: TExtraEntry) => Component;\n}\n\nexport type ChatSessionHostEntry<TExtraEntry extends ChatTranscriptEntryLike = never> =\n | ChatMessageEntry\n | TExtraEntry;\ntype AgentSnapshotMessage = AgentSession[\"messages\"][number];\n\nexport class ChatSessionHost<TExtraEntry extends ChatTranscriptEntryLike = never>\n implements Component, Focusable\n{\n focused = true;\n\n private readonly style: ChatSessionHostStyle;\n private readonly commands: ChatSessionHostCommands;\n private readonly requestRender: (() => void) | undefined;\n private readonly getAgentSession: (() => AgentSession | undefined) | undefined;\n private readonly isStreamingOverride: (() => boolean) | undefined;\n private readonly isPaused: (() => boolean) | undefined;\n private readonly isDisabled: (() => boolean) | undefined;\n private readonly isBashRunningOverride: (() => boolean) | undefined;\n private readonly showWarning: ((message: string) => void) | undefined;\n private readonly showStatus: ((message: string) => void) | undefined;\n private readonly actions: Record<string, () => void | Promise<void>> | undefined;\n private readonly getActionKeyDisplay: ((action: string) => string) | undefined;\n private readonly getMarkdownTheme: (() => MarkdownTheme) | undefined;\n private readonly getChatRenderSettings: ChatSessionHostOpts<TExtraEntry>[\"getChatRenderSettings\"];\n private readonly getCwd: (() => string) | undefined;\n private readonly footerData: ReadonlyFooterDataProvider | undefined;\n private readonly renderExtraEntry: ((entry: TExtraEntry) => Component) | undefined;\n private readonly tui: TUI | undefined;\n\n private inputBuffer = \"\";\n private transcript: ChatSessionHostEntry<TExtraEntry>[] = [];\n private statusMessage = \"\";\n private isBashMode = false;\n private localBashRunning = false;\n private sdkBusy = false;\n private workingMessage: string | undefined;\n private pendingSteeringMessages: readonly string[] = [];\n private pendingFollowUpMessages: readonly string[] = [];\n private compactionQueuedMessages: readonly string[] = [];\n private compacting = false;\n private animationTimer: ReturnType<typeof setInterval> | undefined;\n private renderThrottleTimer: ReturnType<typeof setTimeout> | undefined;\n private bodyViewport = new ScrollableComponentViewport();\n private liveChat: LiveChatEntriesController;\n private editor: EditorComponent | undefined;\n private optimisticUserSignatureCounts = new Map<string, number>();\n\n constructor(opts: ChatSessionHostOpts<TExtraEntry>) {\n this.style = opts.style;\n this.commands = opts.commands ?? {};\n this.requestRender = opts.requestRender;\n this.getAgentSession = opts.getAgentSession;\n this.isStreamingOverride = opts.isStreaming;\n this.isPaused = opts.isPaused;\n this.isDisabled = opts.isDisabled;\n this.isBashRunningOverride = opts.isBashRunning;\n this.showWarning = opts.showWarning;\n this.showStatus = opts.showStatus;\n this.actions = opts.actions;\n this.getActionKeyDisplay = opts.getActionKeyDisplay;\n this.getMarkdownTheme = opts.getMarkdownTheme;\n this.getChatRenderSettings = opts.getChatRenderSettings;\n this.getCwd = opts.getCwd;\n this.footerData = opts.footerData;\n this.renderExtraEntry = opts.renderExtraEntry;\n this.tui = opts.tui;\n this.liveChat = new LiveChatEntriesController(this.transcript);\n this.editor = this.createEditor(\n opts.tui,\n opts.keybindings,\n opts.editorTheme,\n opts.editorFactory,\n );\n this.syncAnimationTick();\n }\n\n appendMessages(messages: readonly AgentSnapshotMessage[]): void {\n this.liveChat.appendMessages(messages);\n }\n\n loadSessionFile(sessionFile: string | undefined): void {\n if (this.transcript.length > 0 || sessionFile === undefined) return;\n let messages: readonly AgentSnapshotMessage[];\n try {\n messages = SessionManager.open(sessionFile).buildSessionContext()\n .messages as readonly AgentSnapshotMessage[];\n } catch {\n return;\n }\n this.liveChat.appendMessages(messages);\n }\n\n appendExtraEntry(entry: TExtraEntry): void {\n this.transcript.push(entry);\n }\n\n entries(): readonly ChatSessionHostEntry<TExtraEntry>[] {\n return this.transcript;\n }\n\n private incrementOptimisticUserSignature(signature: string): void {\n this.optimisticUserSignatureCounts.set(\n signature,\n (this.optimisticUserSignatureCounts.get(signature) ?? 0) + 1,\n );\n }\n\n private decrementOptimisticUserSignature(signature: string): void {\n const count = this.optimisticUserSignatureCounts.get(signature) ?? 0;\n if (count <= 1) this.optimisticUserSignatureCounts.delete(signature);\n else this.optimisticUserSignatureCounts.set(signature, count - 1);\n }\n\n applyAgentEvent(event: AgentSessionEvent): boolean {\n const type = String((event as { type?: unknown }).type ?? \"\");\n if (type === \"message_start\") {\n const message = (event as { message?: unknown }).message;\n if (isUserMessageLike(message)) {\n const signature = userMessageSignature(\n extractMessageText(message.content),\n );\n const count = this.optimisticUserSignatureCounts.get(signature) ?? 0;\n if (count > 0) {\n this.decrementOptimisticUserSignature(signature);\n return false;\n }\n }\n }\n if (isSharedLiveChatEvent(type)) {\n const changed = this.liveChat.applyEvent(event);\n const toolCallEvent = assistantToolCallEvent(event);\n const changedByToolCall = toolCallEvent !== undefined\n ? this.liveChat.applyEvent(toolCallEvent)\n : false;\n this.afterEvent(changed || changedByToolCall);\n return changed || changedByToolCall;\n }\n let changed = false;\n switch (type) {\n case \"agent_start\":\n this.sdkBusy = true;\n this.liveChat.clearPendingTools();\n this.statusMessage = \"\";\n changed = true;\n break;\n case \"agent_end\":\n this.sdkBusy = false;\n this.workingMessage = undefined;\n this.liveChat.clearPendingTools();\n this.statusMessage = \"\";\n changed = true;\n break;\n case \"turn_start\":\n this.workingMessage = pickWhimsicalWorkingMessage();\n changed = true;\n break;\n case \"turn_end\":\n this.workingMessage = undefined;\n changed = true;\n break;\n case \"queue_update\": {\n const queue = event as { steering?: unknown; followUp?: unknown };\n this.pendingSteeringMessages = Array.isArray(queue.steering)\n ? queue.steering.filter((item): item is string => typeof item === \"string\")\n : [];\n this.pendingFollowUpMessages = Array.isArray(queue.followUp)\n ? queue.followUp.filter((item): item is string => typeof item === \"string\")\n : [];\n changed = true;\n break;\n }\n case \"tool_call\":\n case \"tool_use\":\n changed = this.liveChat.applyEvent(legacyToolStartEvent(event));\n break;\n case \"tool_result\":\n changed = this.liveChat.applyEvent(legacyToolResultEvent(event));\n break;\n case \"thinking_delta\":\n case \"thinking\":\n changed = this.liveChat.applyEvent(legacyThinkingEvent(event));\n break;\n case \"compaction_start\":\n this.compacting = true;\n this.sdkBusy = true;\n this.statusMessage = \"compacting context…\";\n changed = true;\n break;\n case \"compaction_end\": {\n const compaction = event as Extract<AgentSessionEvent, { type: \"compaction_end\" }>;\n this.compacting = false;\n this.sdkBusy = false;\n this.statusMessage = compaction.errorMessage ?? \"\";\n if (!compaction.aborted && !compaction.errorMessage && this.compactionQueuedMessages.length > 0) {\n void this.flushCompactionQueue();\n }\n changed = true;\n break;\n }\n case \"auto_retry_start\":\n this.sdkBusy = true;\n this.statusMessage = \"retrying…\";\n changed = true;\n break;\n case \"auto_retry_end\": {\n const retry = event as Extract<AgentSessionEvent, { type: \"auto_retry_end\" }>;\n this.statusMessage = \"\";\n if (!retry.success) {\n this.sdkBusy = false;\n this.workingMessage = undefined;\n }\n changed = true;\n break;\n }\n default:\n changed = false;\n }\n this.afterEvent(changed);\n return changed;\n }\n\n render(width: number): string[] {\n return this.renderBody(width, 1);\n }\n\n invalidate(): void {}\n\n renderBody(width: number, budget: number): string[] {\n const components: Component[] = [];\n if (this.transcript.length > 0) {\n components.push(\n new ChatTranscriptComponent(this.transcript, (entry) =>\n this.renderEntry(entry),\n ),\n );\n }\n if (this.statusMessage) {\n components.push(new Spacer(1));\n components.push(new Text(this.style.dim(this.statusMessage), 2, 0));\n }\n this.bodyViewport.setVisibleRows(budget);\n this.bodyViewport.setComponents(components);\n return this.bodyViewport.render(width);\n }\n\n renderPendingMessages(width: number): string[] {\n if (\n this.pendingSteeringMessages.length === 0 &&\n this.pendingFollowUpMessages.length === 0 &&\n this.compactionQueuedMessages.length === 0\n ) {\n return [];\n }\n const lines = [this.style.blank(width)];\n for (const message of this.pendingSteeringMessages) {\n lines.push(...this.pendingMessageLine(width, \"Steering\", message));\n }\n for (const message of this.pendingFollowUpMessages) {\n lines.push(...this.pendingMessageLine(width, \"Follow-up\", message));\n }\n for (const message of this.compactionQueuedMessages) {\n lines.push(...this.pendingMessageLine(width, \"Queued\", message));\n }\n const hint = this.getActionKeyDisplay?.(\"app.message.dequeue\") ?? \"alt+up\";\n lines.push(...new Text(this.style.dim(`↳ ${hint} to edit all queued messages`), 1, 0).render(width));\n return lines;\n }\n\n renderWorkingStatus(width: number): string[] {\n if (!this.isStreaming()) return [];\n const message = this.workingMessage ?? \"Working...\";\n return new WorkingStatusComponent({\n spinner: spinnerFrame(),\n message,\n spinnerColor: (text) => this.style.accentBold(text),\n messageColor: (text) => this.style.textMuted(text),\n }).render(width);\n }\n\n renderUsage(width: number): string[] {\n const agentSession = this.getAgentSession?.();\n if (!agentSession) return [];\n return new UsageMeterComponent(agentSession).render(width);\n }\n\n renderEditor(width: number): string[] {\n const disabled = this.isDisabled?.() === true;\n const agentSession = this.getAgentSession?.();\n const ruleHex = this.style.editorRuleColor(disabled, agentSession, {\n isBashMode: this.isBashMode,\n });\n if (!disabled && this.editor) {\n setEditorFocused(this.editor, this.focused);\n setEditorPlaceholder(this.editor, undefined);\n setEditorBorderColor(this.editor, (text) => this.style.rule(ruleHex, text));\n return this.editor.render(width);\n }\n if (this.editor) setEditorFocused(this.editor, false);\n const rule = this.style.rule(ruleHex, \"─\".repeat(width));\n const available = Math.max(1, width - 3);\n const value = this.inputBuffer\n ? this.style.text(truncateToWidth(this.inputBuffer, available)) + this.style.cursor()\n : disabled\n ? \"\"\n : this.style.cursor();\n const left = this.style.accentBold(\"❯\") + \" \" + value;\n const gap = Math.max(0, width - visibleWidth(stripAnsi(left)));\n const body = left + \" \".repeat(gap);\n return [rule, body, rule];\n }\n\n renderFooter(width: number): string[] {\n const agentSession = this.getAgentSession?.();\n if (agentSession && this.footerData) {\n return new FooterComponent(agentSession, this.footerData).render(width);\n }\n return [];\n }\n\n handleScrollInput(data: string): boolean {\n return this.bodyViewport.handleInput(data);\n }\n\n handleInput(data: string): boolean {\n if (this.handleScrollInput(data)) return true;\n if (matchesKey(data, \"alt+up\")) {\n this.restoreQueuedMessagesToEditor();\n return true;\n }\n if (matchesKey(data, \"ctrl+f\")) {\n void this.submit(\"followUp\");\n return true;\n }\n if (matchesKey(data, \"escape\")) {\n if (this.compacting) {\n void this.abortCompaction();\n return true;\n }\n if (this.isStreaming()) {\n void this.interrupt();\n return true;\n }\n if (this.isBashRunning()) {\n void this.abortBash();\n return true;\n }\n if (this.isBashMode) {\n this.setEditorText(\"\");\n this.notifyStatus(\"Bash mode cleared\");\n return true;\n }\n }\n if (this.editor) {\n this.editor.handleInput(data);\n return true;\n }\n if (matchesKey(data, \"enter\")) {\n void this.submit(\"auto\");\n return true;\n }\n if (matchesKey(data, \"backspace\")) {\n this.setEditorText(this.inputBuffer.slice(0, -1));\n return true;\n }\n if (data.length === 1 && data >= \" \" && data <= \"~\") {\n this.setEditorText(`${this.inputBuffer}${data}`);\n return true;\n }\n return false;\n }\n\n async interrupt(): Promise<void> {\n try {\n this.restoreQueuedMessagesToEditor();\n this.sdkBusy = false;\n this.workingMessage = undefined;\n await this.commands.interrupt?.();\n } catch (err) {\n this.statusMessage = errorMessage(err);\n } finally {\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n async submit(mode: \"auto\" | \"followUp\" = \"auto\", submittedText?: string): Promise<void> {\n const text = (submittedText ?? this.inputBuffer).trim();\n if (!text) return;\n if (text.startsWith(\"/\") && this.commands.handleSlashCommand) {\n const handled = await this.commands.handleSlashCommand(text);\n if (handled) {\n this.setEditorText(\"\");\n this.requestRender?.();\n return;\n }\n }\n if (this.compacting) {\n this.setEditorText(\"\");\n this.compactionQueuedMessages = [...this.compactionQueuedMessages, text];\n this.notifyStatus(\"Queued message until compaction completes\");\n return;\n }\n const bash = parseBashInput(text);\n if (bash?.command) {\n if (this.isBashRunning()) {\n this.notifyWarning(\"A bash command is already running. esc cancel first.\");\n this.setEditorText(text);\n return;\n }\n this.setEditorText(\"\");\n await this.runBashCommand(bash.command, bash.excludeFromContext);\n return;\n }\n this.setEditorText(\"\");\n const isPaused = this.isPaused?.() === true;\n const isStreaming = this.isStreaming();\n const shouldAppendOptimisticUser = mode === \"auto\" && !isStreaming;\n const optimisticSignature = shouldAppendOptimisticUser\n ? userMessageSignature(text)\n : undefined;\n if (optimisticSignature !== undefined) {\n this.liveChat.appendUserText(text);\n this.bodyViewport.scrollToBottom();\n this.incrementOptimisticUserSignature(optimisticSignature);\n }\n this.requestRender?.();\n try {\n if (isPaused) {\n this.sdkBusy = true;\n this.statusMessage = \"resuming…\";\n this.syncAnimationTick();\n this.requestRender?.();\n await this.requiredCommand(\"resume\")(text);\n this.sdkBusy = false;\n this.statusMessage = \"\";\n this.syncAnimationTick();\n return;\n }\n if (mode === \"followUp\" && isStreaming) {\n await this.queueFollowUp(text);\n return;\n }\n if (isStreaming) {\n await this.queueSteer(text);\n } else {\n this.sdkBusy = true;\n this.syncAnimationTick();\n await this.commands.ensureAttached?.();\n await this.requiredCommand(\"prompt\")(text);\n this.sdkBusy = false;\n this.syncAnimationTick();\n }\n } catch (err) {\n if (optimisticSignature !== undefined) {\n this.decrementOptimisticUserSignature(optimisticSignature);\n }\n this.sdkBusy = false;\n this.statusMessage = errorMessage(err);\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n isStreaming(): boolean {\n return this.sdkBusy || this.isStreamingOverride?.() === true;\n }\n\n isBashRunning(): boolean {\n return this.localBashRunning || this.isBashRunningOverride?.() === true;\n }\n\n isEditingBashCommand(): boolean {\n return this.isBashMode;\n }\n\n hasInputText(): boolean {\n return this.inputBuffer.length > 0;\n }\n\n hasAnimationTick(): boolean {\n return this.animationTimer !== undefined;\n }\n\n bodyScrollFromBottom(): number {\n return this.bodyViewport.getScrollFromBottom();\n }\n\n bodyMaxScroll(): number {\n return this.bodyViewport.getMaxScroll();\n }\n\n inputText(): string {\n return this.inputBuffer;\n }\n\n statusText(): string {\n return this.statusMessage;\n }\n\n scrollToBottom(): void {\n this.bodyViewport.scrollToBottom();\n }\n\n syncAnimationTick(): void {\n const shouldAnimate =\n this.isStreaming() || (this.sdkBusy && this.liveChat.pendingToolIds().length > 0);\n if (shouldAnimate && !this.animationTimer) {\n this.animationTimer = setInterval(() => {\n this.requestRender?.();\n }, ANIMATION_FRAME_MS);\n this.animationTimer.unref?.();\n return;\n }\n if (!shouldAnimate && this.animationTimer) {\n clearInterval(this.animationTimer);\n this.animationTimer = undefined;\n }\n }\n\n dispose(): void {\n if (this.animationTimer) {\n clearInterval(this.animationTimer);\n this.animationTimer = undefined;\n }\n if (this.renderThrottleTimer) {\n clearTimeout(this.renderThrottleTimer);\n this.renderThrottleTimer = undefined;\n }\n this.editor = undefined;\n }\n\n private createEditor(\n tui: TUI | undefined,\n keybindings: unknown,\n editorTheme: EditorTheme,\n editorFactory: ChatSessionHostOpts<TExtraEntry>[\"editorFactory\"],\n ): EditorComponent | undefined {\n if (!tui || !keybindings) return undefined;\n const editor = this.createInheritedEditor(tui, editorTheme, keybindings, editorFactory) ??\n new CustomEditor(\n tui,\n editorTheme,\n keybindings as ConstructorParameters<typeof CustomEditor>[2],\n { paddingX: 0, autocompleteMaxVisible: 5 },\n );\n editor.onChange = (text) => {\n this.inputBuffer = text;\n this.isBashMode = text.trimStart().startsWith(\"!\");\n };\n editor.onSubmit = (text) => {\n void this.submit(\"auto\", text);\n };\n const actionEditor = editor as EditorComponent & {\n onAction?: (action: string, handler: () => void) => void;\n onEscape?: () => void;\n onPasteImage?: () => void;\n };\n actionEditor.onAction?.(\"app.message.followUp\", () => {\n void this.submit(\"followUp\");\n });\n actionEditor.onAction?.(\"app.message.dequeue\", () => {\n this.restoreQueuedMessagesToEditor();\n });\n actionEditor.onAction?.(\"app.editor.external\", () => {\n this.openExternalEditor();\n });\n if (this.actions) {\n for (const [action, handler] of Object.entries(this.actions)) {\n actionEditor.onAction?.(action, () => {\n void handler();\n });\n }\n }\n const previousPasteImage = actionEditor.onPasteImage;\n actionEditor.onPasteImage = () => {\n previousPasteImage?.();\n void pasteClipboardImageToEditor(\n this.editorAccess(),\n () => this.requestRender?.(),\n { showWarning: (message) => this.notifyWarning(message) },\n );\n };\n const previousEscape = actionEditor.onEscape;\n actionEditor.onEscape = () => {\n if (this.compacting) {\n void this.abortCompaction();\n return;\n }\n if (this.isStreaming()) {\n void this.interrupt();\n return;\n }\n if (this.isBashRunning()) {\n void this.abortBash();\n return;\n }\n if (this.isBashMode) {\n this.setEditorText(\"\");\n this.notifyStatus(\"Bash mode cleared\");\n return;\n }\n previousEscape?.();\n };\n return editor;\n }\n\n private createInheritedEditor(\n tui: TUI,\n editorTheme: EditorTheme,\n keybindings: unknown,\n editorFactory: ChatSessionHostOpts<TExtraEntry>[\"editorFactory\"],\n ): EditorComponent | undefined {\n if (!editorFactory) return undefined;\n try {\n return editorFactory(tui, editorTheme, keybindings);\n } catch {\n return undefined;\n }\n }\n\n private editorAccess(): {\n insertTextAtCursor: (text: string) => void;\n getText: () => string;\n setText: (text: string) => void;\n } {\n return {\n insertTextAtCursor: (text: string) => {\n if (this.editor?.insertTextAtCursor) {\n this.editor.insertTextAtCursor(text);\n return;\n }\n this.setEditorText(`${this.inputBuffer}${text}`);\n },\n getText: () => this.inputBuffer,\n setText: (text: string) => this.setEditorText(text),\n };\n }\n\n private openExternalEditor(): void {\n if (!this.editor) return;\n const host = this.tuiHost();\n if (!host) return;\n const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();\n const updated = openExternalEditorForText(currentText, host, {\n showWarning: (message) => this.notifyWarning(message),\n });\n if (updated !== undefined) this.setEditorText(updated);\n }\n\n private tuiHost(): Pick<TUI, \"stop\" | \"start\" | \"requestRender\"> | undefined {\n return this.tui;\n }\n\n private setEditorText(text: string): void {\n this.inputBuffer = text;\n this.isBashMode = text.trimStart().startsWith(\"!\");\n this.editor?.setText(text);\n }\n\n private renderEntry(entry: ChatSessionHostEntry<TExtraEntry>): Component {\n if (isChatMessageEntry(entry)) {\n return renderChatMessageEntry(\n this.streamingWindowedEntry(entry),\n this.chatMessageRenderOptions(),\n );\n }\n if (!this.renderExtraEntry) {\n return new Text(\"\", 0, 0);\n }\n return this.renderExtraEntry(entry);\n }\n\n private streamingWindowedEntry(entry: ChatMessageEntry): ChatMessageEntry {\n if (!this.isStreaming() || this.bodyViewport.getScrollFromBottom() !== 0) {\n return entry;\n }\n if (entry.kind !== \"assistant\") return entry;\n const content = entry.message.content.map((item) => {\n if (item.type === \"text\") return { ...item, text: tailStreamingText(item.text) };\n if (item.type === \"thinking\") return { ...item, thinking: tailStreamingText(item.thinking) };\n return item;\n });\n return { ...entry, message: { ...entry.message, content } };\n }\n\n private chatMessageRenderOptions(): ChatMessageRenderOptions {\n const inherited = this.getChatRenderSettings?.();\n return {\n ...inherited,\n ui: this.toolTui(),\n cwd: this.getCwd?.() ?? this.getAgentSession?.()?.sessionManager.getCwd() ?? process.cwd(),\n markdownTheme: inherited?.markdownTheme ?? this.getMarkdownTheme?.(),\n showImages: inherited?.showImages ?? true,\n };\n }\n\n private toolTui(): Pick<TUI, \"requestRender\"> {\n return { requestRender: () => this.requestRender?.() };\n }\n\n private pendingMessageLine(\n width: number,\n label: \"Steering\" | \"Follow-up\" | \"Queued\",\n message: string,\n ): string[] {\n const text = `${label}: ${message}`;\n return new Text(\n this.style.dim(truncateToWidth(text, Math.max(1, width - 2))),\n 1,\n 0,\n ).render(width);\n }\n\n private async abortCompaction(): Promise<void> {\n try {\n await this.commands.abortCompaction?.();\n this.compacting = false;\n this.sdkBusy = false;\n this.notifyStatus(\"Compaction cancelled\");\n } catch (err) {\n this.notifyWarning(errorMessage(err));\n } finally {\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n private async abortBash(): Promise<void> {\n try {\n await this.commands.abortBash?.();\n this.localBashRunning = false;\n this.notifyStatus(\"Bash command cancelled\");\n } catch (err) {\n this.notifyWarning(errorMessage(err));\n } finally {\n this.requestRender?.();\n }\n }\n\n private async runBashCommand(\n command: string,\n excludeFromContext: boolean,\n ): Promise<void> {\n const runBash = this.commands.runBash;\n if (!runBash) {\n this.notifyWarning(\"no bash command configured for this chat session\");\n return;\n }\n const bashMessage: BashExecutionMessage = {\n role: \"bashExecution\",\n command,\n output: \"\",\n exitCode: undefined,\n cancelled: false,\n truncated: false,\n timestamp: Date.now(),\n ...(excludeFromContext ? { excludeFromContext: true } : {}),\n };\n const bashEntry: ChatMessageEntry = {\n role: \"tool\",\n kind: \"bashExecution\",\n message: bashMessage,\n isPartial: true,\n };\n this.transcript.push(bashEntry);\n this.localBashRunning = true;\n this.bodyViewport.scrollToBottom();\n this.requestRender?.();\n try {\n const result = await runBash({\n command,\n excludeFromContext,\n onChunk: (chunk) => {\n bashMessage.output += chunk;\n this.requestRender?.();\n },\n });\n bashMessage.output = result.output;\n bashMessage.exitCode = result.exitCode;\n bashMessage.cancelled = result.cancelled;\n bashMessage.truncated = result.truncated;\n if (result.fullOutputPath !== undefined) {\n bashMessage.fullOutputPath = result.fullOutputPath;\n }\n } catch (err) {\n bashMessage.output = errorMessage(err);\n bashMessage.exitCode = undefined;\n bashMessage.cancelled = false;\n bashMessage.truncated = false;\n } finally {\n bashEntry.isPartial = false;\n this.localBashRunning = false;\n this.requestRender?.();\n }\n }\n\n private async flushCompactionQueue(): Promise<void> {\n const queued = [...this.compactionQueuedMessages];\n this.compactionQueuedMessages = [];\n if (queued.length === 0) return;\n let nextIndex = 0;\n try {\n const first = queued[0];\n if (first !== undefined) {\n await this.requiredCommand(\"prompt\")(first);\n nextIndex = 1;\n }\n for (; nextIndex < queued.length; nextIndex++) {\n await this.queueFollowUp(queued[nextIndex]!);\n }\n } catch (err) {\n this.compactionQueuedMessages = [\n ...queued.slice(nextIndex),\n ...this.compactionQueuedMessages,\n ];\n this.notifyWarning(errorMessage(err));\n this.requestRender?.();\n }\n }\n\n private async queueSteer(text: string): Promise<void> {\n const agentSession = this.getAgentSession?.();\n if (agentSession?.isStreaming) {\n await agentSession.prompt(text, { streamingBehavior: \"steer\" });\n return;\n }\n await this.requiredCommand(\"steer\")(text);\n }\n\n private async queueFollowUp(text: string): Promise<void> {\n const agentSession = this.getAgentSession?.();\n if (agentSession?.isStreaming) {\n await agentSession.prompt(text, { streamingBehavior: \"followUp\" });\n return;\n }\n await this.requiredCommand(\"followUp\")(text);\n }\n\n restoreQueuedMessagesToEditor(): boolean {\n const queuedMessages = [\n ...this.pendingSteeringMessages,\n ...this.pendingFollowUpMessages,\n ...this.compactionQueuedMessages,\n ];\n if (queuedMessages.length === 0) {\n this.notifyStatus(\"No queued messages to restore\");\n return false;\n }\n const restoredText = combineQueuedMessagesForEditor(queuedMessages, this.inputBuffer);\n this.pendingSteeringMessages = [];\n this.pendingFollowUpMessages = [];\n this.compactionQueuedMessages = [];\n this.setEditorText(restoredText);\n this.getAgentSession?.()?.clearQueue();\n this.notifyStatus(\n `Restored ${queuedMessages.length} queued message${queuedMessages.length === 1 ? \"\" : \"s\"} to editor`,\n );\n this.requestRender?.();\n return true;\n }\n\n private notifyWarning(message: string): void {\n this.statusMessage = message;\n this.showWarning?.(message);\n this.requestRender?.();\n }\n\n private notifyStatus(message: string): void {\n this.statusMessage = message;\n this.showStatus?.(message);\n this.requestRender?.();\n }\n\n private requiredCommand(\n name: \"prompt\" | \"steer\" | \"followUp\" | \"resume\",\n ): (text?: string) => Promise<void> {\n switch (name) {\n case \"prompt\":\n return async (text) => {\n if (!this.commands.prompt) throw new Error(\"no prompt command configured for this chat session\");\n await this.commands.prompt(text ?? \"\");\n };\n case \"steer\":\n return async (text) => {\n if (!this.commands.steer) throw new Error(\"no steer command configured for this chat session\");\n await this.commands.steer(text ?? \"\");\n };\n case \"followUp\":\n return async (text) => {\n if (!this.commands.followUp) throw new Error(\"no followUp command configured for this chat session\");\n await this.commands.followUp(text ?? \"\");\n };\n case \"resume\":\n return async (text) => {\n if (!this.commands.resume) throw new Error(\"no resume command configured for this chat session\");\n await this.commands.resume(text);\n };\n }\n }\n\n private afterEvent(changed: boolean): void {\n this.syncAnimationTick();\n if (!changed) return;\n this.requestEventRender();\n }\n\n private requestEventRender(): void {\n if (!this.isStreaming()) {\n this.requestRender?.();\n return;\n }\n if (this.renderThrottleTimer) return;\n this.renderThrottleTimer = setTimeout(() => {\n this.renderThrottleTimer = undefined;\n this.requestRender?.();\n }, STREAMING_RENDER_THROTTLE_MS);\n this.renderThrottleTimer.unref?.();\n }\n}\n\nfunction setEditorPlaceholder(\n editor: EditorComponent,\n placeholder: string | undefined,\n): void {\n const candidate = editor as EditorComponent & {\n setPlaceholder?: (value: string | undefined) => void;\n };\n candidate.setPlaceholder?.(placeholder);\n}\n\nfunction setEditorBorderColor(\n editor: EditorComponent,\n borderColor: (text: string) => string,\n): void {\n const candidate = editor as EditorComponent & {\n borderColor?: (text: string) => string;\n };\n if (candidate.borderColor !== undefined) candidate.borderColor = borderColor;\n}\n\nfunction setEditorFocused(editor: EditorComponent, focused: boolean): void {\n const candidate = editor as EditorComponent & Partial<Focusable>;\n if (\"focused\" in candidate) candidate.focused = focused;\n}\n\nfunction matchesKey(\n data: string,\n key: \"enter\" | \"backspace\" | \"escape\" | \"ctrl+f\" | \"alt+up\",\n): boolean {\n if (key === \"enter\" && (data === \"\\r\" || data === \"\\n\")) return true;\n if (key === \"backspace\" && (data === \"\\x7f\" || data === \"\\b\")) return true;\n if (key === \"escape\" && data === \"\\x1b\") return true;\n if (key === \"ctrl+f\" && data === \"\\x06\") return true;\n return tuiMatchesKey(data, key);\n}\n\nfunction parseBashInput(text: string):\n | { command: string; excludeFromContext: boolean }\n | undefined {\n if (!text.startsWith(\"!\")) return undefined;\n const excludeFromContext = text.startsWith(\"!!\");\n const command = text.slice(excludeFromContext ? 2 : 1).trim();\n return { command, excludeFromContext };\n}\n\nfunction isSharedLiveChatEvent(type: string): boolean {\n return (\n type === \"message_start\" ||\n type === \"message_update\" ||\n type === \"message_end\" ||\n type === \"tool_execution_start\" ||\n type === \"tool_execution_update\" ||\n type === \"tool_execution_end\"\n );\n}\n\nfunction isChatMessageEntry<TExtraEntry extends ChatTranscriptEntryLike>(\n entry: ChatSessionHostEntry<TExtraEntry>,\n): entry is ChatMessageEntry {\n if (!(\"role\" in entry) || !(\"kind\" in entry)) return false;\n const candidate = entry as { role?: unknown; kind?: unknown; message?: unknown; text?: unknown };\n switch (candidate.kind) {\n case \"assistant\":\n return candidate.role === \"assistant\" && candidate.message !== undefined;\n case \"tool\":\n return candidate.role === \"tool\" && \"toolName\" in candidate && \"toolCallId\" in candidate && \"args\" in candidate;\n case \"bashExecution\":\n return candidate.role === \"tool\" && candidate.message !== undefined;\n case \"user\":\n return candidate.role === \"user\" && typeof candidate.text === \"string\";\n case \"custom\":\n return candidate.role === \"custom\" && candidate.message !== undefined;\n case \"branchSummary\":\n case \"compactionSummary\":\n return candidate.role === \"summary\" && candidate.message !== undefined;\n case \"system\":\n return candidate.role === \"system\" && candidate.message !== undefined;\n default:\n return false;\n }\n}\n\nfunction isMessageLike(message: unknown): message is { role?: unknown; content?: unknown } {\n return message !== null && typeof message === \"object\" && \"role\" in message;\n}\n\nfunction isUserMessageLike(\n message: unknown,\n): message is { role: \"user\"; content?: unknown } {\n return isMessageLike(message) && message.role === \"user\";\n}\n\nfunction userMessageSignature(text: string): string {\n return text.trim();\n}\n\nfunction assistantToolCallEvent(event: AgentSessionEvent): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} | undefined {\n const assistantEvent = (event as {\n assistantMessageEvent?: {\n type?: unknown;\n contentIndex?: unknown;\n partial?: unknown;\n toolCall?: unknown;\n };\n }).assistantMessageEvent;\n const streamType = String(assistantEvent?.type ?? \"\");\n if (!streamType.startsWith(\"toolcall_\")) return undefined;\n const explicit = toolCallPayload(assistantEvent?.toolCall);\n if (explicit) return explicit;\n const contentIndex = typeof assistantEvent?.contentIndex === \"number\" ? assistantEvent.contentIndex : undefined;\n if (contentIndex === undefined) return undefined;\n const partial = assistantEvent?.partial;\n if (!isMessageLike(partial) || partial.role !== \"assistant\") return undefined;\n const content = partial.content;\n if (!Array.isArray(content)) return undefined;\n return toolCallPayload(content[contentIndex]);\n}\n\nfunction toolCallPayload(value: unknown): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} | undefined {\n if (value === null || typeof value !== \"object\") return undefined;\n const candidate = value as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };\n if (candidate.type !== \"toolCall\") return undefined;\n if (typeof candidate.id !== \"string\" || typeof candidate.name !== \"string\") return undefined;\n return {\n type: \"tool_execution_start\",\n toolCallId: candidate.id,\n toolName: candidate.name,\n args: candidate.arguments ?? {},\n };\n}\n\nfunction legacyToolStartEvent(event: AgentSessionEvent): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} {\n const payload = event as { toolCallId?: unknown; name?: unknown; input?: unknown; args?: unknown };\n const toolName = typeof payload.name === \"string\" ? payload.name : \"tool\";\n const toolCallId =\n typeof payload.toolCallId === \"string\" ? payload.toolCallId : `live-${toolName}`;\n return {\n type: \"tool_execution_start\",\n toolCallId,\n toolName,\n args: payload.input ?? payload.args ?? {},\n };\n}\n\nfunction legacyToolResultEvent(event: AgentSessionEvent): {\n type: \"tool_execution_end\";\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError: boolean;\n} {\n const payload = event as {\n toolCallId?: unknown;\n name?: unknown;\n output?: unknown;\n isError?: unknown;\n };\n const toolName = typeof payload.name === \"string\" ? payload.name : \"tool\";\n const toolCallId =\n typeof payload.toolCallId === \"string\" ? payload.toolCallId : `live-${toolName}`;\n const output = payload.output;\n return {\n type: \"tool_execution_end\",\n toolCallId,\n toolName,\n result:\n output !== null && typeof output === \"object\" && \"content\" in output\n ? output\n : { content: typeof output === \"string\" ? [{ type: \"text\", text: output }] : [] },\n isError: payload.isError === true,\n };\n}\n\nfunction legacyThinkingEvent(event: AgentSessionEvent): {\n type: \"message_update\";\n assistantMessageEvent: { type: \"thinking_delta\"; delta: string };\n message: { role: \"assistant\"; content: [] };\n} {\n const delta = String(\n (event as { delta?: unknown }).delta ??\n (event as { text?: unknown }).text ??\n \"\",\n );\n return {\n type: \"message_update\",\n assistantMessageEvent: { type: \"thinking_delta\", delta },\n message: { role: \"assistant\", content: [] },\n };\n}\n\nfunction extractMessageText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (item == null) continue;\n if (typeof item === \"string\") {\n parts.push(item);\n continue;\n }\n const obj = item as { type?: unknown; text?: unknown };\n if (typeof obj.text === \"string\") parts.push(obj.text);\n else if (obj.type === \"text\" && typeof obj.text === \"string\") parts.push(obj.text);\n }\n return parts.join(\"\");\n}\n\nfunction tailStreamingText(text: string): string {\n if (\n text.length <= STREAMING_TEXT_TAIL_CHARS &&\n text.split(\"\\n\").length <= STREAMING_TEXT_TAIL_LINES\n ) {\n return text;\n }\n const byChars = text.slice(-STREAMING_TEXT_TAIL_CHARS);\n const lines = byChars.split(\"\\n\");\n const tail =\n lines.length > STREAMING_TEXT_TAIL_LINES\n ? lines.slice(-STREAMING_TEXT_TAIL_LINES).join(\"\\n\")\n : byChars;\n return `[earlier streaming output hidden while attached]\\n\\n${tail.trimStart()}`;\n}\n\nfunction spinnerFrame(): string {\n const idx = Math.floor(Date.now() / 80) % SPINNER_FRAMES.length;\n return SPINNER_FRAMES[idx]!;\n}\n\nfunction stripAnsi(s: string): string {\n return s.replace(/\\x1B\\[[0-?]*[ -/]*[@-~]/g, \"\");\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chat-session-host.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/chat-session-host.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentSessionEvent } from \"../../../core/agent-session.ts\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { BashResult } from \"../../../core/bash-executor.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { BashExecutionMessage } from \"../../../core/messages.ts\";\nimport {\n type Component,\n type EditorComponent,\n type EditorTheme,\n type Focusable,\n type MarkdownTheme,\n type TUI,\n Spacer,\n Text,\n matchesKey as tuiMatchesKey,\n truncateToWidth,\n visibleWidth,\n} from \"@earendil-works/pi-tui\";\nimport { SessionManager } from \"../../../core/session-manager.ts\";\nimport { CustomEditor } from \"./custom-editor.ts\";\nimport {\n LiveChatEntriesController,\n renderChatMessageEntry,\n type ChatMessageEntry,\n type ChatMessageRenderOptions,\n} from \"./chat-message-renderer.ts\";\nimport {\n ChatTranscriptComponent,\n ScrollableComponentViewport,\n type ChatTranscriptEntryLike,\n} from \"./chat-transcript.ts\";\nimport { UsageMeterComponent, FooterComponent } from \"./footer.ts\";\nimport { WorkingStatusComponent } from \"./working-status.ts\";\nimport {\n combineQueuedMessagesForEditor,\n openExternalEditorForText,\n pasteClipboardImageToEditor,\n} from \"../chat-input-actions.ts\";\nimport { pickWhimsicalWorkingMessage } from \"../whimsical-messages.ts\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst ANIMATION_FRAME_MS = 80;\nconst STREAMING_RENDER_THROTTLE_MS = 80;\nconst STREAMING_TEXT_TAIL_LINES = 240;\nconst STREAMING_TEXT_TAIL_CHARS = 16_000;\n\nexport interface ChatSessionHostStyle {\n dim(text: string): string;\n text(text: string): string;\n textMuted(text: string): string;\n accent(text: string): string;\n accentBold(text: string): string;\n rule(hex: string, text: string): string;\n cursor(): string;\n blank(width: number): string;\n editorRuleColor(\n disabled: boolean,\n agentSession: AgentSession | undefined,\n state?: { isBashMode: boolean },\n ): string;\n}\n\nexport interface ChatSessionHostBashRequest {\n command: string;\n excludeFromContext: boolean;\n onChunk: (chunk: string) => void;\n}\n\nexport interface ChatSessionHostCommands {\n ensureAttached?: () => Promise<void>;\n prompt?: (text: string) => Promise<void>;\n steer?: (text: string) => Promise<void>;\n followUp?: (text: string) => Promise<void>;\n interrupt?: () => Promise<void>;\n resume?: (message?: string) => Promise<void>;\n runBash?: (request: ChatSessionHostBashRequest) => Promise<BashResult>;\n abortBash?: () => void | Promise<void>;\n abortCompaction?: () => void | Promise<void>;\n handleSlashCommand?: (text: string) => Promise<boolean> | boolean;\n}\n\nexport interface ChatSessionHostOpts<TExtraEntry extends ChatTranscriptEntryLike = never> {\n style: ChatSessionHostStyle;\n commands?: ChatSessionHostCommands;\n requestRender?: () => void;\n getAgentSession?: () => AgentSession | undefined;\n isStreaming?: () => boolean;\n isPaused?: () => boolean;\n isDisabled?: () => boolean;\n isBashRunning?: () => boolean;\n showWarning?: (message: string) => void;\n showStatus?: (message: string) => void;\n actions?: Record<string, () => void | Promise<void>>;\n getActionKeyDisplay?: (action: string) => string;\n getMarkdownTheme?: () => MarkdownTheme;\n tui?: TUI;\n keybindings?: unknown;\n editorFactory?: (\n tui: TUI,\n theme: EditorTheme,\n keybindings: unknown,\n ) => EditorComponent;\n editorTheme: EditorTheme;\n getChatRenderSettings?: () =>\n | Partial<Omit<ChatMessageRenderOptions, \"ui\" | \"cwd\">>\n | undefined;\n getCwd?: () => string;\n footerData?: ReadonlyFooterDataProvider;\n renderExtraEntry?: (entry: TExtraEntry) => Component;\n}\n\nexport type ChatSessionHostEntry<TExtraEntry extends ChatTranscriptEntryLike = never> =\n | ChatMessageEntry\n | TExtraEntry;\ntype AgentSnapshotMessage = AgentSession[\"messages\"][number];\ntype CacheKeyPart = string | number | boolean | null;\n\nexport class ChatSessionHost<TExtraEntry extends ChatTranscriptEntryLike = never>\n implements Component, Focusable\n{\n focused = true;\n\n private readonly style: ChatSessionHostStyle;\n private readonly commands: ChatSessionHostCommands;\n private readonly requestRender: (() => void) | undefined;\n private readonly getAgentSession: (() => AgentSession | undefined) | undefined;\n private readonly isStreamingOverride: (() => boolean) | undefined;\n private readonly isPaused: (() => boolean) | undefined;\n private readonly isDisabled: (() => boolean) | undefined;\n private readonly isBashRunningOverride: (() => boolean) | undefined;\n private readonly showWarning: ((message: string) => void) | undefined;\n private readonly showStatus: ((message: string) => void) | undefined;\n private readonly actions: Record<string, () => void | Promise<void>> | undefined;\n private readonly getActionKeyDisplay: ((action: string) => string) | undefined;\n private readonly getMarkdownTheme: (() => MarkdownTheme) | undefined;\n private readonly getChatRenderSettings: ChatSessionHostOpts<TExtraEntry>[\"getChatRenderSettings\"];\n private readonly getCwd: (() => string) | undefined;\n private readonly footerData: ReadonlyFooterDataProvider | undefined;\n private readonly renderExtraEntry: ((entry: TExtraEntry) => Component) | undefined;\n private readonly tui: TUI | undefined;\n\n private inputBuffer = \"\";\n private transcript: ChatSessionHostEntry<TExtraEntry>[] = [];\n private statusMessage = \"\";\n private isBashMode = false;\n private localBashRunning = false;\n private sdkBusy = false;\n private workingMessage: string | undefined;\n private pendingSteeringMessages: readonly string[] = [];\n private pendingFollowUpMessages: readonly string[] = [];\n private compactionQueuedMessages: readonly string[] = [];\n private compacting = false;\n private animationTimer: ReturnType<typeof setInterval> | undefined;\n private renderThrottleTimer: ReturnType<typeof setTimeout> | undefined;\n private bodyViewport = new ScrollableComponentViewport();\n private transcriptComponent: ChatTranscriptComponent<ChatSessionHostEntry<TExtraEntry>>;\n private transcriptRenderSettingsKey = \"\";\n private renderIdentityIds = new WeakMap<object, number>();\n private nextRenderIdentityId = 1;\n private liveChat: LiveChatEntriesController;\n private editor: EditorComponent | undefined;\n private optimisticUserSignatureCounts = new Map<string, number>();\n\n constructor(opts: ChatSessionHostOpts<TExtraEntry>) {\n this.style = opts.style;\n this.commands = opts.commands ?? {};\n this.requestRender = opts.requestRender;\n this.getAgentSession = opts.getAgentSession;\n this.isStreamingOverride = opts.isStreaming;\n this.isPaused = opts.isPaused;\n this.isDisabled = opts.isDisabled;\n this.isBashRunningOverride = opts.isBashRunning;\n this.showWarning = opts.showWarning;\n this.showStatus = opts.showStatus;\n this.actions = opts.actions;\n this.getActionKeyDisplay = opts.getActionKeyDisplay;\n this.getMarkdownTheme = opts.getMarkdownTheme;\n this.getChatRenderSettings = opts.getChatRenderSettings;\n this.getCwd = opts.getCwd;\n this.footerData = opts.footerData;\n this.renderExtraEntry = opts.renderExtraEntry;\n this.tui = opts.tui;\n this.liveChat = new LiveChatEntriesController(this.transcript);\n this.transcriptComponent = new ChatTranscriptComponent(\n this.transcript,\n (entry) => this.renderEntry(entry),\n (entry, index) => this.transcriptCacheKey(entry, index),\n );\n this.editor = this.createEditor(\n opts.tui,\n opts.keybindings,\n opts.editorTheme,\n opts.editorFactory,\n );\n this.syncAnimationTick();\n }\n\n appendMessages(messages: readonly AgentSnapshotMessage[]): void {\n this.liveChat.appendMessages(messages);\n }\n\n loadSessionFile(sessionFile: string | undefined): void {\n if (this.transcript.length > 0 || sessionFile === undefined) return;\n let messages: readonly AgentSnapshotMessage[];\n try {\n messages = SessionManager.open(sessionFile).buildSessionContext()\n .messages as readonly AgentSnapshotMessage[];\n } catch {\n return;\n }\n this.liveChat.appendMessages(messages);\n }\n\n appendExtraEntry(entry: TExtraEntry): void {\n this.transcript.push(entry);\n }\n\n entries(): readonly ChatSessionHostEntry<TExtraEntry>[] {\n return this.transcript;\n }\n\n private incrementOptimisticUserSignature(signature: string): void {\n this.optimisticUserSignatureCounts.set(\n signature,\n (this.optimisticUserSignatureCounts.get(signature) ?? 0) + 1,\n );\n }\n\n private decrementOptimisticUserSignature(signature: string): void {\n const count = this.optimisticUserSignatureCounts.get(signature) ?? 0;\n if (count <= 1) this.optimisticUserSignatureCounts.delete(signature);\n else this.optimisticUserSignatureCounts.set(signature, count - 1);\n }\n\n applyAgentEvent(event: AgentSessionEvent): boolean {\n const type = String((event as { type?: unknown }).type ?? \"\");\n if (type === \"message_start\") {\n const message = (event as { message?: unknown }).message;\n if (isUserMessageLike(message)) {\n const signature = userMessageSignature(\n extractMessageText(message.content),\n );\n const count = this.optimisticUserSignatureCounts.get(signature) ?? 0;\n if (count > 0) {\n this.decrementOptimisticUserSignature(signature);\n return false;\n }\n }\n }\n if (isSharedLiveChatEvent(type)) {\n const changed = this.liveChat.applyEvent(event);\n const toolCallEvent = assistantToolCallEvent(event);\n const changedByToolCall = toolCallEvent !== undefined\n ? this.liveChat.applyEvent(toolCallEvent)\n : false;\n this.afterEvent(changed || changedByToolCall);\n return changed || changedByToolCall;\n }\n let changed = false;\n switch (type) {\n case \"agent_start\":\n this.sdkBusy = true;\n this.liveChat.clearPendingTools();\n this.statusMessage = \"\";\n changed = true;\n break;\n case \"agent_end\":\n this.sdkBusy = false;\n this.workingMessage = undefined;\n this.liveChat.clearPendingTools();\n this.statusMessage = \"\";\n changed = true;\n break;\n case \"turn_start\":\n this.workingMessage = pickWhimsicalWorkingMessage();\n changed = true;\n break;\n case \"turn_end\":\n this.workingMessage = undefined;\n changed = true;\n break;\n case \"queue_update\": {\n const queue = event as { steering?: unknown; followUp?: unknown };\n this.pendingSteeringMessages = Array.isArray(queue.steering)\n ? queue.steering.filter((item): item is string => typeof item === \"string\")\n : [];\n this.pendingFollowUpMessages = Array.isArray(queue.followUp)\n ? queue.followUp.filter((item): item is string => typeof item === \"string\")\n : [];\n changed = true;\n break;\n }\n case \"tool_call\":\n case \"tool_use\":\n changed = this.liveChat.applyEvent(legacyToolStartEvent(event));\n break;\n case \"tool_result\":\n changed = this.liveChat.applyEvent(legacyToolResultEvent(event));\n break;\n case \"thinking_delta\":\n case \"thinking\":\n changed = this.liveChat.applyEvent(legacyThinkingEvent(event));\n break;\n case \"compaction_start\":\n this.compacting = true;\n this.sdkBusy = true;\n this.statusMessage = \"compacting context…\";\n changed = true;\n break;\n case \"compaction_end\": {\n const compaction = event as Extract<AgentSessionEvent, { type: \"compaction_end\" }>;\n this.compacting = false;\n this.sdkBusy = false;\n this.statusMessage = compaction.errorMessage ?? \"\";\n if (!compaction.aborted && !compaction.errorMessage && this.compactionQueuedMessages.length > 0) {\n void this.flushCompactionQueue();\n }\n changed = true;\n break;\n }\n case \"auto_retry_start\":\n this.sdkBusy = true;\n this.statusMessage = \"retrying…\";\n changed = true;\n break;\n case \"auto_retry_end\": {\n const retry = event as Extract<AgentSessionEvent, { type: \"auto_retry_end\" }>;\n this.statusMessage = \"\";\n if (!retry.success) {\n this.sdkBusy = false;\n this.workingMessage = undefined;\n }\n changed = true;\n break;\n }\n default:\n changed = false;\n }\n this.afterEvent(changed);\n return changed;\n }\n\n render(width: number): string[] {\n return this.renderBody(width, 1);\n }\n\n invalidate(): void {\n this.transcriptComponent.invalidate();\n this.bodyViewport.invalidate();\n this.editor?.invalidate();\n }\n\n renderBody(width: number, budget: number): string[] {\n const components: Component[] = [];\n this.transcriptRenderSettingsKey = this.chatRenderSettingsCacheKey();\n if (this.transcript.length > 0) {\n components.push(this.transcriptComponent);\n }\n if (this.statusMessage) {\n components.push(new Spacer(1));\n components.push(new Text(this.style.dim(this.statusMessage), 2, 0));\n }\n this.bodyViewport.setVisibleRows(budget);\n this.bodyViewport.setComponents(components);\n return this.bodyViewport.render(width);\n }\n\n renderPendingMessages(width: number): string[] {\n if (\n this.pendingSteeringMessages.length === 0 &&\n this.pendingFollowUpMessages.length === 0 &&\n this.compactionQueuedMessages.length === 0\n ) {\n return [];\n }\n const lines = [this.style.blank(width)];\n for (const message of this.pendingSteeringMessages) {\n lines.push(...this.pendingMessageLine(width, \"Steering\", message));\n }\n for (const message of this.pendingFollowUpMessages) {\n lines.push(...this.pendingMessageLine(width, \"Follow-up\", message));\n }\n for (const message of this.compactionQueuedMessages) {\n lines.push(...this.pendingMessageLine(width, \"Queued\", message));\n }\n const hint = this.getActionKeyDisplay?.(\"app.message.dequeue\") ?? \"alt+up\";\n lines.push(...new Text(this.style.dim(`↳ ${hint} to edit all queued messages`), 1, 0).render(width));\n return lines;\n }\n\n renderWorkingStatus(width: number): string[] {\n if (!this.isStreaming()) return [];\n const message = this.workingMessage ?? \"Working...\";\n return new WorkingStatusComponent({\n spinner: spinnerFrame(),\n message,\n spinnerColor: (text) => this.style.accentBold(text),\n messageColor: (text) => this.style.textMuted(text),\n }).render(width);\n }\n\n renderUsage(width: number): string[] {\n const agentSession = this.getAgentSession?.();\n if (!agentSession) return [];\n return new UsageMeterComponent(agentSession).render(width);\n }\n\n renderEditor(width: number): string[] {\n const disabled = this.isDisabled?.() === true;\n const agentSession = this.getAgentSession?.();\n const ruleHex = this.style.editorRuleColor(disabled, agentSession, {\n isBashMode: this.isBashMode,\n });\n if (!disabled && this.editor) {\n setEditorFocused(this.editor, this.focused);\n setEditorPlaceholder(this.editor, undefined);\n setEditorBorderColor(this.editor, (text) => this.style.rule(ruleHex, text));\n return this.editor.render(width);\n }\n if (this.editor) setEditorFocused(this.editor, false);\n const rule = this.style.rule(ruleHex, \"─\".repeat(width));\n const available = Math.max(1, width - 3);\n const value = this.inputBuffer\n ? this.style.text(truncateToWidth(this.inputBuffer, available)) + this.style.cursor()\n : disabled\n ? \"\"\n : this.style.cursor();\n const left = this.style.accentBold(\"❯\") + \" \" + value;\n const gap = Math.max(0, width - visibleWidth(stripAnsi(left)));\n const body = left + \" \".repeat(gap);\n return [rule, body, rule];\n }\n\n renderFooter(width: number): string[] {\n const agentSession = this.getAgentSession?.();\n if (agentSession && this.footerData) {\n return new FooterComponent(agentSession, this.footerData).render(width);\n }\n return [];\n }\n\n handleScrollInput(data: string): boolean {\n return this.bodyViewport.handleInput(data);\n }\n\n handleInput(data: string): boolean {\n if (this.handleScrollInput(data)) return true;\n if (matchesKey(data, \"alt+up\")) {\n this.restoreQueuedMessagesToEditor();\n return true;\n }\n if (matchesKey(data, \"ctrl+f\")) {\n void this.submit(\"followUp\");\n return true;\n }\n if (matchesKey(data, \"escape\")) {\n if (this.compacting) {\n void this.abortCompaction();\n return true;\n }\n if (this.isStreaming()) {\n void this.interrupt();\n return true;\n }\n if (this.isBashRunning()) {\n void this.abortBash();\n return true;\n }\n if (this.isBashMode) {\n this.setEditorText(\"\");\n this.notifyStatus(\"Bash mode cleared\");\n return true;\n }\n }\n if (this.editor) {\n this.editor.handleInput(data);\n return true;\n }\n if (matchesKey(data, \"enter\")) {\n void this.submit(\"auto\");\n return true;\n }\n if (matchesKey(data, \"backspace\")) {\n this.setEditorText(this.inputBuffer.slice(0, -1));\n return true;\n }\n if (data.length === 1 && data >= \" \" && data <= \"~\") {\n this.setEditorText(`${this.inputBuffer}${data}`);\n return true;\n }\n return false;\n }\n\n async interrupt(): Promise<void> {\n try {\n this.restoreQueuedMessagesToEditor();\n this.sdkBusy = false;\n this.workingMessage = undefined;\n await this.commands.interrupt?.();\n } catch (err) {\n this.statusMessage = errorMessage(err);\n } finally {\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n async submit(mode: \"auto\" | \"followUp\" = \"auto\", submittedText?: string): Promise<void> {\n const text = (submittedText ?? this.inputBuffer).trim();\n if (!text) return;\n if (text.startsWith(\"/\") && this.commands.handleSlashCommand) {\n const handled = await this.commands.handleSlashCommand(text);\n if (handled) {\n this.setEditorText(\"\");\n this.requestRender?.();\n return;\n }\n }\n if (this.compacting) {\n this.setEditorText(\"\");\n this.compactionQueuedMessages = [...this.compactionQueuedMessages, text];\n this.notifyStatus(\"Queued message until compaction completes\");\n return;\n }\n const bash = parseBashInput(text);\n if (bash?.command) {\n if (this.isBashRunning()) {\n this.notifyWarning(\"A bash command is already running. esc cancel first.\");\n this.setEditorText(text);\n return;\n }\n this.setEditorText(\"\");\n await this.runBashCommand(bash.command, bash.excludeFromContext);\n return;\n }\n this.setEditorText(\"\");\n const isPaused = this.isPaused?.() === true;\n const isStreaming = this.isStreaming();\n const shouldAppendOptimisticUser = mode === \"auto\" && !isStreaming;\n const optimisticSignature = shouldAppendOptimisticUser\n ? userMessageSignature(text)\n : undefined;\n if (optimisticSignature !== undefined) {\n this.liveChat.appendUserText(text);\n this.bodyViewport.scrollToBottom();\n this.incrementOptimisticUserSignature(optimisticSignature);\n }\n this.requestRender?.();\n try {\n if (isPaused) {\n this.sdkBusy = true;\n this.statusMessage = \"resuming…\";\n this.syncAnimationTick();\n this.requestRender?.();\n await this.requiredCommand(\"resume\")(text);\n this.sdkBusy = false;\n this.statusMessage = \"\";\n this.syncAnimationTick();\n return;\n }\n if (mode === \"followUp\" && isStreaming) {\n await this.queueFollowUp(text);\n return;\n }\n if (isStreaming) {\n await this.queueSteer(text);\n } else {\n this.sdkBusy = true;\n this.syncAnimationTick();\n await this.commands.ensureAttached?.();\n await this.requiredCommand(\"prompt\")(text);\n this.sdkBusy = false;\n this.syncAnimationTick();\n }\n } catch (err) {\n if (optimisticSignature !== undefined) {\n this.decrementOptimisticUserSignature(optimisticSignature);\n }\n this.sdkBusy = false;\n this.statusMessage = errorMessage(err);\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n isStreaming(): boolean {\n return this.sdkBusy || this.isStreamingOverride?.() === true;\n }\n\n isBashRunning(): boolean {\n return this.localBashRunning || this.isBashRunningOverride?.() === true;\n }\n\n isEditingBashCommand(): boolean {\n return this.isBashMode;\n }\n\n hasInputText(): boolean {\n return this.inputBuffer.length > 0;\n }\n\n hasAnimationTick(): boolean {\n return this.animationTimer !== undefined;\n }\n\n bodyScrollFromBottom(): number {\n return this.bodyViewport.getScrollFromBottom();\n }\n\n bodyMaxScroll(): number {\n return this.bodyViewport.getMaxScroll();\n }\n\n inputText(): string {\n return this.inputBuffer;\n }\n\n statusText(): string {\n return this.statusMessage;\n }\n\n scrollToBottom(): void {\n this.bodyViewport.scrollToBottom();\n }\n\n syncAnimationTick(): void {\n const shouldAnimate =\n this.isStreaming() || (this.sdkBusy && this.liveChat.pendingToolIds().length > 0);\n if (shouldAnimate && !this.animationTimer) {\n this.animationTimer = setInterval(() => {\n this.requestRender?.();\n }, ANIMATION_FRAME_MS);\n this.animationTimer.unref?.();\n return;\n }\n if (!shouldAnimate && this.animationTimer) {\n clearInterval(this.animationTimer);\n this.animationTimer = undefined;\n }\n }\n\n dispose(): void {\n if (this.animationTimer) {\n clearInterval(this.animationTimer);\n this.animationTimer = undefined;\n }\n if (this.renderThrottleTimer) {\n clearTimeout(this.renderThrottleTimer);\n this.renderThrottleTimer = undefined;\n }\n this.editor = undefined;\n }\n\n private createEditor(\n tui: TUI | undefined,\n keybindings: unknown,\n editorTheme: EditorTheme,\n editorFactory: ChatSessionHostOpts<TExtraEntry>[\"editorFactory\"],\n ): EditorComponent | undefined {\n if (!tui || !keybindings) return undefined;\n const editor = this.createInheritedEditor(tui, editorTheme, keybindings, editorFactory) ??\n new CustomEditor(\n tui,\n editorTheme,\n keybindings as ConstructorParameters<typeof CustomEditor>[2],\n { paddingX: 0, autocompleteMaxVisible: 5 },\n );\n editor.onChange = (text) => {\n this.inputBuffer = text;\n this.isBashMode = text.trimStart().startsWith(\"!\");\n };\n editor.onSubmit = (text) => {\n void this.submit(\"auto\", text);\n };\n const actionEditor = editor as EditorComponent & {\n onAction?: (action: string, handler: () => void) => void;\n onEscape?: () => void;\n onPasteImage?: () => void;\n };\n actionEditor.onAction?.(\"app.message.followUp\", () => {\n void this.submit(\"followUp\");\n });\n actionEditor.onAction?.(\"app.message.dequeue\", () => {\n this.restoreQueuedMessagesToEditor();\n });\n actionEditor.onAction?.(\"app.editor.external\", () => {\n this.openExternalEditor();\n });\n if (this.actions) {\n for (const [action, handler] of Object.entries(this.actions)) {\n actionEditor.onAction?.(action, () => {\n void handler();\n });\n }\n }\n const previousPasteImage = actionEditor.onPasteImage;\n actionEditor.onPasteImage = () => {\n previousPasteImage?.();\n void pasteClipboardImageToEditor(\n this.editorAccess(),\n () => this.requestRender?.(),\n { showWarning: (message) => this.notifyWarning(message) },\n );\n };\n const previousEscape = actionEditor.onEscape;\n actionEditor.onEscape = () => {\n if (this.compacting) {\n void this.abortCompaction();\n return;\n }\n if (this.isStreaming()) {\n void this.interrupt();\n return;\n }\n if (this.isBashRunning()) {\n void this.abortBash();\n return;\n }\n if (this.isBashMode) {\n this.setEditorText(\"\");\n this.notifyStatus(\"Bash mode cleared\");\n return;\n }\n previousEscape?.();\n };\n return editor;\n }\n\n private createInheritedEditor(\n tui: TUI,\n editorTheme: EditorTheme,\n keybindings: unknown,\n editorFactory: ChatSessionHostOpts<TExtraEntry>[\"editorFactory\"],\n ): EditorComponent | undefined {\n if (!editorFactory) return undefined;\n try {\n return editorFactory(tui, editorTheme, keybindings);\n } catch {\n return undefined;\n }\n }\n\n private editorAccess(): {\n insertTextAtCursor: (text: string) => void;\n getText: () => string;\n setText: (text: string) => void;\n } {\n return {\n insertTextAtCursor: (text: string) => {\n if (this.editor?.insertTextAtCursor) {\n this.editor.insertTextAtCursor(text);\n return;\n }\n this.setEditorText(`${this.inputBuffer}${text}`);\n },\n getText: () => this.inputBuffer,\n setText: (text: string) => this.setEditorText(text),\n };\n }\n\n private openExternalEditor(): void {\n if (!this.editor) return;\n const host = this.tuiHost();\n if (!host) return;\n const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();\n const updated = openExternalEditorForText(currentText, host, {\n showWarning: (message) => this.notifyWarning(message),\n });\n if (updated !== undefined) this.setEditorText(updated);\n }\n\n private tuiHost(): Pick<TUI, \"stop\" | \"start\" | \"requestRender\"> | undefined {\n return this.tui;\n }\n\n private setEditorText(text: string): void {\n this.inputBuffer = text;\n this.isBashMode = text.trimStart().startsWith(\"!\");\n this.editor?.setText(text);\n }\n\n private renderEntry(entry: ChatSessionHostEntry<TExtraEntry>): Component {\n if (isChatMessageEntry(entry)) {\n return renderChatMessageEntry(\n this.streamingWindowedEntry(entry),\n this.chatMessageRenderOptions(),\n );\n }\n if (!this.renderExtraEntry) {\n return new Text(\"\", 0, 0);\n }\n return this.renderExtraEntry(entry);\n }\n\n private streamingWindowedEntry(entry: ChatMessageEntry): ChatMessageEntry {\n if (!this.isStreaming() || this.bodyViewport.getScrollFromBottom() !== 0) {\n return entry;\n }\n if (entry.kind !== \"assistant\") return entry;\n const content = entry.message.content.map((item) => {\n if (item.type === \"text\") return { ...item, text: tailStreamingText(item.text) };\n if (item.type === \"thinking\") return { ...item, thinking: tailStreamingText(item.thinking) };\n return item;\n });\n return { ...entry, message: { ...entry.message, content } };\n }\n\n private chatMessageRenderOptions(): ChatMessageRenderOptions {\n const inherited = this.getChatRenderSettings?.();\n return {\n ...inherited,\n ui: this.toolTui(),\n cwd: this.getCwd?.() ?? this.getAgentSession?.()?.sessionManager.getCwd() ?? process.cwd(),\n markdownTheme: inherited?.markdownTheme ?? this.getMarkdownTheme?.(),\n showImages: inherited?.showImages ?? true,\n };\n }\n\n private toolTui(): Pick<TUI, \"requestRender\"> {\n return { requestRender: () => this.requestRender?.() };\n }\n\n private chatRenderSettingsCacheKey(): string {\n const inherited = this.getChatRenderSettings?.();\n return cacheKey([\n \"settings\",\n inherited?.hideThinkingBlock === true,\n inherited?.hiddenThinkingLabel ?? \"\",\n inherited?.toolOutputExpanded === true,\n inherited?.showImages !== false,\n inherited?.imageWidthCells ?? null,\n this.getCwd?.() ?? this.getAgentSession?.()?.sessionManager.getCwd() ?? process.cwd(),\n this.bodyViewport.getScrollFromBottom() === 0,\n this.isStreaming(),\n ]);\n }\n\n private transcriptCacheKey(\n entry: ChatSessionHostEntry<TExtraEntry>,\n index: number,\n ): string {\n return cacheKey([\n this.transcriptRenderSettingsKey,\n index,\n entry.role,\n this.renderIdentityKey(entry),\n this.entryContentCacheKey(entry),\n ]);\n }\n\n private entryContentCacheKey(entry: ChatSessionHostEntry<TExtraEntry>): string {\n if (!isChatMessageEntry(entry)) return cacheKey([\"extra\"]);\n switch (entry.kind) {\n case \"assistant\":\n return cacheKey([\"assistant\", this.renderIdentityKey(entry.message)]);\n case \"tool\":\n return cacheKey([\n \"tool\",\n entry.toolCallId,\n entry.toolName,\n entry.isPartial === true,\n entry.result === undefined ? null : this.renderIdentityKey(entry.result),\n ]);\n case \"bashExecution\":\n return cacheKey([\n \"bashExecution\",\n entry.isPartial === true,\n entry.message.command,\n entry.message.output,\n entry.message.exitCode ?? null,\n entry.message.cancelled,\n entry.message.truncated,\n entry.message.fullOutputPath ?? null,\n ]);\n case \"user\":\n return cacheKey([\"user\", entry.text]);\n case \"custom\":\n return cacheKey([\"custom\", this.renderIdentityKey(entry.message)]);\n case \"branchSummary\":\n return cacheKey([\"branchSummary\", this.renderIdentityKey(entry.message)]);\n case \"compactionSummary\":\n return cacheKey([\"compactionSummary\", this.renderIdentityKey(entry.message)]);\n case \"system\":\n return cacheKey([\"system\", entry.text]);\n }\n }\n\n private renderIdentityKey(value: object): string {\n const existing = this.renderIdentityIds.get(value);\n if (existing !== undefined) return String(existing);\n const id = this.nextRenderIdentityId;\n this.nextRenderIdentityId += 1;\n this.renderIdentityIds.set(value, id);\n return String(id);\n }\n\n private pendingMessageLine(\n width: number,\n label: \"Steering\" | \"Follow-up\" | \"Queued\",\n message: string,\n ): string[] {\n const text = `${label}: ${message}`;\n return new Text(\n this.style.dim(truncateToWidth(text, Math.max(1, width - 2))),\n 1,\n 0,\n ).render(width);\n }\n\n private async abortCompaction(): Promise<void> {\n try {\n await this.commands.abortCompaction?.();\n this.compacting = false;\n this.sdkBusy = false;\n this.notifyStatus(\"Compaction cancelled\");\n } catch (err) {\n this.notifyWarning(errorMessage(err));\n } finally {\n this.syncAnimationTick();\n this.requestRender?.();\n }\n }\n\n private async abortBash(): Promise<void> {\n try {\n await this.commands.abortBash?.();\n this.localBashRunning = false;\n this.notifyStatus(\"Bash command cancelled\");\n } catch (err) {\n this.notifyWarning(errorMessage(err));\n } finally {\n this.requestRender?.();\n }\n }\n\n private async runBashCommand(\n command: string,\n excludeFromContext: boolean,\n ): Promise<void> {\n const runBash = this.commands.runBash;\n if (!runBash) {\n this.notifyWarning(\"no bash command configured for this chat session\");\n return;\n }\n const bashMessage: BashExecutionMessage = {\n role: \"bashExecution\",\n command,\n output: \"\",\n exitCode: undefined,\n cancelled: false,\n truncated: false,\n timestamp: Date.now(),\n ...(excludeFromContext ? { excludeFromContext: true } : {}),\n };\n const bashEntry: ChatMessageEntry = {\n role: \"tool\",\n kind: \"bashExecution\",\n message: bashMessage,\n isPartial: true,\n };\n this.transcript.push(bashEntry);\n this.localBashRunning = true;\n this.bodyViewport.scrollToBottom();\n this.requestRender?.();\n try {\n const result = await runBash({\n command,\n excludeFromContext,\n onChunk: (chunk) => {\n bashMessage.output += chunk;\n this.requestRender?.();\n },\n });\n bashMessage.output = result.output;\n bashMessage.exitCode = result.exitCode;\n bashMessage.cancelled = result.cancelled;\n bashMessage.truncated = result.truncated;\n if (result.fullOutputPath !== undefined) {\n bashMessage.fullOutputPath = result.fullOutputPath;\n }\n } catch (err) {\n bashMessage.output = errorMessage(err);\n bashMessage.exitCode = undefined;\n bashMessage.cancelled = false;\n bashMessage.truncated = false;\n } finally {\n bashEntry.isPartial = false;\n this.localBashRunning = false;\n this.requestRender?.();\n }\n }\n\n private async flushCompactionQueue(): Promise<void> {\n const queued = [...this.compactionQueuedMessages];\n this.compactionQueuedMessages = [];\n if (queued.length === 0) return;\n let nextIndex = 0;\n try {\n const first = queued[0];\n if (first !== undefined) {\n await this.requiredCommand(\"prompt\")(first);\n nextIndex = 1;\n }\n for (; nextIndex < queued.length; nextIndex++) {\n await this.queueFollowUp(queued[nextIndex]!);\n }\n } catch (err) {\n this.compactionQueuedMessages = [\n ...queued.slice(nextIndex),\n ...this.compactionQueuedMessages,\n ];\n this.notifyWarning(errorMessage(err));\n this.requestRender?.();\n }\n }\n\n private async queueSteer(text: string): Promise<void> {\n const agentSession = this.getAgentSession?.();\n if (agentSession?.isStreaming) {\n await agentSession.prompt(text, { streamingBehavior: \"steer\" });\n return;\n }\n await this.requiredCommand(\"steer\")(text);\n }\n\n private async queueFollowUp(text: string): Promise<void> {\n const agentSession = this.getAgentSession?.();\n if (agentSession?.isStreaming) {\n await agentSession.prompt(text, { streamingBehavior: \"followUp\" });\n return;\n }\n await this.requiredCommand(\"followUp\")(text);\n }\n\n restoreQueuedMessagesToEditor(): boolean {\n const queuedMessages = [\n ...this.pendingSteeringMessages,\n ...this.pendingFollowUpMessages,\n ...this.compactionQueuedMessages,\n ];\n if (queuedMessages.length === 0) {\n this.notifyStatus(\"No queued messages to restore\");\n return false;\n }\n const restoredText = combineQueuedMessagesForEditor(queuedMessages, this.inputBuffer);\n this.pendingSteeringMessages = [];\n this.pendingFollowUpMessages = [];\n this.compactionQueuedMessages = [];\n this.setEditorText(restoredText);\n this.getAgentSession?.()?.clearQueue();\n this.notifyStatus(\n `Restored ${queuedMessages.length} queued message${queuedMessages.length === 1 ? \"\" : \"s\"} to editor`,\n );\n this.requestRender?.();\n return true;\n }\n\n private notifyWarning(message: string): void {\n this.statusMessage = message;\n this.showWarning?.(message);\n this.requestRender?.();\n }\n\n private notifyStatus(message: string): void {\n this.statusMessage = message;\n this.showStatus?.(message);\n this.requestRender?.();\n }\n\n private requiredCommand(\n name: \"prompt\" | \"steer\" | \"followUp\" | \"resume\",\n ): (text?: string) => Promise<void> {\n switch (name) {\n case \"prompt\":\n return async (text) => {\n if (!this.commands.prompt) throw new Error(\"no prompt command configured for this chat session\");\n await this.commands.prompt(text ?? \"\");\n };\n case \"steer\":\n return async (text) => {\n if (!this.commands.steer) throw new Error(\"no steer command configured for this chat session\");\n await this.commands.steer(text ?? \"\");\n };\n case \"followUp\":\n return async (text) => {\n if (!this.commands.followUp) throw new Error(\"no followUp command configured for this chat session\");\n await this.commands.followUp(text ?? \"\");\n };\n case \"resume\":\n return async (text) => {\n if (!this.commands.resume) throw new Error(\"no resume command configured for this chat session\");\n await this.commands.resume(text);\n };\n }\n }\n\n private afterEvent(changed: boolean): void {\n this.syncAnimationTick();\n if (!changed) return;\n this.requestEventRender();\n }\n\n private requestEventRender(): void {\n if (!this.isStreaming()) {\n this.requestRender?.();\n return;\n }\n if (this.renderThrottleTimer) return;\n this.renderThrottleTimer = setTimeout(() => {\n this.renderThrottleTimer = undefined;\n this.requestRender?.();\n }, STREAMING_RENDER_THROTTLE_MS);\n this.renderThrottleTimer.unref?.();\n }\n}\n\nfunction setEditorPlaceholder(\n editor: EditorComponent,\n placeholder: string | undefined,\n): void {\n const candidate = editor as EditorComponent & {\n setPlaceholder?: (value: string | undefined) => void;\n };\n candidate.setPlaceholder?.(placeholder);\n}\n\nfunction setEditorBorderColor(\n editor: EditorComponent,\n borderColor: (text: string) => string,\n): void {\n const candidate = editor as EditorComponent & {\n borderColor?: (text: string) => string;\n };\n if (candidate.borderColor !== undefined) candidate.borderColor = borderColor;\n}\n\nfunction setEditorFocused(editor: EditorComponent, focused: boolean): void {\n const candidate = editor as EditorComponent & Partial<Focusable>;\n if (\"focused\" in candidate) candidate.focused = focused;\n}\n\nfunction matchesKey(\n data: string,\n key: \"enter\" | \"backspace\" | \"escape\" | \"ctrl+f\" | \"alt+up\",\n): boolean {\n if (key === \"enter\" && (data === \"\\r\" || data === \"\\n\")) return true;\n if (key === \"backspace\" && (data === \"\\x7f\" || data === \"\\b\")) return true;\n if (key === \"escape\" && data === \"\\x1b\") return true;\n if (key === \"ctrl+f\" && data === \"\\x06\") return true;\n return tuiMatchesKey(data, key);\n}\n\nfunction parseBashInput(text: string):\n | { command: string; excludeFromContext: boolean }\n | undefined {\n if (!text.startsWith(\"!\")) return undefined;\n const excludeFromContext = text.startsWith(\"!!\");\n const command = text.slice(excludeFromContext ? 2 : 1).trim();\n return { command, excludeFromContext };\n}\n\nfunction isSharedLiveChatEvent(type: string): boolean {\n return (\n type === \"message_start\" ||\n type === \"message_update\" ||\n type === \"message_end\" ||\n type === \"tool_execution_start\" ||\n type === \"tool_execution_update\" ||\n type === \"tool_execution_end\"\n );\n}\n\nfunction cacheKey(parts: readonly CacheKeyPart[]): string {\n return JSON.stringify(parts);\n}\n\nfunction isChatMessageEntry<TExtraEntry extends ChatTranscriptEntryLike>(\n entry: ChatSessionHostEntry<TExtraEntry>,\n): entry is ChatMessageEntry {\n if (!(\"role\" in entry) || !(\"kind\" in entry)) return false;\n const candidate = entry as { role?: unknown; kind?: unknown; message?: unknown; text?: unknown };\n switch (candidate.kind) {\n case \"assistant\":\n return candidate.role === \"assistant\" && candidate.message !== undefined;\n case \"tool\":\n return candidate.role === \"tool\" && \"toolName\" in candidate && \"toolCallId\" in candidate && \"args\" in candidate;\n case \"bashExecution\":\n return candidate.role === \"tool\" && candidate.message !== undefined;\n case \"user\":\n return candidate.role === \"user\" && typeof candidate.text === \"string\";\n case \"custom\":\n return candidate.role === \"custom\" && candidate.message !== undefined;\n case \"branchSummary\":\n case \"compactionSummary\":\n return candidate.role === \"summary\" && candidate.message !== undefined;\n case \"system\":\n return candidate.role === \"system\" && candidate.message !== undefined;\n default:\n return false;\n }\n}\n\nfunction isMessageLike(message: unknown): message is { role?: unknown; content?: unknown } {\n return message !== null && typeof message === \"object\" && \"role\" in message;\n}\n\nfunction isUserMessageLike(\n message: unknown,\n): message is { role: \"user\"; content?: unknown } {\n return isMessageLike(message) && message.role === \"user\";\n}\n\nfunction userMessageSignature(text: string): string {\n return text.trim();\n}\n\nfunction assistantToolCallEvent(event: AgentSessionEvent): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} | undefined {\n const assistantEvent = (event as {\n assistantMessageEvent?: {\n type?: unknown;\n contentIndex?: unknown;\n partial?: unknown;\n toolCall?: unknown;\n };\n }).assistantMessageEvent;\n const streamType = String(assistantEvent?.type ?? \"\");\n if (!streamType.startsWith(\"toolcall_\")) return undefined;\n const explicit = toolCallPayload(assistantEvent?.toolCall);\n if (explicit) return explicit;\n const contentIndex = typeof assistantEvent?.contentIndex === \"number\" ? assistantEvent.contentIndex : undefined;\n if (contentIndex === undefined) return undefined;\n const partial = assistantEvent?.partial;\n if (!isMessageLike(partial) || partial.role !== \"assistant\") return undefined;\n const content = partial.content;\n if (!Array.isArray(content)) return undefined;\n return toolCallPayload(content[contentIndex]);\n}\n\nfunction toolCallPayload(value: unknown): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} | undefined {\n if (value === null || typeof value !== \"object\") return undefined;\n const candidate = value as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };\n if (candidate.type !== \"toolCall\") return undefined;\n if (typeof candidate.id !== \"string\" || typeof candidate.name !== \"string\") return undefined;\n return {\n type: \"tool_execution_start\",\n toolCallId: candidate.id,\n toolName: candidate.name,\n args: candidate.arguments ?? {},\n };\n}\n\nfunction legacyToolStartEvent(event: AgentSessionEvent): {\n type: \"tool_execution_start\";\n toolCallId: string;\n toolName: string;\n args: unknown;\n} {\n const payload = event as { toolCallId?: unknown; name?: unknown; input?: unknown; args?: unknown };\n const toolName = typeof payload.name === \"string\" ? payload.name : \"tool\";\n const toolCallId =\n typeof payload.toolCallId === \"string\" ? payload.toolCallId : `live-${toolName}`;\n return {\n type: \"tool_execution_start\",\n toolCallId,\n toolName,\n args: payload.input ?? payload.args ?? {},\n };\n}\n\nfunction legacyToolResultEvent(event: AgentSessionEvent): {\n type: \"tool_execution_end\";\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError: boolean;\n} {\n const payload = event as {\n toolCallId?: unknown;\n name?: unknown;\n output?: unknown;\n isError?: unknown;\n };\n const toolName = typeof payload.name === \"string\" ? payload.name : \"tool\";\n const toolCallId =\n typeof payload.toolCallId === \"string\" ? payload.toolCallId : `live-${toolName}`;\n const output = payload.output;\n return {\n type: \"tool_execution_end\",\n toolCallId,\n toolName,\n result:\n output !== null && typeof output === \"object\" && \"content\" in output\n ? output\n : { content: typeof output === \"string\" ? [{ type: \"text\", text: output }] : [] },\n isError: payload.isError === true,\n };\n}\n\nfunction legacyThinkingEvent(event: AgentSessionEvent): {\n type: \"message_update\";\n assistantMessageEvent: { type: \"thinking_delta\"; delta: string };\n message: { role: \"assistant\"; content: [] };\n} {\n const delta = String(\n (event as { delta?: unknown }).delta ??\n (event as { text?: unknown }).text ??\n \"\",\n );\n return {\n type: \"message_update\",\n assistantMessageEvent: { type: \"thinking_delta\", delta },\n message: { role: \"assistant\", content: [] },\n };\n}\n\nfunction extractMessageText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (item == null) continue;\n if (typeof item === \"string\") {\n parts.push(item);\n continue;\n }\n const obj = item as { type?: unknown; text?: unknown };\n if (typeof obj.text === \"string\") parts.push(obj.text);\n else if (obj.type === \"text\" && typeof obj.text === \"string\") parts.push(obj.text);\n }\n return parts.join(\"\");\n}\n\nfunction tailStreamingText(text: string): string {\n if (\n text.length <= STREAMING_TEXT_TAIL_CHARS &&\n text.split(\"\\n\").length <= STREAMING_TEXT_TAIL_LINES\n ) {\n return text;\n }\n const byChars = text.slice(-STREAMING_TEXT_TAIL_CHARS);\n const lines = byChars.split(\"\\n\");\n const tail =\n lines.length > STREAMING_TEXT_TAIL_LINES\n ? lines.slice(-STREAMING_TEXT_TAIL_LINES).join(\"\\n\")\n : byChars;\n return `[earlier streaming output hidden while attached]\\n\\n${tail.trimStart()}`;\n}\n\nfunction spinnerFrame(): string {\n const idx = Math.floor(Date.now() / 80) % SPINNER_FRAMES.length;\n return SPINNER_FRAMES[idx]!;\n}\n\nfunction stripAnsi(s: string): string {\n return s.replace(/\\x1B\\[[0-?]*[ -/]*[@-~]/g, \"\");\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|
|
@@ -26,6 +26,9 @@ export class ChatSessionHost {
|
|
|
26
26
|
this.compactionQueuedMessages = [];
|
|
27
27
|
this.compacting = false;
|
|
28
28
|
this.bodyViewport = new ScrollableComponentViewport();
|
|
29
|
+
this.transcriptRenderSettingsKey = "";
|
|
30
|
+
this.renderIdentityIds = new WeakMap();
|
|
31
|
+
this.nextRenderIdentityId = 1;
|
|
29
32
|
this.optimisticUserSignatureCounts = new Map();
|
|
30
33
|
this.style = opts.style;
|
|
31
34
|
this.commands = opts.commands ?? {};
|
|
@@ -46,6 +49,7 @@ export class ChatSessionHost {
|
|
|
46
49
|
this.renderExtraEntry = opts.renderExtraEntry;
|
|
47
50
|
this.tui = opts.tui;
|
|
48
51
|
this.liveChat = new LiveChatEntriesController(this.transcript);
|
|
52
|
+
this.transcriptComponent = new ChatTranscriptComponent(this.transcript, (entry) => this.renderEntry(entry), (entry, index) => this.transcriptCacheKey(entry, index));
|
|
49
53
|
this.editor = this.createEditor(opts.tui, opts.keybindings, opts.editorTheme, opts.editorFactory);
|
|
50
54
|
this.syncAnimationTick();
|
|
51
55
|
}
|
|
@@ -189,11 +193,16 @@ export class ChatSessionHost {
|
|
|
189
193
|
render(width) {
|
|
190
194
|
return this.renderBody(width, 1);
|
|
191
195
|
}
|
|
192
|
-
invalidate() {
|
|
196
|
+
invalidate() {
|
|
197
|
+
this.transcriptComponent.invalidate();
|
|
198
|
+
this.bodyViewport.invalidate();
|
|
199
|
+
this.editor?.invalidate();
|
|
200
|
+
}
|
|
193
201
|
renderBody(width, budget) {
|
|
194
202
|
const components = [];
|
|
203
|
+
this.transcriptRenderSettingsKey = this.chatRenderSettingsCacheKey();
|
|
195
204
|
if (this.transcript.length > 0) {
|
|
196
|
-
components.push(
|
|
205
|
+
components.push(this.transcriptComponent);
|
|
197
206
|
}
|
|
198
207
|
if (this.statusMessage) {
|
|
199
208
|
components.push(new Spacer(1));
|
|
@@ -612,6 +621,75 @@ export class ChatSessionHost {
|
|
|
612
621
|
toolTui() {
|
|
613
622
|
return { requestRender: () => this.requestRender?.() };
|
|
614
623
|
}
|
|
624
|
+
chatRenderSettingsCacheKey() {
|
|
625
|
+
const inherited = this.getChatRenderSettings?.();
|
|
626
|
+
return cacheKey([
|
|
627
|
+
"settings",
|
|
628
|
+
inherited?.hideThinkingBlock === true,
|
|
629
|
+
inherited?.hiddenThinkingLabel ?? "",
|
|
630
|
+
inherited?.toolOutputExpanded === true,
|
|
631
|
+
inherited?.showImages !== false,
|
|
632
|
+
inherited?.imageWidthCells ?? null,
|
|
633
|
+
this.getCwd?.() ?? this.getAgentSession?.()?.sessionManager.getCwd() ?? process.cwd(),
|
|
634
|
+
this.bodyViewport.getScrollFromBottom() === 0,
|
|
635
|
+
this.isStreaming(),
|
|
636
|
+
]);
|
|
637
|
+
}
|
|
638
|
+
transcriptCacheKey(entry, index) {
|
|
639
|
+
return cacheKey([
|
|
640
|
+
this.transcriptRenderSettingsKey,
|
|
641
|
+
index,
|
|
642
|
+
entry.role,
|
|
643
|
+
this.renderIdentityKey(entry),
|
|
644
|
+
this.entryContentCacheKey(entry),
|
|
645
|
+
]);
|
|
646
|
+
}
|
|
647
|
+
entryContentCacheKey(entry) {
|
|
648
|
+
if (!isChatMessageEntry(entry))
|
|
649
|
+
return cacheKey(["extra"]);
|
|
650
|
+
switch (entry.kind) {
|
|
651
|
+
case "assistant":
|
|
652
|
+
return cacheKey(["assistant", this.renderIdentityKey(entry.message)]);
|
|
653
|
+
case "tool":
|
|
654
|
+
return cacheKey([
|
|
655
|
+
"tool",
|
|
656
|
+
entry.toolCallId,
|
|
657
|
+
entry.toolName,
|
|
658
|
+
entry.isPartial === true,
|
|
659
|
+
entry.result === undefined ? null : this.renderIdentityKey(entry.result),
|
|
660
|
+
]);
|
|
661
|
+
case "bashExecution":
|
|
662
|
+
return cacheKey([
|
|
663
|
+
"bashExecution",
|
|
664
|
+
entry.isPartial === true,
|
|
665
|
+
entry.message.command,
|
|
666
|
+
entry.message.output,
|
|
667
|
+
entry.message.exitCode ?? null,
|
|
668
|
+
entry.message.cancelled,
|
|
669
|
+
entry.message.truncated,
|
|
670
|
+
entry.message.fullOutputPath ?? null,
|
|
671
|
+
]);
|
|
672
|
+
case "user":
|
|
673
|
+
return cacheKey(["user", entry.text]);
|
|
674
|
+
case "custom":
|
|
675
|
+
return cacheKey(["custom", this.renderIdentityKey(entry.message)]);
|
|
676
|
+
case "branchSummary":
|
|
677
|
+
return cacheKey(["branchSummary", this.renderIdentityKey(entry.message)]);
|
|
678
|
+
case "compactionSummary":
|
|
679
|
+
return cacheKey(["compactionSummary", this.renderIdentityKey(entry.message)]);
|
|
680
|
+
case "system":
|
|
681
|
+
return cacheKey(["system", entry.text]);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
renderIdentityKey(value) {
|
|
685
|
+
const existing = this.renderIdentityIds.get(value);
|
|
686
|
+
if (existing !== undefined)
|
|
687
|
+
return String(existing);
|
|
688
|
+
const id = this.nextRenderIdentityId;
|
|
689
|
+
this.nextRenderIdentityId += 1;
|
|
690
|
+
this.renderIdentityIds.set(value, id);
|
|
691
|
+
return String(id);
|
|
692
|
+
}
|
|
615
693
|
pendingMessageLine(width, label, message) {
|
|
616
694
|
const text = `${label}: ${message}`;
|
|
617
695
|
return new Text(this.style.dim(truncateToWidth(text, Math.max(1, width - 2))), 1, 0).render(width);
|
|
@@ -858,6 +936,9 @@ function isSharedLiveChatEvent(type) {
|
|
|
858
936
|
type === "tool_execution_update" ||
|
|
859
937
|
type === "tool_execution_end");
|
|
860
938
|
}
|
|
939
|
+
function cacheKey(parts) {
|
|
940
|
+
return JSON.stringify(parts);
|
|
941
|
+
}
|
|
861
942
|
function isChatMessageEntry(entry) {
|
|
862
943
|
if (!("role" in entry) || !("kind" in entry))
|
|
863
944
|
return false;
|