@oh-my-pi/pi-coding-agent 15.11.7 → 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 +63 -1
- package/dist/cli.js +8106 -7708
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/collab/crypto.d.ts +7 -0
- package/dist/types/collab/guest.d.ts +23 -0
- package/dist/types/collab/host.d.ts +29 -0
- package/dist/types/collab/protocol.d.ts +113 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +60 -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/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +32 -1
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/status-line/component.d.ts +10 -2
- package/dist/types/modes/components/status-line/types.d.ts +11 -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 +16 -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 +20 -0
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +14 -13
- package/scripts/bench-guard.ts +71 -0
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -0
- package/src/collab/crypto.ts +63 -0
- package/src/collab/guest.ts +450 -0
- package/src/collab/host.ts +556 -0
- package/src/collab/protocol.ts +232 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +67 -5
- 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/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +74 -73
- package/src/main.ts +33 -11
- package/src/modes/components/agent-hub.ts +659 -431
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/status-line/component.ts +59 -6
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +18 -1
- package/src/modes/components/status-line/types.ts +12 -0
- package/src/modes/components/tips.txt +4 -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 +175 -9
- package/src/modes/controllers/selector-controller.ts +13 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +56 -6
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +20 -0
- package/src/modes/utils/ui-helpers.ts +23 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +82 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +261 -24
- 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/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
- 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,47 @@ 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
|
+
|
|
128
|
+
/** Guest-side proxy for hub actions executed on the collab host. */
|
|
129
|
+
export interface AgentHubRemote {
|
|
130
|
+
chat(id: string, text: string): void;
|
|
131
|
+
kill(id: string): void;
|
|
132
|
+
revive(id: string): void;
|
|
133
|
+
/** Mirrors readFileIncremental: text from fromByte (complete JSONL lines), newSize = next fromByte base; null = unavailable. */
|
|
134
|
+
readTranscript(id: string, fromByte: number): Promise<{ text: string; newSize: number } | null>;
|
|
135
|
+
}
|
|
136
|
+
|
|
84
137
|
export interface AgentHubDeps {
|
|
85
138
|
/** Progress/status snapshot source (task lifecycle + progress channels). */
|
|
86
139
|
observers: SessionObserverRegistry;
|
|
@@ -94,6 +147,24 @@ export interface AgentHubDeps {
|
|
|
94
147
|
lifecycle?: AgentLifecycleManager;
|
|
95
148
|
/** Injectable for tests; defaults to the process-global bus. */
|
|
96
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;
|
|
166
|
+
/** Collab guest: route actions/transcripts to the host instead of local sessions. */
|
|
167
|
+
remote?: AgentHubRemote;
|
|
97
168
|
}
|
|
98
169
|
|
|
99
170
|
export class AgentHubOverlayComponent extends Container {
|
|
@@ -106,6 +177,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
106
177
|
#hubKeys: KeyId[];
|
|
107
178
|
#unsubscribers: Array<() => void> = [];
|
|
108
179
|
#ageTimer: NodeJS.Timeout | undefined;
|
|
180
|
+
#remote: AgentHubRemote | undefined;
|
|
181
|
+
#remoteFetchInFlight = false;
|
|
182
|
+
/** Invalidates stale in-flight fetch callbacks after openChat resets the cache. */
|
|
183
|
+
#remoteFetchToken = 0;
|
|
184
|
+
#remoteTranscriptUnavailable = false;
|
|
109
185
|
|
|
110
186
|
// Table state
|
|
111
187
|
#view: "table" | "chat" = "table";
|
|
@@ -121,16 +197,33 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
121
197
|
#chatRefreshTimer: NodeJS.Timeout | undefined;
|
|
122
198
|
#transcriptCache: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string } | undefined;
|
|
123
199
|
|
|
124
|
-
//
|
|
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
|
|
125
221
|
#scrollOffset = 0;
|
|
126
|
-
#
|
|
222
|
+
#lastMaxScroll = 0;
|
|
127
223
|
#viewportHeight = 20;
|
|
128
224
|
#wasAtBottom = true;
|
|
129
|
-
#viewerEntries: ViewerEntry[] = [];
|
|
130
|
-
#selectedEntryIndex = 0;
|
|
131
|
-
#expandedEntries = new Set<number>();
|
|
132
225
|
#viewerHeaderLines: string[] = [];
|
|
133
|
-
#
|
|
226
|
+
#lastLeftTap = 0;
|
|
134
227
|
|
|
135
228
|
constructor(deps: AgentHubDeps) {
|
|
136
229
|
super();
|
|
@@ -143,6 +236,19 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
143
236
|
this.#onDone = deps.onDone;
|
|
144
237
|
this.#requestRender = deps.requestRender;
|
|
145
238
|
this.#hubKeys = deps.hubKeys;
|
|
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;
|
|
146
252
|
|
|
147
253
|
this.#editor = new Editor(getEditorTheme());
|
|
148
254
|
this.#editor.setMaxHeight(4);
|
|
@@ -153,6 +259,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
153
259
|
this.#ageTimer = setInterval(() => this.#requestRender(), AGE_TICK_MS);
|
|
154
260
|
this.#ageTimer.unref?.();
|
|
155
261
|
|
|
262
|
+
if (!this.#remote) registerPersistedSubagents(this.#registry, deps.sessionFile);
|
|
156
263
|
this.#refreshRows();
|
|
157
264
|
}
|
|
158
265
|
|
|
@@ -168,6 +275,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
168
275
|
this.#chatRefreshTimer = undefined;
|
|
169
276
|
}
|
|
170
277
|
this.#detachLiveSession();
|
|
278
|
+
this.#resetChatLog();
|
|
171
279
|
}
|
|
172
280
|
|
|
173
281
|
override render(width: number): readonly string[] {
|
|
@@ -196,18 +304,16 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
196
304
|
this.#chatAgentId = id;
|
|
197
305
|
this.#notice = undefined;
|
|
198
306
|
this.#transcriptCache = undefined;
|
|
307
|
+
this.#remoteTranscriptUnavailable = false;
|
|
308
|
+
this.#remoteFetchInFlight = false;
|
|
309
|
+
this.#remoteFetchToken++;
|
|
310
|
+
this.#resetChatLog();
|
|
199
311
|
this.#scrollOffset = 0;
|
|
200
|
-
this.#selectedEntryIndex = 0;
|
|
201
|
-
this.#expandedEntries.clear();
|
|
202
312
|
this.#wasAtBottom = true;
|
|
313
|
+
this.#lastLeftTap = 0;
|
|
203
314
|
this.#editor.setText("");
|
|
204
315
|
this.#attachLiveSession();
|
|
205
316
|
this.#rebuildChatContent();
|
|
206
|
-
// Auto-scroll to bottom and select last entry on open
|
|
207
|
-
if (this.#viewerEntries.length > 0) {
|
|
208
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
209
|
-
this.#rebuildChatContent();
|
|
210
|
-
}
|
|
211
317
|
this.#requestRender();
|
|
212
318
|
}
|
|
213
319
|
|
|
@@ -238,6 +344,8 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
238
344
|
|
|
239
345
|
/** Subscribe to the chat agent's live session (if any) for transcript refreshes. Idempotent per session. */
|
|
240
346
|
#attachLiveSession(): void {
|
|
347
|
+
// Remote refs carry no live session handle; refreshes come from observer onChange.
|
|
348
|
+
if (this.#remote) return;
|
|
241
349
|
const session = this.#chatAgentId ? (this.#registry.get(this.#chatAgentId)?.session ?? undefined) : undefined;
|
|
242
350
|
if (session === this.#attachedSession) return;
|
|
243
351
|
this.#detachLiveSession();
|
|
@@ -261,12 +369,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
261
369
|
this.#chatRefreshTimer = setTimeout(() => {
|
|
262
370
|
this.#chatRefreshTimer = undefined;
|
|
263
371
|
if (this.#view !== "chat") return;
|
|
264
|
-
// Keep auto-scrolling to bottom unless the user navigated away
|
|
265
|
-
this.#wasAtBottom = this.#selectedEntryIndex >= this.#viewerEntries.length - 1;
|
|
266
372
|
this.#rebuildChatContent();
|
|
267
|
-
if (this.#wasAtBottom && this.#viewerEntries.length > 0) {
|
|
268
|
-
this.#selectedEntryIndex = this.#viewerEntries.length - 1;
|
|
269
|
-
}
|
|
270
373
|
this.#requestRender();
|
|
271
374
|
}, CHAT_REFRESH_DEBOUNCE_MS);
|
|
272
375
|
this.#chatRefreshTimer.unref?.();
|
|
@@ -313,7 +416,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
313
416
|
lines.push(` ${theme.fg("error", sanitizeLine(this.#notice, Math.max(10, width - 2)))}`);
|
|
314
417
|
}
|
|
315
418
|
lines.push("");
|
|
316
|
-
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")}`);
|
|
317
420
|
lines.push(...new DynamicBorder().render(width));
|
|
318
421
|
return lines;
|
|
319
422
|
}
|
|
@@ -353,6 +456,16 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
353
456
|
this.#onDone();
|
|
354
457
|
return;
|
|
355
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
|
+
}
|
|
356
469
|
if (keyData === "j" || matchesSelectDown(keyData)) {
|
|
357
470
|
if (this.#rows.length > 0) {
|
|
358
471
|
this.#selectedRow = Math.min(this.#selectedRow + 1, this.#rows.length - 1);
|
|
@@ -369,7 +482,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
369
482
|
}
|
|
370
483
|
if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
|
|
371
484
|
const selected = this.#rows[this.#selectedRow];
|
|
372
|
-
if (selected) this
|
|
485
|
+
if (selected) this.#activateAgent(selected);
|
|
373
486
|
return;
|
|
374
487
|
}
|
|
375
488
|
if (keyData === "r") {
|
|
@@ -382,6 +495,30 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
382
495
|
}
|
|
383
496
|
}
|
|
384
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
|
+
|
|
385
522
|
#reviveSelected(): void {
|
|
386
523
|
const ref = this.#rows[this.#selectedRow];
|
|
387
524
|
if (!ref) return;
|
|
@@ -391,6 +528,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
391
528
|
return;
|
|
392
529
|
}
|
|
393
530
|
this.#notice = undefined;
|
|
531
|
+
if (this.#remote) {
|
|
532
|
+
this.#remote.revive(ref.id);
|
|
533
|
+
this.#requestRender();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
394
536
|
// Fire-and-forget; failures surface as an inline notice
|
|
395
537
|
this.#lifecycle()
|
|
396
538
|
.ensureLive(ref.id)
|
|
@@ -405,6 +547,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
405
547
|
const ref = this.#rows[this.#selectedRow];
|
|
406
548
|
if (!ref) return;
|
|
407
549
|
this.#notice = undefined;
|
|
550
|
+
if (this.#remote) {
|
|
551
|
+
this.#remote.kill(ref.id);
|
|
552
|
+
this.#refreshRows();
|
|
553
|
+
this.#requestRender();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
408
556
|
void (async () => {
|
|
409
557
|
try {
|
|
410
558
|
if (ref.status === "running" && ref.session) {
|
|
@@ -438,7 +586,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
438
586
|
const footerChrome = editorLines.length + footerLines.length + (noticeLine ? 1 : 0) + 1;
|
|
439
587
|
this.#viewportHeight = Math.max(5, termHeight - headerChrome - footerChrome);
|
|
440
588
|
|
|
441
|
-
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;
|
|
442
595
|
if (this.#wasAtBottom) this.#scrollOffset = maxScroll;
|
|
443
596
|
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScroll));
|
|
444
597
|
|
|
@@ -450,11 +603,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
450
603
|
lines.push(...new DynamicBorder().render(width));
|
|
451
604
|
|
|
452
605
|
const scrollView = new ScrollView(
|
|
453
|
-
|
|
606
|
+
contentLines.slice(this.#scrollOffset, this.#scrollOffset + this.#viewportHeight),
|
|
454
607
|
{
|
|
455
608
|
height: this.#viewportHeight,
|
|
456
609
|
scrollbar: "auto",
|
|
457
|
-
totalRows:
|
|
610
|
+
totalRows: contentLines.length,
|
|
458
611
|
theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
|
|
459
612
|
},
|
|
460
613
|
);
|
|
@@ -473,7 +626,9 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
473
626
|
const observed = this.#chatAgentId ? this.#observableFor(this.#chatAgentId) : undefined;
|
|
474
627
|
const statsLine = this.#buildStatsLine(observed);
|
|
475
628
|
if (statsLine) lines.push(` ${statsLine}`);
|
|
476
|
-
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
|
+
);
|
|
477
632
|
return lines;
|
|
478
633
|
}
|
|
479
634
|
|
|
@@ -505,14 +660,17 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
505
660
|
return parts.join(theme.sep.dot);
|
|
506
661
|
}
|
|
507
662
|
|
|
508
|
-
/** Rebuild the chat header
|
|
663
|
+
/** Rebuild the chat header and sync transcript components from new entries */
|
|
509
664
|
#rebuildChatContent(): void {
|
|
510
665
|
const id = this.#chatAgentId;
|
|
511
666
|
const ref = id ? this.#registry.get(id) : undefined;
|
|
512
667
|
|
|
513
668
|
// Load transcript first so model info is available for the header
|
|
514
669
|
let messageEntries: SessionMessageEntry[] | null = null;
|
|
515
|
-
if (
|
|
670
|
+
if (this.#remote) {
|
|
671
|
+
if (id) this.#fetchRemoteTranscript(id);
|
|
672
|
+
messageEntries = this.#transcriptCache?.entries ?? [];
|
|
673
|
+
} else if (ref?.sessionFile) {
|
|
516
674
|
messageEntries = this.#loadTranscript(ref.sessionFile);
|
|
517
675
|
}
|
|
518
676
|
|
|
@@ -526,20 +684,24 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
526
684
|
this.#viewerHeaderLines.push(`${theme.bold(ref.id)} ${statusBadge(ref.status)}${kindTag}${modelLabel}`);
|
|
527
685
|
}
|
|
528
686
|
|
|
529
|
-
const contentLines: string[] = [];
|
|
530
|
-
this.#viewerEntries = [];
|
|
531
687
|
if (!ref) {
|
|
532
|
-
|
|
533
|
-
} else if (!ref.sessionFile) {
|
|
534
|
-
|
|
688
|
+
this.#chatPlaceholder = "Agent no longer registered.";
|
|
689
|
+
} else if (!this.#remote && !ref.sessionFile) {
|
|
690
|
+
this.#chatPlaceholder = "No session file available yet.";
|
|
535
691
|
} else if (!messageEntries) {
|
|
536
|
-
|
|
692
|
+
this.#chatPlaceholder = "Unable to read session file.";
|
|
537
693
|
} else if (messageEntries.length === 0) {
|
|
538
|
-
|
|
694
|
+
if (this.#remote && this.#remoteTranscriptUnavailable) {
|
|
695
|
+
this.#chatPlaceholder = "Transcript lives on the host — not available.";
|
|
696
|
+
} else if (this.#remote && !this.#transcriptCache) {
|
|
697
|
+
this.#chatPlaceholder = "Loading transcript from host…";
|
|
698
|
+
} else {
|
|
699
|
+
this.#chatPlaceholder = "No messages yet.";
|
|
700
|
+
}
|
|
539
701
|
} else {
|
|
540
|
-
this.#
|
|
702
|
+
this.#chatPlaceholder = undefined;
|
|
703
|
+
this.#syncChatComponents(messageEntries);
|
|
541
704
|
}
|
|
542
|
-
this.#renderedLines = contentLines;
|
|
543
705
|
}
|
|
544
706
|
|
|
545
707
|
#handleChatInput(keyData: string): void {
|
|
@@ -555,8 +717,31 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
555
717
|
return;
|
|
556
718
|
}
|
|
557
719
|
|
|
558
|
-
//
|
|
559
|
-
|
|
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.
|
|
560
745
|
if (editorEmpty && this.#handleViewerNavigation(keyData)) {
|
|
561
746
|
return;
|
|
562
747
|
}
|
|
@@ -565,11 +750,23 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
565
750
|
this.#requestRender();
|
|
566
751
|
}
|
|
567
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
|
+
|
|
568
764
|
#closeChat(): void {
|
|
569
765
|
this.#view = "table";
|
|
570
766
|
this.#chatAgentId = undefined;
|
|
571
767
|
this.#notice = undefined;
|
|
572
768
|
this.#detachLiveSession();
|
|
769
|
+
this.#resetChatLog();
|
|
573
770
|
this.#refreshRows();
|
|
574
771
|
this.#requestRender();
|
|
575
772
|
}
|
|
@@ -580,6 +777,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
580
777
|
if (!id || !trimmed) return;
|
|
581
778
|
this.#editor.setText("");
|
|
582
779
|
this.#notice = undefined;
|
|
780
|
+
if (this.#remote) {
|
|
781
|
+
this.#remote.chat(id, trimmed);
|
|
782
|
+
this.#scheduleChatRefresh();
|
|
783
|
+
this.#requestRender();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
583
786
|
void (async () => {
|
|
584
787
|
try {
|
|
585
788
|
// Revives a parked agent; returns the live session for running/idle.
|
|
@@ -596,415 +799,391 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
596
799
|
this.#requestRender();
|
|
597
800
|
}
|
|
598
801
|
|
|
599
|
-
/**
|
|
802
|
+
/** Viewport scrolling for the chat transcript. Returns true when handled. */
|
|
600
803
|
#handleViewerNavigation(keyData: string): boolean {
|
|
601
|
-
const
|
|
602
|
-
|
|
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
|
+
};
|
|
603
810
|
if (keyData === "j" || matchesSelectDown(keyData)) {
|
|
604
|
-
|
|
605
|
-
this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 1, entryCount - 1);
|
|
606
|
-
}
|
|
607
|
-
this.#rebuildAndScroll();
|
|
811
|
+
scrollBy(1);
|
|
608
812
|
return true;
|
|
609
813
|
}
|
|
610
814
|
if (keyData === "k" || matchesSelectUp(keyData)) {
|
|
611
|
-
|
|
612
|
-
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 1, 0);
|
|
613
|
-
}
|
|
614
|
-
this.#rebuildAndScroll();
|
|
815
|
+
scrollBy(-1);
|
|
615
816
|
return true;
|
|
616
817
|
}
|
|
617
818
|
if (matchesKey(keyData, "pageDown")) {
|
|
618
|
-
|
|
619
|
-
const prevIndex = this.#selectedEntryIndex;
|
|
620
|
-
this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 5, entryCount - 1);
|
|
621
|
-
if (this.#selectedEntryIndex === prevIndex) {
|
|
622
|
-
this.#scrollOffset = Math.min(
|
|
623
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
624
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
this.#scrollOffset = Math.min(
|
|
629
|
-
this.#scrollOffset + PAGE_SIZE,
|
|
630
|
-
Math.max(0, this.#renderedLines.length - this.#viewportHeight),
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
this.#rebuildAndScroll();
|
|
819
|
+
scrollBy(PAGE_SIZE);
|
|
634
820
|
return true;
|
|
635
821
|
}
|
|
636
822
|
if (matchesKey(keyData, "pageUp")) {
|
|
637
|
-
|
|
638
|
-
const prevIndex = this.#selectedEntryIndex;
|
|
639
|
-
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 5, 0);
|
|
640
|
-
if (this.#selectedEntryIndex === prevIndex) {
|
|
641
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
642
|
-
}
|
|
643
|
-
} else {
|
|
644
|
-
this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
|
|
645
|
-
}
|
|
646
|
-
this.#rebuildAndScroll();
|
|
647
|
-
return true;
|
|
648
|
-
}
|
|
649
|
-
if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
|
|
650
|
-
if (entryCount > 0 && this.#selectedEntryIndex < entryCount) {
|
|
651
|
-
if (this.#expandedEntries.has(this.#selectedEntryIndex)) {
|
|
652
|
-
this.#expandedEntries.delete(this.#selectedEntryIndex);
|
|
653
|
-
} else {
|
|
654
|
-
this.#expandedEntries.add(this.#selectedEntryIndex);
|
|
655
|
-
}
|
|
656
|
-
this.#rebuildAndScroll();
|
|
657
|
-
}
|
|
823
|
+
scrollBy(-PAGE_SIZE);
|
|
658
824
|
return true;
|
|
659
825
|
}
|
|
660
826
|
if (keyData === "G") {
|
|
661
|
-
|
|
662
|
-
this.#
|
|
663
|
-
this.#
|
|
827
|
+
this.#scrollOffset = maxScroll;
|
|
828
|
+
this.#wasAtBottom = true;
|
|
829
|
+
this.#requestRender();
|
|
664
830
|
return true;
|
|
665
831
|
}
|
|
666
832
|
if (keyData === "g") {
|
|
667
|
-
this.#selectedEntryIndex = 0;
|
|
668
833
|
this.#scrollOffset = 0;
|
|
669
|
-
this.#
|
|
834
|
+
this.#wasAtBottom = maxScroll === 0;
|
|
835
|
+
this.#requestRender();
|
|
670
836
|
return true;
|
|
671
837
|
}
|
|
672
838
|
return false;
|
|
673
839
|
}
|
|
674
840
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
this.#rebuildChatContent();
|
|
680
|
-
this.#scrollToSelectedEntry();
|
|
681
|
-
this.#requestRender();
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
#scrollToSelectedEntry(): void {
|
|
685
|
-
if (this.#viewerEntries.length === 0) return;
|
|
686
|
-
const entry = this.#viewerEntries[this.#selectedEntryIndex];
|
|
687
|
-
if (!entry) return;
|
|
841
|
+
// ========================================================================
|
|
842
|
+
// Transcript assembly — the same components as the main session transcript
|
|
843
|
+
// (mirrors UiHelpers.renderSessionContext / addMessageToChat).
|
|
844
|
+
// ========================================================================
|
|
688
845
|
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
691
860
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// Entry fits in viewport: ensure it's fully visible
|
|
701
|
-
if (entryTop < this.#scrollOffset) {
|
|
702
|
-
this.#scrollOffset = Math.max(0, entryTop - 1);
|
|
703
|
-
}
|
|
704
|
-
if (entryBottom > this.#scrollOffset + this.#viewportHeight) {
|
|
705
|
-
this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight + 1);
|
|
706
|
-
}
|
|
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;
|
|
866
|
+
}
|
|
867
|
+
for (let i = this.#chatBuiltCount; i < entries.length; i++) {
|
|
868
|
+
this.#appendChatMessage(entries[i].message);
|
|
707
869
|
}
|
|
870
|
+
this.#chatBuiltCount = entries.length;
|
|
708
871
|
}
|
|
709
872
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
873
|
+
#trackExpandable(component: { setExpanded(expanded: boolean): void }): void {
|
|
874
|
+
component.setExpanded(this.#chatExpanded);
|
|
875
|
+
this.#chatExpandables.push(component);
|
|
876
|
+
}
|
|
713
877
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
let entryIndex = 0;
|
|
724
|
-
for (const entry of messageEntries) {
|
|
725
|
-
const msg = entry.message;
|
|
726
|
-
|
|
727
|
-
if (msg.role === "assistant") {
|
|
728
|
-
// Handle error messages with empty content
|
|
729
|
-
if (msg.content.length === 0 && msg.errorMessage && !isSilentAbort(msg.errorMessage)) {
|
|
730
|
-
const startLine = lines.length;
|
|
731
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
732
|
-
const cursor = isSelected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
733
|
-
lines.push("");
|
|
734
|
-
const errorLines = msg.errorMessage.split("\n");
|
|
735
|
-
const maxWidth = contentWidth();
|
|
736
|
-
lines.push(`${cursor} ${theme.fg("error", `✗ Error: ${sanitizeLine(errorLines[0], maxWidth)}`)}`);
|
|
737
|
-
for (let i = 1; i < errorLines.length; i++) {
|
|
738
|
-
lines.push(`${INDENT}${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
|
|
739
|
-
}
|
|
740
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "text" });
|
|
741
|
-
entryIndex++;
|
|
742
|
-
} else {
|
|
743
|
-
for (const content of msg.content) {
|
|
744
|
-
if (content.type === "thinking" && content.thinking.trim()) {
|
|
745
|
-
const startLine = lines.length;
|
|
746
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
747
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
748
|
-
this.#renderThinkingLines(lines, content.thinking.trim(), isExpanded, isSelected);
|
|
749
|
-
this.#viewerEntries.push({
|
|
750
|
-
lineStart: startLine,
|
|
751
|
-
lineCount: lines.length - startLine,
|
|
752
|
-
kind: "thinking",
|
|
753
|
-
});
|
|
754
|
-
entryIndex++;
|
|
755
|
-
} else if (content.type === "text" && content.text.trim()) {
|
|
756
|
-
const startLine = lines.length;
|
|
757
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
758
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
759
|
-
this.#renderTextLines(lines, content.text.trim(), isExpanded, isSelected);
|
|
760
|
-
this.#viewerEntries.push({
|
|
761
|
-
lineStart: startLine,
|
|
762
|
-
lineCount: lines.length - startLine,
|
|
763
|
-
kind: "text",
|
|
764
|
-
});
|
|
765
|
-
entryIndex++;
|
|
766
|
-
} else if (content.type === "toolCall") {
|
|
767
|
-
const startLine = lines.length;
|
|
768
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
769
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
770
|
-
const result = toolResults.get(content.id);
|
|
771
|
-
this.#renderToolCallLines(lines, content, result, isExpanded, isSelected);
|
|
772
|
-
this.#viewerEntries.push({
|
|
773
|
-
lineStart: startLine,
|
|
774
|
-
lineCount: lines.length - startLine,
|
|
775
|
-
kind: "toolCall",
|
|
776
|
-
});
|
|
777
|
-
entryIndex++;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
} else if (msg.role === "user" || msg.role === "developer") {
|
|
782
|
-
const text =
|
|
783
|
-
typeof msg.content === "string"
|
|
784
|
-
? msg.content
|
|
785
|
-
: msg.content
|
|
786
|
-
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
787
|
-
.map(b => b.text)
|
|
788
|
-
.join("\n");
|
|
789
|
-
if (text.trim()) {
|
|
790
|
-
const startLine = lines.length;
|
|
791
|
-
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
792
|
-
const isExpanded = this.#expandedEntries.has(entryIndex);
|
|
793
|
-
const label = msg.role === "developer" ? "System" : "User";
|
|
794
|
-
const cursor = isSelected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
795
|
-
lines.push("");
|
|
796
|
-
if (isExpanded) {
|
|
797
|
-
lines.push(`${cursor} ${theme.fg("dim", `[${label}]`)}`);
|
|
798
|
-
const mdLines = this.#renderMarkdownToLines(text.trim());
|
|
799
|
-
for (const ml of mdLines) {
|
|
800
|
-
lines.push(ml);
|
|
801
|
-
}
|
|
802
|
-
} else {
|
|
803
|
-
const firstLine = text.trim().split("\n")[0];
|
|
804
|
-
const totalLines = text.trim().split("\n").length;
|
|
805
|
-
const hint = totalLines > 1 ? theme.fg("dim", ` (${totalLines} lines)`) : "";
|
|
806
|
-
lines.push(
|
|
807
|
-
`${cursor} ${theme.fg("dim", `[${label}]`)} ${theme.fg("muted", sanitizeLine(firstLine, TRUNCATE_LENGTHS.TITLE))}${hint}`,
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "user" });
|
|
811
|
-
entryIndex++;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
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);
|
|
814
885
|
}
|
|
886
|
+
previous.seal();
|
|
815
887
|
}
|
|
816
888
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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;
|
|
823
898
|
}
|
|
824
899
|
|
|
825
|
-
#
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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;
|
|
841
926
|
}
|
|
842
|
-
|
|
843
|
-
|
|
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;
|
|
844
933
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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;
|
|
851
940
|
}
|
|
852
|
-
|
|
853
|
-
|
|
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;
|
|
854
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;
|
|
855
982
|
}
|
|
856
983
|
}
|
|
857
984
|
|
|
858
|
-
#
|
|
859
|
-
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);
|
|
860
991
|
|
|
861
|
-
|
|
862
|
-
|
|
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
|
+
}
|
|
863
1002
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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;
|
|
877
1036
|
}
|
|
878
|
-
|
|
879
|
-
|
|
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);
|
|
880
1066
|
}
|
|
881
1067
|
}
|
|
882
1068
|
}
|
|
883
1069
|
|
|
884
|
-
#
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const argSummary = this.#formatToolArgs(call.name, call.arguments);
|
|
900
|
-
if (argSummary) {
|
|
901
|
-
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;
|
|
902
1085
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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;
|
|
907
1091
|
}
|
|
908
1092
|
}
|
|
909
1093
|
|
|
910
|
-
#
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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));
|
|
926
1131
|
}
|
|
1132
|
+
this.#chatLog.addChild(block);
|
|
927
1133
|
return;
|
|
928
1134
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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);
|
|
932
1140
|
return;
|
|
933
1141
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const maxLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
937
|
-
|
|
938
|
-
// Status line
|
|
939
|
-
const statusPrefix = `${INDENT}${theme.fg("success", "✓")}`;
|
|
940
|
-
|
|
941
|
-
if (resultLines.length === 1 && text.length < TRUNCATE_LENGTHS.LONG) {
|
|
942
|
-
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>));
|
|
943
1144
|
return;
|
|
944
1145
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
if (resultLines.length > maxLines) {
|
|
952
|
-
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;
|
|
953
1151
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
case "lsp":
|
|
984
|
-
return [args.action, args.file, args.symbol].filter(Boolean).join(" ");
|
|
985
|
-
case "ast_grep":
|
|
986
|
-
case "ast_edit":
|
|
987
|
-
return args.path ? `path: ${args.path}` : "";
|
|
988
|
-
case "task": {
|
|
989
|
-
const target = typeof args.agent === "string" ? args.agent : "";
|
|
990
|
-
const id = typeof args.id === "string" && args.id ? ` ${args.id}` : "";
|
|
991
|
-
return `${target}${id}`.trim();
|
|
992
|
-
}
|
|
993
|
-
default: {
|
|
994
|
-
const parts: string[] = [];
|
|
995
|
-
let total = 0;
|
|
996
|
-
for (const key in args) {
|
|
997
|
-
if (key.startsWith("_")) continue;
|
|
998
|
-
const value = args[key];
|
|
999
|
-
const v = typeof value === "string" ? value : JSON.stringify(value);
|
|
1000
|
-
const entry = `${key}: ${replaceTabs(v ?? "")}`;
|
|
1001
|
-
if (total + entry.length > MAX_TOOL_ARGS_CHARS) break;
|
|
1002
|
-
parts.push(entry);
|
|
1003
|
-
total += entry.length;
|
|
1004
|
-
}
|
|
1005
|
-
return parts.join(", ");
|
|
1006
|
-
}
|
|
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;
|
|
1007
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);
|
|
1008
1187
|
}
|
|
1009
1188
|
|
|
1010
1189
|
#loadTranscript(sessionFile: string): SessionMessageEntry[] | null {
|
|
@@ -1024,31 +1203,80 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
1024
1203
|
return this.#loadTranscript(sessionFile);
|
|
1025
1204
|
}
|
|
1026
1205
|
|
|
1206
|
+
this.#ingestTranscriptChunk(sessionFile, result.text, fromByte);
|
|
1207
|
+
return this.#transcriptCache?.entries ?? null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** Parse a complete-line JSONL chunk into the transcript cache and advance bytesRead. Shared by the local file and remote paths. */
|
|
1211
|
+
#ingestTranscriptChunk(cacheKey: string, text: string, fromByte: number): void {
|
|
1027
1212
|
if (!this.#transcriptCache) {
|
|
1028
|
-
this.#transcriptCache = { path:
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
this.#transcriptCache.model = msg.model;
|
|
1043
|
-
}
|
|
1044
|
-
} else if (entry.type === "model_change") {
|
|
1045
|
-
this.#transcriptCache.model = entry.model;
|
|
1046
|
-
}
|
|
1213
|
+
this.#transcriptCache = { path: cacheKey, bytesRead: 0, entries: [] };
|
|
1214
|
+
}
|
|
1215
|
+
if (text.length === 0) return;
|
|
1216
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
1217
|
+
if (lastNewline < 0) return;
|
|
1218
|
+
const completeChunk = text.slice(0, lastNewline + 1);
|
|
1219
|
+
const newEntries = parseSessionEntries(completeChunk);
|
|
1220
|
+
for (const entry of newEntries) {
|
|
1221
|
+
if (entry.type === "message") {
|
|
1222
|
+
this.#transcriptCache.entries.push(entry);
|
|
1223
|
+
// Extract model from first assistant message
|
|
1224
|
+
const msg = entry.message;
|
|
1225
|
+
if (!this.#transcriptCache.model && msg.role === "assistant") {
|
|
1226
|
+
this.#transcriptCache.model = msg.model;
|
|
1047
1227
|
}
|
|
1048
|
-
|
|
1228
|
+
} else if (entry.type === "model_change") {
|
|
1229
|
+
this.#transcriptCache.model = entry.model;
|
|
1049
1230
|
}
|
|
1050
1231
|
}
|
|
1051
|
-
|
|
1232
|
+
this.#transcriptCache.bytesRead = fromByte + Buffer.byteLength(completeChunk, "utf-8");
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Kick an incremental transcript fetch from the collab host (single-flight). */
|
|
1236
|
+
#fetchRemoteTranscript(id: string): void {
|
|
1237
|
+
const remote = this.#remote;
|
|
1238
|
+
if (!remote || this.#remoteFetchInFlight) return;
|
|
1239
|
+
const cacheKey = `remote:${id}`;
|
|
1240
|
+
if (this.#transcriptCache && this.#transcriptCache.path !== cacheKey) {
|
|
1241
|
+
this.#transcriptCache = undefined;
|
|
1242
|
+
}
|
|
1243
|
+
const fromByte = this.#transcriptCache?.bytesRead ?? 0;
|
|
1244
|
+
this.#remoteFetchInFlight = true;
|
|
1245
|
+
const token = ++this.#remoteFetchToken;
|
|
1246
|
+
void remote
|
|
1247
|
+
.readTranscript(id, fromByte)
|
|
1248
|
+
.then(result => {
|
|
1249
|
+
if (token !== this.#remoteFetchToken) return;
|
|
1250
|
+
this.#remoteFetchInFlight = false;
|
|
1251
|
+
if (this.#chatAgentId !== id) return;
|
|
1252
|
+
if (!result) {
|
|
1253
|
+
if (!this.#transcriptCache || this.#transcriptCache.entries.length === 0) {
|
|
1254
|
+
if (!this.#remoteTranscriptUnavailable) {
|
|
1255
|
+
this.#remoteTranscriptUnavailable = true;
|
|
1256
|
+
this.#scheduleChatRefresh();
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (result.newSize < fromByte) {
|
|
1262
|
+
// Host transcript truncated/rotated — restart from 0.
|
|
1263
|
+
this.#transcriptCache = undefined;
|
|
1264
|
+
this.#fetchRemoteTranscript(id);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
this.#remoteTranscriptUnavailable = false;
|
|
1268
|
+
const hadCache = this.#transcriptCache !== undefined;
|
|
1269
|
+
const before = this.#transcriptCache?.entries.length ?? 0;
|
|
1270
|
+
this.#ingestTranscriptChunk(cacheKey, result.text, fromByte);
|
|
1271
|
+
const after = this.#transcriptCache?.entries.length ?? 0;
|
|
1272
|
+
// Only refresh on new content (or first completed fetch) — an
|
|
1273
|
+
// unconditional rebuild would re-kick the fetch in a tight loop.
|
|
1274
|
+
if (after > before || !hadCache) this.#scheduleChatRefresh();
|
|
1275
|
+
})
|
|
1276
|
+
.catch((error: unknown) => {
|
|
1277
|
+
if (token === this.#remoteFetchToken) this.#remoteFetchInFlight = false;
|
|
1278
|
+
logger.warn("Agent hub: remote transcript fetch failed", { id, error: String(error) });
|
|
1279
|
+
});
|
|
1052
1280
|
}
|
|
1053
1281
|
}
|
|
1054
1282
|
|