@bastani/atomic 0.8.4 → 0.8.5-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 +6 -0
- package/README.md +24 -23
- package/dist/builtin/intercom/README.md +5 -5
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +19 -1
- package/dist/builtin/intercom/ui/session-list.ts +19 -1
- package/dist/builtin/mcp/README.md +3 -3
- package/dist/builtin/mcp/commands.ts +1 -1
- package/dist/builtin/mcp/host-html-template.ts +1 -1
- package/dist/builtin/mcp/mcp-panel.ts +14 -14
- package/dist/builtin/mcp/mcp-setup-panel.ts +4 -4
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/mcp/tool-result-renderer.ts +1 -1
- package/dist/builtin/subagents/README.md +3 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/tui/render.ts +1844 -1062
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/curator-page.ts +2 -2
- package/dist/builtin/web-access/index.ts +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/README.md +34 -7
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +23 -4
- package/dist/builtin/workflows/builtin/ralph.ts +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +75 -16
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +34 -11
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +111 -20
- package/dist/builtin/workflows/src/extension/discovery.ts +32 -4
- package/dist/builtin/workflows/src/extension/index.ts +347 -63
- package/dist/builtin/workflows/src/extension/render-call.ts +3 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +7 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +4 -2
- package/dist/builtin/workflows/src/extension/wiring.ts +32 -8
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +36 -14
- package/dist/builtin/workflows/src/runs/background/runner.ts +2 -2
- package/dist/builtin/workflows/src/runs/background/status.ts +89 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +338 -78
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +2 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +55 -7
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +146 -10
- package/dist/builtin/workflows/src/shared/store.ts +29 -0
- package/dist/builtin/workflows/src/shared/types.ts +25 -4
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +69 -2
- package/dist/builtin/workflows/src/tui/graph-view.ts +97 -182
- package/dist/builtin/workflows/src/tui/header.ts +36 -20
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +129 -46
- package/dist/builtin/workflows/src/tui/inline-form-editor.ts +111 -36
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +311 -91
- package/dist/builtin/workflows/src/tui/layout.ts +1 -1
- package/dist/builtin/workflows/src/tui/node-card.ts +66 -37
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +20 -6
- package/dist/builtin/workflows/src/tui/prompt-card.ts +262 -85
- package/dist/builtin/workflows/src/tui/run-detail.ts +50 -31
- package/dist/builtin/workflows/src/tui/session-confirm.ts +21 -14
- package/dist/builtin/workflows/src/tui/session-picker.ts +35 -26
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +531 -960
- package/dist/builtin/workflows/src/tui/status-helpers.ts +6 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +8 -4
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -2
- package/dist/builtin/workflows/src/tui/switcher.ts +55 -25
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +33 -1
- package/dist/builtin/workflows/src/tui/workflow-list.ts +10 -6
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -6
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -3
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +7 -7
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -2
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +3 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +24 -12
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +28 -17
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +65 -28
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +13 -5
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts +8 -8
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/dialog-builder.js +6 -6
- package/dist/core/tools/ask-user-question/view/dialog-builder.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +7 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +3 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +3 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +2 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +2 -1
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +1 -1
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts +4 -3
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +3 -3
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +2 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +13 -3
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +47 -5
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +5 -5
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +7 -7
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +8 -8
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +3 -3
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +10 -12
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +3 -3
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/working-status.d.ts +25 -0
- package/dist/modes/interactive/components/working-status.d.ts.map +1 -0
- package/dist/modes/interactive/components/working-status.js +28 -0
- package/dist/modes/interactive/components/working-status.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +8 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +8 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +5 -5
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/development.md +2 -2
- package/docs/extensions.md +7 -7
- package/docs/packages.md +11 -8
- package/docs/quickstart.md +2 -2
- package/docs/rpc.md +1 -1
- package/docs/sdk.md +14 -11
- package/docs/session-format.md +1 -1
- package/docs/sessions.md +10 -10
- package/docs/settings.md +1 -1
- package/docs/terminal-setup.md +9 -9
- package/docs/tmux.md +10 -10
- package/docs/tui.md +2 -2
- package/docs/usage.md +9 -9
- package/package.json +6 -1
|
@@ -19,13 +19,15 @@
|
|
|
19
19
|
* - **Running** stage with a live stream: Enter calls `handle.steer(text)`
|
|
20
20
|
* (interrupt mid-turn). Ctrl+F always queues a follow-up via
|
|
21
21
|
* `handle.followUp(text)`.
|
|
22
|
-
* - **
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* - **Escape** mirrors the main coding-agent chat interrupt path for active
|
|
23
|
+
* live stages: it requests a controlled pause/abort while keeping the
|
|
24
|
+
* composer active. While paused, Enter calls `handle.resume(text)`.
|
|
25
|
+
* - **Ctrl+D** detaches (back to graph); **Escape** closes the popup when idle.
|
|
25
26
|
* - **Blocked** stage: keystrokes absorbed; BLOCKED banner names the
|
|
26
27
|
* upstream awaiter.
|
|
27
|
-
* - **Settled** stage
|
|
28
|
-
*
|
|
28
|
+
* - **Settled** stage with a live handle remains a normal chat session:
|
|
29
|
+
* Enter sends `handle.prompt(text)` and Escape interrupts any active
|
|
30
|
+
* post-stage response without mutating workflow dependencies.
|
|
29
31
|
*
|
|
30
32
|
* cross-ref:
|
|
31
33
|
* - ui/stage-chat-mockup.html (canonical visual)
|
|
@@ -39,25 +41,34 @@
|
|
|
39
41
|
import {
|
|
40
42
|
ChatTranscriptComponent,
|
|
41
43
|
CustomEditor,
|
|
44
|
+
FooterComponent,
|
|
42
45
|
ScrollableComponentViewport,
|
|
43
46
|
SessionManager,
|
|
44
47
|
LiveChatEntriesController,
|
|
45
|
-
|
|
48
|
+
UsageMeterComponent,
|
|
49
|
+
WorkingStatusComponent,
|
|
50
|
+
pickWhimsicalWorkingMessage,
|
|
46
51
|
renderChatMessageEntry,
|
|
47
52
|
type AgentSession,
|
|
48
53
|
type AgentSessionEvent,
|
|
49
54
|
type ChatMessageEntry,
|
|
50
55
|
type ChatMessageRenderOptions,
|
|
51
|
-
type
|
|
56
|
+
type ReadonlyFooterDataProvider,
|
|
52
57
|
} from "@bastani/atomic";
|
|
53
|
-
import { Box,
|
|
54
|
-
import type {
|
|
58
|
+
import { Box, Spacer, Text } from "@earendil-works/pi-tui";
|
|
59
|
+
import type {
|
|
60
|
+
Component,
|
|
61
|
+
EditorComponent,
|
|
62
|
+
EditorTheme,
|
|
63
|
+
Focusable,
|
|
64
|
+
TUI,
|
|
65
|
+
} from "@earendil-works/pi-tui";
|
|
55
66
|
import type { Store } from "../shared/store.js";
|
|
56
67
|
import type { StageNotice, StageSnapshot } from "../shared/store-types.js";
|
|
57
68
|
import type { GraphTheme } from "./graph-theme.js";
|
|
58
69
|
import type { StageControlHandle } from "../runs/foreground/stage-control-registry.js";
|
|
59
70
|
import { BOLD, RESET, hexBg, hexToAnsi, lerpColor } from "./color-utils.js";
|
|
60
|
-
import { truncateToWidth, visibleWidth } from "./text-helpers.js";
|
|
71
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
|
|
61
72
|
|
|
62
73
|
// ---------------------------------------------------------------------------
|
|
63
74
|
// Options & types
|
|
@@ -85,13 +96,21 @@ export interface StageChatViewOpts {
|
|
|
85
96
|
piTui?: TUI;
|
|
86
97
|
piKeybindings?: unknown;
|
|
87
98
|
/** Currently installed host editor factory, inherited from extension `ctx.ui.setEditorComponent()`. */
|
|
88
|
-
piEditorFactory?: (
|
|
99
|
+
piEditorFactory?: (
|
|
100
|
+
tui: TUI,
|
|
101
|
+
theme: EditorTheme,
|
|
102
|
+
keybindings: unknown,
|
|
103
|
+
) => EditorComponent;
|
|
89
104
|
/** Parent chat rendering settings and extension renderers inherited from the host UI. */
|
|
90
|
-
getChatRenderSettings?: () =>
|
|
105
|
+
getChatRenderSettings?: () =>
|
|
106
|
+
| Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd" | "markdownTheme">>
|
|
107
|
+
| undefined;
|
|
108
|
+
/** Parent footer data provider inherited from the host UI for core footer/usage rendering. */
|
|
109
|
+
footerData?: ReadonlyFooterDataProvider;
|
|
91
110
|
/**
|
|
92
111
|
* Optional accessor returning the current terminal row count. The chat
|
|
93
112
|
* surface expands its body band to roughly `viewportRows` minus the fixed
|
|
94
|
-
* header / loader / editor / footer
|
|
113
|
+
* header / loader / editor / footer rows so the popup fills the
|
|
95
114
|
* terminal under pi-tui's `width: "100%" / maxHeight: "100%"` geometry.
|
|
96
115
|
* Returning `undefined` falls back to the constant 32-row frame.
|
|
97
116
|
*/
|
|
@@ -103,46 +122,16 @@ export interface StageChatViewOpts {
|
|
|
103
122
|
* that read `_transcript` (tests, future serialisers) can recover the
|
|
104
123
|
* canonical user-visible string without knowing about the Pi-box payload.
|
|
105
124
|
*/
|
|
106
|
-
interface
|
|
107
|
-
readonly role: ChatTranscriptRole;
|
|
108
|
-
readonly text: string;
|
|
109
|
-
}
|
|
110
|
-
interface UserEntry extends BaseEntry {
|
|
111
|
-
readonly role: "user";
|
|
112
|
-
}
|
|
113
|
-
interface AssistantEntry extends BaseEntry {
|
|
114
|
-
readonly role: "assistant";
|
|
115
|
-
}
|
|
116
|
-
interface ThinkingEntry extends BaseEntry {
|
|
117
|
-
readonly role: "thinking";
|
|
118
|
-
}
|
|
119
|
-
interface SystemEntry extends BaseEntry {
|
|
120
|
-
readonly role: "system";
|
|
121
|
-
}
|
|
122
|
-
interface ToolEntry extends BaseEntry {
|
|
123
|
-
readonly role: "tool";
|
|
124
|
-
readonly name: string;
|
|
125
|
-
readonly toolCallId?: string;
|
|
126
|
-
readonly args?: string;
|
|
127
|
-
readonly output?: string;
|
|
128
|
-
readonly state: "pending" | "success" | "error";
|
|
129
|
-
}
|
|
130
|
-
interface NoticeEntry extends BaseEntry {
|
|
125
|
+
interface NoticeEntry {
|
|
131
126
|
readonly role: "notice";
|
|
127
|
+
readonly text: string;
|
|
132
128
|
readonly noticeId: string;
|
|
133
129
|
readonly kind: StageNotice["kind"];
|
|
134
130
|
readonly value: string;
|
|
135
131
|
readonly from?: string;
|
|
136
132
|
readonly meta?: string;
|
|
137
133
|
}
|
|
138
|
-
type TranscriptEntry =
|
|
139
|
-
| UserEntry
|
|
140
|
-
| AssistantEntry
|
|
141
|
-
| ThinkingEntry
|
|
142
|
-
| SystemEntry
|
|
143
|
-
| ToolEntry
|
|
144
|
-
| NoticeEntry
|
|
145
|
-
| ChatMessageEntry;
|
|
134
|
+
type TranscriptEntry = NoticeEntry | ChatMessageEntry;
|
|
146
135
|
type AgentSnapshotMessage = AgentSession["messages"][number];
|
|
147
136
|
|
|
148
137
|
// ---------------------------------------------------------------------------
|
|
@@ -160,15 +149,13 @@ const VIEW_LINE_COUNT = 32;
|
|
|
160
149
|
const HEADER_ROWS = 1;
|
|
161
150
|
/** Single dim rule between header and body. */
|
|
162
151
|
const SEP_ROWS = 1;
|
|
163
|
-
/** Footer: two dim lines. */
|
|
164
|
-
const FOOTER_ROWS = 2;
|
|
165
|
-
/** Hint strip: dashed rule + key bindings line. */
|
|
166
|
-
const HINTS_ROWS = 2;
|
|
167
|
-
|
|
168
152
|
/** Spinner glyphs — Braille spinner at 80ms per frame. */
|
|
169
153
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
170
154
|
/** Pi's Loader advances at 80ms; use the same cadence for embedded stage chats. */
|
|
171
155
|
const ANIMATION_FRAME_MS = 80;
|
|
156
|
+
const STREAMING_RENDER_THROTTLE_MS = 80;
|
|
157
|
+
const STREAMING_TEXT_TAIL_LINES = 240;
|
|
158
|
+
const STREAMING_TEXT_TAIL_CHARS = 16_000;
|
|
172
159
|
|
|
173
160
|
const ITALIC = "\x1b[3m";
|
|
174
161
|
const FG_RESET = "\x1b[39m";
|
|
@@ -179,7 +166,8 @@ const ITALIC_RESET = "\x1b[23m";
|
|
|
179
166
|
// StageChatView
|
|
180
167
|
// ---------------------------------------------------------------------------
|
|
181
168
|
|
|
182
|
-
export class StageChatView implements Component {
|
|
169
|
+
export class StageChatView implements Component, Focusable {
|
|
170
|
+
focused = true;
|
|
183
171
|
private store: Store;
|
|
184
172
|
private theme: GraphTheme;
|
|
185
173
|
private runId: string;
|
|
@@ -191,7 +179,10 @@ export class StageChatView implements Component {
|
|
|
191
179
|
private requestRender: (() => void) | undefined;
|
|
192
180
|
private getViewportRows?: () => number | undefined;
|
|
193
181
|
private editor: EditorComponent | undefined;
|
|
194
|
-
private getChatRenderSettings?: () =>
|
|
182
|
+
private getChatRenderSettings?: () =>
|
|
183
|
+
| Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd" | "markdownTheme">>
|
|
184
|
+
| undefined;
|
|
185
|
+
private footerData?: ReadonlyFooterDataProvider;
|
|
195
186
|
|
|
196
187
|
private inputBuffer = "";
|
|
197
188
|
private transcript: TranscriptEntry[] = [];
|
|
@@ -204,15 +195,14 @@ export class StageChatView implements Component {
|
|
|
204
195
|
private attachedAt = Date.now();
|
|
205
196
|
/** True after SDK `agent_start` until `agent_end`; mirrors Pi's working-loader lifecycle. */
|
|
206
197
|
private sdkBusy = false;
|
|
207
|
-
/**
|
|
208
|
-
private
|
|
209
|
-
private streamingThinkingIndex: number | undefined;
|
|
210
|
-
/** Stable tool-call rows keyed exactly like Pi's `pendingTools` map. */
|
|
211
|
-
private toolEntryIndexes = new Map<string, number>();
|
|
198
|
+
/** Pi-style per-turn working message, populated from coding-agent's message picker. */
|
|
199
|
+
private workingMessage: string | undefined;
|
|
212
200
|
/** User rows optimistically appended by this embedded editor, de-duped on SDK echo. */
|
|
213
201
|
private optimisticUserSignatures = new Set<string>();
|
|
214
202
|
/** Chat-mode repaint driver for Pi-style loaders/spinners. */
|
|
215
203
|
private animationTimer: ReturnType<typeof setInterval> | undefined;
|
|
204
|
+
/** Coalesces high-frequency SDK deltas while the fixed overlay is streaming. */
|
|
205
|
+
private renderThrottleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
216
206
|
/** Scrollable fixed-height body viewport for attached chat history. */
|
|
217
207
|
private bodyViewport = new ScrollableComponentViewport();
|
|
218
208
|
private liveChat: LiveChatEntriesController;
|
|
@@ -232,8 +222,13 @@ export class StageChatView implements Component {
|
|
|
232
222
|
this.requestRender = opts.requestRender;
|
|
233
223
|
this.getViewportRows = opts.getViewportRows;
|
|
234
224
|
this.getChatRenderSettings = opts.getChatRenderSettings;
|
|
225
|
+
this.footerData = opts.footerData;
|
|
235
226
|
this.liveChat = new LiveChatEntriesController(this.transcript);
|
|
236
|
-
this.editor = this._createEditor(
|
|
227
|
+
this.editor = this._createEditor(
|
|
228
|
+
opts.piTui,
|
|
229
|
+
opts.piKeybindings,
|
|
230
|
+
opts.piEditorFactory,
|
|
231
|
+
);
|
|
237
232
|
|
|
238
233
|
// Seed transcript from the live SDK session at attach time, plus any
|
|
239
234
|
// stage notices the workflow body has already recorded.
|
|
@@ -264,7 +259,7 @@ export class StageChatView implements Component {
|
|
|
264
259
|
this._unsubscribeHandle = this.handle.subscribe((event) => {
|
|
265
260
|
const changed = this._appendEvent(event);
|
|
266
261
|
this._syncAnimationTick();
|
|
267
|
-
if (changed) this.
|
|
262
|
+
if (changed) this._requestEventRender();
|
|
268
263
|
});
|
|
269
264
|
}
|
|
270
265
|
this._syncAnimationTick();
|
|
@@ -273,16 +268,28 @@ export class StageChatView implements Component {
|
|
|
273
268
|
private _createEditor(
|
|
274
269
|
tui: TUI | undefined,
|
|
275
270
|
keybindings: unknown,
|
|
276
|
-
editorFactory:
|
|
271
|
+
editorFactory:
|
|
272
|
+
| ((
|
|
273
|
+
tui: TUI,
|
|
274
|
+
theme: EditorTheme,
|
|
275
|
+
keybindings: unknown,
|
|
276
|
+
) => EditorComponent)
|
|
277
|
+
| undefined,
|
|
277
278
|
): EditorComponent | undefined {
|
|
278
279
|
if (!tui || !keybindings) return undefined;
|
|
279
280
|
const editorTheme = editorThemeFromGraphTheme(this.theme);
|
|
280
|
-
const editor =
|
|
281
|
+
const editor =
|
|
282
|
+
this._createInheritedEditor(
|
|
283
|
+
tui,
|
|
284
|
+
editorTheme,
|
|
285
|
+
keybindings,
|
|
286
|
+
editorFactory,
|
|
287
|
+
) ??
|
|
281
288
|
new CustomEditor(
|
|
282
289
|
tui,
|
|
283
290
|
editorTheme,
|
|
284
291
|
keybindings as ConstructorParameters<typeof CustomEditor>[2],
|
|
285
|
-
{ paddingX:
|
|
292
|
+
{ paddingX: 0, autocompleteMaxVisible: 5 },
|
|
286
293
|
);
|
|
287
294
|
editor.onChange = (text) => {
|
|
288
295
|
this.inputBuffer = text;
|
|
@@ -297,7 +304,13 @@ export class StageChatView implements Component {
|
|
|
297
304
|
tui: TUI,
|
|
298
305
|
editorTheme: EditorTheme,
|
|
299
306
|
keybindings: unknown,
|
|
300
|
-
editorFactory:
|
|
307
|
+
editorFactory:
|
|
308
|
+
| ((
|
|
309
|
+
tui: TUI,
|
|
310
|
+
theme: EditorTheme,
|
|
311
|
+
keybindings: unknown,
|
|
312
|
+
) => EditorComponent)
|
|
313
|
+
| undefined,
|
|
301
314
|
): EditorComponent | undefined {
|
|
302
315
|
if (!editorFactory) return undefined;
|
|
303
316
|
try {
|
|
@@ -316,14 +329,17 @@ export class StageChatView implements Component {
|
|
|
316
329
|
this.liveChat.appendMessages(this.handle.messages);
|
|
317
330
|
}
|
|
318
331
|
|
|
319
|
-
private _snapshotMessagesFromSessionFile(
|
|
332
|
+
private _snapshotMessagesFromSessionFile(
|
|
333
|
+
stage: StageSnapshot | undefined,
|
|
334
|
+
): void {
|
|
320
335
|
if (this.transcript.length > 0) return;
|
|
321
336
|
const sessionFile = this.handle?.sessionFile ?? stage?.sessionFile;
|
|
322
337
|
if (sessionFile === undefined) return;
|
|
323
338
|
|
|
324
339
|
let messages: readonly AgentSnapshotMessage[];
|
|
325
340
|
try {
|
|
326
|
-
messages = SessionManager.open(sessionFile).buildSessionContext()
|
|
341
|
+
messages = SessionManager.open(sessionFile).buildSessionContext()
|
|
342
|
+
.messages as readonly AgentSnapshotMessage[];
|
|
327
343
|
} catch {
|
|
328
344
|
return;
|
|
329
345
|
}
|
|
@@ -339,103 +355,55 @@ export class StageChatView implements Component {
|
|
|
339
355
|
if (type === "message_start") {
|
|
340
356
|
const message = (event as { message?: unknown }).message;
|
|
341
357
|
if (isUserMessageLike(message)) {
|
|
342
|
-
const signature = userMessageSignature(
|
|
358
|
+
const signature = userMessageSignature(
|
|
359
|
+
extractMessageText(message.content),
|
|
360
|
+
);
|
|
343
361
|
if (this.optimisticUserSignatures.delete(signature)) return false;
|
|
344
362
|
}
|
|
345
363
|
}
|
|
346
364
|
if (isSharedLiveChatEvent(type)) {
|
|
347
|
-
|
|
365
|
+
const changed = this.liveChat.applyEvent(event);
|
|
366
|
+
const toolCallEvent = assistantToolCallEvent(event);
|
|
367
|
+
const changedByToolCall = toolCallEvent !== undefined
|
|
368
|
+
? this.liveChat.applyEvent(toolCallEvent)
|
|
369
|
+
: false;
|
|
370
|
+
return changed || changedByToolCall;
|
|
348
371
|
}
|
|
349
372
|
switch (type) {
|
|
350
373
|
case "agent_start":
|
|
351
374
|
this.sdkBusy = true;
|
|
352
|
-
this.toolEntryIndexes.clear();
|
|
353
375
|
this.liveChat.clearPendingTools();
|
|
354
376
|
this.statusMessage = "";
|
|
355
377
|
return true;
|
|
356
378
|
|
|
357
379
|
case "agent_end":
|
|
358
380
|
this.sdkBusy = false;
|
|
359
|
-
this.
|
|
360
|
-
this.streamingThinkingIndex = undefined;
|
|
381
|
+
this.workingMessage = undefined;
|
|
361
382
|
this.liveChat.clearPendingTools();
|
|
362
383
|
this.statusMessage = "";
|
|
363
384
|
return true;
|
|
364
385
|
|
|
365
|
-
case "
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
case "message_update":
|
|
369
|
-
return this._handleMessageUpdate(event);
|
|
370
|
-
|
|
371
|
-
case "message_end":
|
|
372
|
-
return this._handleMessageEnd((event as { message?: unknown }).message);
|
|
373
|
-
|
|
374
|
-
case "tool_execution_start": {
|
|
375
|
-
const payload = event as { toolCallId?: unknown; toolName?: unknown; args?: unknown };
|
|
376
|
-
const name = typeof payload.toolName === "string" ? payload.toolName : "tool";
|
|
377
|
-
const toolCallId = typeof payload.toolCallId === "string" ? payload.toolCallId : undefined;
|
|
378
|
-
const args = summariseArgs(payload.args);
|
|
379
|
-
this._upsertToolEntry({ toolCallId, name, args, state: "pending" });
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
case "tool_execution_update": {
|
|
384
|
-
const payload = event as { toolCallId?: unknown; toolName?: unknown; partialResult?: unknown };
|
|
385
|
-
const partialOutput = extractToolResultText(payload.partialResult);
|
|
386
|
-
if (!partialOutput) return false;
|
|
387
|
-
this._upsertToolEntry({
|
|
388
|
-
toolCallId: typeof payload.toolCallId === "string" ? payload.toolCallId : undefined,
|
|
389
|
-
name: typeof payload.toolName === "string" ? payload.toolName : "tool",
|
|
390
|
-
output: partialOutput,
|
|
391
|
-
state: "pending",
|
|
392
|
-
});
|
|
386
|
+
case "turn_start":
|
|
387
|
+
this.workingMessage = pickWhimsicalWorkingMessage();
|
|
393
388
|
return true;
|
|
394
|
-
}
|
|
395
389
|
|
|
396
|
-
case "
|
|
397
|
-
|
|
398
|
-
const toolCallId = typeof payload.toolCallId === "string" ? payload.toolCallId : undefined;
|
|
399
|
-
const output = extractToolResultText(payload.result);
|
|
400
|
-
this._upsertToolEntry({
|
|
401
|
-
toolCallId,
|
|
402
|
-
name: typeof payload.toolName === "string" ? payload.toolName : "tool",
|
|
403
|
-
output,
|
|
404
|
-
state: payload.isError === true ? "error" : "success",
|
|
405
|
-
});
|
|
406
|
-
if (toolCallId) this.toolEntryIndexes.delete(toolCallId);
|
|
390
|
+
case "turn_end":
|
|
391
|
+
this.workingMessage = undefined;
|
|
407
392
|
return true;
|
|
408
|
-
}
|
|
409
393
|
|
|
394
|
+
// Compatibility with older/headless shims that predate the SDK's
|
|
395
|
+
// tool_execution_* events. Project these shims into coding-agent's live
|
|
396
|
+
// controller rather than maintaining a second workflow tool renderer.
|
|
410
397
|
case "tool_call":
|
|
411
|
-
case "tool_use":
|
|
412
|
-
|
|
413
|
-
const args = summariseArgs((event as { input?: unknown }).input);
|
|
414
|
-
this._upsertToolEntry({ name, args, state: "pending" });
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
398
|
+
case "tool_use":
|
|
399
|
+
return this.liveChat.applyEvent(legacyToolStartEvent(event));
|
|
417
400
|
|
|
418
|
-
case "tool_result":
|
|
419
|
-
|
|
420
|
-
const rawOutput = (event as { output?: unknown }).output;
|
|
421
|
-
const output = typeof rawOutput === "string" ? rawOutput : extractMessageText(rawOutput);
|
|
422
|
-
this._upsertToolEntry({
|
|
423
|
-
name,
|
|
424
|
-
output,
|
|
425
|
-
state: Boolean((event as { isError?: unknown }).isError) ? "error" : "success",
|
|
426
|
-
});
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
401
|
+
case "tool_result":
|
|
402
|
+
return this.liveChat.applyEvent(legacyToolResultEvent(event));
|
|
429
403
|
|
|
430
404
|
case "thinking_delta":
|
|
431
|
-
case "thinking":
|
|
432
|
-
|
|
433
|
-
(event as { delta?: unknown }).delta ?? (event as { text?: unknown }).text ?? "",
|
|
434
|
-
);
|
|
435
|
-
if (!delta) return false;
|
|
436
|
-
this._appendTextDelta("thinking", delta);
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
405
|
+
case "thinking":
|
|
406
|
+
return this.liveChat.applyEvent(legacyThinkingEvent(event));
|
|
439
407
|
|
|
440
408
|
case "compaction_start":
|
|
441
409
|
this.sdkBusy = true;
|
|
@@ -461,115 +429,6 @@ export class StageChatView implements Component {
|
|
|
461
429
|
}
|
|
462
430
|
}
|
|
463
431
|
|
|
464
|
-
private _handleMessageStart(message: unknown): boolean {
|
|
465
|
-
if (!isMessageLike(message)) return false;
|
|
466
|
-
if (message.role === "assistant") {
|
|
467
|
-
this.streamingAssistantIndex = undefined;
|
|
468
|
-
this.streamingThinkingIndex = undefined;
|
|
469
|
-
return this._updateAssistantFromMessage(message);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const entry = transcriptEntryFromSnapshotMessage(message as AgentSnapshotMessage);
|
|
473
|
-
if (!entry) return false;
|
|
474
|
-
if (entry.role === "user") {
|
|
475
|
-
const signature = userMessageSignature(entry.text);
|
|
476
|
-
if (this.optimisticUserSignatures.delete(signature)) return false;
|
|
477
|
-
}
|
|
478
|
-
this.transcript.push(entry);
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private _handleMessageUpdate(event: AgentSessionEvent): boolean {
|
|
483
|
-
const message = (event as { message?: unknown }).message;
|
|
484
|
-
const hasAssistantSnapshot = isMessageLike(message) && message.role === "assistant";
|
|
485
|
-
const snapshotHasPayload = hasAssistantSnapshot && assistantContentHasRenderablePayload(message.content);
|
|
486
|
-
let changed = false;
|
|
487
|
-
if (hasAssistantSnapshot) {
|
|
488
|
-
changed = this._updateAssistantFromMessage(message) || changed;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const assistantEvent = (event as { assistantMessageEvent?: { type?: unknown; delta?: unknown } }).assistantMessageEvent;
|
|
492
|
-
const streamType = String(assistantEvent?.type ?? "");
|
|
493
|
-
const delta = typeof assistantEvent?.delta === "string" ? assistantEvent.delta : "";
|
|
494
|
-
// Prefer Pi's full assistant message snapshot when it contains visible
|
|
495
|
-
// payload; use deltas only for delta-only SDK shims/events.
|
|
496
|
-
if (!changed && !snapshotHasPayload && streamType === "text_delta" && delta) {
|
|
497
|
-
this._appendTextDelta("assistant", delta);
|
|
498
|
-
changed = true;
|
|
499
|
-
} else if (!changed && !snapshotHasPayload && streamType === "thinking_delta" && delta) {
|
|
500
|
-
this._appendTextDelta("thinking", delta);
|
|
501
|
-
changed = true;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return changed;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
private _handleMessageEnd(message: unknown): boolean {
|
|
508
|
-
let changed = false;
|
|
509
|
-
if (isMessageLike(message) && message.role === "assistant") {
|
|
510
|
-
changed = this._updateAssistantFromMessage(message) || changed;
|
|
511
|
-
for (const [toolCallId, index] of this.toolEntryIndexes.entries()) {
|
|
512
|
-
const entry = this.transcript[index];
|
|
513
|
-
if (isLocalToolEntry(entry) && entry.state === "pending") {
|
|
514
|
-
this.transcript[index] = { ...entry, text: entry.text };
|
|
515
|
-
}
|
|
516
|
-
this.toolEntryIndexes.set(toolCallId, index);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
this.streamingAssistantIndex = undefined;
|
|
520
|
-
this.streamingThinkingIndex = undefined;
|
|
521
|
-
return changed || isMessageLike(message);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
private _updateAssistantFromMessage(message: { role?: unknown; content?: unknown; stopReason?: unknown; errorMessage?: unknown }): boolean {
|
|
525
|
-
const projection = projectAssistantContent(message.content);
|
|
526
|
-
let changed = false;
|
|
527
|
-
if (projection.thinking) {
|
|
528
|
-
changed = this._upsertStreamingText("thinking", projection.thinking) || changed;
|
|
529
|
-
}
|
|
530
|
-
if (projection.text) {
|
|
531
|
-
changed = this._upsertStreamingText("assistant", projection.text) || changed;
|
|
532
|
-
}
|
|
533
|
-
const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
|
|
534
|
-
if (stopReason === "aborted" || stopReason === "error") {
|
|
535
|
-
const errorText = typeof message.errorMessage === "string" && message.errorMessage
|
|
536
|
-
? message.errorMessage
|
|
537
|
-
: stopReason === "aborted"
|
|
538
|
-
? "Operation aborted"
|
|
539
|
-
: "Unknown error";
|
|
540
|
-
changed = this._failPendingToolEntries(errorText) || changed;
|
|
541
|
-
if (!projection.toolCalls.length) {
|
|
542
|
-
changed = this._upsertStreamingText("system", stopReason === "error" ? `Error: ${errorText}` : errorText) || changed;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
for (const toolCall of projection.toolCalls) {
|
|
546
|
-
changed = this._upsertToolEntry(toolCall) || changed;
|
|
547
|
-
}
|
|
548
|
-
return changed;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
private _upsertStreamingText(
|
|
552
|
-
role: "assistant" | "thinking" | "system",
|
|
553
|
-
text: string,
|
|
554
|
-
): boolean {
|
|
555
|
-
if (!text) return false;
|
|
556
|
-
if (role === "system") {
|
|
557
|
-
this._upsertTextLastByRole("system", text);
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
const index = role === "assistant" ? this.streamingAssistantIndex : this.streamingThinkingIndex;
|
|
561
|
-
if (index !== undefined && isLocalTextRoleEntry(this.transcript[index], role)) {
|
|
562
|
-
if (this.transcript[index]?.text === text) return false;
|
|
563
|
-
this.transcript[index] = { role, text } as TranscriptEntry;
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
this.transcript.push({ role, text } as TranscriptEntry);
|
|
567
|
-
const nextIndex = this.transcript.length - 1;
|
|
568
|
-
if (role === "assistant") this.streamingAssistantIndex = nextIndex;
|
|
569
|
-
else this.streamingThinkingIndex = nextIndex;
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
432
|
private _absorbStageNotices(stage: StageSnapshot | undefined): boolean {
|
|
574
433
|
const notices = stage?.notices;
|
|
575
434
|
if (!notices) return false;
|
|
@@ -591,89 +450,6 @@ export class StageChatView implements Component {
|
|
|
591
450
|
return changed;
|
|
592
451
|
}
|
|
593
452
|
|
|
594
|
-
private _upsertTextLastByRole(
|
|
595
|
-
role: "user" | "assistant" | "thinking" | "system",
|
|
596
|
-
text: string,
|
|
597
|
-
): void {
|
|
598
|
-
const last = this.transcript[this.transcript.length - 1];
|
|
599
|
-
if (isLocalTextRoleEntry(last, role)) {
|
|
600
|
-
this.transcript[this.transcript.length - 1] = { role, text } as TranscriptEntry;
|
|
601
|
-
} else {
|
|
602
|
-
this.transcript.push({ role, text } as TranscriptEntry);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
private _appendTextDelta(
|
|
607
|
-
role: "assistant" | "thinking",
|
|
608
|
-
delta: string,
|
|
609
|
-
): void {
|
|
610
|
-
const index = role === "assistant" ? this.streamingAssistantIndex : this.streamingThinkingIndex;
|
|
611
|
-
if (index !== undefined && isLocalTextRoleEntry(this.transcript[index], role)) {
|
|
612
|
-
const current = this.transcript[index];
|
|
613
|
-
this.transcript[index] = { role, text: current.text + delta } as TranscriptEntry;
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
this.transcript.push({ role, text: delta } as TranscriptEntry);
|
|
617
|
-
const nextIndex = this.transcript.length - 1;
|
|
618
|
-
if (role === "assistant") this.streamingAssistantIndex = nextIndex;
|
|
619
|
-
else this.streamingThinkingIndex = nextIndex;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private _failPendingToolEntries(errorText: string): boolean {
|
|
623
|
-
let changed = false;
|
|
624
|
-
for (const [toolCallId, index] of this.toolEntryIndexes.entries()) {
|
|
625
|
-
const entry = this.transcript[index];
|
|
626
|
-
if (!isLocalToolEntry(entry) || entry.state !== "pending") continue;
|
|
627
|
-
changed = this._upsertToolEntry({
|
|
628
|
-
toolCallId,
|
|
629
|
-
name: entry.name,
|
|
630
|
-
output: errorText,
|
|
631
|
-
state: "error",
|
|
632
|
-
}) || changed;
|
|
633
|
-
}
|
|
634
|
-
this.toolEntryIndexes.clear();
|
|
635
|
-
return changed;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
private _upsertToolEntry(update: {
|
|
639
|
-
toolCallId?: string;
|
|
640
|
-
name: string;
|
|
641
|
-
args?: string;
|
|
642
|
-
output?: string;
|
|
643
|
-
state: "pending" | "success" | "error";
|
|
644
|
-
}): boolean {
|
|
645
|
-
const mappedIndex = update.toolCallId ? this.toolEntryIndexes.get(update.toolCallId) : undefined;
|
|
646
|
-
const index = mappedIndex ?? findToolEntryIndex(this.transcript, update.toolCallId, update.name);
|
|
647
|
-
const existing = index !== undefined && index >= 0 ? this.transcript[index] : undefined;
|
|
648
|
-
const previous = isLocalToolEntry(existing) ? existing : undefined;
|
|
649
|
-
const output = update.output || previous?.output;
|
|
650
|
-
const name = previous?.name ?? update.name;
|
|
651
|
-
const args = update.args ?? previous?.args;
|
|
652
|
-
const summary = output ? truncateToWidth(output.replace(/\s+/g, " "), 80) : "";
|
|
653
|
-
const next: ToolEntry = {
|
|
654
|
-
role: "tool",
|
|
655
|
-
name,
|
|
656
|
-
toolCallId: previous?.toolCallId ?? update.toolCallId,
|
|
657
|
-
args,
|
|
658
|
-
output,
|
|
659
|
-
state: update.state,
|
|
660
|
-
text: summary
|
|
661
|
-
? `← ${name} ${summary}`
|
|
662
|
-
: args
|
|
663
|
-
? `→ ${name} ${args}`
|
|
664
|
-
: `→ ${name}`,
|
|
665
|
-
};
|
|
666
|
-
if (previous && shallowToolEntryEqual(previous, next)) return false;
|
|
667
|
-
if (index !== undefined && index >= 0) {
|
|
668
|
-
this.transcript[index] = next;
|
|
669
|
-
if (next.toolCallId) this.toolEntryIndexes.set(next.toolCallId, index);
|
|
670
|
-
} else {
|
|
671
|
-
this.transcript.push(next);
|
|
672
|
-
if (next.toolCallId) this.toolEntryIndexes.set(next.toolCallId, this.transcript.length - 1);
|
|
673
|
-
}
|
|
674
|
-
return true;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
453
|
private _currentStage(): StageSnapshot | undefined {
|
|
678
454
|
const snap = this.store.snapshot();
|
|
679
455
|
const run = snap.runs.find((r) => r.id === this.runId);
|
|
@@ -703,12 +479,12 @@ export class StageChatView implements Component {
|
|
|
703
479
|
}
|
|
704
480
|
|
|
705
481
|
private _hasPendingToolEntries(): boolean {
|
|
706
|
-
return this.liveChat.pendingToolIds().length > 0
|
|
707
|
-
this.transcript.some((entry) => isLocalToolEntry(entry) && entry.state === "pending");
|
|
482
|
+
return this.liveChat.pendingToolIds().length > 0;
|
|
708
483
|
}
|
|
709
484
|
|
|
710
485
|
private _syncAnimationTick(): void {
|
|
711
|
-
const shouldAnimate =
|
|
486
|
+
const shouldAnimate =
|
|
487
|
+
this._isStreaming() || (this.sdkBusy && this._hasPendingToolEntries());
|
|
712
488
|
if (shouldAnimate && !this.animationTimer) {
|
|
713
489
|
this.animationTimer = setInterval(() => {
|
|
714
490
|
this.requestRender?.();
|
|
@@ -722,66 +498,68 @@ export class StageChatView implements Component {
|
|
|
722
498
|
}
|
|
723
499
|
}
|
|
724
500
|
|
|
501
|
+
private _requestEventRender(): void {
|
|
502
|
+
if (!this._isStreaming()) {
|
|
503
|
+
this.requestRender?.();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (this.renderThrottleTimer) return;
|
|
507
|
+
this.renderThrottleTimer = setTimeout(() => {
|
|
508
|
+
this.renderThrottleTimer = undefined;
|
|
509
|
+
this.requestRender?.();
|
|
510
|
+
}, STREAMING_RENDER_THROTTLE_MS);
|
|
511
|
+
this.renderThrottleTimer.unref?.();
|
|
512
|
+
}
|
|
513
|
+
|
|
725
514
|
private _isBlocked(): boolean {
|
|
726
515
|
return this._currentStage()?.status === "blocked";
|
|
727
516
|
}
|
|
728
517
|
|
|
729
|
-
private
|
|
730
|
-
|
|
731
|
-
|
|
518
|
+
private _isPaused(
|
|
519
|
+
stage: StageSnapshot | undefined = this._currentStage(),
|
|
520
|
+
): boolean {
|
|
521
|
+
return this.localPaused || stage?.status === "paused";
|
|
732
522
|
}
|
|
733
523
|
|
|
734
524
|
// -------------------------------------------------------------------------
|
|
735
|
-
// Top-level render — composes header / body /
|
|
525
|
+
// Top-level render — composes header / body / usage / editor / footer
|
|
736
526
|
// -------------------------------------------------------------------------
|
|
737
527
|
|
|
738
528
|
render(width: number): string[] {
|
|
739
529
|
const w = Math.max(40, width);
|
|
740
530
|
const stage = this._currentStage();
|
|
741
531
|
const blocked = this._isBlocked();
|
|
742
|
-
const
|
|
743
|
-
const streaming = this._isStreaming() && !blocked && !settled;
|
|
744
|
-
const paused = this.localPaused || stage?.status === "paused";
|
|
532
|
+
const streaming = this._isStreaming() && !blocked;
|
|
745
533
|
|
|
746
534
|
const headerLines = this._renderHeader(w, stage);
|
|
747
535
|
const sepLines = [this._sepRule(w)];
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const editorLines = this._renderEditor(w, {
|
|
753
|
-
paused,
|
|
754
|
-
streaming,
|
|
755
|
-
settled,
|
|
756
|
-
blocked,
|
|
757
|
-
omitTopRule: loaderLines.length > 0,
|
|
758
|
-
});
|
|
759
|
-
const hintsLines = this._renderHints(w, { paused, streaming, settled });
|
|
536
|
+
const workingLines = this._renderWorkingStatus(w, stage, { streaming });
|
|
537
|
+
const usageLines = this._renderUsage(w);
|
|
538
|
+
const editorLines = this._renderEditor(w, blocked);
|
|
539
|
+
const footerLines = this._renderFooter(w);
|
|
760
540
|
|
|
761
541
|
const fixed =
|
|
762
542
|
HEADER_ROWS +
|
|
763
543
|
SEP_ROWS +
|
|
764
|
-
|
|
544
|
+
workingLines.length +
|
|
545
|
+
usageLines.length +
|
|
765
546
|
editorLines.length +
|
|
766
|
-
|
|
767
|
-
HINTS_ROWS;
|
|
547
|
+
footerLines.length;
|
|
768
548
|
const totalRows = this._viewLineCount();
|
|
769
549
|
const bodyBudget = Math.max(1, totalRows - fixed);
|
|
770
550
|
this.bodyViewport.setVisibleRows(bodyBudget);
|
|
771
551
|
if (blocked) this.bodyViewport.scrollToBottom();
|
|
772
552
|
const bodyLines = blocked
|
|
773
553
|
? this._renderBlockedBody(w, bodyBudget, stage)
|
|
774
|
-
: this._renderBody(w, bodyBudget
|
|
775
|
-
const footerLines = this._renderFooter(w, stage, { paused, streaming, settled });
|
|
776
|
-
|
|
554
|
+
: this._renderBody(w, bodyBudget);
|
|
777
555
|
const lines = [
|
|
778
556
|
...headerLines,
|
|
779
557
|
...sepLines,
|
|
780
558
|
...bodyLines,
|
|
781
|
-
...
|
|
559
|
+
...workingLines,
|
|
560
|
+
...usageLines,
|
|
782
561
|
...editorLines,
|
|
783
562
|
...footerLines,
|
|
784
|
-
...hintsLines,
|
|
785
563
|
];
|
|
786
564
|
while (lines.length < totalRows) lines.push(this._blank(w));
|
|
787
565
|
if (lines.length > totalRows) lines.length = totalRows;
|
|
@@ -792,7 +570,10 @@ export class StageChatView implements Component {
|
|
|
792
570
|
// Header
|
|
793
571
|
// -------------------------------------------------------------------------
|
|
794
572
|
|
|
795
|
-
private _renderHeader(
|
|
573
|
+
private _renderHeader(
|
|
574
|
+
width: number,
|
|
575
|
+
stage: StageSnapshot | undefined,
|
|
576
|
+
): string[] {
|
|
796
577
|
const t = this.theme;
|
|
797
578
|
const stageName = stage?.name ?? "stage";
|
|
798
579
|
const status = stage?.status ?? (this.handle ? "pending" : "completed");
|
|
@@ -811,7 +592,11 @@ export class StageChatView implements Component {
|
|
|
811
592
|
const pill = this._statusPill(status);
|
|
812
593
|
const right = (meta ? paint(meta, t.dim) + " " : "") + pill.styled + " ";
|
|
813
594
|
|
|
814
|
-
const leftW =
|
|
595
|
+
const leftW =
|
|
596
|
+
visibleWidth(this.workflowName) +
|
|
597
|
+
visibleWidth(stageName) +
|
|
598
|
+
visibleWidth(" STAGE / ") +
|
|
599
|
+
1;
|
|
815
600
|
const rightW = visibleWidth(meta) + (meta ? 2 : 0) + pill.width + 1;
|
|
816
601
|
const gap = Math.max(1, width - leftW - rightW);
|
|
817
602
|
return [left + " ".repeat(gap) + right];
|
|
@@ -836,11 +621,31 @@ export class StageChatView implements Component {
|
|
|
836
621
|
const t = this.theme;
|
|
837
622
|
const map: Record<string, { fg: string; bg: string; label: string }> = {
|
|
838
623
|
pending: { fg: t.dim, bg: blendBg(t.bg, t.dim, 0.18), label: "pending" },
|
|
839
|
-
running: {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
624
|
+
running: {
|
|
625
|
+
fg: t.accent,
|
|
626
|
+
bg: blendBg(t.bg, t.accent, 0.18),
|
|
627
|
+
label: "running",
|
|
628
|
+
},
|
|
629
|
+
paused: {
|
|
630
|
+
fg: t.warning,
|
|
631
|
+
bg: blendBg(t.bg, t.warning, 0.18),
|
|
632
|
+
label: "paused",
|
|
633
|
+
},
|
|
634
|
+
blocked: {
|
|
635
|
+
fg: t.warning,
|
|
636
|
+
bg: blendBg(t.bg, t.warning, 0.18),
|
|
637
|
+
label: "blocked",
|
|
638
|
+
},
|
|
639
|
+
completed: {
|
|
640
|
+
fg: t.success,
|
|
641
|
+
bg: blendBg(t.bg, t.success, 0.18),
|
|
642
|
+
label: "completed",
|
|
643
|
+
},
|
|
644
|
+
failed: {
|
|
645
|
+
fg: t.error,
|
|
646
|
+
bg: blendBg(t.bg, t.error, 0.18),
|
|
647
|
+
label: "failed",
|
|
648
|
+
},
|
|
844
649
|
};
|
|
845
650
|
const cfg = map[status] ?? map.pending!;
|
|
846
651
|
const body = ` ● ${cfg.label} `;
|
|
@@ -858,25 +663,39 @@ export class StageChatView implements Component {
|
|
|
858
663
|
// Body — welcome panel / banner + transcript / blocked
|
|
859
664
|
// -------------------------------------------------------------------------
|
|
860
665
|
|
|
861
|
-
private _renderBlockedBody(
|
|
666
|
+
private _renderBlockedBody(
|
|
667
|
+
width: number,
|
|
668
|
+
budget: number,
|
|
669
|
+
stage: StageSnapshot | undefined,
|
|
670
|
+
): string[] {
|
|
862
671
|
const t = this.theme;
|
|
863
672
|
const upstream = stage?.blockedByStageId ?? "upstream stage";
|
|
864
673
|
const lines: string[] = [];
|
|
865
674
|
// Yellow banner — uses the same chrome vocabulary as paused/completed.
|
|
866
|
-
lines.push(
|
|
675
|
+
lines.push(
|
|
676
|
+
...this._bannerLines(
|
|
677
|
+
width,
|
|
678
|
+
"warning",
|
|
679
|
+
"↑",
|
|
680
|
+
"BLOCKED",
|
|
681
|
+
`waiting on ${upstream}`,
|
|
682
|
+
),
|
|
683
|
+
);
|
|
867
684
|
lines.push(this._blank(width));
|
|
868
685
|
lines.push(
|
|
869
686
|
...new Text(
|
|
870
|
-
paint(
|
|
687
|
+
paint(
|
|
688
|
+
"This stage is waiting for the upstream stage to resume.",
|
|
689
|
+
t.textMuted,
|
|
690
|
+
),
|
|
871
691
|
2,
|
|
872
692
|
0,
|
|
873
693
|
).render(width),
|
|
874
694
|
);
|
|
875
695
|
lines.push(
|
|
876
696
|
...new Text(
|
|
877
|
-
paint("
|
|
878
|
-
paint("
|
|
879
|
-
paint(" to return to the graph.", t.textMuted),
|
|
697
|
+
paint("ctrl+d", t.accent, { bold: true }) +
|
|
698
|
+
paint(" return to graph", t.textMuted),
|
|
880
699
|
2,
|
|
881
700
|
0,
|
|
882
701
|
).render(width),
|
|
@@ -889,109 +708,31 @@ export class StageChatView implements Component {
|
|
|
889
708
|
private _renderBody(
|
|
890
709
|
width: number,
|
|
891
710
|
budget: number,
|
|
892
|
-
stage: StageSnapshot | undefined,
|
|
893
|
-
flags: { paused: boolean; streaming: boolean; settled: boolean },
|
|
894
711
|
): string[] {
|
|
895
|
-
// Empty + not paused + not settled + not streaming → welcome panel.
|
|
896
|
-
const transcriptEmpty = this.transcript.length === 0;
|
|
897
|
-
if (transcriptEmpty && !flags.paused && !flags.settled && !flags.streaming) {
|
|
898
|
-
return this._fitToBudget(this._renderWelcome(width, stage), budget, width);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
712
|
const components: Component[] = [];
|
|
902
|
-
if (flags.paused) {
|
|
903
|
-
components.push(
|
|
904
|
-
this._banner(
|
|
905
|
-
"warning",
|
|
906
|
-
"❚❚",
|
|
907
|
-
"PAUSED",
|
|
908
|
-
"stopped between turns · type to resume, or Ctrl+P to release without input",
|
|
909
|
-
),
|
|
910
|
-
);
|
|
911
|
-
components.push(new Spacer(1));
|
|
912
|
-
} else if (flags.settled && stage?.status === "completed") {
|
|
913
|
-
components.push(this._banner("success", "✓", "COMPLETED", this._completedMeta(stage)));
|
|
914
|
-
components.push(new Spacer(1));
|
|
915
|
-
} else if (flags.settled && stage?.status === "failed") {
|
|
916
|
-
components.push(
|
|
917
|
-
this._banner(
|
|
918
|
-
"error",
|
|
919
|
-
"✗",
|
|
920
|
-
"FAILED",
|
|
921
|
-
stage?.error?.replace(/\s+/g, " ") ?? "stage exited with an error",
|
|
922
|
-
),
|
|
923
|
-
);
|
|
924
|
-
components.push(new Spacer(1));
|
|
925
|
-
}
|
|
926
|
-
|
|
927
713
|
// Base chat body: delegate transcript composition to the Pi-style
|
|
928
714
|
// transcript component so the attached stage chat uses the same message
|
|
929
715
|
// spacing and coding-agent message widgets as the main interactive chat.
|
|
930
716
|
if (this.transcript.length > 0) {
|
|
931
717
|
components.push(
|
|
932
|
-
new ChatTranscriptComponent(this.transcript, (entry) =>
|
|
718
|
+
new ChatTranscriptComponent(this.transcript, (entry) =>
|
|
719
|
+
this._renderEntry(entry),
|
|
720
|
+
),
|
|
933
721
|
);
|
|
934
722
|
}
|
|
935
723
|
|
|
936
724
|
// Stream a static status message (e.g. "pausing…") as a dim trailing row.
|
|
937
725
|
if (this.statusMessage) {
|
|
938
726
|
components.push(new Spacer(1));
|
|
939
|
-
components.push(
|
|
727
|
+
components.push(
|
|
728
|
+
new Text(paint(this.statusMessage, this.theme.dim), 2, 0),
|
|
729
|
+
);
|
|
940
730
|
}
|
|
941
731
|
|
|
942
732
|
this.bodyViewport.setComponents(components);
|
|
943
733
|
return this.bodyViewport.render(width);
|
|
944
734
|
}
|
|
945
735
|
|
|
946
|
-
private _fitToBudget(lines: string[], budget: number, width: number): string[] {
|
|
947
|
-
if (lines.length >= budget) return lines.slice(lines.length - budget);
|
|
948
|
-
const out = lines.slice();
|
|
949
|
-
while (out.length < budget) out.push(this._blank(width));
|
|
950
|
-
return out;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// -------------------------------------------------------------------------
|
|
954
|
-
// Welcome panel — first attach, no transcript yet
|
|
955
|
-
// -------------------------------------------------------------------------
|
|
956
|
-
|
|
957
|
-
private _renderWelcome(width: number, stage: StageSnapshot | undefined): string[] {
|
|
958
|
-
const t = this.theme;
|
|
959
|
-
const sessionId = this.handle?.sessionId ?? stage?.sessionId;
|
|
960
|
-
const sessionFile = this.handle?.sessionFile ?? stage?.sessionFile;
|
|
961
|
-
const status = stage?.status ?? "pending";
|
|
962
|
-
|
|
963
|
-
const out: string[] = [];
|
|
964
|
-
out.push(...new Spacer(1).render(width));
|
|
965
|
-
out.push(centred(paint("▎", t.mauve, { bold: true }), width));
|
|
966
|
-
out.push(
|
|
967
|
-
centred(
|
|
968
|
-
paint("Attached to ", t.text) +
|
|
969
|
-
paint(this.workflowName, t.textMuted) +
|
|
970
|
-
paint(" / ", t.dim) +
|
|
971
|
-
paint(stage?.name ?? "stage", t.text, { bold: true }),
|
|
972
|
-
width,
|
|
973
|
-
),
|
|
974
|
-
);
|
|
975
|
-
out.push(...new Spacer(1).render(width));
|
|
976
|
-
const sub =
|
|
977
|
-
"This stage is idle. Press ↵ to send the first prompt — the SDK session " +
|
|
978
|
-
"will be created on submit. The workflow body keeps running in the " +
|
|
979
|
-
"background; closing this overlay does not kill the run.";
|
|
980
|
-
out.push(...new Text(paint(sub, t.textMuted), 4, 0).render(width));
|
|
981
|
-
out.push(...new Spacer(1).render(width));
|
|
982
|
-
|
|
983
|
-
const grid: Array<[string, string]> = [
|
|
984
|
-
["session", sessionId ? shortenId(sessionId) : "(not yet realised)"],
|
|
985
|
-
["status", status],
|
|
986
|
-
];
|
|
987
|
-
if (sessionFile) grid.push(["session file", shortenFile(sessionFile)]);
|
|
988
|
-
for (const [k, v] of grid) {
|
|
989
|
-
const row = paint(k.padEnd(13), t.dim) + paint(v, t.text);
|
|
990
|
-
out.push(...new Text(row, 8, 0).render(width));
|
|
991
|
-
}
|
|
992
|
-
return out;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
736
|
// -------------------------------------------------------------------------
|
|
996
737
|
// Transcript entry → pi/coding-agent Component. Stage chat deliberately uses
|
|
997
738
|
// the same exported message/tool components as the main interactive chat
|
|
@@ -1000,56 +741,34 @@ export class StageChatView implements Component {
|
|
|
1000
741
|
|
|
1001
742
|
private _renderEntry(entry: TranscriptEntry): Component {
|
|
1002
743
|
if (isChatMessageEntry(entry)) {
|
|
1003
|
-
return renderChatMessageEntry(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
return renderChatMessageEntry(
|
|
1008
|
-
{ role: "user", kind: "user", text: entry.text },
|
|
1009
|
-
this._chatMessageRenderOptions(),
|
|
1010
|
-
);
|
|
1011
|
-
case "assistant":
|
|
1012
|
-
return renderChatMessageEntry(
|
|
1013
|
-
{ role: "assistant", kind: "assistant", message: assistantMessageForText(entry.text) },
|
|
1014
|
-
this._chatMessageRenderOptions(),
|
|
1015
|
-
);
|
|
1016
|
-
case "thinking":
|
|
1017
|
-
return renderChatMessageEntry(
|
|
1018
|
-
{ role: "assistant", kind: "assistant", message: assistantMessageForThinking(entry.text) },
|
|
1019
|
-
this._chatMessageRenderOptions(),
|
|
1020
|
-
);
|
|
1021
|
-
case "tool":
|
|
1022
|
-
return renderChatMessageEntry(this._toolEntryToChatMessage(entry), this._chatMessageRenderOptions());
|
|
1023
|
-
case "notice":
|
|
1024
|
-
return this._noticeRow(entry);
|
|
1025
|
-
case "system":
|
|
1026
|
-
return renderChatMessageEntry(
|
|
1027
|
-
{ role: "system", kind: "system", text: entry.text },
|
|
1028
|
-
this._chatMessageRenderOptions(),
|
|
1029
|
-
);
|
|
744
|
+
return renderChatMessageEntry(
|
|
745
|
+
this._streamingWindowedEntry(entry),
|
|
746
|
+
this._chatMessageRenderOptions(),
|
|
747
|
+
);
|
|
1030
748
|
}
|
|
749
|
+
return this._noticeRow(entry);
|
|
1031
750
|
}
|
|
1032
751
|
|
|
1033
|
-
private
|
|
1034
|
-
|
|
752
|
+
private _streamingWindowedEntry(entry: ChatMessageEntry): ChatMessageEntry {
|
|
753
|
+
if (!this._isStreaming() || this.bodyViewport.getScrollFromBottom() !== 0) {
|
|
754
|
+
return entry;
|
|
755
|
+
}
|
|
756
|
+
if (entry.kind !== "assistant") return entry;
|
|
757
|
+
const content = entry.message.content.map((item) => {
|
|
758
|
+
if (item.type === "text") {
|
|
759
|
+
return { ...item, text: tailStreamingText(item.text) };
|
|
760
|
+
}
|
|
761
|
+
if (item.type === "thinking") {
|
|
762
|
+
return { ...item, thinking: tailStreamingText(item.thinking) };
|
|
763
|
+
}
|
|
764
|
+
return item;
|
|
765
|
+
});
|
|
1035
766
|
return {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
isPartial: entry.state === "pending",
|
|
1042
|
-
result:
|
|
1043
|
-
entry.state !== "pending" || entry.output
|
|
1044
|
-
? {
|
|
1045
|
-
role: "toolResult",
|
|
1046
|
-
toolCallId,
|
|
1047
|
-
toolName: entry.name,
|
|
1048
|
-
content: entry.output ? [{ type: "text", text: entry.output }] : [],
|
|
1049
|
-
isError: entry.state === "error",
|
|
1050
|
-
timestamp: Date.now(),
|
|
1051
|
-
}
|
|
1052
|
-
: undefined,
|
|
767
|
+
...entry,
|
|
768
|
+
message: {
|
|
769
|
+
...entry.message,
|
|
770
|
+
content,
|
|
771
|
+
},
|
|
1053
772
|
};
|
|
1054
773
|
}
|
|
1055
774
|
|
|
@@ -1058,7 +777,7 @@ export class StageChatView implements Component {
|
|
|
1058
777
|
return {
|
|
1059
778
|
...inherited,
|
|
1060
779
|
ui: this._toolTui(),
|
|
1061
|
-
cwd: process.cwd(),
|
|
780
|
+
cwd: this.handle?.agentSession?.sessionManager.getCwd() ?? process.cwd(),
|
|
1062
781
|
showImages: inherited?.showImages ?? true,
|
|
1063
782
|
};
|
|
1064
783
|
}
|
|
@@ -1094,8 +813,9 @@ export class StageChatView implements Component {
|
|
|
1094
813
|
meta: string,
|
|
1095
814
|
): Component {
|
|
1096
815
|
const t = this.theme;
|
|
1097
|
-
const fg =
|
|
1098
|
-
|
|
816
|
+
const fg =
|
|
817
|
+
kind === "warning" ? t.warning : kind === "success" ? t.success : t.error;
|
|
818
|
+
const bg = blendBg(t.bg, fg, 0.1);
|
|
1099
819
|
const head =
|
|
1100
820
|
paintOnFill(glyph, fg, { bold: true }) +
|
|
1101
821
|
" " +
|
|
@@ -1121,215 +841,98 @@ export class StageChatView implements Component {
|
|
|
1121
841
|
return this._banner(kind, glyph, label, meta).render(width);
|
|
1122
842
|
}
|
|
1123
843
|
|
|
1124
|
-
// -------------------------------------------------------------------------
|
|
1125
|
-
// Loader — top rule + spinner row + bottom rule
|
|
1126
|
-
// -------------------------------------------------------------------------
|
|
1127
|
-
|
|
1128
|
-
private _renderLoader(width: number, stage: StageSnapshot | undefined): string[] {
|
|
1129
|
-
const t = this.theme;
|
|
1130
|
-
const rule = hexToAnsi(t.border) + "─".repeat(width) + RESET;
|
|
1131
|
-
const dur = stageDurationText(stage);
|
|
1132
|
-
const msg = `Working${dur ? " · " + dur : ""}`;
|
|
1133
|
-
const escapeHint = paint("Esc", t.text, { bold: true }) + " " + paint("interrupt", t.dim);
|
|
1134
|
-
const left = " " + paint(spinnerFrame(), t.accent, { bold: true }) + " " + paint(msg, t.textMuted) + " ";
|
|
1135
|
-
const leftW = visibleWidth(spinnerFrame()) + 4 + visibleWidth(msg);
|
|
1136
|
-
const rightW = visibleWidth("Esc interrupt");
|
|
1137
|
-
const gap = Math.max(1, width - leftW - rightW - 2);
|
|
1138
|
-
const body = left + " ".repeat(gap) + escapeHint + " ";
|
|
1139
|
-
// No closing rule — the editor's top rule (or the editor's body when
|
|
1140
|
-
// `omitTopRule: true`) sits directly underneath and provides the divider.
|
|
1141
|
-
return [rule, body];
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
844
|
// -------------------------------------------------------------------------
|
|
1145
845
|
// Editor — top rule + ` ❯ … ` + bottom rule
|
|
1146
846
|
// -------------------------------------------------------------------------
|
|
1147
847
|
|
|
1148
|
-
private _renderEditor(
|
|
1149
|
-
width: number,
|
|
1150
|
-
flags: {
|
|
1151
|
-
paused: boolean;
|
|
1152
|
-
streaming: boolean;
|
|
1153
|
-
settled: boolean;
|
|
1154
|
-
blocked: boolean;
|
|
1155
|
-
/**
|
|
1156
|
-
* When `true`, drop the editor's top rule — the loader directly above
|
|
1157
|
-
* already paints a horizontal rule and we don't want a doubled border.
|
|
1158
|
-
*/
|
|
1159
|
-
omitTopRule: boolean;
|
|
1160
|
-
},
|
|
1161
|
-
): string[] {
|
|
848
|
+
private _renderEditor(width: number, blocked: boolean): string[] {
|
|
1162
849
|
const t = this.theme;
|
|
1163
|
-
// Disabled
|
|
1164
|
-
|
|
850
|
+
// Disabled only when no live chat handle exists or workflow dependencies
|
|
851
|
+
// are blocked. A settled attached stage remains a regular chat session.
|
|
852
|
+
const disabled = blocked || !this.handle;
|
|
853
|
+
const ruleHex = this._editorRuleColor(disabled);
|
|
1165
854
|
if (!disabled && this.editor) {
|
|
855
|
+
setEditorFocused(this.editor, this.focused);
|
|
856
|
+
setEditorPlaceholder(this.editor, undefined);
|
|
857
|
+
setEditorBorderColor(this.editor, ruleHex);
|
|
1166
858
|
return this.editor.render(width);
|
|
1167
859
|
}
|
|
1168
|
-
|
|
860
|
+
if (this.editor) setEditorFocused(this.editor, false);
|
|
1169
861
|
const rule = hexToAnsi(ruleHex) + "─".repeat(width) + RESET;
|
|
1170
862
|
|
|
1171
863
|
const glyphHex = disabled ? t.dim : t.accent;
|
|
1172
|
-
const
|
|
1173
|
-
? "blocked · upstream stage owns the prompt"
|
|
1174
|
-
: flags.settled || !this.handle
|
|
1175
|
-
? "read-only · stage has no live handle"
|
|
1176
|
-
: flags.paused
|
|
1177
|
-
? "type to resume, or Ctrl+P to release without input…"
|
|
1178
|
-
: flags.streaming
|
|
1179
|
-
? "type to steer the current turn… (queues with ↵)"
|
|
1180
|
-
: "type a message…";
|
|
1181
|
-
|
|
864
|
+
const available = Math.max(1, width - 3);
|
|
1182
865
|
const value = this.inputBuffer
|
|
1183
|
-
? paint(truncateToWidth(this.inputBuffer,
|
|
1184
|
-
:
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
866
|
+
? paint(truncateToWidth(this.inputBuffer, available), t.text) + cursorBlock()
|
|
867
|
+
: disabled
|
|
868
|
+
? ""
|
|
869
|
+
: cursorBlock();
|
|
870
|
+
|
|
871
|
+
const left = paint("❯", glyphHex, { bold: true }) + " " + value;
|
|
872
|
+
const gap = Math.max(0, width - visibleWidth(stripAnsi(left)));
|
|
873
|
+
const body = left + " ".repeat(gap);
|
|
874
|
+
return [rule, body, rule];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private _editorRuleColor(disabled: boolean): string {
|
|
878
|
+
if (disabled) return this.theme.borderDim;
|
|
879
|
+
const level = this.handle?.agentSession?.state.thinkingLevel ?? "off";
|
|
880
|
+
switch (level) {
|
|
881
|
+
case "minimal":
|
|
882
|
+
return this.theme.borderDim;
|
|
883
|
+
case "low":
|
|
884
|
+
return this.theme.info;
|
|
885
|
+
case "medium":
|
|
886
|
+
return this.theme.accent;
|
|
887
|
+
case "high":
|
|
888
|
+
return this.theme.mauve;
|
|
889
|
+
case "xhigh":
|
|
890
|
+
return this.theme.error;
|
|
891
|
+
case "off":
|
|
892
|
+
default:
|
|
893
|
+
return this.theme.border;
|
|
894
|
+
}
|
|
1200
895
|
}
|
|
1201
896
|
|
|
1202
897
|
// -------------------------------------------------------------------------
|
|
1203
|
-
//
|
|
898
|
+
// Working, usage + footer — mirrors the main chat composer stack
|
|
1204
899
|
// -------------------------------------------------------------------------
|
|
1205
900
|
|
|
1206
|
-
private
|
|
901
|
+
private _renderWorkingStatus(
|
|
1207
902
|
width: number,
|
|
1208
903
|
stage: StageSnapshot | undefined,
|
|
1209
|
-
flags: {
|
|
904
|
+
flags: { streaming: boolean },
|
|
1210
905
|
): string[] {
|
|
906
|
+
if (!flags.streaming) return [];
|
|
1211
907
|
const t = this.theme;
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
: paint("session not yet realised", t.dim);
|
|
1221
|
-
const top = layoutRow(width, " ", " " + lTop, rTop + " ", t);
|
|
1222
|
-
|
|
1223
|
-
// Bottom line — left: messages / duration; right: caption
|
|
1224
|
-
const history = this.bodyViewport.getMaxScroll() > 0
|
|
1225
|
-
? this.bodyViewport.getScrollFromBottom() > 0
|
|
1226
|
-
? ` · history ↑${this.bodyViewport.getScrollFromBottom()}`
|
|
1227
|
-
: " · history bottom"
|
|
1228
|
-
: "";
|
|
1229
|
-
const lBot =
|
|
1230
|
-
paint(`◇ ${messages} messages`, t.dim) +
|
|
1231
|
-
(dur ? " " + paint(`· ${dur}`, t.dim) : "") +
|
|
1232
|
-
paint(history, t.dim);
|
|
1233
|
-
const rBot = flags.streaming
|
|
1234
|
-
? paint("streaming · live", t.accent)
|
|
1235
|
-
: flags.paused
|
|
1236
|
-
? paint("paused · ready to resume", t.warning)
|
|
1237
|
-
: flags.settled && stage?.status === "completed"
|
|
1238
|
-
? paint("completed · session persisted", t.success)
|
|
1239
|
-
: flags.settled && stage?.status === "failed"
|
|
1240
|
-
? paint("failed · see error", t.error)
|
|
1241
|
-
: paint(this.statusMessage || "idle · awaiting input", t.dim);
|
|
1242
|
-
const bot = layoutRow(width, " ", " " + lBot, rBot + " ", t);
|
|
1243
|
-
return [top, bot];
|
|
908
|
+
const dur = stageDurationText(stage);
|
|
909
|
+
const message = this.workingMessage ?? `Working${dur ? " · " + dur : ""}`;
|
|
910
|
+
return new WorkingStatusComponent({
|
|
911
|
+
spinner: spinnerFrame(),
|
|
912
|
+
message,
|
|
913
|
+
spinnerColor: (text) => paint(text, t.accent, { bold: true }),
|
|
914
|
+
messageColor: (text) => paint(text, t.textMuted),
|
|
915
|
+
}).render(width);
|
|
1244
916
|
}
|
|
1245
917
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
private _renderHints(
|
|
1251
|
-
width: number,
|
|
1252
|
-
flags: { paused: boolean; streaming: boolean; settled: boolean },
|
|
1253
|
-
): string[] {
|
|
1254
|
-
const t = this.theme;
|
|
1255
|
-
const dash = hexToAnsi(t.borderDim) + "╌".repeat(width) + RESET;
|
|
1256
|
-
const hints = this._hintSet(flags);
|
|
1257
|
-
const sep = paint(" · ", t.dim);
|
|
1258
|
-
const rendered = hints
|
|
1259
|
-
.map(({ key, label, emphasis }) =>
|
|
1260
|
-
paint(key, t.text, { bold: true }) +
|
|
1261
|
-
" " +
|
|
1262
|
-
paint(label, emphasis ? t.textMuted : t.dim, emphasis ? { bold: true } : {}),
|
|
1263
|
-
)
|
|
1264
|
-
.join(sep);
|
|
1265
|
-
const tagPlain = `pi-workflows/${this.workflowName}`;
|
|
1266
|
-
const renderedW = visibleWidth(stripAnsi(rendered));
|
|
1267
|
-
const tagW = visibleWidth(tagPlain);
|
|
1268
|
-
// Right-side tag is "nice to have". When the hint line + tag overflows
|
|
1269
|
-
// the chrome, drop the tag — the hints are the load-bearing affordance.
|
|
1270
|
-
if (renderedW + tagW + 3 > width) {
|
|
1271
|
-
const gap = Math.max(1, width - renderedW - 1);
|
|
1272
|
-
return [dash, " " + rendered + " ".repeat(gap)];
|
|
1273
|
-
}
|
|
1274
|
-
const tag = paint(tagPlain, t.dim);
|
|
1275
|
-
const gap = Math.max(1, width - renderedW - tagW - 2);
|
|
1276
|
-
return [dash, " " + rendered + " ".repeat(gap) + tag + " "];
|
|
918
|
+
private _renderUsage(width: number): string[] {
|
|
919
|
+
const agentSession = this.handle?.agentSession;
|
|
920
|
+
if (!agentSession) return [];
|
|
921
|
+
return new UsageMeterComponent(agentSession).render(width);
|
|
1277
922
|
}
|
|
1278
923
|
|
|
1279
|
-
private
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}): Array<{ key: string; label: string; emphasis?: boolean }> {
|
|
1284
|
-
const historyHint = { key: "PgUp/PgDn", label: "history" };
|
|
1285
|
-
if (flags.settled) {
|
|
1286
|
-
return [
|
|
1287
|
-
historyHint,
|
|
1288
|
-
{ key: "Ctrl+D", label: "back to graph", emphasis: true },
|
|
1289
|
-
{ key: "Esc", label: "close" },
|
|
1290
|
-
];
|
|
1291
|
-
}
|
|
1292
|
-
if (flags.paused) {
|
|
1293
|
-
return [
|
|
1294
|
-
{ key: "↵", label: "resume with message", emphasis: true },
|
|
1295
|
-
{ key: "Ctrl+P", label: "resume empty" },
|
|
1296
|
-
historyHint,
|
|
1297
|
-
{ key: "Ctrl+D", label: "back" },
|
|
1298
|
-
{ key: "Esc", label: "close" },
|
|
1299
|
-
];
|
|
1300
|
-
}
|
|
1301
|
-
if (flags.streaming) {
|
|
1302
|
-
return [
|
|
1303
|
-
{ key: "↵", label: "steer", emphasis: true },
|
|
1304
|
-
{ key: "Ctrl+F", label: "follow-up", emphasis: true },
|
|
1305
|
-
{ key: "Ctrl+P", label: "pause" },
|
|
1306
|
-
historyHint,
|
|
1307
|
-
{ key: "Ctrl+D", label: "back" },
|
|
1308
|
-
{ key: "Esc", label: "interrupt" },
|
|
1309
|
-
];
|
|
924
|
+
private _renderFooter(width: number): string[] {
|
|
925
|
+
const agentSession = this.handle?.agentSession;
|
|
926
|
+
if (agentSession && this.footerData) {
|
|
927
|
+
return new FooterComponent(agentSession, this.footerData).render(width);
|
|
1310
928
|
}
|
|
1311
|
-
return [
|
|
1312
|
-
{ key: "↵", label: "send", emphasis: true },
|
|
1313
|
-
{ key: "Ctrl+F", label: "follow-up" },
|
|
1314
|
-
{ key: "Ctrl+P", label: "pause" },
|
|
1315
|
-
historyHint,
|
|
1316
|
-
{ key: "Ctrl+D", label: "back" },
|
|
1317
|
-
{ key: "Esc", label: "close" },
|
|
1318
|
-
];
|
|
929
|
+
return [];
|
|
1319
930
|
}
|
|
1320
931
|
|
|
1321
932
|
// -------------------------------------------------------------------------
|
|
1322
933
|
// Small helpers
|
|
1323
934
|
// -------------------------------------------------------------------------
|
|
1324
935
|
|
|
1325
|
-
private _completedMeta(stage: StageSnapshot | undefined): string {
|
|
1326
|
-
const dur = stageDurationText(stage);
|
|
1327
|
-
const parts: string[] = ["stage settled"];
|
|
1328
|
-
if (dur) parts.push(dur);
|
|
1329
|
-
if (stage?.sessionFile) parts.push(`session ${shortenFile(stage.sessionFile)}`);
|
|
1330
|
-
return parts.join(" · ");
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
936
|
private _blank(width: number): string {
|
|
1334
937
|
return " ".repeat(width);
|
|
1335
938
|
}
|
|
@@ -1350,20 +953,19 @@ export class StageChatView implements Component {
|
|
|
1350
953
|
this.onDetach();
|
|
1351
954
|
return true;
|
|
1352
955
|
}
|
|
1353
|
-
if (data
|
|
1354
|
-
if (this.
|
|
956
|
+
if (matchesKey(data, "escape")) {
|
|
957
|
+
if (this._canPause()) {
|
|
1355
958
|
void this._pause();
|
|
1356
959
|
} else {
|
|
1357
960
|
this.onClose();
|
|
1358
961
|
}
|
|
1359
962
|
return true;
|
|
1360
963
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (blocked) return true;
|
|
1364
|
-
void this._pause();
|
|
964
|
+
if (data === "\x03") {
|
|
965
|
+
this.onClose();
|
|
1365
966
|
return true;
|
|
1366
967
|
}
|
|
968
|
+
const blocked = this._isBlocked();
|
|
1367
969
|
if (data === "\x06") {
|
|
1368
970
|
if (blocked) return true;
|
|
1369
971
|
void this._submit("followUp");
|
|
@@ -1392,6 +994,13 @@ export class StageChatView implements Component {
|
|
|
1392
994
|
return false;
|
|
1393
995
|
}
|
|
1394
996
|
|
|
997
|
+
private _canPause(): boolean {
|
|
998
|
+
if (!this.handle || this.localPaused || this._isBlocked()) return false;
|
|
999
|
+
const stage = this._currentStage();
|
|
1000
|
+
if (stage?.status === "paused") return false;
|
|
1001
|
+
return this._isStreaming();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1395
1004
|
private async _pause(): Promise<void> {
|
|
1396
1005
|
if (!this.handle) {
|
|
1397
1006
|
this.statusMessage = "no live handle on this stage";
|
|
@@ -1404,7 +1013,7 @@ export class StageChatView implements Component {
|
|
|
1404
1013
|
try {
|
|
1405
1014
|
await this.handle.pause();
|
|
1406
1015
|
this.sdkBusy = false;
|
|
1407
|
-
this.statusMessage = "
|
|
1016
|
+
this.statusMessage = "";
|
|
1408
1017
|
} catch (err) {
|
|
1409
1018
|
this.statusMessage = `pause failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1410
1019
|
this.localPaused = false;
|
|
@@ -1414,7 +1023,35 @@ export class StageChatView implements Component {
|
|
|
1414
1023
|
}
|
|
1415
1024
|
}
|
|
1416
1025
|
|
|
1417
|
-
private async
|
|
1026
|
+
private async _resume(message?: string): Promise<void> {
|
|
1027
|
+
if (!this.handle) {
|
|
1028
|
+
this.statusMessage = "no live handle on this stage";
|
|
1029
|
+
this.requestRender?.();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
this.localPaused = true;
|
|
1033
|
+
this.sdkBusy = true;
|
|
1034
|
+
this.statusMessage = "resuming…";
|
|
1035
|
+
this._syncAnimationTick();
|
|
1036
|
+
this.requestRender?.();
|
|
1037
|
+
try {
|
|
1038
|
+
await this.handle.resume(message);
|
|
1039
|
+
this.localPaused = false;
|
|
1040
|
+
this.sdkBusy = false;
|
|
1041
|
+
this.statusMessage = "";
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
this.sdkBusy = false;
|
|
1044
|
+
this.statusMessage = `resume failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1045
|
+
} finally {
|
|
1046
|
+
this._syncAnimationTick();
|
|
1047
|
+
this.requestRender?.();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private async _submit(
|
|
1052
|
+
mode: "auto" | "followUp",
|
|
1053
|
+
submittedText?: string,
|
|
1054
|
+
): Promise<void> {
|
|
1418
1055
|
const text = (submittedText ?? this.inputBuffer).trim();
|
|
1419
1056
|
if (!text) return;
|
|
1420
1057
|
this.inputBuffer = "";
|
|
@@ -1423,6 +1060,7 @@ export class StageChatView implements Component {
|
|
|
1423
1060
|
this.statusMessage = "no live handle on this stage";
|
|
1424
1061
|
this.transcript.push({
|
|
1425
1062
|
role: "system",
|
|
1063
|
+
kind: "system",
|
|
1426
1064
|
text: "(no live handle — message dropped)",
|
|
1427
1065
|
});
|
|
1428
1066
|
this.requestRender?.();
|
|
@@ -1433,13 +1071,8 @@ export class StageChatView implements Component {
|
|
|
1433
1071
|
this.optimisticUserSignatures.add(userMessageSignature(text));
|
|
1434
1072
|
this.requestRender?.();
|
|
1435
1073
|
try {
|
|
1436
|
-
if (this.
|
|
1437
|
-
this.
|
|
1438
|
-
this._syncAnimationTick();
|
|
1439
|
-
await this.handle.resume(text);
|
|
1440
|
-
this.localPaused = false;
|
|
1441
|
-
this.statusMessage = "resumed";
|
|
1442
|
-
this.requestRender?.();
|
|
1074
|
+
if (this._isPaused()) {
|
|
1075
|
+
await this._resume(text);
|
|
1443
1076
|
return;
|
|
1444
1077
|
}
|
|
1445
1078
|
if (mode === "followUp") {
|
|
@@ -1475,6 +1108,10 @@ export class StageChatView implements Component {
|
|
|
1475
1108
|
clearInterval(this.animationTimer);
|
|
1476
1109
|
this.animationTimer = undefined;
|
|
1477
1110
|
}
|
|
1111
|
+
if (this.renderThrottleTimer) {
|
|
1112
|
+
clearTimeout(this.renderThrottleTimer);
|
|
1113
|
+
this.renderThrottleTimer = undefined;
|
|
1114
|
+
}
|
|
1478
1115
|
this.editor = undefined;
|
|
1479
1116
|
}
|
|
1480
1117
|
|
|
@@ -1482,22 +1119,8 @@ export class StageChatView implements Component {
|
|
|
1482
1119
|
get _inputBuffer(): string {
|
|
1483
1120
|
return this.inputBuffer;
|
|
1484
1121
|
}
|
|
1485
|
-
get _transcript(): ReadonlyArray<
|
|
1486
|
-
|
|
1487
|
-
readonly text: string;
|
|
1488
|
-
readonly toolCallId: string;
|
|
1489
|
-
readonly state: string;
|
|
1490
|
-
readonly output: string;
|
|
1491
|
-
}
|
|
1492
|
-
> {
|
|
1493
|
-
return this.transcript.flatMap((entry) => transcriptDebugEntries(entry)) as ReadonlyArray<
|
|
1494
|
-
TranscriptEntry & {
|
|
1495
|
-
readonly text: string;
|
|
1496
|
-
readonly toolCallId: string;
|
|
1497
|
-
readonly state: string;
|
|
1498
|
-
readonly output: string;
|
|
1499
|
-
}
|
|
1500
|
-
>;
|
|
1122
|
+
get _transcript(): ReadonlyArray<TranscriptDebugEntry> {
|
|
1123
|
+
return this.transcript.flatMap((entry) => transcriptDebugEntries(entry));
|
|
1501
1124
|
}
|
|
1502
1125
|
get _statusMessage(): string {
|
|
1503
1126
|
return this.statusMessage;
|
|
@@ -1520,29 +1143,41 @@ export class StageChatView implements Component {
|
|
|
1520
1143
|
// Module-private helpers
|
|
1521
1144
|
// ---------------------------------------------------------------------------
|
|
1522
1145
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
function transcriptDebugEntries(entry: TranscriptEntry): Array<TranscriptEntry & {
|
|
1146
|
+
interface TranscriptDebugEntry {
|
|
1147
|
+
readonly role: string;
|
|
1526
1148
|
readonly text: string;
|
|
1527
1149
|
readonly toolCallId: string;
|
|
1528
1150
|
readonly state: string;
|
|
1529
1151
|
readonly output: string;
|
|
1530
|
-
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function transcriptDebugEntries(entry: TranscriptEntry): TranscriptDebugEntry[] {
|
|
1531
1155
|
if (isChatMessageEntry(entry) && entry.kind === "assistant") {
|
|
1532
|
-
const entries:
|
|
1156
|
+
const entries: TranscriptDebugEntry[] = [];
|
|
1533
1157
|
const thinking = extractThinkingText(entry.message.content);
|
|
1534
1158
|
const text = extractMessageText(entry.message.content);
|
|
1535
|
-
if (thinking)
|
|
1536
|
-
|
|
1159
|
+
if (thinking)
|
|
1160
|
+
entries.push({
|
|
1161
|
+
role: "thinking",
|
|
1162
|
+
text: thinking,
|
|
1163
|
+
toolCallId: "",
|
|
1164
|
+
state: "",
|
|
1165
|
+
output: "",
|
|
1166
|
+
});
|
|
1167
|
+
if (text || entries.length === 0)
|
|
1168
|
+
entries.push({ ...entry, text, toolCallId: "", state: "", output: "" });
|
|
1537
1169
|
return entries;
|
|
1538
1170
|
}
|
|
1539
|
-
return [
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1171
|
+
return [
|
|
1172
|
+
{
|
|
1173
|
+
...entry,
|
|
1174
|
+
role: entry.role,
|
|
1175
|
+
text: transcriptDebugText(entry),
|
|
1176
|
+
toolCallId: transcriptDebugToolCallId(entry),
|
|
1177
|
+
state: transcriptDebugToolState(entry),
|
|
1178
|
+
output: transcriptDebugToolOutput(entry),
|
|
1179
|
+
},
|
|
1180
|
+
];
|
|
1546
1181
|
}
|
|
1547
1182
|
|
|
1548
1183
|
function transcriptDebugText(entry: TranscriptEntry): string {
|
|
@@ -1571,8 +1206,10 @@ function transcriptDebugText(entry: TranscriptEntry): string {
|
|
|
1571
1206
|
}
|
|
1572
1207
|
|
|
1573
1208
|
function transcriptDebugToolCallId(entry: TranscriptEntry): string {
|
|
1574
|
-
if (isChatMessageEntry(entry) && entry.kind === "tool")
|
|
1575
|
-
|
|
1209
|
+
if (isChatMessageEntry(entry) && entry.kind === "tool")
|
|
1210
|
+
return entry.toolCallId;
|
|
1211
|
+
if ("toolCallId" in entry && typeof entry.toolCallId === "string")
|
|
1212
|
+
return entry.toolCallId;
|
|
1576
1213
|
return "";
|
|
1577
1214
|
}
|
|
1578
1215
|
|
|
@@ -1586,64 +1223,63 @@ function transcriptDebugToolState(entry: TranscriptEntry): string {
|
|
|
1586
1223
|
}
|
|
1587
1224
|
|
|
1588
1225
|
function transcriptDebugToolOutput(entry: TranscriptEntry): string {
|
|
1589
|
-
if (isChatMessageEntry(entry) && entry.kind === "tool")
|
|
1590
|
-
|
|
1226
|
+
if (isChatMessageEntry(entry) && entry.kind === "tool")
|
|
1227
|
+
return entry.result ? extractToolResultText(entry.result) : "";
|
|
1228
|
+
if ("output" in entry && typeof entry.output === "string")
|
|
1229
|
+
return entry.output;
|
|
1591
1230
|
return "";
|
|
1592
1231
|
}
|
|
1593
1232
|
|
|
1594
|
-
function
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
function isChatMessageEntry(entry: TranscriptEntry): entry is ChatMessageEntry {
|
|
1604
|
-
return "kind" in entry && entry.role !== "notice";
|
|
1233
|
+
function setEditorPlaceholder(
|
|
1234
|
+
editor: EditorComponent,
|
|
1235
|
+
placeholder: string | undefined,
|
|
1236
|
+
): void {
|
|
1237
|
+
const candidate = editor as EditorComponent & {
|
|
1238
|
+
setPlaceholder?: (value: string | undefined) => void;
|
|
1239
|
+
};
|
|
1240
|
+
candidate.setPlaceholder?.(placeholder);
|
|
1605
1241
|
}
|
|
1606
1242
|
|
|
1607
|
-
function
|
|
1608
|
-
return
|
|
1243
|
+
function cursorBlock(): string {
|
|
1244
|
+
return "\x1b[7m \x1b[0m";
|
|
1609
1245
|
}
|
|
1610
1246
|
|
|
1611
|
-
function
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1247
|
+
function setEditorBorderColor(editor: EditorComponent, hex: string): void {
|
|
1248
|
+
const candidate = editor as EditorComponent & {
|
|
1249
|
+
borderColor?: (text: string) => string;
|
|
1250
|
+
};
|
|
1251
|
+
if (candidate.borderColor !== undefined) {
|
|
1252
|
+
candidate.borderColor = (text: string) => hexToAnsi(hex) + text + RESET;
|
|
1253
|
+
}
|
|
1616
1254
|
}
|
|
1617
1255
|
|
|
1618
|
-
function
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
content: [{ type: "text", text }],
|
|
1622
|
-
stopReason: "stop",
|
|
1623
|
-
} as AssistantComponentMessage;
|
|
1256
|
+
function setEditorFocused(editor: EditorComponent, focused: boolean): void {
|
|
1257
|
+
const candidate = editor as EditorComponent & Partial<Focusable>;
|
|
1258
|
+
if ("focused" in candidate) candidate.focused = focused;
|
|
1624
1259
|
}
|
|
1625
1260
|
|
|
1626
|
-
function
|
|
1627
|
-
return
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1261
|
+
function isSharedLiveChatEvent(type: string): boolean {
|
|
1262
|
+
return (
|
|
1263
|
+
type === "message_start" ||
|
|
1264
|
+
type === "message_update" ||
|
|
1265
|
+
type === "message_end" ||
|
|
1266
|
+
type === "tool_execution_start" ||
|
|
1267
|
+
type === "tool_execution_update" ||
|
|
1268
|
+
type === "tool_execution_end"
|
|
1269
|
+
);
|
|
1632
1270
|
}
|
|
1633
1271
|
|
|
1634
|
-
function
|
|
1635
|
-
|
|
1636
|
-
if (entry.name === "bash") {
|
|
1637
|
-
return { command: entry.args.replace(/^command=/, "") };
|
|
1638
|
-
}
|
|
1639
|
-
return { input: entry.args };
|
|
1272
|
+
function isChatMessageEntry(entry: TranscriptEntry): entry is ChatMessageEntry {
|
|
1273
|
+
return "kind" in entry && entry.role !== "notice";
|
|
1640
1274
|
}
|
|
1641
1275
|
|
|
1642
|
-
function isMessageLike(message: unknown): message is { role?: unknown; content?: unknown
|
|
1276
|
+
function isMessageLike(message: unknown): message is { role?: unknown; content?: unknown } {
|
|
1643
1277
|
return message !== null && typeof message === "object" && "role" in message;
|
|
1644
1278
|
}
|
|
1645
1279
|
|
|
1646
|
-
function isUserMessageLike(
|
|
1280
|
+
function isUserMessageLike(
|
|
1281
|
+
message: unknown,
|
|
1282
|
+
): message is { role: "user"; content?: unknown } {
|
|
1647
1283
|
return isMessageLike(message) && message.role === "user";
|
|
1648
1284
|
}
|
|
1649
1285
|
|
|
@@ -1651,136 +1287,115 @@ function userMessageSignature(text: string): string {
|
|
|
1651
1287
|
return text.trim();
|
|
1652
1288
|
}
|
|
1653
1289
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1290
|
+
function assistantToolCallEvent(event: AgentSessionEvent): {
|
|
1291
|
+
type: "tool_execution_start";
|
|
1292
|
+
toolCallId: string;
|
|
1293
|
+
toolName: string;
|
|
1294
|
+
args: unknown;
|
|
1295
|
+
} | undefined {
|
|
1296
|
+
const assistantEvent = (event as {
|
|
1297
|
+
assistantMessageEvent?: {
|
|
1298
|
+
type?: unknown;
|
|
1299
|
+
contentIndex?: unknown;
|
|
1300
|
+
partial?: unknown;
|
|
1301
|
+
toolCall?: unknown;
|
|
1302
|
+
};
|
|
1303
|
+
}).assistantMessageEvent;
|
|
1304
|
+
const streamType = String(assistantEvent?.type ?? "");
|
|
1305
|
+
if (!streamType.startsWith("toolcall_")) return undefined;
|
|
1306
|
+
|
|
1307
|
+
const explicit = toolCallPayload(assistantEvent?.toolCall);
|
|
1308
|
+
if (explicit) return explicit;
|
|
1309
|
+
|
|
1310
|
+
const contentIndex = typeof assistantEvent?.contentIndex === "number" ? assistantEvent.contentIndex : undefined;
|
|
1311
|
+
if (contentIndex === undefined) return undefined;
|
|
1312
|
+
const partial = assistantEvent?.partial;
|
|
1313
|
+
if (!isMessageLike(partial) || partial.role !== "assistant") return undefined;
|
|
1314
|
+
const content = partial.content;
|
|
1315
|
+
if (!Array.isArray(content)) return undefined;
|
|
1316
|
+
return toolCallPayload(content[contentIndex]);
|
|
1658
1317
|
}
|
|
1659
1318
|
|
|
1660
|
-
function
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1319
|
+
function toolCallPayload(value: unknown): {
|
|
1320
|
+
type: "tool_execution_start";
|
|
1321
|
+
toolCallId: string;
|
|
1322
|
+
toolName: string;
|
|
1323
|
+
args: unknown;
|
|
1324
|
+
} | undefined {
|
|
1325
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
1326
|
+
const candidate = value as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
|
|
1327
|
+
if (candidate.type !== "toolCall") return undefined;
|
|
1328
|
+
if (typeof candidate.id !== "string" || typeof candidate.name !== "string") return undefined;
|
|
1329
|
+
return {
|
|
1330
|
+
type: "tool_execution_start",
|
|
1331
|
+
toolCallId: candidate.id,
|
|
1332
|
+
toolName: candidate.name,
|
|
1333
|
+
args: candidate.arguments ?? {},
|
|
1334
|
+
};
|
|
1671
1335
|
}
|
|
1672
1336
|
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
type?: unknown;
|
|
1690
|
-
text?: unknown;
|
|
1691
|
-
thinking?: unknown;
|
|
1692
|
-
id?: unknown;
|
|
1693
|
-
name?: unknown;
|
|
1694
|
-
arguments?: unknown;
|
|
1695
|
-
args?: unknown;
|
|
1696
|
-
};
|
|
1697
|
-
if (obj.type === "text" && typeof obj.text === "string") {
|
|
1698
|
-
textParts.push(obj.text);
|
|
1699
|
-
continue;
|
|
1700
|
-
}
|
|
1701
|
-
if (obj.type === "thinking" && typeof obj.thinking === "string") {
|
|
1702
|
-
thinkingParts.push(obj.thinking);
|
|
1703
|
-
continue;
|
|
1704
|
-
}
|
|
1705
|
-
if (obj.type === "toolCall") {
|
|
1706
|
-
const name = typeof obj.name === "string" ? obj.name : "tool";
|
|
1707
|
-
const toolCallId = typeof obj.id === "string" ? obj.id : undefined;
|
|
1708
|
-
const args = summariseArgs(obj.arguments ?? obj.args);
|
|
1709
|
-
projection.toolCalls.push({ toolCallId, name, args, state: "pending" });
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
projection.text = textParts.join("");
|
|
1713
|
-
projection.thinking = thinkingParts.join("\n\n");
|
|
1714
|
-
return projection;
|
|
1337
|
+
function legacyToolStartEvent(event: AgentSessionEvent): {
|
|
1338
|
+
type: "tool_execution_start";
|
|
1339
|
+
toolCallId: string;
|
|
1340
|
+
toolName: string;
|
|
1341
|
+
args: unknown;
|
|
1342
|
+
} {
|
|
1343
|
+
const payload = event as { toolCallId?: unknown; name?: unknown; input?: unknown; args?: unknown };
|
|
1344
|
+
const toolName = typeof payload.name === "string" ? payload.name : "tool";
|
|
1345
|
+
const toolCallId =
|
|
1346
|
+
typeof payload.toolCallId === "string" ? payload.toolCallId : `live-${toolName}`;
|
|
1347
|
+
return {
|
|
1348
|
+
type: "tool_execution_start",
|
|
1349
|
+
toolCallId,
|
|
1350
|
+
toolName,
|
|
1351
|
+
args: payload.input ?? payload.args ?? {},
|
|
1352
|
+
};
|
|
1715
1353
|
}
|
|
1716
1354
|
|
|
1717
|
-
function
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1355
|
+
function legacyToolResultEvent(event: AgentSessionEvent): {
|
|
1356
|
+
type: "tool_execution_end";
|
|
1357
|
+
toolCallId: string;
|
|
1358
|
+
toolName: string;
|
|
1359
|
+
result: unknown;
|
|
1360
|
+
isError: boolean;
|
|
1361
|
+
} {
|
|
1362
|
+
const payload = event as {
|
|
1363
|
+
toolCallId?: unknown;
|
|
1364
|
+
name?: unknown;
|
|
1365
|
+
output?: unknown;
|
|
1366
|
+
isError?: unknown;
|
|
1367
|
+
};
|
|
1368
|
+
const toolName = typeof payload.name === "string" ? payload.name : "tool";
|
|
1369
|
+
const toolCallId =
|
|
1370
|
+
typeof payload.toolCallId === "string" ? payload.toolCallId : `live-${toolName}`;
|
|
1371
|
+
const output = payload.output;
|
|
1372
|
+
return {
|
|
1373
|
+
type: "tool_execution_end",
|
|
1374
|
+
toolCallId,
|
|
1375
|
+
toolName,
|
|
1376
|
+
result:
|
|
1377
|
+
output !== null && typeof output === "object" && "content" in output
|
|
1378
|
+
? output
|
|
1379
|
+
: { content: typeof output === "string" ? [{ type: "text", text: output }] : [] },
|
|
1380
|
+
isError: payload.isError === true,
|
|
1381
|
+
};
|
|
1725
1382
|
}
|
|
1726
1383
|
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
return {
|
|
1743
|
-
role: "tool",
|
|
1744
|
-
name: message.toolName,
|
|
1745
|
-
output,
|
|
1746
|
-
state: message.isError ? "error" : "success",
|
|
1747
|
-
text: summary ? `← ${message.toolName} ${summary}` : `← ${message.toolName}`,
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
case "bashExecution": {
|
|
1751
|
-
const state =
|
|
1752
|
-
message.cancelled || (message.exitCode !== undefined && message.exitCode !== 0)
|
|
1753
|
-
? "error"
|
|
1754
|
-
: "success";
|
|
1755
|
-
const summary = message.output ? truncateToWidth(message.output.replace(/\s+/g, " "), 80) : "";
|
|
1756
|
-
return {
|
|
1757
|
-
role: "tool",
|
|
1758
|
-
name: "bash",
|
|
1759
|
-
args: truncateToWidth(message.command.replace(/\s+/g, " "), 60),
|
|
1760
|
-
output: message.output,
|
|
1761
|
-
state,
|
|
1762
|
-
text: summary ? `← bash ${summary}` : `→ bash ${message.command}`,
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
case "custom": {
|
|
1766
|
-
if (!message.display) return undefined;
|
|
1767
|
-
const text = extractMessageText(message.content);
|
|
1768
|
-
return text ? { role: "system", text } : undefined;
|
|
1769
|
-
}
|
|
1770
|
-
case "branchSummary": {
|
|
1771
|
-
const text = `Branch summary: ${message.summary}`;
|
|
1772
|
-
return { role: "system", text };
|
|
1773
|
-
}
|
|
1774
|
-
case "compactionSummary": {
|
|
1775
|
-
const text = `Compaction summary: ${message.summary}`;
|
|
1776
|
-
return { role: "system", text };
|
|
1777
|
-
}
|
|
1778
|
-
default:
|
|
1779
|
-
// The SDK message union is extensible. Snapshot unknown roles must be
|
|
1780
|
-
// skipped here instead of being cast into `TranscriptEntry`; `_renderBody`
|
|
1781
|
-
// only flattens the closed set of components returned by `_renderEntry`.
|
|
1782
|
-
return undefined;
|
|
1783
|
-
}
|
|
1384
|
+
function legacyThinkingEvent(event: AgentSessionEvent): {
|
|
1385
|
+
type: "message_update";
|
|
1386
|
+
assistantMessageEvent: { type: "thinking_delta"; delta: string };
|
|
1387
|
+
message: { role: "assistant"; content: [] };
|
|
1388
|
+
} {
|
|
1389
|
+
const delta = String(
|
|
1390
|
+
(event as { delta?: unknown }).delta ??
|
|
1391
|
+
(event as { text?: unknown }).text ??
|
|
1392
|
+
"",
|
|
1393
|
+
);
|
|
1394
|
+
return {
|
|
1395
|
+
type: "message_update",
|
|
1396
|
+
assistantMessageEvent: { type: "thinking_delta", delta },
|
|
1397
|
+
message: { role: "assistant", content: [] },
|
|
1398
|
+
};
|
|
1784
1399
|
}
|
|
1785
1400
|
|
|
1786
1401
|
function extractThinkingText(content: unknown): string {
|
|
@@ -1789,7 +1404,11 @@ function extractThinkingText(content: unknown): string {
|
|
|
1789
1404
|
for (const item of content) {
|
|
1790
1405
|
if (item == null || typeof item !== "object") continue;
|
|
1791
1406
|
const thinking = (item as { type?: unknown; thinking?: unknown }).thinking;
|
|
1792
|
-
if (
|
|
1407
|
+
if (
|
|
1408
|
+
(item as { type?: unknown }).type === "thinking" &&
|
|
1409
|
+
typeof thinking === "string"
|
|
1410
|
+
)
|
|
1411
|
+
parts.push(thinking);
|
|
1793
1412
|
}
|
|
1794
1413
|
return parts.join("\n\n");
|
|
1795
1414
|
}
|
|
@@ -1806,7 +1425,8 @@ function extractMessageText(content: unknown): string {
|
|
|
1806
1425
|
}
|
|
1807
1426
|
const obj = item as { type?: unknown; text?: unknown };
|
|
1808
1427
|
if (typeof obj.text === "string") parts.push(obj.text);
|
|
1809
|
-
else if (obj.type === "text" && typeof obj.text === "string")
|
|
1428
|
+
else if (obj.type === "text" && typeof obj.text === "string")
|
|
1429
|
+
parts.push(obj.text);
|
|
1810
1430
|
}
|
|
1811
1431
|
return parts.join("");
|
|
1812
1432
|
}
|
|
@@ -1818,43 +1438,27 @@ function extractToolResultText(result: unknown): string {
|
|
|
1818
1438
|
return extractMessageText(content);
|
|
1819
1439
|
}
|
|
1820
1440
|
|
|
1821
|
-
function findToolEntryIndex(
|
|
1822
|
-
entries: readonly TranscriptEntry[],
|
|
1823
|
-
toolCallId: string | undefined,
|
|
1824
|
-
name: string,
|
|
1825
|
-
): number {
|
|
1826
|
-
if (toolCallId !== undefined) {
|
|
1827
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1828
|
-
const entry = entries[i];
|
|
1829
|
-
if (isLocalToolEntry(entry) && entry.toolCallId === toolCallId) return i;
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1833
|
-
const entry = entries[i];
|
|
1834
|
-
if (isLocalToolEntry(entry) && entry.name === name && entry.state === "pending") return i;
|
|
1835
|
-
}
|
|
1836
|
-
return -1;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function summariseArgs(input: unknown): string {
|
|
1840
|
-
if (input == null) return "";
|
|
1841
|
-
if (typeof input === "string") return truncateToWidth(input.replace(/\s+/g, " "), 60);
|
|
1842
|
-
if (typeof input !== "object") return String(input);
|
|
1843
|
-
const obj = input as Record<string, unknown>;
|
|
1844
|
-
const keys = Object.keys(obj);
|
|
1845
|
-
if (keys.length === 0) return "";
|
|
1846
|
-
const head = keys[0]!;
|
|
1847
|
-
const value = obj[head];
|
|
1848
|
-
const summary = typeof value === "string" ? value : JSON.stringify(value);
|
|
1849
|
-
const formatted = `${head}=${summary}`;
|
|
1850
|
-
return truncateToWidth(formatted.replace(/\s+/g, " "), 60);
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
1441
|
function noticeSummary(n: StageNotice): string {
|
|
1854
1442
|
const base = `~ ${n.kind} → ${n.to}`;
|
|
1855
1443
|
return n.from ? `${base} (was ${n.from})` : base;
|
|
1856
1444
|
}
|
|
1857
1445
|
|
|
1446
|
+
function tailStreamingText(text: string): string {
|
|
1447
|
+
if (
|
|
1448
|
+
text.length <= STREAMING_TEXT_TAIL_CHARS &&
|
|
1449
|
+
text.split("\n").length <= STREAMING_TEXT_TAIL_LINES
|
|
1450
|
+
) {
|
|
1451
|
+
return text;
|
|
1452
|
+
}
|
|
1453
|
+
const byChars = text.slice(-STREAMING_TEXT_TAIL_CHARS);
|
|
1454
|
+
const lines = byChars.split("\n");
|
|
1455
|
+
const tail =
|
|
1456
|
+
lines.length > STREAMING_TEXT_TAIL_LINES
|
|
1457
|
+
? lines.slice(-STREAMING_TEXT_TAIL_LINES).join("\n")
|
|
1458
|
+
: byChars;
|
|
1459
|
+
return `[earlier streaming output hidden while attached]\n\n${tail.trimStart()}`;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1858
1462
|
function stageDurationText(stage: StageSnapshot | undefined): string {
|
|
1859
1463
|
if (!stage?.startedAt) return "";
|
|
1860
1464
|
const end = stage.endedAt ?? Date.now();
|
|
@@ -1878,15 +1482,6 @@ function shortenId(id: string): string {
|
|
|
1878
1482
|
return id.length > 10 ? id.slice(0, 8) : id;
|
|
1879
1483
|
}
|
|
1880
1484
|
|
|
1881
|
-
function shortenFile(path: string): string {
|
|
1882
|
-
if (path.length <= 36) return path;
|
|
1883
|
-
// Keep the basename and an ellipsis prefix so the user can still recognise
|
|
1884
|
-
// which session file we're pointing at.
|
|
1885
|
-
const slash = path.lastIndexOf("/");
|
|
1886
|
-
if (slash < 0) return "…" + path.slice(-35);
|
|
1887
|
-
return "…" + path.slice(Math.max(slash - 12, 0));
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
1485
|
function spinnerFrame(): string {
|
|
1891
1486
|
const idx = Math.floor(Date.now() / 80) % SPINNER_FRAMES.length;
|
|
1892
1487
|
return SPINNER_FRAMES[idx]!;
|
|
@@ -1898,7 +1493,8 @@ function bgFn(hex: string): (text: string) => string {
|
|
|
1898
1493
|
}
|
|
1899
1494
|
|
|
1900
1495
|
function editorThemeFromGraphTheme(t: GraphTheme): EditorTheme {
|
|
1901
|
-
const selected = (text: string): string =>
|
|
1496
|
+
const selected = (text: string): string =>
|
|
1497
|
+
hexBg(t.backgroundPanel) + hexToAnsi(t.text) + text + RESET;
|
|
1902
1498
|
const normal = (text: string): string => hexToAnsi(t.text) + text + RESET;
|
|
1903
1499
|
return {
|
|
1904
1500
|
borderColor: (text: string) => hexToAnsi(t.border) + text + RESET,
|
|
@@ -1948,31 +1544,6 @@ function stripAnsi(s: string): string {
|
|
|
1948
1544
|
return s.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
1949
1545
|
}
|
|
1950
1546
|
|
|
1951
|
-
function centred(content: string, width: number): string {
|
|
1952
|
-
const w = visibleWidth(stripAnsi(content));
|
|
1953
|
-
if (w >= width) return content;
|
|
1954
|
-
const left = Math.floor((width - w) / 2);
|
|
1955
|
-
const right = width - w - left;
|
|
1956
|
-
return " ".repeat(left) + content + " ".repeat(right);
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
/**
|
|
1960
|
-
* Compose a two-column row of `${prefix}${left}…${right}` padded to width.
|
|
1961
|
-
* Used by the footer to lay out left/right slabs without losing ANSI runs.
|
|
1962
|
-
*/
|
|
1963
|
-
function layoutRow(
|
|
1964
|
-
width: number,
|
|
1965
|
-
_prefix: string,
|
|
1966
|
-
left: string,
|
|
1967
|
-
right: string,
|
|
1968
|
-
_theme: GraphTheme,
|
|
1969
|
-
): string {
|
|
1970
|
-
const lw = visibleWidth(stripAnsi(left));
|
|
1971
|
-
const rw = visibleWidth(stripAnsi(right));
|
|
1972
|
-
const gap = Math.max(1, width - lw - rw);
|
|
1973
|
-
return left + " ".repeat(gap) + right;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
1547
|
/**
|
|
1977
1548
|
* Approximate a tinted background by mixing the base canvas with a saturated
|
|
1978
1549
|
* hue at low alpha. Used for status pills and tool-bar tints. Returns a hex
|