skydive-cli 0.4.0 → 0.4.1-beta.2
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-on5ucI_Z.mjs → boot-DZYbTxev.mjs} +988 -259
- package/dist/js/{client-CKzK-12y.mjs → client-Cf7cvftg.mjs} +19 -6
- package/dist/js/client-CneLVPei.mjs +4 -0
- package/dist/js/{daemon-CaFz7Wtw.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-X2N5wJ5l.mjs → print-CbayCa87.mjs} +6 -5
- package/dist/js/{print-BXNq9UxE.mjs → print-ba_0hiV9.mjs} +3 -3
- package/dist/js/{print-share-D72CfWJk.mjs → print-share-CT4ltX7u.mjs} +3 -3
- package/dist/js/{install-Ba-UTi-l.mjs → profiler-BA-c3-gP.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-imDZZGQA.mjs → rest-BY2nADw5.mjs} +24 -10
- package/dist/js/rest-DADJh0bi.mjs +6 -0
- package/package.json +3 -2
- package/dist/js/client-D6apKmV0.mjs +0 -6
- package/dist/js/daemon-BADmpJAs.mjs +0 -7
- package/dist/js/raw-pty-t5DC-e1T.mjs +0 -5
- package/dist/js/rest-Br2eFTlj.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-BA-c3-gP.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
|
/**
|
|
@@ -5728,6 +6261,10 @@ const keybindGroups = [
|
|
|
5728
6261
|
keys: "/plan [show|hide]",
|
|
5729
6262
|
action: "show / hide the pinned plan card"
|
|
5730
6263
|
},
|
|
6264
|
+
{
|
|
6265
|
+
keys: "/rename <title>",
|
|
6266
|
+
action: "rename this conversation (for everyone)"
|
|
6267
|
+
},
|
|
5731
6268
|
{
|
|
5732
6269
|
keys: "/",
|
|
5733
6270
|
action: "open the command menu (start of message)"
|
|
@@ -6051,6 +6588,9 @@ function HelpModal({ onClose }) {
|
|
|
6051
6588
|
fg: theme.muted,
|
|
6052
6589
|
children: "search "
|
|
6053
6590
|
}), /* @__PURE__ */ jsx("input", {
|
|
6591
|
+
textColor: theme.fg,
|
|
6592
|
+
focusedTextColor: theme.fg,
|
|
6593
|
+
placeholderColor: theme.dim,
|
|
6054
6594
|
placeholder: "type to filter…",
|
|
6055
6595
|
focused: true,
|
|
6056
6596
|
width: Math.max(10, width - 12),
|
|
@@ -6090,7 +6630,7 @@ function HelpModal({ onClose }) {
|
|
|
6090
6630
|
children: [
|
|
6091
6631
|
"… ",
|
|
6092
6632
|
hidden,
|
|
6093
|
-
" more
|
|
6633
|
+
" more (search to narrow down)"
|
|
6094
6634
|
]
|
|
6095
6635
|
}) : null]
|
|
6096
6636
|
}),
|
|
@@ -6135,7 +6675,7 @@ function CompletionMenu({ items, highlight }) {
|
|
|
6135
6675
|
},
|
|
6136
6676
|
children: windowed.length === 0 ? /* @__PURE__ */ jsx("text", {
|
|
6137
6677
|
fg: theme.muted,
|
|
6138
|
-
children: "no matching command
|
|
6678
|
+
children: "no matching command (↵ sends as a message)"
|
|
6139
6679
|
}) : windowed.map((item, i) => {
|
|
6140
6680
|
const selected = start + i === highlight;
|
|
6141
6681
|
return /* @__PURE__ */ jsxs("text", {
|
|
@@ -6188,6 +6728,13 @@ const slashCommands = [
|
|
|
6188
6728
|
description: "archive this conversation and hide it from your list",
|
|
6189
6729
|
action: { kind: "archive-conversation" }
|
|
6190
6730
|
},
|
|
6731
|
+
{
|
|
6732
|
+
name: "rename",
|
|
6733
|
+
aliases: ["title"],
|
|
6734
|
+
argsHint: "<new title>",
|
|
6735
|
+
description: "rename this conversation (for everyone)",
|
|
6736
|
+
action: { kind: "rename-conversation" }
|
|
6737
|
+
},
|
|
6191
6738
|
{
|
|
6192
6739
|
name: "conversation",
|
|
6193
6740
|
aliases: ["conversations", "conv"],
|
|
@@ -6595,7 +7142,7 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
6595
7142
|
}),
|
|
6596
7143
|
modelLocked ? /* @__PURE__ */ jsx("text", {
|
|
6597
7144
|
fg: theme.warning,
|
|
6598
|
-
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."
|
|
6599
7146
|
}) : null,
|
|
6600
7147
|
/* @__PURE__ */ jsxs("box", {
|
|
6601
7148
|
style: {
|
|
@@ -6607,6 +7154,9 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
6607
7154
|
fg: theme.muted,
|
|
6608
7155
|
children: "search "
|
|
6609
7156
|
}), /* @__PURE__ */ jsx("input", {
|
|
7157
|
+
textColor: theme.fg,
|
|
7158
|
+
focusedTextColor: theme.fg,
|
|
7159
|
+
placeholderColor: theme.dim,
|
|
6610
7160
|
placeholder: "type to filter…",
|
|
6611
7161
|
focused: true,
|
|
6612
7162
|
width: Math.max(10, width - 12),
|
|
@@ -6733,7 +7283,7 @@ function ThemePreview() {
|
|
|
6733
7283
|
/* @__PURE__ */ jsx("text", { children: " " }),
|
|
6734
7284
|
/* @__PURE__ */ jsx("markdown", {
|
|
6735
7285
|
content: [
|
|
6736
|
-
"Wrapped it in exponential backoff
|
|
7286
|
+
"Wrapped it in exponential backoff: three attempts, then it",
|
|
6737
7287
|
"throws the last error:",
|
|
6738
7288
|
"",
|
|
6739
7289
|
"```typescript",
|
|
@@ -6869,7 +7419,7 @@ function ThemePicker({ onClose }) {
|
|
|
6869
7419
|
},
|
|
6870
7420
|
children: /* @__PURE__ */ jsx("text", {
|
|
6871
7421
|
fg: theme.muted,
|
|
6872
|
-
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."
|
|
6873
7423
|
})
|
|
6874
7424
|
}),
|
|
6875
7425
|
/* @__PURE__ */ jsx("text", {
|
|
@@ -6913,7 +7463,7 @@ function ThemePicker({ onClose }) {
|
|
|
6913
7463
|
}),
|
|
6914
7464
|
/* @__PURE__ */ jsxs("text", {
|
|
6915
7465
|
fg: theme.muted,
|
|
6916
|
-
children: [themeMode, " themes
|
|
7466
|
+
children: [themeMode, " themes (following the terminal background)"]
|
|
6917
7467
|
}),
|
|
6918
7468
|
placement === "right" ? /* @__PURE__ */ jsxs("box", {
|
|
6919
7469
|
style: {
|
|
@@ -7551,6 +8101,9 @@ function ReviewFileSearch({ query, onQueryChange, placeholder = "filter files…
|
|
|
7551
8101
|
fg: theme.accent,
|
|
7552
8102
|
children: "/ "
|
|
7553
8103
|
}), /* @__PURE__ */ jsx("input", {
|
|
8104
|
+
textColor: theme.fg,
|
|
8105
|
+
focusedTextColor: theme.fg,
|
|
8106
|
+
placeholderColor: theme.dim,
|
|
7554
8107
|
placeholder,
|
|
7555
8108
|
focused: true,
|
|
7556
8109
|
width: "100%",
|
|
@@ -8270,6 +8823,9 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
8270
8823
|
height: 3
|
|
8271
8824
|
},
|
|
8272
8825
|
children: /* @__PURE__ */ jsx("input", {
|
|
8826
|
+
textColor: theme.fg,
|
|
8827
|
+
focusedTextColor: theme.fg,
|
|
8828
|
+
placeholderColor: theme.dim,
|
|
8273
8829
|
placeholder: "Leave a comment…",
|
|
8274
8830
|
focused: true,
|
|
8275
8831
|
width: Math.max(8, width - 10),
|
|
@@ -8625,7 +9181,7 @@ function ReviewPane({ agentId, conversationId, items, viewportWidth, viewportHei
|
|
|
8625
9181
|
if (ok) {
|
|
8626
9182
|
persist([]);
|
|
8627
9183
|
setStatus("copied to clipboard");
|
|
8628
|
-
} else setStatus("clipboard unavailable
|
|
9184
|
+
} else setStatus("clipboard unavailable; comments kept");
|
|
8629
9185
|
});
|
|
8630
9186
|
return;
|
|
8631
9187
|
}
|
|
@@ -8797,6 +9353,9 @@ function ReviewPane({ agentId, conversationId, items, viewportWidth, viewportHei
|
|
|
8797
9353
|
height: 3
|
|
8798
9354
|
},
|
|
8799
9355
|
children: /* @__PURE__ */ jsx("input", {
|
|
9356
|
+
textColor: theme.fg,
|
|
9357
|
+
focusedTextColor: theme.fg,
|
|
9358
|
+
placeholderColor: theme.dim,
|
|
8800
9359
|
placeholder: "Leave a comment…",
|
|
8801
9360
|
focused: true,
|
|
8802
9361
|
width: Math.max(8, sourceWidth - 8),
|
|
@@ -9321,6 +9880,10 @@ function routeInput(raw) {
|
|
|
9321
9880
|
kind: "open-conversation",
|
|
9322
9881
|
ref: match.rest
|
|
9323
9882
|
} : { kind: "conversation-picker" };
|
|
9883
|
+
case "rename-conversation": return {
|
|
9884
|
+
kind: "rename-conversation",
|
|
9885
|
+
title: match.rest
|
|
9886
|
+
};
|
|
9324
9887
|
case "plan": {
|
|
9325
9888
|
const arg = match.rest.toLowerCase();
|
|
9326
9889
|
return {
|
|
@@ -9547,9 +10110,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9547
10110
|
hadTodosRef.current = hasTodos;
|
|
9548
10111
|
}, [todos]);
|
|
9549
10112
|
const [historyLoaded, setHistoryLoaded] = useState(initialConversationId === null);
|
|
9550
|
-
const [input, setInput] = useState(
|
|
10113
|
+
const [input, setInput] = useState(() => useStore.getState().takeEarlyInput());
|
|
9551
10114
|
const [run, setRun] = useState({ kind: "idle" });
|
|
9552
10115
|
const [model, setModel] = useState(agent.model ?? null);
|
|
10116
|
+
const [modelLocked, setModelLocked] = useState(agent.modelLocked ?? false);
|
|
9553
10117
|
const [cardPickerOpen, setCardPickerOpen] = useState(false);
|
|
9554
10118
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
9555
10119
|
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
|
@@ -9848,6 +10412,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9848
10412
|
onCompactionEvent(event.status);
|
|
9849
10413
|
return;
|
|
9850
10414
|
}
|
|
10415
|
+
if (event.kind === "agent") {
|
|
10416
|
+
if (event.id !== agent.id) return;
|
|
10417
|
+
setModel(event.model);
|
|
10418
|
+
setModelLocked(event.modelLocked);
|
|
10419
|
+
return;
|
|
10420
|
+
}
|
|
9851
10421
|
if (event.kind === "conversation" && event.title) setChatTitle(event.title);
|
|
9852
10422
|
}
|
|
9853
10423
|
}).catch(() => {});
|
|
@@ -9857,6 +10427,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9857
10427
|
}, [
|
|
9858
10428
|
rest,
|
|
9859
10429
|
conversationId,
|
|
10430
|
+
agent.id,
|
|
9860
10431
|
setChatTitle,
|
|
9861
10432
|
attachToRun,
|
|
9862
10433
|
onCompactionEvent
|
|
@@ -9970,12 +10541,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9970
10541
|
if (autoGrantMachine && portalClient && portalStatus === "connected" && !portalClient.isGranted(agent.id)) try {
|
|
9971
10542
|
await portalClient.grantAgent(agent.id);
|
|
9972
10543
|
useStore.getState().showToast({
|
|
9973
|
-
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}`,
|
|
9974
10545
|
durationMs: 1e4
|
|
9975
10546
|
});
|
|
9976
10547
|
} catch (err) {
|
|
9977
10548
|
useStore.getState().showToast({
|
|
9978
|
-
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.`,
|
|
9979
10550
|
variant: "warning"
|
|
9980
10551
|
});
|
|
9981
10552
|
}
|
|
@@ -10315,7 +10886,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10315
10886
|
setItems((prev) => [...prev, {
|
|
10316
10887
|
kind: "error",
|
|
10317
10888
|
id: crypto.randomUUID(),
|
|
10318
|
-
text: `attachment limit reached (${maxAttachments})
|
|
10889
|
+
text: `attachment limit reached (${maxAttachments}); ${fileName} skipped`
|
|
10319
10890
|
}]);
|
|
10320
10891
|
return;
|
|
10321
10892
|
}
|
|
@@ -10441,7 +11012,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10441
11012
|
const openInBrowser = useCallback(() => {
|
|
10442
11013
|
if (!appUrl) return;
|
|
10443
11014
|
if (!conversationId) {
|
|
10444
|
-
useStore.getState().showToast({ message: "no web page yet
|
|
11015
|
+
useStore.getState().showToast({ message: "no web page yet; send a message to start the conversation" });
|
|
10445
11016
|
return;
|
|
10446
11017
|
}
|
|
10447
11018
|
const url = `${resolveWebUrl(appUrl)}/c/${encodeURIComponent(conversationId)}`;
|
|
@@ -10451,7 +11022,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10451
11022
|
})).catch((err) => setItems((prev) => [...prev, {
|
|
10452
11023
|
kind: "error",
|
|
10453
11024
|
id: crypto.randomUUID(),
|
|
10454
|
-
text: `couldn't open a browser
|
|
11025
|
+
text: `couldn't open a browser; visit ${url} (${errorMessage(err)})`
|
|
10455
11026
|
}]));
|
|
10456
11027
|
}, [appUrl, conversationId]);
|
|
10457
11028
|
const forkAndContinue = useCallback(() => {
|
|
@@ -10497,7 +11068,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10497
11068
|
return;
|
|
10498
11069
|
}
|
|
10499
11070
|
if (!conversationId) {
|
|
10500
|
-
useStore.getState().showToast({ message: "nothing to archive yet
|
|
11071
|
+
useStore.getState().showToast({ message: "nothing to archive yet; send a message to start this one" });
|
|
10501
11072
|
return;
|
|
10502
11073
|
}
|
|
10503
11074
|
if (!rest) return;
|
|
@@ -10507,7 +11078,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10507
11078
|
}).then(() => {
|
|
10508
11079
|
useStore.getState().showToast({
|
|
10509
11080
|
variant: "success",
|
|
10510
|
-
message: "archived
|
|
11081
|
+
message: "archived (shift+tab in the picker shows it, ctrl+a restores)"
|
|
10511
11082
|
});
|
|
10512
11083
|
goTo({
|
|
10513
11084
|
kind: "conversation-picker",
|
|
@@ -10515,7 +11086,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10515
11086
|
});
|
|
10516
11087
|
}).catch((err) => useStore.getState().showToast({
|
|
10517
11088
|
variant: "error",
|
|
10518
|
-
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)}`
|
|
10519
11090
|
}));
|
|
10520
11091
|
}, [
|
|
10521
11092
|
grantPrompt,
|
|
@@ -10525,6 +11096,38 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10525
11096
|
goTo,
|
|
10526
11097
|
agent
|
|
10527
11098
|
]);
|
|
11099
|
+
const renameConversation = useCallback((rawTitle) => {
|
|
11100
|
+
const title = rawTitle.trim();
|
|
11101
|
+
if (!title) {
|
|
11102
|
+
useStore.getState().showToast({ message: "usage: /rename <new title>" });
|
|
11103
|
+
return;
|
|
11104
|
+
}
|
|
11105
|
+
if (title.length > 100) {
|
|
11106
|
+
useStore.getState().showToast({
|
|
11107
|
+
variant: "error",
|
|
11108
|
+
message: `title is too long (${String(title.length)}/100 characters)`
|
|
11109
|
+
});
|
|
11110
|
+
return;
|
|
11111
|
+
}
|
|
11112
|
+
if (!conversationId) {
|
|
11113
|
+
useStore.getState().showToast({ message: "nothing to rename yet; send a message to start this one" });
|
|
11114
|
+
return;
|
|
11115
|
+
}
|
|
11116
|
+
if (!rest) return;
|
|
11117
|
+
rest.renameConversation({
|
|
11118
|
+
conversationId,
|
|
11119
|
+
title
|
|
11120
|
+
}).then(() => {
|
|
11121
|
+
useStore.getState().setChatTitle(title);
|
|
11122
|
+
useStore.getState().showToast({
|
|
11123
|
+
variant: "success",
|
|
11124
|
+
message: `renamed to “${title}”`
|
|
11125
|
+
});
|
|
11126
|
+
}).catch((err) => useStore.getState().showToast({
|
|
11127
|
+
variant: "error",
|
|
11128
|
+
message: `couldn't rename: ${errorMessage(err)}`
|
|
11129
|
+
}));
|
|
11130
|
+
}, [conversationId, rest]);
|
|
10528
11131
|
/**
|
|
10529
11132
|
* Leave this conversation for another screen without ending its run: drop
|
|
10530
11133
|
* our own SSE subscription and nothing else — never `rest.cancelRun` — so
|
|
@@ -10536,7 +11139,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10536
11139
|
const leaveForScreen = useCallback((next) => {
|
|
10537
11140
|
const current = runRef.current;
|
|
10538
11141
|
if (current.kind === "streaming") current.abort.abort();
|
|
10539
|
-
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` });
|
|
10540
11143
|
goTo(next);
|
|
10541
11144
|
}, [agent.name, goTo]);
|
|
10542
11145
|
/**
|
|
@@ -10555,7 +11158,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10555
11158
|
setItems((prev) => [...prev, {
|
|
10556
11159
|
kind: "error",
|
|
10557
11160
|
id: crypto.randomUUID(),
|
|
10558
|
-
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`
|
|
10559
11162
|
}]);
|
|
10560
11163
|
return;
|
|
10561
11164
|
}
|
|
@@ -10585,7 +11188,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10585
11188
|
]);
|
|
10586
11189
|
const toggleShare = useCallback(() => {
|
|
10587
11190
|
if (!portalClient) {
|
|
10588
|
-
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" });
|
|
10589
11192
|
return;
|
|
10590
11193
|
}
|
|
10591
11194
|
if (portal.status === "off") {
|
|
@@ -10628,7 +11231,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10628
11231
|
}, [agent.id, setPortalPrompt]);
|
|
10629
11232
|
const openPortalAccess = useCallback(() => {
|
|
10630
11233
|
if (!portalClient) {
|
|
10631
|
-
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" });
|
|
10632
11235
|
return;
|
|
10633
11236
|
}
|
|
10634
11237
|
switch (portal.status) {
|
|
@@ -10693,6 +11296,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10693
11296
|
case "archive-conversation":
|
|
10694
11297
|
archiveConversation();
|
|
10695
11298
|
break;
|
|
11299
|
+
case "rename-conversation":
|
|
11300
|
+
renameConversation(routed.title);
|
|
11301
|
+
break;
|
|
10696
11302
|
case "new-conversation":
|
|
10697
11303
|
if (runRef.current.kind !== "idle") {
|
|
10698
11304
|
useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
|
|
@@ -10714,7 +11320,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10714
11320
|
break;
|
|
10715
11321
|
}
|
|
10716
11322
|
if (!conversationId) {
|
|
10717
|
-
useStore.getState().showToast({ message: "nothing to fork yet
|
|
11323
|
+
useStore.getState().showToast({ message: "nothing to fork yet; send a message first" });
|
|
10718
11324
|
break;
|
|
10719
11325
|
}
|
|
10720
11326
|
forkAndContinue();
|
|
@@ -10806,6 +11412,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10806
11412
|
runSandboxExec,
|
|
10807
11413
|
runSandboxPty,
|
|
10808
11414
|
archiveConversation,
|
|
11415
|
+
renameConversation,
|
|
10809
11416
|
forkAndContinue,
|
|
10810
11417
|
leaveForScreen,
|
|
10811
11418
|
openConversationRef,
|
|
@@ -11044,7 +11651,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11044
11651
|
if (runRef.current.kind === "streaming") {
|
|
11045
11652
|
if (cancelLatestSteer()) return;
|
|
11046
11653
|
runRef.current.abort.abort();
|
|
11047
|
-
useStore.getState().showToast({ message: `${agent.name} keeps working
|
|
11654
|
+
useStore.getState().showToast({ message: `${agent.name} keeps working; reopen to reattach` });
|
|
11048
11655
|
goTo({
|
|
11049
11656
|
kind: "conversation-picker",
|
|
11050
11657
|
agent
|
|
@@ -11058,6 +11665,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11058
11665
|
}
|
|
11059
11666
|
});
|
|
11060
11667
|
const hasPendingSteer = items.some((m) => m.kind === "pending-steer");
|
|
11668
|
+
const conversationContext = useMemo(() => ({
|
|
11669
|
+
rest,
|
|
11670
|
+
conversationId
|
|
11671
|
+
}), [rest, conversationId]);
|
|
11061
11672
|
const actionableCards = useMemo(() => items.filter(isActionableCard), [items]);
|
|
11062
11673
|
const statusText = useMemo(() => formatChatFooterStatus({
|
|
11063
11674
|
runKind: run.kind,
|
|
@@ -11086,7 +11697,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11086
11697
|
if (modelPickerOpen) return /* @__PURE__ */ jsx(ModelPicker, {
|
|
11087
11698
|
agentId: agent.id,
|
|
11088
11699
|
currentModel: model,
|
|
11089
|
-
modelLocked
|
|
11700
|
+
modelLocked,
|
|
11090
11701
|
onSelected: (modelId) => {
|
|
11091
11702
|
setModel(modelId);
|
|
11092
11703
|
setModelPickerOpen(false);
|
|
@@ -11140,7 +11751,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11140
11751
|
setReviewFocused(false);
|
|
11141
11752
|
}
|
|
11142
11753
|
}) : null;
|
|
11143
|
-
const
|
|
11754
|
+
const chatViewBody = /* @__PURE__ */ jsxs("box", {
|
|
11144
11755
|
onMouseDown: (event) => {
|
|
11145
11756
|
const paneBox = reviewBelowBoxRef.current;
|
|
11146
11757
|
if (reviewOpen && reviewPlacement === "below" && paneBox && event.y === paneBox.screenY + paneBox.height - 1) {
|
|
@@ -11388,6 +11999,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11388
11999
|
},
|
|
11389
12000
|
children: /* @__PURE__ */ jsx("textarea", {
|
|
11390
12001
|
ref: composerRef,
|
|
12002
|
+
textColor: theme.fg,
|
|
12003
|
+
focusedTextColor: theme.fg,
|
|
12004
|
+
placeholderColor: theme.dim,
|
|
11391
12005
|
placeholder: forking ? "forking into a new chat…" : `message ${agent.name}…`,
|
|
11392
12006
|
keyBindings: composerKeyBindings,
|
|
11393
12007
|
onContentChange: handleComposerChange,
|
|
@@ -11427,6 +12041,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11427
12041
|
})
|
|
11428
12042
|
]
|
|
11429
12043
|
});
|
|
12044
|
+
const chatView = /* @__PURE__ */ jsx(ConversationContext.Provider, {
|
|
12045
|
+
value: conversationContext,
|
|
12046
|
+
children: chatViewBody
|
|
12047
|
+
});
|
|
11430
12048
|
if (!reviewView || reviewPlacement === "below") return chatView;
|
|
11431
12049
|
const fullscreen = reviewPlacement === "fullscreen";
|
|
11432
12050
|
return /* @__PURE__ */ jsxs("box", {
|
|
@@ -11520,12 +12138,18 @@ function CredentialPromptBox({ prompt, height, onInput, onSubmit }) {
|
|
|
11520
12138
|
]
|
|
11521
12139
|
}),
|
|
11522
12140
|
field.secret ? /* @__PURE__ */ jsx("input", {
|
|
12141
|
+
textColor: theme.fg,
|
|
12142
|
+
focusedTextColor: theme.fg,
|
|
12143
|
+
placeholderColor: theme.dim,
|
|
11523
12144
|
value: MASK_CHAR.repeat(prompt.current.length),
|
|
11524
12145
|
placeholder: field.placeholder ?? "",
|
|
11525
12146
|
onInput,
|
|
11526
12147
|
onSubmit,
|
|
11527
12148
|
focused: !prompt.submitting
|
|
11528
12149
|
}, field.key) : /* @__PURE__ */ jsx("input", {
|
|
12150
|
+
textColor: theme.fg,
|
|
12151
|
+
focusedTextColor: theme.fg,
|
|
12152
|
+
placeholderColor: theme.dim,
|
|
11529
12153
|
placeholder: field.placeholder ?? "",
|
|
11530
12154
|
onInput,
|
|
11531
12155
|
onSubmit,
|
|
@@ -11765,7 +12389,12 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
11765
12389
|
setClients,
|
|
11766
12390
|
fail
|
|
11767
12391
|
]);
|
|
12392
|
+
const appendEarlyInput = useStore((s) => s.appendEarlyInput);
|
|
11768
12393
|
useKeyboard((key) => {
|
|
12394
|
+
if (screen.kind === "splash" && !(key.name === "c" && key.ctrl)) {
|
|
12395
|
+
appendEarlyInput(key);
|
|
12396
|
+
return;
|
|
12397
|
+
}
|
|
11769
12398
|
if (key.name === "c" && key.ctrl && screen.kind !== "chat") {
|
|
11770
12399
|
useStore.getState().portalClient?.dispose();
|
|
11771
12400
|
agentHost.dispose();
|
|
@@ -12233,6 +12862,97 @@ async function settleBootThemeMode({ source, applyThemeMode, graceMs = THEME_QUE
|
|
|
12233
12862
|
if (answered) applyThemeMode(answered);
|
|
12234
12863
|
}
|
|
12235
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
|
+
|
|
12236
12956
|
//#endregion
|
|
12237
12957
|
//#region src/chat/boot.tsx
|
|
12238
12958
|
/**
|
|
@@ -12245,7 +12965,12 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
12245
12965
|
await registerGrammars();
|
|
12246
12966
|
const watchdog = startMemoryWatchdog();
|
|
12247
12967
|
installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
|
|
12248
|
-
const renderer = await createCliRenderer({
|
|
12968
|
+
const renderer = await createCliRenderer({
|
|
12969
|
+
exitOnCtrlC: false,
|
|
12970
|
+
...profilingRendererOptions()
|
|
12971
|
+
});
|
|
12972
|
+
startRendererStatsCapture(renderer);
|
|
12973
|
+
startStateCapture(useStore);
|
|
12249
12974
|
extend({ markdown: LinkifiedMarkdownRenderable });
|
|
12250
12975
|
useStore.setState({
|
|
12251
12976
|
authKind,
|
|
@@ -12265,16 +12990,20 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
12265
12990
|
source: renderer,
|
|
12266
12991
|
applyThemeMode: (mode) => useStore.getState().setThemeMode(mode)
|
|
12267
12992
|
});
|
|
12268
|
-
createRoot(renderer).render(/* @__PURE__ */ jsx(
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
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
|
+
})
|
|
12278
13007
|
}));
|
|
12279
13008
|
}
|
|
12280
13009
|
/** Open the same workspace picker used by the initial agent screen. */
|