@oh-my-pi/pi-coding-agent 15.11.8 → 15.12.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 +36 -2
- package/dist/cli.js +8083 -7692
- package/dist/types/collab/crypto.d.ts +1 -6
- package/dist/types/collab/guest.d.ts +2 -0
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +14 -1
- package/dist/types/config/settings-schema.d.ts +40 -5
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +19 -1
- package/dist/types/modes/components/status-line/component.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +13 -14
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/collab/crypto.ts +10 -4
- package/src/collab/guest.ts +31 -2
- package/src/collab/host.ts +73 -11
- package/src/collab/protocol.ts +48 -7
- package/src/commands/join.ts +1 -1
- package/src/config/settings-schema.ts +40 -4
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/internal-urls/docs-index.generated.ts +73 -73
- package/src/main.ts +22 -9
- package/src/modes/components/agent-hub.ts +541 -410
- package/src/modes/components/status-line/component.ts +38 -5
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tips.txt +3 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +104 -4
- package/src/modes/controllers/selector-controller.ts +11 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/interactive-mode.ts +44 -2
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +16 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/session/agent-session.ts +65 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +62 -35
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/job.ts +6 -9
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
|
@@ -14,43 +14,62 @@
|
|
|
14
14
|
* Replaces the old SessionObserverOverlayComponent (ctrl+s observer).
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
19
|
+
import { Container, Editor, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
20
|
+
import { formatAge, formatBytes, formatDuration, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
21
|
+
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
20
22
|
import type { KeyId } from "../../config/keybindings";
|
|
23
|
+
import { settings } from "../../config/settings";
|
|
24
|
+
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
21
25
|
import { IrcBus } from "../../irc/bus";
|
|
22
26
|
import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
23
27
|
import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
24
28
|
import type { AgentSession } from "../../session/agent-session";
|
|
25
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type CustomMessage,
|
|
31
|
+
isSilentAbort,
|
|
32
|
+
LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
|
|
33
|
+
resolveAbortLabel,
|
|
34
|
+
SKILL_PROMPT_MESSAGE_TYPE,
|
|
35
|
+
type SkillPromptDetails,
|
|
36
|
+
USER_INTERRUPT_LABEL,
|
|
37
|
+
} from "../../session/messages";
|
|
26
38
|
import type { SessionMessageEntry } from "../../session/session-manager";
|
|
27
39
|
import { parseSessionEntries } from "../../session/session-manager";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
40
|
+
import { createIrcMessageCard } from "../../tools/irc";
|
|
41
|
+
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
30
42
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
31
|
-
import { getEditorTheme,
|
|
43
|
+
import { getEditorTheme, theme } from "../theme/theme";
|
|
32
44
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
45
|
+
import { AssistantMessageComponent } from "./assistant-message";
|
|
46
|
+
import { BashExecutionComponent } from "./bash-execution";
|
|
47
|
+
import { BranchSummaryMessageComponent } from "./branch-summary-message";
|
|
48
|
+
import { CollabPromptMessageComponent } from "./collab-prompt-message";
|
|
49
|
+
import { CompactionSummaryMessageComponent } from "./compaction-summary-message";
|
|
50
|
+
import { CustomMessageComponent } from "./custom-message";
|
|
33
51
|
import { DynamicBorder } from "./dynamic-border";
|
|
52
|
+
import { EvalExecutionComponent } from "./eval-execution";
|
|
53
|
+
import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
|
|
54
|
+
import { ReadToolGroupComponent, readArgsHaveTarget, readArgsTargetInternalUrl } from "./read-tool-group";
|
|
55
|
+
import { SkillMessageComponent } from "./skill-message";
|
|
34
56
|
import { formatContextUsage } from "./status-line/context-thresholds";
|
|
57
|
+
import { ToolExecutionComponent } from "./tool-execution";
|
|
58
|
+
import { TranscriptBlock, TranscriptContainer } from "./transcript-container";
|
|
59
|
+
import { UserMessageComponent } from "./user-message";
|
|
35
60
|
|
|
36
|
-
/** Max thinking characters in collapsed state */
|
|
37
|
-
const MAX_THINKING_CHARS_COLLAPSED = 200;
|
|
38
|
-
/** Max thinking characters in expanded state */
|
|
39
|
-
const MAX_THINKING_CHARS_EXPANDED = 4000;
|
|
40
|
-
/** Max tool args characters to display */
|
|
41
|
-
const MAX_TOOL_ARGS_CHARS = 500;
|
|
42
61
|
/** Lines per page for PageUp/PageDown */
|
|
43
62
|
const PAGE_SIZE = 15;
|
|
44
|
-
/** Left indent for content under entry headers */
|
|
45
|
-
const INDENT = " ";
|
|
46
63
|
/** Refresh cadence for the relative-time column */
|
|
47
64
|
const AGE_TICK_MS = 5_000;
|
|
48
65
|
/** Debounce for live-session transcript refreshes */
|
|
49
66
|
const CHAT_REFRESH_DEBOUNCE_MS = 80;
|
|
67
|
+
/** Double-tap window for the left-left "go to parent" gesture (matches the editor's). */
|
|
68
|
+
const LEFT_TAP_WINDOW_MS = 500;
|
|
50
69
|
|
|
51
|
-
/** Compute the max content width for the current terminal, accounting for
|
|
52
|
-
function contentWidth(
|
|
53
|
-
return Math.max(TRUNCATE_LENGTHS.SHORT, (process.stdout.columns || 80) -
|
|
70
|
+
/** Compute the max content width for the current terminal, accounting for chrome. */
|
|
71
|
+
function contentWidth(): number {
|
|
72
|
+
return Math.max(TRUNCATE_LENGTHS.SHORT, (process.stdout.columns || 80) - 6);
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
/** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
|
|
@@ -58,13 +77,6 @@ function sanitizeLine(text: string, maxWidth?: number): string {
|
|
|
58
77
|
return truncateToWidth(replaceTabs(text), maxWidth ?? contentWidth());
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
/** Represents a rendered entry in the viewer for selection/expand tracking */
|
|
62
|
-
interface ViewerEntry {
|
|
63
|
-
lineStart: number;
|
|
64
|
-
lineCount: number;
|
|
65
|
-
kind: "thinking" | "text" | "toolCall" | "user";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
80
|
const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
|
|
69
81
|
|
|
70
82
|
/** Glyph + status word, colored per theme status conventions. */
|
|
@@ -81,6 +93,38 @@ function statusBadge(status: AgentStatus): string {
|
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
function registerPersistedSubagents(registry: AgentRegistry, sessionFile: string | null | undefined): void {
|
|
97
|
+
if (!sessionFile?.endsWith(".jsonl")) return;
|
|
98
|
+
const root = sessionFile.slice(0, -6);
|
|
99
|
+
registerPersistedSubagentsFromDir(registry, root, undefined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function registerPersistedSubagentsFromDir(registry: AgentRegistry, dir: string, parentId: string | undefined): void {
|
|
103
|
+
let entries: fs.Dirent[];
|
|
104
|
+
try {
|
|
105
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
|
|
111
|
+
const id = entry.name.slice(0, -6);
|
|
112
|
+
const sessionFile = path.join(dir, entry.name);
|
|
113
|
+
if (!registry.get(id)) {
|
|
114
|
+
registry.register({
|
|
115
|
+
id,
|
|
116
|
+
displayName: id,
|
|
117
|
+
kind: "sub",
|
|
118
|
+
parentId: parentId ?? MAIN_AGENT_ID,
|
|
119
|
+
session: null,
|
|
120
|
+
sessionFile,
|
|
121
|
+
status: "parked",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
registerPersistedSubagentsFromDir(registry, path.join(dir, id), id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
84
128
|
/** Guest-side proxy for hub actions executed on the collab host. */
|
|
85
129
|
export interface AgentHubRemote {
|
|
86
130
|
chat(id: string, text: string): void;
|
|
@@ -103,6 +147,22 @@ export interface AgentHubDeps {
|
|
|
103
147
|
lifecycle?: AgentLifecycleManager;
|
|
104
148
|
/** Injectable for tests; defaults to the process-global bus. */
|
|
105
149
|
irc?: IrcBus;
|
|
150
|
+
/** TUI handle for transcript components; tests omit it and get a render-only stub. */
|
|
151
|
+
ui?: TUI;
|
|
152
|
+
/** Tool lookup for transcript renderers (labels, custom render functions). */
|
|
153
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
154
|
+
/** Extension message renderers for custom messages in the transcript. */
|
|
155
|
+
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
156
|
+
/** Cwd used by tool renderers for path shortening; defaults to the project dir. */
|
|
157
|
+
cwd?: string;
|
|
158
|
+
/** Mirrors the main transcript's thinking-block visibility. */
|
|
159
|
+
hideThinkingBlock?: () => boolean;
|
|
160
|
+
/** Keys toggling tool output expansion (app.tools.expand). */
|
|
161
|
+
expandKeys?: KeyId[];
|
|
162
|
+
/** Focus the main view on this agent's live session (ctx.focusAgentSession). When absent (collab guest, tests), Enter opens the in-hub chat view instead. */
|
|
163
|
+
focusAgent?: (id: string) => Promise<void>;
|
|
164
|
+
/** Current main session file; used to seed parked historical subagents after restart. */
|
|
165
|
+
sessionFile?: string | null;
|
|
106
166
|
/** Collab guest: route actions/transcripts to the host instead of local sessions. */
|
|
107
167
|
remote?: AgentHubRemote;
|
|
108
168
|
}
|
|
@@ -137,16 +197,33 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
137
197
|
#chatRefreshTimer: NodeJS.Timeout | undefined;
|
|
138
198
|
#transcriptCache: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string } | undefined;
|
|
139
199
|
|
|
140
|
-
//
|
|
200
|
+
// Chat transcript: the same component renderers as the main session
|
|
201
|
+
// transcript, assembled incrementally from the persisted JSONL entries.
|
|
202
|
+
#ui: TUI;
|
|
203
|
+
#getTool: ((name: string) => AgentTool | undefined) | undefined;
|
|
204
|
+
#getMessageRenderer: ((customType: string) => MessageRenderer | undefined) | undefined;
|
|
205
|
+
#cwd: string;
|
|
206
|
+
#hideThinkingBlock: (() => boolean) | undefined;
|
|
207
|
+
#expandKeys: KeyId[];
|
|
208
|
+
#focusAgent: ((id: string) => Promise<void>) | undefined;
|
|
209
|
+
#chatLog = new TranscriptContainer();
|
|
210
|
+
#chatEntriesRef: SessionMessageEntry[] | undefined;
|
|
211
|
+
#chatBuiltCount = 0;
|
|
212
|
+
#chatPendingTools = new Map<string, ToolExecutionComponent | ReadToolGroupComponent>();
|
|
213
|
+
#chatReadArgs = new Map<string, Record<string, unknown>>();
|
|
214
|
+
#chatReadGroup: ReadToolGroupComponent | null = null;
|
|
215
|
+
#chatWaitingPoll: ToolExecutionComponent | null = null;
|
|
216
|
+
#chatExpandables: Array<{ setExpanded(expanded: boolean): void }> = [];
|
|
217
|
+
#chatExpanded = false;
|
|
218
|
+
#chatPlaceholder: string | undefined;
|
|
219
|
+
|
|
220
|
+
// Viewport state
|
|
141
221
|
#scrollOffset = 0;
|
|
142
|
-
#
|
|
222
|
+
#lastMaxScroll = 0;
|
|
143
223
|
#viewportHeight = 20;
|
|
144
224
|
#wasAtBottom = true;
|
|
145
|
-
#viewerEntries: ViewerEntry[] = [];
|
|
146
|
-
#selectedEntryIndex = 0;
|
|
147
|
-
#expandedEntries = new Set<number>();
|
|
148
225
|
#viewerHeaderLines: string[] = [];
|
|
149
|
-
#
|
|
226
|
+
#lastLeftTap = 0;
|
|
150
227
|
|
|
151
228
|
constructor(deps: AgentHubDeps) {
|
|
152
229
|
super();
|
|
@@ -160,6 +237,18 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
160
237
|
this.#requestRender = deps.requestRender;
|
|
161
238
|
this.#hubKeys = deps.hubKeys;
|
|
162
239
|
this.#remote = deps.remote;
|
|
240
|
+
this.#ui =
|
|
241
|
+
deps.ui ??
|
|
242
|
+
({
|
|
243
|
+
requestRender: () => deps.requestRender(),
|
|
244
|
+
requestComponentRender: () => deps.requestRender(),
|
|
245
|
+
} as unknown as TUI);
|
|
246
|
+
this.#getTool = deps.getTool;
|
|
247
|
+
this.#getMessageRenderer = deps.getMessageRenderer;
|
|
248
|
+
this.#cwd = deps.cwd ?? getProjectDir();
|
|
249
|
+
this.#hideThinkingBlock = deps.hideThinkingBlock;
|
|
250
|
+
this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
|
|
251
|
+
this.#focusAgent = deps.focusAgent;
|
|
163
252
|
|
|
164
253
|
this.#editor = new Editor(getEditorTheme());
|
|
165
254
|
this.#editor.setMaxHeight(4);
|
|
@@ -170,6 +259,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
170
259
|
this.#ageTimer = setInterval(() => this.#requestRender(), AGE_TICK_MS);
|
|
171
260
|
this.#ageTimer.unref?.();
|
|
172
261
|
|
|
262
|
+
if (!this.#remote) registerPersistedSubagents(this.#registry, deps.sessionFile);
|
|
173
263
|
this.#refreshRows();
|
|
174
264
|
}
|
|
175
265
|
|
|
@@ -185,6 +275,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
185
275
|
this.#chatRefreshTimer = undefined;
|
|
186
276
|
}
|
|
187
277
|
this.#detachLiveSession();
|
|
278
|
+
this.#resetChatLog();
|
|
188
279
|
}
|
|
189
280
|
|
|
190
281
|
override render(width: number): readonly string[] {
|
|
@@ -216,18 +307,13 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
216
307
|
this.#remoteTranscriptUnavailable = false;
|
|
217
308
|
this.#remoteFetchInFlight = false;
|
|
218
309
|
this.#remoteFetchToken++;
|
|
310
|
+
this.#resetChatLog();
|
|
219
311
|
this.#scrollOffset = 0;
|
|
220
|
-
this.#selectedEntryIndex = 0;
|
|
221
|
-
this.#expandedEntries.clear();
|
|
222
312
|
this.#wasAtBottom = true;
|
|
313
|
+
this.#lastLeftTap = 0;
|
|
223
314
|
this.#editor.setText("");
|
|
224
315
|
this.#attachLiveSession();
|
|
225
316
|
this.#rebuildChatContent();
|
|
226
|
-
// Auto-scroll to bottom and select last entry on open
|
|
227
|
-
if (this.#viewerEntries.length > 0) {
|
|
228
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
229
|
-
this.#rebuildChatContent();
|
|
230
|
-
}
|
|
231
317
|
this.#requestRender();
|
|
232
318
|
}
|
|
233
319
|
|
|
@@ -283,12 +369,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
283
369
|
this.#chatRefreshTimer = setTimeout(() => {
|
|
284
370
|
this.#chatRefreshTimer = undefined;
|
|
285
371
|
if (this.#view !== "chat") return;
|
|
286
|
-
// Keep auto-scrolling to bottom unless the user navigated away
|
|
287
|
-
this.#wasAtBottom = this.#selectedEntryIndex >= this.#viewerEntries.length - 1;
|
|
288
372
|
this.#rebuildChatContent();
|
|
289
|
-
if (this.#wasAtBottom && this.#viewerEntries.length > 0) {
|
|
290
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
291
|
-
}
|
|
292
373
|
this.#requestRender();
|
|
293
374
|
}, CHAT_REFRESH_DEBOUNCE_MS);
|
|
294
375
|
this.#chatRefreshTimer.unref?.();
|
|
@@ -335,7 +416,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
335
416
|
lines.push(` ${theme.fg("error", sanitizeLine(this.#notice, Math.max(10, width - 2)))}`);
|
|
336
417
|
}
|
|
337
418
|
lines.push("");
|
|
338
|
-
lines.push(` ${theme.fg("dim", "j/k:select Enter:
|
|
419
|
+
lines.push(` ${theme.fg("dim", "j/k:select Enter:open r:revive x:kill Esc/←←:close")}`);
|
|
339
420
|
lines.push(...new DynamicBorder().render(width));
|
|
340
421
|
return lines;
|
|
341
422
|
}
|
|
@@ -375,6 +456,16 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
375
456
|
this.#onDone();
|
|
376
457
|
return;
|
|
377
458
|
}
|
|
459
|
+
if (matchesKey(keyData, "left")) {
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
if (now - this.#lastLeftTap < LEFT_TAP_WINDOW_MS) {
|
|
462
|
+
this.#lastLeftTap = 0;
|
|
463
|
+
this.#onDone();
|
|
464
|
+
} else {
|
|
465
|
+
this.#lastLeftTap = now;
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
378
469
|
if (keyData === "j" || matchesSelectDown(keyData)) {
|
|
379
470
|
if (this.#rows.length > 0) {
|
|
380
471
|
this.#selectedRow = Math.min(this.#selectedRow + 1, this.#rows.length - 1);
|
|
@@ -391,7 +482,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
391
482
|
}
|
|
392
483
|
if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
|
|
393
484
|
const selected = this.#rows[this.#selectedRow];
|
|
394
|
-
if (selected) this
|
|
485
|
+
if (selected) this.#activateAgent(selected);
|
|
395
486
|
return;
|
|
396
487
|
}
|
|
397
488
|
if (keyData === "r") {
|
|
@@ -404,6 +495,30 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
404
495
|
}
|
|
405
496
|
}
|
|
406
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Enter on a row: focus the main view on the agent's live session and close
|
|
500
|
+
* the hub. The transcript then renders through the regular session pipeline —
|
|
501
|
+
* exact parity by construction. Collab guests (no local sessions) keep the
|
|
502
|
+
* in-hub chat view.
|
|
503
|
+
*/
|
|
504
|
+
#activateAgent(ref: AgentRef): void {
|
|
505
|
+
this.#notice = undefined;
|
|
506
|
+
const focusAgent = this.#focusAgent;
|
|
507
|
+
if (this.#remote || !focusAgent) {
|
|
508
|
+
this.openChat(ref.id);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
void (async () => {
|
|
512
|
+
try {
|
|
513
|
+
await focusAgent(ref.id); // ensureLive inside revives parked agents; no parking, no session files
|
|
514
|
+
this.#onDone();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
this.#notice = error instanceof Error ? error.message : String(error);
|
|
517
|
+
this.#requestRender();
|
|
518
|
+
}
|
|
519
|
+
})();
|
|
520
|
+
}
|
|
521
|
+
|
|
407
522
|
#reviveSelected(): void {
|
|
408
523
|
const ref = this.#rows[this.#selectedRow];
|
|
409
524
|
if (!ref) return;
|
|
@@ -471,7 +586,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
471
586
|
const footerChrome = editorLines.length + footerLines.length + (noticeLine ? 1 : 0) + 1;
|
|
472
587
|
this.#viewportHeight = Math.max(5, termHeight - headerChrome - footerChrome);
|
|
473
588
|
|
|
474
|
-
const
|
|
589
|
+
const contentLines: readonly string[] = this.#chatPlaceholder
|
|
590
|
+
? [theme.fg("dim", this.#chatPlaceholder)]
|
|
591
|
+
: this.#chatLog.render(innerWidth);
|
|
592
|
+
|
|
593
|
+
const maxScroll = Math.max(0, contentLines.length - this.#viewportHeight);
|
|
594
|
+
this.#lastMaxScroll = maxScroll;
|
|
475
595
|
if (this.#wasAtBottom) this.#scrollOffset = maxScroll;
|
|
476
596
|
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScroll));
|
|
477
597
|
|
|
@@ -483,11 +603,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
483
603
|
lines.push(...new DynamicBorder().render(width));
|
|
484
604
|
|
|
485
605
|
const scrollView = new ScrollView(
|
|
486
|
-
|
|
606
|
+
contentLines.slice(this.#scrollOffset, this.#scrollOffset + this.#viewportHeight),
|
|
487
607
|
{
|
|
488
608
|
height: this.#viewportHeight,
|
|
489
609
|
scrollbar: "auto",
|
|
490
|
-
totalRows:
|
|
610
|
+
totalRows: contentLines.length,
|
|
491
611
|
theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
|
|
492
612
|
},
|
|
493
613
|
);
|
|
@@ -506,7 +626,9 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
506
626
|
const observed = this.#chatAgentId ? this.#observableFor(this.#chatAgentId) : undefined;
|
|
507
627
|
const statsLine = this.#buildStatsLine(observed);
|
|
508
628
|
if (statsLine) lines.push(` ${statsLine}`);
|
|
509
|
-
lines.push(
|
|
629
|
+
lines.push(
|
|
630
|
+
` ${theme.fg("dim", `Enter:send Esc:back ←←:parent ${this.#expandKeys[0] ?? "ctrl+o"}:expand empty input: j/k:scroll g/G:top/bottom`)}`,
|
|
631
|
+
);
|
|
510
632
|
return lines;
|
|
511
633
|
}
|
|
512
634
|
|
|
@@ -538,7 +660,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
538
660
|
return parts.join(theme.sep.dot);
|
|
539
661
|
}
|
|
540
662
|
|
|
541
|
-
/** Rebuild the chat header
|
|
663
|
+
/** Rebuild the chat header and sync transcript components from new entries */
|
|
542
664
|
#rebuildChatContent(): void {
|
|
543
665
|
const id = this.#chatAgentId;
|
|
544
666
|
const ref = id ? this.#registry.get(id) : undefined;
|
|
@@ -562,26 +684,24 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
562
684
|
this.#viewerHeaderLines.push(`${theme.bold(ref.id)} ${statusBadge(ref.status)}${kindTag}${modelLabel}`);
|
|
563
685
|
}
|
|
564
686
|
|
|
565
|
-
const contentLines: string[] = [];
|
|
566
|
-
this.#viewerEntries = [];
|
|
567
687
|
if (!ref) {
|
|
568
|
-
|
|
688
|
+
this.#chatPlaceholder = "Agent no longer registered.";
|
|
569
689
|
} else if (!this.#remote && !ref.sessionFile) {
|
|
570
|
-
|
|
690
|
+
this.#chatPlaceholder = "No session file available yet.";
|
|
571
691
|
} else if (!messageEntries) {
|
|
572
|
-
|
|
692
|
+
this.#chatPlaceholder = "Unable to read session file.";
|
|
573
693
|
} else if (messageEntries.length === 0) {
|
|
574
694
|
if (this.#remote && this.#remoteTranscriptUnavailable) {
|
|
575
|
-
|
|
695
|
+
this.#chatPlaceholder = "Transcript lives on the host — not available.";
|
|
576
696
|
} else if (this.#remote && !this.#transcriptCache) {
|
|
577
|
-
|
|
697
|
+
this.#chatPlaceholder = "Loading transcript from host…";
|
|
578
698
|
} else {
|
|
579
|
-
|
|
699
|
+
this.#chatPlaceholder = "No messages yet.";
|
|
580
700
|
}
|
|
581
701
|
} else {
|
|
582
|
-
this.#
|
|
702
|
+
this.#chatPlaceholder = undefined;
|
|
703
|
+
this.#syncChatComponents(messageEntries);
|
|
583
704
|
}
|
|
584
|
-
this.#renderedLines = contentLines;
|
|
585
705
|
}
|
|
586
706
|
|
|
587
707
|
#handleChatInput(keyData: string): void {
|
|
@@ -597,8 +717,31 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
597
717
|
return;
|
|
598
718
|
}
|
|
599
719
|
|
|
600
|
-
//
|
|
601
|
-
|
|
720
|
+
// Tool output expansion mirrors the main transcript's app.tools.expand toggle.
|
|
721
|
+
for (const key of this.#expandKeys) {
|
|
722
|
+
if (matchesKey(keyData, key)) {
|
|
723
|
+
this.#chatExpanded = !this.#chatExpanded;
|
|
724
|
+
for (const component of this.#chatExpandables) component.setExpanded(this.#chatExpanded);
|
|
725
|
+
this.#requestRender();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Double-tap left on an empty editor hops to the parent session —
|
|
731
|
+
// the inverse of the main editor's double-left opening the hub.
|
|
732
|
+
if (editorEmpty && matchesKey(keyData, "left")) {
|
|
733
|
+
const now = Date.now();
|
|
734
|
+
if (now - this.#lastLeftTap < LEFT_TAP_WINDOW_MS) {
|
|
735
|
+
this.#lastLeftTap = 0;
|
|
736
|
+
this.#openParent();
|
|
737
|
+
} else {
|
|
738
|
+
this.#lastLeftTap = now;
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Scrolling works while the input is empty; once the user starts
|
|
744
|
+
// typing, the editor owns every key.
|
|
602
745
|
if (editorEmpty && this.#handleViewerNavigation(keyData)) {
|
|
603
746
|
return;
|
|
604
747
|
}
|
|
@@ -607,11 +750,23 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
607
750
|
this.#requestRender();
|
|
608
751
|
}
|
|
609
752
|
|
|
753
|
+
/** Open the chat for the agent's parent, or close the hub when the parent is the main session. */
|
|
754
|
+
#openParent(): void {
|
|
755
|
+
const ref = this.#chatAgentId ? this.#registry.get(this.#chatAgentId) : undefined;
|
|
756
|
+
const parentId = ref?.parentId;
|
|
757
|
+
if (parentId && parentId !== MAIN_AGENT_ID && this.#registry.get(parentId)) {
|
|
758
|
+
this.openChat(parentId);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
this.#onDone();
|
|
762
|
+
}
|
|
763
|
+
|
|
610
764
|
#closeChat(): void {
|
|
611
765
|
this.#view = "table";
|
|
612
766
|
this.#chatAgentId = undefined;
|
|
613
767
|
this.#notice = undefined;
|
|
614
768
|
this.#detachLiveSession();
|
|
769
|
+
this.#resetChatLog();
|
|
615
770
|
this.#refreshRows();
|
|
616
771
|
this.#requestRender();
|
|
617
772
|
}
|
|
@@ -644,415 +799,391 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
644
799
|
this.#requestRender();
|
|
645
800
|
}
|
|
646
801
|
|
|
647
|
-
/**
|
|
802
|
+
/** Viewport scrolling for the chat transcript. Returns true when handled. */
|
|
648
803
|
#handleViewerNavigation(keyData: string): boolean {
|
|
649
|
-
const
|
|
650
|
-
|
|
804
|
+
const maxScroll = this.#lastMaxScroll;
|
|
805
|
+
const scrollBy = (delta: number) => {
|
|
806
|
+
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset + delta, maxScroll));
|
|
807
|
+
this.#wasAtBottom = this.#scrollOffset >= maxScroll;
|
|
808
|
+
this.#requestRender();
|
|
809
|
+
};
|
|
651
810
|
if (keyData === "j" || matchesSelectDown(keyData)) {
|
|
652
|
-
|
|
653
|
-
this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 1, entryCount - 1);
|
|
654
|
-
}
|
|
655
|
-
this.#rebuildAndScroll();
|
|
811
|
+
scrollBy(1);
|
|
656
812
|
return true;
|
|
657
813
|
}
|
|
658
814
|
if (keyData === "k" || matchesSelectUp(keyData)) {
|
|
659
|
-
|
|
660
|
-
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 1, 0);
|
|
661
|
-
}
|
|
662
|
-
this.#rebuildAndScroll();
|
|
815
|
+
scrollBy(-1);
|
|
663
816
|
return true;
|
|
664
817
|
}
|
|
665
818
|
if (matchesKey(keyData, "pageDown")) {
|
|
666
|
-
|
|
667
|
-
const prevIndex = this.#selectedEntryIndex;
|
|
668
|
-
this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 5, entryCount - 1);
|
|
669
|
-
if (this.#selectedEntryIndex === prevIndex) {
|
|
670
|
-
this.#scrollOffset = Math.min(
|
|
671
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
672
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
} else {
|
|
676
|
-
this.#scrollOffset = Math.min(
|
|
677
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
678
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
this.#rebuildAndScroll();
|
|
819
|
+
scrollBy(PAGE_SIZE);
|
|
682
820
|
return true;
|
|
683
821
|
}
|
|
684
822
|
if (matchesKey(keyData, "pageUp")) {
|
|
685
|
-
|
|
686
|
-
const prevIndex = this.#selectedEntryIndex;
|
|
687
|
-
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 5, 0);
|
|
688
|
-
if (this.#selectedEntryIndex === prevIndex) {
|
|
689
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
693
|
-
}
|
|
694
|
-
this.#rebuildAndScroll();
|
|
695
|
-
return true;
|
|
696
|
-
}
|
|
697
|
-
if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
|
|
698
|
-
if (entryCount > 0 && this.#selectedEntryIndex < entryCount) {
|
|
699
|
-
if (this.#expandedEntries.has(this.#selectedEntryIndex)) {
|
|
700
|
-
this.#expandedEntries.delete(this.#selectedEntryIndex);
|
|
701
|
-
} else {
|
|
702
|
-
this.#expandedEntries.add(this.#selectedEntryIndex);
|
|
703
|
-
}
|
|
704
|
-
this.#rebuildAndScroll();
|
|
705
|
-
}
|
|
823
|
+
scrollBy(-PAGE_SIZE);
|
|
706
824
|
return true;
|
|
707
825
|
}
|
|
708
826
|
if (keyData === "G") {
|
|
709
|
-
|
|
710
|
-
this.#
|
|
711
|
-
this.#
|
|
827
|
+
this.#scrollOffset = maxScroll;
|
|
828
|
+
this.#wasAtBottom = true;
|
|
829
|
+
this.#requestRender();
|
|
712
830
|
return true;
|
|
713
831
|
}
|
|
714
832
|
if (keyData === "g") {
|
|
715
|
-
this.#selectedEntryIndex = 0;
|
|
716
833
|
this.#scrollOffset = 0;
|
|
717
|
-
this.#
|
|
834
|
+
this.#wasAtBottom = maxScroll === 0;
|
|
835
|
+
this.#requestRender();
|
|
718
836
|
return true;
|
|
719
837
|
}
|
|
720
838
|
return false;
|
|
721
839
|
}
|
|
722
840
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
this.#rebuildChatContent();
|
|
728
|
-
this.#scrollToSelectedEntry();
|
|
729
|
-
this.#requestRender();
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
#scrollToSelectedEntry(): void {
|
|
733
|
-
if (this.#viewerEntries.length === 0) return;
|
|
734
|
-
const entry = this.#viewerEntries[this.#selectedEntryIndex];
|
|
735
|
-
if (!entry) return;
|
|
841
|
+
// ========================================================================
|
|
842
|
+
// Transcript assembly — the same components as the main session transcript
|
|
843
|
+
// (mirrors UiHelpers.renderSessionContext / addMessageToChat).
|
|
844
|
+
// ========================================================================
|
|
736
845
|
|
|
737
|
-
|
|
738
|
-
|
|
846
|
+
/** Tear down transcript components (sealing pending spinners) and reset build state. */
|
|
847
|
+
#resetChatLog(): void {
|
|
848
|
+
for (const pending of this.#chatPendingTools.values()) pending.seal();
|
|
849
|
+
this.#chatPendingTools.clear();
|
|
850
|
+
this.#chatReadArgs.clear();
|
|
851
|
+
this.#chatReadGroup = null;
|
|
852
|
+
this.#chatWaitingPoll = null;
|
|
853
|
+
this.#chatExpandables = [];
|
|
854
|
+
this.#chatLog.dispose();
|
|
855
|
+
this.#chatLog.clear();
|
|
856
|
+
this.#chatEntriesRef = undefined;
|
|
857
|
+
this.#chatBuiltCount = 0;
|
|
858
|
+
this.#chatPlaceholder = undefined;
|
|
859
|
+
}
|
|
739
860
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight);
|
|
746
|
-
}
|
|
747
|
-
} else {
|
|
748
|
-
// Entry fits in viewport: ensure it's fully visible
|
|
749
|
-
if (entryTop < this.#scrollOffset) {
|
|
750
|
-
this.#scrollOffset = Math.max(0, entryTop - 1);
|
|
751
|
-
}
|
|
752
|
-
if (entryBottom > this.#scrollOffset + this.#viewportHeight) {
|
|
753
|
-
this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight + 1);
|
|
754
|
-
}
|
|
861
|
+
/** Append components for entries not yet materialized. Rebuilds from scratch when the cache was replaced (agent switch, file rotation). */
|
|
862
|
+
#syncChatComponents(entries: SessionMessageEntry[]): void {
|
|
863
|
+
if (this.#chatEntriesRef !== entries) {
|
|
864
|
+
this.#resetChatLog();
|
|
865
|
+
this.#chatEntriesRef = entries;
|
|
755
866
|
}
|
|
867
|
+
for (let i = this.#chatBuiltCount; i < entries.length; i++) {
|
|
868
|
+
this.#appendChatMessage(entries[i].message);
|
|
869
|
+
}
|
|
870
|
+
this.#chatBuiltCount = entries.length;
|
|
756
871
|
}
|
|
757
872
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
#buildTranscriptLines(messageEntries: SessionMessageEntry[], lines: string[]): void {
|
|
763
|
-
// Build a tool call ID -> tool result map
|
|
764
|
-
const toolResults = new Map<string, ToolResultMessage>();
|
|
765
|
-
for (const entry of messageEntries) {
|
|
766
|
-
if (entry.message.role === "toolResult") {
|
|
767
|
-
toolResults.set(entry.message.toolCallId, entry.message);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
873
|
+
#trackExpandable(component: { setExpanded(expanded: boolean): void }): void {
|
|
874
|
+
component.setExpanded(this.#chatExpanded);
|
|
875
|
+
this.#chatExpandables.push(component);
|
|
876
|
+
}
|
|
770
877
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const startLine = lines.length;
|
|
779
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
780
|
-
const cursor = isSelected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
781
|
-
lines.push("");
|
|
782
|
-
const errorLines = msg.errorMessage.split("\n");
|
|
783
|
-
const maxWidth = contentWidth();
|
|
784
|
-
lines.push(`${cursor} ${theme.fg("error", `✗ Error: ${sanitizeLine(errorLines[0], maxWidth)}`)}`);
|
|
785
|
-
for (let i = 1; i < errorLines.length; i++) {
|
|
786
|
-
lines.push(`${INDENT}${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
|
|
787
|
-
}
|
|
788
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "text" });
|
|
789
|
-
entryIndex++;
|
|
790
|
-
} else {
|
|
791
|
-
for (const content of msg.content) {
|
|
792
|
-
if (content.type === "thinking" && content.thinking.trim()) {
|
|
793
|
-
const startLine = lines.length;
|
|
794
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
795
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
796
|
-
this.#renderThinkingLines(lines, content.thinking.trim(), isExpanded, isSelected);
|
|
797
|
-
this.#viewerEntries.push({
|
|
798
|
-
lineStart: startLine,
|
|
799
|
-
lineCount: lines.length - startLine,
|
|
800
|
-
kind: "thinking",
|
|
801
|
-
});
|
|
802
|
-
entryIndex++;
|
|
803
|
-
} else if (content.type === "text" && content.text.trim()) {
|
|
804
|
-
const startLine = lines.length;
|
|
805
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
806
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
807
|
-
this.#renderTextLines(lines, content.text.trim(), isExpanded, isSelected);
|
|
808
|
-
this.#viewerEntries.push({
|
|
809
|
-
lineStart: startLine,
|
|
810
|
-
lineCount: lines.length - startLine,
|
|
811
|
-
kind: "text",
|
|
812
|
-
});
|
|
813
|
-
entryIndex++;
|
|
814
|
-
} else if (content.type === "toolCall") {
|
|
815
|
-
const startLine = lines.length;
|
|
816
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
817
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
818
|
-
const result = toolResults.get(content.id);
|
|
819
|
-
this.#renderToolCallLines(lines, content, result, isExpanded, isSelected);
|
|
820
|
-
this.#viewerEntries.push({
|
|
821
|
-
lineStart: startLine,
|
|
822
|
-
lineCount: lines.length - startLine,
|
|
823
|
-
kind: "toolCall",
|
|
824
|
-
});
|
|
825
|
-
entryIndex++;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
} else if (msg.role === "user" || msg.role === "developer") {
|
|
830
|
-
const text =
|
|
831
|
-
typeof msg.content === "string"
|
|
832
|
-
? msg.content
|
|
833
|
-
: msg.content
|
|
834
|
-
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
835
|
-
.map(b => b.text)
|
|
836
|
-
.join("\n");
|
|
837
|
-
if (text.trim()) {
|
|
838
|
-
const startLine = lines.length;
|
|
839
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
840
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
841
|
-
const label = msg.role === "developer" ? "System" : "User";
|
|
842
|
-
const cursor = isSelected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
843
|
-
lines.push("");
|
|
844
|
-
if (isExpanded) {
|
|
845
|
-
lines.push(`${cursor} ${theme.fg("dim", `[${label}]`)}`);
|
|
846
|
-
const mdLines = this.#renderMarkdownToLines(text.trim());
|
|
847
|
-
for (const ml of mdLines) {
|
|
848
|
-
lines.push(ml);
|
|
849
|
-
}
|
|
850
|
-
} else {
|
|
851
|
-
const firstLine = text.trim().split("\n")[0];
|
|
852
|
-
const totalLines = text.trim().split("\n").length;
|
|
853
|
-
const hint = totalLines > 1 ? theme.fg("dim", ` (${totalLines} lines)`) : "";
|
|
854
|
-
lines.push(
|
|
855
|
-
`${cursor} ${theme.fg("dim", `[${label}]`)} ${theme.fg("muted", sanitizeLine(firstLine, TRUNCATE_LENGTHS.TITLE))}${hint}`,
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "user" });
|
|
859
|
-
entryIndex++;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
878
|
+
/** A `job` poll showing all-running is displaced by the next `job` call (mirrors the rebuild path). */
|
|
879
|
+
#resolveWaitingPoll(nextToolName?: string): void {
|
|
880
|
+
const previous = this.#chatWaitingPoll;
|
|
881
|
+
if (!previous) return;
|
|
882
|
+
this.#chatWaitingPoll = null;
|
|
883
|
+
if (nextToolName === "job" && previous.isDisplaceableBlock()) {
|
|
884
|
+
this.#chatLog.removeChild(previous);
|
|
862
885
|
}
|
|
886
|
+
previous.seal();
|
|
863
887
|
}
|
|
864
888
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
889
|
+
#ensureReadGroup(): ReadToolGroupComponent {
|
|
890
|
+
if (!this.#chatReadGroup) {
|
|
891
|
+
this.#chatReadGroup = new ReadToolGroupComponent({
|
|
892
|
+
showContentPreview: settings.get("read.toolResultPreview"),
|
|
893
|
+
});
|
|
894
|
+
this.#trackExpandable(this.#chatReadGroup);
|
|
895
|
+
this.#chatLog.addChild(this.#chatReadGroup);
|
|
896
|
+
}
|
|
897
|
+
return this.#chatReadGroup;
|
|
871
898
|
}
|
|
872
899
|
|
|
873
|
-
#
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
900
|
+
#appendChatMessage(message: AgentMessage): void {
|
|
901
|
+
switch (message.role) {
|
|
902
|
+
case "assistant":
|
|
903
|
+
this.#appendAssistantMessage(message);
|
|
904
|
+
break;
|
|
905
|
+
case "toolResult":
|
|
906
|
+
this.#appendToolResult(message);
|
|
907
|
+
break;
|
|
908
|
+
case "user":
|
|
909
|
+
case "developer": {
|
|
910
|
+
// A user prompt closes the poll-displacement window, same as the live path.
|
|
911
|
+
if (message.role === "user") this.#resolveWaitingPoll();
|
|
912
|
+
const textContent =
|
|
913
|
+
message.role !== "user"
|
|
914
|
+
? ""
|
|
915
|
+
: typeof message.content === "string"
|
|
916
|
+
? message.content
|
|
917
|
+
: message.content
|
|
918
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
919
|
+
.map(block => block.text)
|
|
920
|
+
.join("");
|
|
921
|
+
if (textContent) {
|
|
922
|
+
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
923
|
+
this.#chatLog.addChild(new UserMessageComponent(textContent, isSynthetic));
|
|
924
|
+
}
|
|
925
|
+
break;
|
|
889
926
|
}
|
|
890
|
-
|
|
891
|
-
|
|
927
|
+
case "bashExecution": {
|
|
928
|
+
const component = new BashExecutionComponent(message.command, this.#ui, message.excludeFromContext);
|
|
929
|
+
if (message.output) component.appendOutput(message.output);
|
|
930
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
931
|
+
this.#chatLog.addChild(component);
|
|
932
|
+
break;
|
|
892
933
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
934
|
+
case "pythonExecution": {
|
|
935
|
+
const component = new EvalExecutionComponent(message.code, this.#ui, message.excludeFromContext);
|
|
936
|
+
if (message.output) component.appendOutput(message.output);
|
|
937
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
938
|
+
this.#chatLog.addChild(component);
|
|
939
|
+
break;
|
|
899
940
|
}
|
|
900
|
-
|
|
901
|
-
|
|
941
|
+
case "hookMessage":
|
|
942
|
+
case "custom":
|
|
943
|
+
this.#appendCustomMessage(message);
|
|
944
|
+
break;
|
|
945
|
+
case "compactionSummary": {
|
|
946
|
+
const component = new CompactionSummaryMessageComponent(message);
|
|
947
|
+
this.#trackExpandable(component);
|
|
948
|
+
this.#chatLog.addChild(component);
|
|
949
|
+
break;
|
|
902
950
|
}
|
|
951
|
+
case "branchSummary": {
|
|
952
|
+
const component = new BranchSummaryMessageComponent(message);
|
|
953
|
+
this.#trackExpandable(component);
|
|
954
|
+
this.#chatLog.addChild(component);
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
case "fileMention": {
|
|
958
|
+
const block = new TranscriptBlock();
|
|
959
|
+
for (const file of message.files) {
|
|
960
|
+
let suffix: string;
|
|
961
|
+
if (file.skippedReason === "tooLarge") {
|
|
962
|
+
const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
|
|
963
|
+
suffix = `(skipped: ${size})`;
|
|
964
|
+
} else {
|
|
965
|
+
suffix = file.image
|
|
966
|
+
? "(image)"
|
|
967
|
+
: file.lineCount === undefined
|
|
968
|
+
? "(unknown lines)"
|
|
969
|
+
: `(${file.lineCount} lines)`;
|
|
970
|
+
}
|
|
971
|
+
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
972
|
+
"accent",
|
|
973
|
+
file.path,
|
|
974
|
+
)} ${theme.fg("dim", suffix)}`;
|
|
975
|
+
block.addChild(new Text(text, 0, 0));
|
|
976
|
+
}
|
|
977
|
+
if (block.children.length > 0) this.#chatLog.addChild(block);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
default:
|
|
981
|
+
message satisfies never;
|
|
903
982
|
}
|
|
904
983
|
}
|
|
905
984
|
|
|
906
|
-
#
|
|
907
|
-
const
|
|
985
|
+
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
986
|
+
const assistantComponent = new AssistantMessageComponent(message, this.#hideThinkingBlock?.() ?? false, () =>
|
|
987
|
+
this.#requestRender(),
|
|
988
|
+
);
|
|
989
|
+
assistantComponent.setUsageInfo(message.usage);
|
|
990
|
+
this.#chatLog.addChild(assistantComponent);
|
|
908
991
|
|
|
909
|
-
|
|
910
|
-
|
|
992
|
+
const hasVisibleAssistantContent = message.content.some(
|
|
993
|
+
content =>
|
|
994
|
+
(content.type === "text" && content.text.trim().length > 0) ||
|
|
995
|
+
(content.type === "thinking" && content.thinking.trim().length > 0),
|
|
996
|
+
);
|
|
997
|
+
if (hasVisibleAssistantContent) {
|
|
998
|
+
// New visible turn content closes the current read run (mirrors rebuild).
|
|
999
|
+
this.#chatReadGroup?.seal();
|
|
1000
|
+
this.#chatReadGroup = null;
|
|
1001
|
+
}
|
|
911
1002
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1003
|
+
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
|
|
1004
|
+
const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
1005
|
+
const errorMessage = hasErrorStop
|
|
1006
|
+
? message.stopReason === "aborted"
|
|
1007
|
+
? resolveAbortLabel(message.errorMessage)
|
|
1008
|
+
: message.errorMessage || "Error"
|
|
1009
|
+
: null;
|
|
1010
|
+
|
|
1011
|
+
for (const content of message.content) {
|
|
1012
|
+
if (content.type !== "toolCall") continue;
|
|
1013
|
+
this.#resolveWaitingPoll(content.name);
|
|
1014
|
+
|
|
1015
|
+
if (
|
|
1016
|
+
content.name === "read" &&
|
|
1017
|
+
readArgsHaveTarget(content.arguments) &&
|
|
1018
|
+
!readArgsTargetInternalUrl(content.arguments)
|
|
1019
|
+
) {
|
|
1020
|
+
if (hasErrorStop && errorMessage) {
|
|
1021
|
+
const group = this.#ensureReadGroup();
|
|
1022
|
+
group.updateArgs(content.arguments, content.id);
|
|
1023
|
+
group.updateResult(
|
|
1024
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
1025
|
+
false,
|
|
1026
|
+
content.id,
|
|
1027
|
+
);
|
|
1028
|
+
} else {
|
|
1029
|
+
const normalizedArgs =
|
|
1030
|
+
content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
|
|
1031
|
+
? (content.arguments as Record<string, unknown>)
|
|
1032
|
+
: {};
|
|
1033
|
+
this.#chatReadArgs.set(content.id, normalizedArgs);
|
|
1034
|
+
}
|
|
1035
|
+
continue;
|
|
925
1036
|
}
|
|
926
|
-
|
|
927
|
-
|
|
1037
|
+
|
|
1038
|
+
this.#chatReadGroup?.seal();
|
|
1039
|
+
this.#chatReadGroup = null;
|
|
1040
|
+
const component = new ToolExecutionComponent(
|
|
1041
|
+
content.name,
|
|
1042
|
+
content.arguments,
|
|
1043
|
+
{
|
|
1044
|
+
// Images can't be sliced through the scroll viewport; keep them off.
|
|
1045
|
+
showImages: false,
|
|
1046
|
+
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
1047
|
+
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
1048
|
+
liveRegion: this.#chatLog,
|
|
1049
|
+
},
|
|
1050
|
+
this.#getTool?.(content.name),
|
|
1051
|
+
this.#ui,
|
|
1052
|
+
this.#cwd,
|
|
1053
|
+
content.id,
|
|
1054
|
+
);
|
|
1055
|
+
this.#trackExpandable(component);
|
|
1056
|
+
this.#chatLog.addChild(component);
|
|
1057
|
+
|
|
1058
|
+
if (hasErrorStop && errorMessage) {
|
|
1059
|
+
component.updateResult(
|
|
1060
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
1061
|
+
false,
|
|
1062
|
+
content.id,
|
|
1063
|
+
);
|
|
1064
|
+
} else {
|
|
1065
|
+
this.#chatPendingTools.set(content.id, component);
|
|
928
1066
|
}
|
|
929
1067
|
}
|
|
930
1068
|
}
|
|
931
1069
|
|
|
932
|
-
#
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const argSummary = this.#formatToolArgs(call.name, call.arguments);
|
|
948
|
-
if (argSummary) {
|
|
949
|
-
lines.push(`${INDENT}${theme.fg("dim", sanitizeLine(argSummary, contentWidth()))}`);
|
|
1070
|
+
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|
|
1071
|
+
const pending = this.#chatPendingTools.get(message.toolCallId);
|
|
1072
|
+
const isReadGroupResult = message.toolName === "read" && (!pending || pending instanceof ReadToolGroupComponent);
|
|
1073
|
+
if (isReadGroupResult) {
|
|
1074
|
+
let component = pending;
|
|
1075
|
+
if (!component) {
|
|
1076
|
+
const group = this.#ensureReadGroup();
|
|
1077
|
+
const args = this.#chatReadArgs.get(message.toolCallId);
|
|
1078
|
+
if (args) group.updateArgs(args, message.toolCallId);
|
|
1079
|
+
component = group;
|
|
1080
|
+
}
|
|
1081
|
+
component.updateResult(message, false, message.toolCallId);
|
|
1082
|
+
this.#chatPendingTools.delete(message.toolCallId);
|
|
1083
|
+
this.#chatReadArgs.delete(message.toolCallId);
|
|
1084
|
+
return;
|
|
950
1085
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1086
|
+
if (!pending) return;
|
|
1087
|
+
pending.updateResult(message, false, message.toolCallId);
|
|
1088
|
+
this.#chatPendingTools.delete(message.toolCallId);
|
|
1089
|
+
if (message.toolName === "job" && pending instanceof ToolExecutionComponent && pending.isDisplaceableBlock()) {
|
|
1090
|
+
this.#chatWaitingPoll = pending;
|
|
955
1091
|
}
|
|
956
1092
|
}
|
|
957
1093
|
|
|
958
|
-
#
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1094
|
+
#appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
|
|
1095
|
+
if (!message.display) return;
|
|
1096
|
+
if (message.customType === "async-result") {
|
|
1097
|
+
const details = (
|
|
1098
|
+
message as CustomMessage<{
|
|
1099
|
+
jobId?: string;
|
|
1100
|
+
type?: "bash" | "task";
|
|
1101
|
+
label?: string;
|
|
1102
|
+
durationMs?: number;
|
|
1103
|
+
jobs?: Array<{ jobId?: string; type?: "bash" | "task"; label?: string; durationMs?: number }>;
|
|
1104
|
+
}>
|
|
1105
|
+
).details;
|
|
1106
|
+
const jobs =
|
|
1107
|
+
details?.jobs && details.jobs.length > 0
|
|
1108
|
+
? details.jobs
|
|
1109
|
+
: [
|
|
1110
|
+
{
|
|
1111
|
+
jobId: details?.jobId,
|
|
1112
|
+
type: details?.type,
|
|
1113
|
+
label: details?.label,
|
|
1114
|
+
durationMs: details?.durationMs,
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
const block = new TranscriptBlock();
|
|
1118
|
+
for (const job of jobs) {
|
|
1119
|
+
const jobId = job.jobId ?? "unknown";
|
|
1120
|
+
const typeLabel = job.type ? `[${job.type}]` : "[job]";
|
|
1121
|
+
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
1122
|
+
const line = [
|
|
1123
|
+
theme.fg("success", `${theme.status.done} Background job completed`),
|
|
1124
|
+
theme.fg("dim", typeLabel),
|
|
1125
|
+
theme.fg("accent", jobId),
|
|
1126
|
+
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
1127
|
+
]
|
|
1128
|
+
.filter(Boolean)
|
|
1129
|
+
.join(" ");
|
|
1130
|
+
block.addChild(new Text(line, 1, 0));
|
|
974
1131
|
}
|
|
1132
|
+
this.#chatLog.addChild(block);
|
|
975
1133
|
return;
|
|
976
1134
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1135
|
+
if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
|
|
1136
|
+
const details = (message as CustomMessage<{ files?: LateDiagnosticsFile[] }>).details;
|
|
1137
|
+
const component = new LateDiagnosticsMessageComponent(details?.files ?? []);
|
|
1138
|
+
this.#trackExpandable(component);
|
|
1139
|
+
this.#chatLog.addChild(component);
|
|
980
1140
|
return;
|
|
981
1141
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const maxLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
985
|
-
|
|
986
|
-
// Status line
|
|
987
|
-
const statusPrefix = `${INDENT}${theme.fg("success", "✓")}`;
|
|
988
|
-
|
|
989
|
-
if (resultLines.length === 1 && text.length < TRUNCATE_LENGTHS.LONG) {
|
|
990
|
-
lines.push(`${statusPrefix} ${theme.fg("dim", sanitizeLine(text))}`);
|
|
1142
|
+
if (message.customType === COLLAB_PROMPT_MESSAGE_TYPE) {
|
|
1143
|
+
this.#chatLog.addChild(new CollabPromptMessageComponent(message as CustomMessage<CollabPromptDetails>));
|
|
991
1144
|
return;
|
|
992
1145
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
if (resultLines.length > maxLines) {
|
|
1000
|
-
lines.push(`${INDENT} ${theme.fg("dim", `... ${resultLines.length - maxLines} more`)}`);
|
|
1146
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
1147
|
+
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
1148
|
+
this.#trackExpandable(component);
|
|
1149
|
+
this.#chatLog.addChild(component);
|
|
1150
|
+
return;
|
|
1001
1151
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
case "lsp":
|
|
1032
|
-
return [args.action, args.file, args.symbol].filter(Boolean).join(" ");
|
|
1033
|
-
case "ast_grep":
|
|
1034
|
-
case "ast_edit":
|
|
1035
|
-
return args.path ? `path: ${args.path}` : "";
|
|
1036
|
-
case "task": {
|
|
1037
|
-
const target = typeof args.agent === "string" ? args.agent : "";
|
|
1038
|
-
const id = typeof args.id === "string" && args.id ? ` ${args.id}` : "";
|
|
1039
|
-
return `${target}${id}`.trim();
|
|
1040
|
-
}
|
|
1041
|
-
default: {
|
|
1042
|
-
const parts: string[] = [];
|
|
1043
|
-
let total = 0;
|
|
1044
|
-
for (const key in args) {
|
|
1045
|
-
if (key.startsWith("_")) continue;
|
|
1046
|
-
const value = args[key];
|
|
1047
|
-
const v = typeof value === "string" ? value : JSON.stringify(value);
|
|
1048
|
-
const entry = `${key}: ${replaceTabs(v ?? "")}`;
|
|
1049
|
-
if (total + entry.length > MAX_TOOL_ARGS_CHARS) break;
|
|
1050
|
-
parts.push(entry);
|
|
1051
|
-
total += entry.length;
|
|
1052
|
-
}
|
|
1053
|
-
return parts.join(", ");
|
|
1054
|
-
}
|
|
1152
|
+
if (
|
|
1153
|
+
message.customType === "irc:incoming" ||
|
|
1154
|
+
message.customType === "irc:autoreply" ||
|
|
1155
|
+
message.customType === "irc:relay"
|
|
1156
|
+
) {
|
|
1157
|
+
const details = (
|
|
1158
|
+
message as CustomMessage<{ from?: string; to?: string; message?: string; body?: string; replyTo?: string }>
|
|
1159
|
+
).details;
|
|
1160
|
+
const kind =
|
|
1161
|
+
message.customType === "irc:incoming"
|
|
1162
|
+
? ("incoming" as const)
|
|
1163
|
+
: message.customType === "irc:autoreply"
|
|
1164
|
+
? ("autoreply" as const)
|
|
1165
|
+
: ("relay" as const);
|
|
1166
|
+
const card = createIrcMessageCard(
|
|
1167
|
+
{
|
|
1168
|
+
kind,
|
|
1169
|
+
from: details?.from,
|
|
1170
|
+
to: details?.to,
|
|
1171
|
+
body: kind === "incoming" ? details?.message : details?.body,
|
|
1172
|
+
replyTo: details?.replyTo,
|
|
1173
|
+
timestamp: message.timestamp,
|
|
1174
|
+
},
|
|
1175
|
+
() => this.#chatExpanded,
|
|
1176
|
+
theme,
|
|
1177
|
+
);
|
|
1178
|
+
this.#chatLog.addChild(card);
|
|
1179
|
+
return;
|
|
1055
1180
|
}
|
|
1181
|
+
const component = new CustomMessageComponent(
|
|
1182
|
+
message as CustomMessage<unknown>,
|
|
1183
|
+
this.#getMessageRenderer?.(message.customType),
|
|
1184
|
+
);
|
|
1185
|
+
this.#trackExpandable(component);
|
|
1186
|
+
this.#chatLog.addChild(component);
|
|
1056
1187
|
}
|
|
1057
1188
|
|
|
1058
1189
|
#loadTranscript(sessionFile: string): SessionMessageEntry[] | null {
|