skydive-cli 0.5.0-beta.3 → 0.5.0-beta.31
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 +3 -0
- package/README.md +15 -4
- package/dist/js/api-DG5W6iwx.mjs +131 -0
- package/dist/js/bin.mjs +251 -84
- package/dist/js/{boot-BVVbrf2k.mjs → boot-iaVSnd7h.mjs} +1800 -605
- package/dist/js/{client-Dd5sMXPv.mjs → client-DbqRBquD.mjs} +206 -133
- package/dist/js/client-hH2PJL8y.mjs +5 -0
- package/dist/js/{daemon-Bq93vOIk.mjs → daemon-BTyg329O.mjs} +93 -7
- package/dist/js/{daemon-DDBIumf1.mjs → daemon-SPQgtsbW.mjs} +3 -2
- package/dist/js/{daemon-client-Bewt98dE.mjs → daemon-client--A_yMKq6.mjs} +12 -8
- package/dist/js/daemon-client-C7emKKlj.mjs +7 -0
- package/dist/js/forward-DL6DYqyc.mjs +187 -0
- package/dist/js/{print-CbayCa87.mjs → print-C_TnQSPg.mjs} +211 -35
- package/dist/js/{print-ba_0hiV9.mjs → print-DACzRUwQ.mjs} +2 -2
- package/dist/js/{print-share-C1LPsgTP.mjs → print-share-BH9gvCls.mjs} +3 -1
- package/dist/js/{profiler-DtuXkIkR.mjs → profiler-BVbYs_Yg.mjs} +258 -4
- package/dist/js/{raw-pty-B6mAroiI.mjs → raw-pty-DY4KelZW.mjs} +1 -1
- package/dist/js/raw-pty-DmdUf4_w.mjs +5 -0
- package/dist/js/{rest-BY2nADw5.mjs → rest-CgfbKXst.mjs} +40 -12
- package/dist/js/{rest-DADJh0bi.mjs → rest-VV5nc-Mn.mjs} +1 -1
- package/package.json +1 -1
- package/dist/js/client-CKBQ8pft.mjs +0 -4
- package/dist/js/daemon-client-srAit8Fz.mjs +0 -6
- package/dist/js/raw-pty-3EkG-jjH.mjs +0 -5
- /package/dist/js/{client-Cn2af31H.mjs → client-c4c5MmgN.mjs} +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as
|
|
3
|
-
import { B as
|
|
2
|
+
import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-BVbYs_Yg.mjs";
|
|
3
|
+
import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-C_TnQSPg.mjs";
|
|
4
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-
|
|
5
|
+
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-CgfbKXst.mjs";
|
|
6
6
|
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
|
|
7
|
-
import { t as PortalClient } from "./client-
|
|
8
|
-
import "./daemon-
|
|
9
|
-
import
|
|
10
|
-
import { t as
|
|
11
|
-
import { t as
|
|
7
|
+
import { t as PortalClient } from "./client-DbqRBquD.mjs";
|
|
8
|
+
import "./daemon-BTyg329O.mjs";
|
|
9
|
+
import "./api-DG5W6iwx.mjs";
|
|
10
|
+
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
11
|
+
import { t as PortalDaemonClient } from "./daemon-client--A_yMKq6.mjs";
|
|
12
|
+
import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
|
|
12
13
|
import * as os$1 from "node:os";
|
|
13
14
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
14
15
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
@@ -136,6 +137,7 @@ const useStore = create((set, get) => ({
|
|
|
136
137
|
seedPrompt: null,
|
|
137
138
|
autoGrantMachine: false,
|
|
138
139
|
newConversationIntent: false,
|
|
140
|
+
forcedOnboarding: false,
|
|
139
141
|
earlyInput: "",
|
|
140
142
|
goTo: (screen) => set({
|
|
141
143
|
screen,
|
|
@@ -212,7 +214,17 @@ const useStore = create((set, get) => ({
|
|
|
212
214
|
/**
|
|
213
215
|
* Decide the screen the chat TUI boots into.
|
|
214
216
|
*
|
|
215
|
-
* With no selector we open the agent picker
|
|
217
|
+
* With no selector we open the agent picker — unless the config stores a
|
|
218
|
+
* `defaultAgent`, in which case a bare `skydive chat` opens a fresh
|
|
219
|
+
* conversation with that agent directly. The default is maintained
|
|
220
|
+
* automatically (every conversation the TUI opens records its agent, so a
|
|
221
|
+
* bare launch returns to whoever you talked to last — the dominant launch
|
|
222
|
+
* pattern), and `skydive config set defaultAgent` can seed or override it. A
|
|
223
|
+
* stored selector that no longer resolves degrades silently to the agent
|
|
224
|
+
* picker rather than an error screen: config goes stale (agent renamed,
|
|
225
|
+
* archived, another one claiming the same name), and unlike a mistyped
|
|
226
|
+
* `--agent` there is no command line to correct — and picking an agent
|
|
227
|
+
* there heals the default on its own.
|
|
216
228
|
*
|
|
217
229
|
* With `--agent` (`skydive chat --agent grace`, no `-p`) we resolve it
|
|
218
230
|
* against the org roster and open that agent's conversation list, skipping
|
|
@@ -228,20 +240,31 @@ const useStore = create((set, get) => ({
|
|
|
228
240
|
* `--agent`. When both are given, the conversation wins (it pins the agent
|
|
229
241
|
* anyway).
|
|
230
242
|
*
|
|
243
|
+
* None of these paths load the whole org agent roster. The only decision that
|
|
244
|
+
* needs every candidate is disambiguating a fuzzy name/slug selector; an id
|
|
245
|
+
* (or the `--resume` conversation's agentId) is exact and resolves with a
|
|
246
|
+
* single `getAgent`, and `--new` with no selector only needs "exactly one
|
|
247
|
+
* agent, or not", which a two-row page answers. A large org must never sit on
|
|
248
|
+
* the `connecting…` splash while the roster streams in.
|
|
249
|
+
*
|
|
231
250
|
* Kept out of the React component so it's unit-testable under Node (the
|
|
232
251
|
* component itself can't render without Bun + OpenTUI).
|
|
233
252
|
*/
|
|
234
|
-
|
|
253
|
+
/** A selector that is a bare uuid is an exact agent id: it can be resolved
|
|
254
|
+
* with a single `getAgent` fetch, so no boot path that carries one needs the
|
|
255
|
+
* org roster. A name/slug selector is the only case that can be ambiguous and
|
|
256
|
+
* therefore still needs the full candidate list to disambiguate. */
|
|
257
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
258
|
+
async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
|
|
235
259
|
if (conversationId) {
|
|
236
|
-
const
|
|
260
|
+
const conversation = await rest.getConversation({ conversationId }).catch((err) => {
|
|
237
261
|
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.`);
|
|
238
262
|
throw err;
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!agent) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
|
|
263
|
+
});
|
|
264
|
+
const agent = await rest.getAgent({ agentId: conversation.agentId }).catch((err) => {
|
|
265
|
+
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
|
|
266
|
+
throw err;
|
|
267
|
+
});
|
|
245
268
|
return {
|
|
246
269
|
kind: "chat",
|
|
247
270
|
agent,
|
|
@@ -262,26 +285,68 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
262
285
|
}
|
|
263
286
|
};
|
|
264
287
|
}
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
288
|
+
if (forceOnboarding && !agentSelector) return { kind: "first-run" };
|
|
289
|
+
if (!agentSelector && !newConversation && !defaultAgentSelector) {
|
|
290
|
+
if ((await rest.listAgents({
|
|
291
|
+
scope: "org",
|
|
292
|
+
onPage: null
|
|
293
|
+
})).length === 0) return { kind: "first-run" };
|
|
294
|
+
return { kind: "agent-picker" };
|
|
295
|
+
}
|
|
296
|
+
if (!agentSelector && defaultAgentSelector) {
|
|
297
|
+
const agent = UUID_RE.test(defaultAgentSelector) ? await rest.getAgent({ agentId: defaultAgentSelector }).catch((err) => {
|
|
298
|
+
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) return null;
|
|
299
|
+
throw err;
|
|
300
|
+
}) : resolveAgentOrNull(await rest.listAgents({
|
|
301
|
+
scope: "org",
|
|
302
|
+
onPage: null
|
|
303
|
+
}), defaultAgentSelector);
|
|
304
|
+
if (agent) return newChat(agent);
|
|
305
|
+
return { kind: "agent-picker" };
|
|
306
|
+
}
|
|
307
|
+
if (agentSelector && UUID_RE.test(agentSelector)) {
|
|
308
|
+
const agent = await rest.getAgent({ agentId: agentSelector }).catch((err) => {
|
|
309
|
+
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`No agent with id ${agentSelector}. It may not exist, or belong to another workspace.`);
|
|
310
|
+
throw err;
|
|
311
|
+
});
|
|
312
|
+
if (newConversation) return {
|
|
274
313
|
kind: "chat",
|
|
275
|
-
agent
|
|
314
|
+
agent,
|
|
276
315
|
conversation: {
|
|
277
316
|
kind: "new",
|
|
278
|
-
agentId:
|
|
317
|
+
agentId: agent.id,
|
|
279
318
|
nonce: crypto.randomUUID()
|
|
280
319
|
}
|
|
281
320
|
};
|
|
321
|
+
return {
|
|
322
|
+
kind: "conversation-picker",
|
|
323
|
+
agent,
|
|
324
|
+
initialQuery: ""
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (!agentSelector && newConversation) {
|
|
328
|
+
const firstTwo = await rest.listAgents({
|
|
329
|
+
scope: "org",
|
|
330
|
+
onPage: null,
|
|
331
|
+
limit: 2
|
|
332
|
+
});
|
|
333
|
+
const only = firstTwo.length === 1 ? firstTwo[0] : null;
|
|
334
|
+
if (!only) return { kind: "agent-picker" };
|
|
335
|
+
return newChat(only);
|
|
282
336
|
}
|
|
283
|
-
const agent = resolveAgent(
|
|
284
|
-
|
|
337
|
+
const agent = resolveAgent(await rest.listAgents({
|
|
338
|
+
scope: "org",
|
|
339
|
+
onPage: null
|
|
340
|
+
}), agentSelector);
|
|
341
|
+
if (newConversation) return newChat(agent);
|
|
342
|
+
return {
|
|
343
|
+
kind: "conversation-picker",
|
|
344
|
+
agent,
|
|
345
|
+
initialQuery: ""
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function newChat(agent) {
|
|
349
|
+
return {
|
|
285
350
|
kind: "chat",
|
|
286
351
|
agent,
|
|
287
352
|
conversation: {
|
|
@@ -290,10 +355,17 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
290
355
|
nonce: crypto.randomUUID()
|
|
291
356
|
}
|
|
292
357
|
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
358
|
+
}
|
|
359
|
+
/** `resolveAgent` throws for a selector that matches nothing or matches
|
|
360
|
+
* ambiguously — both phrased for a human who can retype `--agent`. A stored
|
|
361
|
+
* default has no command line behind it, so both shapes just mean "no usable
|
|
362
|
+
* default". */
|
|
363
|
+
function resolveAgentOrNull(agents, selector) {
|
|
364
|
+
try {
|
|
365
|
+
return resolveAgent(agents, selector);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
297
369
|
}
|
|
298
370
|
|
|
299
371
|
//#endregion
|
|
@@ -324,6 +396,8 @@ function createPortalClient(opts) {
|
|
|
324
396
|
deviceToken: null
|
|
325
397
|
}),
|
|
326
398
|
resolveCwd: () => process.cwd(),
|
|
399
|
+
persistedMachineName: null,
|
|
400
|
+
onMachineName: () => {},
|
|
327
401
|
onState: (state) => opts.onState({
|
|
328
402
|
status: state.status,
|
|
329
403
|
machineName: state.machineName,
|
|
@@ -337,7 +411,7 @@ function createPortalClient(opts) {
|
|
|
337
411
|
start: () => Promise.resolve(),
|
|
338
412
|
enable: () => client.enable(),
|
|
339
413
|
disable: () => client.disable(),
|
|
340
|
-
grantAgent: (agentId) => client.grantAgent(agentId),
|
|
414
|
+
grantAgent: (agentId, conversationId) => client.grantAgent(agentId, conversationId),
|
|
341
415
|
decline: (_agentId, declined) => {
|
|
342
416
|
if (declined && client.grantedAgentIds().length === 0) client.disable();
|
|
343
417
|
},
|
|
@@ -1016,42 +1090,133 @@ function createAgentHost({ renderer, notifications }) {
|
|
|
1016
1090
|
* take the one host we can identify unambiguously (github.com, incl. `www.`),
|
|
1017
1091
|
* which covers the common product case; GHES stays the native badge's job.
|
|
1018
1092
|
*
|
|
1019
|
-
* Only the `/pull/<n>` form (not `/issues
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*/
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1093
|
+
* Only the `/pull/<n>` form (not `/issues/`). A trailing path/query/fragment
|
|
1094
|
+
* (e.g. `/files`, `#discussion`) is allowed but not captured into the canonical
|
|
1095
|
+
* URL. Also matches inside markdown links, backticks, and trailing punctuation.
|
|
1096
|
+
*
|
|
1097
|
+
* The `https://` (and `www.`) prefix is optional: agents and people routinely
|
|
1098
|
+
* write a bare `github.com/o/r/pull/7`, and that should link a PR the same way
|
|
1099
|
+
* the full URL does. To keep a bare host from matching inside a longer hostname
|
|
1100
|
+
* (`mygithub.com/...`, `notgithub.com/...`), a protocol-less match must be
|
|
1101
|
+
* preceded by a start-of-string or a non-host character (anything but a word
|
|
1102
|
+
* char or `.`) — a leading boundary the `(?:^|[^\w.])` group asserts and
|
|
1103
|
+
* consumes. The consumed boundary char is outside the captured groups, so it
|
|
1104
|
+
* never leaks into owner/repo. `matchAll` won't overlap, but the boundary is a
|
|
1105
|
+
* single delimiter (space, `(`, `[`, etc.) between PRs in practice, so distinct
|
|
1106
|
+
* back-to-back URLs still each match.
|
|
1107
|
+
*/
|
|
1108
|
+
const PR_URL_RE = /(?:^|[^\w.])(?:https?:\/\/)?(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
|
|
1109
|
+
/**
|
|
1110
|
+
* Matches a bare, human-style PR reference `#<n>` — the way people (and LLMs)
|
|
1111
|
+
* name a PR in prose ("opened #20858", "landed #42"). Requires a non-word
|
|
1112
|
+
* boundary before the `#` so a fragment tail like `/pull/7#discussion_r5` or a
|
|
1113
|
+
* hex color `#20858` embedded in a word doesn't count, and captures the digits
|
|
1114
|
+
* up to a word boundary. We only harvest these from **assistant prose**, never
|
|
1115
|
+
* from tool output, because `#N` is exactly the relevance signal we're after:
|
|
1116
|
+
* the agent chose to talk about this PR.
|
|
1117
|
+
*/
|
|
1118
|
+
const PR_HASH_RE = /(?:^|[^\w#/])#(\d+)\b/g;
|
|
1119
|
+
/**
|
|
1120
|
+
* Every full `/pull/<n>` github.com URL in `urlText`, keyed by number to the
|
|
1121
|
+
* candidate refs for that number. A number can map to more than one repo, so we
|
|
1122
|
+
* keep all candidates; a bare `#N` label can't say which repo, but the pill's
|
|
1123
|
+
* own URL does. Deduped by repo-qualified identity (`owner/repo#number`) so the
|
|
1124
|
+
* same PR linked twice — even with a different trailing path/fragment —
|
|
1125
|
+
* collapses to one ref.
|
|
1126
|
+
*
|
|
1127
|
+
* `deferTrailingNumber` guards the streaming truncation case: when true, a URL
|
|
1128
|
+
* whose number runs to the very end of `urlText` is skipped because the next
|
|
1129
|
+
* delta may extend those digits (`.../pull/19` now, `.../pull/19739` later).
|
|
1130
|
+
* Only the still-growing PROSE buffer sets this; settled tool output is always
|
|
1131
|
+
* complete, so its URLs are harvested with the guard off.
|
|
1132
|
+
*/
|
|
1133
|
+
function urlsByNumber(urlText, deferTrailingNumber, byNumber) {
|
|
1134
|
+
for (const match of urlText.matchAll(PR_URL_RE)) {
|
|
1038
1135
|
const owner = match[1];
|
|
1039
1136
|
const repo = match[2];
|
|
1040
1137
|
const numberText = match[3];
|
|
1041
1138
|
if (!owner || !repo || !numberText) continue;
|
|
1042
|
-
if (
|
|
1043
|
-
if (match.index + match[0].length >=
|
|
1139
|
+
if (deferTrailingNumber) {
|
|
1140
|
+
if (match.index + match[0].length >= urlText.length) continue;
|
|
1044
1141
|
}
|
|
1045
1142
|
const number = Number.parseInt(numberText, 10);
|
|
1046
1143
|
if (!Number.isFinite(number) || number <= 0) continue;
|
|
1047
|
-
const
|
|
1048
|
-
if (byKey.has(key)) continue;
|
|
1049
|
-
byKey.set(key, {
|
|
1144
|
+
const ref = {
|
|
1050
1145
|
number,
|
|
1051
|
-
key
|
|
1146
|
+
key: `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`,
|
|
1052
1147
|
url: `https://github.com/${owner}/${repo}/pull/${number}`
|
|
1053
|
-
}
|
|
1148
|
+
};
|
|
1149
|
+
const existing = byNumber.get(number);
|
|
1150
|
+
if (existing) {
|
|
1151
|
+
if (!existing.some((r) => r.key === ref.key)) existing.push(ref);
|
|
1152
|
+
} else byNumber.set(number, [ref]);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/** Every human-style `#N` named in `proseText` — the relevance signal. */
|
|
1156
|
+
function hashNumbers(proseText) {
|
|
1157
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
1158
|
+
for (const match of proseText.matchAll(PR_HASH_RE)) {
|
|
1159
|
+
const numberText = match[1];
|
|
1160
|
+
if (!numberText) continue;
|
|
1161
|
+
const number = Number.parseInt(numberText, 10);
|
|
1162
|
+
if (Number.isFinite(number) && number > 0) numbers.add(number);
|
|
1163
|
+
}
|
|
1164
|
+
return numbers;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* The relevance contract, in one place.
|
|
1168
|
+
*
|
|
1169
|
+
* A GitHub PR URL is cheap to emit and shows up in bulk any time a tool dumps
|
|
1170
|
+
* PRs the conversation isn't actually about — `gh pr list`, `gh pr status`, a
|
|
1171
|
+
* `gh search` — printing dozens of `/pull/N` URLs the agent never discusses.
|
|
1172
|
+
* Pinning a sidebar pill for every one of those is noise.
|
|
1173
|
+
*
|
|
1174
|
+
* So a PR is surfaced only when BOTH hold:
|
|
1175
|
+
* 1. a fully-qualified `github.com/<o>/<r>/pull/<n>` URL exists — this may
|
|
1176
|
+
* come from the assistant's prose OR from tool output (a `gh pr create`
|
|
1177
|
+
* that printed the URL the agent then referenced by number), and
|
|
1178
|
+
* 2. the **assistant's prose** names the number as a human-style `#<n>`.
|
|
1179
|
+
*
|
|
1180
|
+
* The `#N` in prose is the intent signal — "the agent is talking about this
|
|
1181
|
+
* PR", as opposed to "a tool happened to list it". The full URL (from wherever)
|
|
1182
|
+
* proves the PR is real and pins down its repo + canonical link. A URL with no
|
|
1183
|
+
* `#N` in prose (the whole of a `gh pr list` dump) is dropped; a `#N` with no
|
|
1184
|
+
* matching URL anywhere (a bare issue-style ref) is dropped.
|
|
1185
|
+
*
|
|
1186
|
+
* `proseText` is the assistant's prose alone (the only source of the `#N`
|
|
1187
|
+
* relevance signal); `urlOnlyText` is settled tool output that may carry full
|
|
1188
|
+
* PR URLs but no `#N` intent. URLs are harvested from both; `#N` from prose
|
|
1189
|
+
* only.
|
|
1190
|
+
*/
|
|
1191
|
+
function collectRelevant(proseText, urlOnlyText, options, byKey) {
|
|
1192
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
1193
|
+
urlsByNumber(proseText, options.requireCompleteNumber, byNumber);
|
|
1194
|
+
if (urlOnlyText) urlsByNumber(urlOnlyText, false, byNumber);
|
|
1195
|
+
if (byNumber.size === 0) return;
|
|
1196
|
+
const named = hashNumbers(proseText);
|
|
1197
|
+
if (named.size === 0) return;
|
|
1198
|
+
for (const [number, refs] of byNumber) {
|
|
1199
|
+
if (!named.has(number)) continue;
|
|
1200
|
+
for (const ref of refs) if (!byKey.has(ref.key)) byKey.set(ref.key, ref);
|
|
1054
1201
|
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Extracts every distinct GitHub PR that is both linked (a full `/pull/<n>`
|
|
1205
|
+
* URL) AND named (`#<n>`), de-duplicated by **repo-qualified identity**
|
|
1206
|
+
* (`owner/repo#number`) — NOT by number, since `#42` can exist in many repos.
|
|
1207
|
+
*
|
|
1208
|
+
* The two-argument form takes the URL pool and the prose separately, so a URL
|
|
1209
|
+
* from tool output can satisfy the link requirement while the `#N` stays a
|
|
1210
|
+
* prose-only signal. The one-argument form (prose only) is the common case: it
|
|
1211
|
+
* requires the URL to be in the same prose that names it.
|
|
1212
|
+
*
|
|
1213
|
+
* See {@link collectRelevant} for the contract.
|
|
1214
|
+
*/
|
|
1215
|
+
function extractPullRequests(proseText, options) {
|
|
1216
|
+
const requireCompleteNumber = options?.requireCompleteNumber ?? true;
|
|
1217
|
+
const urlOnly = options?.urlOnlyText ?? "";
|
|
1218
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1219
|
+
collectRelevant(proseText, urlOnly, { requireCompleteNumber }, byKey);
|
|
1055
1220
|
return [...byKey.values()];
|
|
1056
1221
|
}
|
|
1057
1222
|
/**
|
|
@@ -1090,28 +1255,34 @@ function stringifyToolOutput(value) {
|
|
|
1090
1255
|
}
|
|
1091
1256
|
/**
|
|
1092
1257
|
* A streaming PR scanner: fed run chunks as they stream, it reports each
|
|
1093
|
-
* distinct PR the moment
|
|
1094
|
-
* the
|
|
1258
|
+
* distinct PR the moment BOTH conditions hold — a full `/pull/<n>` URL has been
|
|
1259
|
+
* seen (in prose OR tool output) AND the assistant's prose names it `#<n>` —
|
|
1260
|
+
* exactly once, without ever rescanning the transcript.
|
|
1095
1261
|
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1262
|
+
* Two sources feed it, with different roles:
|
|
1263
|
+
* - **Assistant text** (`text-delta`): the ONLY source of the `#N` relevance
|
|
1264
|
+
* signal, and also a source of URLs. Accumulated per part id (a URL or `#N`
|
|
1265
|
+
* can split across two deltas); only the current part's buffer is scanned.
|
|
1266
|
+
* - **Tool output** (`tool-output-available`): a URL-only source. A
|
|
1267
|
+
* `gh pr create` prints the URL the agent then references by number, so the
|
|
1268
|
+
* URL for a #N mention often lives here, not in the prose. Tool output can
|
|
1269
|
+
* NEVER originate a pill on its own — a `gh pr list` dump (URLs, no `#N`)
|
|
1270
|
+
* stays silent — but it satisfies the URL half for a PR the prose names.
|
|
1103
1271
|
*
|
|
1104
1272
|
* `onPullRequest` fires once per never-before-seen PR (keyed by
|
|
1105
|
-
* `owner/repo#number`)
|
|
1106
|
-
*
|
|
1107
|
-
* same chunks the transcript reducer sees.
|
|
1273
|
+
* `owner/repo#number`). Construct one per run (or per conversation) and feed it
|
|
1274
|
+
* the same chunks the transcript reducer sees. See {@link collectRelevant}.
|
|
1108
1275
|
*/
|
|
1109
1276
|
function createPullRequestScanner(onPullRequest) {
|
|
1110
1277
|
let text = "";
|
|
1111
1278
|
let lastPartId;
|
|
1279
|
+
let toolUrlText = "";
|
|
1112
1280
|
const seen = /* @__PURE__ */ new Set();
|
|
1113
|
-
const report = (
|
|
1114
|
-
for (const pr of extractPullRequests(
|
|
1281
|
+
const report = (final) => {
|
|
1282
|
+
for (const pr of extractPullRequests(text, {
|
|
1283
|
+
requireCompleteNumber: !final,
|
|
1284
|
+
urlOnlyText: toolUrlText
|
|
1285
|
+
})) {
|
|
1115
1286
|
if (seen.has(pr.key)) continue;
|
|
1116
1287
|
seen.add(pr.key);
|
|
1117
1288
|
onPullRequest(pr);
|
|
@@ -1121,7 +1292,8 @@ function createPullRequestScanner(onPullRequest) {
|
|
|
1121
1292
|
if (chunk["type"] === "tool-output-available") {
|
|
1122
1293
|
const outputText = stringifyToolOutput(chunk["output"]);
|
|
1123
1294
|
if (!outputText.includes("/pull/")) return;
|
|
1124
|
-
|
|
1295
|
+
toolUrlText += `\n${outputText}`;
|
|
1296
|
+
report(true);
|
|
1125
1297
|
return;
|
|
1126
1298
|
}
|
|
1127
1299
|
if (chunk["type"] !== "text-delta") return;
|
|
@@ -1129,13 +1301,12 @@ function createPullRequestScanner(onPullRequest) {
|
|
|
1129
1301
|
if (!delta) return;
|
|
1130
1302
|
const partId = typeof chunk["id"] === "string" ? chunk["id"] : void 0;
|
|
1131
1303
|
if (partId && lastPartId && partId !== lastPartId) {
|
|
1132
|
-
|
|
1304
|
+
report(true);
|
|
1133
1305
|
text = "";
|
|
1134
1306
|
}
|
|
1135
1307
|
text += delta;
|
|
1136
1308
|
lastPartId = partId ?? lastPartId;
|
|
1137
|
-
|
|
1138
|
-
report(text, false);
|
|
1309
|
+
report(false);
|
|
1139
1310
|
} };
|
|
1140
1311
|
}
|
|
1141
1312
|
|
|
@@ -1479,6 +1650,7 @@ function screenLabelParts(screen, chatTitle) {
|
|
|
1479
1650
|
case "error": return { title: "" };
|
|
1480
1651
|
case "agent-picker": return { title: "pick an agent" };
|
|
1481
1652
|
case "agent-create": return { title: "create an agent" };
|
|
1653
|
+
case "first-run": return { title: "welcome" };
|
|
1482
1654
|
case "workspace-picker": return { title: "switch workspace" };
|
|
1483
1655
|
case "conversation-picker": return {
|
|
1484
1656
|
title: "pick a conversation",
|
|
@@ -1649,13 +1821,15 @@ function WordmarkRows({ mono }) {
|
|
|
1649
1821
|
});
|
|
1650
1822
|
}
|
|
1651
1823
|
/**
|
|
1652
|
-
* Height-aware brand header for the pickers. Always shown
|
|
1653
|
-
* list is loading / empty / erroring) so the splash is
|
|
1654
|
-
* Full mark + wordmark when the terminal is tall
|
|
1655
|
-
*
|
|
1824
|
+
* Height- and width-aware brand header for the pickers. Always shown
|
|
1825
|
+
* (including while the list is loading / empty / erroring) so the splash is
|
|
1826
|
+
* never gated on data. Full mark + wordmark only when the terminal is tall
|
|
1827
|
+
* enough AND wide enough that the art sits on six lines; otherwise a compact
|
|
1828
|
+
* one-liner. Wrapping the splash would blow the 7-row budget the list
|
|
1829
|
+
* subtracts, and the overflow collapses the top list rows onto each other.
|
|
1656
1830
|
*/
|
|
1657
|
-
function BrandHeader({ height }) {
|
|
1658
|
-
if (height
|
|
1831
|
+
function BrandHeader({ height, width }) {
|
|
1832
|
+
if (showFullSplash(height, width)) return /* @__PURE__ */ jsxs("box", {
|
|
1659
1833
|
style: { flexDirection: "column" },
|
|
1660
1834
|
children: [/* @__PURE__ */ jsx(SkydiveMark, { variant: "full" }), /* @__PURE__ */ jsx("box", { style: { height: 1 } })]
|
|
1661
1835
|
});
|
|
@@ -1664,9 +1838,12 @@ function BrandHeader({ height }) {
|
|
|
1664
1838
|
children: "✦ Skydive"
|
|
1665
1839
|
});
|
|
1666
1840
|
}
|
|
1841
|
+
function showFullSplash(height, width) {
|
|
1842
|
+
return height >= 24 && splashFitsWidth(width);
|
|
1843
|
+
}
|
|
1667
1844
|
/** Row budget the BrandHeader consumes; callers subtract this from visibleRows. */
|
|
1668
|
-
function brandHeaderRows(height) {
|
|
1669
|
-
return height
|
|
1845
|
+
function brandHeaderRows(height, width) {
|
|
1846
|
+
return showFullSplash(height, width) ? 7 : 1;
|
|
1670
1847
|
}
|
|
1671
1848
|
function SkydiveMark({ variant = "full" }) {
|
|
1672
1849
|
const mono = noColor();
|
|
@@ -1753,13 +1930,16 @@ function AgentPickerScreen() {
|
|
|
1753
1930
|
scope,
|
|
1754
1931
|
cache
|
|
1755
1932
|
]);
|
|
1756
|
-
const { height } = useTerminalDimensions();
|
|
1933
|
+
const { width, height } = useTerminalDimensions();
|
|
1757
1934
|
if (state.kind === "loading") return /* @__PURE__ */ jsxs("box", {
|
|
1758
1935
|
style: {
|
|
1759
1936
|
flexDirection: "column",
|
|
1760
1937
|
flexGrow: 1
|
|
1761
1938
|
},
|
|
1762
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1939
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1940
|
+
height,
|
|
1941
|
+
width
|
|
1942
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
1763
1943
|
fg: theme.muted,
|
|
1764
1944
|
children: "loading agents…"
|
|
1765
1945
|
})]
|
|
@@ -1769,7 +1949,10 @@ function AgentPickerScreen() {
|
|
|
1769
1949
|
flexDirection: "column",
|
|
1770
1950
|
flexGrow: 1
|
|
1771
1951
|
},
|
|
1772
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1952
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1953
|
+
height,
|
|
1954
|
+
width
|
|
1955
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
1773
1956
|
fg: theme.error,
|
|
1774
1957
|
children: ["error: ", state.message]
|
|
1775
1958
|
})]
|
|
@@ -1779,7 +1962,10 @@ function AgentPickerScreen() {
|
|
|
1779
1962
|
flexDirection: "column",
|
|
1780
1963
|
flexGrow: 1
|
|
1781
1964
|
},
|
|
1782
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1965
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1966
|
+
height,
|
|
1967
|
+
width
|
|
1968
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
1783
1969
|
fg: theme.muted,
|
|
1784
1970
|
children: scope === "mine" ? "no agents of yours yet. press tab to see all agents, ctrl+w to switch workspace, or ctrl+n to create one." : "no agents in this workspace. press ctrl+w to switch workspace, or ctrl+n to create one."
|
|
1785
1971
|
})]
|
|
@@ -1800,7 +1986,7 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1800
1986
|
const hits = fuzzyFilter(query, agents, agentKeys);
|
|
1801
1987
|
const filtered = hits.map((h) => h.item);
|
|
1802
1988
|
const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
|
|
1803
|
-
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height));
|
|
1989
|
+
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height, width));
|
|
1804
1990
|
const start = windowStart(clamped, filtered.length, visibleRows);
|
|
1805
1991
|
const windowed = hits.slice(start, start + visibleRows);
|
|
1806
1992
|
useKeyboard((key) => {
|
|
@@ -1822,7 +2008,8 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1822
2008
|
}
|
|
1823
2009
|
} : {
|
|
1824
2010
|
kind: "conversation-picker",
|
|
1825
|
-
agent
|
|
2011
|
+
agent,
|
|
2012
|
+
initialQuery: ""
|
|
1826
2013
|
});
|
|
1827
2014
|
}
|
|
1828
2015
|
});
|
|
@@ -1832,7 +2019,10 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1832
2019
|
flexGrow: 1
|
|
1833
2020
|
},
|
|
1834
2021
|
children: [
|
|
1835
|
-
/* @__PURE__ */ jsx(BrandHeader, {
|
|
2022
|
+
/* @__PURE__ */ jsx(BrandHeader, {
|
|
2023
|
+
height,
|
|
2024
|
+
width
|
|
2025
|
+
}),
|
|
1836
2026
|
/* @__PURE__ */ jsxs("box", {
|
|
1837
2027
|
style: {
|
|
1838
2028
|
flexDirection: "row",
|
|
@@ -1954,7 +2144,54 @@ function AgentCreateScreen() {
|
|
|
1954
2144
|
cancelled = true;
|
|
1955
2145
|
};
|
|
1956
2146
|
}, [rest]);
|
|
2147
|
+
const openChat = (agent) => {
|
|
2148
|
+
goTo({
|
|
2149
|
+
kind: "chat",
|
|
2150
|
+
agent,
|
|
2151
|
+
conversation: {
|
|
2152
|
+
kind: "new",
|
|
2153
|
+
agentId: agent.id,
|
|
2154
|
+
nonce: crypto.randomUUID()
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
};
|
|
2158
|
+
const acceptAlias = (agent, alias) => {
|
|
2159
|
+
try {
|
|
2160
|
+
const shell = detectShell();
|
|
2161
|
+
if (!shell) {
|
|
2162
|
+
openChat(agent);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const result = installAgentAlias({
|
|
2166
|
+
alias,
|
|
2167
|
+
agentId: agent.id,
|
|
2168
|
+
shell
|
|
2169
|
+
});
|
|
2170
|
+
setState({
|
|
2171
|
+
kind: "aliased",
|
|
2172
|
+
agent,
|
|
2173
|
+
message: `Added \`${result.alias}\`. ${aliasActivationHint(result)}`
|
|
2174
|
+
});
|
|
2175
|
+
} catch {
|
|
2176
|
+
openChat(agent);
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
1957
2179
|
useKeyboard((key) => {
|
|
2180
|
+
if (state.kind === "offer-alias") {
|
|
2181
|
+
if (key.name === "y") {
|
|
2182
|
+
acceptAlias(state.agent, state.alias);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (key.name === "n" || key.name === "escape" || key.name === "return") {
|
|
2186
|
+
openChat(state.agent);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
if (state.kind === "aliased") {
|
|
2192
|
+
openChat(state.agent);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
1958
2195
|
if (key.name === "escape" && state.kind !== "creating") goTo({ kind: "agent-picker" });
|
|
1959
2196
|
});
|
|
1960
2197
|
const createAgent = async () => {
|
|
@@ -1970,15 +2207,13 @@ function AgentCreateScreen() {
|
|
|
1970
2207
|
setState({ kind: "creating" });
|
|
1971
2208
|
try {
|
|
1972
2209
|
const agent = await rest.createAgent({ name: result.name });
|
|
1973
|
-
|
|
1974
|
-
|
|
2210
|
+
const shell = detectShell();
|
|
2211
|
+
if (shell) setState({
|
|
2212
|
+
kind: "offer-alias",
|
|
1975
2213
|
agent,
|
|
1976
|
-
|
|
1977
|
-
kind: "new",
|
|
1978
|
-
agentId: agent.id,
|
|
1979
|
-
nonce: crypto.randomUUID()
|
|
1980
|
-
}
|
|
2214
|
+
alias: dedupeAliasName(slugifyAliasName(agent.name), takenAliasNames(shell))
|
|
1981
2215
|
});
|
|
2216
|
+
else openChat(agent);
|
|
1982
2217
|
} catch (err) {
|
|
1983
2218
|
if (err instanceof HttpError && err.status === 401) {
|
|
1984
2219
|
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
@@ -2018,6 +2253,11 @@ function AgentCreateScreen() {
|
|
|
2018
2253
|
});
|
|
2019
2254
|
}
|
|
2020
2255
|
};
|
|
2256
|
+
if (state.kind === "offer-alias") return /* @__PURE__ */ jsx(AliasOffer, {
|
|
2257
|
+
agentName: state.agent.name,
|
|
2258
|
+
alias: state.alias
|
|
2259
|
+
});
|
|
2260
|
+
if (state.kind === "aliased") return /* @__PURE__ */ jsx(AliasResult, { message: state.message });
|
|
2021
2261
|
return /* @__PURE__ */ jsxs("box", {
|
|
2022
2262
|
style: {
|
|
2023
2263
|
flexDirection: "column",
|
|
@@ -2070,87 +2310,341 @@ function AgentCreateScreen() {
|
|
|
2070
2310
|
]
|
|
2071
2311
|
});
|
|
2072
2312
|
}
|
|
2313
|
+
/**
|
|
2314
|
+
* The post-create prompt offering to alias the agent's name to a shell
|
|
2315
|
+
* command. Presentational so the frame gallery can render it directly.
|
|
2316
|
+
*/
|
|
2317
|
+
function AliasOffer({ agentName, alias }) {
|
|
2318
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2319
|
+
style: {
|
|
2320
|
+
flexDirection: "column",
|
|
2321
|
+
flexGrow: 1,
|
|
2322
|
+
gap: 1
|
|
2323
|
+
},
|
|
2324
|
+
children: [
|
|
2325
|
+
/* @__PURE__ */ jsxs("text", {
|
|
2326
|
+
fg: theme.fg,
|
|
2327
|
+
children: [
|
|
2328
|
+
"Created ",
|
|
2329
|
+
agentName,
|
|
2330
|
+
"."
|
|
2331
|
+
]
|
|
2332
|
+
}),
|
|
2333
|
+
/* @__PURE__ */ jsxs("text", {
|
|
2334
|
+
fg: theme.muted,
|
|
2335
|
+
children: [
|
|
2336
|
+
"Add a shell alias so you can run `",
|
|
2337
|
+
alias,
|
|
2338
|
+
"` to open this agent?"
|
|
2339
|
+
]
|
|
2340
|
+
}),
|
|
2341
|
+
/* @__PURE__ */ jsx("text", {
|
|
2342
|
+
fg: theme.dim,
|
|
2343
|
+
children: "y add alias · n skip"
|
|
2344
|
+
})
|
|
2345
|
+
]
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
/** The confirmation shown after an alias is written. */
|
|
2349
|
+
function AliasResult({ message }) {
|
|
2350
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2351
|
+
style: {
|
|
2352
|
+
flexDirection: "column",
|
|
2353
|
+
flexGrow: 1,
|
|
2354
|
+
gap: 1
|
|
2355
|
+
},
|
|
2356
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
2357
|
+
fg: theme.fg,
|
|
2358
|
+
children: message
|
|
2359
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2360
|
+
fg: theme.dim,
|
|
2361
|
+
children: "press any key to continue"
|
|
2362
|
+
})]
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2073
2365
|
|
|
2074
2366
|
//#endregion
|
|
2075
|
-
//#region src/chat/tui/screens/
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2367
|
+
//#region src/chat/tui/screens/first-run.tsx
|
|
2368
|
+
/** Rows the mark lockup needs before it's worth painting (matches the
|
|
2369
|
+
* new-chat splash): the 6-row mark, a spacer, and a couple of content rows.
|
|
2370
|
+
* Below this the mark is dropped and only the message renders. */
|
|
2371
|
+
const MARK_MIN_ROWS$1 = 12;
|
|
2372
|
+
/** Columns the full mark + wordmark lockup spans; below this the pinwheel
|
|
2373
|
+
* mark renders alone instead of wrapping the wordmark. */
|
|
2374
|
+
const FULL_MARK_MIN_COLS$1 = 70;
|
|
2375
|
+
function FirstRunScreen() {
|
|
2376
|
+
const rest = useStore((s) => s.rest);
|
|
2377
|
+
const goTo = useStore((s) => s.goTo);
|
|
2378
|
+
const fail = useStore((s) => s.fail);
|
|
2379
|
+
const authKind = useStore((s) => s.authKind);
|
|
2380
|
+
const forced = useStore((s) => s.forcedOnboarding);
|
|
2381
|
+
const [state, setState] = useState({ kind: "creating" });
|
|
2382
|
+
const canShareMachine = authKind === "session";
|
|
2081
2383
|
useEffect(() => {
|
|
2384
|
+
if (!rest) return;
|
|
2082
2385
|
let cancelled = false;
|
|
2083
2386
|
(async () => {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
}), getActiveWorkspaceId({
|
|
2088
|
-
appUrl,
|
|
2089
|
-
sessionToken
|
|
2090
|
-
})]);
|
|
2091
|
-
if (cancelled) return;
|
|
2092
|
-
if (workspaces.isErr()) {
|
|
2387
|
+
try {
|
|
2388
|
+
const agent = await rest.createAgent({});
|
|
2389
|
+
if (cancelled) return;
|
|
2093
2390
|
setState({
|
|
2094
|
-
kind: "
|
|
2095
|
-
|
|
2391
|
+
kind: "offer",
|
|
2392
|
+
agent
|
|
2096
2393
|
});
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
if (cancelled) return;
|
|
2396
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
2397
|
+
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2100
2400
|
setState({
|
|
2101
2401
|
kind: "error",
|
|
2102
|
-
message:
|
|
2402
|
+
message: createErrorMessage(err)
|
|
2103
2403
|
});
|
|
2104
|
-
return;
|
|
2105
2404
|
}
|
|
2106
|
-
setHighlight(Math.max(0, workspaces.value.findIndex((workspace) => workspace.id === activeId.value)));
|
|
2107
|
-
setState({
|
|
2108
|
-
kind: "ready",
|
|
2109
|
-
workspaces: workspaces.value,
|
|
2110
|
-
activeId: activeId.value
|
|
2111
|
-
});
|
|
2112
2405
|
})();
|
|
2113
2406
|
return () => {
|
|
2114
2407
|
cancelled = true;
|
|
2115
2408
|
};
|
|
2116
|
-
}, [
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2409
|
+
}, [rest, fail]);
|
|
2410
|
+
const openNewChat = (agent) => {
|
|
2411
|
+
goTo({
|
|
2412
|
+
kind: "chat",
|
|
2413
|
+
agent,
|
|
2414
|
+
conversation: {
|
|
2415
|
+
kind: "new",
|
|
2416
|
+
agentId: agent.id,
|
|
2417
|
+
nonce: crypto.randomUUID()
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
};
|
|
2421
|
+
const openOnboardingChat = (agent, conversationId, title) => {
|
|
2422
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2423
|
+
goTo({
|
|
2424
|
+
kind: "chat",
|
|
2425
|
+
agent,
|
|
2426
|
+
conversation: {
|
|
2427
|
+
id: conversationId,
|
|
2428
|
+
title,
|
|
2429
|
+
createdAt: now,
|
|
2430
|
+
updatedAt: now,
|
|
2431
|
+
preview: null,
|
|
2432
|
+
channel: "web",
|
|
2433
|
+
channelLabel: null,
|
|
2434
|
+
agent: {
|
|
2435
|
+
id: agent.id,
|
|
2436
|
+
name: agent.name,
|
|
2437
|
+
slug: agent.slug ?? null,
|
|
2438
|
+
title: agent.title ?? null
|
|
2141
2439
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
};
|
|
2443
|
+
const acceptImport = async (agent) => {
|
|
2444
|
+
if (!rest) return;
|
|
2445
|
+
setState({ kind: "starting" });
|
|
2446
|
+
const { portalClient } = useStore.getState();
|
|
2447
|
+
try {
|
|
2448
|
+
const os = machineOsFromPlatform(process.platform);
|
|
2449
|
+
const { conversationId } = await rest.createOnboardingConversation({
|
|
2450
|
+
agentId: agent.id,
|
|
2451
|
+
projectDir: process.cwd(),
|
|
2452
|
+
os
|
|
2453
|
+
});
|
|
2454
|
+
if (portalClient) {
|
|
2455
|
+
portalClient.enable();
|
|
2456
|
+
portalClient.grantAgent(agent.id, conversationId).catch(() => {
|
|
2457
|
+
useStore.getState().showToast({
|
|
2458
|
+
message: `Couldn't share this machine automatically. ${agent.name} will ask in chat.`,
|
|
2459
|
+
variant: "warning"
|
|
2460
|
+
});
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
openOnboardingChat(agent, conversationId, `Onboarding ${agent.name}`);
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
2466
|
+
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
useStore.getState().showToast({
|
|
2470
|
+
message: "Couldn't start onboarding automatically. Opening a chat instead.",
|
|
2471
|
+
variant: "warning"
|
|
2472
|
+
});
|
|
2473
|
+
openNewChat(agent);
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
const declineImport = (agent) => {
|
|
2477
|
+
setState({ kind: "starting" });
|
|
2478
|
+
openNewChat(agent);
|
|
2479
|
+
};
|
|
2480
|
+
useKeyboard((key) => {
|
|
2481
|
+
if (state.kind !== "offer") return;
|
|
2482
|
+
if (!canShareMachine) {
|
|
2483
|
+
declineImport(state.agent);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (key.name === "return" || key.name === "y") acceptImport(state.agent);
|
|
2487
|
+
else if (key.name === "n" || key.name === "escape") declineImport(state.agent);
|
|
2488
|
+
});
|
|
2489
|
+
return /* @__PURE__ */ jsx(Centered, { children: renderBody({
|
|
2490
|
+
state,
|
|
2491
|
+
forced,
|
|
2492
|
+
canShareMachine
|
|
2493
|
+
}) });
|
|
2494
|
+
}
|
|
2495
|
+
/** The Skydive mark centered above the screen's content, matching the
|
|
2496
|
+
* new-chat splash lockup and degrading by available space. */
|
|
2497
|
+
function Centered({ children }) {
|
|
2498
|
+
const { width, height } = useTerminalDimensions();
|
|
2499
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2500
|
+
style: {
|
|
2501
|
+
flexGrow: 1,
|
|
2502
|
+
flexDirection: "column",
|
|
2503
|
+
justifyContent: "center",
|
|
2504
|
+
alignItems: "center"
|
|
2505
|
+
},
|
|
2506
|
+
children: [height >= MARK_MIN_ROWS$1 ? /* @__PURE__ */ jsx("box", {
|
|
2507
|
+
style: {
|
|
2508
|
+
flexDirection: "column",
|
|
2509
|
+
alignItems: "center",
|
|
2510
|
+
marginBottom: 1
|
|
2511
|
+
},
|
|
2512
|
+
children: /* @__PURE__ */ jsx(SkydiveMark, { variant: width >= FULL_MARK_MIN_COLS$1 ? "full" : "mark" })
|
|
2513
|
+
}) : null, /* @__PURE__ */ jsx("box", {
|
|
2514
|
+
style: {
|
|
2515
|
+
flexDirection: "column",
|
|
2516
|
+
alignItems: "center",
|
|
2517
|
+
gap: 1
|
|
2518
|
+
},
|
|
2519
|
+
children
|
|
2520
|
+
})]
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
function renderBody({ state, forced, canShareMachine }) {
|
|
2524
|
+
if (state.kind === "error") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2525
|
+
fg: theme.error,
|
|
2526
|
+
children: state.message
|
|
2527
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2528
|
+
fg: theme.muted,
|
|
2529
|
+
children: "press ctrl+c to quit, then run `skydive` again to retry."
|
|
2530
|
+
})] });
|
|
2531
|
+
if (state.kind === "creating") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2532
|
+
fg: theme.fg,
|
|
2533
|
+
children: "Welcome to Skydive."
|
|
2534
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2535
|
+
fg: theme.muted,
|
|
2536
|
+
children: forced ? "creating an agent…" : "setting up your first agent…"
|
|
2537
|
+
})] });
|
|
2538
|
+
if (state.kind === "starting") return /* @__PURE__ */ jsx("text", {
|
|
2539
|
+
fg: theme.muted,
|
|
2540
|
+
children: "opening your agent…"
|
|
2541
|
+
});
|
|
2542
|
+
const { agent } = state;
|
|
2543
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2544
|
+
fg: theme.fg,
|
|
2545
|
+
children: forced ? `${agent.name} is ready.` : `Your first agent, ${agent.name}, is ready.`
|
|
2546
|
+
}), canShareMachine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", {
|
|
2547
|
+
fg: theme.muted,
|
|
2548
|
+
children: [
|
|
2549
|
+
"Want ",
|
|
2550
|
+
agent.name,
|
|
2551
|
+
" to learn how you already work? It can look at the coding agents set up on this machine to get up to speed fast. It will connect to your computer to read your setup, show you a plan first, and never copy credentials."
|
|
2552
|
+
]
|
|
2553
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2554
|
+
fg: theme.dim,
|
|
2555
|
+
children: "↵/y yes, learn from my setup · n skip"
|
|
2556
|
+
})] }) : /* @__PURE__ */ jsx("text", {
|
|
2557
|
+
fg: theme.dim,
|
|
2558
|
+
children: "press any key to open the chat"
|
|
2559
|
+
})] });
|
|
2560
|
+
}
|
|
2561
|
+
function createErrorMessage(err) {
|
|
2562
|
+
if (err instanceof HttpError && err.status === 403) return "You don't have permission to create an agent in this workspace.";
|
|
2563
|
+
if (err instanceof HttpError && err.status === 429) return "Too many requests. Wait a moment, then run `skydive` again.";
|
|
2564
|
+
if (err instanceof HttpError) return "Skydive could not create your agent. Try again in a moment.";
|
|
2565
|
+
return "Could not reach Skydive. Check your connection and try again.";
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
//#endregion
|
|
2569
|
+
//#region src/chat/tui/screens/workspace-picker.tsx
|
|
2570
|
+
function WorkspacePicker({ appUrl, sessionToken, onSelect, onCancel }) {
|
|
2571
|
+
const { height } = useTerminalDimensions();
|
|
2572
|
+
const [state, setState] = useState({ kind: "loading" });
|
|
2573
|
+
const [highlight, setHighlight] = useState(0);
|
|
2574
|
+
const [switching, setSwitching] = useState(false);
|
|
2575
|
+
useEffect(() => {
|
|
2576
|
+
let cancelled = false;
|
|
2577
|
+
(async () => {
|
|
2578
|
+
const [workspaces, activeId] = await Promise.all([listWorkspaces({
|
|
2579
|
+
appUrl,
|
|
2580
|
+
sessionToken
|
|
2581
|
+
}), getActiveWorkspaceId({
|
|
2582
|
+
appUrl,
|
|
2583
|
+
sessionToken
|
|
2584
|
+
})]);
|
|
2585
|
+
if (cancelled) return;
|
|
2586
|
+
if (workspaces.isErr()) {
|
|
2587
|
+
setState({
|
|
2588
|
+
kind: "error",
|
|
2589
|
+
message: workspaces.error.message
|
|
2590
|
+
});
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
if (activeId.isErr()) {
|
|
2594
|
+
setState({
|
|
2595
|
+
kind: "error",
|
|
2596
|
+
message: activeId.error.message
|
|
2597
|
+
});
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
setHighlight(Math.max(0, workspaces.value.findIndex((workspace) => workspace.id === activeId.value)));
|
|
2601
|
+
setState({
|
|
2602
|
+
kind: "ready",
|
|
2603
|
+
workspaces: workspaces.value,
|
|
2604
|
+
activeId: activeId.value
|
|
2605
|
+
});
|
|
2606
|
+
})();
|
|
2607
|
+
return () => {
|
|
2608
|
+
cancelled = true;
|
|
2609
|
+
};
|
|
2610
|
+
}, [appUrl, sessionToken]);
|
|
2611
|
+
const rows = state.kind === "ready" ? state.workspaces : [];
|
|
2612
|
+
const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
|
|
2613
|
+
useKeyboard((key) => {
|
|
2614
|
+
if (switching) return;
|
|
2615
|
+
if (key.name === "escape" || key.name === "c" && key.ctrl) onCancel();
|
|
2616
|
+
else if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
|
|
2617
|
+
else if (key.name === "down") setHighlight(Math.min(rows.length - 1, clamped + 1));
|
|
2618
|
+
else if (key.name === "return") {
|
|
2619
|
+
const workspace = rows[clamped];
|
|
2620
|
+
if (!workspace) return;
|
|
2621
|
+
setSwitching(true);
|
|
2622
|
+
(async () => {
|
|
2623
|
+
const result = await setActiveWorkspace({
|
|
2624
|
+
appUrl,
|
|
2625
|
+
sessionToken,
|
|
2626
|
+
organizationId: workspace.id
|
|
2627
|
+
});
|
|
2628
|
+
if (result.isErr()) {
|
|
2629
|
+
setState({
|
|
2630
|
+
kind: "error",
|
|
2631
|
+
message: result.error.message
|
|
2632
|
+
});
|
|
2633
|
+
setSwitching(false);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
onSelect(workspace);
|
|
2637
|
+
})();
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
if (state.kind === "loading") return /* @__PURE__ */ jsx("text", {
|
|
2641
|
+
fg: theme.muted,
|
|
2642
|
+
children: "loading workspaces…"
|
|
2643
|
+
});
|
|
2644
|
+
if (state.kind === "error") return /* @__PURE__ */ jsxs("box", {
|
|
2645
|
+
style: { flexDirection: "column" },
|
|
2646
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
2647
|
+
fg: theme.error,
|
|
2154
2648
|
children: ["error: ", state.message]
|
|
2155
2649
|
}), /* @__PURE__ */ jsx("text", {
|
|
2156
2650
|
fg: theme.dim,
|
|
@@ -2419,16 +2913,81 @@ function useDebouncedValue(value, delayMs) {
|
|
|
2419
2913
|
}
|
|
2420
2914
|
|
|
2421
2915
|
//#endregion
|
|
2422
|
-
//#region src/chat/tui/screens/conversation-
|
|
2423
|
-
|
|
2424
|
-
|
|
2916
|
+
//#region src/chat/tui/screens/conversation-channel.ts
|
|
2917
|
+
function isNewConversation(c) {
|
|
2918
|
+
return "kind" in c && c.kind === "new";
|
|
2919
|
+
}
|
|
2920
|
+
/**
|
|
2921
|
+
* The channels a human can start a conversation on. Enumerated, not derived,
|
|
2922
|
+
* to keep `@createinc/anyone-db` out of the CLI build (see rest.ts). A newly
|
|
2923
|
+
* added human channel needs adding here too. The picker filters its default
|
|
2924
|
+
* view to these; `isAgentRunConversation` is their complement.
|
|
2925
|
+
*/
|
|
2425
2926
|
const humanChannels = [
|
|
2426
2927
|
"web",
|
|
2427
2928
|
"slack",
|
|
2428
2929
|
"email",
|
|
2429
2930
|
"imessage"
|
|
2430
2931
|
];
|
|
2431
|
-
|
|
2932
|
+
/**
|
|
2933
|
+
* Whether a conversation is an agent run — a thread the agent opened for
|
|
2934
|
+
* itself (cron firing, inbound webhook wake) rather than one a person
|
|
2935
|
+
* started. These have no human participant row, so per-user archive can
|
|
2936
|
+
* never apply to them (there is nothing to mark); delete is their only
|
|
2937
|
+
* cleanup affordance, and the UI should not offer archive on them.
|
|
2938
|
+
*
|
|
2939
|
+
* Best-effort: an a2a/subagent thread carries a `web` channel binding, so it
|
|
2940
|
+
* reads as `'web'` here and is NOT detected. Archive on one of those still
|
|
2941
|
+
* fails server-side with the existing error notice — this predicate only
|
|
2942
|
+
* removes the affordance where the client can know it's dead.
|
|
2943
|
+
*/
|
|
2944
|
+
function isAgentRunConversation(c) {
|
|
2945
|
+
if (isNewConversation(c)) return false;
|
|
2946
|
+
if (c.channel === null) return false;
|
|
2947
|
+
const { channel } = c;
|
|
2948
|
+
return !humanChannels.some((human) => human === channel);
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* The channel a conversation belongs to when it isn't the web channel, else
|
|
2952
|
+
* null. A brand-new conversation is always web, and the list route reports the
|
|
2953
|
+
* web channel as either `'web'` or (legacy) null — both mean "post from here".
|
|
2954
|
+
* Anything else (slack, email, imessage, webhook, cron) is owned by that
|
|
2955
|
+
* channel: the message has to go through it, so the TUI can't send here.
|
|
2956
|
+
* Mirrors the web app's `channel !== null && channel !== 'web'` gate.
|
|
2957
|
+
*/
|
|
2958
|
+
function nonWebChannelLabel(c) {
|
|
2959
|
+
if (isNewConversation(c)) return null;
|
|
2960
|
+
if (c.channel === null || c.channel === "web") return null;
|
|
2961
|
+
return c.channelLabel ?? c.channel;
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* The list-row shape for a conversation the TUI just forked, so the chat
|
|
2965
|
+
* screen can reopen on the copy without waiting for a list refetch. A fork
|
|
2966
|
+
* carries none of the source's channel binding — that's what makes it
|
|
2967
|
+
* sendable from here — so it is always a web conversation.
|
|
2968
|
+
*/
|
|
2969
|
+
function forkedConversationRef({ agent, forked }) {
|
|
2970
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2971
|
+
return {
|
|
2972
|
+
id: forked.id,
|
|
2973
|
+
title: forked.title,
|
|
2974
|
+
createdAt: now,
|
|
2975
|
+
updatedAt: now,
|
|
2976
|
+
preview: null,
|
|
2977
|
+
channel: "web",
|
|
2978
|
+
channelLabel: null,
|
|
2979
|
+
agent: {
|
|
2980
|
+
id: agent.id,
|
|
2981
|
+
name: agent.name
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
//#endregion
|
|
2987
|
+
//#region src/chat/tui/screens/conversation-picker.tsx
|
|
2988
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
2989
|
+
const SEARCH_QUERY_MAX_LENGTH = 200;
|
|
2990
|
+
function ConversationPickerScreen({ agent, initialQuery }) {
|
|
2432
2991
|
const rest = useStore((s) => s.rest);
|
|
2433
2992
|
const goTo = useStore((s) => s.goTo);
|
|
2434
2993
|
const [showAutomated, setShowAutomated] = useState(false);
|
|
@@ -2492,13 +3051,16 @@ function ConversationPickerScreen({ agent }) {
|
|
|
2492
3051
|
cacheKey,
|
|
2493
3052
|
cache
|
|
2494
3053
|
]);
|
|
2495
|
-
const { height } = useTerminalDimensions();
|
|
3054
|
+
const { width, height } = useTerminalDimensions();
|
|
2496
3055
|
if (state.kind === "loading") return /* @__PURE__ */ jsxs("box", {
|
|
2497
3056
|
style: {
|
|
2498
3057
|
flexDirection: "column",
|
|
2499
3058
|
flexGrow: 1
|
|
2500
3059
|
},
|
|
2501
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3060
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3061
|
+
height,
|
|
3062
|
+
width
|
|
3063
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2502
3064
|
fg: theme.muted,
|
|
2503
3065
|
children: "loading conversations…"
|
|
2504
3066
|
})]
|
|
@@ -2508,13 +3070,17 @@ function ConversationPickerScreen({ agent }) {
|
|
|
2508
3070
|
flexDirection: "column",
|
|
2509
3071
|
flexGrow: 1
|
|
2510
3072
|
},
|
|
2511
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3073
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3074
|
+
height,
|
|
3075
|
+
width
|
|
3076
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
2512
3077
|
fg: theme.error,
|
|
2513
3078
|
children: ["error: ", state.message]
|
|
2514
3079
|
})]
|
|
2515
3080
|
});
|
|
2516
3081
|
return /* @__PURE__ */ jsx(ConversationList, {
|
|
2517
3082
|
agent,
|
|
3083
|
+
initialQuery,
|
|
2518
3084
|
conversations: state.conversations,
|
|
2519
3085
|
complete: state.complete,
|
|
2520
3086
|
showAutomated,
|
|
@@ -2524,10 +3090,10 @@ function ConversationPickerScreen({ agent }) {
|
|
|
2524
3090
|
onPick: goTo
|
|
2525
3091
|
});
|
|
2526
3092
|
}
|
|
2527
|
-
function ConversationList({ agent, conversations, complete, showAutomated, onToggleAutomated, showArchived, onToggleArchived, onPick }) {
|
|
3093
|
+
function ConversationList({ agent, initialQuery, conversations, complete, showAutomated, onToggleAutomated, showArchived, onToggleArchived, onPick }) {
|
|
2528
3094
|
const { width, height } = useTerminalDimensions();
|
|
2529
3095
|
const rest = useStore((s) => s.rest);
|
|
2530
|
-
const [query, setQuery] = useState(
|
|
3096
|
+
const [query, setQuery] = useState(initialQuery);
|
|
2531
3097
|
const [highlight, setHighlight] = useState(0);
|
|
2532
3098
|
const debouncedQuery = useDebouncedValue(query.trim(), SEARCH_DEBOUNCE_MS);
|
|
2533
3099
|
const isSearching = debouncedQuery.length > 0;
|
|
@@ -2615,6 +3181,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2615
3181
|
const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
|
|
2616
3182
|
const highlightRow = rows[clamped];
|
|
2617
3183
|
const highlightArchived = highlightRow?.kind === "conv" && isArchived(highlightRow.hit.item);
|
|
3184
|
+
const highlightAgentRun = highlightRow?.kind === "conv" && isAgentRunConversation(highlightRow.hit.item);
|
|
2618
3185
|
const confirmConv = confirmId !== null ? list.find((c) => c.id === confirmId) : void 0;
|
|
2619
3186
|
const searchState = isSearching ? search?.state : void 0;
|
|
2620
3187
|
const countSegment = isSearching && (searchState?.kind === "loading" || search === null) ? "searching…" : isSearching && searchState?.kind === "error" ? `search failed: ${searchState.message}` : `${hits.length}/${list.length}${complete || isSearching ? "" : " (loading…)"}`;
|
|
@@ -2628,7 +3195,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2628
3195
|
fg: theme.dim,
|
|
2629
3196
|
text: fitHintLine([
|
|
2630
3197
|
countSegment,
|
|
2631
|
-
`ctrl+a ${highlightArchived ? "unarchive" : "archive"}
|
|
3198
|
+
...highlightAgentRun ? [] : [`ctrl+a ${highlightArchived ? "unarchive" : "archive"}`],
|
|
2632
3199
|
"ctrl+d delete",
|
|
2633
3200
|
`tab ${showAutomated ? "hide" : "show"} agent runs`,
|
|
2634
3201
|
`shift+tab ${showArchived ? "hide" : "show"} archived`,
|
|
@@ -2637,7 +3204,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2637
3204
|
"↵ open"
|
|
2638
3205
|
], innerWidth)
|
|
2639
3206
|
};
|
|
2640
|
-
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height));
|
|
3207
|
+
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height, width));
|
|
2641
3208
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
2642
3209
|
const windowed = rows.slice(start, start + visibleRows);
|
|
2643
3210
|
const open = (row) => {
|
|
@@ -2722,6 +3289,10 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2722
3289
|
} else if (key.name === "a" && key.ctrl) {
|
|
2723
3290
|
const row = rows[clamped];
|
|
2724
3291
|
if (row?.kind === "conv") {
|
|
3292
|
+
if (isAgentRunConversation(row.hit.item)) {
|
|
3293
|
+
setError("agent runs can't be archived — ctrl+d deletes");
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
2725
3296
|
setError(null);
|
|
2726
3297
|
toggleArchived(row.hit.item);
|
|
2727
3298
|
}
|
|
@@ -2738,7 +3309,10 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2738
3309
|
flexGrow: 1
|
|
2739
3310
|
},
|
|
2740
3311
|
children: [
|
|
2741
|
-
/* @__PURE__ */ jsx(BrandHeader, {
|
|
3312
|
+
/* @__PURE__ */ jsx(BrandHeader, {
|
|
3313
|
+
height,
|
|
3314
|
+
width
|
|
3315
|
+
}),
|
|
2742
3316
|
/* @__PURE__ */ jsxs("box", {
|
|
2743
3317
|
style: {
|
|
2744
3318
|
flexDirection: "row",
|
|
@@ -2755,6 +3329,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2755
3329
|
focused: confirmId === null,
|
|
2756
3330
|
width: Math.max(10, width - 12),
|
|
2757
3331
|
maxLength: SEARCH_QUERY_MAX_LENGTH,
|
|
3332
|
+
value: query,
|
|
2758
3333
|
onInput: (value) => {
|
|
2759
3334
|
setQuery(value.slice(0, SEARCH_QUERY_MAX_LENGTH));
|
|
2760
3335
|
setHighlight(0);
|
|
@@ -2779,15 +3354,20 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2779
3354
|
children: [marker, truncate("+ new conversation", rowWidth)]
|
|
2780
3355
|
}, "new");
|
|
2781
3356
|
const archived = isArchived(row.hit.item);
|
|
3357
|
+
const unread = row.hit.item.unread === true;
|
|
2782
3358
|
const { title, detail, tag } = fitRowText(singleLine(row.hit.item.title ?? "(untitled)"), `${archived ? "archived · " : ""}${previewLine(row.hit.item)}`, rowWidth, coAgentsTag(row.hit.item, agent.id));
|
|
3359
|
+
const titleText = /* @__PURE__ */ jsx(FuzzyText, {
|
|
3360
|
+
text: title,
|
|
3361
|
+
indexes: row.hit.item.title ? row.hit.highlights[0] : null
|
|
3362
|
+
});
|
|
2783
3363
|
return /* @__PURE__ */ jsxs("text", {
|
|
2784
3364
|
fg: archived && !selected ? theme.dim : fg,
|
|
2785
3365
|
children: [
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
}),
|
|
3366
|
+
!selected && unread ? /* @__PURE__ */ jsx("span", {
|
|
3367
|
+
fg: theme.accent,
|
|
3368
|
+
children: "● "
|
|
3369
|
+
}) : marker,
|
|
3370
|
+
unread ? /* @__PURE__ */ jsx("b", { children: titleText }) : titleText,
|
|
2791
3371
|
tag ? /* @__PURE__ */ jsxs("span", {
|
|
2792
3372
|
fg: theme.muted,
|
|
2793
3373
|
children: [" ", tag]
|
|
@@ -4169,328 +4749,297 @@ function parseMatches(output) {
|
|
|
4169
4749
|
return null;
|
|
4170
4750
|
}
|
|
4171
4751
|
|
|
4752
|
+
//#endregion
|
|
4753
|
+
//#region src/chat/tui/chunks/shell-local.tsx
|
|
4754
|
+
/**
|
|
4755
|
+
* Renders a `! <cmd>` local-shell escape: the command the user ran on their own
|
|
4756
|
+
* machine, followed by its combined stdout/stderr. Visually distinct from agent
|
|
4757
|
+
* tool calls (which run in the sandbox) — a `!` sigil colored by outcome (dim
|
|
4758
|
+
* while running, success/error once done), so a glance tells you this executed
|
|
4759
|
+
* locally, not on the agent.
|
|
4760
|
+
*/
|
|
4761
|
+
function ShellLocalItem({ item }) {
|
|
4762
|
+
const running = item.exit === null;
|
|
4763
|
+
const failed = item.exit !== null && item.exit !== 0;
|
|
4764
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
4765
|
+
style: { flexDirection: "column" },
|
|
4766
|
+
children: [/* @__PURE__ */ jsxs("text", { children: [
|
|
4767
|
+
/* @__PURE__ */ jsx("span", {
|
|
4768
|
+
fg: running ? theme.dim : failed ? theme.error : theme.success,
|
|
4769
|
+
children: "! "
|
|
4770
|
+
}),
|
|
4771
|
+
/* @__PURE__ */ jsx("span", {
|
|
4772
|
+
fg: theme.tool,
|
|
4773
|
+
children: item.command
|
|
4774
|
+
}),
|
|
4775
|
+
running ? /* @__PURE__ */ jsx("span", {
|
|
4776
|
+
fg: theme.dim,
|
|
4777
|
+
children: " · running…"
|
|
4778
|
+
}) : null,
|
|
4779
|
+
failed ? /* @__PURE__ */ jsxs("span", {
|
|
4780
|
+
fg: theme.error,
|
|
4781
|
+
children: [" · exit ", item.exit]
|
|
4782
|
+
}) : null
|
|
4783
|
+
] }), item.output ? /* @__PURE__ */ jsx(Indented, { children: /* @__PURE__ */ jsx("text", {
|
|
4784
|
+
fg: theme.muted,
|
|
4785
|
+
children: item.output.replace(/\n+$/, "")
|
|
4786
|
+
}) }) : null]
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4172
4790
|
//#endregion
|
|
4173
4791
|
//#region src/chat/tui/chat/conversation-context.tsx
|
|
4174
4792
|
const ConversationContext = createContext({
|
|
4175
4793
|
rest: null,
|
|
4176
|
-
conversationId: null
|
|
4794
|
+
conversationId: null,
|
|
4795
|
+
collectSandboxStats: null,
|
|
4796
|
+
latestComputerId: null,
|
|
4797
|
+
scrolledUp: false
|
|
4177
4798
|
});
|
|
4178
4799
|
function useConversationContext() {
|
|
4179
4800
|
return useContext(ConversationContext);
|
|
4180
4801
|
}
|
|
4181
4802
|
|
|
4182
4803
|
//#endregion
|
|
4183
|
-
//#region src/chat/tui/
|
|
4184
|
-
/** Braille dot-cycle frames — the classic smooth terminal spinner. */
|
|
4185
|
-
const SPINNER_FRAMES = [
|
|
4186
|
-
"⠋",
|
|
4187
|
-
"⠙",
|
|
4188
|
-
"⠹",
|
|
4189
|
-
"⠸",
|
|
4190
|
-
"⠼",
|
|
4191
|
-
"⠴",
|
|
4192
|
-
"⠦",
|
|
4193
|
-
"⠧",
|
|
4194
|
-
"⠇",
|
|
4195
|
-
"⠏"
|
|
4196
|
-
];
|
|
4197
|
-
/** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
|
|
4198
|
-
const SPINNER_INTERVAL_MS = 80;
|
|
4199
|
-
/** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
|
|
4200
|
-
function spinnerFrame(tick) {
|
|
4201
|
-
const n = SPINNER_FRAMES.length;
|
|
4202
|
-
return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
|
|
4203
|
-
}
|
|
4204
|
-
|
|
4205
|
-
//#endregion
|
|
4206
|
-
//#region src/chat/tui/spinner.tsx
|
|
4207
|
-
/**
|
|
4208
|
-
* An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
|
|
4209
|
-
* a self-owned interval that's cleared on unmount, so the animation runs only
|
|
4210
|
-
* while the component is mounted. Returns a <span>, so render it inside a
|
|
4211
|
-
* <text> (matching how the rest of the TUI composes inline glyphs).
|
|
4212
|
-
*/
|
|
4213
|
-
function Spinner({ color }) {
|
|
4214
|
-
const [tick, setTick] = useState(0);
|
|
4215
|
-
useEffect(() => {
|
|
4216
|
-
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
|
|
4217
|
-
return () => clearInterval(id);
|
|
4218
|
-
}, []);
|
|
4219
|
-
return /* @__PURE__ */ jsx("span", {
|
|
4220
|
-
fg: color,
|
|
4221
|
-
children: spinnerFrame(tick)
|
|
4222
|
-
});
|
|
4223
|
-
}
|
|
4224
|
-
|
|
4225
|
-
//#endregion
|
|
4226
|
-
//#region src/chat/tui/chat/subagents.ts
|
|
4227
|
-
/** A run the platform hasn't settled yet — still queued or mid-run. */
|
|
4228
|
-
function isLiveStatus(status) {
|
|
4229
|
-
return status === "queued" || status === "running";
|
|
4230
|
-
}
|
|
4231
|
-
function toTaskStatus(raw) {
|
|
4232
|
-
switch (raw) {
|
|
4233
|
-
case "queued":
|
|
4234
|
-
case "running":
|
|
4235
|
-
case "completed":
|
|
4236
|
-
case "failed":
|
|
4237
|
-
case "canceled": return raw;
|
|
4238
|
-
default: return "queued";
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
const FALLBACK_TITLE = "Subagent task";
|
|
4242
|
-
/**
|
|
4243
|
-
* Each delegated task's display title from the `subagent` call's input, in
|
|
4244
|
-
* call order — the short name the tool requires per task. A malformed entry
|
|
4245
|
-
* yields a fallback label rather than being dropped, so the row count always
|
|
4246
|
-
* matches the fan-out width. Mirrors the web client's `subagentTaskTitles`.
|
|
4247
|
-
*/
|
|
4248
|
-
function subagentTaskTitles(input) {
|
|
4249
|
-
if (!isRecord(input)) return [];
|
|
4250
|
-
const tasks = input["tasks"];
|
|
4251
|
-
if (!Array.isArray(tasks)) return [];
|
|
4252
|
-
return tasks.map((task) => {
|
|
4253
|
-
if (!isRecord(task)) return FALLBACK_TITLE;
|
|
4254
|
-
const title = task["title"];
|
|
4255
|
-
return typeof title === "string" && title.trim() ? title.trim() : FALLBACK_TITLE;
|
|
4256
|
-
});
|
|
4257
|
-
}
|
|
4258
|
-
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;
|
|
4259
|
-
/**
|
|
4260
|
-
* The ids of the runs a `subagent` call queued, read back out of its own
|
|
4261
|
-
* output. The call returns the moment the runs exist, so the ids are there
|
|
4262
|
-
* long before any child finishes — they are what ties this card to the task
|
|
4263
|
-
* rows the status poll returns, and their order is the tasks' call order.
|
|
4264
|
-
*/
|
|
4265
|
-
function queuedSubagentTaskIds(output) {
|
|
4266
|
-
const text = flattenOutputText(output);
|
|
4267
|
-
if (!text) return [];
|
|
4268
|
-
return text.split("\n").map((line) => line.match(QUEUED_TASK_ID_LINE)?.[1] ?? null).filter((id) => id !== null);
|
|
4269
|
-
}
|
|
4270
|
-
/**
|
|
4271
|
-
* Tool output reaches the TUI as `unknown`: the raw string live, possibly
|
|
4272
|
-
* wrapped in content parts or arrays when loaded from history. Walk it for
|
|
4273
|
-
* string leaves, newline-joined — the PR scanner's `stringifyToolOutput`
|
|
4274
|
-
* shape, bounded small because the id list sits at the top of the output.
|
|
4275
|
-
*/
|
|
4276
|
-
function flattenOutputText(value) {
|
|
4277
|
-
const MAX_LEN = 2e4;
|
|
4278
|
-
const parts = [];
|
|
4279
|
-
let total = 0;
|
|
4280
|
-
const visit = (v, depth) => {
|
|
4281
|
-
if (total >= MAX_LEN || depth > 4) return;
|
|
4282
|
-
if (typeof v === "string") {
|
|
4283
|
-
parts.push(v);
|
|
4284
|
-
total += v.length;
|
|
4285
|
-
return;
|
|
4286
|
-
}
|
|
4287
|
-
if (Array.isArray(v)) {
|
|
4288
|
-
for (const item of v) visit(item, depth + 1);
|
|
4289
|
-
return;
|
|
4290
|
-
}
|
|
4291
|
-
if (isRecord(v)) for (const item of Object.values(v)) visit(item, depth + 1);
|
|
4292
|
-
};
|
|
4293
|
-
visit(value, 0);
|
|
4294
|
-
return parts.join("\n");
|
|
4295
|
-
}
|
|
4804
|
+
//#region src/chat/tui/chat/sandbox-stats.ts
|
|
4296
4805
|
/**
|
|
4297
|
-
*
|
|
4298
|
-
*
|
|
4299
|
-
*
|
|
4300
|
-
*
|
|
4301
|
-
*
|
|
4302
|
-
*
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4806
|
+
* `/computer`: a live snapshot of the agent's sandbox as a set of bar graphs.
|
|
4807
|
+
*
|
|
4808
|
+
* The numbers come from the api, NOT from the CLI reaching into the sandbox.
|
|
4809
|
+
* The worker's fleet metrics sweep already samples every live box's CPU /
|
|
4810
|
+
* memory / disk from the provider and stores the latest per-sandbox snapshot;
|
|
4811
|
+
* the CLI reads that back over one plain REST call (`collectSandboxStats`). So
|
|
4812
|
+
* there is no exec into the box and no cold-start wake — when the agent has no
|
|
4813
|
+
* running sandbox the api answers "no snapshot" immediately instead of the card
|
|
4814
|
+
* hanging on a stream that never replies.
|
|
4815
|
+
*
|
|
4816
|
+
* Everything in this module is pure so the layout math is unit-testable without
|
|
4817
|
+
* an api or a renderer.
|
|
4818
|
+
*/
|
|
4819
|
+
/**
|
|
4820
|
+
* How often the live `/computer` card refreshes its snapshot, in ms.
|
|
4821
|
+
* Deliberately slow: the card is a background health readout, not a `top`, so
|
|
4822
|
+
* it refreshes quietly every 30s rather than flickering every second. Also
|
|
4823
|
+
* comfortably slower than the worker's ~60s sweep cadence, so we never poll for
|
|
4824
|
+
* a sample that can't have changed.
|
|
4825
|
+
*/
|
|
4826
|
+
const COMPUTER_POLL_INTERVAL_MS = 3e4;
|
|
4827
|
+
/**
|
|
4828
|
+
* A snapshot older than this is stale enough that we no longer present it as
|
|
4829
|
+
* "current". The api sample carries the provider's measurement time; the sweep
|
|
4830
|
+
* that writes it runs ~every 60s and the Redis key TTLs at 5 min, so a sample
|
|
4831
|
+
* this old means the box likely stopped reporting (paused / gone). The card
|
|
4832
|
+
* treats it as "no current snapshot" rather than drawing minutes-old bars as if
|
|
4833
|
+
* they were live.
|
|
4834
|
+
*/
|
|
4835
|
+
const COMPUTER_STATS_STALE_MS = 5 * 6e4;
|
|
4836
|
+
/**
|
|
4837
|
+
* Whether a `/computer` card should refresh right now. Pure so the gating is
|
|
4838
|
+
* unit-testable. A card refreshes only when it is the most recent `/computer`
|
|
4839
|
+
* in the transcript (`isLatest`), the session can reach the api
|
|
4840
|
+
* (`canCollect`), and the transcript is at the bottom (`!scrolledUp`) — while
|
|
4841
|
+
* you're scrolled up reading something else it quietly stops (no wasted polls),
|
|
4842
|
+
* and resumes when you scroll back down. Older cards freeze regardless, so a
|
|
4843
|
+
* scrolled-up history of runs isn't a fleet of background pollers. The pause is
|
|
4844
|
+
* silent: nothing about it is rendered.
|
|
4845
|
+
*/
|
|
4846
|
+
function shouldPollComputer(opts) {
|
|
4847
|
+
return opts.isLatest && opts.canCollect && !opts.scrolledUp;
|
|
4848
|
+
}
|
|
4849
|
+
function severityFor(fraction) {
|
|
4850
|
+
if (fraction >= .9) return "crit";
|
|
4851
|
+
if (fraction >= .75) return "warn";
|
|
4852
|
+
return "ok";
|
|
4853
|
+
}
|
|
4854
|
+
const GiB = 1024 * 1024 * 1024;
|
|
4855
|
+
function gb(bytes) {
|
|
4856
|
+
return (bytes / GiB).toFixed(1);
|
|
4857
|
+
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Reduce a snapshot to the ordered list of gauges the `/computer` card draws:
|
|
4860
|
+
* CPU utilization, memory, and disk — the three signals the api's live-stats
|
|
4861
|
+
* read carries. CPU is a provider-measured utilization percent (already
|
|
4862
|
+
* normalized across cores), so it maps straight to a 0..1 bar; the caption
|
|
4863
|
+
* shows the vCPU count for context.
|
|
4864
|
+
*/
|
|
4865
|
+
function toGauges(stats) {
|
|
4866
|
+
const gauges = [];
|
|
4867
|
+
const cpuFraction = stats.cpuUsedPct / 100;
|
|
4868
|
+
gauges.push({
|
|
4869
|
+
label: "cpu",
|
|
4870
|
+
fraction: cpuFraction,
|
|
4871
|
+
detail: `${stats.cpuUsedPct.toFixed(0)}% · ${stats.cpuCount} vCPU`,
|
|
4872
|
+
severity: severityFor(cpuFraction)
|
|
4873
|
+
});
|
|
4874
|
+
const memFraction = stats.memTotalBytes > 0 ? stats.memUsedBytes / stats.memTotalBytes : 0;
|
|
4875
|
+
gauges.push({
|
|
4876
|
+
label: "memory",
|
|
4877
|
+
fraction: memFraction,
|
|
4878
|
+
detail: `${gb(stats.memUsedBytes)} / ${gb(stats.memTotalBytes)} GB`,
|
|
4879
|
+
severity: severityFor(memFraction)
|
|
4880
|
+
});
|
|
4881
|
+
const diskFraction = stats.diskTotalBytes > 0 ? stats.diskUsedBytes / stats.diskTotalBytes : 0;
|
|
4882
|
+
gauges.push({
|
|
4883
|
+
label: "disk",
|
|
4884
|
+
fraction: diskFraction,
|
|
4885
|
+
detail: `${gb(stats.diskUsedBytes)} / ${gb(stats.diskTotalBytes)} GB`,
|
|
4886
|
+
severity: severityFor(diskFraction)
|
|
4887
|
+
});
|
|
4888
|
+
return gauges;
|
|
4889
|
+
}
|
|
4890
|
+
/**
|
|
4891
|
+
* A single-line horizontal bar of `width` cells filled to `fraction`
|
|
4892
|
+
* (clamped 0..1), using block glyphs. The last partial cell uses one of the
|
|
4893
|
+
* eighth-block glyphs so a 37%% bar reads differently from a 40%% one even in a
|
|
4894
|
+
* narrow width. Pure string, so the renderer just colors it.
|
|
4895
|
+
*/
|
|
4896
|
+
function renderBar(fraction, width) {
|
|
4897
|
+
const cells = Math.max(1, width);
|
|
4898
|
+
const exact = Math.min(1, Math.max(0, fraction)) * cells;
|
|
4899
|
+
const full = Math.floor(exact);
|
|
4900
|
+
const remainder = exact - full;
|
|
4901
|
+
const eighths = [
|
|
4902
|
+
"",
|
|
4903
|
+
"▏",
|
|
4904
|
+
"▎",
|
|
4905
|
+
"▍",
|
|
4906
|
+
"▌",
|
|
4907
|
+
"▋",
|
|
4908
|
+
"▊",
|
|
4909
|
+
"▉"
|
|
4910
|
+
];
|
|
4911
|
+
const partialIndex = Math.round(remainder * 8);
|
|
4912
|
+
const partial = full < cells ? eighths[partialIndex] : "";
|
|
4913
|
+
const filled = "█".repeat(Math.min(full, cells)) + partial;
|
|
4914
|
+
return (filled + "░".repeat(Math.max(0, cells - filled.length))).slice(0, cells);
|
|
4322
4915
|
}
|
|
4323
4916
|
|
|
4324
4917
|
//#endregion
|
|
4325
|
-
//#region src/chat/tui/chunks/
|
|
4326
|
-
/** How often a live batch re-reads its tasks' status. */
|
|
4327
|
-
const POLL_INTERVAL_MS = 3e3;
|
|
4328
|
-
function rowGlyph(status) {
|
|
4329
|
-
switch (status) {
|
|
4330
|
-
case "completed": return {
|
|
4331
|
-
ch: "✔",
|
|
4332
|
-
fg: theme.success
|
|
4333
|
-
};
|
|
4334
|
-
case "failed":
|
|
4335
|
-
case "canceled": return {
|
|
4336
|
-
ch: "✗",
|
|
4337
|
-
fg: theme.error
|
|
4338
|
-
};
|
|
4339
|
-
case "running": return {
|
|
4340
|
-
ch: "▶",
|
|
4341
|
-
fg: theme.accent
|
|
4342
|
-
};
|
|
4343
|
-
case "queued": return {
|
|
4344
|
-
ch: "○",
|
|
4345
|
-
fg: theme.dim
|
|
4346
|
-
};
|
|
4347
|
-
}
|
|
4348
|
-
}
|
|
4349
|
-
function rowColor$1(status) {
|
|
4350
|
-
switch (status) {
|
|
4351
|
-
case "running": return theme.fg;
|
|
4352
|
-
case "completed": return theme.dim;
|
|
4353
|
-
default: return theme.muted;
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
function headerLabel(rows) {
|
|
4357
|
-
const subject = rows.length === 1 ? "Subagent" : `${rows.length} subagents`;
|
|
4358
|
-
return rows.some((row) => isLiveStatus(row.status)) ? `${subject} running` : subject;
|
|
4359
|
-
}
|
|
4918
|
+
//#region src/chat/tui/chunks/computer.tsx
|
|
4360
4919
|
/**
|
|
4361
|
-
*
|
|
4362
|
-
*
|
|
4363
|
-
* title the delegating agent wrote, ticking over live as children settle.
|
|
4920
|
+
* Renders the `/computer` card: a snapshot of the agent's sandbox as a stack of
|
|
4921
|
+
* colored bar graphs (cpu, memory, disk).
|
|
4364
4922
|
*
|
|
4365
|
-
*
|
|
4366
|
-
*
|
|
4367
|
-
*
|
|
4368
|
-
*
|
|
4369
|
-
*
|
|
4370
|
-
*
|
|
4371
|
-
*
|
|
4372
|
-
*
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4923
|
+
* The numbers come from the api, not from reaching into the box: the card polls
|
|
4924
|
+
* `collectSandboxStats` (injected via context), which reads the live resource
|
|
4925
|
+
* sample the worker's fleet sweep already stores. So there's no exec and no
|
|
4926
|
+
* cold-start wake — when the agent has no running sandbox the read returns
|
|
4927
|
+
* "no snapshot" at once and the card shows an idle line instead of hanging.
|
|
4928
|
+
*
|
|
4929
|
+
* The card keeps itself current in place rather than making the user re-run the
|
|
4930
|
+
* command — but quietly: it refreshes on a slow interval with no "updating"
|
|
4931
|
+
* chrome, so the numbers just stay fresh without any flicker. Only the most
|
|
4932
|
+
* recent `/computer` card refreshes (older ones freeze at their last snapshot),
|
|
4933
|
+
* and the latest one silently stops while the transcript is scrolled up (no
|
|
4934
|
+
* wasted polls on a card you're not looking at) and resumes at the bottom — the
|
|
4935
|
+
* pause is never rendered. The parse/layout math lives in `sandbox-stats.ts` and
|
|
4936
|
+
* this component only draws.
|
|
4937
|
+
*/
|
|
4938
|
+
function ComputerItem({ item }) {
|
|
4939
|
+
const { collectSandboxStats, latestComputerId, scrolledUp } = useConversationContext();
|
|
4940
|
+
const live = shouldPollComputer({
|
|
4941
|
+
isLatest: latestComputerId === item.id,
|
|
4942
|
+
canCollect: collectSandboxStats !== null,
|
|
4943
|
+
scrolledUp
|
|
4944
|
+
});
|
|
4945
|
+
const [stats, setStats] = useState(item.stats);
|
|
4946
|
+
const [error, setError] = useState(item.error);
|
|
4947
|
+
const hasResolved = useRef(item.stats !== null || item.state !== "loading");
|
|
4948
|
+
const [idle, setIdle] = useState(item.state !== "loading" && item.stats === null && item.error === null);
|
|
4386
4949
|
useEffect(() => {
|
|
4387
|
-
if (!
|
|
4950
|
+
if (!live || !collectSandboxStats) return;
|
|
4388
4951
|
let cancelled = false;
|
|
4389
4952
|
let timer = null;
|
|
4953
|
+
let inFlight = null;
|
|
4390
4954
|
const tick = async () => {
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
if (
|
|
4955
|
+
const collection = collectSandboxStats();
|
|
4956
|
+
inFlight = collection;
|
|
4957
|
+
const result = await collection.result;
|
|
4958
|
+
inFlight = null;
|
|
4959
|
+
if (cancelled) return;
|
|
4960
|
+
if (result.ok) {
|
|
4961
|
+
hasResolved.current = true;
|
|
4962
|
+
setStats(result.stats);
|
|
4963
|
+
setError(null);
|
|
4964
|
+
setIdle(result.stats === null);
|
|
4965
|
+
} else if (!("aborted" in result)) {
|
|
4966
|
+
if (!hasResolved.current) setError(result.error);
|
|
4967
|
+
}
|
|
4968
|
+
if (!cancelled) timer = setTimeout(tick, COMPUTER_POLL_INTERVAL_MS);
|
|
4397
4969
|
};
|
|
4398
4970
|
tick();
|
|
4399
4971
|
return () => {
|
|
4400
4972
|
cancelled = true;
|
|
4401
4973
|
if (timer !== null) clearTimeout(timer);
|
|
4974
|
+
inFlight?.abort();
|
|
4402
4975
|
};
|
|
4403
|
-
}, [
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
fg: theme.
|
|
4414
|
-
children: "
|
|
4415
|
-
})
|
|
4976
|
+
}, [live, collectSandboxStats]);
|
|
4977
|
+
if (stats === null && error === null && !idle) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
|
|
4978
|
+
fg: theme.dim,
|
|
4979
|
+
children: "reading sandbox stats…"
|
|
4980
|
+
}) });
|
|
4981
|
+
if (stats === null && error !== null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsxs("text", {
|
|
4982
|
+
fg: theme.error,
|
|
4983
|
+
children: ["couldn't read sandbox stats: ", error]
|
|
4984
|
+
}) });
|
|
4985
|
+
if (stats === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
|
|
4986
|
+
fg: theme.dim,
|
|
4987
|
+
children: "no sandbox running"
|
|
4988
|
+
}) });
|
|
4989
|
+
const gauges = toGauges(stats);
|
|
4990
|
+
const labelWidth = Math.max(...gauges.map((g) => g.label.length));
|
|
4991
|
+
const barWidth = 24;
|
|
4992
|
+
return /* @__PURE__ */ jsx(Shell, { children: gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
|
|
4993
|
+
gauge: g,
|
|
4994
|
+
labelWidth,
|
|
4995
|
+
barWidth
|
|
4996
|
+
}, g.label)) });
|
|
4997
|
+
}
|
|
4998
|
+
/** The card frame: a bold `computer` title over the graph rows. */
|
|
4999
|
+
function Shell({ children }) {
|
|
4416
5000
|
return /* @__PURE__ */ jsxs("box", {
|
|
4417
5001
|
style: { flexDirection: "column" },
|
|
4418
|
-
children: [/* @__PURE__ */ jsxs("text", {
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
children: "✓"
|
|
4422
|
-
}),
|
|
4423
|
-
/* @__PURE__ */ jsxs("span", {
|
|
4424
|
-
fg: theme.tool,
|
|
4425
|
-
children: [" ", /* @__PURE__ */ jsx("b", { children: headerLabel(rows) })]
|
|
4426
|
-
}),
|
|
4427
|
-
/* @__PURE__ */ jsxs("span", {
|
|
5002
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
5003
|
+
fg: theme.muted,
|
|
5004
|
+
children: [/* @__PURE__ */ jsx("b", { children: "computer" }), /* @__PURE__ */ jsx("span", {
|
|
4428
5005
|
fg: theme.dim,
|
|
4429
|
-
children:
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
"/",
|
|
4433
|
-
rows.length
|
|
4434
|
-
]
|
|
4435
|
-
})
|
|
4436
|
-
] }), /* @__PURE__ */ jsx("box", {
|
|
4437
|
-
style: {
|
|
4438
|
-
flexDirection: "column",
|
|
4439
|
-
paddingLeft: 2
|
|
4440
|
-
},
|
|
4441
|
-
children: rows.map((row) => {
|
|
4442
|
-
const glyph = rowGlyph(row.status);
|
|
4443
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
4444
|
-
style: { flexDirection: "row" },
|
|
4445
|
-
children: [/* @__PURE__ */ jsxs("text", {
|
|
4446
|
-
fg: glyph.fg,
|
|
4447
|
-
children: [glyph.ch, " "]
|
|
4448
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
4449
|
-
fg: rowColor$1(row.status),
|
|
4450
|
-
children: row.title
|
|
4451
|
-
})]
|
|
4452
|
-
}, row.id);
|
|
4453
|
-
})
|
|
4454
|
-
})]
|
|
5006
|
+
children: " · agent sandbox"
|
|
5007
|
+
})]
|
|
5008
|
+
}), children]
|
|
4455
5009
|
});
|
|
4456
5010
|
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
const
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
/* @__PURE__ */ jsx("span", {
|
|
4474
|
-
fg: running ? theme.dim : failed ? theme.error : theme.success,
|
|
4475
|
-
children: "! "
|
|
4476
|
-
}),
|
|
4477
|
-
/* @__PURE__ */ jsx("span", {
|
|
4478
|
-
fg: theme.tool,
|
|
4479
|
-
children: item.command
|
|
4480
|
-
}),
|
|
4481
|
-
running ? /* @__PURE__ */ jsx("span", {
|
|
4482
|
-
fg: theme.dim,
|
|
4483
|
-
children: " · running…"
|
|
4484
|
-
}) : null,
|
|
4485
|
-
failed ? /* @__PURE__ */ jsxs("span", {
|
|
4486
|
-
fg: theme.error,
|
|
4487
|
-
children: [" · exit ", item.exit]
|
|
4488
|
-
}) : null
|
|
4489
|
-
] }), item.output ? /* @__PURE__ */ jsx(Indented, { children: /* @__PURE__ */ jsx("text", {
|
|
5011
|
+
function severityColor(severity) {
|
|
5012
|
+
switch (severity) {
|
|
5013
|
+
case "ok": return theme.success;
|
|
5014
|
+
case "warn": return theme.warning;
|
|
5015
|
+
case "crit": return theme.error;
|
|
5016
|
+
default: {
|
|
5017
|
+
const exhaustive = severity;
|
|
5018
|
+
throw new Error(`unhandled severity: ${String(exhaustive)}`);
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
function GaugeRow({ gauge, labelWidth, barWidth }) {
|
|
5023
|
+
const color = severityColor(gauge.severity);
|
|
5024
|
+
const pct = `${Math.round(Math.min(1, Math.max(0, gauge.fraction)) * 100)}%`;
|
|
5025
|
+
return /* @__PURE__ */ jsxs("text", { children: [
|
|
5026
|
+
/* @__PURE__ */ jsx("span", {
|
|
4490
5027
|
fg: theme.muted,
|
|
4491
|
-
children:
|
|
4492
|
-
})
|
|
4493
|
-
|
|
5028
|
+
children: `${gauge.label.padEnd(labelWidth)} `
|
|
5029
|
+
}),
|
|
5030
|
+
/* @__PURE__ */ jsx("span", {
|
|
5031
|
+
fg: color,
|
|
5032
|
+
children: renderBar(gauge.fraction, barWidth)
|
|
5033
|
+
}),
|
|
5034
|
+
/* @__PURE__ */ jsx("span", {
|
|
5035
|
+
fg: color,
|
|
5036
|
+
children: ` ${pct.padStart(4)}`
|
|
5037
|
+
}),
|
|
5038
|
+
/* @__PURE__ */ jsx("span", {
|
|
5039
|
+
fg: theme.dim,
|
|
5040
|
+
children: ` ${gauge.detail}`
|
|
5041
|
+
})
|
|
5042
|
+
] });
|
|
4494
5043
|
}
|
|
4495
5044
|
|
|
4496
5045
|
//#endregion
|
|
@@ -4529,20 +5078,23 @@ function RenderItem({ item }) {
|
|
|
4529
5078
|
children: [item.text ? /* @__PURE__ */ jsx("text", {
|
|
4530
5079
|
fg: theme.user,
|
|
4531
5080
|
children: item.text
|
|
4532
|
-
}) : null, item.attachments
|
|
5081
|
+
}) : null, item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
|
|
4533
5082
|
fg: theme.muted,
|
|
4534
5083
|
children: ["⎘ ", item.attachments.join(" · ")]
|
|
4535
5084
|
}) : null]
|
|
4536
5085
|
});
|
|
4537
|
-
case "pending-steer": return /* @__PURE__ */
|
|
5086
|
+
case "pending-steer": return /* @__PURE__ */ jsxs(Row, {
|
|
4538
5087
|
barColor: theme.dim,
|
|
4539
|
-
children: /* @__PURE__ */ jsxs("text", {
|
|
5088
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
4540
5089
|
fg: theme.muted,
|
|
4541
5090
|
children: [item.text, /* @__PURE__ */ jsx("span", {
|
|
4542
5091
|
fg: theme.dim,
|
|
4543
5092
|
children: " · steering… (esc to cancel)"
|
|
4544
5093
|
})]
|
|
4545
|
-
})
|
|
5094
|
+
}), item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
|
|
5095
|
+
fg: theme.dim,
|
|
5096
|
+
children: ["⎘ ", item.attachments.join(" · ")]
|
|
5097
|
+
}) : null]
|
|
4546
5098
|
});
|
|
4547
5099
|
case "assistant-text":
|
|
4548
5100
|
if (item.done) return /* @__PURE__ */ jsx(Row, {
|
|
@@ -4588,6 +5140,10 @@ function RenderItem({ item }) {
|
|
|
4588
5140
|
})]
|
|
4589
5141
|
})
|
|
4590
5142
|
});
|
|
5143
|
+
case "computer": return /* @__PURE__ */ jsx(Row, {
|
|
5144
|
+
barColor: null,
|
|
5145
|
+
children: /* @__PURE__ */ jsx(ComputerItem, { item })
|
|
5146
|
+
});
|
|
4591
5147
|
case "recap": return /* @__PURE__ */ jsx(Row, {
|
|
4592
5148
|
barColor: null,
|
|
4593
5149
|
children: /* @__PURE__ */ jsxs("text", {
|
|
@@ -4678,7 +5234,7 @@ function ToolItem({ item }) {
|
|
|
4678
5234
|
case "read": return /* @__PURE__ */ jsx(ToolRead, { item });
|
|
4679
5235
|
case "grep":
|
|
4680
5236
|
case "glob": return /* @__PURE__ */ jsx(ToolGrep, { item });
|
|
4681
|
-
case "subagent": return
|
|
5237
|
+
case "subagent": return null;
|
|
4682
5238
|
default: return /* @__PURE__ */ jsx(ToolGeneric, { item });
|
|
4683
5239
|
}
|
|
4684
5240
|
}
|
|
@@ -5277,7 +5833,8 @@ function applyChunk(items, chunk, agentName = null) {
|
|
|
5277
5833
|
return items.map((m) => m.kind === "pending-steer" && ids.has(m.id) ? {
|
|
5278
5834
|
kind: "user",
|
|
5279
5835
|
id: m.id,
|
|
5280
|
-
text: m.text
|
|
5836
|
+
text: m.text,
|
|
5837
|
+
attachments: m.attachments
|
|
5281
5838
|
} : m);
|
|
5282
5839
|
}
|
|
5283
5840
|
case "data-anyone-render-spec": {
|
|
@@ -5287,6 +5844,22 @@ function applyChunk(items, chunk, agentName = null) {
|
|
|
5287
5844
|
if (!card) return [...items];
|
|
5288
5845
|
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : null;
|
|
5289
5846
|
const specKey = specKeyFor(data.spec);
|
|
5847
|
+
if (card.computeRequestId !== null) {
|
|
5848
|
+
const existingIndex = items.findIndex((m) => m.kind === "card" && m.card.computeRequestId === card.computeRequestId);
|
|
5849
|
+
const existing = items[existingIndex];
|
|
5850
|
+
if (existing?.kind === "card") {
|
|
5851
|
+
if (existing.specKey === specKey || card.settled === null) return [...items];
|
|
5852
|
+
const next = [...items];
|
|
5853
|
+
next[existingIndex] = {
|
|
5854
|
+
...existing,
|
|
5855
|
+
specKey,
|
|
5856
|
+
card,
|
|
5857
|
+
status: "done",
|
|
5858
|
+
detail: card.settled
|
|
5859
|
+
};
|
|
5860
|
+
return next;
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5290
5863
|
if (items.some((m) => m.kind === "card" && m.toolCallId === toolCallId && m.specKey === specKey)) return [...items];
|
|
5291
5864
|
return [...items, {
|
|
5292
5865
|
kind: "card",
|
|
@@ -5294,8 +5867,8 @@ function applyChunk(items, chunk, agentName = null) {
|
|
|
5294
5867
|
toolCallId,
|
|
5295
5868
|
specKey,
|
|
5296
5869
|
card,
|
|
5297
|
-
status: "idle",
|
|
5298
|
-
detail:
|
|
5870
|
+
status: card.settled === null ? "idle" : "done",
|
|
5871
|
+
detail: card.settled,
|
|
5299
5872
|
agentName
|
|
5300
5873
|
}];
|
|
5301
5874
|
}
|
|
@@ -5518,7 +6091,7 @@ function uiMessagesToItems(messages) {
|
|
|
5518
6091
|
kind: "user",
|
|
5519
6092
|
id: m.id,
|
|
5520
6093
|
text,
|
|
5521
|
-
|
|
6094
|
+
attachments
|
|
5522
6095
|
});
|
|
5523
6096
|
continue;
|
|
5524
6097
|
}
|
|
@@ -5557,6 +6130,20 @@ function uiMessagesToItems(messages) {
|
|
|
5557
6130
|
if (card && data) {
|
|
5558
6131
|
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : null;
|
|
5559
6132
|
const specKey = specKeyFor(data.spec);
|
|
6133
|
+
if (card.computeRequestId !== null) {
|
|
6134
|
+
const existingIndex = out.findIndex((item) => item.kind === "card" && item.card.computeRequestId === card.computeRequestId);
|
|
6135
|
+
const existing = out[existingIndex];
|
|
6136
|
+
if (existing?.kind === "card") {
|
|
6137
|
+
if (existing.specKey !== specKey && card.settled !== null) out[existingIndex] = {
|
|
6138
|
+
...existing,
|
|
6139
|
+
specKey,
|
|
6140
|
+
card,
|
|
6141
|
+
status: "done",
|
|
6142
|
+
detail: card.settled
|
|
6143
|
+
};
|
|
6144
|
+
continue;
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
5560
6147
|
if (out.some((item) => item.kind === "card" && item.toolCallId === toolCallId && item.specKey === specKey)) continue;
|
|
5561
6148
|
const ordinal = out.filter((item) => item.kind === "card" && item.toolCallId === toolCallId).length;
|
|
5562
6149
|
out.push({
|
|
@@ -5565,8 +6152,8 @@ function uiMessagesToItems(messages) {
|
|
|
5565
6152
|
toolCallId,
|
|
5566
6153
|
specKey,
|
|
5567
6154
|
card,
|
|
5568
|
-
status: "idle",
|
|
5569
|
-
detail:
|
|
6155
|
+
status: card.settled === null ? "idle" : "done",
|
|
6156
|
+
detail: card.settled,
|
|
5570
6157
|
agentName
|
|
5571
6158
|
});
|
|
5572
6159
|
}
|
|
@@ -5675,7 +6262,7 @@ function windowTodos(todos, cap = MAX_VISIBLE_ROWS) {
|
|
|
5675
6262
|
const sorted = [...todos].sort((a, b) => a.position - b.position);
|
|
5676
6263
|
let leadingDone = 0;
|
|
5677
6264
|
while (leadingDone < sorted.length && sorted[leadingDone]?.status === "completed") leadingDone += 1;
|
|
5678
|
-
const head = leadingDone >= 2 || sorted.length > cap ? leadingDone : 0;
|
|
6265
|
+
const head = leadingDone === sorted.length && sorted.length > 0 || leadingDone >= 2 || sorted.length > cap ? leadingDone : 0;
|
|
5679
6266
|
const rest = sorted.slice(head);
|
|
5680
6267
|
const rowBudget = Math.max(1, head > 0 ? cap - 1 : cap);
|
|
5681
6268
|
if (rest.length <= rowBudget) return {
|
|
@@ -5706,7 +6293,7 @@ function todoCardHeight(todos) {
|
|
|
5706
6293
|
}
|
|
5707
6294
|
|
|
5708
6295
|
//#endregion
|
|
5709
|
-
//#region src/chat/tui/chat/
|
|
6296
|
+
//#region src/chat/tui/chat/task-card.tsx
|
|
5710
6297
|
function glyph(status) {
|
|
5711
6298
|
switch (status) {
|
|
5712
6299
|
case "completed": return {
|
|
@@ -5735,38 +6322,18 @@ function rowColor(status) {
|
|
|
5735
6322
|
default: return theme.muted;
|
|
5736
6323
|
}
|
|
5737
6324
|
}
|
|
5738
|
-
/**
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
* written a list (`platform todo write`) and until it clears one, so a
|
|
5742
|
-
* conversation the agent never wrote todos for grows no chrome for it.
|
|
5743
|
-
*
|
|
5744
|
-
* The card can't scroll like the web card, so it windows (see `windowTodos`):
|
|
5745
|
-
* a leading run of completed todos folds into a `✔ N done` line, the active
|
|
5746
|
-
* and upcoming rows fill the rest, and a `+N more` tail counts pending work
|
|
5747
|
-
* past the cap. The window follows the in-progress row down as todos complete.
|
|
5748
|
-
*
|
|
5749
|
-
* `isActive` dims the header while the run is idle: an in-progress row only
|
|
5750
|
-
* reads as "happening now" when a run is actually producing output.
|
|
5751
|
-
*/
|
|
5752
|
-
function TodoCardView({ todos, isActive }) {
|
|
5753
|
-
if (todos.length === 0) return null;
|
|
5754
|
-
const completed = todos.filter((t) => t.status === "completed").length;
|
|
6325
|
+
/** Shared plan/subagent checklist content. The caller owns outer spacing. */
|
|
6326
|
+
function TaskCardContent({ label, todos, isActive }) {
|
|
6327
|
+
const completed = todos.filter((todo) => todo.status === "completed").length;
|
|
5755
6328
|
const { doneSummary, rows, moreCount } = windowTodos(todos);
|
|
5756
6329
|
return /* @__PURE__ */ jsxs("box", {
|
|
5757
|
-
style: {
|
|
5758
|
-
flexDirection: "column",
|
|
5759
|
-
flexShrink: 0,
|
|
5760
|
-
marginTop: 1,
|
|
5761
|
-
paddingLeft: 1,
|
|
5762
|
-
paddingRight: 1
|
|
5763
|
-
},
|
|
6330
|
+
style: { flexDirection: "column" },
|
|
5764
6331
|
children: [
|
|
5765
6332
|
/* @__PURE__ */ jsxs("box", {
|
|
5766
6333
|
style: { flexDirection: "row" },
|
|
5767
6334
|
children: [/* @__PURE__ */ jsx("text", {
|
|
5768
6335
|
fg: isActive ? theme.accent : theme.muted,
|
|
5769
|
-
children: /* @__PURE__ */ jsx("b", { children:
|
|
6336
|
+
children: /* @__PURE__ */ jsx("b", { children: label })
|
|
5770
6337
|
}), /* @__PURE__ */ jsxs("text", {
|
|
5771
6338
|
fg: theme.dim,
|
|
5772
6339
|
children: [
|
|
@@ -5811,6 +6378,260 @@ function TodoCardView({ todos, isActive }) {
|
|
|
5811
6378
|
]
|
|
5812
6379
|
});
|
|
5813
6380
|
}
|
|
6381
|
+
function taskCardContentHeight(todos) {
|
|
6382
|
+
if (todos.length === 0) return 0;
|
|
6383
|
+
const window = windowTodos(todos);
|
|
6384
|
+
return 1 + (window.doneSummary > 0 ? 1 : 0) + window.rows.length + (window.moreCount > 0 ? 1 : 0);
|
|
6385
|
+
}
|
|
6386
|
+
|
|
6387
|
+
//#endregion
|
|
6388
|
+
//#region src/chat/tui/chat/todo-card.tsx
|
|
6389
|
+
/**
|
|
6390
|
+
* The agent's live todo list, pinned just above the composer — the TUI analog
|
|
6391
|
+
* of the web chat's todo card. Rendered only once the agent has actually
|
|
6392
|
+
* written a list (`platform todo write`) and until it clears one, so a
|
|
6393
|
+
* conversation the agent never wrote todos for grows no chrome for it.
|
|
6394
|
+
*
|
|
6395
|
+
* The card can't scroll like the web card, so it windows completed and pending
|
|
6396
|
+
* work. `isActive` dims the header while the run is idle.
|
|
6397
|
+
*/
|
|
6398
|
+
function TodoCardView({ todos, isActive }) {
|
|
6399
|
+
if (todos.length === 0) return null;
|
|
6400
|
+
return /* @__PURE__ */ jsx("box", {
|
|
6401
|
+
style: {
|
|
6402
|
+
flexDirection: "column",
|
|
6403
|
+
flexShrink: 0,
|
|
6404
|
+
marginTop: 1,
|
|
6405
|
+
paddingLeft: 1,
|
|
6406
|
+
paddingRight: 1
|
|
6407
|
+
},
|
|
6408
|
+
children: /* @__PURE__ */ jsx(TaskCardContent, {
|
|
6409
|
+
label: "Plan",
|
|
6410
|
+
todos,
|
|
6411
|
+
isActive
|
|
6412
|
+
})
|
|
6413
|
+
});
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
//#endregion
|
|
6417
|
+
//#region src/chat/tui/chat/subagents.ts
|
|
6418
|
+
/** A run the platform hasn't settled yet — still queued or mid-run. */
|
|
6419
|
+
function isLiveStatus(status) {
|
|
6420
|
+
return status === "queued" || status === "running";
|
|
6421
|
+
}
|
|
6422
|
+
function toTaskStatus(raw) {
|
|
6423
|
+
switch (raw) {
|
|
6424
|
+
case "queued":
|
|
6425
|
+
case "running":
|
|
6426
|
+
case "completed":
|
|
6427
|
+
case "failed":
|
|
6428
|
+
case "canceled": return raw;
|
|
6429
|
+
default: return "queued";
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
const FALLBACK_TITLE = "Subagent task";
|
|
6433
|
+
/**
|
|
6434
|
+
* Each delegated task's display title from the `subagent` call's input, in
|
|
6435
|
+
* call order — the short name the tool requires per task. A malformed entry
|
|
6436
|
+
* yields a fallback label rather than being dropped, so the row count always
|
|
6437
|
+
* matches the fan-out width. Mirrors the web client's `subagentTaskTitles`.
|
|
6438
|
+
*/
|
|
6439
|
+
function subagentTaskTitles(input) {
|
|
6440
|
+
if (!isRecord(input)) return [];
|
|
6441
|
+
const tasks = input["tasks"];
|
|
6442
|
+
if (!Array.isArray(tasks)) return [];
|
|
6443
|
+
return tasks.map((task) => {
|
|
6444
|
+
if (!isRecord(task)) return FALLBACK_TITLE;
|
|
6445
|
+
const title = task["title"];
|
|
6446
|
+
return typeof title === "string" && title.trim() ? title.trim() : FALLBACK_TITLE;
|
|
6447
|
+
});
|
|
6448
|
+
}
|
|
6449
|
+
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;
|
|
6450
|
+
/**
|
|
6451
|
+
* The ids of the runs a `subagent` call queued, read back out of its own
|
|
6452
|
+
* output. The call returns the moment the runs exist, so the ids are there
|
|
6453
|
+
* long before any child finishes — they are what ties this card to the task
|
|
6454
|
+
* rows the status poll returns, and their order is the tasks' call order.
|
|
6455
|
+
*/
|
|
6456
|
+
function queuedSubagentTaskIds(output) {
|
|
6457
|
+
const text = flattenOutputText(output);
|
|
6458
|
+
if (!text) return [];
|
|
6459
|
+
return text.split("\n").map((line) => line.match(QUEUED_TASK_ID_LINE)?.[1] ?? null).filter((id) => id !== null);
|
|
6460
|
+
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Tool output reaches the TUI as `unknown`: the raw string live, possibly
|
|
6463
|
+
* wrapped in content parts or arrays when loaded from history. Walk it for
|
|
6464
|
+
* string leaves, newline-joined — the PR scanner's `stringifyToolOutput`
|
|
6465
|
+
* shape, bounded small because the id list sits at the top of the output.
|
|
6466
|
+
*/
|
|
6467
|
+
function flattenOutputText(value) {
|
|
6468
|
+
const MAX_LEN = 2e4;
|
|
6469
|
+
const parts = [];
|
|
6470
|
+
let total = 0;
|
|
6471
|
+
const visit = (v, depth) => {
|
|
6472
|
+
if (total >= MAX_LEN || depth > 4) return;
|
|
6473
|
+
if (typeof v === "string") {
|
|
6474
|
+
parts.push(v);
|
|
6475
|
+
total += v.length;
|
|
6476
|
+
return;
|
|
6477
|
+
}
|
|
6478
|
+
if (Array.isArray(v)) {
|
|
6479
|
+
for (const item of v) visit(item, depth + 1);
|
|
6480
|
+
return;
|
|
6481
|
+
}
|
|
6482
|
+
if (isRecord(v)) for (const item of Object.values(v)) visit(item, depth + 1);
|
|
6483
|
+
};
|
|
6484
|
+
visit(value, 0);
|
|
6485
|
+
return parts.join("\n");
|
|
6486
|
+
}
|
|
6487
|
+
function nextAnchoredSubagentIds({ current, nextId, liveById }) {
|
|
6488
|
+
if (current.includes(nextId)) return current;
|
|
6489
|
+
return current.some((id) => liveById[id] ?? true) ? [...current, nextId] : [nextId];
|
|
6490
|
+
}
|
|
6491
|
+
/**
|
|
6492
|
+
* The card's rows: one per queued run id (call order), each carrying the
|
|
6493
|
+
* freshest known state. A task the status fetch hasn't returned yet falls
|
|
6494
|
+
* back to the input's title and reads as queued; a fetched row's own title
|
|
6495
|
+
* wins over the input's (the server's copy is the one the child actually
|
|
6496
|
+
* ran under). Before the ids exist — the call is still streaming — rows come
|
|
6497
|
+
* from the input titles alone, so the card never renders empty mid-turn.
|
|
6498
|
+
*/
|
|
6499
|
+
function subagentRows({ titles, ids, fetched }) {
|
|
6500
|
+
if (ids.length === 0) return titles.map((title, index) => ({
|
|
6501
|
+
id: `queued:${index}`,
|
|
6502
|
+
title,
|
|
6503
|
+
status: "queued",
|
|
6504
|
+
createdAt: null,
|
|
6505
|
+
completedAt: null
|
|
6506
|
+
}));
|
|
6507
|
+
return ids.map((id, index) => {
|
|
6508
|
+
const row = fetched?.get(id) ?? null;
|
|
6509
|
+
return {
|
|
6510
|
+
id,
|
|
6511
|
+
title: row?.title ?? titles[index] ?? FALLBACK_TITLE,
|
|
6512
|
+
status: row ? toTaskStatus(row.status) : "queued",
|
|
6513
|
+
createdAt: row?.createdAt ?? null,
|
|
6514
|
+
completedAt: row?.completedAt ?? null
|
|
6515
|
+
};
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
//#endregion
|
|
6520
|
+
//#region src/chat/tui/chunks/tool-subagent.tsx
|
|
6521
|
+
/** How often a live batch re-reads its tasks' status. */
|
|
6522
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
6523
|
+
function subagentTodos(rows) {
|
|
6524
|
+
return rows.map((row, position) => ({
|
|
6525
|
+
id: row.id,
|
|
6526
|
+
content: row.title,
|
|
6527
|
+
position,
|
|
6528
|
+
status: row.status === "completed" ? "completed" : row.status === "running" ? "in_progress" : row.status === "failed" || row.status === "canceled" ? "failed" : "pending"
|
|
6529
|
+
}));
|
|
6530
|
+
}
|
|
6531
|
+
/**
|
|
6532
|
+
* The transcript block for a `subagent` tool call — the TUI analog of the web
|
|
6533
|
+
* chat's fan-out card. One row per delegated run: status glyph, the short
|
|
6534
|
+
* title the delegating agent wrote, ticking over live as children settle.
|
|
6535
|
+
*
|
|
6536
|
+
* Rows are seeded from the call itself (titles from its input, task ids from
|
|
6537
|
+
* its own output), so the card renders the moment the call does. Status then
|
|
6538
|
+
* comes from the server's task rows, polled while any run is live — the
|
|
6539
|
+
* delegated runs outlive the call that spawned them, so the transcript stream
|
|
6540
|
+
* carries nothing more about them. The poll stops the moment the batch
|
|
6541
|
+
* settles; reopening an old conversation does one fetch and stops there. A
|
|
6542
|
+
* failed poll is skipped silently (the next tick retries; stale rows beat an
|
|
6543
|
+
* error row in the scrollback).
|
|
6544
|
+
*/
|
|
6545
|
+
function ToolSubagent({ item, isActive, onLiveChange, onHeightChange }) {
|
|
6546
|
+
const { rest, conversationId } = useConversationContext();
|
|
6547
|
+
const titles = subagentTaskTitles(item.input);
|
|
6548
|
+
const ids = item.output?.kind === "ok" ? queuedSubagentTaskIds(item.output.value) : [];
|
|
6549
|
+
const idsKey = ids.join(",");
|
|
6550
|
+
const [fetched, setFetched] = useState(null);
|
|
6551
|
+
const rows = subagentRows({
|
|
6552
|
+
titles,
|
|
6553
|
+
ids,
|
|
6554
|
+
fetched
|
|
6555
|
+
});
|
|
6556
|
+
const live = rows.length === 0 || rows.some((row) => isLiveStatus(row.status));
|
|
6557
|
+
const todos = subagentTodos(rows);
|
|
6558
|
+
useEffect(() => {
|
|
6559
|
+
onLiveChange?.(live);
|
|
6560
|
+
}, [live, onLiveChange]);
|
|
6561
|
+
useEffect(() => {
|
|
6562
|
+
onHeightChange?.(taskCardContentHeight(todos));
|
|
6563
|
+
}, [onHeightChange, todos]);
|
|
6564
|
+
useEffect(() => {
|
|
6565
|
+
if (!rest || !conversationId || idsKey.length === 0 || !live) return;
|
|
6566
|
+
let cancelled = false;
|
|
6567
|
+
let timer = null;
|
|
6568
|
+
const tick = async () => {
|
|
6569
|
+
try {
|
|
6570
|
+
const tasks = await rest.listSubagentTasks({ conversationId });
|
|
6571
|
+
if (cancelled) return;
|
|
6572
|
+
setFetched(tasks);
|
|
6573
|
+
} catch (_error) {}
|
|
6574
|
+
if (!cancelled) timer = setTimeout(tick, POLL_INTERVAL_MS);
|
|
6575
|
+
};
|
|
6576
|
+
tick();
|
|
6577
|
+
return () => {
|
|
6578
|
+
cancelled = true;
|
|
6579
|
+
if (timer !== null) clearTimeout(timer);
|
|
6580
|
+
};
|
|
6581
|
+
}, [
|
|
6582
|
+
rest,
|
|
6583
|
+
conversationId,
|
|
6584
|
+
idsKey,
|
|
6585
|
+
live
|
|
6586
|
+
]);
|
|
6587
|
+
if (rows.length === 0) return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
6588
|
+
fg: theme.warning,
|
|
6589
|
+
children: "◐ "
|
|
6590
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6591
|
+
fg: theme.tool,
|
|
6592
|
+
children: "Delegating to subagents…"
|
|
6593
|
+
})] });
|
|
6594
|
+
return /* @__PURE__ */ jsx(TaskCardContent, {
|
|
6595
|
+
label: rows.length === 1 ? "Subagent" : "Subagents",
|
|
6596
|
+
todos,
|
|
6597
|
+
isActive: isActive ?? live
|
|
6598
|
+
});
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
//#endregion
|
|
6602
|
+
//#region src/chat/tui/chat/subagent-card.tsx
|
|
6603
|
+
/**
|
|
6604
|
+
* Delegated work pinned above the plan. Each batch owns its status polling and
|
|
6605
|
+
* stays as a dimmed completion summary after every run settles.
|
|
6606
|
+
*/
|
|
6607
|
+
function SubagentCardView({ items, isActive, onHeightChange, onBatchLiveChange }) {
|
|
6608
|
+
const [heightById, setHeightById] = useState({});
|
|
6609
|
+
const setBatchHeight = useCallback((id, height) => {
|
|
6610
|
+
setHeightById((current) => current[id] === height ? current : {
|
|
6611
|
+
...current,
|
|
6612
|
+
[id]: height
|
|
6613
|
+
});
|
|
6614
|
+
}, []);
|
|
6615
|
+
const height = useMemo(() => items.length === 0 ? 0 : 1 + items.reduce((rows, item) => rows + (heightById[item.id] ?? 0), 0) + (items.length - 1), [heightById, items]);
|
|
6616
|
+
useEffect(() => onHeightChange?.(height), [height, onHeightChange]);
|
|
6617
|
+
if (items.length === 0) return null;
|
|
6618
|
+
return /* @__PURE__ */ jsx("box", {
|
|
6619
|
+
style: {
|
|
6620
|
+
flexDirection: "column",
|
|
6621
|
+
flexShrink: 0,
|
|
6622
|
+
marginTop: 1,
|
|
6623
|
+
paddingLeft: 1,
|
|
6624
|
+
paddingRight: 1,
|
|
6625
|
+
gap: 1
|
|
6626
|
+
},
|
|
6627
|
+
children: items.map((item) => /* @__PURE__ */ jsx(ToolSubagent, {
|
|
6628
|
+
item,
|
|
6629
|
+
isActive,
|
|
6630
|
+
onLiveChange: (live) => onBatchLiveChange?.(item.id, live),
|
|
6631
|
+
onHeightChange: (batchHeight) => setBatchHeight(item.id, batchHeight)
|
|
6632
|
+
}, item.id))
|
|
6633
|
+
});
|
|
6634
|
+
}
|
|
5814
6635
|
|
|
5815
6636
|
//#endregion
|
|
5816
6637
|
//#region src/chat/tui/chat/pr-links.tsx
|
|
@@ -5969,6 +6790,55 @@ function formatShare(share) {
|
|
|
5969
6790
|
return share.grantedToAgent ? "shared (this agent can run commands here)" : "shared (this agent is not granted)";
|
|
5970
6791
|
}
|
|
5971
6792
|
|
|
6793
|
+
//#endregion
|
|
6794
|
+
//#region src/chat/sandbox/collect-stats.ts
|
|
6795
|
+
/**
|
|
6796
|
+
* Drop a snapshot the api hasn't refreshed recently: the sweep that writes it
|
|
6797
|
+
* runs ~every 60s, so a sample older than {@link COMPUTER_STATS_STALE_MS} means
|
|
6798
|
+
* the box stopped reporting (paused / gone) and its numbers are no longer live.
|
|
6799
|
+
* Presenting it as current would be a lie, so treat it as "no snapshot".
|
|
6800
|
+
*/
|
|
6801
|
+
function freshOrNull(stats, now) {
|
|
6802
|
+
if (!stats) return null;
|
|
6803
|
+
return now - stats.sampledAt <= COMPUTER_STATS_STALE_MS ? stats : null;
|
|
6804
|
+
}
|
|
6805
|
+
function collectSandboxStats(cfg) {
|
|
6806
|
+
let aborted = false;
|
|
6807
|
+
let resolveRef = null;
|
|
6808
|
+
let settled = false;
|
|
6809
|
+
const settle = (r) => {
|
|
6810
|
+
if (settled) return;
|
|
6811
|
+
settled = true;
|
|
6812
|
+
resolveRef?.(r);
|
|
6813
|
+
};
|
|
6814
|
+
return {
|
|
6815
|
+
result: new Promise((resolve) => {
|
|
6816
|
+
resolveRef = resolve;
|
|
6817
|
+
cfg.read().then((stats) => {
|
|
6818
|
+
if (aborted) return;
|
|
6819
|
+
settle({
|
|
6820
|
+
ok: true,
|
|
6821
|
+
stats: freshOrNull(stats, cfg.now())
|
|
6822
|
+
});
|
|
6823
|
+
}).catch((err) => {
|
|
6824
|
+
if (aborted) return;
|
|
6825
|
+
settle({
|
|
6826
|
+
ok: false,
|
|
6827
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6828
|
+
});
|
|
6829
|
+
});
|
|
6830
|
+
}),
|
|
6831
|
+
abort: () => {
|
|
6832
|
+
aborted = true;
|
|
6833
|
+
settle({
|
|
6834
|
+
ok: false,
|
|
6835
|
+
aborted: true,
|
|
6836
|
+
error: "aborted"
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
6839
|
+
};
|
|
6840
|
+
}
|
|
6841
|
+
|
|
5972
6842
|
//#endregion
|
|
5973
6843
|
//#region src/chat/notification-preview.ts
|
|
5974
6844
|
const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
|
|
@@ -6019,6 +6889,101 @@ function createResponsePreview() {
|
|
|
6019
6889
|
};
|
|
6020
6890
|
}
|
|
6021
6891
|
|
|
6892
|
+
//#endregion
|
|
6893
|
+
//#region src/chat/tui/new-chat-splash.tsx
|
|
6894
|
+
/** Rows the splash art needs before it's worth painting: the 6-row mark, a
|
|
6895
|
+
* spacer, the two identity/hint lines, and a row of breathing room. Below
|
|
6896
|
+
* this only the identity + hint render (small panes must keep every
|
|
6897
|
+
* transcript row). */
|
|
6898
|
+
const MARK_MIN_ROWS = 10;
|
|
6899
|
+
/** Columns the full mark + wordmark lockup spans (~68), with margin; below
|
|
6900
|
+
* this the pinwheel mark renders alone instead of wrapping the wordmark. */
|
|
6901
|
+
const FULL_MARK_MIN_COLS = 70;
|
|
6902
|
+
/**
|
|
6903
|
+
* Brand splash filling an empty conversation's transcript area: the Skydive
|
|
6904
|
+
* mark centered above the agent's identity and the "say hello" hint, with
|
|
6905
|
+
* the composer live below. The identity line matters here because nothing
|
|
6906
|
+
* else on this screen names the agent — a bare `skydive chat` boots straight
|
|
6907
|
+
* into this view via the configured default. Rendered only while the
|
|
6908
|
+
* transcript is empty, so the first submission replaces it with the regular
|
|
6909
|
+
* conversation view. Degrades by available space: full lockup → mark only →
|
|
6910
|
+
* identity + hint only.
|
|
6911
|
+
*/
|
|
6912
|
+
function NewChatSplash({ agentName, agentTitle, width, height }) {
|
|
6913
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6914
|
+
style: {
|
|
6915
|
+
height,
|
|
6916
|
+
flexDirection: "column",
|
|
6917
|
+
justifyContent: "center",
|
|
6918
|
+
alignItems: "center"
|
|
6919
|
+
},
|
|
6920
|
+
children: [
|
|
6921
|
+
height >= MARK_MIN_ROWS ? /* @__PURE__ */ jsx("box", {
|
|
6922
|
+
style: {
|
|
6923
|
+
flexDirection: "column",
|
|
6924
|
+
alignItems: "center",
|
|
6925
|
+
marginBottom: 1
|
|
6926
|
+
},
|
|
6927
|
+
children: /* @__PURE__ */ jsx(SkydiveMark, { variant: width >= FULL_MARK_MIN_COLS ? "full" : "mark" })
|
|
6928
|
+
}) : null,
|
|
6929
|
+
/* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
6930
|
+
fg: theme.accent,
|
|
6931
|
+
children: agentName
|
|
6932
|
+
}), agentTitle ? /* @__PURE__ */ jsxs("span", {
|
|
6933
|
+
fg: theme.muted,
|
|
6934
|
+
children: [" · ", agentTitle]
|
|
6935
|
+
}) : null] }),
|
|
6936
|
+
/* @__PURE__ */ jsx("text", {
|
|
6937
|
+
fg: theme.dim,
|
|
6938
|
+
children: "say hello below"
|
|
6939
|
+
})
|
|
6940
|
+
]
|
|
6941
|
+
});
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6944
|
+
//#endregion
|
|
6945
|
+
//#region src/chat/tui/spinner-frames.ts
|
|
6946
|
+
/** Braille dot-cycle frames — the classic smooth terminal spinner. */
|
|
6947
|
+
const SPINNER_FRAMES = [
|
|
6948
|
+
"⠋",
|
|
6949
|
+
"⠙",
|
|
6950
|
+
"⠹",
|
|
6951
|
+
"⠸",
|
|
6952
|
+
"⠼",
|
|
6953
|
+
"⠴",
|
|
6954
|
+
"⠦",
|
|
6955
|
+
"⠧",
|
|
6956
|
+
"⠇",
|
|
6957
|
+
"⠏"
|
|
6958
|
+
];
|
|
6959
|
+
/** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
|
|
6960
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
6961
|
+
/** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
|
|
6962
|
+
function spinnerFrame(tick) {
|
|
6963
|
+
const n = SPINNER_FRAMES.length;
|
|
6964
|
+
return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
|
|
6965
|
+
}
|
|
6966
|
+
|
|
6967
|
+
//#endregion
|
|
6968
|
+
//#region src/chat/tui/spinner.tsx
|
|
6969
|
+
/**
|
|
6970
|
+
* An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
|
|
6971
|
+
* a self-owned interval that's cleared on unmount, so the animation runs only
|
|
6972
|
+
* while the component is mounted. Returns a <span>, so render it inside a
|
|
6973
|
+
* <text> (matching how the rest of the TUI composes inline glyphs).
|
|
6974
|
+
*/
|
|
6975
|
+
function Spinner({ color }) {
|
|
6976
|
+
const [tick, setTick] = useState(0);
|
|
6977
|
+
useEffect(() => {
|
|
6978
|
+
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
|
|
6979
|
+
return () => clearInterval(id);
|
|
6980
|
+
}, []);
|
|
6981
|
+
return /* @__PURE__ */ jsx("span", {
|
|
6982
|
+
fg: color,
|
|
6983
|
+
children: spinnerFrame(tick)
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6022
6987
|
//#endregion
|
|
6023
6988
|
//#region src/chat/tui/prompt-history.ts
|
|
6024
6989
|
/**
|
|
@@ -6752,6 +7717,13 @@ const slashCommands = [
|
|
|
6752
7717
|
description: "switch conversation (no id: pick from the list)",
|
|
6753
7718
|
action: { kind: "conversation" }
|
|
6754
7719
|
},
|
|
7720
|
+
{
|
|
7721
|
+
name: "search",
|
|
7722
|
+
aliases: ["find"],
|
|
7723
|
+
argsHint: "[query]",
|
|
7724
|
+
description: "search conversations by title, message, or participant",
|
|
7725
|
+
action: { kind: "search" }
|
|
7726
|
+
},
|
|
6755
7727
|
{
|
|
6756
7728
|
name: "model",
|
|
6757
7729
|
aliases: [],
|
|
@@ -6773,6 +7745,13 @@ const slashCommands = [
|
|
|
6773
7745
|
description: "show connection and session status",
|
|
6774
7746
|
action: { kind: "status" }
|
|
6775
7747
|
},
|
|
7748
|
+
{
|
|
7749
|
+
name: "computer",
|
|
7750
|
+
aliases: ["stats"],
|
|
7751
|
+
argsHint: null,
|
|
7752
|
+
description: "show the agent sandbox's cpu, memory, and disk",
|
|
7753
|
+
action: { kind: "computer" }
|
|
7754
|
+
},
|
|
6776
7755
|
{
|
|
6777
7756
|
name: "plan",
|
|
6778
7757
|
aliases: ["todos", "todo"],
|
|
@@ -6847,7 +7826,8 @@ const slashCommands = [
|
|
|
6847
7826
|
*
|
|
6848
7827
|
* An argument-less command only matches as the exact line — `/help me write
|
|
6849
7828
|
* a plan` stays an ordinary message rather than swallowing the user's text
|
|
6850
|
-
* into the help modal.
|
|
7829
|
+
* into the help modal. `sandbox`, `conversation`, `search` and `rename`
|
|
7830
|
+
* accept a rest (their argsHint is non-null).
|
|
6851
7831
|
*/
|
|
6852
7832
|
function matchSlashCommand(line) {
|
|
6853
7833
|
if (!line.startsWith("/")) return null;
|
|
@@ -9950,6 +10930,10 @@ function routeInput(raw) {
|
|
|
9950
10930
|
kind: "open-conversation",
|
|
9951
10931
|
ref: match.rest
|
|
9952
10932
|
} : { kind: "conversation-picker" };
|
|
10933
|
+
case "search": return {
|
|
10934
|
+
kind: "conversation-search",
|
|
10935
|
+
query: match.rest
|
|
10936
|
+
};
|
|
9953
10937
|
case "rename-conversation": return {
|
|
9954
10938
|
kind: "rename-conversation",
|
|
9955
10939
|
title: match.rest
|
|
@@ -9967,6 +10951,7 @@ function routeInput(raw) {
|
|
|
9967
10951
|
case "model-picker":
|
|
9968
10952
|
case "theme-picker":
|
|
9969
10953
|
case "status":
|
|
10954
|
+
case "computer":
|
|
9970
10955
|
case "share":
|
|
9971
10956
|
case "browser":
|
|
9972
10957
|
case "connect":
|
|
@@ -10034,47 +11019,6 @@ async function runPtySession(opts) {
|
|
|
10034
11019
|
}
|
|
10035
11020
|
}
|
|
10036
11021
|
|
|
10037
|
-
//#endregion
|
|
10038
|
-
//#region src/chat/tui/screens/conversation-channel.ts
|
|
10039
|
-
function isNewConversation(c) {
|
|
10040
|
-
return "kind" in c && c.kind === "new";
|
|
10041
|
-
}
|
|
10042
|
-
/**
|
|
10043
|
-
* The channel a conversation belongs to when it isn't the web channel, else
|
|
10044
|
-
* null. A brand-new conversation is always web, and the list route reports the
|
|
10045
|
-
* web channel as either `'web'` or (legacy) null — both mean "post from here".
|
|
10046
|
-
* Anything else (slack, email, imessage, webhook, cron) is owned by that
|
|
10047
|
-
* channel: the message has to go through it, so the TUI can't send here.
|
|
10048
|
-
* Mirrors the web app's `channel !== null && channel !== 'web'` gate.
|
|
10049
|
-
*/
|
|
10050
|
-
function nonWebChannelLabel(c) {
|
|
10051
|
-
if (isNewConversation(c)) return null;
|
|
10052
|
-
if (c.channel === null || c.channel === "web") return null;
|
|
10053
|
-
return c.channelLabel ?? c.channel;
|
|
10054
|
-
}
|
|
10055
|
-
/**
|
|
10056
|
-
* The list-row shape for a conversation the TUI just forked, so the chat
|
|
10057
|
-
* screen can reopen on the copy without waiting for a list refetch. A fork
|
|
10058
|
-
* carries none of the source's channel binding — that's what makes it
|
|
10059
|
-
* sendable from here — so it is always a web conversation.
|
|
10060
|
-
*/
|
|
10061
|
-
function forkedConversationRef({ agent, forked }) {
|
|
10062
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10063
|
-
return {
|
|
10064
|
-
id: forked.id,
|
|
10065
|
-
title: forked.title,
|
|
10066
|
-
createdAt: now,
|
|
10067
|
-
updatedAt: now,
|
|
10068
|
-
preview: null,
|
|
10069
|
-
channel: "web",
|
|
10070
|
-
channelLabel: null,
|
|
10071
|
-
agent: {
|
|
10072
|
-
id: agent.id,
|
|
10073
|
-
name: agent.name
|
|
10074
|
-
}
|
|
10075
|
-
};
|
|
10076
|
-
}
|
|
10077
|
-
|
|
10078
11022
|
//#endregion
|
|
10079
11023
|
//#region src/chat/tui/screens/chat.tsx
|
|
10080
11024
|
/**
|
|
@@ -10159,11 +11103,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10159
11103
|
const portalClient = useStore((s) => s.portalClient);
|
|
10160
11104
|
const setPortalPrompt = useStore((s) => s.setPortalPrompt);
|
|
10161
11105
|
const declinedRef = useRef(/* @__PURE__ */ new Set());
|
|
11106
|
+
const grantInFlightRef = useRef(/* @__PURE__ */ new Set());
|
|
10162
11107
|
const grantPrompt = portal.prompt && portal.prompt.agentId === agent.id ? portal.prompt : null;
|
|
10163
11108
|
const isShared = portal.status !== "off";
|
|
10164
11109
|
const initialConversationId = isNewConversation(conversation) ? null : conversation.id;
|
|
10165
11110
|
const agentHost = useAgentHost();
|
|
10166
11111
|
const nonWebChannel = nonWebChannelLabel(conversation);
|
|
11112
|
+
const agentRunConversation = isAgentRunConversation(conversation);
|
|
10167
11113
|
const [conversationId, setConversationId] = useState(initialConversationId);
|
|
10168
11114
|
const { compacting, compactConversation, onCompactionEvent } = useCompaction({
|
|
10169
11115
|
rest,
|
|
@@ -10172,6 +11118,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10172
11118
|
const [items, setItems] = useState([]);
|
|
10173
11119
|
const [todos, setTodos] = useState(null);
|
|
10174
11120
|
const [planHidden, setPlanHidden] = useState(false);
|
|
11121
|
+
const [subagentRows, setSubagentRows] = useState(0);
|
|
11122
|
+
const [anchoredSubagentIds, setAnchoredSubagentIds] = useState([]);
|
|
11123
|
+
const anchoredSubagentLiveRef = useRef({});
|
|
11124
|
+
const setAnchoredBatchLive = useCallback((id, live) => {
|
|
11125
|
+
anchoredSubagentLiveRef.current[id] = live;
|
|
11126
|
+
}, []);
|
|
10175
11127
|
const [scrolledUp, setScrolledUp] = useState(false);
|
|
10176
11128
|
const hadTodosRef = useRef(false);
|
|
10177
11129
|
useEffect(() => {
|
|
@@ -10214,14 +11166,18 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10214
11166
|
const forkingRef = useRef(false);
|
|
10215
11167
|
const [credPrompt, setCredPrompt] = useState(null);
|
|
10216
11168
|
const credPromptOpen = credPrompt !== null;
|
|
11169
|
+
const [computePrompt, setComputePrompt] = useState(null);
|
|
11170
|
+
const computePromptOpen = computePrompt !== null;
|
|
10217
11171
|
const [menuDismissed, setMenuDismissed] = useState(false);
|
|
10218
11172
|
const [menuHighlight, setMenuHighlight] = useState(0);
|
|
10219
|
-
const commandQuery = grantPrompt || credPrompt ? null : detectCommandTrigger(input);
|
|
11173
|
+
const commandQuery = grantPrompt || credPrompt || computePrompt ? null : detectCommandTrigger(input);
|
|
10220
11174
|
const menuCommands = commandQuery === null ? [] : filterSlashCommands(commandQuery);
|
|
10221
11175
|
const menuOpen = commandQuery !== null && !menuDismissed;
|
|
10222
11176
|
const menuHighlightClamped = Math.min(menuHighlight, Math.max(0, menuCommands.length - 1));
|
|
10223
11177
|
const runRef = useRef(run);
|
|
10224
11178
|
runRef.current = run;
|
|
11179
|
+
const conversationIdRef = useRef(conversationId);
|
|
11180
|
+
conversationIdRef.current = conversationId;
|
|
10225
11181
|
const itemsRef = useRef(items);
|
|
10226
11182
|
itemsRef.current = items;
|
|
10227
11183
|
const attachmentsRef = useRef(attachments);
|
|
@@ -10232,7 +11188,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10232
11188
|
credPromptRef.current = credPrompt;
|
|
10233
11189
|
const seenRunsRef = useRef(/* @__PURE__ */ new Set());
|
|
10234
11190
|
const shellSessionRef = useRef(null);
|
|
10235
|
-
const hostState = grantPrompt || credPrompt ? "blocked" : run.kind === "idle" ? "idle" : "working";
|
|
11191
|
+
const hostState = grantPrompt || credPrompt || computePrompt ? "blocked" : run.kind === "idle" ? "idle" : "working";
|
|
10236
11192
|
useEffect(() => {
|
|
10237
11193
|
agentHost.transition({ state: hostState });
|
|
10238
11194
|
}, [agentHost, hostState]);
|
|
@@ -10271,13 +11227,16 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10271
11227
|
});
|
|
10272
11228
|
setScrolledUp(false);
|
|
10273
11229
|
}, []);
|
|
11230
|
+
const scrolledUpRef = useRef(false);
|
|
10274
11231
|
useEffect(() => {
|
|
10275
11232
|
const id = setInterval(() => {
|
|
10276
11233
|
const box = scrollRef.current;
|
|
10277
11234
|
if (!box) return;
|
|
10278
11235
|
const viewport = box.viewport.height;
|
|
10279
|
-
const
|
|
10280
|
-
|
|
11236
|
+
const next = !(box.scrollTop + viewport >= box.scrollHeight - 1);
|
|
11237
|
+
if (scrolledUpRef.current === next) return;
|
|
11238
|
+
scrolledUpRef.current = next;
|
|
11239
|
+
setScrolledUp(next);
|
|
10281
11240
|
}, 150);
|
|
10282
11241
|
return () => clearInterval(id);
|
|
10283
11242
|
}, []);
|
|
@@ -10285,11 +11244,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10285
11244
|
const reviewBelowBoxRef = useRef(null);
|
|
10286
11245
|
const paneDragRef = useRef(null);
|
|
10287
11246
|
const composerBoxHeight = composerRows + 2;
|
|
10288
|
-
const pendingVisible = !grantPrompt && !credPrompt && !nonWebChannel && attachments.length > 0;
|
|
11247
|
+
const pendingVisible = !grantPrompt && !credPrompt && !computePrompt && !nonWebChannel && attachments.length > 0;
|
|
10289
11248
|
const pendingRows = pendingVisible ? attachments.length + 2 : 0;
|
|
10290
11249
|
const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
|
|
10291
11250
|
const nonWebNoticeHeight = 5;
|
|
10292
|
-
const
|
|
11251
|
+
const computePromptHeight = 4;
|
|
11252
|
+
const bottomBoxHeight = credPrompt ? credPromptHeight : computePrompt ? computePromptHeight : nonWebChannel ? nonWebNoticeHeight : composerBoxHeight;
|
|
10293
11253
|
const bodyWidth = Math.max(1, width - 2);
|
|
10294
11254
|
const bodyHeight = Math.max(1, height - 3);
|
|
10295
11255
|
const reviewPlacement = filePanePlacement({
|
|
@@ -10306,9 +11266,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10306
11266
|
const menuRows = menuOpen ? completionMenuHeight(menuCommands.length) : 0;
|
|
10307
11267
|
const todoCardVisible = !credPrompt && !planHidden && !!todos && todos.length > 0;
|
|
10308
11268
|
const todoRows = todoCardVisible ? todoCardHeight(todos) : 0;
|
|
11269
|
+
const subagentItems = useMemo(() => {
|
|
11270
|
+
const ids = new Set(anchoredSubagentIds);
|
|
11271
|
+
return items.filter((item) => item.kind === "tool" && item.toolName === "subagent" && ids.has(item.id));
|
|
11272
|
+
}, [items, anchoredSubagentIds]);
|
|
10309
11273
|
const jumpHintVisible = scrolledUp && !credPrompt;
|
|
10310
11274
|
const jumpHintRows = jumpHintVisible ? 1 : 0;
|
|
10311
|
-
const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - jumpHintRows - todoRows);
|
|
11275
|
+
const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - jumpHintRows - subagentRows - todoRows);
|
|
10312
11276
|
useEffect(() => {
|
|
10313
11277
|
if (reviewOpen && reviewFocused) {
|
|
10314
11278
|
const composer = composerRef.current;
|
|
@@ -10342,7 +11306,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10342
11306
|
reviewOpen,
|
|
10343
11307
|
reviewFocused,
|
|
10344
11308
|
reviewPlacement,
|
|
10345
|
-
credPromptOpen
|
|
11309
|
+
credPromptOpen,
|
|
11310
|
+
computePromptOpen
|
|
10346
11311
|
]);
|
|
10347
11312
|
useEffect(() => {
|
|
10348
11313
|
if (!rest || !initialConversationId) return;
|
|
@@ -10358,10 +11323,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10358
11323
|
text: recapText
|
|
10359
11324
|
});
|
|
10360
11325
|
setItems(loaded);
|
|
11326
|
+
const toolUrlText = loaded.map((item) => item.kind === "tool" && item.output?.kind === "ok" ? stringifyToolOutput(item.output.value) : "").join("\n");
|
|
10361
11327
|
for (const item of loaded) {
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
11328
|
+
if (item.kind !== "assistant-text") continue;
|
|
11329
|
+
for (const pr of extractPullRequests(item.text, {
|
|
11330
|
+
requireCompleteNumber: false,
|
|
11331
|
+
urlOnlyText: toolUrlText
|
|
11332
|
+
})) {
|
|
10365
11333
|
if (reportedPrsRef.current.has(pr.key)) continue;
|
|
10366
11334
|
reportedPrsRef.current.set(pr.key, pr);
|
|
10367
11335
|
setPrLinks((prev) => [...prev, pr]);
|
|
@@ -10388,6 +11356,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10388
11356
|
initialConversationId,
|
|
10389
11357
|
agentHost
|
|
10390
11358
|
]);
|
|
11359
|
+
const markRead = useCallback(() => {
|
|
11360
|
+
const id = conversationIdRef.current;
|
|
11361
|
+
if (!rest || !id) return;
|
|
11362
|
+
rest.markConversationRead({ conversationId: id }).catch(() => {});
|
|
11363
|
+
}, [rest]);
|
|
11364
|
+
useEffect(() => {
|
|
11365
|
+
if (!conversationId) return;
|
|
11366
|
+
markRead();
|
|
11367
|
+
}, [conversationId, markRead]);
|
|
10391
11368
|
const attachToRun = useCallback((runId, runAgentName, runStartedAtMs) => {
|
|
10392
11369
|
if (!rest) return;
|
|
10393
11370
|
seenRunsRef.current.add(runId);
|
|
@@ -10400,6 +11377,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10400
11377
|
agentHost.addPullRequest(pr);
|
|
10401
11378
|
});
|
|
10402
11379
|
const streamAgentName = runAgentName ?? agent.name;
|
|
11380
|
+
let sawErrorChunk = false;
|
|
10403
11381
|
setRun((prev) => ({
|
|
10404
11382
|
kind: "streaming",
|
|
10405
11383
|
runId,
|
|
@@ -10411,8 +11389,21 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10411
11389
|
signal: abort.signal,
|
|
10412
11390
|
onEvent: (event) => {
|
|
10413
11391
|
if (event.kind === "chunk") {
|
|
11392
|
+
if (event.chunk.type === "error") sawErrorChunk = true;
|
|
10414
11393
|
responsePreview.addChunk(event.chunk);
|
|
10415
11394
|
prScanner.addChunk(event.chunk);
|
|
11395
|
+
if ((event.chunk.type === "tool-input-start" || event.chunk.type === "tool-input-available") && event.chunk.toolName === "subagent" && typeof event.chunk.toolCallId === "string") {
|
|
11396
|
+
const id = event.chunk.toolCallId;
|
|
11397
|
+
setAnchoredSubagentIds((current) => {
|
|
11398
|
+
const next = nextAnchoredSubagentIds({
|
|
11399
|
+
current,
|
|
11400
|
+
nextId: id,
|
|
11401
|
+
liveById: anchoredSubagentLiveRef.current
|
|
11402
|
+
});
|
|
11403
|
+
if (next.length === 1 && next[0] === id) anchoredSubagentLiveRef.current = { [id]: true };
|
|
11404
|
+
return next;
|
|
11405
|
+
});
|
|
11406
|
+
}
|
|
10416
11407
|
if (event.chunk.type === "data-anyone-todos-updated") {
|
|
10417
11408
|
const next = todosFromPayload(event.chunk.data);
|
|
10418
11409
|
if (next) setTodos(next.length > 0 ? next : null);
|
|
@@ -10428,7 +11419,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10428
11419
|
id: crypto.randomUUID(),
|
|
10429
11420
|
text: billingOutcome.message
|
|
10430
11421
|
}]);
|
|
10431
|
-
else if (event.status !== "ok" && errorText) setItems((prev) => [...prev, {
|
|
11422
|
+
else if (event.status !== "ok" && errorText && !sawErrorChunk) setItems((prev) => [...prev, {
|
|
10432
11423
|
kind: "error",
|
|
10433
11424
|
id: crypto.randomUUID(),
|
|
10434
11425
|
text: errorText
|
|
@@ -10436,9 +11427,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10436
11427
|
setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
|
|
10437
11428
|
kind: "user",
|
|
10438
11429
|
id: m.id,
|
|
10439
|
-
text: m.text
|
|
11430
|
+
text: m.text,
|
|
11431
|
+
attachments: m.attachments
|
|
10440
11432
|
} : m));
|
|
10441
11433
|
setRun({ kind: "idle" });
|
|
11434
|
+
markRead();
|
|
10442
11435
|
agentHost.transition({
|
|
10443
11436
|
state: "idle",
|
|
10444
11437
|
notification: {
|
|
@@ -10460,7 +11453,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10460
11453
|
}, [
|
|
10461
11454
|
rest,
|
|
10462
11455
|
agent.name,
|
|
10463
|
-
agentHost
|
|
11456
|
+
agentHost,
|
|
11457
|
+
markRead
|
|
10464
11458
|
]);
|
|
10465
11459
|
const multiAgent = useMemo(() => conversationAgentNames(items, agent.name), [items, agent.name]).size > 1;
|
|
10466
11460
|
const transcriptWindow = useMemo(() => windowItems(items), [items]);
|
|
@@ -10513,7 +11507,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10513
11507
|
kind: "user",
|
|
10514
11508
|
id: optimisticId,
|
|
10515
11509
|
text: trimmed,
|
|
10516
|
-
|
|
11510
|
+
attachments: stagedNames
|
|
10517
11511
|
}]);
|
|
10518
11512
|
const wasStreaming = runRef.current.kind === "streaming";
|
|
10519
11513
|
if (!wasStreaming) setRun({
|
|
@@ -10562,7 +11556,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10562
11556
|
if (directiveId) setItems((prev) => prev.map((m) => m.id === optimisticId ? {
|
|
10563
11557
|
kind: "pending-steer",
|
|
10564
11558
|
id: directiveId,
|
|
10565
|
-
text: trimmed
|
|
11559
|
+
text: trimmed,
|
|
11560
|
+
attachments: m.kind === "user" || m.kind === "pending-steer" ? m.attachments : []
|
|
10566
11561
|
} : m));
|
|
10567
11562
|
const current = runRef.current;
|
|
10568
11563
|
if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId, result.agentName ?? null);
|
|
@@ -10609,12 +11604,14 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10609
11604
|
});
|
|
10610
11605
|
(async () => {
|
|
10611
11606
|
if (autoGrantMachine && portalClient && portalStatus === "connected" && !portalClient.isGranted(agent.id)) try {
|
|
10612
|
-
|
|
11607
|
+
grantInFlightRef.current.add(agent.id);
|
|
11608
|
+
await portalClient.grantAgent(agent.id, null);
|
|
10613
11609
|
useStore.getState().showToast({
|
|
10614
|
-
message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent ${agent.name}`,
|
|
11610
|
+
message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent "${agent.name}"`,
|
|
10615
11611
|
durationMs: 1e4
|
|
10616
11612
|
});
|
|
10617
11613
|
} catch (err) {
|
|
11614
|
+
grantInFlightRef.current.delete(agent.id);
|
|
10618
11615
|
useStore.getState().showToast({
|
|
10619
11616
|
message: `Couldn't grant machine access automatically (${errorMessage(err)}). The agent will ask in chat instead.`,
|
|
10620
11617
|
variant: "warning"
|
|
@@ -10669,6 +11666,14 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10669
11666
|
});
|
|
10670
11667
|
return;
|
|
10671
11668
|
}
|
|
11669
|
+
if (action.kind === "decide_compute") {
|
|
11670
|
+
setComputePrompt({
|
|
11671
|
+
cardId: item.id,
|
|
11672
|
+
requestId: action.requestId,
|
|
11673
|
+
summary: item.card.subtitle
|
|
11674
|
+
});
|
|
11675
|
+
return;
|
|
11676
|
+
}
|
|
10672
11677
|
updateCard(item.id, {
|
|
10673
11678
|
status: "busy",
|
|
10674
11679
|
detail: null
|
|
@@ -10678,9 +11683,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10678
11683
|
if (!portalClient) throw new Error("portal is unavailable in this session");
|
|
10679
11684
|
const justEnabled = portal.status === "off";
|
|
10680
11685
|
if (justEnabled) portalClient.enable();
|
|
11686
|
+
grantInFlightRef.current.add(action.agentId);
|
|
11687
|
+
if (useStore.getState().portal.prompt?.agentId === action.agentId) setPortalPrompt(null);
|
|
10681
11688
|
try {
|
|
10682
|
-
await portalClient.grantAgent(action.agentId);
|
|
11689
|
+
await portalClient.grantAgent(action.agentId, action.conversationId);
|
|
10683
11690
|
} catch (grantErr) {
|
|
11691
|
+
grantInFlightRef.current.delete(action.agentId);
|
|
10684
11692
|
if (justEnabled) portalClient.disable();
|
|
10685
11693
|
throw grantErr;
|
|
10686
11694
|
}
|
|
@@ -10741,8 +11749,45 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10741
11749
|
appUrl,
|
|
10742
11750
|
updateCard,
|
|
10743
11751
|
portalClient,
|
|
10744
|
-
portal.status
|
|
11752
|
+
portal.status,
|
|
11753
|
+
setPortalPrompt
|
|
10745
11754
|
]);
|
|
11755
|
+
const decideCompute = useCallback((prompt, decision) => {
|
|
11756
|
+
setComputePrompt(null);
|
|
11757
|
+
if (!rest) return;
|
|
11758
|
+
updateCard(prompt.cardId, {
|
|
11759
|
+
status: "busy",
|
|
11760
|
+
detail: null
|
|
11761
|
+
});
|
|
11762
|
+
const settleIfStillDeciding = (patch) => {
|
|
11763
|
+
setItems((prev) => prev.map((m) => m.kind === "card" && m.id === prompt.cardId && m.status === "busy" && m.card.settled === null ? {
|
|
11764
|
+
...m,
|
|
11765
|
+
...patch
|
|
11766
|
+
} : m));
|
|
11767
|
+
};
|
|
11768
|
+
(async () => {
|
|
11769
|
+
try {
|
|
11770
|
+
const { status } = await rest.decideComputeRequest({
|
|
11771
|
+
requestId: prompt.requestId,
|
|
11772
|
+
decision
|
|
11773
|
+
});
|
|
11774
|
+
settleIfStillDeciding({
|
|
11775
|
+
status: "done",
|
|
11776
|
+
detail: computeSettledLabel(status) ?? status
|
|
11777
|
+
});
|
|
11778
|
+
} catch (err) {
|
|
11779
|
+
settleIfStillDeciding({
|
|
11780
|
+
status: "error",
|
|
11781
|
+
detail: cardActionErrorMessage(err)
|
|
11782
|
+
});
|
|
11783
|
+
}
|
|
11784
|
+
})();
|
|
11785
|
+
}, [rest, updateCard]);
|
|
11786
|
+
useEffect(() => {
|
|
11787
|
+
if (!computePrompt) return;
|
|
11788
|
+
const target = items.find((m) => m.kind === "card" && m.id === computePrompt.cardId);
|
|
11789
|
+
if (!target || target.card.settled !== null || target.status === "done") setComputePrompt(null);
|
|
11790
|
+
}, [computePrompt, items]);
|
|
10746
11791
|
const promptForCards = useCallback(() => {
|
|
10747
11792
|
const targets = itemsRef.current.filter(isActionableCard);
|
|
10748
11793
|
const [only] = targets;
|
|
@@ -10905,6 +11950,16 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10905
11950
|
sessionToken,
|
|
10906
11951
|
agent.id
|
|
10907
11952
|
]);
|
|
11953
|
+
const runComputerStats = useCallback(() => {
|
|
11954
|
+
if (!appUrl || !sessionToken) return;
|
|
11955
|
+
setItems((prev) => [...prev, {
|
|
11956
|
+
kind: "computer",
|
|
11957
|
+
id: crypto.randomUUID(),
|
|
11958
|
+
state: "loading",
|
|
11959
|
+
stats: null,
|
|
11960
|
+
error: null
|
|
11961
|
+
}]);
|
|
11962
|
+
}, [appUrl, sessionToken]);
|
|
10908
11963
|
const runSandboxPty = useCallback(async () => {
|
|
10909
11964
|
if (!appUrl || !sessionToken) return;
|
|
10910
11965
|
const result = await runPtySession({
|
|
@@ -10999,7 +12054,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10999
12054
|
stageAttachment(image);
|
|
11000
12055
|
}, [stageAttachment]);
|
|
11001
12056
|
usePaste((event) => {
|
|
11002
|
-
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt || nonWebChannel) return;
|
|
12057
|
+
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt || computePromptOpen || nonWebChannel) return;
|
|
11003
12058
|
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
11004
12059
|
const route = routePaste({
|
|
11005
12060
|
kind: event.metadata?.kind,
|
|
@@ -11053,6 +12108,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11053
12108
|
themePickerOpen,
|
|
11054
12109
|
helpOpen,
|
|
11055
12110
|
credPromptOpen,
|
|
12111
|
+
computePromptOpen,
|
|
11056
12112
|
applyComposerText
|
|
11057
12113
|
]);
|
|
11058
12114
|
const cancelRun = useCallback(async () => {
|
|
@@ -11066,7 +12122,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11066
12122
|
setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
|
|
11067
12123
|
kind: "user",
|
|
11068
12124
|
id: m.id,
|
|
11069
|
-
text: m.text
|
|
12125
|
+
text: m.text,
|
|
12126
|
+
attachments: m.attachments
|
|
11070
12127
|
} : m));
|
|
11071
12128
|
useStore.getState().showToast({ message: "run cancelled" });
|
|
11072
12129
|
}, [rest]);
|
|
@@ -11134,7 +12191,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11134
12191
|
goTo
|
|
11135
12192
|
]);
|
|
11136
12193
|
const archiveConversation = useCallback(() => {
|
|
11137
|
-
if (grantPrompt || credPrompt) return;
|
|
12194
|
+
if (grantPrompt || credPrompt || computePrompt) return;
|
|
12195
|
+
if (agentRunConversation) {
|
|
12196
|
+
useStore.getState().showToast({ message: "agent runs can't be archived — ctrl+d in the picker deletes" });
|
|
12197
|
+
return;
|
|
12198
|
+
}
|
|
11138
12199
|
if (runRef.current.kind !== "idle") {
|
|
11139
12200
|
useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
|
|
11140
12201
|
return;
|
|
@@ -11154,7 +12215,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11154
12215
|
});
|
|
11155
12216
|
goTo({
|
|
11156
12217
|
kind: "conversation-picker",
|
|
11157
|
-
agent
|
|
12218
|
+
agent,
|
|
12219
|
+
initialQuery: ""
|
|
11158
12220
|
});
|
|
11159
12221
|
}).catch((err) => useStore.getState().showToast({
|
|
11160
12222
|
variant: "error",
|
|
@@ -11163,6 +12225,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11163
12225
|
}, [
|
|
11164
12226
|
grantPrompt,
|
|
11165
12227
|
credPrompt,
|
|
12228
|
+
computePrompt,
|
|
12229
|
+
agentRunConversation,
|
|
11166
12230
|
conversationId,
|
|
11167
12231
|
rest,
|
|
11168
12232
|
goTo,
|
|
@@ -11244,7 +12308,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11244
12308
|
rest,
|
|
11245
12309
|
agentSelector: null,
|
|
11246
12310
|
conversationId: target,
|
|
11247
|
-
newConversation: false
|
|
12311
|
+
newConversation: false,
|
|
12312
|
+
defaultAgentSelector: null,
|
|
12313
|
+
forceOnboarding: false
|
|
11248
12314
|
}));
|
|
11249
12315
|
} catch (err) {
|
|
11250
12316
|
setItems((prev) => [...prev, {
|
|
@@ -11283,19 +12349,24 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11283
12349
|
if (!portalClient) return;
|
|
11284
12350
|
const agentName = agent.name;
|
|
11285
12351
|
setPortalPrompt(null);
|
|
11286
|
-
|
|
12352
|
+
grantInFlightRef.current.add(agent.id);
|
|
12353
|
+
portalClient.grantAgent(agent.id, conversationId).then(() => useStore.getState().showToast({
|
|
11287
12354
|
variant: "success",
|
|
11288
12355
|
message: `shared this machine with ${agentName}`
|
|
11289
|
-
})).catch((err) =>
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
12356
|
+
})).catch((err) => {
|
|
12357
|
+
grantInFlightRef.current.delete(agent.id);
|
|
12358
|
+
setItems((prev) => [...prev, {
|
|
12359
|
+
kind: "error",
|
|
12360
|
+
id: crypto.randomUUID(),
|
|
12361
|
+
text: `couldn't share machine: ${errorDetail(err)}`
|
|
12362
|
+
}]);
|
|
12363
|
+
});
|
|
11294
12364
|
}, [
|
|
11295
12365
|
portalClient,
|
|
11296
12366
|
agent.id,
|
|
11297
12367
|
agent.name,
|
|
11298
|
-
setPortalPrompt
|
|
12368
|
+
setPortalPrompt,
|
|
12369
|
+
conversationId
|
|
11299
12370
|
]);
|
|
11300
12371
|
const declineGrant = useCallback(() => {
|
|
11301
12372
|
declinedRef.current.add(agent.id);
|
|
@@ -11408,7 +12479,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11408
12479
|
case "conversation-picker":
|
|
11409
12480
|
leaveForScreen({
|
|
11410
12481
|
kind: "conversation-picker",
|
|
11411
|
-
agent
|
|
12482
|
+
agent,
|
|
12483
|
+
initialQuery: ""
|
|
12484
|
+
});
|
|
12485
|
+
break;
|
|
12486
|
+
case "conversation-search":
|
|
12487
|
+
leaveForScreen({
|
|
12488
|
+
kind: "conversation-picker",
|
|
12489
|
+
agent,
|
|
12490
|
+
initialQuery: routed.query
|
|
11412
12491
|
});
|
|
11413
12492
|
break;
|
|
11414
12493
|
case "open-conversation":
|
|
@@ -11438,6 +12517,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11438
12517
|
})
|
|
11439
12518
|
}]);
|
|
11440
12519
|
break;
|
|
12520
|
+
case "computer":
|
|
12521
|
+
runComputerStats();
|
|
12522
|
+
break;
|
|
11441
12523
|
case "plan": {
|
|
11442
12524
|
if (!(!!todos && todos.length > 0)) {
|
|
11443
12525
|
useStore.getState().showToast({ message: "no plan to show yet" });
|
|
@@ -11490,6 +12572,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11490
12572
|
history,
|
|
11491
12573
|
runLocalShell,
|
|
11492
12574
|
runSandboxExec,
|
|
12575
|
+
runComputerStats,
|
|
11493
12576
|
runSandboxPty,
|
|
11494
12577
|
archiveConversation,
|
|
11495
12578
|
renameConversation,
|
|
@@ -11535,6 +12618,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11535
12618
|
if (portal.grantedAgentIds.includes(agent.id)) return;
|
|
11536
12619
|
if (portal.declinedAgentIds.includes(agent.id)) return;
|
|
11537
12620
|
if (declinedRef.current.has(agent.id)) return;
|
|
12621
|
+
if (grantInFlightRef.current.has(agent.id)) return;
|
|
11538
12622
|
if (portal.prompt?.agentId === agent.id) return;
|
|
11539
12623
|
setPortalPrompt({
|
|
11540
12624
|
agentId: agent.id,
|
|
@@ -11558,6 +12642,14 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11558
12642
|
setPortalPrompt,
|
|
11559
12643
|
agentHost
|
|
11560
12644
|
]);
|
|
12645
|
+
useEffect(() => {
|
|
12646
|
+
for (const agentId of portal.grantedAgentIds) grantInFlightRef.current.delete(agentId);
|
|
12647
|
+
if (portal.prompt && portal.grantedAgentIds.includes(portal.prompt.agentId)) setPortalPrompt(null);
|
|
12648
|
+
}, [
|
|
12649
|
+
portal.grantedAgentIds,
|
|
12650
|
+
portal.prompt,
|
|
12651
|
+
setPortalPrompt
|
|
12652
|
+
]);
|
|
11561
12653
|
useKeyboard((key) => {
|
|
11562
12654
|
if (reviewOpen) {
|
|
11563
12655
|
if (key.name === "g" && key.ctrl) {
|
|
@@ -11581,6 +12673,20 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11581
12673
|
return;
|
|
11582
12674
|
}
|
|
11583
12675
|
}
|
|
12676
|
+
if (computePrompt) {
|
|
12677
|
+
if (key.name === "y") {
|
|
12678
|
+
decideCompute(computePrompt, "approve");
|
|
12679
|
+
return;
|
|
12680
|
+
}
|
|
12681
|
+
if (key.name === "n") {
|
|
12682
|
+
decideCompute(computePrompt, "deny");
|
|
12683
|
+
return;
|
|
12684
|
+
}
|
|
12685
|
+
if (key.name === "escape") {
|
|
12686
|
+
setComputePrompt(null);
|
|
12687
|
+
return;
|
|
12688
|
+
}
|
|
12689
|
+
}
|
|
11584
12690
|
if (credPrompt) {
|
|
11585
12691
|
if (key.name === "escape") {
|
|
11586
12692
|
if (!credPrompt.submitting) setCredPrompt(null);
|
|
@@ -11736,21 +12842,46 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11736
12842
|
useStore.getState().showToast({ message: `${agent.name} keeps working; reopen to reattach` });
|
|
11737
12843
|
goTo({
|
|
11738
12844
|
kind: "conversation-picker",
|
|
11739
|
-
agent
|
|
12845
|
+
agent,
|
|
12846
|
+
initialQuery: ""
|
|
11740
12847
|
});
|
|
11741
12848
|
return;
|
|
11742
12849
|
}
|
|
11743
12850
|
if (runRef.current.kind === "idle") goTo({
|
|
11744
12851
|
kind: "conversation-picker",
|
|
11745
|
-
agent
|
|
12852
|
+
agent,
|
|
12853
|
+
initialQuery: ""
|
|
11746
12854
|
});
|
|
11747
12855
|
}
|
|
11748
12856
|
});
|
|
11749
12857
|
const hasPendingSteer = items.some((m) => m.kind === "pending-steer");
|
|
12858
|
+
const latestComputerId = useMemo(() => {
|
|
12859
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
12860
|
+
const item = items[i];
|
|
12861
|
+
if (item && item.kind === "computer") return item.id;
|
|
12862
|
+
}
|
|
12863
|
+
return null;
|
|
12864
|
+
}, [items]);
|
|
12865
|
+
const boundCollectSandboxStats = useMemo(() => {
|
|
12866
|
+
if (!rest) return null;
|
|
12867
|
+
return () => collectSandboxStats({
|
|
12868
|
+
read: () => rest.computeLiveStats(),
|
|
12869
|
+
now: () => Date.now()
|
|
12870
|
+
});
|
|
12871
|
+
}, [rest]);
|
|
11750
12872
|
const conversationContext = useMemo(() => ({
|
|
11751
12873
|
rest,
|
|
11752
|
-
conversationId
|
|
11753
|
-
|
|
12874
|
+
conversationId,
|
|
12875
|
+
collectSandboxStats: boundCollectSandboxStats,
|
|
12876
|
+
latestComputerId,
|
|
12877
|
+
scrolledUp
|
|
12878
|
+
}), [
|
|
12879
|
+
rest,
|
|
12880
|
+
conversationId,
|
|
12881
|
+
boundCollectSandboxStats,
|
|
12882
|
+
latestComputerId,
|
|
12883
|
+
scrolledUp
|
|
12884
|
+
]);
|
|
11754
12885
|
const actionableCards = useMemo(() => items.filter(isActionableCard), [items]);
|
|
11755
12886
|
const statusText = useMemo(() => formatChatFooterStatus({
|
|
11756
12887
|
runKind: run.kind,
|
|
@@ -11899,13 +13030,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11899
13030
|
children: [!historyLoaded ? /* @__PURE__ */ jsx("text", {
|
|
11900
13031
|
fg: theme.muted,
|
|
11901
13032
|
children: "loading history…"
|
|
11902
|
-
}) : items.length === 0 ? /* @__PURE__ */
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
" below."
|
|
11908
|
-
]
|
|
13033
|
+
}) : items.length === 0 ? /* @__PURE__ */ jsx(NewChatSplash, {
|
|
13034
|
+
agentName: agent.name,
|
|
13035
|
+
agentTitle: agent.title ?? null,
|
|
13036
|
+
width: reviewView && reviewPlacement === "side" ? Math.max(1, bodyWidth - reviewWidth - 1) : bodyWidth,
|
|
13037
|
+
height: Math.max(1, scrollHeight - 1)
|
|
11909
13038
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [transcriptWindow.hiddenCount > 0 ? /* @__PURE__ */ jsx("box", {
|
|
11910
13039
|
style: {
|
|
11911
13040
|
paddingLeft: 1,
|
|
@@ -11948,6 +13077,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11948
13077
|
children: [" ", "end"]
|
|
11949
13078
|
})]
|
|
11950
13079
|
}) : null,
|
|
13080
|
+
/* @__PURE__ */ jsx(SubagentCardView, {
|
|
13081
|
+
items: subagentItems,
|
|
13082
|
+
isActive: run.kind !== "idle",
|
|
13083
|
+
onHeightChange: setSubagentRows,
|
|
13084
|
+
onBatchLiveChange: setAnchoredBatchLive
|
|
13085
|
+
}),
|
|
11951
13086
|
todoCardVisible && todos ? /* @__PURE__ */ jsx(TodoCardView, {
|
|
11952
13087
|
todos,
|
|
11953
13088
|
isActive: run.kind !== "idle"
|
|
@@ -12040,6 +13175,46 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12040
13175
|
children: "n"
|
|
12041
13176
|
})
|
|
12042
13177
|
]
|
|
13178
|
+
}) : computePrompt ? /* @__PURE__ */ jsxs("box", {
|
|
13179
|
+
style: {
|
|
13180
|
+
border: true,
|
|
13181
|
+
borderColor: theme.warning,
|
|
13182
|
+
paddingLeft: 1,
|
|
13183
|
+
paddingRight: 1,
|
|
13184
|
+
height: computePromptHeight,
|
|
13185
|
+
flexShrink: 0,
|
|
13186
|
+
marginTop: 1,
|
|
13187
|
+
flexDirection: "column"
|
|
13188
|
+
},
|
|
13189
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
13190
|
+
fg: theme.warning,
|
|
13191
|
+
children: computePrompt.summary ?? "more compute requested"
|
|
13192
|
+
}), /* @__PURE__ */ jsxs("text", { children: [
|
|
13193
|
+
/* @__PURE__ */ jsx("span", {
|
|
13194
|
+
fg: theme.success,
|
|
13195
|
+
children: "y"
|
|
13196
|
+
}),
|
|
13197
|
+
/* @__PURE__ */ jsx("span", {
|
|
13198
|
+
fg: theme.muted,
|
|
13199
|
+
children: " upgrade / "
|
|
13200
|
+
}),
|
|
13201
|
+
/* @__PURE__ */ jsx("span", {
|
|
13202
|
+
fg: theme.error,
|
|
13203
|
+
children: "n"
|
|
13204
|
+
}),
|
|
13205
|
+
/* @__PURE__ */ jsx("span", {
|
|
13206
|
+
fg: theme.muted,
|
|
13207
|
+
children: " keep current / "
|
|
13208
|
+
}),
|
|
13209
|
+
/* @__PURE__ */ jsx("span", {
|
|
13210
|
+
fg: theme.dim,
|
|
13211
|
+
children: "esc"
|
|
13212
|
+
}),
|
|
13213
|
+
/* @__PURE__ */ jsx("span", {
|
|
13214
|
+
fg: theme.muted,
|
|
13215
|
+
children: " later"
|
|
13216
|
+
})
|
|
13217
|
+
] })]
|
|
12043
13218
|
}) : nonWebChannel ? /* @__PURE__ */ jsxs("box", {
|
|
12044
13219
|
style: {
|
|
12045
13220
|
border: true,
|
|
@@ -12336,13 +13511,16 @@ function ToastView() {
|
|
|
12336
13511
|
return () => clearTimeout(handle);
|
|
12337
13512
|
}, [toast, dismissToast]);
|
|
12338
13513
|
if (!toast) return null;
|
|
13514
|
+
const cap = Math.min(60, Math.max(20, width - 6));
|
|
13515
|
+
const chrome = 4;
|
|
13516
|
+
const contentWidth = Math.max(...toast.message.split("\n").map((line) => Bun.stringWidth(line)));
|
|
12339
13517
|
return /* @__PURE__ */ jsx("box", {
|
|
12340
13518
|
style: {
|
|
12341
13519
|
position: "absolute",
|
|
12342
13520
|
top: 1,
|
|
12343
13521
|
right: 1,
|
|
12344
13522
|
zIndex: 100,
|
|
12345
|
-
|
|
13523
|
+
width: Math.min(cap, contentWidth + chrome),
|
|
12346
13524
|
border: true,
|
|
12347
13525
|
borderColor: toastColor(toast.variant),
|
|
12348
13526
|
backgroundColor: theme.surface ?? "#1a1a1a",
|
|
@@ -12359,9 +13537,12 @@ function ToastView() {
|
|
|
12359
13537
|
|
|
12360
13538
|
//#endregion
|
|
12361
13539
|
//#region src/chat/tui/app.tsx
|
|
12362
|
-
function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation }) {
|
|
13540
|
+
function App({ appUrl, sessionToken, authKind, workspaceId, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
|
|
12363
13541
|
const renderer = useRenderer();
|
|
12364
13542
|
const screen = useStore((s) => s.screen);
|
|
13543
|
+
useEffect(() => {
|
|
13544
|
+
if (screen.kind === "chat") recordDefaultAgent(screen.agent.id);
|
|
13545
|
+
}, [screen]);
|
|
12365
13546
|
const chatTitle = useStore((s) => s.chatTitle);
|
|
12366
13547
|
useStore((s) => s.themeId);
|
|
12367
13548
|
const rest = useStore((s) => s.rest);
|
|
@@ -12429,7 +13610,8 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12429
13610
|
useEffect(() => {
|
|
12430
13611
|
const rest = createRestClient({
|
|
12431
13612
|
appUrl,
|
|
12432
|
-
sessionToken
|
|
13613
|
+
sessionToken,
|
|
13614
|
+
workspaceId
|
|
12433
13615
|
});
|
|
12434
13616
|
setClients({
|
|
12435
13617
|
appUrl,
|
|
@@ -12444,7 +13626,9 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12444
13626
|
rest,
|
|
12445
13627
|
agentSelector,
|
|
12446
13628
|
conversationId,
|
|
12447
|
-
newConversation: newConversation || useStore.getState().seedPrompt !== null
|
|
13629
|
+
newConversation: newConversation || useStore.getState().seedPrompt !== null,
|
|
13630
|
+
defaultAgentSelector,
|
|
13631
|
+
forceOnboarding
|
|
12448
13632
|
});
|
|
12449
13633
|
if (cancelled) return;
|
|
12450
13634
|
goTo(screen);
|
|
@@ -12463,10 +13647,13 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12463
13647
|
}, [
|
|
12464
13648
|
appUrl,
|
|
12465
13649
|
sessionToken,
|
|
13650
|
+
workspaceId,
|
|
12466
13651
|
promptHistoryPath,
|
|
12467
13652
|
agentSelector,
|
|
12468
13653
|
conversationId,
|
|
12469
13654
|
newConversation,
|
|
13655
|
+
defaultAgentSelector,
|
|
13656
|
+
forceOnboarding,
|
|
12470
13657
|
goTo,
|
|
12471
13658
|
setClients,
|
|
12472
13659
|
fail
|
|
@@ -12507,6 +13694,7 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12507
13694
|
screen.kind === "error" && /* @__PURE__ */ jsx(ErrorScreen, { message: screen.message }),
|
|
12508
13695
|
screen.kind === "agent-picker" && rest && /* @__PURE__ */ jsx(AgentPickerScreen, {}),
|
|
12509
13696
|
screen.kind === "agent-create" && rest && /* @__PURE__ */ jsx(AgentCreateScreen, {}),
|
|
13697
|
+
screen.kind === "first-run" && rest && /* @__PURE__ */ jsx(FirstRunScreen, {}),
|
|
12510
13698
|
screen.kind === "workspace-picker" && /* @__PURE__ */ jsx(WorkspacePicker, {
|
|
12511
13699
|
appUrl,
|
|
12512
13700
|
sessionToken,
|
|
@@ -12516,7 +13704,10 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12516
13704
|
goTo({ kind: "agent-picker" });
|
|
12517
13705
|
}
|
|
12518
13706
|
}),
|
|
12519
|
-
screen.kind === "conversation-picker" && rest && /* @__PURE__ */ jsx(ConversationPickerScreen, {
|
|
13707
|
+
screen.kind === "conversation-picker" && rest && /* @__PURE__ */ jsx(ConversationPickerScreen, {
|
|
13708
|
+
agent: screen.agent,
|
|
13709
|
+
initialQuery: screen.initialQuery
|
|
13710
|
+
}),
|
|
12520
13711
|
screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
|
|
12521
13712
|
agent: screen.agent,
|
|
12522
13713
|
conversation: screen.conversation
|
|
@@ -13045,7 +14236,7 @@ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, comm
|
|
|
13045
14236
|
* module — and the heavy OpenTUI dependency graph it pulls in — is
|
|
13046
14237
|
* dynamically imported and never loaded on the Node management path.
|
|
13047
14238
|
*/
|
|
13048
|
-
async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, seedPrompt }) {
|
|
14239
|
+
async function runChat({ appUrl, sessionToken, authKind = "session", workspaceId, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding, seedPrompt }) {
|
|
13049
14240
|
await registerGrammars();
|
|
13050
14241
|
const watchdog = startMemoryWatchdog();
|
|
13051
14242
|
installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
|
|
@@ -13060,7 +14251,8 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
13060
14251
|
authKind,
|
|
13061
14252
|
seedPrompt: seedPrompt ?? null,
|
|
13062
14253
|
autoGrantMachine: Boolean(seedPrompt && shareMachine),
|
|
13063
|
-
newConversationIntent: newConversation
|
|
14254
|
+
newConversationIntent: newConversation,
|
|
14255
|
+
forcedOnboarding: forceOnboarding
|
|
13064
14256
|
});
|
|
13065
14257
|
const store = useStore.getState();
|
|
13066
14258
|
if (noColorRequested()) {
|
|
@@ -13081,12 +14273,15 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
13081
14273
|
appUrl,
|
|
13082
14274
|
sessionToken,
|
|
13083
14275
|
authKind,
|
|
14276
|
+
workspaceId,
|
|
13084
14277
|
shareMachine,
|
|
13085
14278
|
promptHistoryPath,
|
|
13086
14279
|
notifications,
|
|
13087
14280
|
agentSelector: agentSelector ?? null,
|
|
13088
14281
|
conversationId: conversationId ?? null,
|
|
13089
|
-
newConversation
|
|
14282
|
+
newConversation,
|
|
14283
|
+
defaultAgentSelector: defaultAgentSelector ?? null,
|
|
14284
|
+
forceOnboarding
|
|
13090
14285
|
})
|
|
13091
14286
|
}));
|
|
13092
14287
|
}
|