skydive-cli 0.4.0-beta.979 → 0.4.1-beta.1
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 +4 -0
- package/README.md +51 -0
- package/dist/js/{billing-blocked-euid6MN9.mjs → billing-blocked-Dgu5-oDy.mjs} +1 -1
- package/dist/js/bin.mjs +333 -51
- package/dist/js/{boot-DwFRniN5.mjs → boot-DPmh-zP7.mjs} +938 -260
- package/dist/js/{client-DiaJZQG6.mjs → client-Cf7cvftg.mjs} +19 -6
- package/dist/js/client-CneLVPei.mjs +4 -0
- package/dist/js/{daemon-BSSZpzto.mjs → daemon-DaUaCMwC.mjs} +22 -5
- package/dist/js/daemon-client-CJxbCLOb.mjs +168 -0
- package/dist/js/daemon-client-DlblQIR9.mjs +6 -0
- package/dist/js/daemon-imLBqTGg.mjs +5 -0
- package/dist/js/http-error-DzyrsLAZ.mjs +20 -0
- package/dist/js/{print-CiaU2ygh.mjs → print-CbayCa87.mjs} +6 -5
- package/dist/js/{print-vUf9stcK.mjs → print-ba_0hiV9.mjs} +3 -3
- package/dist/js/{print-share-DlO5RSBt.mjs → print-share-CT4ltX7u.mjs} +3 -3
- package/dist/js/{install-Io3UMV51.mjs → profiler-QFAEvYKe.mjs} +210 -3
- package/dist/js/raw-pty-3EkG-jjH.mjs +5 -0
- package/dist/js/{raw-pty-DAmb8uqt.mjs → raw-pty-B6mAroiI.mjs} +1 -1
- package/dist/js/{rest-CG7VpQ56.mjs → rest-BY2nADw5.mjs} +21 -10
- package/dist/js/rest-DADJh0bi.mjs +6 -0
- package/package.json +3 -2
- package/dist/js/client-BpAnESuq.mjs +0 -6
- package/dist/js/daemon-ybnlDUgi.mjs +0 -7
- package/dist/js/raw-pty-t5DC-e1T.mjs +0 -5
- package/dist/js/rest-DfpoRJ_Q.mjs +0 -5
- /package/dist/js/{billing-blocked-CwfG1BTR.mjs → billing-blocked-2wju4gC_.mjs} +0 -0
- /package/dist/js/{client-D8s9vY4p.mjs → client-Cn2af31H.mjs} +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-
|
|
4
|
-
import {
|
|
5
|
-
import { i as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { t as
|
|
2
|
+
import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-QFAEvYKe.mjs";
|
|
3
|
+
import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CbayCa87.mjs";
|
|
4
|
+
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
5
|
+
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-BY2nADw5.mjs";
|
|
6
|
+
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
|
|
7
|
+
import { t as PortalClient } from "./client-Cf7cvftg.mjs";
|
|
8
|
+
import "./daemon-DaUaCMwC.mjs";
|
|
9
|
+
import { t as SandboxStream } from "./client-Cn2af31H.mjs";
|
|
10
|
+
import { t as PortalDaemonClient } from "./daemon-client-CJxbCLOb.mjs";
|
|
11
|
+
import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
|
|
10
12
|
import * as os$1 from "node:os";
|
|
11
13
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
12
14
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
@@ -14,12 +16,12 @@ import { z } from "zod";
|
|
|
14
16
|
import open from "open";
|
|
15
17
|
import { constants } from "node:fs";
|
|
16
18
|
import { execFile, spawn } from "node:child_process";
|
|
17
|
-
import { createHash
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
18
20
|
import { connect, createConnection } from "node:net";
|
|
19
21
|
import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
20
22
|
import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, addDefaultParsers, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
|
|
21
23
|
import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
22
|
-
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
24
|
+
import { Profiler, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
23
25
|
import { create } from "zustand";
|
|
24
26
|
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
25
27
|
import fuzzysort from "fuzzysort";
|
|
@@ -48,6 +50,58 @@ function toastColor(variant) {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/chat/tui/early-input.ts
|
|
55
|
+
/**
|
|
56
|
+
* Buffering for keystrokes typed BEFORE the chat composer exists.
|
|
57
|
+
*
|
|
58
|
+
* When the CLI boots straight into a conversation (`skydive chat --new`, or a
|
|
59
|
+
* seeded `skydive import`), it must resolve the agent roster over the network
|
|
60
|
+
* before the chat screen — and its composer — can mount. During that window
|
|
61
|
+
* the splash screen is up and there is no textarea to receive input, so a user
|
|
62
|
+
* who starts typing immediately would lose every character.
|
|
63
|
+
*
|
|
64
|
+
* These helpers accumulate that early input as plain text; the chat screen
|
|
65
|
+
* drains it into the composer on mount so nothing typed during the load is
|
|
66
|
+
* dropped. This is deliberately a text buffer, not a queue of key events: the
|
|
67
|
+
* composer is a plain single-field editor here, so replaying the resulting
|
|
68
|
+
* string (via `composer.setText`) reproduces exactly what the user meant to
|
|
69
|
+
* type, and stays trivially unit-testable without an OpenTUI renderer.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Whether a key is an ordinary printable character we should buffer as text.
|
|
73
|
+
*
|
|
74
|
+
* Excludes anything with a modifier (ctrl/meta/super/hyper — those are
|
|
75
|
+
* shortcuts, not text) and any named non-character key (enter, arrows, tab,
|
|
76
|
+
* escape, function keys, …). A printable stroke arrives with its character in
|
|
77
|
+
* `sequence` and a `name` that is that same single character (or empty for
|
|
78
|
+
* some symbols), never a multi-letter key name like `return`/`backspace`.
|
|
79
|
+
*/
|
|
80
|
+
function isTextKey(key) {
|
|
81
|
+
if (key.ctrl || key.meta || key.super || key.hyper) return false;
|
|
82
|
+
const seq = key.sequence;
|
|
83
|
+
if (seq.length !== 1) return false;
|
|
84
|
+
const code = seq.codePointAt(0);
|
|
85
|
+
if (code === void 0) return false;
|
|
86
|
+
if (code < 32 || code === 127) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Whether a key is Backspace/Delete-left (erase one buffered character). */
|
|
90
|
+
function isBackspaceKey(key) {
|
|
91
|
+
return !key.ctrl && !key.meta && key.name === "backspace";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fold one key into the early-input buffer. Printable characters append,
|
|
95
|
+
* backspace erases the last character, everything else (enter, arrows,
|
|
96
|
+
* shortcuts) is ignored — we only reconstruct the draft text, not caret moves
|
|
97
|
+
* or submission, which the real composer owns once it takes over.
|
|
98
|
+
*/
|
|
99
|
+
function reduceEarlyInput(buffer, key) {
|
|
100
|
+
if (isTextKey(key)) return buffer + key.sequence;
|
|
101
|
+
if (isBackspaceKey(key)) return buffer.slice(0, -1);
|
|
102
|
+
return buffer;
|
|
103
|
+
}
|
|
104
|
+
|
|
51
105
|
//#endregion
|
|
52
106
|
//#region src/chat/state/store.ts
|
|
53
107
|
const initialPortalUi = {
|
|
@@ -81,6 +135,7 @@ const useStore = create((set, get) => ({
|
|
|
81
135
|
seedPrompt: null,
|
|
82
136
|
autoGrantMachine: false,
|
|
83
137
|
newConversationIntent: false,
|
|
138
|
+
earlyInput: "",
|
|
84
139
|
goTo: (screen) => set({
|
|
85
140
|
screen,
|
|
86
141
|
chatTitle: initialChatTitle(screen)
|
|
@@ -138,7 +193,16 @@ const useStore = create((set, get) => ({
|
|
|
138
193
|
prompt
|
|
139
194
|
} })),
|
|
140
195
|
showToast: (input) => set({ toast: createToast(input) }),
|
|
141
|
-
dismissToast: (id) => set((state) => state.toast?.id === id ? { toast: null } : {})
|
|
196
|
+
dismissToast: (id) => set((state) => state.toast?.id === id ? { toast: null } : {}),
|
|
197
|
+
appendEarlyInput: (key) => set((state) => {
|
|
198
|
+
const next = reduceEarlyInput(state.earlyInput, key);
|
|
199
|
+
return next === state.earlyInput ? {} : { earlyInput: next };
|
|
200
|
+
}),
|
|
201
|
+
takeEarlyInput: () => {
|
|
202
|
+
const { earlyInput } = get();
|
|
203
|
+
if (earlyInput !== "") set({ earlyInput: "" });
|
|
204
|
+
return earlyInput;
|
|
205
|
+
}
|
|
142
206
|
}));
|
|
143
207
|
|
|
144
208
|
//#endregion
|
|
@@ -168,7 +232,7 @@ const useStore = create((set, get) => ({
|
|
|
168
232
|
async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation }) {
|
|
169
233
|
if (conversationId) {
|
|
170
234
|
const [conversation, agents] = await Promise.all([rest.getConversation({ conversationId }).catch((err) => {
|
|
171
|
-
if (err instanceof HttpError && err.status === 404) throw new Error(`No conversation with id ${conversationId}
|
|
235
|
+
if (err instanceof HttpError && err.status === 404) throw new Error(`No conversation with id ${conversationId}. It may have been deleted, or belong to another workspace.`);
|
|
172
236
|
throw err;
|
|
173
237
|
}), rest.listAgents({
|
|
174
238
|
scope: "org",
|
|
@@ -231,152 +295,7 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
231
295
|
}
|
|
232
296
|
|
|
233
297
|
//#endregion
|
|
234
|
-
//#region src/
|
|
235
|
-
var PortalDaemonClient = class {
|
|
236
|
-
sessionId = randomUUID();
|
|
237
|
-
socket = null;
|
|
238
|
-
disposed = false;
|
|
239
|
-
reconnectTimer = null;
|
|
240
|
-
lastBind = null;
|
|
241
|
-
wantEnabled = false;
|
|
242
|
-
pendingGrants = /* @__PURE__ */ new Set();
|
|
243
|
-
grantedAgentIds = /* @__PURE__ */ new Set();
|
|
244
|
-
constructor(opts) {
|
|
245
|
-
this.opts = opts;
|
|
246
|
-
}
|
|
247
|
-
/** Spawn the daemon if needed and attach. Idempotent. */
|
|
248
|
-
async start() {
|
|
249
|
-
if (this.disposed) return;
|
|
250
|
-
await ensureDaemonRunning(this.opts.appUrl);
|
|
251
|
-
this.connect();
|
|
252
|
-
}
|
|
253
|
-
connect() {
|
|
254
|
-
if (this.disposed) return;
|
|
255
|
-
const { socketPath } = daemonPaths(this.opts.appUrl);
|
|
256
|
-
const socket = connect(socketPath);
|
|
257
|
-
socket.setEncoding("utf8");
|
|
258
|
-
this.socket = socket;
|
|
259
|
-
const parse = makeLineParser();
|
|
260
|
-
socket.on("connect", () => {
|
|
261
|
-
this.send({
|
|
262
|
-
t: "hello",
|
|
263
|
-
v: LOCAL_PROTOCOL_VERSION,
|
|
264
|
-
sessionId: this.sessionId,
|
|
265
|
-
token: this.opts.sessionToken
|
|
266
|
-
});
|
|
267
|
-
if (this.wantEnabled) this.send({ t: "enable" });
|
|
268
|
-
if (this.lastBind) this.send({
|
|
269
|
-
t: "bind",
|
|
270
|
-
sessionId: this.sessionId,
|
|
271
|
-
conversationId: this.lastBind.conversationId,
|
|
272
|
-
cwd: this.lastBind.cwd
|
|
273
|
-
});
|
|
274
|
-
for (const agentId of this.pendingGrants) this.send({
|
|
275
|
-
t: "grant",
|
|
276
|
-
agentId
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
socket.on("data", (chunk) => {
|
|
280
|
-
for (const line of parse(chunk)) {
|
|
281
|
-
const msg = parseDaemonMessage(line);
|
|
282
|
-
if (msg) this.onMessage(msg);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
const onClose = () => {
|
|
286
|
-
if (this.socket === socket) this.socket = null;
|
|
287
|
-
this.scheduleReconnect();
|
|
288
|
-
};
|
|
289
|
-
socket.on("close", onClose);
|
|
290
|
-
socket.on("error", onClose);
|
|
291
|
-
}
|
|
292
|
-
scheduleReconnect() {
|
|
293
|
-
if (this.disposed || this.reconnectTimer) return;
|
|
294
|
-
this.reconnectTimer = setTimeout(() => {
|
|
295
|
-
this.reconnectTimer = null;
|
|
296
|
-
this.start();
|
|
297
|
-
}, 500);
|
|
298
|
-
this.reconnectTimer.unref();
|
|
299
|
-
}
|
|
300
|
-
onMessage(msg) {
|
|
301
|
-
if (msg.t === "state") {
|
|
302
|
-
this.grantedAgentIds = new Set(msg.grantedAgentIds);
|
|
303
|
-
this.opts.onState({
|
|
304
|
-
status: msg.status,
|
|
305
|
-
machineName: msg.machineName,
|
|
306
|
-
friendlyName: msg.friendlyName,
|
|
307
|
-
error: msg.error,
|
|
308
|
-
grantedAgentIds: msg.grantedAgentIds
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/** Whether this agent is currently authorized on the machine (last daemon state). */
|
|
313
|
-
isGranted(agentId) {
|
|
314
|
-
return this.grantedAgentIds.has(agentId);
|
|
315
|
-
}
|
|
316
|
-
/** Whether the user has turned machine sharing on from this CLI. */
|
|
317
|
-
isEnabled() {
|
|
318
|
-
return this.wantEnabled;
|
|
319
|
-
}
|
|
320
|
-
send(msg) {
|
|
321
|
-
if (this.socket?.writable) this.socket.write(encodeLine(msg));
|
|
322
|
-
}
|
|
323
|
-
/** Turn machine sharing on (the daemon connects the portal). */
|
|
324
|
-
enable() {
|
|
325
|
-
this.wantEnabled = true;
|
|
326
|
-
this.send({ t: "enable" });
|
|
327
|
-
}
|
|
328
|
-
/** Turn machine sharing off. */
|
|
329
|
-
disable() {
|
|
330
|
-
this.wantEnabled = false;
|
|
331
|
-
this.send({ t: "disable" });
|
|
332
|
-
}
|
|
333
|
-
/** Register / update this conversation's working directory for exec routing. */
|
|
334
|
-
bind(conversationId, cwd) {
|
|
335
|
-
this.lastBind = {
|
|
336
|
-
conversationId,
|
|
337
|
-
cwd
|
|
338
|
-
};
|
|
339
|
-
this.send({
|
|
340
|
-
t: "bind",
|
|
341
|
-
sessionId: this.sessionId,
|
|
342
|
-
conversationId,
|
|
343
|
-
cwd
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Authorize one agent to run commands on this machine. Resolves once the
|
|
348
|
-
* request is sent to the daemon (the daemon performs the grant and pushes the
|
|
349
|
-
* updated state); kept async so it's a drop-in for the old in-process client's
|
|
350
|
-
* awaited `grantAgent`.
|
|
351
|
-
*/
|
|
352
|
-
grantAgent(agentId) {
|
|
353
|
-
this.pendingGrants.add(agentId);
|
|
354
|
-
this.send({
|
|
355
|
-
t: "grant",
|
|
356
|
-
agentId
|
|
357
|
-
});
|
|
358
|
-
return Promise.resolve();
|
|
359
|
-
}
|
|
360
|
-
/** Detach this CLI. The daemon keeps running for other clients. */
|
|
361
|
-
dispose() {
|
|
362
|
-
this.disposed = true;
|
|
363
|
-
if (this.reconnectTimer) {
|
|
364
|
-
clearTimeout(this.reconnectTimer);
|
|
365
|
-
this.reconnectTimer = null;
|
|
366
|
-
}
|
|
367
|
-
this.send({
|
|
368
|
-
t: "bye",
|
|
369
|
-
sessionId: this.sessionId
|
|
370
|
-
});
|
|
371
|
-
try {
|
|
372
|
-
this.socket?.destroy();
|
|
373
|
-
} catch (_error) {}
|
|
374
|
-
this.socket = null;
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
//#endregion
|
|
379
|
-
//#region src/chat/portal/create-portal-client.ts
|
|
298
|
+
//#region ../portal-daemon/src/create-portal-client.ts
|
|
380
299
|
/**
|
|
381
300
|
* Escape hatch: setting `SKYDIVE_NO_DAEMON` (to any non-empty value)
|
|
382
301
|
* makes the CLI bypass the portal daemon and open the portal connection
|
|
@@ -681,9 +600,9 @@ function createCmuxWorkspaceTitleAdapter({ socket, workspaceId }) {
|
|
|
681
600
|
//#endregion
|
|
682
601
|
//#region src/chat/agent-host/adapters/cmux/sidebar-pr.ts
|
|
683
602
|
/** Neutral gray — deliberately not green/purple; we don't render PR state. */
|
|
684
|
-
const PILL_COLOR = "#8b949e";
|
|
685
|
-
const PILL_ICON = "git-pull-request";
|
|
686
|
-
const PILL_PRIORITY = 90;
|
|
603
|
+
const PILL_COLOR$1 = "#8b949e";
|
|
604
|
+
const PILL_ICON$1 = "git-pull-request";
|
|
605
|
+
const PILL_PRIORITY$1 = 90;
|
|
687
606
|
/**
|
|
688
607
|
* The cmux sidebar status key for a PR, derived from its repo-qualified
|
|
689
608
|
* identity (`owner/repo#number`) so PRs from different repos never collide on a
|
|
@@ -712,7 +631,7 @@ function createCmuxSidebarPrAdapter({ socket, workspaceId }) {
|
|
|
712
631
|
const runSet = async (pr) => {
|
|
713
632
|
if (applied.has(pr.key)) return;
|
|
714
633
|
applied.add(pr.key);
|
|
715
|
-
const line = `set_status ${pillKey(pr.key)} ${quote(`PR #${pr.number}`)} --icon=${PILL_ICON} --color=${PILL_COLOR} --priority=${PILL_PRIORITY} --url=${pr.url} --tab=${workspaceId}`;
|
|
634
|
+
const line = `set_status ${pillKey(pr.key)} ${quote(`PR #${pr.number}`)} --icon=${PILL_ICON$1} --color=${PILL_COLOR$1} --priority=${PILL_PRIORITY$1} --url=${pr.url} --tab=${workspaceId}`;
|
|
716
635
|
if (!await socket.command(line)) applied.delete(pr.key);
|
|
717
636
|
};
|
|
718
637
|
const runClear = async (prKey) => {
|
|
@@ -733,6 +652,54 @@ function createCmuxSidebarPrAdapter({ socket, workspaceId }) {
|
|
|
733
652
|
};
|
|
734
653
|
}
|
|
735
654
|
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/chat/agent-host/adapters/cmux/run-status.ts
|
|
657
|
+
/**
|
|
658
|
+
* Our sidebar status key. One stable key means every run reuses the same pill
|
|
659
|
+
* rather than leaking a new one per run, and teardown clears exactly what we
|
|
660
|
+
* set. cmux keys are space-split tokens, so it must be a single bare token.
|
|
661
|
+
*/
|
|
662
|
+
const PILL_KEY = "skydive-run";
|
|
663
|
+
/** The label shown on the pill while the agent is working. */
|
|
664
|
+
const PILL_LABEL = "Running";
|
|
665
|
+
/**
|
|
666
|
+
* `sync` is a cmux-rendered named glyph (spinning-arrows), the closest
|
|
667
|
+
* "in progress" affordance to the disabled loading spinner.
|
|
668
|
+
*/
|
|
669
|
+
const PILL_ICON = "sync";
|
|
670
|
+
/** A distinct blue so "Running" reads apart from the gray PR pills. */
|
|
671
|
+
const PILL_COLOR = "#3b82f6";
|
|
672
|
+
/**
|
|
673
|
+
* Above the PR pills (priority 90) so the live "Running" signal sorts to the
|
|
674
|
+
* top of the workspace's status line while a run is in flight.
|
|
675
|
+
*/
|
|
676
|
+
const PILL_PRIORITY = 100;
|
|
677
|
+
/** cmux renders a spinner while its `working` (working) state is on. */
|
|
678
|
+
function isWorking(state) {
|
|
679
|
+
return state === "working";
|
|
680
|
+
}
|
|
681
|
+
function createCmuxRunStatusAdapter({ socket, workspaceId }) {
|
|
682
|
+
let applied;
|
|
683
|
+
let chain = Promise.resolve();
|
|
684
|
+
const enqueue = (step) => {
|
|
685
|
+
chain = chain.then(step).catch((_error) => {});
|
|
686
|
+
};
|
|
687
|
+
const runSet = async (on) => {
|
|
688
|
+
if (applied === on) return;
|
|
689
|
+
const line = on ? `set_status ${PILL_KEY} "${PILL_LABEL}" --icon=${PILL_ICON} --color=${PILL_COLOR} --priority=${PILL_PRIORITY} --tab=${workspaceId}` : `clear_status ${PILL_KEY} --tab=${workspaceId}`;
|
|
690
|
+
if (await socket.command(line)) applied = on;
|
|
691
|
+
};
|
|
692
|
+
return {
|
|
693
|
+
transition({ state }) {
|
|
694
|
+
const on = isWorking(state);
|
|
695
|
+
enqueue(() => runSet(on));
|
|
696
|
+
},
|
|
697
|
+
dispose() {
|
|
698
|
+
enqueue(() => runSet(false));
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
736
703
|
//#endregion
|
|
737
704
|
//#region src/chat/agent-host/adapters/cmux/socket.ts
|
|
738
705
|
const cmuxRpcResponseSchema = z.object({
|
|
@@ -785,6 +752,9 @@ function createCmuxSocket({ socketPath, timeoutMs = RPC_TIMEOUT_MS }) {
|
|
|
785
752
|
`${++seq}`;
|
|
786
753
|
return roundTrip(`${line}\n`, (reply) => reply.startsWith("OK") || reply === "OK", false);
|
|
787
754
|
},
|
|
755
|
+
commandRaw(line) {
|
|
756
|
+
return roundTrip(`${line}\n`, (reply) => reply, null);
|
|
757
|
+
},
|
|
788
758
|
request(method, params) {
|
|
789
759
|
return new Promise((resolve) => {
|
|
790
760
|
let settled = false;
|
|
@@ -1011,6 +981,9 @@ function createAgentHost({ renderer, notifications }) {
|
|
|
1011
981
|
}), createCmuxSidebarPrAdapter({
|
|
1012
982
|
socket,
|
|
1013
983
|
workspaceId: cmux.workspaceId
|
|
984
|
+
}), createCmuxRunStatusAdapter({
|
|
985
|
+
socket,
|
|
986
|
+
workspaceId: cmux.workspaceId
|
|
1014
987
|
}));
|
|
1015
988
|
}
|
|
1016
989
|
const herdr = createHerdrReporter();
|
|
@@ -1418,7 +1391,7 @@ function StatusBar({ rightHint }) {
|
|
|
1418
1391
|
const portal = useStore((s) => s.portal);
|
|
1419
1392
|
const left = headerLeftLabel(appUrl, workspaceName);
|
|
1420
1393
|
const middle = screenLabel(screen, chatTitle);
|
|
1421
|
-
const share = portalIndicator(portal);
|
|
1394
|
+
const share = portalIndicator(portal, screenAgentId(screen));
|
|
1422
1395
|
return /* @__PURE__ */ jsxs("box", {
|
|
1423
1396
|
style: {
|
|
1424
1397
|
flexDirection: "row",
|
|
@@ -1450,23 +1423,40 @@ function StatusBar({ rightHint }) {
|
|
|
1450
1423
|
]
|
|
1451
1424
|
});
|
|
1452
1425
|
}
|
|
1453
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* The share indicator, which answers exactly one question: can the agent on
|
|
1428
|
+
* screen run commands on this machine right now? Not "is the portal
|
|
1429
|
+
* connected" — the connection is a per-machine fact, access is per agent, and
|
|
1430
|
+
* a badge that tracks the former tells a user who just declined that the agent
|
|
1431
|
+
* is in.
|
|
1432
|
+
*
|
|
1433
|
+
* So it is scoped to the current agent and shown only once that agent holds a
|
|
1434
|
+
* grant. Everything else — no agent on screen, portal off, connected but this
|
|
1435
|
+
* agent ungranted, still connecting — renders as the off state (nothing), which
|
|
1436
|
+
* is the truthful reading: this agent cannot reach the machine.
|
|
1437
|
+
*
|
|
1438
|
+
* The one exception is an error, and only for an agent that IS granted: its
|
|
1439
|
+
* access is supposed to be live and isn't, which the user needs to see.
|
|
1440
|
+
*/
|
|
1441
|
+
function portalIndicator(portal, agentId) {
|
|
1442
|
+
if (!agentId || !portal.grantedAgentIds.includes(agentId)) return null;
|
|
1454
1443
|
switch (portal.status) {
|
|
1455
|
-
case "off":
|
|
1456
|
-
case "connecting": return
|
|
1457
|
-
text: "◌ sharing…",
|
|
1458
|
-
color: theme.warning
|
|
1459
|
-
};
|
|
1444
|
+
case "off":
|
|
1445
|
+
case "connecting": return null;
|
|
1460
1446
|
case "connected": return {
|
|
1461
|
-
text: "● machine
|
|
1447
|
+
text: "● machine connected",
|
|
1462
1448
|
color: theme.success
|
|
1463
1449
|
};
|
|
1464
1450
|
case "error": return {
|
|
1465
|
-
text: "●
|
|
1451
|
+
text: "● machine error",
|
|
1466
1452
|
color: theme.error
|
|
1467
1453
|
};
|
|
1468
1454
|
}
|
|
1469
1455
|
}
|
|
1456
|
+
/** The agent whose access the indicator speaks for, if a screen names one. */
|
|
1457
|
+
function screenAgentId(screen) {
|
|
1458
|
+
return screen.kind === "chat" || screen.kind === "conversation-picker" ? screen.agent.id : null;
|
|
1459
|
+
}
|
|
1470
1460
|
/**
|
|
1471
1461
|
* The per-screen label, split into its parts: the screen's own `title` and,
|
|
1472
1462
|
* when the screen belongs to an agent, that `agentName`. Kept structured so the
|
|
@@ -1842,6 +1832,9 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1842
1832
|
fg: theme.muted,
|
|
1843
1833
|
children: "search "
|
|
1844
1834
|
}), /* @__PURE__ */ jsx("input", {
|
|
1835
|
+
textColor: theme.fg,
|
|
1836
|
+
focusedTextColor: theme.fg,
|
|
1837
|
+
placeholderColor: theme.dim,
|
|
1845
1838
|
placeholder: "type to filter…",
|
|
1846
1839
|
focused: true,
|
|
1847
1840
|
width: Math.max(10, width - 12),
|
|
@@ -2040,6 +2033,9 @@ function AgentCreateScreen() {
|
|
|
2040
2033
|
flexShrink: 0
|
|
2041
2034
|
},
|
|
2042
2035
|
children: /* @__PURE__ */ jsx("input", {
|
|
2036
|
+
textColor: theme.fg,
|
|
2037
|
+
focusedTextColor: theme.fg,
|
|
2038
|
+
placeholderColor: theme.dim,
|
|
2043
2039
|
placeholder: "Agent name",
|
|
2044
2040
|
onInput: (value) => {
|
|
2045
2041
|
setName(value);
|
|
@@ -2742,6 +2738,9 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2742
2738
|
fg: theme.muted,
|
|
2743
2739
|
children: "search "
|
|
2744
2740
|
}), /* @__PURE__ */ jsx("input", {
|
|
2741
|
+
textColor: theme.fg,
|
|
2742
|
+
focusedTextColor: theme.fg,
|
|
2743
|
+
placeholderColor: theme.dim,
|
|
2745
2744
|
placeholder: "type to filter…",
|
|
2746
2745
|
focused: confirmId === null,
|
|
2747
2746
|
width: Math.max(10, width - 12),
|
|
@@ -4160,6 +4159,292 @@ function parseMatches(output) {
|
|
|
4160
4159
|
return null;
|
|
4161
4160
|
}
|
|
4162
4161
|
|
|
4162
|
+
//#endregion
|
|
4163
|
+
//#region src/chat/tui/chat/conversation-context.tsx
|
|
4164
|
+
const ConversationContext = createContext({
|
|
4165
|
+
rest: null,
|
|
4166
|
+
conversationId: null
|
|
4167
|
+
});
|
|
4168
|
+
function useConversationContext() {
|
|
4169
|
+
return useContext(ConversationContext);
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
//#endregion
|
|
4173
|
+
//#region src/chat/tui/spinner-frames.ts
|
|
4174
|
+
/** Braille dot-cycle frames — the classic smooth terminal spinner. */
|
|
4175
|
+
const SPINNER_FRAMES = [
|
|
4176
|
+
"⠋",
|
|
4177
|
+
"⠙",
|
|
4178
|
+
"⠹",
|
|
4179
|
+
"⠸",
|
|
4180
|
+
"⠼",
|
|
4181
|
+
"⠴",
|
|
4182
|
+
"⠦",
|
|
4183
|
+
"⠧",
|
|
4184
|
+
"⠇",
|
|
4185
|
+
"⠏"
|
|
4186
|
+
];
|
|
4187
|
+
/** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
|
|
4188
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
4189
|
+
/** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
|
|
4190
|
+
function spinnerFrame(tick) {
|
|
4191
|
+
const n = SPINNER_FRAMES.length;
|
|
4192
|
+
return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
//#endregion
|
|
4196
|
+
//#region src/chat/tui/spinner.tsx
|
|
4197
|
+
/**
|
|
4198
|
+
* An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
|
|
4199
|
+
* a self-owned interval that's cleared on unmount, so the animation runs only
|
|
4200
|
+
* while the component is mounted. Returns a <span>, so render it inside a
|
|
4201
|
+
* <text> (matching how the rest of the TUI composes inline glyphs).
|
|
4202
|
+
*/
|
|
4203
|
+
function Spinner({ color }) {
|
|
4204
|
+
const [tick, setTick] = useState(0);
|
|
4205
|
+
useEffect(() => {
|
|
4206
|
+
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
|
|
4207
|
+
return () => clearInterval(id);
|
|
4208
|
+
}, []);
|
|
4209
|
+
return /* @__PURE__ */ jsx("span", {
|
|
4210
|
+
fg: color,
|
|
4211
|
+
children: spinnerFrame(tick)
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
//#endregion
|
|
4216
|
+
//#region src/chat/tui/chat/subagents.ts
|
|
4217
|
+
/** A run the platform hasn't settled yet — still queued or mid-run. */
|
|
4218
|
+
function isLiveStatus(status) {
|
|
4219
|
+
return status === "queued" || status === "running";
|
|
4220
|
+
}
|
|
4221
|
+
function toTaskStatus(raw) {
|
|
4222
|
+
switch (raw) {
|
|
4223
|
+
case "queued":
|
|
4224
|
+
case "running":
|
|
4225
|
+
case "completed":
|
|
4226
|
+
case "failed":
|
|
4227
|
+
case "canceled": return raw;
|
|
4228
|
+
default: return "queued";
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
const FALLBACK_TITLE = "Subagent task";
|
|
4232
|
+
/**
|
|
4233
|
+
* Each delegated task's display title from the `subagent` call's input, in
|
|
4234
|
+
* call order — the short name the tool requires per task. A malformed entry
|
|
4235
|
+
* yields a fallback label rather than being dropped, so the row count always
|
|
4236
|
+
* matches the fan-out width. Mirrors the web client's `subagentTaskTitles`.
|
|
4237
|
+
*/
|
|
4238
|
+
function subagentTaskTitles(input) {
|
|
4239
|
+
if (!isRecord(input)) return [];
|
|
4240
|
+
const tasks = input["tasks"];
|
|
4241
|
+
if (!Array.isArray(tasks)) return [];
|
|
4242
|
+
return tasks.map((task) => {
|
|
4243
|
+
if (!isRecord(task)) return FALLBACK_TITLE;
|
|
4244
|
+
const title = task["title"];
|
|
4245
|
+
return typeof title === "string" && title.trim() ? title.trim() : FALLBACK_TITLE;
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
const QUEUED_TASK_ID_LINE = /^\s*-\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):\s/i;
|
|
4249
|
+
/**
|
|
4250
|
+
* The ids of the runs a `subagent` call queued, read back out of its own
|
|
4251
|
+
* output. The call returns the moment the runs exist, so the ids are there
|
|
4252
|
+
* long before any child finishes — they are what ties this card to the task
|
|
4253
|
+
* rows the status poll returns, and their order is the tasks' call order.
|
|
4254
|
+
*/
|
|
4255
|
+
function queuedSubagentTaskIds(output) {
|
|
4256
|
+
const text = flattenOutputText(output);
|
|
4257
|
+
if (!text) return [];
|
|
4258
|
+
return text.split("\n").map((line) => line.match(QUEUED_TASK_ID_LINE)?.[1] ?? null).filter((id) => id !== null);
|
|
4259
|
+
}
|
|
4260
|
+
/**
|
|
4261
|
+
* Tool output reaches the TUI as `unknown`: the raw string live, possibly
|
|
4262
|
+
* wrapped in content parts or arrays when loaded from history. Walk it for
|
|
4263
|
+
* string leaves, newline-joined — the PR scanner's `stringifyToolOutput`
|
|
4264
|
+
* shape, bounded small because the id list sits at the top of the output.
|
|
4265
|
+
*/
|
|
4266
|
+
function flattenOutputText(value) {
|
|
4267
|
+
const MAX_LEN = 2e4;
|
|
4268
|
+
const parts = [];
|
|
4269
|
+
let total = 0;
|
|
4270
|
+
const visit = (v, depth) => {
|
|
4271
|
+
if (total >= MAX_LEN || depth > 4) return;
|
|
4272
|
+
if (typeof v === "string") {
|
|
4273
|
+
parts.push(v);
|
|
4274
|
+
total += v.length;
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
if (Array.isArray(v)) {
|
|
4278
|
+
for (const item of v) visit(item, depth + 1);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
if (isRecord(v)) for (const item of Object.values(v)) visit(item, depth + 1);
|
|
4282
|
+
};
|
|
4283
|
+
visit(value, 0);
|
|
4284
|
+
return parts.join("\n");
|
|
4285
|
+
}
|
|
4286
|
+
/**
|
|
4287
|
+
* The card's rows: one per queued run id (call order), each carrying the
|
|
4288
|
+
* freshest known state. A task the status fetch hasn't returned yet falls
|
|
4289
|
+
* back to the input's title and reads as queued; a fetched row's own title
|
|
4290
|
+
* wins over the input's (the server's copy is the one the child actually
|
|
4291
|
+
* ran under). Before the ids exist — the call is still streaming — rows come
|
|
4292
|
+
* from the input titles alone, so the card never renders empty mid-turn.
|
|
4293
|
+
*/
|
|
4294
|
+
function subagentRows({ titles, ids, fetched }) {
|
|
4295
|
+
if (ids.length === 0) return titles.map((title, index) => ({
|
|
4296
|
+
id: `queued:${index}`,
|
|
4297
|
+
title,
|
|
4298
|
+
status: "queued",
|
|
4299
|
+
createdAt: null,
|
|
4300
|
+
completedAt: null
|
|
4301
|
+
}));
|
|
4302
|
+
return ids.map((id, index) => {
|
|
4303
|
+
const row = fetched?.get(id) ?? null;
|
|
4304
|
+
return {
|
|
4305
|
+
id,
|
|
4306
|
+
title: row?.title ?? titles[index] ?? FALLBACK_TITLE,
|
|
4307
|
+
status: row ? toTaskStatus(row.status) : "queued",
|
|
4308
|
+
createdAt: row?.createdAt ?? null,
|
|
4309
|
+
completedAt: row?.completedAt ?? null
|
|
4310
|
+
};
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
//#endregion
|
|
4315
|
+
//#region src/chat/tui/chunks/tool-subagent.tsx
|
|
4316
|
+
/** How often a live batch re-reads its tasks' status. */
|
|
4317
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
4318
|
+
function rowGlyph(status) {
|
|
4319
|
+
switch (status) {
|
|
4320
|
+
case "completed": return {
|
|
4321
|
+
ch: "✔",
|
|
4322
|
+
fg: theme.success
|
|
4323
|
+
};
|
|
4324
|
+
case "failed":
|
|
4325
|
+
case "canceled": return {
|
|
4326
|
+
ch: "✗",
|
|
4327
|
+
fg: theme.error
|
|
4328
|
+
};
|
|
4329
|
+
case "running": return {
|
|
4330
|
+
ch: "▶",
|
|
4331
|
+
fg: theme.accent
|
|
4332
|
+
};
|
|
4333
|
+
case "queued": return {
|
|
4334
|
+
ch: "○",
|
|
4335
|
+
fg: theme.dim
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
function rowColor$1(status) {
|
|
4340
|
+
switch (status) {
|
|
4341
|
+
case "running": return theme.fg;
|
|
4342
|
+
case "completed": return theme.dim;
|
|
4343
|
+
default: return theme.muted;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
function headerLabel(rows) {
|
|
4347
|
+
const subject = rows.length === 1 ? "Subagent" : `${rows.length} subagents`;
|
|
4348
|
+
return rows.some((row) => isLiveStatus(row.status)) ? `${subject} running` : subject;
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* The transcript block for a `subagent` tool call — the TUI analog of the web
|
|
4352
|
+
* chat's fan-out card. One row per delegated run: status glyph, the short
|
|
4353
|
+
* title the delegating agent wrote, ticking over live as children settle.
|
|
4354
|
+
*
|
|
4355
|
+
* Rows are seeded from the call itself (titles from its input, task ids from
|
|
4356
|
+
* its own output), so the card renders the moment the call does. Status then
|
|
4357
|
+
* comes from the server's task rows, polled while any run is live — the
|
|
4358
|
+
* delegated runs outlive the call that spawned them, so the transcript stream
|
|
4359
|
+
* carries nothing more about them. The poll stops the moment the batch
|
|
4360
|
+
* settles; reopening an old conversation does one fetch and stops there. A
|
|
4361
|
+
* failed poll is skipped silently (the next tick retries; stale rows beat an
|
|
4362
|
+
* error row in the scrollback).
|
|
4363
|
+
*/
|
|
4364
|
+
function ToolSubagent({ item }) {
|
|
4365
|
+
const { rest, conversationId } = useConversationContext();
|
|
4366
|
+
const titles = subagentTaskTitles(item.input);
|
|
4367
|
+
const ids = item.output?.kind === "ok" ? queuedSubagentTaskIds(item.output.value) : [];
|
|
4368
|
+
const idsKey = ids.join(",");
|
|
4369
|
+
const [fetched, setFetched] = useState(null);
|
|
4370
|
+
const rows = subagentRows({
|
|
4371
|
+
titles,
|
|
4372
|
+
ids,
|
|
4373
|
+
fetched
|
|
4374
|
+
});
|
|
4375
|
+
const live = rows.length === 0 || rows.some((row) => isLiveStatus(row.status));
|
|
4376
|
+
useEffect(() => {
|
|
4377
|
+
if (!rest || !conversationId || idsKey.length === 0 || !live) return;
|
|
4378
|
+
let cancelled = false;
|
|
4379
|
+
let timer = null;
|
|
4380
|
+
const tick = async () => {
|
|
4381
|
+
try {
|
|
4382
|
+
const tasks = await rest.listSubagentTasks({ conversationId });
|
|
4383
|
+
if (cancelled) return;
|
|
4384
|
+
setFetched(tasks);
|
|
4385
|
+
} catch (_error) {}
|
|
4386
|
+
if (!cancelled) timer = setTimeout(tick, POLL_INTERVAL_MS);
|
|
4387
|
+
};
|
|
4388
|
+
tick();
|
|
4389
|
+
return () => {
|
|
4390
|
+
cancelled = true;
|
|
4391
|
+
if (timer !== null) clearTimeout(timer);
|
|
4392
|
+
};
|
|
4393
|
+
}, [
|
|
4394
|
+
rest,
|
|
4395
|
+
conversationId,
|
|
4396
|
+
idsKey,
|
|
4397
|
+
live
|
|
4398
|
+
]);
|
|
4399
|
+
if (rows.length === 0) return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
4400
|
+
fg: theme.warning,
|
|
4401
|
+
children: "◐ "
|
|
4402
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4403
|
+
fg: theme.tool,
|
|
4404
|
+
children: "Delegating to subagents…"
|
|
4405
|
+
})] });
|
|
4406
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
4407
|
+
style: { flexDirection: "column" },
|
|
4408
|
+
children: [/* @__PURE__ */ jsxs("text", { children: [
|
|
4409
|
+
live ? /* @__PURE__ */ jsx(Spinner, { color: theme.accent }) : /* @__PURE__ */ jsx("span", {
|
|
4410
|
+
fg: theme.success,
|
|
4411
|
+
children: "✓"
|
|
4412
|
+
}),
|
|
4413
|
+
/* @__PURE__ */ jsxs("span", {
|
|
4414
|
+
fg: theme.tool,
|
|
4415
|
+
children: [" ", /* @__PURE__ */ jsx("b", { children: headerLabel(rows) })]
|
|
4416
|
+
}),
|
|
4417
|
+
/* @__PURE__ */ jsxs("span", {
|
|
4418
|
+
fg: theme.dim,
|
|
4419
|
+
children: [
|
|
4420
|
+
" ",
|
|
4421
|
+
rows.filter((row) => row.status === "completed").length,
|
|
4422
|
+
"/",
|
|
4423
|
+
rows.length
|
|
4424
|
+
]
|
|
4425
|
+
})
|
|
4426
|
+
] }), /* @__PURE__ */ jsx("box", {
|
|
4427
|
+
style: {
|
|
4428
|
+
flexDirection: "column",
|
|
4429
|
+
paddingLeft: 2
|
|
4430
|
+
},
|
|
4431
|
+
children: rows.map((row) => {
|
|
4432
|
+
const glyph = rowGlyph(row.status);
|
|
4433
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
4434
|
+
style: { flexDirection: "row" },
|
|
4435
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
4436
|
+
fg: glyph.fg,
|
|
4437
|
+
children: [glyph.ch, " "]
|
|
4438
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
4439
|
+
fg: rowColor$1(row.status),
|
|
4440
|
+
children: row.title
|
|
4441
|
+
})]
|
|
4442
|
+
}, row.id);
|
|
4443
|
+
})
|
|
4444
|
+
})]
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4163
4448
|
//#endregion
|
|
4164
4449
|
//#region src/chat/tui/chunks/shell-local.tsx
|
|
4165
4450
|
/**
|
|
@@ -4383,6 +4668,7 @@ function ToolItem({ item }) {
|
|
|
4383
4668
|
case "read": return /* @__PURE__ */ jsx(ToolRead, { item });
|
|
4384
4669
|
case "grep":
|
|
4385
4670
|
case "glob": return /* @__PURE__ */ jsx(ToolGrep, { item });
|
|
4671
|
+
case "subagent": return /* @__PURE__ */ jsx(ToolSubagent, { item });
|
|
4386
4672
|
default: return /* @__PURE__ */ jsx(ToolGeneric, { item });
|
|
4387
4673
|
}
|
|
4388
4674
|
}
|
|
@@ -4552,6 +4838,270 @@ function clampItem(item) {
|
|
|
4552
4838
|
}
|
|
4553
4839
|
}
|
|
4554
4840
|
|
|
4841
|
+
//#endregion
|
|
4842
|
+
//#region src/chat/tui/chat/fix-json.ts
|
|
4843
|
+
/**
|
|
4844
|
+
* Implemented as a scanner with additional fixing
|
|
4845
|
+
* that performs a single linear time scan pass over the partial JSON.
|
|
4846
|
+
*
|
|
4847
|
+
* The states should ideally match relevant states from the JSON spec:
|
|
4848
|
+
* https://www.json.org/json-en.html
|
|
4849
|
+
*
|
|
4850
|
+
* Please note that invalid JSON is not considered/covered, because it
|
|
4851
|
+
* is assumed that the resulting JSON will be processed by a standard
|
|
4852
|
+
* JSON parser that will detect any invalid JSON.
|
|
4853
|
+
*/
|
|
4854
|
+
function fixJson(input) {
|
|
4855
|
+
const stack = ["ROOT"];
|
|
4856
|
+
let lastValidIndex = -1;
|
|
4857
|
+
let literalStart = null;
|
|
4858
|
+
function processValueStart(char, i, swapState) {
|
|
4859
|
+
switch (char) {
|
|
4860
|
+
case "\"":
|
|
4861
|
+
lastValidIndex = i;
|
|
4862
|
+
stack.pop();
|
|
4863
|
+
stack.push(swapState);
|
|
4864
|
+
stack.push("INSIDE_STRING");
|
|
4865
|
+
break;
|
|
4866
|
+
case "f":
|
|
4867
|
+
case "t":
|
|
4868
|
+
case "n":
|
|
4869
|
+
lastValidIndex = i;
|
|
4870
|
+
literalStart = i;
|
|
4871
|
+
stack.pop();
|
|
4872
|
+
stack.push(swapState);
|
|
4873
|
+
stack.push("INSIDE_LITERAL");
|
|
4874
|
+
break;
|
|
4875
|
+
case "-":
|
|
4876
|
+
stack.pop();
|
|
4877
|
+
stack.push(swapState);
|
|
4878
|
+
stack.push("INSIDE_NUMBER");
|
|
4879
|
+
break;
|
|
4880
|
+
case "0":
|
|
4881
|
+
case "1":
|
|
4882
|
+
case "2":
|
|
4883
|
+
case "3":
|
|
4884
|
+
case "4":
|
|
4885
|
+
case "5":
|
|
4886
|
+
case "6":
|
|
4887
|
+
case "7":
|
|
4888
|
+
case "8":
|
|
4889
|
+
case "9":
|
|
4890
|
+
lastValidIndex = i;
|
|
4891
|
+
stack.pop();
|
|
4892
|
+
stack.push(swapState);
|
|
4893
|
+
stack.push("INSIDE_NUMBER");
|
|
4894
|
+
break;
|
|
4895
|
+
case "{":
|
|
4896
|
+
lastValidIndex = i;
|
|
4897
|
+
stack.pop();
|
|
4898
|
+
stack.push(swapState);
|
|
4899
|
+
stack.push("INSIDE_OBJECT_START");
|
|
4900
|
+
break;
|
|
4901
|
+
case "[":
|
|
4902
|
+
lastValidIndex = i;
|
|
4903
|
+
stack.pop();
|
|
4904
|
+
stack.push(swapState);
|
|
4905
|
+
stack.push("INSIDE_ARRAY_START");
|
|
4906
|
+
break;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
function processAfterObjectValue(char, i) {
|
|
4910
|
+
switch (char) {
|
|
4911
|
+
case ",":
|
|
4912
|
+
stack.pop();
|
|
4913
|
+
stack.push("INSIDE_OBJECT_AFTER_COMMA");
|
|
4914
|
+
break;
|
|
4915
|
+
case "}":
|
|
4916
|
+
lastValidIndex = i;
|
|
4917
|
+
stack.pop();
|
|
4918
|
+
break;
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
function processAfterArrayValue(char, i) {
|
|
4922
|
+
switch (char) {
|
|
4923
|
+
case ",":
|
|
4924
|
+
stack.pop();
|
|
4925
|
+
stack.push("INSIDE_ARRAY_AFTER_COMMA");
|
|
4926
|
+
break;
|
|
4927
|
+
case "]":
|
|
4928
|
+
lastValidIndex = i;
|
|
4929
|
+
stack.pop();
|
|
4930
|
+
break;
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
for (let i = 0; i < input.length; i++) {
|
|
4934
|
+
const char = input[i];
|
|
4935
|
+
if (!char) continue;
|
|
4936
|
+
switch (stack[stack.length - 1]) {
|
|
4937
|
+
case "ROOT":
|
|
4938
|
+
processValueStart(char, i, "FINISH");
|
|
4939
|
+
break;
|
|
4940
|
+
case "INSIDE_OBJECT_START":
|
|
4941
|
+
switch (char) {
|
|
4942
|
+
case "\"":
|
|
4943
|
+
stack.pop();
|
|
4944
|
+
stack.push("INSIDE_OBJECT_KEY");
|
|
4945
|
+
break;
|
|
4946
|
+
case "}":
|
|
4947
|
+
lastValidIndex = i;
|
|
4948
|
+
stack.pop();
|
|
4949
|
+
break;
|
|
4950
|
+
}
|
|
4951
|
+
break;
|
|
4952
|
+
case "INSIDE_OBJECT_AFTER_COMMA":
|
|
4953
|
+
switch (char) {
|
|
4954
|
+
case "\"":
|
|
4955
|
+
stack.pop();
|
|
4956
|
+
stack.push("INSIDE_OBJECT_KEY");
|
|
4957
|
+
break;
|
|
4958
|
+
}
|
|
4959
|
+
break;
|
|
4960
|
+
case "INSIDE_OBJECT_KEY":
|
|
4961
|
+
switch (char) {
|
|
4962
|
+
case "\"":
|
|
4963
|
+
stack.pop();
|
|
4964
|
+
stack.push("INSIDE_OBJECT_AFTER_KEY");
|
|
4965
|
+
break;
|
|
4966
|
+
}
|
|
4967
|
+
break;
|
|
4968
|
+
case "INSIDE_OBJECT_AFTER_KEY":
|
|
4969
|
+
switch (char) {
|
|
4970
|
+
case ":":
|
|
4971
|
+
stack.pop();
|
|
4972
|
+
stack.push("INSIDE_OBJECT_BEFORE_VALUE");
|
|
4973
|
+
break;
|
|
4974
|
+
}
|
|
4975
|
+
break;
|
|
4976
|
+
case "INSIDE_OBJECT_BEFORE_VALUE":
|
|
4977
|
+
processValueStart(char, i, "INSIDE_OBJECT_AFTER_VALUE");
|
|
4978
|
+
break;
|
|
4979
|
+
case "INSIDE_OBJECT_AFTER_VALUE":
|
|
4980
|
+
processAfterObjectValue(char, i);
|
|
4981
|
+
break;
|
|
4982
|
+
case "INSIDE_STRING":
|
|
4983
|
+
switch (char) {
|
|
4984
|
+
case "\"":
|
|
4985
|
+
stack.pop();
|
|
4986
|
+
lastValidIndex = i;
|
|
4987
|
+
break;
|
|
4988
|
+
case "\\":
|
|
4989
|
+
stack.push("INSIDE_STRING_ESCAPE");
|
|
4990
|
+
break;
|
|
4991
|
+
default: lastValidIndex = i;
|
|
4992
|
+
}
|
|
4993
|
+
break;
|
|
4994
|
+
case "INSIDE_ARRAY_START":
|
|
4995
|
+
switch (char) {
|
|
4996
|
+
case "]":
|
|
4997
|
+
lastValidIndex = i;
|
|
4998
|
+
stack.pop();
|
|
4999
|
+
break;
|
|
5000
|
+
default:
|
|
5001
|
+
lastValidIndex = i;
|
|
5002
|
+
processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE");
|
|
5003
|
+
break;
|
|
5004
|
+
}
|
|
5005
|
+
break;
|
|
5006
|
+
case "INSIDE_ARRAY_AFTER_VALUE":
|
|
5007
|
+
switch (char) {
|
|
5008
|
+
case ",":
|
|
5009
|
+
stack.pop();
|
|
5010
|
+
stack.push("INSIDE_ARRAY_AFTER_COMMA");
|
|
5011
|
+
break;
|
|
5012
|
+
case "]":
|
|
5013
|
+
lastValidIndex = i;
|
|
5014
|
+
stack.pop();
|
|
5015
|
+
break;
|
|
5016
|
+
default:
|
|
5017
|
+
lastValidIndex = i;
|
|
5018
|
+
break;
|
|
5019
|
+
}
|
|
5020
|
+
break;
|
|
5021
|
+
case "INSIDE_ARRAY_AFTER_COMMA":
|
|
5022
|
+
processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE");
|
|
5023
|
+
break;
|
|
5024
|
+
case "INSIDE_STRING_ESCAPE":
|
|
5025
|
+
stack.pop();
|
|
5026
|
+
lastValidIndex = i;
|
|
5027
|
+
break;
|
|
5028
|
+
case "INSIDE_NUMBER":
|
|
5029
|
+
switch (char) {
|
|
5030
|
+
case "0":
|
|
5031
|
+
case "1":
|
|
5032
|
+
case "2":
|
|
5033
|
+
case "3":
|
|
5034
|
+
case "4":
|
|
5035
|
+
case "5":
|
|
5036
|
+
case "6":
|
|
5037
|
+
case "7":
|
|
5038
|
+
case "8":
|
|
5039
|
+
case "9":
|
|
5040
|
+
lastValidIndex = i;
|
|
5041
|
+
break;
|
|
5042
|
+
case "e":
|
|
5043
|
+
case "E":
|
|
5044
|
+
case "-":
|
|
5045
|
+
case ".": break;
|
|
5046
|
+
case ",":
|
|
5047
|
+
stack.pop();
|
|
5048
|
+
if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
|
|
5049
|
+
if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
|
|
5050
|
+
break;
|
|
5051
|
+
case "}":
|
|
5052
|
+
stack.pop();
|
|
5053
|
+
if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
|
|
5054
|
+
break;
|
|
5055
|
+
case "]":
|
|
5056
|
+
stack.pop();
|
|
5057
|
+
if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
|
|
5058
|
+
break;
|
|
5059
|
+
default:
|
|
5060
|
+
stack.pop();
|
|
5061
|
+
break;
|
|
5062
|
+
}
|
|
5063
|
+
break;
|
|
5064
|
+
case "INSIDE_LITERAL": {
|
|
5065
|
+
if (!literalStart) continue;
|
|
5066
|
+
const partialLiteral = input.substring(literalStart, i + 1);
|
|
5067
|
+
if (!"false".startsWith(partialLiteral) && !"true".startsWith(partialLiteral) && !"null".startsWith(partialLiteral)) {
|
|
5068
|
+
stack.pop();
|
|
5069
|
+
if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") processAfterObjectValue(char, i);
|
|
5070
|
+
else if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") processAfterArrayValue(char, i);
|
|
5071
|
+
} else lastValidIndex = i;
|
|
5072
|
+
break;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
let result = input.slice(0, lastValidIndex + 1);
|
|
5077
|
+
for (let i = stack.length - 1; i >= 0; i--) switch (stack[i]) {
|
|
5078
|
+
case "INSIDE_STRING":
|
|
5079
|
+
result += "\"";
|
|
5080
|
+
break;
|
|
5081
|
+
case "INSIDE_OBJECT_KEY":
|
|
5082
|
+
case "INSIDE_OBJECT_AFTER_KEY":
|
|
5083
|
+
case "INSIDE_OBJECT_AFTER_COMMA":
|
|
5084
|
+
case "INSIDE_OBJECT_START":
|
|
5085
|
+
case "INSIDE_OBJECT_BEFORE_VALUE":
|
|
5086
|
+
case "INSIDE_OBJECT_AFTER_VALUE":
|
|
5087
|
+
result += "}";
|
|
5088
|
+
break;
|
|
5089
|
+
case "INSIDE_ARRAY_START":
|
|
5090
|
+
case "INSIDE_ARRAY_AFTER_COMMA":
|
|
5091
|
+
case "INSIDE_ARRAY_AFTER_VALUE":
|
|
5092
|
+
result += "]";
|
|
5093
|
+
break;
|
|
5094
|
+
case "INSIDE_LITERAL": {
|
|
5095
|
+
if (!literalStart) continue;
|
|
5096
|
+
const partialLiteral = input.substring(literalStart, input.length);
|
|
5097
|
+
if ("true".startsWith(partialLiteral)) result += "true".slice(partialLiteral.length);
|
|
5098
|
+
else if ("false".startsWith(partialLiteral)) result += "false".slice(partialLiteral.length);
|
|
5099
|
+
else if ("null".startsWith(partialLiteral)) result += "null".slice(partialLiteral.length);
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
return result;
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4555
5105
|
//#endregion
|
|
4556
5106
|
//#region src/chat/tui/chat/reducer.ts
|
|
4557
5107
|
function str(chunk, key) {
|
|
@@ -4559,6 +5109,24 @@ function str(chunk, key) {
|
|
|
4559
5109
|
return typeof value === "string" ? value : null;
|
|
4560
5110
|
}
|
|
4561
5111
|
/**
|
|
5112
|
+
* Best-effort parse of a tool call's partial arguments while they stream.
|
|
5113
|
+
* `fixJson` (the vercel/ai scanner) closes off the truncated JSON so a
|
|
5114
|
+
* half-arrived `{"command":"npm ins` parses to `{ command: 'npm ins' }`,
|
|
5115
|
+
* letting the renderers show the structured input filling in live rather
|
|
5116
|
+
* than a raw JSON fragment. Returns null until enough has arrived to parse
|
|
5117
|
+
* (or when the text isn't an object/array), so callers keep their raw
|
|
5118
|
+
* `inputText` fallback for the first few characters.
|
|
5119
|
+
*/
|
|
5120
|
+
function parsePartialInput(text) {
|
|
5121
|
+
const trimmed = text.trimStart();
|
|
5122
|
+
if (trimmed[0] !== "{" && trimmed[0] !== "[") return null;
|
|
5123
|
+
try {
|
|
5124
|
+
return JSON.parse(fixJson(text));
|
|
5125
|
+
} catch (_error) {
|
|
5126
|
+
return null;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
/**
|
|
4562
5130
|
* `agentName` is the display name of the agent producing this run's stream.
|
|
4563
5131
|
* It's stamped onto each assistant-origin item so a multi-agent conversation
|
|
4564
5132
|
* can attribute live turns; pass null in a solo thread to leave items
|
|
@@ -4626,10 +5194,17 @@ function applyChunk(items, chunk, agentName = null) {
|
|
|
4626
5194
|
const toolCallId = str(chunk, "toolCallId");
|
|
4627
5195
|
const delta = str(chunk, "inputTextDelta") ?? str(chunk, "delta") ?? "";
|
|
4628
5196
|
if (!toolCallId) return [...items];
|
|
4629
|
-
return items.map((m) =>
|
|
4630
|
-
|
|
4631
|
-
inputText
|
|
4632
|
-
|
|
5197
|
+
return items.map((m) => {
|
|
5198
|
+
if (m.kind !== "tool" || m.id !== toolCallId) return m;
|
|
5199
|
+
const inputText = m.inputText + delta;
|
|
5200
|
+
const parsed = parsePartialInput(inputText);
|
|
5201
|
+
const input = parsed == null ? null : clampPayload(parsed);
|
|
5202
|
+
return {
|
|
5203
|
+
...m,
|
|
5204
|
+
inputText,
|
|
5205
|
+
input
|
|
5206
|
+
};
|
|
5207
|
+
});
|
|
4633
5208
|
}
|
|
4634
5209
|
case "tool-input-available": {
|
|
4635
5210
|
const toolCallId = str(chunk, "toolCallId");
|
|
@@ -5245,13 +5820,14 @@ const MAX_PR_CHIPS = 3;
|
|
|
5245
5820
|
*
|
|
5246
5821
|
* Caps at {@link MAX_PR_CHIPS} shown; any beyond that collapse into a trailing
|
|
5247
5822
|
* `+N` so the footer row can't be flooded by a run that links many PRs. Shows
|
|
5248
|
-
* the most recent (last-seen) PRs, since those are what's in play
|
|
5823
|
+
* the most recent (last-seen) PRs, since those are what's in play, and renders
|
|
5824
|
+
* them newest-first (reverse chronological) so the freshest PR leads the chip.
|
|
5249
5825
|
*/
|
|
5250
5826
|
function prLinksStyledText(prs) {
|
|
5251
5827
|
const dot = fg(theme.dim ?? "#565f89");
|
|
5252
5828
|
const label = fg(theme.muted ?? "#7a7a7a");
|
|
5253
5829
|
const accent = fg(theme.accent ?? "#7aa2f7");
|
|
5254
|
-
const shown = prs.slice(-MAX_PR_CHIPS);
|
|
5830
|
+
const shown = prs.slice(-MAX_PR_CHIPS).reverse();
|
|
5255
5831
|
const overflow = prs.length - shown.length;
|
|
5256
5832
|
const chunks = [dot("· "), label("PR ")];
|
|
5257
5833
|
shown.forEach((pr, i) => {
|
|
@@ -5313,7 +5889,7 @@ function useCompaction({ rest, conversationId }) {
|
|
|
5313
5889
|
}, [armIndicator, clearTimer]);
|
|
5314
5890
|
const compactConversation = useCallback(() => {
|
|
5315
5891
|
if (!rest || !conversationId) {
|
|
5316
|
-
useStore.getState().showToast({ message: "nothing to compact yet
|
|
5892
|
+
useStore.getState().showToast({ message: "nothing to compact yet; send a message first" });
|
|
5317
5893
|
return;
|
|
5318
5894
|
}
|
|
5319
5895
|
armIndicator();
|
|
@@ -5370,7 +5946,7 @@ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, mo
|
|
|
5370
5946
|
["app", appUrl ?? "unknown"],
|
|
5371
5947
|
["workspace", workspaceName ?? "unknown"],
|
|
5372
5948
|
["agent", agentName],
|
|
5373
|
-
["conversation", conversationId ?? "new
|
|
5949
|
+
["conversation", conversationId ?? "new (created when you send the first message)"],
|
|
5374
5950
|
["model", model ?? "default"],
|
|
5375
5951
|
["local access", formatShare(share)]
|
|
5376
5952
|
];
|
|
@@ -5378,9 +5954,9 @@ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, mo
|
|
|
5378
5954
|
return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
|
|
5379
5955
|
}
|
|
5380
5956
|
function formatShare(share) {
|
|
5381
|
-
if (share.status === "off") return "off
|
|
5957
|
+
if (share.status === "off") return "off (ctrl+t to share this machine)";
|
|
5382
5958
|
if (share.status !== "connected") return share.status;
|
|
5383
|
-
return share.grantedToAgent ? "shared
|
|
5959
|
+
return share.grantedToAgent ? "shared (this agent can run commands here)" : "shared (this agent is not granted)";
|
|
5384
5960
|
}
|
|
5385
5961
|
|
|
5386
5962
|
//#endregion
|
|
@@ -5433,49 +6009,6 @@ function createResponsePreview() {
|
|
|
5433
6009
|
};
|
|
5434
6010
|
}
|
|
5435
6011
|
|
|
5436
|
-
//#endregion
|
|
5437
|
-
//#region src/chat/tui/spinner-frames.ts
|
|
5438
|
-
/** Braille dot-cycle frames — the classic smooth terminal spinner. */
|
|
5439
|
-
const SPINNER_FRAMES = [
|
|
5440
|
-
"⠋",
|
|
5441
|
-
"⠙",
|
|
5442
|
-
"⠹",
|
|
5443
|
-
"⠸",
|
|
5444
|
-
"⠼",
|
|
5445
|
-
"⠴",
|
|
5446
|
-
"⠦",
|
|
5447
|
-
"⠧",
|
|
5448
|
-
"⠇",
|
|
5449
|
-
"⠏"
|
|
5450
|
-
];
|
|
5451
|
-
/** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
|
|
5452
|
-
const SPINNER_INTERVAL_MS = 80;
|
|
5453
|
-
/** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
|
|
5454
|
-
function spinnerFrame(tick) {
|
|
5455
|
-
const n = SPINNER_FRAMES.length;
|
|
5456
|
-
return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
|
|
5457
|
-
}
|
|
5458
|
-
|
|
5459
|
-
//#endregion
|
|
5460
|
-
//#region src/chat/tui/spinner.tsx
|
|
5461
|
-
/**
|
|
5462
|
-
* An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
|
|
5463
|
-
* a self-owned interval that's cleared on unmount, so the animation runs only
|
|
5464
|
-
* while the component is mounted. Returns a <span>, so render it inside a
|
|
5465
|
-
* <text> (matching how the rest of the TUI composes inline glyphs).
|
|
5466
|
-
*/
|
|
5467
|
-
function Spinner({ color }) {
|
|
5468
|
-
const [tick, setTick] = useState(0);
|
|
5469
|
-
useEffect(() => {
|
|
5470
|
-
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
|
|
5471
|
-
return () => clearInterval(id);
|
|
5472
|
-
}, []);
|
|
5473
|
-
return /* @__PURE__ */ jsx("span", {
|
|
5474
|
-
fg: color,
|
|
5475
|
-
children: spinnerFrame(tick)
|
|
5476
|
-
});
|
|
5477
|
-
}
|
|
5478
|
-
|
|
5479
6012
|
//#endregion
|
|
5480
6013
|
//#region src/chat/tui/prompt-history.ts
|
|
5481
6014
|
/**
|
|
@@ -6055,6 +6588,9 @@ function HelpModal({ onClose }) {
|
|
|
6055
6588
|
fg: theme.muted,
|
|
6056
6589
|
children: "search "
|
|
6057
6590
|
}), /* @__PURE__ */ jsx("input", {
|
|
6591
|
+
textColor: theme.fg,
|
|
6592
|
+
focusedTextColor: theme.fg,
|
|
6593
|
+
placeholderColor: theme.dim,
|
|
6058
6594
|
placeholder: "type to filter…",
|
|
6059
6595
|
focused: true,
|
|
6060
6596
|
width: Math.max(10, width - 12),
|
|
@@ -6094,7 +6630,7 @@ function HelpModal({ onClose }) {
|
|
|
6094
6630
|
children: [
|
|
6095
6631
|
"… ",
|
|
6096
6632
|
hidden,
|
|
6097
|
-
" more
|
|
6633
|
+
" more (search to narrow down)"
|
|
6098
6634
|
]
|
|
6099
6635
|
}) : null]
|
|
6100
6636
|
}),
|
|
@@ -6139,7 +6675,7 @@ function CompletionMenu({ items, highlight }) {
|
|
|
6139
6675
|
},
|
|
6140
6676
|
children: windowed.length === 0 ? /* @__PURE__ */ jsx("text", {
|
|
6141
6677
|
fg: theme.muted,
|
|
6142
|
-
children: "no matching command
|
|
6678
|
+
children: "no matching command (↵ sends as a message)"
|
|
6143
6679
|
}) : windowed.map((item, i) => {
|
|
6144
6680
|
const selected = start + i === highlight;
|
|
6145
6681
|
return /* @__PURE__ */ jsxs("text", {
|
|
@@ -6606,7 +7142,7 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
6606
7142
|
}),
|
|
6607
7143
|
modelLocked ? /* @__PURE__ */ jsx("text", {
|
|
6608
7144
|
fg: theme.warning,
|
|
6609
|
-
children: "this agent is locked to a Google-compliant model
|
|
7145
|
+
children: "this agent is locked to a Google-compliant model; changes will be rejected. disconnect Google to switch freely."
|
|
6610
7146
|
}) : null,
|
|
6611
7147
|
/* @__PURE__ */ jsxs("box", {
|
|
6612
7148
|
style: {
|
|
@@ -6618,6 +7154,9 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
6618
7154
|
fg: theme.muted,
|
|
6619
7155
|
children: "search "
|
|
6620
7156
|
}), /* @__PURE__ */ jsx("input", {
|
|
7157
|
+
textColor: theme.fg,
|
|
7158
|
+
focusedTextColor: theme.fg,
|
|
7159
|
+
placeholderColor: theme.dim,
|
|
6621
7160
|
placeholder: "type to filter…",
|
|
6622
7161
|
focused: true,
|
|
6623
7162
|
width: Math.max(10, width - 12),
|
|
@@ -6744,7 +7283,7 @@ function ThemePreview() {
|
|
|
6744
7283
|
/* @__PURE__ */ jsx("text", { children: " " }),
|
|
6745
7284
|
/* @__PURE__ */ jsx("markdown", {
|
|
6746
7285
|
content: [
|
|
6747
|
-
"Wrapped it in exponential backoff
|
|
7286
|
+
"Wrapped it in exponential backoff: three attempts, then it",
|
|
6748
7287
|
"throws the last error:",
|
|
6749
7288
|
"",
|
|
6750
7289
|
"```typescript",
|
|
@@ -6880,7 +7419,7 @@ function ThemePicker({ onClose }) {
|
|
|
6880
7419
|
},
|
|
6881
7420
|
children: /* @__PURE__ */ jsx("text", {
|
|
6882
7421
|
fg: theme.muted,
|
|
6883
|
-
children: "theme is pinned (NO_COLOR, --theme, or SKYDIVE_THEME)
|
|
7422
|
+
children: "theme is pinned (NO_COLOR, --theme, or SKYDIVE_THEME). unset it to pick interactively."
|
|
6884
7423
|
})
|
|
6885
7424
|
}),
|
|
6886
7425
|
/* @__PURE__ */ jsx("text", {
|
|
@@ -6924,7 +7463,7 @@ function ThemePicker({ onClose }) {
|
|
|
6924
7463
|
}),
|
|
6925
7464
|
/* @__PURE__ */ jsxs("text", {
|
|
6926
7465
|
fg: theme.muted,
|
|
6927
|
-
children: [themeMode, " themes
|
|
7466
|
+
children: [themeMode, " themes (following the terminal background)"]
|
|
6928
7467
|
}),
|
|
6929
7468
|
placement === "right" ? /* @__PURE__ */ jsxs("box", {
|
|
6930
7469
|
style: {
|
|
@@ -7562,6 +8101,9 @@ function ReviewFileSearch({ query, onQueryChange, placeholder = "filter files…
|
|
|
7562
8101
|
fg: theme.accent,
|
|
7563
8102
|
children: "/ "
|
|
7564
8103
|
}), /* @__PURE__ */ jsx("input", {
|
|
8104
|
+
textColor: theme.fg,
|
|
8105
|
+
focusedTextColor: theme.fg,
|
|
8106
|
+
placeholderColor: theme.dim,
|
|
7565
8107
|
placeholder,
|
|
7566
8108
|
focused: true,
|
|
7567
8109
|
width: "100%",
|
|
@@ -8281,6 +8823,9 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
8281
8823
|
height: 3
|
|
8282
8824
|
},
|
|
8283
8825
|
children: /* @__PURE__ */ jsx("input", {
|
|
8826
|
+
textColor: theme.fg,
|
|
8827
|
+
focusedTextColor: theme.fg,
|
|
8828
|
+
placeholderColor: theme.dim,
|
|
8284
8829
|
placeholder: "Leave a comment…",
|
|
8285
8830
|
focused: true,
|
|
8286
8831
|
width: Math.max(8, width - 10),
|
|
@@ -8636,7 +9181,7 @@ function ReviewPane({ agentId, conversationId, items, viewportWidth, viewportHei
|
|
|
8636
9181
|
if (ok) {
|
|
8637
9182
|
persist([]);
|
|
8638
9183
|
setStatus("copied to clipboard");
|
|
8639
|
-
} else setStatus("clipboard unavailable
|
|
9184
|
+
} else setStatus("clipboard unavailable; comments kept");
|
|
8640
9185
|
});
|
|
8641
9186
|
return;
|
|
8642
9187
|
}
|
|
@@ -8808,6 +9353,9 @@ function ReviewPane({ agentId, conversationId, items, viewportWidth, viewportHei
|
|
|
8808
9353
|
height: 3
|
|
8809
9354
|
},
|
|
8810
9355
|
children: /* @__PURE__ */ jsx("input", {
|
|
9356
|
+
textColor: theme.fg,
|
|
9357
|
+
focusedTextColor: theme.fg,
|
|
9358
|
+
placeholderColor: theme.dim,
|
|
8811
9359
|
placeholder: "Leave a comment…",
|
|
8812
9360
|
focused: true,
|
|
8813
9361
|
width: Math.max(8, sourceWidth - 8),
|
|
@@ -9562,9 +10110,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9562
10110
|
hadTodosRef.current = hasTodos;
|
|
9563
10111
|
}, [todos]);
|
|
9564
10112
|
const [historyLoaded, setHistoryLoaded] = useState(initialConversationId === null);
|
|
9565
|
-
const [input, setInput] = useState(
|
|
10113
|
+
const [input, setInput] = useState(() => useStore.getState().takeEarlyInput());
|
|
9566
10114
|
const [run, setRun] = useState({ kind: "idle" });
|
|
9567
10115
|
const [model, setModel] = useState(agent.model ?? null);
|
|
10116
|
+
const [modelLocked, setModelLocked] = useState(agent.modelLocked ?? false);
|
|
9568
10117
|
const [cardPickerOpen, setCardPickerOpen] = useState(false);
|
|
9569
10118
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
9570
10119
|
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
|
@@ -9863,6 +10412,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9863
10412
|
onCompactionEvent(event.status);
|
|
9864
10413
|
return;
|
|
9865
10414
|
}
|
|
10415
|
+
if (event.kind === "agent") {
|
|
10416
|
+
if (event.id !== agent.id) return;
|
|
10417
|
+
setModel(event.model);
|
|
10418
|
+
setModelLocked(event.modelLocked);
|
|
10419
|
+
return;
|
|
10420
|
+
}
|
|
9866
10421
|
if (event.kind === "conversation" && event.title) setChatTitle(event.title);
|
|
9867
10422
|
}
|
|
9868
10423
|
}).catch(() => {});
|
|
@@ -9872,6 +10427,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9872
10427
|
}, [
|
|
9873
10428
|
rest,
|
|
9874
10429
|
conversationId,
|
|
10430
|
+
agent.id,
|
|
9875
10431
|
setChatTitle,
|
|
9876
10432
|
attachToRun,
|
|
9877
10433
|
onCompactionEvent
|
|
@@ -9985,12 +10541,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9985
10541
|
if (autoGrantMachine && portalClient && portalStatus === "connected" && !portalClient.isGranted(agent.id)) try {
|
|
9986
10542
|
await portalClient.grantAgent(agent.id);
|
|
9987
10543
|
useStore.getState().showToast({
|
|
9988
|
-
message: `Shared this machine with ${agent.name} for the import. The grant persists
|
|
10544
|
+
message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent ${agent.name}`,
|
|
9989
10545
|
durationMs: 1e4
|
|
9990
10546
|
});
|
|
9991
10547
|
} catch (err) {
|
|
9992
10548
|
useStore.getState().showToast({
|
|
9993
|
-
message: `Couldn't grant machine access automatically (${errorMessage(err)})
|
|
10549
|
+
message: `Couldn't grant machine access automatically (${errorMessage(err)}). The agent will ask in chat instead.`,
|
|
9994
10550
|
variant: "warning"
|
|
9995
10551
|
});
|
|
9996
10552
|
}
|
|
@@ -10330,7 +10886,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10330
10886
|
setItems((prev) => [...prev, {
|
|
10331
10887
|
kind: "error",
|
|
10332
10888
|
id: crypto.randomUUID(),
|
|
10333
|
-
text: `attachment limit reached (${maxAttachments})
|
|
10889
|
+
text: `attachment limit reached (${maxAttachments}); ${fileName} skipped`
|
|
10334
10890
|
}]);
|
|
10335
10891
|
return;
|
|
10336
10892
|
}
|
|
@@ -10456,7 +11012,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10456
11012
|
const openInBrowser = useCallback(() => {
|
|
10457
11013
|
if (!appUrl) return;
|
|
10458
11014
|
if (!conversationId) {
|
|
10459
|
-
useStore.getState().showToast({ message: "no web page yet
|
|
11015
|
+
useStore.getState().showToast({ message: "no web page yet; send a message to start the conversation" });
|
|
10460
11016
|
return;
|
|
10461
11017
|
}
|
|
10462
11018
|
const url = `${resolveWebUrl(appUrl)}/c/${encodeURIComponent(conversationId)}`;
|
|
@@ -10466,7 +11022,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10466
11022
|
})).catch((err) => setItems((prev) => [...prev, {
|
|
10467
11023
|
kind: "error",
|
|
10468
11024
|
id: crypto.randomUUID(),
|
|
10469
|
-
text: `couldn't open a browser
|
|
11025
|
+
text: `couldn't open a browser; visit ${url} (${errorMessage(err)})`
|
|
10470
11026
|
}]));
|
|
10471
11027
|
}, [appUrl, conversationId]);
|
|
10472
11028
|
const forkAndContinue = useCallback(() => {
|
|
@@ -10512,7 +11068,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10512
11068
|
return;
|
|
10513
11069
|
}
|
|
10514
11070
|
if (!conversationId) {
|
|
10515
|
-
useStore.getState().showToast({ message: "nothing to archive yet
|
|
11071
|
+
useStore.getState().showToast({ message: "nothing to archive yet; send a message to start this one" });
|
|
10516
11072
|
return;
|
|
10517
11073
|
}
|
|
10518
11074
|
if (!rest) return;
|
|
@@ -10522,7 +11078,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10522
11078
|
}).then(() => {
|
|
10523
11079
|
useStore.getState().showToast({
|
|
10524
11080
|
variant: "success",
|
|
10525
|
-
message: "archived
|
|
11081
|
+
message: "archived (shift+tab in the picker shows it, ctrl+a restores)"
|
|
10526
11082
|
});
|
|
10527
11083
|
goTo({
|
|
10528
11084
|
kind: "conversation-picker",
|
|
@@ -10530,7 +11086,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10530
11086
|
});
|
|
10531
11087
|
}).catch((err) => useStore.getState().showToast({
|
|
10532
11088
|
variant: "error",
|
|
10533
|
-
message: err instanceof HttpError && err.status === 404 ? "can't archive this one
|
|
11089
|
+
message: err instanceof HttpError && err.status === 404 ? "can't archive this one; it's already gone from your list" : `couldn't archive: ${errorMessage(err)}`
|
|
10534
11090
|
}));
|
|
10535
11091
|
}, [
|
|
10536
11092
|
grantPrompt,
|
|
@@ -10554,7 +11110,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10554
11110
|
return;
|
|
10555
11111
|
}
|
|
10556
11112
|
if (!conversationId) {
|
|
10557
|
-
useStore.getState().showToast({ message: "nothing to rename yet
|
|
11113
|
+
useStore.getState().showToast({ message: "nothing to rename yet; send a message to start this one" });
|
|
10558
11114
|
return;
|
|
10559
11115
|
}
|
|
10560
11116
|
if (!rest) return;
|
|
@@ -10583,7 +11139,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10583
11139
|
const leaveForScreen = useCallback((next) => {
|
|
10584
11140
|
const current = runRef.current;
|
|
10585
11141
|
if (current.kind === "streaming") current.abort.abort();
|
|
10586
|
-
if (current.kind !== "idle") useStore.getState().showToast({ message: `${agent.name} keeps working
|
|
11142
|
+
if (current.kind !== "idle") useStore.getState().showToast({ message: `${agent.name} keeps working; reopen to reattach` });
|
|
10587
11143
|
goTo(next);
|
|
10588
11144
|
}, [agent.name, goTo]);
|
|
10589
11145
|
/**
|
|
@@ -10602,7 +11158,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10602
11158
|
setItems((prev) => [...prev, {
|
|
10603
11159
|
kind: "error",
|
|
10604
11160
|
id: crypto.randomUUID(),
|
|
10605
|
-
text: `not a conversation id: ${ref}
|
|
11161
|
+
text: `not a conversation id: ${ref}. pass an id, its /c/<id> link, or run /conversation on its own to pick from the list`
|
|
10606
11162
|
}]);
|
|
10607
11163
|
return;
|
|
10608
11164
|
}
|
|
@@ -10632,7 +11188,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10632
11188
|
]);
|
|
10633
11189
|
const toggleShare = useCallback(() => {
|
|
10634
11190
|
if (!portalClient) {
|
|
10635
|
-
useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session
|
|
11191
|
+
useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
|
|
10636
11192
|
return;
|
|
10637
11193
|
}
|
|
10638
11194
|
if (portal.status === "off") {
|
|
@@ -10675,7 +11231,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10675
11231
|
}, [agent.id, setPortalPrompt]);
|
|
10676
11232
|
const openPortalAccess = useCallback(() => {
|
|
10677
11233
|
if (!portalClient) {
|
|
10678
|
-
useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session
|
|
11234
|
+
useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
|
|
10679
11235
|
return;
|
|
10680
11236
|
}
|
|
10681
11237
|
switch (portal.status) {
|
|
@@ -10764,7 +11320,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10764
11320
|
break;
|
|
10765
11321
|
}
|
|
10766
11322
|
if (!conversationId) {
|
|
10767
|
-
useStore.getState().showToast({ message: "nothing to fork yet
|
|
11323
|
+
useStore.getState().showToast({ message: "nothing to fork yet; send a message first" });
|
|
10768
11324
|
break;
|
|
10769
11325
|
}
|
|
10770
11326
|
forkAndContinue();
|
|
@@ -11095,7 +11651,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11095
11651
|
if (runRef.current.kind === "streaming") {
|
|
11096
11652
|
if (cancelLatestSteer()) return;
|
|
11097
11653
|
runRef.current.abort.abort();
|
|
11098
|
-
useStore.getState().showToast({ message: `${agent.name} keeps working
|
|
11654
|
+
useStore.getState().showToast({ message: `${agent.name} keeps working; reopen to reattach` });
|
|
11099
11655
|
goTo({
|
|
11100
11656
|
kind: "conversation-picker",
|
|
11101
11657
|
agent
|
|
@@ -11109,6 +11665,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11109
11665
|
}
|
|
11110
11666
|
});
|
|
11111
11667
|
const hasPendingSteer = items.some((m) => m.kind === "pending-steer");
|
|
11668
|
+
const conversationContext = useMemo(() => ({
|
|
11669
|
+
rest,
|
|
11670
|
+
conversationId
|
|
11671
|
+
}), [rest, conversationId]);
|
|
11112
11672
|
const actionableCards = useMemo(() => items.filter(isActionableCard), [items]);
|
|
11113
11673
|
const statusText = useMemo(() => formatChatFooterStatus({
|
|
11114
11674
|
runKind: run.kind,
|
|
@@ -11137,7 +11697,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11137
11697
|
if (modelPickerOpen) return /* @__PURE__ */ jsx(ModelPicker, {
|
|
11138
11698
|
agentId: agent.id,
|
|
11139
11699
|
currentModel: model,
|
|
11140
|
-
modelLocked
|
|
11700
|
+
modelLocked,
|
|
11141
11701
|
onSelected: (modelId) => {
|
|
11142
11702
|
setModel(modelId);
|
|
11143
11703
|
setModelPickerOpen(false);
|
|
@@ -11191,7 +11751,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11191
11751
|
setReviewFocused(false);
|
|
11192
11752
|
}
|
|
11193
11753
|
}) : null;
|
|
11194
|
-
const
|
|
11754
|
+
const chatViewBody = /* @__PURE__ */ jsxs("box", {
|
|
11195
11755
|
onMouseDown: (event) => {
|
|
11196
11756
|
const paneBox = reviewBelowBoxRef.current;
|
|
11197
11757
|
if (reviewOpen && reviewPlacement === "below" && paneBox && event.y === paneBox.screenY + paneBox.height - 1) {
|
|
@@ -11439,6 +11999,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11439
11999
|
},
|
|
11440
12000
|
children: /* @__PURE__ */ jsx("textarea", {
|
|
11441
12001
|
ref: composerRef,
|
|
12002
|
+
textColor: theme.fg,
|
|
12003
|
+
focusedTextColor: theme.fg,
|
|
12004
|
+
placeholderColor: theme.dim,
|
|
11442
12005
|
placeholder: forking ? "forking into a new chat…" : `message ${agent.name}…`,
|
|
11443
12006
|
keyBindings: composerKeyBindings,
|
|
11444
12007
|
onContentChange: handleComposerChange,
|
|
@@ -11478,6 +12041,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11478
12041
|
})
|
|
11479
12042
|
]
|
|
11480
12043
|
});
|
|
12044
|
+
const chatView = /* @__PURE__ */ jsx(ConversationContext.Provider, {
|
|
12045
|
+
value: conversationContext,
|
|
12046
|
+
children: chatViewBody
|
|
12047
|
+
});
|
|
11481
12048
|
if (!reviewView || reviewPlacement === "below") return chatView;
|
|
11482
12049
|
const fullscreen = reviewPlacement === "fullscreen";
|
|
11483
12050
|
return /* @__PURE__ */ jsxs("box", {
|
|
@@ -11571,12 +12138,18 @@ function CredentialPromptBox({ prompt, height, onInput, onSubmit }) {
|
|
|
11571
12138
|
]
|
|
11572
12139
|
}),
|
|
11573
12140
|
field.secret ? /* @__PURE__ */ jsx("input", {
|
|
12141
|
+
textColor: theme.fg,
|
|
12142
|
+
focusedTextColor: theme.fg,
|
|
12143
|
+
placeholderColor: theme.dim,
|
|
11574
12144
|
value: MASK_CHAR.repeat(prompt.current.length),
|
|
11575
12145
|
placeholder: field.placeholder ?? "",
|
|
11576
12146
|
onInput,
|
|
11577
12147
|
onSubmit,
|
|
11578
12148
|
focused: !prompt.submitting
|
|
11579
12149
|
}, field.key) : /* @__PURE__ */ jsx("input", {
|
|
12150
|
+
textColor: theme.fg,
|
|
12151
|
+
focusedTextColor: theme.fg,
|
|
12152
|
+
placeholderColor: theme.dim,
|
|
11580
12153
|
placeholder: field.placeholder ?? "",
|
|
11581
12154
|
onInput,
|
|
11582
12155
|
onSubmit,
|
|
@@ -11816,7 +12389,12 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
11816
12389
|
setClients,
|
|
11817
12390
|
fail
|
|
11818
12391
|
]);
|
|
12392
|
+
const appendEarlyInput = useStore((s) => s.appendEarlyInput);
|
|
11819
12393
|
useKeyboard((key) => {
|
|
12394
|
+
if (screen.kind === "splash" && !(key.name === "c" && key.ctrl)) {
|
|
12395
|
+
appendEarlyInput(key);
|
|
12396
|
+
return;
|
|
12397
|
+
}
|
|
11820
12398
|
if (key.name === "c" && key.ctrl && screen.kind !== "chat") {
|
|
11821
12399
|
useStore.getState().portalClient?.dispose();
|
|
11822
12400
|
agentHost.dispose();
|
|
@@ -12284,6 +12862,97 @@ async function settleBootThemeMode({ source, applyThemeMode, graceMs = THEME_QUE
|
|
|
12284
12862
|
if (answered) applyThemeMode(answered);
|
|
12285
12863
|
}
|
|
12286
12864
|
|
|
12865
|
+
//#endregion
|
|
12866
|
+
//#region src/profiling/tui.ts
|
|
12867
|
+
/**
|
|
12868
|
+
* TUI-side profiling instrumentation. Everything here rides on the shared
|
|
12869
|
+
* profile directory created in bin.ts (see profiler.ts) and is inert unless
|
|
12870
|
+
* SKYDIVE_PROFILE is set.
|
|
12871
|
+
*/
|
|
12872
|
+
/**
|
|
12873
|
+
* Renderer options to merge into createCliRenderer when profiling:
|
|
12874
|
+
* gatherStats makes OpenTUI retain per-frame timings (frame time, cells
|
|
12875
|
+
* updated, native render + stdout write time) that getStats() exposes.
|
|
12876
|
+
*/
|
|
12877
|
+
function profilingRendererOptions() {
|
|
12878
|
+
return profilingEnabled() ? { gatherStats: true } : {};
|
|
12879
|
+
}
|
|
12880
|
+
/**
|
|
12881
|
+
* Samples renderer stats periodically and dumps the full series at exit.
|
|
12882
|
+
* The periodic samples give the timeline shape (fps over the session); the
|
|
12883
|
+
* final dump carries the complete frame-time distribution.
|
|
12884
|
+
*/
|
|
12885
|
+
function startRendererStatsCapture(renderer) {
|
|
12886
|
+
if (!profilingEnabled()) return;
|
|
12887
|
+
const sample = () => {
|
|
12888
|
+
const s = renderer.getStats();
|
|
12889
|
+
record("render", {
|
|
12890
|
+
fps: s.fps,
|
|
12891
|
+
frameCount: s.frameCount,
|
|
12892
|
+
averageFrameTime: s.averageFrameTime,
|
|
12893
|
+
maxFrameTime: s.maxFrameTime,
|
|
12894
|
+
cellsUpdated: s.cellsUpdated,
|
|
12895
|
+
nativeRenderTime: s.nativeRenderTime ?? null,
|
|
12896
|
+
nativeStdoutWriteTime: s.nativeStdoutWriteTime ?? null
|
|
12897
|
+
});
|
|
12898
|
+
};
|
|
12899
|
+
const interval = setInterval(sample, 1e3);
|
|
12900
|
+
interval.unref?.();
|
|
12901
|
+
process.on("exit", () => {
|
|
12902
|
+
clearInterval(interval);
|
|
12903
|
+
const s = renderer.getStats();
|
|
12904
|
+
writeArtifact("render.json", {
|
|
12905
|
+
fps: s.fps,
|
|
12906
|
+
frameCount: s.frameCount,
|
|
12907
|
+
frameTimes: s.frameTimes,
|
|
12908
|
+
averageFrameTime: s.averageFrameTime,
|
|
12909
|
+
minFrameTime: s.minFrameTime,
|
|
12910
|
+
maxFrameTime: s.maxFrameTime,
|
|
12911
|
+
cellsUpdated: s.cellsUpdated,
|
|
12912
|
+
averageCellsUpdated: s.averageCellsUpdated
|
|
12913
|
+
});
|
|
12914
|
+
});
|
|
12915
|
+
}
|
|
12916
|
+
/**
|
|
12917
|
+
* Records zustand store transitions. Subscribes to the whole store and
|
|
12918
|
+
* logs a compact diff-flavored event per change: the screen kind always,
|
|
12919
|
+
* plus which top-level keys changed. Gives an agent the "what was the UI
|
|
12920
|
+
* doing at t=X" axis without serializing message payloads.
|
|
12921
|
+
*/
|
|
12922
|
+
function startStateCapture(store) {
|
|
12923
|
+
if (!profilingEnabled()) return;
|
|
12924
|
+
store.subscribe((state, prev) => {
|
|
12925
|
+
const prevEntries = new Map(Object.entries(prev));
|
|
12926
|
+
const changed = [];
|
|
12927
|
+
let screenKind = null;
|
|
12928
|
+
for (const [key, value] of Object.entries(state)) {
|
|
12929
|
+
if (typeof value === "function") continue;
|
|
12930
|
+
if (value !== prevEntries.get(key)) changed.push(key);
|
|
12931
|
+
if (key === "screen" && typeof value === "object" && value !== null && "kind" in value && typeof value.kind === "string") screenKind = value.kind;
|
|
12932
|
+
}
|
|
12933
|
+
if (changed.length === 0) return;
|
|
12934
|
+
record("state", {
|
|
12935
|
+
screen: screenKind,
|
|
12936
|
+
changed
|
|
12937
|
+
});
|
|
12938
|
+
});
|
|
12939
|
+
}
|
|
12940
|
+
/**
|
|
12941
|
+
* React Profiler onRender callback for the <Profiler> wrapping <App>.
|
|
12942
|
+
* One event per commit: which subtree, mount/update, and actual vs base
|
|
12943
|
+
* duration (actualDuration >> baseDuration means memoization is losing).
|
|
12944
|
+
*/
|
|
12945
|
+
function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, commitTime) {
|
|
12946
|
+
record("commits", {
|
|
12947
|
+
id,
|
|
12948
|
+
phase,
|
|
12949
|
+
actualDuration,
|
|
12950
|
+
baseDuration,
|
|
12951
|
+
startTime,
|
|
12952
|
+
commitTime
|
|
12953
|
+
});
|
|
12954
|
+
}
|
|
12955
|
+
|
|
12287
12956
|
//#endregion
|
|
12288
12957
|
//#region src/chat/boot.tsx
|
|
12289
12958
|
/**
|
|
@@ -12296,7 +12965,12 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
12296
12965
|
await registerGrammars();
|
|
12297
12966
|
const watchdog = startMemoryWatchdog();
|
|
12298
12967
|
installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
|
|
12299
|
-
const renderer = await createCliRenderer({
|
|
12968
|
+
const renderer = await createCliRenderer({
|
|
12969
|
+
exitOnCtrlC: false,
|
|
12970
|
+
...profilingRendererOptions()
|
|
12971
|
+
});
|
|
12972
|
+
startRendererStatsCapture(renderer);
|
|
12973
|
+
startStateCapture(useStore);
|
|
12300
12974
|
extend({ markdown: LinkifiedMarkdownRenderable });
|
|
12301
12975
|
useStore.setState({
|
|
12302
12976
|
authKind,
|
|
@@ -12316,16 +12990,20 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
12316
12990
|
source: renderer,
|
|
12317
12991
|
applyThemeMode: (mode) => useStore.getState().setThemeMode(mode)
|
|
12318
12992
|
});
|
|
12319
|
-
createRoot(renderer).render(/* @__PURE__ */ jsx(
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12993
|
+
createRoot(renderer).render(/* @__PURE__ */ jsx(Profiler, {
|
|
12994
|
+
id: "app",
|
|
12995
|
+
onRender: onRenderCommit,
|
|
12996
|
+
children: /* @__PURE__ */ jsx(App, {
|
|
12997
|
+
appUrl,
|
|
12998
|
+
sessionToken,
|
|
12999
|
+
authKind,
|
|
13000
|
+
shareMachine,
|
|
13001
|
+
promptHistoryPath,
|
|
13002
|
+
notifications,
|
|
13003
|
+
agentSelector: agentSelector ?? null,
|
|
13004
|
+
conversationId: conversationId ?? null,
|
|
13005
|
+
newConversation
|
|
13006
|
+
})
|
|
12329
13007
|
}));
|
|
12330
13008
|
}
|
|
12331
13009
|
/** Open the same workspace picker used by the initial agent screen. */
|