@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.8
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 +57 -1
- package/dist/cli.js +431 -381
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +93 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- 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/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +13 -9
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +8 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +74 -19
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +119 -1
- package/src/edit/renderer.ts +5 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +11 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/agent-hub.ts +119 -22
- 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/oauth-selector.ts +67 -7
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/settings-selector.ts +27 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +18 -10
- package/src/modes/controllers/input-controller.ts +80 -12
- package/src/modes/controllers/selector-controller.ts +6 -2
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/context-usage.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/prompts/bench.md +7 -0
- package/src/sdk.ts +240 -36
- package/src/session/agent-session.ts +22 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +20 -22
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/write.ts +13 -42
package/src/main.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
import type { AgentSession } from "./session/agent-session";
|
|
67
67
|
import type { AuthStorage } from "./session/auth-storage";
|
|
68
68
|
import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
|
|
69
|
+
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
69
70
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
|
|
70
71
|
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
71
72
|
import { AUTO_THINKING } from "./thinking";
|
|
@@ -346,6 +347,7 @@ async function runInteractiveMode(
|
|
|
346
347
|
initialMessage?: string,
|
|
347
348
|
initialImages?: ImageContent[],
|
|
348
349
|
titleSystemPrompt?: string,
|
|
350
|
+
joinLink?: string,
|
|
349
351
|
): Promise<void> {
|
|
350
352
|
const mode = new InteractiveMode(
|
|
351
353
|
session,
|
|
@@ -414,6 +416,12 @@ async function runInteractiveMode(
|
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
418
|
|
|
419
|
+
// `omp join <link>`: dispatch through the same builtin path as a typed
|
|
420
|
+
// `/join` so collab guards and error rendering stay in one place.
|
|
421
|
+
if (joinLink !== undefined) {
|
|
422
|
+
await executeBuiltinSlashCommand(`/join ${joinLink}`, { ctx: mode });
|
|
423
|
+
}
|
|
424
|
+
|
|
417
425
|
if (initialMessage !== undefined) {
|
|
418
426
|
try {
|
|
419
427
|
using _keepalive = new EventLoopKeepalive();
|
|
@@ -889,7 +897,7 @@ export async function runRootCommand(
|
|
|
889
897
|
|
|
890
898
|
// Create AuthStorage and ModelRegistry upfront
|
|
891
899
|
const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
|
|
892
|
-
const modelRegistry = new ModelRegistry(authStorage);
|
|
900
|
+
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
893
901
|
|
|
894
902
|
if (parsedArgs.version) {
|
|
895
903
|
process.stdout.write(`${VERSION}\n`);
|
|
@@ -1138,7 +1146,7 @@ export async function runRootCommand(
|
|
|
1138
1146
|
// Both are no-ops when OTEL_EXPORTER_OTLP_ENDPOINT is unset. An empty config
|
|
1139
1147
|
// is enough to enable telemetry — content capture is governed by the
|
|
1140
1148
|
// standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
|
|
1141
|
-
await initTelemetryExport
|
|
1149
|
+
await logger.time("initTelemetryExport", initTelemetryExport);
|
|
1142
1150
|
if (isTelemetryExportEnabled()) {
|
|
1143
1151
|
sessionOptions.telemetry = {};
|
|
1144
1152
|
}
|
|
@@ -1298,6 +1306,7 @@ export async function runRootCommand(
|
|
|
1298
1306
|
initialMessage,
|
|
1299
1307
|
initialImages,
|
|
1300
1308
|
titleSystemPrompt,
|
|
1309
|
+
parsedArgs.join,
|
|
1301
1310
|
);
|
|
1302
1311
|
} else {
|
|
1303
1312
|
// Branch-only single-shot runner: keep print-mode code out of normal interactive startup.
|
|
@@ -85,27 +85,99 @@ async function resolveWindowsCommandPath(
|
|
|
85
85
|
env: Record<string, string | undefined>,
|
|
86
86
|
): Promise<string | null> {
|
|
87
87
|
const extensions = getWindowsPathExt(env);
|
|
88
|
-
|
|
88
|
+
const hasExt = hasExecutableExtension(command, extensions);
|
|
89
|
+
const candidates = hasExt ? [command] : extensions.map(ext => `${command}${ext}`);
|
|
89
90
|
|
|
90
|
-
const candidates = extensions.map(ext => `${command}${ext}`);
|
|
91
91
|
if (hasPathSegment(command)) {
|
|
92
92
|
for (const candidate of candidates) {
|
|
93
93
|
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
|
|
94
94
|
if (await fileExists(resolved)) return resolved;
|
|
95
95
|
}
|
|
96
|
-
return null;
|
|
96
|
+
return hasExt ? command : null;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Match cmd.exe's lookup order for an unqualified name: current directory
|
|
100
|
+
// first, then PATH. Skipping cwd would launch a global shim instead of a
|
|
101
|
+
// project-local one with the same name.
|
|
102
|
+
const searchDirs = [cwd];
|
|
99
103
|
const pathValue = getCaseInsensitiveEnv(env, "PATH");
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
if (pathValue) {
|
|
105
|
+
for (const dir of pathValue.split(";")) {
|
|
106
|
+
if (dir) searchDirs.push(dir);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const dir of searchDirs) {
|
|
103
110
|
for (const candidate of candidates) {
|
|
104
111
|
const resolved = path.join(dir, candidate);
|
|
105
112
|
if (await fileExists(resolved)) return resolved;
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
|
-
return null;
|
|
115
|
+
return hasExt ? command : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveWindowsShimPath(value: string, shimDir: string): string | null {
|
|
119
|
+
const match = /^%dp0%[\\/]*(.*)$/i.exec(value);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
const suffix = match[1];
|
|
122
|
+
if (!suffix) return shimDir;
|
|
123
|
+
return path.join(shimDir, ...suffix.split(/[\\/]+/).filter(Boolean));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractWindowsNpmShimTarget(content: string): string | null {
|
|
127
|
+
const match = /"%_prog%"\s+"([^"]+)"\s+%\*/i.exec(content);
|
|
128
|
+
return match?.[1] ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract the shim's PATH-fallback interpreter (`SET "_prog=node"`). The
|
|
133
|
+
* `IF EXIST` branch assigns a `%dp0%`-prefixed value, so requiring a
|
|
134
|
+
* non-`%`-leading value picks the bare program name.
|
|
135
|
+
*/
|
|
136
|
+
function extractWindowsNpmShimProg(content: string): string | null {
|
|
137
|
+
const match = /SET\s+"_prog=([^%"][^"]*)"/i.exec(content);
|
|
138
|
+
return match?.[1] ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function resolveWindowsNpmShimCommand(
|
|
142
|
+
command: string,
|
|
143
|
+
args: readonly string[],
|
|
144
|
+
cwd: string,
|
|
145
|
+
): Promise<StdioSpawnCommand | null> {
|
|
146
|
+
if (!isWindowsBatchCommand(command)) return null;
|
|
147
|
+
if (!hasPathSegment(command)) return null;
|
|
148
|
+
const commandPath = path.resolve(cwd, command);
|
|
149
|
+
|
|
150
|
+
let content: string;
|
|
151
|
+
try {
|
|
152
|
+
content = await Bun.file(commandPath).text();
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// cmd-shim emits the same invocation line for every interpreter; only
|
|
158
|
+
// bypass cmd.exe when the shim's fallback interpreter is actually node.
|
|
159
|
+
const prog = extractWindowsNpmShimProg(content);
|
|
160
|
+
if (
|
|
161
|
+
!prog ||
|
|
162
|
+
path
|
|
163
|
+
.basename(prog)
|
|
164
|
+
.replace(/\.exe$/i, "")
|
|
165
|
+
.toLowerCase() !== "node"
|
|
166
|
+
)
|
|
167
|
+
return null;
|
|
168
|
+
|
|
169
|
+
const rawTarget = extractWindowsNpmShimTarget(content);
|
|
170
|
+
if (!rawTarget) return null;
|
|
171
|
+
|
|
172
|
+
const target = resolveWindowsShimPath(rawTarget, path.dirname(commandPath));
|
|
173
|
+
if (!target) return null;
|
|
174
|
+
|
|
175
|
+
const siblingNode = path.join(path.dirname(commandPath), "node.exe");
|
|
176
|
+
const nodeCommand = (await fileExists(siblingNode)) ? siblingNode : "node";
|
|
177
|
+
return {
|
|
178
|
+
cmd: [nodeCommand, target, ...args],
|
|
179
|
+
windowsHide: true,
|
|
180
|
+
};
|
|
109
181
|
}
|
|
110
182
|
|
|
111
183
|
function quoteCmdArg(value: string): string {
|
|
@@ -150,6 +222,8 @@ export async function resolveStdioSpawnCommand(
|
|
|
150
222
|
|
|
151
223
|
const resolvedCommand =
|
|
152
224
|
(await resolveWindowsCommandPath(config.command, options.cwd, options.env)) ?? config.command;
|
|
225
|
+
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args, options.cwd);
|
|
226
|
+
if (npmShimCommand) return npmShimCommand;
|
|
153
227
|
if (!isWindowsBatchCommand(resolvedCommand)) return { cmd: [resolvedCommand, ...args] };
|
|
154
228
|
|
|
155
229
|
return {
|
|
@@ -81,6 +81,15 @@ function statusBadge(status: AgentStatus): string {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** Guest-side proxy for hub actions executed on the collab host. */
|
|
85
|
+
export interface AgentHubRemote {
|
|
86
|
+
chat(id: string, text: string): void;
|
|
87
|
+
kill(id: string): void;
|
|
88
|
+
revive(id: string): void;
|
|
89
|
+
/** Mirrors readFileIncremental: text from fromByte (complete JSONL lines), newSize = next fromByte base; null = unavailable. */
|
|
90
|
+
readTranscript(id: string, fromByte: number): Promise<{ text: string; newSize: number } | null>;
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
export interface AgentHubDeps {
|
|
85
94
|
/** Progress/status snapshot source (task lifecycle + progress channels). */
|
|
86
95
|
observers: SessionObserverRegistry;
|
|
@@ -94,6 +103,8 @@ export interface AgentHubDeps {
|
|
|
94
103
|
lifecycle?: AgentLifecycleManager;
|
|
95
104
|
/** Injectable for tests; defaults to the process-global bus. */
|
|
96
105
|
irc?: IrcBus;
|
|
106
|
+
/** Collab guest: route actions/transcripts to the host instead of local sessions. */
|
|
107
|
+
remote?: AgentHubRemote;
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
export class AgentHubOverlayComponent extends Container {
|
|
@@ -106,6 +117,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
106
117
|
#hubKeys: KeyId[];
|
|
107
118
|
#unsubscribers: Array<() => void> = [];
|
|
108
119
|
#ageTimer: NodeJS.Timeout | undefined;
|
|
120
|
+
#remote: AgentHubRemote | undefined;
|
|
121
|
+
#remoteFetchInFlight = false;
|
|
122
|
+
/** Invalidates stale in-flight fetch callbacks after openChat resets the cache. */
|
|
123
|
+
#remoteFetchToken = 0;
|
|
124
|
+
#remoteTranscriptUnavailable = false;
|
|
109
125
|
|
|
110
126
|
// Table state
|
|
111
127
|
#view: "table" | "chat" = "table";
|
|
@@ -143,6 +159,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
143
159
|
this.#onDone = deps.onDone;
|
|
144
160
|
this.#requestRender = deps.requestRender;
|
|
145
161
|
this.#hubKeys = deps.hubKeys;
|
|
162
|
+
this.#remote = deps.remote;
|
|
146
163
|
|
|
147
164
|
this.#editor = new Editor(getEditorTheme());
|
|
148
165
|
this.#editor.setMaxHeight(4);
|
|
@@ -196,6 +213,9 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
196
213
|
this.#chatAgentId = id;
|
|
197
214
|
this.#notice = undefined;
|
|
198
215
|
this.#transcriptCache = undefined;
|
|
216
|
+
this.#remoteTranscriptUnavailable = false;
|
|
217
|
+
this.#remoteFetchInFlight = false;
|
|
218
|
+
this.#remoteFetchToken++;
|
|
199
219
|
this.#scrollOffset = 0;
|
|
200
220
|
this.#selectedEntryIndex = 0;
|
|
201
221
|
this.#expandedEntries.clear();
|
|
@@ -238,6 +258,8 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
238
258
|
|
|
239
259
|
/** Subscribe to the chat agent's live session (if any) for transcript refreshes. Idempotent per session. */
|
|
240
260
|
#attachLiveSession(): void {
|
|
261
|
+
// Remote refs carry no live session handle; refreshes come from observer onChange.
|
|
262
|
+
if (this.#remote) return;
|
|
241
263
|
const session = this.#chatAgentId ? (this.#registry.get(this.#chatAgentId)?.session ?? undefined) : undefined;
|
|
242
264
|
if (session === this.#attachedSession) return;
|
|
243
265
|
this.#detachLiveSession();
|
|
@@ -391,6 +413,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
391
413
|
return;
|
|
392
414
|
}
|
|
393
415
|
this.#notice = undefined;
|
|
416
|
+
if (this.#remote) {
|
|
417
|
+
this.#remote.revive(ref.id);
|
|
418
|
+
this.#requestRender();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
394
421
|
// Fire-and-forget; failures surface as an inline notice
|
|
395
422
|
this.#lifecycle()
|
|
396
423
|
.ensureLive(ref.id)
|
|
@@ -405,6 +432,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
405
432
|
const ref = this.#rows[this.#selectedRow];
|
|
406
433
|
if (!ref) return;
|
|
407
434
|
this.#notice = undefined;
|
|
435
|
+
if (this.#remote) {
|
|
436
|
+
this.#remote.kill(ref.id);
|
|
437
|
+
this.#refreshRows();
|
|
438
|
+
this.#requestRender();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
408
441
|
void (async () => {
|
|
409
442
|
try {
|
|
410
443
|
if (ref.status === "running" && ref.session) {
|
|
@@ -512,7 +545,10 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
512
545
|
|
|
513
546
|
// Load transcript first so model info is available for the header
|
|
514
547
|
let messageEntries: SessionMessageEntry[] | null = null;
|
|
515
|
-
if (
|
|
548
|
+
if (this.#remote) {
|
|
549
|
+
if (id) this.#fetchRemoteTranscript(id);
|
|
550
|
+
messageEntries = this.#transcriptCache?.entries ?? [];
|
|
551
|
+
} else if (ref?.sessionFile) {
|
|
516
552
|
messageEntries = this.#loadTranscript(ref.sessionFile);
|
|
517
553
|
}
|
|
518
554
|
|
|
@@ -530,12 +566,18 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
530
566
|
this.#viewerEntries = [];
|
|
531
567
|
if (!ref) {
|
|
532
568
|
contentLines.push(theme.fg("dim", "Agent no longer registered."));
|
|
533
|
-
} else if (!ref.sessionFile) {
|
|
569
|
+
} else if (!this.#remote && !ref.sessionFile) {
|
|
534
570
|
contentLines.push(theme.fg("dim", "No session file available yet."));
|
|
535
571
|
} else if (!messageEntries) {
|
|
536
572
|
contentLines.push(theme.fg("dim", "Unable to read session file."));
|
|
537
573
|
} else if (messageEntries.length === 0) {
|
|
538
|
-
|
|
574
|
+
if (this.#remote && this.#remoteTranscriptUnavailable) {
|
|
575
|
+
contentLines.push(theme.fg("dim", "Transcript lives on the host — not available."));
|
|
576
|
+
} else if (this.#remote && !this.#transcriptCache) {
|
|
577
|
+
contentLines.push(theme.fg("dim", "Loading transcript from host…"));
|
|
578
|
+
} else {
|
|
579
|
+
contentLines.push(theme.fg("dim", "No messages yet."));
|
|
580
|
+
}
|
|
539
581
|
} else {
|
|
540
582
|
this.#buildTranscriptLines(messageEntries, contentLines);
|
|
541
583
|
}
|
|
@@ -580,6 +622,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
580
622
|
if (!id || !trimmed) return;
|
|
581
623
|
this.#editor.setText("");
|
|
582
624
|
this.#notice = undefined;
|
|
625
|
+
if (this.#remote) {
|
|
626
|
+
this.#remote.chat(id, trimmed);
|
|
627
|
+
this.#scheduleChatRefresh();
|
|
628
|
+
this.#requestRender();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
583
631
|
void (async () => {
|
|
584
632
|
try {
|
|
585
633
|
// Revives a parked agent; returns the live session for running/idle.
|
|
@@ -1024,31 +1072,80 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
1024
1072
|
return this.#loadTranscript(sessionFile);
|
|
1025
1073
|
}
|
|
1026
1074
|
|
|
1075
|
+
this.#ingestTranscriptChunk(sessionFile, result.text, fromByte);
|
|
1076
|
+
return this.#transcriptCache?.entries ?? null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Parse a complete-line JSONL chunk into the transcript cache and advance bytesRead. Shared by the local file and remote paths. */
|
|
1080
|
+
#ingestTranscriptChunk(cacheKey: string, text: string, fromByte: number): void {
|
|
1027
1081
|
if (!this.#transcriptCache) {
|
|
1028
|
-
this.#transcriptCache = { path:
|
|
1082
|
+
this.#transcriptCache = { path: cacheKey, bytesRead: 0, entries: [] };
|
|
1029
1083
|
}
|
|
1084
|
+
if (text.length === 0) return;
|
|
1085
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
1086
|
+
if (lastNewline < 0) return;
|
|
1087
|
+
const completeChunk = text.slice(0, lastNewline + 1);
|
|
1088
|
+
const newEntries = parseSessionEntries(completeChunk);
|
|
1089
|
+
for (const entry of newEntries) {
|
|
1090
|
+
if (entry.type === "message") {
|
|
1091
|
+
this.#transcriptCache.entries.push(entry);
|
|
1092
|
+
// Extract model from first assistant message
|
|
1093
|
+
const msg = entry.message;
|
|
1094
|
+
if (!this.#transcriptCache.model && msg.role === "assistant") {
|
|
1095
|
+
this.#transcriptCache.model = msg.model;
|
|
1096
|
+
}
|
|
1097
|
+
} else if (entry.type === "model_change") {
|
|
1098
|
+
this.#transcriptCache.model = entry.model;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
this.#transcriptCache.bytesRead = fromByte + Buffer.byteLength(completeChunk, "utf-8");
|
|
1102
|
+
}
|
|
1030
1103
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1104
|
+
/** Kick an incremental transcript fetch from the collab host (single-flight). */
|
|
1105
|
+
#fetchRemoteTranscript(id: string): void {
|
|
1106
|
+
const remote = this.#remote;
|
|
1107
|
+
if (!remote || this.#remoteFetchInFlight) return;
|
|
1108
|
+
const cacheKey = `remote:${id}`;
|
|
1109
|
+
if (this.#transcriptCache && this.#transcriptCache.path !== cacheKey) {
|
|
1110
|
+
this.#transcriptCache = undefined;
|
|
1111
|
+
}
|
|
1112
|
+
const fromByte = this.#transcriptCache?.bytesRead ?? 0;
|
|
1113
|
+
this.#remoteFetchInFlight = true;
|
|
1114
|
+
const token = ++this.#remoteFetchToken;
|
|
1115
|
+
void remote
|
|
1116
|
+
.readTranscript(id, fromByte)
|
|
1117
|
+
.then(result => {
|
|
1118
|
+
if (token !== this.#remoteFetchToken) return;
|
|
1119
|
+
this.#remoteFetchInFlight = false;
|
|
1120
|
+
if (this.#chatAgentId !== id) return;
|
|
1121
|
+
if (!result) {
|
|
1122
|
+
if (!this.#transcriptCache || this.#transcriptCache.entries.length === 0) {
|
|
1123
|
+
if (!this.#remoteTranscriptUnavailable) {
|
|
1124
|
+
this.#remoteTranscriptUnavailable = true;
|
|
1125
|
+
this.#scheduleChatRefresh();
|
|
1043
1126
|
}
|
|
1044
|
-
} else if (entry.type === "model_change") {
|
|
1045
|
-
this.#transcriptCache.model = entry.model;
|
|
1046
1127
|
}
|
|
1128
|
+
return;
|
|
1047
1129
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1130
|
+
if (result.newSize < fromByte) {
|
|
1131
|
+
// Host transcript truncated/rotated — restart from 0.
|
|
1132
|
+
this.#transcriptCache = undefined;
|
|
1133
|
+
this.#fetchRemoteTranscript(id);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
this.#remoteTranscriptUnavailable = false;
|
|
1137
|
+
const hadCache = this.#transcriptCache !== undefined;
|
|
1138
|
+
const before = this.#transcriptCache?.entries.length ?? 0;
|
|
1139
|
+
this.#ingestTranscriptChunk(cacheKey, result.text, fromByte);
|
|
1140
|
+
const after = this.#transcriptCache?.entries.length ?? 0;
|
|
1141
|
+
// Only refresh on new content (or first completed fetch) — an
|
|
1142
|
+
// unconditional rebuild would re-kick the fetch in a tight loop.
|
|
1143
|
+
if (after > before || !hadCache) this.#scheduleChatRefresh();
|
|
1144
|
+
})
|
|
1145
|
+
.catch((error: unknown) => {
|
|
1146
|
+
if (token === this.#remoteFetchToken) this.#remoteFetchInFlight = false;
|
|
1147
|
+
logger.warn("Agent hub: remote transcript fetch failed", { id, error: String(error) });
|
|
1148
|
+
});
|
|
1052
1149
|
}
|
|
1053
1150
|
}
|
|
1054
1151
|
|
|
@@ -49,6 +49,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
49
49
|
/** Whether the last updateContent carried an in-flight streaming partial; such
|
|
50
50
|
* renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
|
|
51
51
|
#lastUpdateTransient = false;
|
|
52
|
+
// Fast-path state: reuse Markdown children when message shape is stable during streaming.
|
|
53
|
+
#fastPathKey: string | undefined;
|
|
54
|
+
#fastPathItems:
|
|
55
|
+
| Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
|
|
56
|
+
| undefined;
|
|
52
57
|
|
|
53
58
|
constructor(
|
|
54
59
|
message?: AssistantMessage,
|
|
@@ -71,6 +76,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
71
76
|
|
|
72
77
|
override invalidate(): void {
|
|
73
78
|
super.invalidate();
|
|
79
|
+
// Theme/symbol changes arrive via invalidate(). Fast-path children captured
|
|
80
|
+
// getMarkdownTheme() at construction, so drop them and force the teardown
|
|
81
|
+
// path to rebuild with the current theme. Streaming updates call
|
|
82
|
+
// updateContent() directly and keep the fast path.
|
|
83
|
+
this.#fastPathKey = undefined;
|
|
84
|
+
this.#fastPathItems = undefined;
|
|
74
85
|
if (this.#lastMessage) {
|
|
75
86
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
76
87
|
}
|
|
@@ -228,14 +239,111 @@ export class AssistantMessageComponent extends Container {
|
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
#computeShapeKey(message: AssistantMessage): string {
|
|
243
|
+
const parts: string[] = [`htb:${this.hideThinkingBlock ? 1 : 0}`];
|
|
244
|
+
for (const content of message.content) {
|
|
245
|
+
if (content.type === "text") {
|
|
246
|
+
parts.push(content.text.trim() ? "T1" : "T0");
|
|
247
|
+
} else if (content.type === "thinking") {
|
|
248
|
+
if (!content.thinking.trim()) parts.push("K0");
|
|
249
|
+
else if (this.hideThinkingBlock) parts.push("KH");
|
|
250
|
+
else parts.push("KV");
|
|
251
|
+
} else {
|
|
252
|
+
// Non-rendered blocks (toolCall, redactedThinking, …) still occupy a
|
|
253
|
+
// content index. Encode their position so an inserted/removed one shifts
|
|
254
|
+
// the key and forces the teardown path instead of mis-indexing children.
|
|
255
|
+
parts.push(`O:${content.type}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
259
|
+
const u = this.#usageInfo;
|
|
260
|
+
parts.push(`u:${u.input + u.cacheWrite}:${u.output}:${u.cacheRead}`);
|
|
261
|
+
} else {
|
|
262
|
+
parts.push("u:");
|
|
263
|
+
}
|
|
264
|
+
return parts.join("|");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#canFastPath(message: AssistantMessage): boolean {
|
|
268
|
+
for (const content of message.content) {
|
|
269
|
+
if (content.type === "toolCall") return false;
|
|
270
|
+
}
|
|
271
|
+
if (this.#toolImagesByCallId.size > 0) return false;
|
|
272
|
+
if (message.stopReason === "aborted" && shouldRenderAbortReason(message.errorMessage)) return false;
|
|
273
|
+
if (message.stopReason === "error" && !this.#errorPinned) return false;
|
|
274
|
+
if (
|
|
275
|
+
message.errorMessage &&
|
|
276
|
+
shouldRenderAbortReason(message.errorMessage) &&
|
|
277
|
+
message.stopReason !== "aborted" &&
|
|
278
|
+
message.stopReason !== "error"
|
|
279
|
+
)
|
|
280
|
+
return false;
|
|
281
|
+
// Extension stability: if thinking renderers exist and any tracked thinking
|
|
282
|
+
// block's text changed, extensions may produce a different child count.
|
|
283
|
+
if (this.thinkingRenderers.length > 0 && this.#fastPathItems) {
|
|
284
|
+
for (const item of this.#fastPathItems) {
|
|
285
|
+
if (item.blockType === "thinking") {
|
|
286
|
+
const content = message.content[item.contentIndex];
|
|
287
|
+
if (content?.type === "thinking" && content.thinking.trim() !== item.lastText) return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#tryFastPathUpdate(message: AssistantMessage, opts?: { transient?: boolean }): boolean {
|
|
295
|
+
if (!this.#fastPathKey || !this.#fastPathItems) return false;
|
|
296
|
+
if (!this.#canFastPath(message)) {
|
|
297
|
+
this.#fastPathKey = undefined;
|
|
298
|
+
this.#fastPathItems = undefined;
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
if (this.#computeShapeKey(message) !== this.#fastPathKey) {
|
|
302
|
+
this.#fastPathKey = undefined;
|
|
303
|
+
this.#fastPathItems = undefined;
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const transient = opts?.transient === true;
|
|
307
|
+
// Shape is identical — setText only on Markdown children whose source changed.
|
|
308
|
+
for (const item of this.#fastPathItems) {
|
|
309
|
+
item.md.transientRenderCache = transient;
|
|
310
|
+
const content = message.content[item.contentIndex];
|
|
311
|
+
let newText: string;
|
|
312
|
+
if (item.blockType === "text" && content?.type === "text") {
|
|
313
|
+
newText = content.text.trim();
|
|
314
|
+
} else if (item.blockType === "thinking" && content?.type === "thinking") {
|
|
315
|
+
newText = content.thinking.trim();
|
|
316
|
+
} else {
|
|
317
|
+
// Block at this index is gone or changed type (index shift) — fail closed.
|
|
318
|
+
this.#fastPathKey = undefined;
|
|
319
|
+
this.#fastPathItems = undefined;
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
if (newText !== item.lastText) {
|
|
323
|
+
item.md.setText(newText);
|
|
324
|
+
item.lastText = newText;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
231
330
|
updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
|
|
232
331
|
this.#blockVersion++;
|
|
233
332
|
this.#lastMessage = message;
|
|
234
333
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
235
334
|
|
|
335
|
+
// Fast path: reuse Markdown children when shape is stable during streaming
|
|
336
|
+
if (this.#tryFastPathUpdate(message)) return;
|
|
337
|
+
|
|
236
338
|
// Clear content container
|
|
237
339
|
this.#contentContainer.clear();
|
|
238
340
|
|
|
341
|
+
// Determine if we should capture Markdown instances for next fast path
|
|
342
|
+
const shouldCapture = this.#canFastPath(message);
|
|
343
|
+
const captureItems:
|
|
344
|
+
| Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
|
|
345
|
+
| undefined = shouldCapture ? [] : undefined;
|
|
346
|
+
|
|
239
347
|
const hasVisibleContent = message.content.some(
|
|
240
348
|
c =>
|
|
241
349
|
(c.type === "text" && c.text.trim()) ||
|
|
@@ -249,9 +357,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
249
357
|
if (content.type === "text" && content.text.trim()) {
|
|
250
358
|
// Assistant text messages with no background - trim the text
|
|
251
359
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
this.#
|
|
360
|
+
const trimmed = content.text.trim();
|
|
361
|
+
const md = new Markdown(trimmed, 1, 0, getMarkdownTheme());
|
|
362
|
+
md.transientRenderCache = this.#lastUpdateTransient;
|
|
363
|
+
this.#contentContainer.addChild(md);
|
|
364
|
+
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
255
365
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
256
366
|
if (this.hideThinkingBlock) {
|
|
257
367
|
thinkingIndex += 1;
|
|
@@ -265,12 +375,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
265
375
|
|
|
266
376
|
const thinkingText = content.thinking.trim();
|
|
267
377
|
// Thinking traces in thinkingText color, italic
|
|
268
|
-
const
|
|
378
|
+
const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
|
|
269
379
|
color: (text: string) => theme.fg("thinkingText", text),
|
|
270
380
|
italic: true,
|
|
271
381
|
});
|
|
272
|
-
|
|
273
|
-
this.#contentContainer.addChild(
|
|
382
|
+
md.transientRenderCache = this.#lastUpdateTransient;
|
|
383
|
+
this.#contentContainer.addChild(md);
|
|
384
|
+
captureItems?.push({ md, contentIndex: i, blockType: "thinking", lastText: thinkingText });
|
|
274
385
|
this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
|
|
275
386
|
thinkingIndex += 1;
|
|
276
387
|
if (hasVisibleContentAfter) {
|
|
@@ -318,5 +429,14 @@ export class AssistantMessageComponent extends Container {
|
|
|
318
429
|
this.#contentContainer.addChild(new Spacer(1));
|
|
319
430
|
this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
|
|
320
431
|
}
|
|
432
|
+
|
|
433
|
+
// Store fast-path state for next call
|
|
434
|
+
if (shouldCapture) {
|
|
435
|
+
this.#fastPathItems = captureItems;
|
|
436
|
+
this.#fastPathKey = this.#computeShapeKey(message);
|
|
437
|
+
} else {
|
|
438
|
+
this.#fastPathKey = undefined;
|
|
439
|
+
this.#fastPathItems = undefined;
|
|
440
|
+
}
|
|
321
441
|
}
|
|
322
442
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Container, Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { CollabPromptDetails } from "../../collab/protocol";
|
|
4
|
+
import type { CustomMessage } from "../../session/messages";
|
|
5
|
+
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders a collab guest prompt on every participant's transcript: a
|
|
9
|
+
* user-message-styled bubble prefixed with the author's name.
|
|
10
|
+
*/
|
|
11
|
+
export class CollabPromptMessageComponent extends Container {
|
|
12
|
+
constructor(message: CustomMessage<CollabPromptDetails>) {
|
|
13
|
+
super();
|
|
14
|
+
const from = message.details?.from?.trim() || "guest";
|
|
15
|
+
this.addChild(new Text(theme.fg("accent", `\x1b[1m«${from}»\x1b[22m ›`), 1, 0));
|
|
16
|
+
const text =
|
|
17
|
+
typeof message.content === "string"
|
|
18
|
+
? message.content
|
|
19
|
+
: message.content
|
|
20
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
21
|
+
.map(content => content.text)
|
|
22
|
+
.join("");
|
|
23
|
+
this.addChild(
|
|
24
|
+
new Markdown(text, 1, 1, getMarkdownTheme(), {
|
|
25
|
+
bgColor: (value: string) => theme.bg("userMessageBg", value),
|
|
26
|
+
color: (value: string) => theme.fg("userMessageText", value),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -31,13 +31,12 @@ import { CountdownTimer } from "./countdown-timer";
|
|
|
31
31
|
import { DynamicBorder } from "./dynamic-border";
|
|
32
32
|
import { renderSegmentTrack } from "./segment-track";
|
|
33
33
|
|
|
34
|
-
/** One segment of a {@link HookSelectorSlider} — a label
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
/** One segment of a {@link HookSelectorSlider} — a label and an optional
|
|
35
|
+
* detail line (e.g. the resolved model name) shown beneath the track while
|
|
36
|
+
* the segment is active. Segment colors come from the track's theme palette,
|
|
37
|
+
* assigned by position. */
|
|
37
38
|
export interface HookSelectorSliderSegment {
|
|
38
39
|
label: string;
|
|
39
|
-
/** Theme color for the segment label; defaults to `accent`. */
|
|
40
|
-
color?: ThemeColor;
|
|
41
40
|
/** Secondary line rendered under the track when this segment is selected. */
|
|
42
41
|
detail?: string;
|
|
43
42
|
}
|