skydive-cli 0.5.0-beta.2 → 0.5.0-beta.21
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 +2 -0
- package/README.md +15 -4
- package/dist/js/api-DG5W6iwx.mjs +131 -0
- package/dist/js/bin.mjs +105 -52
- package/dist/js/{boot-DFYFjlht.mjs → boot-DcQhJadu.mjs} +1181 -192
- package/dist/js/{client-Dd5sMXPv.mjs → client-DbqRBquD.mjs} +206 -133
- package/dist/js/client-hH2PJL8y.mjs +5 -0
- package/dist/js/{daemon-DDBIumf1.mjs → daemon-C6tZH1dO.mjs} +3 -2
- package/dist/js/{daemon-Bq93vOIk.mjs → daemon-Dj9tGT12.mjs} +93 -7
- package/dist/js/{daemon-client-Bewt98dE.mjs → daemon-client-BNK77jX_.mjs} +12 -8
- package/dist/js/daemon-client-CgpMgM8Q.mjs +7 -0
- package/dist/js/forward-18QoL5dO.mjs +68 -0
- package/dist/js/{print-ba_0hiV9.mjs → print-C8DHKrJu.mjs} +2 -2
- package/dist/js/{print-CbayCa87.mjs → print-OvYyj6Uk.mjs} +106 -30
- package/dist/js/{print-share-C1LPsgTP.mjs → print-share-BH9gvCls.mjs} +3 -1
- package/dist/js/{profiler-y43HVmq7.mjs → profiler-Bl25cyUp.mjs} +39 -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-DADJh0bi.mjs → rest-DW05dMMc.mjs} +1 -1
- package/dist/js/{rest-BY2nADw5.mjs → rest-Dx6b-Nq_.mjs} +25 -12
- 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 {
|
|
3
|
-
import {
|
|
2
|
+
import { A as setActiveWorkspace, C as themesForMode, D as getActiveWorkspaceId, 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 listWorkspaces, 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 } from "./profiler-Bl25cyUp.mjs";
|
|
3
|
+
import { D as getReviewStateDir, H as saveTheme, N as recordDefaultAgent, O as getSavedTheme, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL, z as resolveWebUrl } from "./print-OvYyj6Uk.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-Dx6b-Nq_.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-Dj9tGT12.mjs";
|
|
9
|
+
import "./api-DG5W6iwx.mjs";
|
|
10
|
+
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
11
|
+
import { t as PortalDaemonClient } from "./daemon-client-BNK77jX_.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,66 @@ 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
|
+
};
|
|
282
325
|
}
|
|
283
|
-
|
|
284
|
-
|
|
326
|
+
if (!agentSelector && newConversation) {
|
|
327
|
+
const firstTwo = await rest.listAgents({
|
|
328
|
+
scope: "org",
|
|
329
|
+
onPage: null,
|
|
330
|
+
limit: 2
|
|
331
|
+
});
|
|
332
|
+
const only = firstTwo.length === 1 ? firstTwo[0] : null;
|
|
333
|
+
if (!only) return { kind: "agent-picker" };
|
|
334
|
+
return newChat(only);
|
|
335
|
+
}
|
|
336
|
+
const agent = resolveAgent(await rest.listAgents({
|
|
337
|
+
scope: "org",
|
|
338
|
+
onPage: null
|
|
339
|
+
}), agentSelector);
|
|
340
|
+
if (newConversation) return newChat(agent);
|
|
341
|
+
return {
|
|
342
|
+
kind: "conversation-picker",
|
|
343
|
+
agent
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function newChat(agent) {
|
|
347
|
+
return {
|
|
285
348
|
kind: "chat",
|
|
286
349
|
agent,
|
|
287
350
|
conversation: {
|
|
@@ -290,10 +353,17 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
290
353
|
nonce: crypto.randomUUID()
|
|
291
354
|
}
|
|
292
355
|
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
356
|
+
}
|
|
357
|
+
/** `resolveAgent` throws for a selector that matches nothing or matches
|
|
358
|
+
* ambiguously — both phrased for a human who can retype `--agent`. A stored
|
|
359
|
+
* default has no command line behind it, so both shapes just mean "no usable
|
|
360
|
+
* default". */
|
|
361
|
+
function resolveAgentOrNull(agents, selector) {
|
|
362
|
+
try {
|
|
363
|
+
return resolveAgent(agents, selector);
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
297
367
|
}
|
|
298
368
|
|
|
299
369
|
//#endregion
|
|
@@ -324,6 +394,8 @@ function createPortalClient(opts) {
|
|
|
324
394
|
deviceToken: null
|
|
325
395
|
}),
|
|
326
396
|
resolveCwd: () => process.cwd(),
|
|
397
|
+
persistedMachineName: null,
|
|
398
|
+
onMachineName: () => {},
|
|
327
399
|
onState: (state) => opts.onState({
|
|
328
400
|
status: state.status,
|
|
329
401
|
machineName: state.machineName,
|
|
@@ -337,7 +409,7 @@ function createPortalClient(opts) {
|
|
|
337
409
|
start: () => Promise.resolve(),
|
|
338
410
|
enable: () => client.enable(),
|
|
339
411
|
disable: () => client.disable(),
|
|
340
|
-
grantAgent: (agentId) => client.grantAgent(agentId),
|
|
412
|
+
grantAgent: (agentId, conversationId) => client.grantAgent(agentId, conversationId),
|
|
341
413
|
decline: (_agentId, declined) => {
|
|
342
414
|
if (declined && client.grantedAgentIds().length === 0) client.disable();
|
|
343
415
|
},
|
|
@@ -1016,42 +1088,133 @@ function createAgentHost({ renderer, notifications }) {
|
|
|
1016
1088
|
* take the one host we can identify unambiguously (github.com, incl. `www.`),
|
|
1017
1089
|
* which covers the common product case; GHES stays the native badge's job.
|
|
1018
1090
|
*
|
|
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
|
-
|
|
1091
|
+
* Only the `/pull/<n>` form (not `/issues/`). A trailing path/query/fragment
|
|
1092
|
+
* (e.g. `/files`, `#discussion`) is allowed but not captured into the canonical
|
|
1093
|
+
* URL. Also matches inside markdown links, backticks, and trailing punctuation.
|
|
1094
|
+
*
|
|
1095
|
+
* The `https://` (and `www.`) prefix is optional: agents and people routinely
|
|
1096
|
+
* write a bare `github.com/o/r/pull/7`, and that should link a PR the same way
|
|
1097
|
+
* the full URL does. To keep a bare host from matching inside a longer hostname
|
|
1098
|
+
* (`mygithub.com/...`, `notgithub.com/...`), a protocol-less match must be
|
|
1099
|
+
* preceded by a start-of-string or a non-host character (anything but a word
|
|
1100
|
+
* char or `.`) — a leading boundary the `(?:^|[^\w.])` group asserts and
|
|
1101
|
+
* consumes. The consumed boundary char is outside the captured groups, so it
|
|
1102
|
+
* never leaks into owner/repo. `matchAll` won't overlap, but the boundary is a
|
|
1103
|
+
* single delimiter (space, `(`, `[`, etc.) between PRs in practice, so distinct
|
|
1104
|
+
* back-to-back URLs still each match.
|
|
1105
|
+
*/
|
|
1106
|
+
const PR_URL_RE = /(?:^|[^\w.])(?:https?:\/\/)?(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
|
|
1107
|
+
/**
|
|
1108
|
+
* Matches a bare, human-style PR reference `#<n>` — the way people (and LLMs)
|
|
1109
|
+
* name a PR in prose ("opened #20858", "landed #42"). Requires a non-word
|
|
1110
|
+
* boundary before the `#` so a fragment tail like `/pull/7#discussion_r5` or a
|
|
1111
|
+
* hex color `#20858` embedded in a word doesn't count, and captures the digits
|
|
1112
|
+
* up to a word boundary. We only harvest these from **assistant prose**, never
|
|
1113
|
+
* from tool output, because `#N` is exactly the relevance signal we're after:
|
|
1114
|
+
* the agent chose to talk about this PR.
|
|
1115
|
+
*/
|
|
1116
|
+
const PR_HASH_RE = /(?:^|[^\w#/])#(\d+)\b/g;
|
|
1117
|
+
/**
|
|
1118
|
+
* Every full `/pull/<n>` github.com URL in `urlText`, keyed by number to the
|
|
1119
|
+
* candidate refs for that number. A number can map to more than one repo, so we
|
|
1120
|
+
* keep all candidates; a bare `#N` label can't say which repo, but the pill's
|
|
1121
|
+
* own URL does. Deduped by repo-qualified identity (`owner/repo#number`) so the
|
|
1122
|
+
* same PR linked twice — even with a different trailing path/fragment —
|
|
1123
|
+
* collapses to one ref.
|
|
1124
|
+
*
|
|
1125
|
+
* `deferTrailingNumber` guards the streaming truncation case: when true, a URL
|
|
1126
|
+
* whose number runs to the very end of `urlText` is skipped because the next
|
|
1127
|
+
* delta may extend those digits (`.../pull/19` now, `.../pull/19739` later).
|
|
1128
|
+
* Only the still-growing PROSE buffer sets this; settled tool output is always
|
|
1129
|
+
* complete, so its URLs are harvested with the guard off.
|
|
1130
|
+
*/
|
|
1131
|
+
function urlsByNumber(urlText, deferTrailingNumber, byNumber) {
|
|
1132
|
+
for (const match of urlText.matchAll(PR_URL_RE)) {
|
|
1038
1133
|
const owner = match[1];
|
|
1039
1134
|
const repo = match[2];
|
|
1040
1135
|
const numberText = match[3];
|
|
1041
1136
|
if (!owner || !repo || !numberText) continue;
|
|
1042
|
-
if (
|
|
1043
|
-
if (match.index + match[0].length >=
|
|
1137
|
+
if (deferTrailingNumber) {
|
|
1138
|
+
if (match.index + match[0].length >= urlText.length) continue;
|
|
1044
1139
|
}
|
|
1045
1140
|
const number = Number.parseInt(numberText, 10);
|
|
1046
1141
|
if (!Number.isFinite(number) || number <= 0) continue;
|
|
1047
|
-
const
|
|
1048
|
-
if (byKey.has(key)) continue;
|
|
1049
|
-
byKey.set(key, {
|
|
1142
|
+
const ref = {
|
|
1050
1143
|
number,
|
|
1051
|
-
key
|
|
1144
|
+
key: `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`,
|
|
1052
1145
|
url: `https://github.com/${owner}/${repo}/pull/${number}`
|
|
1053
|
-
}
|
|
1146
|
+
};
|
|
1147
|
+
const existing = byNumber.get(number);
|
|
1148
|
+
if (existing) {
|
|
1149
|
+
if (!existing.some((r) => r.key === ref.key)) existing.push(ref);
|
|
1150
|
+
} else byNumber.set(number, [ref]);
|
|
1054
1151
|
}
|
|
1152
|
+
}
|
|
1153
|
+
/** Every human-style `#N` named in `proseText` — the relevance signal. */
|
|
1154
|
+
function hashNumbers(proseText) {
|
|
1155
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
1156
|
+
for (const match of proseText.matchAll(PR_HASH_RE)) {
|
|
1157
|
+
const numberText = match[1];
|
|
1158
|
+
if (!numberText) continue;
|
|
1159
|
+
const number = Number.parseInt(numberText, 10);
|
|
1160
|
+
if (Number.isFinite(number) && number > 0) numbers.add(number);
|
|
1161
|
+
}
|
|
1162
|
+
return numbers;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* The relevance contract, in one place.
|
|
1166
|
+
*
|
|
1167
|
+
* A GitHub PR URL is cheap to emit and shows up in bulk any time a tool dumps
|
|
1168
|
+
* PRs the conversation isn't actually about — `gh pr list`, `gh pr status`, a
|
|
1169
|
+
* `gh search` — printing dozens of `/pull/N` URLs the agent never discusses.
|
|
1170
|
+
* Pinning a sidebar pill for every one of those is noise.
|
|
1171
|
+
*
|
|
1172
|
+
* So a PR is surfaced only when BOTH hold:
|
|
1173
|
+
* 1. a fully-qualified `github.com/<o>/<r>/pull/<n>` URL exists — this may
|
|
1174
|
+
* come from the assistant's prose OR from tool output (a `gh pr create`
|
|
1175
|
+
* that printed the URL the agent then referenced by number), and
|
|
1176
|
+
* 2. the **assistant's prose** names the number as a human-style `#<n>`.
|
|
1177
|
+
*
|
|
1178
|
+
* The `#N` in prose is the intent signal — "the agent is talking about this
|
|
1179
|
+
* PR", as opposed to "a tool happened to list it". The full URL (from wherever)
|
|
1180
|
+
* proves the PR is real and pins down its repo + canonical link. A URL with no
|
|
1181
|
+
* `#N` in prose (the whole of a `gh pr list` dump) is dropped; a `#N` with no
|
|
1182
|
+
* matching URL anywhere (a bare issue-style ref) is dropped.
|
|
1183
|
+
*
|
|
1184
|
+
* `proseText` is the assistant's prose alone (the only source of the `#N`
|
|
1185
|
+
* relevance signal); `urlOnlyText` is settled tool output that may carry full
|
|
1186
|
+
* PR URLs but no `#N` intent. URLs are harvested from both; `#N` from prose
|
|
1187
|
+
* only.
|
|
1188
|
+
*/
|
|
1189
|
+
function collectRelevant(proseText, urlOnlyText, options, byKey) {
|
|
1190
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
1191
|
+
urlsByNumber(proseText, options.requireCompleteNumber, byNumber);
|
|
1192
|
+
if (urlOnlyText) urlsByNumber(urlOnlyText, false, byNumber);
|
|
1193
|
+
if (byNumber.size === 0) return;
|
|
1194
|
+
const named = hashNumbers(proseText);
|
|
1195
|
+
if (named.size === 0) return;
|
|
1196
|
+
for (const [number, refs] of byNumber) {
|
|
1197
|
+
if (!named.has(number)) continue;
|
|
1198
|
+
for (const ref of refs) if (!byKey.has(ref.key)) byKey.set(ref.key, ref);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Extracts every distinct GitHub PR that is both linked (a full `/pull/<n>`
|
|
1203
|
+
* URL) AND named (`#<n>`), de-duplicated by **repo-qualified identity**
|
|
1204
|
+
* (`owner/repo#number`) — NOT by number, since `#42` can exist in many repos.
|
|
1205
|
+
*
|
|
1206
|
+
* The two-argument form takes the URL pool and the prose separately, so a URL
|
|
1207
|
+
* from tool output can satisfy the link requirement while the `#N` stays a
|
|
1208
|
+
* prose-only signal. The one-argument form (prose only) is the common case: it
|
|
1209
|
+
* requires the URL to be in the same prose that names it.
|
|
1210
|
+
*
|
|
1211
|
+
* See {@link collectRelevant} for the contract.
|
|
1212
|
+
*/
|
|
1213
|
+
function extractPullRequests(proseText, options) {
|
|
1214
|
+
const requireCompleteNumber = options?.requireCompleteNumber ?? true;
|
|
1215
|
+
const urlOnly = options?.urlOnlyText ?? "";
|
|
1216
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1217
|
+
collectRelevant(proseText, urlOnly, { requireCompleteNumber }, byKey);
|
|
1055
1218
|
return [...byKey.values()];
|
|
1056
1219
|
}
|
|
1057
1220
|
/**
|
|
@@ -1090,28 +1253,34 @@ function stringifyToolOutput(value) {
|
|
|
1090
1253
|
}
|
|
1091
1254
|
/**
|
|
1092
1255
|
* A streaming PR scanner: fed run chunks as they stream, it reports each
|
|
1093
|
-
* distinct PR the moment
|
|
1094
|
-
* the
|
|
1256
|
+
* distinct PR the moment BOTH conditions hold — a full `/pull/<n>` URL has been
|
|
1257
|
+
* seen (in prose OR tool output) AND the assistant's prose names it `#<n>` —
|
|
1258
|
+
* exactly once, without ever rescanning the transcript.
|
|
1095
1259
|
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1260
|
+
* Two sources feed it, with different roles:
|
|
1261
|
+
* - **Assistant text** (`text-delta`): the ONLY source of the `#N` relevance
|
|
1262
|
+
* signal, and also a source of URLs. Accumulated per part id (a URL or `#N`
|
|
1263
|
+
* can split across two deltas); only the current part's buffer is scanned.
|
|
1264
|
+
* - **Tool output** (`tool-output-available`): a URL-only source. A
|
|
1265
|
+
* `gh pr create` prints the URL the agent then references by number, so the
|
|
1266
|
+
* URL for a #N mention often lives here, not in the prose. Tool output can
|
|
1267
|
+
* NEVER originate a pill on its own — a `gh pr list` dump (URLs, no `#N`)
|
|
1268
|
+
* stays silent — but it satisfies the URL half for a PR the prose names.
|
|
1103
1269
|
*
|
|
1104
1270
|
* `onPullRequest` fires once per never-before-seen PR (keyed by
|
|
1105
|
-
* `owner/repo#number`)
|
|
1106
|
-
*
|
|
1107
|
-
* same chunks the transcript reducer sees.
|
|
1271
|
+
* `owner/repo#number`). Construct one per run (or per conversation) and feed it
|
|
1272
|
+
* the same chunks the transcript reducer sees. See {@link collectRelevant}.
|
|
1108
1273
|
*/
|
|
1109
1274
|
function createPullRequestScanner(onPullRequest) {
|
|
1110
1275
|
let text = "";
|
|
1111
1276
|
let lastPartId;
|
|
1277
|
+
let toolUrlText = "";
|
|
1112
1278
|
const seen = /* @__PURE__ */ new Set();
|
|
1113
|
-
const report = (
|
|
1114
|
-
for (const pr of extractPullRequests(
|
|
1279
|
+
const report = (final) => {
|
|
1280
|
+
for (const pr of extractPullRequests(text, {
|
|
1281
|
+
requireCompleteNumber: !final,
|
|
1282
|
+
urlOnlyText: toolUrlText
|
|
1283
|
+
})) {
|
|
1115
1284
|
if (seen.has(pr.key)) continue;
|
|
1116
1285
|
seen.add(pr.key);
|
|
1117
1286
|
onPullRequest(pr);
|
|
@@ -1121,7 +1290,8 @@ function createPullRequestScanner(onPullRequest) {
|
|
|
1121
1290
|
if (chunk["type"] === "tool-output-available") {
|
|
1122
1291
|
const outputText = stringifyToolOutput(chunk["output"]);
|
|
1123
1292
|
if (!outputText.includes("/pull/")) return;
|
|
1124
|
-
|
|
1293
|
+
toolUrlText += `\n${outputText}`;
|
|
1294
|
+
report(true);
|
|
1125
1295
|
return;
|
|
1126
1296
|
}
|
|
1127
1297
|
if (chunk["type"] !== "text-delta") return;
|
|
@@ -1129,13 +1299,12 @@ function createPullRequestScanner(onPullRequest) {
|
|
|
1129
1299
|
if (!delta) return;
|
|
1130
1300
|
const partId = typeof chunk["id"] === "string" ? chunk["id"] : void 0;
|
|
1131
1301
|
if (partId && lastPartId && partId !== lastPartId) {
|
|
1132
|
-
|
|
1302
|
+
report(true);
|
|
1133
1303
|
text = "";
|
|
1134
1304
|
}
|
|
1135
1305
|
text += delta;
|
|
1136
1306
|
lastPartId = partId ?? lastPartId;
|
|
1137
|
-
|
|
1138
|
-
report(text, false);
|
|
1307
|
+
report(false);
|
|
1139
1308
|
} };
|
|
1140
1309
|
}
|
|
1141
1310
|
|
|
@@ -1479,6 +1648,7 @@ function screenLabelParts(screen, chatTitle) {
|
|
|
1479
1648
|
case "error": return { title: "" };
|
|
1480
1649
|
case "agent-picker": return { title: "pick an agent" };
|
|
1481
1650
|
case "agent-create": return { title: "create an agent" };
|
|
1651
|
+
case "first-run": return { title: "welcome" };
|
|
1482
1652
|
case "workspace-picker": return { title: "switch workspace" };
|
|
1483
1653
|
case "conversation-picker": return {
|
|
1484
1654
|
title: "pick a conversation",
|
|
@@ -1649,13 +1819,15 @@ function WordmarkRows({ mono }) {
|
|
|
1649
1819
|
});
|
|
1650
1820
|
}
|
|
1651
1821
|
/**
|
|
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
|
-
*
|
|
1822
|
+
* Height- and width-aware brand header for the pickers. Always shown
|
|
1823
|
+
* (including while the list is loading / empty / erroring) so the splash is
|
|
1824
|
+
* never gated on data. Full mark + wordmark only when the terminal is tall
|
|
1825
|
+
* enough AND wide enough that the art sits on six lines; otherwise a compact
|
|
1826
|
+
* one-liner. Wrapping the splash would blow the 7-row budget the list
|
|
1827
|
+
* subtracts, and the overflow collapses the top list rows onto each other.
|
|
1656
1828
|
*/
|
|
1657
|
-
function BrandHeader({ height }) {
|
|
1658
|
-
if (height
|
|
1829
|
+
function BrandHeader({ height, width }) {
|
|
1830
|
+
if (showFullSplash(height, width)) return /* @__PURE__ */ jsxs("box", {
|
|
1659
1831
|
style: { flexDirection: "column" },
|
|
1660
1832
|
children: [/* @__PURE__ */ jsx(SkydiveMark, { variant: "full" }), /* @__PURE__ */ jsx("box", { style: { height: 1 } })]
|
|
1661
1833
|
});
|
|
@@ -1664,9 +1836,12 @@ function BrandHeader({ height }) {
|
|
|
1664
1836
|
children: "✦ Skydive"
|
|
1665
1837
|
});
|
|
1666
1838
|
}
|
|
1839
|
+
function showFullSplash(height, width) {
|
|
1840
|
+
return height >= 24 && splashFitsWidth(width);
|
|
1841
|
+
}
|
|
1667
1842
|
/** Row budget the BrandHeader consumes; callers subtract this from visibleRows. */
|
|
1668
|
-
function brandHeaderRows(height) {
|
|
1669
|
-
return height
|
|
1843
|
+
function brandHeaderRows(height, width) {
|
|
1844
|
+
return showFullSplash(height, width) ? 7 : 1;
|
|
1670
1845
|
}
|
|
1671
1846
|
function SkydiveMark({ variant = "full" }) {
|
|
1672
1847
|
const mono = noColor();
|
|
@@ -1753,13 +1928,16 @@ function AgentPickerScreen() {
|
|
|
1753
1928
|
scope,
|
|
1754
1929
|
cache
|
|
1755
1930
|
]);
|
|
1756
|
-
const { height } = useTerminalDimensions();
|
|
1931
|
+
const { width, height } = useTerminalDimensions();
|
|
1757
1932
|
if (state.kind === "loading") return /* @__PURE__ */ jsxs("box", {
|
|
1758
1933
|
style: {
|
|
1759
1934
|
flexDirection: "column",
|
|
1760
1935
|
flexGrow: 1
|
|
1761
1936
|
},
|
|
1762
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1937
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1938
|
+
height,
|
|
1939
|
+
width
|
|
1940
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
1763
1941
|
fg: theme.muted,
|
|
1764
1942
|
children: "loading agents…"
|
|
1765
1943
|
})]
|
|
@@ -1769,7 +1947,10 @@ function AgentPickerScreen() {
|
|
|
1769
1947
|
flexDirection: "column",
|
|
1770
1948
|
flexGrow: 1
|
|
1771
1949
|
},
|
|
1772
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1950
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1951
|
+
height,
|
|
1952
|
+
width
|
|
1953
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
1773
1954
|
fg: theme.error,
|
|
1774
1955
|
children: ["error: ", state.message]
|
|
1775
1956
|
})]
|
|
@@ -1779,7 +1960,10 @@ function AgentPickerScreen() {
|
|
|
1779
1960
|
flexDirection: "column",
|
|
1780
1961
|
flexGrow: 1
|
|
1781
1962
|
},
|
|
1782
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1963
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
1964
|
+
height,
|
|
1965
|
+
width
|
|
1966
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
1783
1967
|
fg: theme.muted,
|
|
1784
1968
|
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
1969
|
})]
|
|
@@ -1800,7 +1984,7 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1800
1984
|
const hits = fuzzyFilter(query, agents, agentKeys);
|
|
1801
1985
|
const filtered = hits.map((h) => h.item);
|
|
1802
1986
|
const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
|
|
1803
|
-
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height));
|
|
1987
|
+
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height, width));
|
|
1804
1988
|
const start = windowStart(clamped, filtered.length, visibleRows);
|
|
1805
1989
|
const windowed = hits.slice(start, start + visibleRows);
|
|
1806
1990
|
useKeyboard((key) => {
|
|
@@ -1832,7 +2016,10 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
1832
2016
|
flexGrow: 1
|
|
1833
2017
|
},
|
|
1834
2018
|
children: [
|
|
1835
|
-
/* @__PURE__ */ jsx(BrandHeader, {
|
|
2019
|
+
/* @__PURE__ */ jsx(BrandHeader, {
|
|
2020
|
+
height,
|
|
2021
|
+
width
|
|
2022
|
+
}),
|
|
1836
2023
|
/* @__PURE__ */ jsxs("box", {
|
|
1837
2024
|
style: {
|
|
1838
2025
|
flexDirection: "row",
|
|
@@ -2071,6 +2258,208 @@ function AgentCreateScreen() {
|
|
|
2071
2258
|
});
|
|
2072
2259
|
}
|
|
2073
2260
|
|
|
2261
|
+
//#endregion
|
|
2262
|
+
//#region src/chat/tui/screens/first-run.tsx
|
|
2263
|
+
/** Rows the mark lockup needs before it's worth painting (matches the
|
|
2264
|
+
* new-chat splash): the 6-row mark, a spacer, and a couple of content rows.
|
|
2265
|
+
* Below this the mark is dropped and only the message renders. */
|
|
2266
|
+
const MARK_MIN_ROWS$1 = 12;
|
|
2267
|
+
/** Columns the full mark + wordmark lockup spans; below this the pinwheel
|
|
2268
|
+
* mark renders alone instead of wrapping the wordmark. */
|
|
2269
|
+
const FULL_MARK_MIN_COLS$1 = 70;
|
|
2270
|
+
function FirstRunScreen() {
|
|
2271
|
+
const rest = useStore((s) => s.rest);
|
|
2272
|
+
const goTo = useStore((s) => s.goTo);
|
|
2273
|
+
const fail = useStore((s) => s.fail);
|
|
2274
|
+
const authKind = useStore((s) => s.authKind);
|
|
2275
|
+
const forced = useStore((s) => s.forcedOnboarding);
|
|
2276
|
+
const [state, setState] = useState({ kind: "creating" });
|
|
2277
|
+
const canShareMachine = authKind === "session";
|
|
2278
|
+
useEffect(() => {
|
|
2279
|
+
if (!rest) return;
|
|
2280
|
+
let cancelled = false;
|
|
2281
|
+
(async () => {
|
|
2282
|
+
try {
|
|
2283
|
+
const agent = await rest.createAgent({});
|
|
2284
|
+
if (cancelled) return;
|
|
2285
|
+
setState({
|
|
2286
|
+
kind: "offer",
|
|
2287
|
+
agent
|
|
2288
|
+
});
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
if (cancelled) return;
|
|
2291
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
2292
|
+
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
setState({
|
|
2296
|
+
kind: "error",
|
|
2297
|
+
message: createErrorMessage(err)
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
})();
|
|
2301
|
+
return () => {
|
|
2302
|
+
cancelled = true;
|
|
2303
|
+
};
|
|
2304
|
+
}, [rest, fail]);
|
|
2305
|
+
const openNewChat = (agent) => {
|
|
2306
|
+
goTo({
|
|
2307
|
+
kind: "chat",
|
|
2308
|
+
agent,
|
|
2309
|
+
conversation: {
|
|
2310
|
+
kind: "new",
|
|
2311
|
+
agentId: agent.id,
|
|
2312
|
+
nonce: crypto.randomUUID()
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
};
|
|
2316
|
+
const openOnboardingChat = (agent, conversationId, title) => {
|
|
2317
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2318
|
+
goTo({
|
|
2319
|
+
kind: "chat",
|
|
2320
|
+
agent,
|
|
2321
|
+
conversation: {
|
|
2322
|
+
id: conversationId,
|
|
2323
|
+
title,
|
|
2324
|
+
createdAt: now,
|
|
2325
|
+
updatedAt: now,
|
|
2326
|
+
preview: null,
|
|
2327
|
+
channel: "web",
|
|
2328
|
+
channelLabel: null,
|
|
2329
|
+
agent: {
|
|
2330
|
+
id: agent.id,
|
|
2331
|
+
name: agent.name,
|
|
2332
|
+
slug: agent.slug ?? null,
|
|
2333
|
+
title: agent.title ?? null
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
};
|
|
2338
|
+
const acceptImport = async (agent) => {
|
|
2339
|
+
if (!rest) return;
|
|
2340
|
+
setState({ kind: "starting" });
|
|
2341
|
+
const { portalClient } = useStore.getState();
|
|
2342
|
+
try {
|
|
2343
|
+
const os = machineOsFromPlatform(process.platform);
|
|
2344
|
+
const { conversationId } = await rest.createOnboardingConversation({
|
|
2345
|
+
agentId: agent.id,
|
|
2346
|
+
projectDir: process.cwd(),
|
|
2347
|
+
os
|
|
2348
|
+
});
|
|
2349
|
+
if (portalClient) {
|
|
2350
|
+
portalClient.enable();
|
|
2351
|
+
portalClient.grantAgent(agent.id, conversationId).catch(() => {
|
|
2352
|
+
useStore.getState().showToast({
|
|
2353
|
+
message: `Couldn't share this machine automatically. ${agent.name} will ask in chat.`,
|
|
2354
|
+
variant: "warning"
|
|
2355
|
+
});
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
openOnboardingChat(agent, conversationId, `Onboarding ${agent.name}`);
|
|
2359
|
+
} catch (err) {
|
|
2360
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
2361
|
+
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
useStore.getState().showToast({
|
|
2365
|
+
message: "Couldn't start onboarding automatically. Opening a chat instead.",
|
|
2366
|
+
variant: "warning"
|
|
2367
|
+
});
|
|
2368
|
+
openNewChat(agent);
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const declineImport = (agent) => {
|
|
2372
|
+
setState({ kind: "starting" });
|
|
2373
|
+
openNewChat(agent);
|
|
2374
|
+
};
|
|
2375
|
+
useKeyboard((key) => {
|
|
2376
|
+
if (state.kind !== "offer") return;
|
|
2377
|
+
if (!canShareMachine) {
|
|
2378
|
+
declineImport(state.agent);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
if (key.name === "return" || key.name === "y") acceptImport(state.agent);
|
|
2382
|
+
else if (key.name === "n" || key.name === "escape") declineImport(state.agent);
|
|
2383
|
+
});
|
|
2384
|
+
return /* @__PURE__ */ jsx(Centered, { children: renderBody({
|
|
2385
|
+
state,
|
|
2386
|
+
forced,
|
|
2387
|
+
canShareMachine
|
|
2388
|
+
}) });
|
|
2389
|
+
}
|
|
2390
|
+
/** The Skydive mark centered above the screen's content, matching the
|
|
2391
|
+
* new-chat splash lockup and degrading by available space. */
|
|
2392
|
+
function Centered({ children }) {
|
|
2393
|
+
const { width, height } = useTerminalDimensions();
|
|
2394
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2395
|
+
style: {
|
|
2396
|
+
flexGrow: 1,
|
|
2397
|
+
flexDirection: "column",
|
|
2398
|
+
justifyContent: "center",
|
|
2399
|
+
alignItems: "center"
|
|
2400
|
+
},
|
|
2401
|
+
children: [height >= MARK_MIN_ROWS$1 ? /* @__PURE__ */ jsx("box", {
|
|
2402
|
+
style: {
|
|
2403
|
+
flexDirection: "column",
|
|
2404
|
+
alignItems: "center",
|
|
2405
|
+
marginBottom: 1
|
|
2406
|
+
},
|
|
2407
|
+
children: /* @__PURE__ */ jsx(SkydiveMark, { variant: width >= FULL_MARK_MIN_COLS$1 ? "full" : "mark" })
|
|
2408
|
+
}) : null, /* @__PURE__ */ jsx("box", {
|
|
2409
|
+
style: {
|
|
2410
|
+
flexDirection: "column",
|
|
2411
|
+
alignItems: "center",
|
|
2412
|
+
gap: 1
|
|
2413
|
+
},
|
|
2414
|
+
children
|
|
2415
|
+
})]
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
function renderBody({ state, forced, canShareMachine }) {
|
|
2419
|
+
if (state.kind === "error") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2420
|
+
fg: theme.error,
|
|
2421
|
+
children: state.message
|
|
2422
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2423
|
+
fg: theme.muted,
|
|
2424
|
+
children: "press ctrl+c to quit, then run `skydive` again to retry."
|
|
2425
|
+
})] });
|
|
2426
|
+
if (state.kind === "creating") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2427
|
+
fg: theme.fg,
|
|
2428
|
+
children: "Welcome to Skydive."
|
|
2429
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2430
|
+
fg: theme.muted,
|
|
2431
|
+
children: forced ? "creating an agent…" : "setting up your first agent…"
|
|
2432
|
+
})] });
|
|
2433
|
+
if (state.kind === "starting") return /* @__PURE__ */ jsx("text", {
|
|
2434
|
+
fg: theme.muted,
|
|
2435
|
+
children: "opening your agent…"
|
|
2436
|
+
});
|
|
2437
|
+
const { agent } = state;
|
|
2438
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2439
|
+
fg: theme.fg,
|
|
2440
|
+
children: forced ? `${agent.name} is ready.` : `Your first agent, ${agent.name}, is ready.`
|
|
2441
|
+
}), canShareMachine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", {
|
|
2442
|
+
fg: theme.muted,
|
|
2443
|
+
children: [
|
|
2444
|
+
"Want ",
|
|
2445
|
+
agent.name,
|
|
2446
|
+
" 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."
|
|
2447
|
+
]
|
|
2448
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2449
|
+
fg: theme.dim,
|
|
2450
|
+
children: "↵/y yes, learn from my setup · n skip"
|
|
2451
|
+
})] }) : /* @__PURE__ */ jsx("text", {
|
|
2452
|
+
fg: theme.dim,
|
|
2453
|
+
children: "press any key to open the chat"
|
|
2454
|
+
})] });
|
|
2455
|
+
}
|
|
2456
|
+
function createErrorMessage(err) {
|
|
2457
|
+
if (err instanceof HttpError && err.status === 403) return "You don't have permission to create an agent in this workspace.";
|
|
2458
|
+
if (err instanceof HttpError && err.status === 429) return "Too many requests. Wait a moment, then run `skydive` again.";
|
|
2459
|
+
if (err instanceof HttpError) return "Skydive could not create your agent. Try again in a moment.";
|
|
2460
|
+
return "Could not reach Skydive. Check your connection and try again.";
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2074
2463
|
//#endregion
|
|
2075
2464
|
//#region src/chat/tui/screens/workspace-picker.tsx
|
|
2076
2465
|
function WorkspacePicker({ appUrl, sessionToken, onSelect, onCancel }) {
|
|
@@ -2419,15 +2808,80 @@ function useDebouncedValue(value, delayMs) {
|
|
|
2419
2808
|
}
|
|
2420
2809
|
|
|
2421
2810
|
//#endregion
|
|
2422
|
-
//#region src/chat/tui/screens/conversation-
|
|
2423
|
-
|
|
2424
|
-
|
|
2811
|
+
//#region src/chat/tui/screens/conversation-channel.ts
|
|
2812
|
+
function isNewConversation(c) {
|
|
2813
|
+
return "kind" in c && c.kind === "new";
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* The channels a human can start a conversation on. Enumerated, not derived,
|
|
2817
|
+
* to keep `@createinc/anyone-db` out of the CLI build (see rest.ts). A newly
|
|
2818
|
+
* added human channel needs adding here too. The picker filters its default
|
|
2819
|
+
* view to these; `isAgentRunConversation` is their complement.
|
|
2820
|
+
*/
|
|
2425
2821
|
const humanChannels = [
|
|
2426
2822
|
"web",
|
|
2427
2823
|
"slack",
|
|
2428
2824
|
"email",
|
|
2429
2825
|
"imessage"
|
|
2430
2826
|
];
|
|
2827
|
+
/**
|
|
2828
|
+
* Whether a conversation is an agent run — a thread the agent opened for
|
|
2829
|
+
* itself (cron firing, inbound webhook wake) rather than one a person
|
|
2830
|
+
* started. These have no human participant row, so per-user archive can
|
|
2831
|
+
* never apply to them (there is nothing to mark); delete is their only
|
|
2832
|
+
* cleanup affordance, and the UI should not offer archive on them.
|
|
2833
|
+
*
|
|
2834
|
+
* Best-effort: an a2a/subagent thread carries a `web` channel binding, so it
|
|
2835
|
+
* reads as `'web'` here and is NOT detected. Archive on one of those still
|
|
2836
|
+
* fails server-side with the existing error notice — this predicate only
|
|
2837
|
+
* removes the affordance where the client can know it's dead.
|
|
2838
|
+
*/
|
|
2839
|
+
function isAgentRunConversation(c) {
|
|
2840
|
+
if (isNewConversation(c)) return false;
|
|
2841
|
+
if (c.channel === null) return false;
|
|
2842
|
+
const { channel } = c;
|
|
2843
|
+
return !humanChannels.some((human) => human === channel);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* The channel a conversation belongs to when it isn't the web channel, else
|
|
2847
|
+
* null. A brand-new conversation is always web, and the list route reports the
|
|
2848
|
+
* web channel as either `'web'` or (legacy) null — both mean "post from here".
|
|
2849
|
+
* Anything else (slack, email, imessage, webhook, cron) is owned by that
|
|
2850
|
+
* channel: the message has to go through it, so the TUI can't send here.
|
|
2851
|
+
* Mirrors the web app's `channel !== null && channel !== 'web'` gate.
|
|
2852
|
+
*/
|
|
2853
|
+
function nonWebChannelLabel(c) {
|
|
2854
|
+
if (isNewConversation(c)) return null;
|
|
2855
|
+
if (c.channel === null || c.channel === "web") return null;
|
|
2856
|
+
return c.channelLabel ?? c.channel;
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* The list-row shape for a conversation the TUI just forked, so the chat
|
|
2860
|
+
* screen can reopen on the copy without waiting for a list refetch. A fork
|
|
2861
|
+
* carries none of the source's channel binding — that's what makes it
|
|
2862
|
+
* sendable from here — so it is always a web conversation.
|
|
2863
|
+
*/
|
|
2864
|
+
function forkedConversationRef({ agent, forked }) {
|
|
2865
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2866
|
+
return {
|
|
2867
|
+
id: forked.id,
|
|
2868
|
+
title: forked.title,
|
|
2869
|
+
createdAt: now,
|
|
2870
|
+
updatedAt: now,
|
|
2871
|
+
preview: null,
|
|
2872
|
+
channel: "web",
|
|
2873
|
+
channelLabel: null,
|
|
2874
|
+
agent: {
|
|
2875
|
+
id: agent.id,
|
|
2876
|
+
name: agent.name
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
//#endregion
|
|
2882
|
+
//#region src/chat/tui/screens/conversation-picker.tsx
|
|
2883
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
2884
|
+
const SEARCH_QUERY_MAX_LENGTH = 200;
|
|
2431
2885
|
function ConversationPickerScreen({ agent }) {
|
|
2432
2886
|
const rest = useStore((s) => s.rest);
|
|
2433
2887
|
const goTo = useStore((s) => s.goTo);
|
|
@@ -2492,13 +2946,16 @@ function ConversationPickerScreen({ agent }) {
|
|
|
2492
2946
|
cacheKey,
|
|
2493
2947
|
cache
|
|
2494
2948
|
]);
|
|
2495
|
-
const { height } = useTerminalDimensions();
|
|
2949
|
+
const { width, height } = useTerminalDimensions();
|
|
2496
2950
|
if (state.kind === "loading") return /* @__PURE__ */ jsxs("box", {
|
|
2497
2951
|
style: {
|
|
2498
2952
|
flexDirection: "column",
|
|
2499
2953
|
flexGrow: 1
|
|
2500
2954
|
},
|
|
2501
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2955
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2956
|
+
height,
|
|
2957
|
+
width
|
|
2958
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2502
2959
|
fg: theme.muted,
|
|
2503
2960
|
children: "loading conversations…"
|
|
2504
2961
|
})]
|
|
@@ -2508,7 +2965,10 @@ function ConversationPickerScreen({ agent }) {
|
|
|
2508
2965
|
flexDirection: "column",
|
|
2509
2966
|
flexGrow: 1
|
|
2510
2967
|
},
|
|
2511
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2968
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2969
|
+
height,
|
|
2970
|
+
width
|
|
2971
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
2512
2972
|
fg: theme.error,
|
|
2513
2973
|
children: ["error: ", state.message]
|
|
2514
2974
|
})]
|
|
@@ -2615,6 +3075,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2615
3075
|
const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
|
|
2616
3076
|
const highlightRow = rows[clamped];
|
|
2617
3077
|
const highlightArchived = highlightRow?.kind === "conv" && isArchived(highlightRow.hit.item);
|
|
3078
|
+
const highlightAgentRun = highlightRow?.kind === "conv" && isAgentRunConversation(highlightRow.hit.item);
|
|
2618
3079
|
const confirmConv = confirmId !== null ? list.find((c) => c.id === confirmId) : void 0;
|
|
2619
3080
|
const searchState = isSearching ? search?.state : void 0;
|
|
2620
3081
|
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 +3089,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2628
3089
|
fg: theme.dim,
|
|
2629
3090
|
text: fitHintLine([
|
|
2630
3091
|
countSegment,
|
|
2631
|
-
`ctrl+a ${highlightArchived ? "unarchive" : "archive"}
|
|
3092
|
+
...highlightAgentRun ? [] : [`ctrl+a ${highlightArchived ? "unarchive" : "archive"}`],
|
|
2632
3093
|
"ctrl+d delete",
|
|
2633
3094
|
`tab ${showAutomated ? "hide" : "show"} agent runs`,
|
|
2634
3095
|
`shift+tab ${showArchived ? "hide" : "show"} archived`,
|
|
@@ -2637,7 +3098,7 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2637
3098
|
"↵ open"
|
|
2638
3099
|
], innerWidth)
|
|
2639
3100
|
};
|
|
2640
|
-
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height));
|
|
3101
|
+
const visibleRows = Math.max(3, height - 6 - brandHeaderRows(height, width));
|
|
2641
3102
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
2642
3103
|
const windowed = rows.slice(start, start + visibleRows);
|
|
2643
3104
|
const open = (row) => {
|
|
@@ -2722,6 +3183,10 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2722
3183
|
} else if (key.name === "a" && key.ctrl) {
|
|
2723
3184
|
const row = rows[clamped];
|
|
2724
3185
|
if (row?.kind === "conv") {
|
|
3186
|
+
if (isAgentRunConversation(row.hit.item)) {
|
|
3187
|
+
setError("agent runs can't be archived — ctrl+d deletes");
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
2725
3190
|
setError(null);
|
|
2726
3191
|
toggleArchived(row.hit.item);
|
|
2727
3192
|
}
|
|
@@ -2738,7 +3203,10 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2738
3203
|
flexGrow: 1
|
|
2739
3204
|
},
|
|
2740
3205
|
children: [
|
|
2741
|
-
/* @__PURE__ */ jsx(BrandHeader, {
|
|
3206
|
+
/* @__PURE__ */ jsx(BrandHeader, {
|
|
3207
|
+
height,
|
|
3208
|
+
width
|
|
3209
|
+
}),
|
|
2742
3210
|
/* @__PURE__ */ jsxs("box", {
|
|
2743
3211
|
style: {
|
|
2744
3212
|
flexDirection: "row",
|
|
@@ -2779,15 +3247,20 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
2779
3247
|
children: [marker, truncate("+ new conversation", rowWidth)]
|
|
2780
3248
|
}, "new");
|
|
2781
3249
|
const archived = isArchived(row.hit.item);
|
|
3250
|
+
const unread = row.hit.item.unread === true;
|
|
2782
3251
|
const { title, detail, tag } = fitRowText(singleLine(row.hit.item.title ?? "(untitled)"), `${archived ? "archived · " : ""}${previewLine(row.hit.item)}`, rowWidth, coAgentsTag(row.hit.item, agent.id));
|
|
3252
|
+
const titleText = /* @__PURE__ */ jsx(FuzzyText, {
|
|
3253
|
+
text: title,
|
|
3254
|
+
indexes: row.hit.item.title ? row.hit.highlights[0] : null
|
|
3255
|
+
});
|
|
2783
3256
|
return /* @__PURE__ */ jsxs("text", {
|
|
2784
3257
|
fg: archived && !selected ? theme.dim : fg,
|
|
2785
3258
|
children: [
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
}),
|
|
3259
|
+
!selected && unread ? /* @__PURE__ */ jsx("span", {
|
|
3260
|
+
fg: theme.accent,
|
|
3261
|
+
children: "● "
|
|
3262
|
+
}) : marker,
|
|
3263
|
+
unread ? /* @__PURE__ */ jsx("b", { children: titleText }) : titleText,
|
|
2791
3264
|
tag ? /* @__PURE__ */ jsxs("span", {
|
|
2792
3265
|
fg: theme.muted,
|
|
2793
3266
|
children: [" ", tag]
|
|
@@ -4173,7 +4646,10 @@ function parseMatches(output) {
|
|
|
4173
4646
|
//#region src/chat/tui/chat/conversation-context.tsx
|
|
4174
4647
|
const ConversationContext = createContext({
|
|
4175
4648
|
rest: null,
|
|
4176
|
-
conversationId: null
|
|
4649
|
+
conversationId: null,
|
|
4650
|
+
collectSandboxStats: null,
|
|
4651
|
+
latestComputerId: null,
|
|
4652
|
+
scrolledUp: false
|
|
4177
4653
|
});
|
|
4178
4654
|
function useConversationContext() {
|
|
4179
4655
|
return useContext(ConversationContext);
|
|
@@ -4493,6 +4969,329 @@ function ShellLocalItem({ item }) {
|
|
|
4493
4969
|
});
|
|
4494
4970
|
}
|
|
4495
4971
|
|
|
4972
|
+
//#endregion
|
|
4973
|
+
//#region src/chat/tui/chat/sandbox-stats.ts
|
|
4974
|
+
/**
|
|
4975
|
+
* `/computer`: a live snapshot of the agent's sandbox as a set of bar graphs.
|
|
4976
|
+
*
|
|
4977
|
+
* The CLI runs on the USER's machine, so it can't read the sandbox's `/proc`
|
|
4978
|
+
* directly — it runs one small, portable shell command over the sandbox-exec
|
|
4979
|
+
* stream (`SandboxStream`, mode `exec`) and parses the output here. Everything
|
|
4980
|
+
* in this module is pure so the parse + layout are unit-testable without a
|
|
4981
|
+
* sandbox or a renderer.
|
|
4982
|
+
*
|
|
4983
|
+
* The command reads `/proc` and `df` only (no tools that might be missing from
|
|
4984
|
+
* a minimal image), emitting a tagged, tab-separated block. The leading
|
|
4985
|
+
* `SKYDIVE_STATS_V1` sentinel lets the parser find the block even when a shell
|
|
4986
|
+
* rc file prints a banner first, and versions the wire shape.
|
|
4987
|
+
*/
|
|
4988
|
+
const SANDBOX_STATS_SENTINEL = "SKYDIVE_STATS_V1";
|
|
4989
|
+
/**
|
|
4990
|
+
* How often the live `/computer` card refreshes its snapshot, in ms.
|
|
4991
|
+
* Deliberately slow: the card is a background health readout, not a `top`, so
|
|
4992
|
+
* it refreshes quietly every 30s rather than flickering every second.
|
|
4993
|
+
*/
|
|
4994
|
+
const COMPUTER_POLL_INTERVAL_MS = 3e4;
|
|
4995
|
+
/**
|
|
4996
|
+
* Whether a `/computer` card should refresh right now. Pure so the gating is
|
|
4997
|
+
* unit-testable. A card refreshes only when it is the most recent `/computer`
|
|
4998
|
+
* in the transcript (`isLatest`), the session can reach the sandbox
|
|
4999
|
+
* (`canCollect`), and the transcript is at the bottom (`!scrolledUp`) — while
|
|
5000
|
+
* you're scrolled up reading something else it quietly stops (no wasted polls),
|
|
5001
|
+
* and resumes when you scroll back down. Older cards freeze regardless, so a
|
|
5002
|
+
* scrolled-up history of runs isn't a fleet of background pollers. The pause is
|
|
5003
|
+
* silent: nothing about it is rendered.
|
|
5004
|
+
*/
|
|
5005
|
+
function shouldPollComputer(opts) {
|
|
5006
|
+
return opts.isLatest && opts.canCollect && !opts.scrolledUp;
|
|
5007
|
+
}
|
|
5008
|
+
/**
|
|
5009
|
+
* The command run in the sandbox to collect the snapshot. `sh`-portable, reads
|
|
5010
|
+
* only `/proc` and `df`. Each metric is one `key<TAB>value` line so the parser
|
|
5011
|
+
* never has to positionally align anything.
|
|
5012
|
+
*/
|
|
5013
|
+
const SANDBOX_STATS_COMMAND = [
|
|
5014
|
+
`printf '${SANDBOX_STATS_SENTINEL}\\n'`,
|
|
5015
|
+
`printf 'nproc\\t%s\\n' "$(nproc 2>/dev/null || echo 1)"`,
|
|
5016
|
+
`printf 'loadavg\\t%s\\n' "$(cut -d' ' -f1-3 /proc/loadavg)"`,
|
|
5017
|
+
`printf 'uptime\\t%s\\n' "$(cut -d' ' -f1 /proc/uptime)"`,
|
|
5018
|
+
`awk '/^MemTotal:/{t=$2}/^MemAvailable:/{a=$2}END{printf "mem_total_kb\\t%d\\nmem_avail_kb\\t%d\\n",t,a}' /proc/meminfo`,
|
|
5019
|
+
`awk '/^SwapTotal:/{t=$2}/^SwapFree:/{f=$2}END{printf "swap_total_kb\\t%d\\nswap_free_kb\\t%d\\n",t,f}' /proc/meminfo`,
|
|
5020
|
+
`df -Pk / | awk 'NR==2{printf "disk_total_kb\\t%d\\ndisk_used_kb\\t%d\\n",$2,$3}'`,
|
|
5021
|
+
`printf 'procs\\t%s\\n' "$(ls -1 /proc 2>/dev/null | grep -c '^[0-9]\\+$')"`
|
|
5022
|
+
].join(" ; ");
|
|
5023
|
+
const KB = 1024;
|
|
5024
|
+
/**
|
|
5025
|
+
* Parse the collector's output into `SandboxStats`, or return null when the
|
|
5026
|
+
* block is absent or malformed (so the caller can show the raw output as an
|
|
5027
|
+
* error instead of a broken graph). Tolerant of extra lines before the
|
|
5028
|
+
* sentinel (shell banners) and of a missing swap section (swap totals default
|
|
5029
|
+
* to 0, which the renderer treats as "no swap").
|
|
5030
|
+
*/
|
|
5031
|
+
function parseSandboxStats(raw) {
|
|
5032
|
+
const start = raw.indexOf(SANDBOX_STATS_SENTINEL);
|
|
5033
|
+
if (start === -1) return null;
|
|
5034
|
+
const fields = /* @__PURE__ */ new Map();
|
|
5035
|
+
for (const line of raw.slice(start).split("\n")) {
|
|
5036
|
+
const tab = line.indexOf(" ");
|
|
5037
|
+
if (tab === -1) continue;
|
|
5038
|
+
fields.set(line.slice(0, tab).trim(), line.slice(tab + 1).trim());
|
|
5039
|
+
}
|
|
5040
|
+
const num = (key) => {
|
|
5041
|
+
const v = fields.get(key);
|
|
5042
|
+
if (v === void 0) return null;
|
|
5043
|
+
const n = Number(v);
|
|
5044
|
+
return Number.isFinite(n) ? n : null;
|
|
5045
|
+
};
|
|
5046
|
+
const load = (fields.get("loadavg") ?? "").split(/\s+/).map(Number).filter((n) => Number.isFinite(n));
|
|
5047
|
+
const nproc = num("nproc");
|
|
5048
|
+
const uptime = num("uptime");
|
|
5049
|
+
const memTotal = num("mem_total_kb");
|
|
5050
|
+
const memAvail = num("mem_avail_kb");
|
|
5051
|
+
const diskTotal = num("disk_total_kb");
|
|
5052
|
+
const diskUsed = num("disk_used_kb");
|
|
5053
|
+
const procs = num("procs");
|
|
5054
|
+
const [load1, load5, load15] = load;
|
|
5055
|
+
if (nproc === null || uptime === null || memTotal === null || memAvail === null || diskTotal === null || diskUsed === null || procs === null || load1 === void 0 || load5 === void 0 || load15 === void 0) return null;
|
|
5056
|
+
return {
|
|
5057
|
+
nproc: Math.max(1, Math.round(nproc)),
|
|
5058
|
+
loadavg: [
|
|
5059
|
+
load1,
|
|
5060
|
+
load5,
|
|
5061
|
+
load15
|
|
5062
|
+
],
|
|
5063
|
+
uptimeSeconds: uptime,
|
|
5064
|
+
memTotalBytes: memTotal * KB,
|
|
5065
|
+
memAvailBytes: memAvail * KB,
|
|
5066
|
+
swapTotalBytes: (num("swap_total_kb") ?? 0) * KB,
|
|
5067
|
+
swapFreeBytes: (num("swap_free_kb") ?? 0) * KB,
|
|
5068
|
+
diskTotalBytes: diskTotal * KB,
|
|
5069
|
+
diskUsedBytes: diskUsed * KB,
|
|
5070
|
+
procs: Math.round(procs)
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
function severityFor(fraction) {
|
|
5074
|
+
if (fraction >= .9) return "crit";
|
|
5075
|
+
if (fraction >= .75) return "warn";
|
|
5076
|
+
return "ok";
|
|
5077
|
+
}
|
|
5078
|
+
const GiB = 1024 * 1024 * 1024;
|
|
5079
|
+
function gb(bytes) {
|
|
5080
|
+
return (bytes / GiB).toFixed(1);
|
|
5081
|
+
}
|
|
5082
|
+
/** `120050` seconds -> `1d 9h 20m`. Always at least `0m`. */
|
|
5083
|
+
function formatUptime(seconds) {
|
|
5084
|
+
const total = Math.max(0, Math.floor(seconds));
|
|
5085
|
+
const d = Math.floor(total / 86400);
|
|
5086
|
+
const h = Math.floor(total % 86400 / 3600);
|
|
5087
|
+
const m = Math.floor(total % 3600 / 60);
|
|
5088
|
+
const parts = [];
|
|
5089
|
+
if (d > 0) parts.push(`${d}d`);
|
|
5090
|
+
if (h > 0 || d > 0) parts.push(`${h}h`);
|
|
5091
|
+
parts.push(`${m}m`);
|
|
5092
|
+
return parts.join(" ");
|
|
5093
|
+
}
|
|
5094
|
+
/**
|
|
5095
|
+
* Reduce a snapshot to the ordered list of gauges the `/computer` card draws:
|
|
5096
|
+
* CPU load (normalized to core count), memory, swap (only when the box has
|
|
5097
|
+
* any), and disk. Load can exceed 1.0 (oversubscribed CPU) — the bar clamps
|
|
5098
|
+
* but the caption shows the true number so that's still legible.
|
|
5099
|
+
*/
|
|
5100
|
+
function toGauges(stats) {
|
|
5101
|
+
const gauges = [];
|
|
5102
|
+
const load1 = stats.loadavg[0];
|
|
5103
|
+
const loadFraction = load1 / stats.nproc;
|
|
5104
|
+
gauges.push({
|
|
5105
|
+
label: "cpu load",
|
|
5106
|
+
fraction: loadFraction,
|
|
5107
|
+
detail: `${load1.toFixed(2)} / ${stats.nproc} cores`,
|
|
5108
|
+
severity: severityFor(loadFraction)
|
|
5109
|
+
});
|
|
5110
|
+
const memUsed = Math.max(0, stats.memTotalBytes - stats.memAvailBytes);
|
|
5111
|
+
const memFraction = stats.memTotalBytes > 0 ? memUsed / stats.memTotalBytes : 0;
|
|
5112
|
+
gauges.push({
|
|
5113
|
+
label: "memory",
|
|
5114
|
+
fraction: memFraction,
|
|
5115
|
+
detail: `${gb(memUsed)} / ${gb(stats.memTotalBytes)} GB`,
|
|
5116
|
+
severity: severityFor(memFraction)
|
|
5117
|
+
});
|
|
5118
|
+
if (stats.swapTotalBytes > 0) {
|
|
5119
|
+
const swapUsed = Math.max(0, stats.swapTotalBytes - stats.swapFreeBytes);
|
|
5120
|
+
const swapFraction = swapUsed / stats.swapTotalBytes;
|
|
5121
|
+
gauges.push({
|
|
5122
|
+
label: "swap",
|
|
5123
|
+
fraction: swapFraction,
|
|
5124
|
+
detail: `${gb(swapUsed)} / ${gb(stats.swapTotalBytes)} GB`,
|
|
5125
|
+
severity: severityFor(swapFraction)
|
|
5126
|
+
});
|
|
5127
|
+
}
|
|
5128
|
+
const diskFraction = stats.diskTotalBytes > 0 ? stats.diskUsedBytes / stats.diskTotalBytes : 0;
|
|
5129
|
+
gauges.push({
|
|
5130
|
+
label: "disk",
|
|
5131
|
+
fraction: diskFraction,
|
|
5132
|
+
detail: `${gb(stats.diskUsedBytes)} / ${gb(stats.diskTotalBytes)} GB`,
|
|
5133
|
+
severity: severityFor(diskFraction)
|
|
5134
|
+
});
|
|
5135
|
+
return gauges;
|
|
5136
|
+
}
|
|
5137
|
+
/**
|
|
5138
|
+
* A single-line horizontal bar of `width` cells filled to `fraction`
|
|
5139
|
+
* (clamped 0..1), using block glyphs. The last partial cell uses one of the
|
|
5140
|
+
* eighth-block glyphs so a 37%% bar reads differently from a 40%% one even in a
|
|
5141
|
+
* narrow width. Pure string, so the renderer just colors it.
|
|
5142
|
+
*/
|
|
5143
|
+
function renderBar(fraction, width) {
|
|
5144
|
+
const cells = Math.max(1, width);
|
|
5145
|
+
const exact = Math.min(1, Math.max(0, fraction)) * cells;
|
|
5146
|
+
const full = Math.floor(exact);
|
|
5147
|
+
const remainder = exact - full;
|
|
5148
|
+
const eighths = [
|
|
5149
|
+
"",
|
|
5150
|
+
"▏",
|
|
5151
|
+
"▎",
|
|
5152
|
+
"▍",
|
|
5153
|
+
"▌",
|
|
5154
|
+
"▋",
|
|
5155
|
+
"▊",
|
|
5156
|
+
"▉"
|
|
5157
|
+
];
|
|
5158
|
+
const partialIndex = Math.round(remainder * 8);
|
|
5159
|
+
const partial = full < cells ? eighths[partialIndex] : "";
|
|
5160
|
+
const filled = "█".repeat(Math.min(full, cells)) + partial;
|
|
5161
|
+
return (filled + "░".repeat(Math.max(0, cells - filled.length))).slice(0, cells);
|
|
5162
|
+
}
|
|
5163
|
+
|
|
5164
|
+
//#endregion
|
|
5165
|
+
//#region src/chat/tui/chunks/computer.tsx
|
|
5166
|
+
/**
|
|
5167
|
+
* Renders the `/computer` card: a snapshot of the agent's sandbox as a stack of
|
|
5168
|
+
* colored bar graphs (cpu load, memory, swap, disk) plus a footer of scalar
|
|
5169
|
+
* facts (uptime, process count).
|
|
5170
|
+
*
|
|
5171
|
+
* The card keeps itself current in place rather than making the user re-run the
|
|
5172
|
+
* command — but quietly: it refreshes on a slow interval with no "updating"
|
|
5173
|
+
* chrome, so the numbers just stay fresh without any flicker. Only the most
|
|
5174
|
+
* recent `/computer` card refreshes (older ones freeze at their last snapshot),
|
|
5175
|
+
* and the latest one silently stops while the transcript is scrolled up (no
|
|
5176
|
+
* wasted polls on a card you're not looking at) and resumes at the bottom — the
|
|
5177
|
+
* pause is never rendered. The refresh uses the same sandbox-exec relay
|
|
5178
|
+
* `/sandbox` uses, injected via context; the parse/layout math lives in
|
|
5179
|
+
* `sandbox-stats.ts` and this component only draws.
|
|
5180
|
+
*/
|
|
5181
|
+
function ComputerItem({ item }) {
|
|
5182
|
+
const { collectSandboxStats, latestComputerId, scrolledUp } = useConversationContext();
|
|
5183
|
+
const live = shouldPollComputer({
|
|
5184
|
+
isLatest: latestComputerId === item.id,
|
|
5185
|
+
canCollect: collectSandboxStats !== null,
|
|
5186
|
+
scrolledUp
|
|
5187
|
+
});
|
|
5188
|
+
const [stats, setStats] = useState(item.stats);
|
|
5189
|
+
const [error, setError] = useState(item.error);
|
|
5190
|
+
const hasStats = useRef(item.stats !== null);
|
|
5191
|
+
useEffect(() => {
|
|
5192
|
+
if (!live || !collectSandboxStats) return;
|
|
5193
|
+
let cancelled = false;
|
|
5194
|
+
let timer = null;
|
|
5195
|
+
let inFlight = null;
|
|
5196
|
+
const tick = async () => {
|
|
5197
|
+
const collection = collectSandboxStats();
|
|
5198
|
+
inFlight = collection;
|
|
5199
|
+
const result = await collection.result;
|
|
5200
|
+
inFlight = null;
|
|
5201
|
+
if (cancelled) return;
|
|
5202
|
+
if (result.ok) {
|
|
5203
|
+
hasStats.current = true;
|
|
5204
|
+
setStats(result.stats);
|
|
5205
|
+
setError(null);
|
|
5206
|
+
} else if (!("aborted" in result)) {
|
|
5207
|
+
if (!hasStats.current) setError(result.error);
|
|
5208
|
+
}
|
|
5209
|
+
if (!cancelled) timer = setTimeout(tick, COMPUTER_POLL_INTERVAL_MS);
|
|
5210
|
+
};
|
|
5211
|
+
tick();
|
|
5212
|
+
return () => {
|
|
5213
|
+
cancelled = true;
|
|
5214
|
+
if (timer !== null) clearTimeout(timer);
|
|
5215
|
+
inFlight?.abort();
|
|
5216
|
+
};
|
|
5217
|
+
}, [live, collectSandboxStats]);
|
|
5218
|
+
if (stats === null && error === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
|
|
5219
|
+
fg: theme.dim,
|
|
5220
|
+
children: "reading sandbox stats…"
|
|
5221
|
+
}) });
|
|
5222
|
+
if (stats === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsxs("text", {
|
|
5223
|
+
fg: theme.error,
|
|
5224
|
+
children: ["couldn't read sandbox stats", error ? `: ${error}` : ""]
|
|
5225
|
+
}) });
|
|
5226
|
+
const gauges = toGauges(stats);
|
|
5227
|
+
const labelWidth = Math.max(...gauges.map((g) => g.label.length));
|
|
5228
|
+
const barWidth = 24;
|
|
5229
|
+
return /* @__PURE__ */ jsxs(Shell, { children: [gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
|
|
5230
|
+
gauge: g,
|
|
5231
|
+
labelWidth,
|
|
5232
|
+
barWidth
|
|
5233
|
+
}, g.label)), /* @__PURE__ */ jsxs("text", {
|
|
5234
|
+
fg: theme.dim,
|
|
5235
|
+
children: [
|
|
5236
|
+
`${"uptime".padEnd(labelWidth)} `,
|
|
5237
|
+
/* @__PURE__ */ jsx("span", {
|
|
5238
|
+
fg: theme.muted,
|
|
5239
|
+
children: formatUptime(stats.uptimeSeconds)
|
|
5240
|
+
}),
|
|
5241
|
+
/* @__PURE__ */ jsx("span", {
|
|
5242
|
+
fg: theme.dim,
|
|
5243
|
+
children: ` · ${stats.procs} processes`
|
|
5244
|
+
})
|
|
5245
|
+
]
|
|
5246
|
+
})] });
|
|
5247
|
+
}
|
|
5248
|
+
/** The card frame: a bold `computer` title over the graph rows. */
|
|
5249
|
+
function Shell({ children }) {
|
|
5250
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
5251
|
+
style: { flexDirection: "column" },
|
|
5252
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
5253
|
+
fg: theme.muted,
|
|
5254
|
+
children: [/* @__PURE__ */ jsx("b", { children: "computer" }), /* @__PURE__ */ jsx("span", {
|
|
5255
|
+
fg: theme.dim,
|
|
5256
|
+
children: " · agent sandbox"
|
|
5257
|
+
})]
|
|
5258
|
+
}), children]
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
function severityColor(severity) {
|
|
5262
|
+
switch (severity) {
|
|
5263
|
+
case "ok": return theme.success;
|
|
5264
|
+
case "warn": return theme.warning;
|
|
5265
|
+
case "crit": return theme.error;
|
|
5266
|
+
default: {
|
|
5267
|
+
const exhaustive = severity;
|
|
5268
|
+
throw new Error(`unhandled severity: ${String(exhaustive)}`);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
function GaugeRow({ gauge, labelWidth, barWidth }) {
|
|
5273
|
+
const color = severityColor(gauge.severity);
|
|
5274
|
+
const pct = `${Math.round(Math.min(1, Math.max(0, gauge.fraction)) * 100)}%`;
|
|
5275
|
+
return /* @__PURE__ */ jsxs("text", { children: [
|
|
5276
|
+
/* @__PURE__ */ jsx("span", {
|
|
5277
|
+
fg: theme.muted,
|
|
5278
|
+
children: `${gauge.label.padEnd(labelWidth)} `
|
|
5279
|
+
}),
|
|
5280
|
+
/* @__PURE__ */ jsx("span", {
|
|
5281
|
+
fg: color,
|
|
5282
|
+
children: renderBar(gauge.fraction, barWidth)
|
|
5283
|
+
}),
|
|
5284
|
+
/* @__PURE__ */ jsx("span", {
|
|
5285
|
+
fg: color,
|
|
5286
|
+
children: ` ${pct.padStart(4)}`
|
|
5287
|
+
}),
|
|
5288
|
+
/* @__PURE__ */ jsx("span", {
|
|
5289
|
+
fg: theme.dim,
|
|
5290
|
+
children: ` ${gauge.detail}`
|
|
5291
|
+
})
|
|
5292
|
+
] });
|
|
5293
|
+
}
|
|
5294
|
+
|
|
4496
5295
|
//#endregion
|
|
4497
5296
|
//#region src/chat/tui/chunks/index.tsx
|
|
4498
5297
|
/**
|
|
@@ -4529,20 +5328,23 @@ function RenderItem({ item }) {
|
|
|
4529
5328
|
children: [item.text ? /* @__PURE__ */ jsx("text", {
|
|
4530
5329
|
fg: theme.user,
|
|
4531
5330
|
children: item.text
|
|
4532
|
-
}) : null, item.attachments
|
|
5331
|
+
}) : null, item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
|
|
4533
5332
|
fg: theme.muted,
|
|
4534
5333
|
children: ["⎘ ", item.attachments.join(" · ")]
|
|
4535
5334
|
}) : null]
|
|
4536
5335
|
});
|
|
4537
|
-
case "pending-steer": return /* @__PURE__ */
|
|
5336
|
+
case "pending-steer": return /* @__PURE__ */ jsxs(Row, {
|
|
4538
5337
|
barColor: theme.dim,
|
|
4539
|
-
children: /* @__PURE__ */ jsxs("text", {
|
|
5338
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
4540
5339
|
fg: theme.muted,
|
|
4541
5340
|
children: [item.text, /* @__PURE__ */ jsx("span", {
|
|
4542
5341
|
fg: theme.dim,
|
|
4543
5342
|
children: " · steering… (esc to cancel)"
|
|
4544
5343
|
})]
|
|
4545
|
-
})
|
|
5344
|
+
}), item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
|
|
5345
|
+
fg: theme.dim,
|
|
5346
|
+
children: ["⎘ ", item.attachments.join(" · ")]
|
|
5347
|
+
}) : null]
|
|
4546
5348
|
});
|
|
4547
5349
|
case "assistant-text":
|
|
4548
5350
|
if (item.done) return /* @__PURE__ */ jsx(Row, {
|
|
@@ -4588,6 +5390,10 @@ function RenderItem({ item }) {
|
|
|
4588
5390
|
})]
|
|
4589
5391
|
})
|
|
4590
5392
|
});
|
|
5393
|
+
case "computer": return /* @__PURE__ */ jsx(Row, {
|
|
5394
|
+
barColor: null,
|
|
5395
|
+
children: /* @__PURE__ */ jsx(ComputerItem, { item })
|
|
5396
|
+
});
|
|
4591
5397
|
case "recap": return /* @__PURE__ */ jsx(Row, {
|
|
4592
5398
|
barColor: null,
|
|
4593
5399
|
children: /* @__PURE__ */ jsxs("text", {
|
|
@@ -5277,7 +6083,8 @@ function applyChunk(items, chunk, agentName = null) {
|
|
|
5277
6083
|
return items.map((m) => m.kind === "pending-steer" && ids.has(m.id) ? {
|
|
5278
6084
|
kind: "user",
|
|
5279
6085
|
id: m.id,
|
|
5280
|
-
text: m.text
|
|
6086
|
+
text: m.text,
|
|
6087
|
+
attachments: m.attachments
|
|
5281
6088
|
} : m);
|
|
5282
6089
|
}
|
|
5283
6090
|
case "data-anyone-render-spec": {
|
|
@@ -5518,7 +6325,7 @@ function uiMessagesToItems(messages) {
|
|
|
5518
6325
|
kind: "user",
|
|
5519
6326
|
id: m.id,
|
|
5520
6327
|
text,
|
|
5521
|
-
|
|
6328
|
+
attachments
|
|
5522
6329
|
});
|
|
5523
6330
|
continue;
|
|
5524
6331
|
}
|
|
@@ -5969,6 +6776,65 @@ function formatShare(share) {
|
|
|
5969
6776
|
return share.grantedToAgent ? "shared (this agent can run commands here)" : "shared (this agent is not granted)";
|
|
5970
6777
|
}
|
|
5971
6778
|
|
|
6779
|
+
//#endregion
|
|
6780
|
+
//#region src/chat/sandbox/collect-stats.ts
|
|
6781
|
+
function collectSandboxStats(cfg) {
|
|
6782
|
+
const open = cfg.open ?? SandboxStream.open;
|
|
6783
|
+
const dec = new TextDecoder();
|
|
6784
|
+
let buffer = "";
|
|
6785
|
+
let settled = false;
|
|
6786
|
+
let stream = null;
|
|
6787
|
+
let resolveRef = null;
|
|
6788
|
+
return {
|
|
6789
|
+
result: new Promise((resolve) => {
|
|
6790
|
+
resolveRef = resolve;
|
|
6791
|
+
const settle = (r) => {
|
|
6792
|
+
if (settled) return;
|
|
6793
|
+
settled = true;
|
|
6794
|
+
resolve(r);
|
|
6795
|
+
};
|
|
6796
|
+
stream = open({
|
|
6797
|
+
mode: "exec",
|
|
6798
|
+
appUrl: cfg.appUrl,
|
|
6799
|
+
sessionToken: cfg.sessionToken,
|
|
6800
|
+
agentId: cfg.agentId,
|
|
6801
|
+
command: SANDBOX_STATS_COMMAND,
|
|
6802
|
+
onEvent: (e) => {
|
|
6803
|
+
if (e.type === "data") buffer += dec.decode(e.bytes);
|
|
6804
|
+
else if (e.type === "error") settle({
|
|
6805
|
+
ok: false,
|
|
6806
|
+
error: e.message
|
|
6807
|
+
});
|
|
6808
|
+
else if (e.type === "exit") {
|
|
6809
|
+
const stats = parseSandboxStats(buffer);
|
|
6810
|
+
if (stats) settle({
|
|
6811
|
+
ok: true,
|
|
6812
|
+
stats
|
|
6813
|
+
});
|
|
6814
|
+
else settle({
|
|
6815
|
+
ok: false,
|
|
6816
|
+
error: e.code === 0 ? "unexpected output from the sandbox" : `sandbox command exited ${e.code}`
|
|
6817
|
+
});
|
|
6818
|
+
} else if (e.type === "close") settle({
|
|
6819
|
+
ok: false,
|
|
6820
|
+
error: e.failure ?? "connection closed before the snapshot finished"
|
|
6821
|
+
});
|
|
6822
|
+
}
|
|
6823
|
+
});
|
|
6824
|
+
}),
|
|
6825
|
+
abort: () => {
|
|
6826
|
+
if (settled) return;
|
|
6827
|
+
settled = true;
|
|
6828
|
+
stream?.close();
|
|
6829
|
+
resolveRef?.({
|
|
6830
|
+
ok: false,
|
|
6831
|
+
aborted: true,
|
|
6832
|
+
error: "aborted"
|
|
6833
|
+
});
|
|
6834
|
+
}
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
|
|
5972
6838
|
//#endregion
|
|
5973
6839
|
//#region src/chat/notification-preview.ts
|
|
5974
6840
|
const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
|
|
@@ -6019,6 +6885,58 @@ function createResponsePreview() {
|
|
|
6019
6885
|
};
|
|
6020
6886
|
}
|
|
6021
6887
|
|
|
6888
|
+
//#endregion
|
|
6889
|
+
//#region src/chat/tui/new-chat-splash.tsx
|
|
6890
|
+
/** Rows the splash art needs before it's worth painting: the 6-row mark, a
|
|
6891
|
+
* spacer, the two identity/hint lines, and a row of breathing room. Below
|
|
6892
|
+
* this only the identity + hint render (small panes must keep every
|
|
6893
|
+
* transcript row). */
|
|
6894
|
+
const MARK_MIN_ROWS = 10;
|
|
6895
|
+
/** Columns the full mark + wordmark lockup spans (~68), with margin; below
|
|
6896
|
+
* this the pinwheel mark renders alone instead of wrapping the wordmark. */
|
|
6897
|
+
const FULL_MARK_MIN_COLS = 70;
|
|
6898
|
+
/**
|
|
6899
|
+
* Brand splash filling an empty conversation's transcript area: the Skydive
|
|
6900
|
+
* mark centered above the agent's identity and the "say hello" hint, with
|
|
6901
|
+
* the composer live below. The identity line matters here because nothing
|
|
6902
|
+
* else on this screen names the agent — a bare `skydive chat` boots straight
|
|
6903
|
+
* into this view via the configured default. Rendered only while the
|
|
6904
|
+
* transcript is empty, so the first submission replaces it with the regular
|
|
6905
|
+
* conversation view. Degrades by available space: full lockup → mark only →
|
|
6906
|
+
* identity + hint only.
|
|
6907
|
+
*/
|
|
6908
|
+
function NewChatSplash({ agentName, agentTitle, width, height }) {
|
|
6909
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6910
|
+
style: {
|
|
6911
|
+
height,
|
|
6912
|
+
flexDirection: "column",
|
|
6913
|
+
justifyContent: "center",
|
|
6914
|
+
alignItems: "center"
|
|
6915
|
+
},
|
|
6916
|
+
children: [
|
|
6917
|
+
height >= MARK_MIN_ROWS ? /* @__PURE__ */ jsx("box", {
|
|
6918
|
+
style: {
|
|
6919
|
+
flexDirection: "column",
|
|
6920
|
+
alignItems: "center",
|
|
6921
|
+
marginBottom: 1
|
|
6922
|
+
},
|
|
6923
|
+
children: /* @__PURE__ */ jsx(SkydiveMark, { variant: width >= FULL_MARK_MIN_COLS ? "full" : "mark" })
|
|
6924
|
+
}) : null,
|
|
6925
|
+
/* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
6926
|
+
fg: theme.accent,
|
|
6927
|
+
children: agentName
|
|
6928
|
+
}), agentTitle ? /* @__PURE__ */ jsxs("span", {
|
|
6929
|
+
fg: theme.muted,
|
|
6930
|
+
children: [" · ", agentTitle]
|
|
6931
|
+
}) : null] }),
|
|
6932
|
+
/* @__PURE__ */ jsx("text", {
|
|
6933
|
+
fg: theme.dim,
|
|
6934
|
+
children: "say hello below"
|
|
6935
|
+
})
|
|
6936
|
+
]
|
|
6937
|
+
});
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6022
6940
|
//#endregion
|
|
6023
6941
|
//#region src/chat/tui/prompt-history.ts
|
|
6024
6942
|
/**
|
|
@@ -6773,6 +7691,13 @@ const slashCommands = [
|
|
|
6773
7691
|
description: "show connection and session status",
|
|
6774
7692
|
action: { kind: "status" }
|
|
6775
7693
|
},
|
|
7694
|
+
{
|
|
7695
|
+
name: "computer",
|
|
7696
|
+
aliases: ["stats"],
|
|
7697
|
+
argsHint: null,
|
|
7698
|
+
description: "show the agent sandbox's cpu, memory, and disk",
|
|
7699
|
+
action: { kind: "computer" }
|
|
7700
|
+
},
|
|
6776
7701
|
{
|
|
6777
7702
|
name: "plan",
|
|
6778
7703
|
aliases: ["todos", "todo"],
|
|
@@ -9967,6 +10892,7 @@ function routeInput(raw) {
|
|
|
9967
10892
|
case "model-picker":
|
|
9968
10893
|
case "theme-picker":
|
|
9969
10894
|
case "status":
|
|
10895
|
+
case "computer":
|
|
9970
10896
|
case "share":
|
|
9971
10897
|
case "browser":
|
|
9972
10898
|
case "connect":
|
|
@@ -10034,47 +10960,6 @@ async function runPtySession(opts) {
|
|
|
10034
10960
|
}
|
|
10035
10961
|
}
|
|
10036
10962
|
|
|
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
10963
|
//#endregion
|
|
10079
10964
|
//#region src/chat/tui/screens/chat.tsx
|
|
10080
10965
|
/**
|
|
@@ -10159,11 +11044,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10159
11044
|
const portalClient = useStore((s) => s.portalClient);
|
|
10160
11045
|
const setPortalPrompt = useStore((s) => s.setPortalPrompt);
|
|
10161
11046
|
const declinedRef = useRef(/* @__PURE__ */ new Set());
|
|
11047
|
+
const grantInFlightRef = useRef(/* @__PURE__ */ new Set());
|
|
10162
11048
|
const grantPrompt = portal.prompt && portal.prompt.agentId === agent.id ? portal.prompt : null;
|
|
10163
11049
|
const isShared = portal.status !== "off";
|
|
10164
11050
|
const initialConversationId = isNewConversation(conversation) ? null : conversation.id;
|
|
10165
11051
|
const agentHost = useAgentHost();
|
|
10166
11052
|
const nonWebChannel = nonWebChannelLabel(conversation);
|
|
11053
|
+
const agentRunConversation = isAgentRunConversation(conversation);
|
|
10167
11054
|
const [conversationId, setConversationId] = useState(initialConversationId);
|
|
10168
11055
|
const { compacting, compactConversation, onCompactionEvent } = useCompaction({
|
|
10169
11056
|
rest,
|
|
@@ -10222,6 +11109,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10222
11109
|
const menuHighlightClamped = Math.min(menuHighlight, Math.max(0, menuCommands.length - 1));
|
|
10223
11110
|
const runRef = useRef(run);
|
|
10224
11111
|
runRef.current = run;
|
|
11112
|
+
const conversationIdRef = useRef(conversationId);
|
|
11113
|
+
conversationIdRef.current = conversationId;
|
|
10225
11114
|
const itemsRef = useRef(items);
|
|
10226
11115
|
itemsRef.current = items;
|
|
10227
11116
|
const attachmentsRef = useRef(attachments);
|
|
@@ -10271,13 +11160,16 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10271
11160
|
});
|
|
10272
11161
|
setScrolledUp(false);
|
|
10273
11162
|
}, []);
|
|
11163
|
+
const scrolledUpRef = useRef(false);
|
|
10274
11164
|
useEffect(() => {
|
|
10275
11165
|
const id = setInterval(() => {
|
|
10276
11166
|
const box = scrollRef.current;
|
|
10277
11167
|
if (!box) return;
|
|
10278
11168
|
const viewport = box.viewport.height;
|
|
10279
|
-
const
|
|
10280
|
-
|
|
11169
|
+
const next = !(box.scrollTop + viewport >= box.scrollHeight - 1);
|
|
11170
|
+
if (scrolledUpRef.current === next) return;
|
|
11171
|
+
scrolledUpRef.current = next;
|
|
11172
|
+
setScrolledUp(next);
|
|
10281
11173
|
}, 150);
|
|
10282
11174
|
return () => clearInterval(id);
|
|
10283
11175
|
}, []);
|
|
@@ -10358,10 +11250,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10358
11250
|
text: recapText
|
|
10359
11251
|
});
|
|
10360
11252
|
setItems(loaded);
|
|
11253
|
+
const toolUrlText = loaded.map((item) => item.kind === "tool" && item.output?.kind === "ok" ? stringifyToolOutput(item.output.value) : "").join("\n");
|
|
10361
11254
|
for (const item of loaded) {
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
11255
|
+
if (item.kind !== "assistant-text") continue;
|
|
11256
|
+
for (const pr of extractPullRequests(item.text, {
|
|
11257
|
+
requireCompleteNumber: false,
|
|
11258
|
+
urlOnlyText: toolUrlText
|
|
11259
|
+
})) {
|
|
10365
11260
|
if (reportedPrsRef.current.has(pr.key)) continue;
|
|
10366
11261
|
reportedPrsRef.current.set(pr.key, pr);
|
|
10367
11262
|
setPrLinks((prev) => [...prev, pr]);
|
|
@@ -10388,6 +11283,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10388
11283
|
initialConversationId,
|
|
10389
11284
|
agentHost
|
|
10390
11285
|
]);
|
|
11286
|
+
const markRead = useCallback(() => {
|
|
11287
|
+
const id = conversationIdRef.current;
|
|
11288
|
+
if (!rest || !id) return;
|
|
11289
|
+
rest.markConversationRead({ conversationId: id }).catch(() => {});
|
|
11290
|
+
}, [rest]);
|
|
11291
|
+
useEffect(() => {
|
|
11292
|
+
if (!conversationId) return;
|
|
11293
|
+
markRead();
|
|
11294
|
+
}, [conversationId, markRead]);
|
|
10391
11295
|
const attachToRun = useCallback((runId, runAgentName, runStartedAtMs) => {
|
|
10392
11296
|
if (!rest) return;
|
|
10393
11297
|
seenRunsRef.current.add(runId);
|
|
@@ -10400,6 +11304,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10400
11304
|
agentHost.addPullRequest(pr);
|
|
10401
11305
|
});
|
|
10402
11306
|
const streamAgentName = runAgentName ?? agent.name;
|
|
11307
|
+
let sawErrorChunk = false;
|
|
10403
11308
|
setRun((prev) => ({
|
|
10404
11309
|
kind: "streaming",
|
|
10405
11310
|
runId,
|
|
@@ -10411,6 +11316,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10411
11316
|
signal: abort.signal,
|
|
10412
11317
|
onEvent: (event) => {
|
|
10413
11318
|
if (event.kind === "chunk") {
|
|
11319
|
+
if (event.chunk.type === "error") sawErrorChunk = true;
|
|
10414
11320
|
responsePreview.addChunk(event.chunk);
|
|
10415
11321
|
prScanner.addChunk(event.chunk);
|
|
10416
11322
|
if (event.chunk.type === "data-anyone-todos-updated") {
|
|
@@ -10428,7 +11334,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10428
11334
|
id: crypto.randomUUID(),
|
|
10429
11335
|
text: billingOutcome.message
|
|
10430
11336
|
}]);
|
|
10431
|
-
else if (event.status !== "ok" && errorText) setItems((prev) => [...prev, {
|
|
11337
|
+
else if (event.status !== "ok" && errorText && !sawErrorChunk) setItems((prev) => [...prev, {
|
|
10432
11338
|
kind: "error",
|
|
10433
11339
|
id: crypto.randomUUID(),
|
|
10434
11340
|
text: errorText
|
|
@@ -10436,9 +11342,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10436
11342
|
setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
|
|
10437
11343
|
kind: "user",
|
|
10438
11344
|
id: m.id,
|
|
10439
|
-
text: m.text
|
|
11345
|
+
text: m.text,
|
|
11346
|
+
attachments: m.attachments
|
|
10440
11347
|
} : m));
|
|
10441
11348
|
setRun({ kind: "idle" });
|
|
11349
|
+
markRead();
|
|
10442
11350
|
agentHost.transition({
|
|
10443
11351
|
state: "idle",
|
|
10444
11352
|
notification: {
|
|
@@ -10460,7 +11368,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10460
11368
|
}, [
|
|
10461
11369
|
rest,
|
|
10462
11370
|
agent.name,
|
|
10463
|
-
agentHost
|
|
11371
|
+
agentHost,
|
|
11372
|
+
markRead
|
|
10464
11373
|
]);
|
|
10465
11374
|
const multiAgent = useMemo(() => conversationAgentNames(items, agent.name), [items, agent.name]).size > 1;
|
|
10466
11375
|
const transcriptWindow = useMemo(() => windowItems(items), [items]);
|
|
@@ -10513,7 +11422,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10513
11422
|
kind: "user",
|
|
10514
11423
|
id: optimisticId,
|
|
10515
11424
|
text: trimmed,
|
|
10516
|
-
|
|
11425
|
+
attachments: stagedNames
|
|
10517
11426
|
}]);
|
|
10518
11427
|
const wasStreaming = runRef.current.kind === "streaming";
|
|
10519
11428
|
if (!wasStreaming) setRun({
|
|
@@ -10562,7 +11471,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10562
11471
|
if (directiveId) setItems((prev) => prev.map((m) => m.id === optimisticId ? {
|
|
10563
11472
|
kind: "pending-steer",
|
|
10564
11473
|
id: directiveId,
|
|
10565
|
-
text: trimmed
|
|
11474
|
+
text: trimmed,
|
|
11475
|
+
attachments: m.kind === "user" || m.kind === "pending-steer" ? m.attachments : []
|
|
10566
11476
|
} : m));
|
|
10567
11477
|
const current = runRef.current;
|
|
10568
11478
|
if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId, result.agentName ?? null);
|
|
@@ -10609,12 +11519,14 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10609
11519
|
});
|
|
10610
11520
|
(async () => {
|
|
10611
11521
|
if (autoGrantMachine && portalClient && portalStatus === "connected" && !portalClient.isGranted(agent.id)) try {
|
|
10612
|
-
|
|
11522
|
+
grantInFlightRef.current.add(agent.id);
|
|
11523
|
+
await portalClient.grantAgent(agent.id, null);
|
|
10613
11524
|
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}`,
|
|
11525
|
+
message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent "${agent.name}"`,
|
|
10615
11526
|
durationMs: 1e4
|
|
10616
11527
|
});
|
|
10617
11528
|
} catch (err) {
|
|
11529
|
+
grantInFlightRef.current.delete(agent.id);
|
|
10618
11530
|
useStore.getState().showToast({
|
|
10619
11531
|
message: `Couldn't grant machine access automatically (${errorMessage(err)}). The agent will ask in chat instead.`,
|
|
10620
11532
|
variant: "warning"
|
|
@@ -10678,9 +11590,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10678
11590
|
if (!portalClient) throw new Error("portal is unavailable in this session");
|
|
10679
11591
|
const justEnabled = portal.status === "off";
|
|
10680
11592
|
if (justEnabled) portalClient.enable();
|
|
11593
|
+
grantInFlightRef.current.add(action.agentId);
|
|
11594
|
+
if (useStore.getState().portal.prompt?.agentId === action.agentId) setPortalPrompt(null);
|
|
10681
11595
|
try {
|
|
10682
|
-
await portalClient.grantAgent(action.agentId);
|
|
11596
|
+
await portalClient.grantAgent(action.agentId, action.conversationId);
|
|
10683
11597
|
} catch (grantErr) {
|
|
11598
|
+
grantInFlightRef.current.delete(action.agentId);
|
|
10684
11599
|
if (justEnabled) portalClient.disable();
|
|
10685
11600
|
throw grantErr;
|
|
10686
11601
|
}
|
|
@@ -10741,7 +11656,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10741
11656
|
appUrl,
|
|
10742
11657
|
updateCard,
|
|
10743
11658
|
portalClient,
|
|
10744
|
-
portal.status
|
|
11659
|
+
portal.status,
|
|
11660
|
+
setPortalPrompt
|
|
10745
11661
|
]);
|
|
10746
11662
|
const promptForCards = useCallback(() => {
|
|
10747
11663
|
const targets = itemsRef.current.filter(isActionableCard);
|
|
@@ -10905,6 +11821,16 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10905
11821
|
sessionToken,
|
|
10906
11822
|
agent.id
|
|
10907
11823
|
]);
|
|
11824
|
+
const runComputerStats = useCallback(() => {
|
|
11825
|
+
if (!appUrl || !sessionToken) return;
|
|
11826
|
+
setItems((prev) => [...prev, {
|
|
11827
|
+
kind: "computer",
|
|
11828
|
+
id: crypto.randomUUID(),
|
|
11829
|
+
state: "loading",
|
|
11830
|
+
stats: null,
|
|
11831
|
+
error: null
|
|
11832
|
+
}]);
|
|
11833
|
+
}, [appUrl, sessionToken]);
|
|
10908
11834
|
const runSandboxPty = useCallback(async () => {
|
|
10909
11835
|
if (!appUrl || !sessionToken) return;
|
|
10910
11836
|
const result = await runPtySession({
|
|
@@ -11066,7 +11992,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11066
11992
|
setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
|
|
11067
11993
|
kind: "user",
|
|
11068
11994
|
id: m.id,
|
|
11069
|
-
text: m.text
|
|
11995
|
+
text: m.text,
|
|
11996
|
+
attachments: m.attachments
|
|
11070
11997
|
} : m));
|
|
11071
11998
|
useStore.getState().showToast({ message: "run cancelled" });
|
|
11072
11999
|
}, [rest]);
|
|
@@ -11135,6 +12062,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11135
12062
|
]);
|
|
11136
12063
|
const archiveConversation = useCallback(() => {
|
|
11137
12064
|
if (grantPrompt || credPrompt) return;
|
|
12065
|
+
if (agentRunConversation) {
|
|
12066
|
+
useStore.getState().showToast({ message: "agent runs can't be archived — ctrl+d in the picker deletes" });
|
|
12067
|
+
return;
|
|
12068
|
+
}
|
|
11138
12069
|
if (runRef.current.kind !== "idle") {
|
|
11139
12070
|
useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
|
|
11140
12071
|
return;
|
|
@@ -11163,6 +12094,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11163
12094
|
}, [
|
|
11164
12095
|
grantPrompt,
|
|
11165
12096
|
credPrompt,
|
|
12097
|
+
agentRunConversation,
|
|
11166
12098
|
conversationId,
|
|
11167
12099
|
rest,
|
|
11168
12100
|
goTo,
|
|
@@ -11244,7 +12176,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11244
12176
|
rest,
|
|
11245
12177
|
agentSelector: null,
|
|
11246
12178
|
conversationId: target,
|
|
11247
|
-
newConversation: false
|
|
12179
|
+
newConversation: false,
|
|
12180
|
+
defaultAgentSelector: null,
|
|
12181
|
+
forceOnboarding: false
|
|
11248
12182
|
}));
|
|
11249
12183
|
} catch (err) {
|
|
11250
12184
|
setItems((prev) => [...prev, {
|
|
@@ -11283,19 +12217,24 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11283
12217
|
if (!portalClient) return;
|
|
11284
12218
|
const agentName = agent.name;
|
|
11285
12219
|
setPortalPrompt(null);
|
|
11286
|
-
|
|
12220
|
+
grantInFlightRef.current.add(agent.id);
|
|
12221
|
+
portalClient.grantAgent(agent.id, conversationId).then(() => useStore.getState().showToast({
|
|
11287
12222
|
variant: "success",
|
|
11288
12223
|
message: `shared this machine with ${agentName}`
|
|
11289
|
-
})).catch((err) =>
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
12224
|
+
})).catch((err) => {
|
|
12225
|
+
grantInFlightRef.current.delete(agent.id);
|
|
12226
|
+
setItems((prev) => [...prev, {
|
|
12227
|
+
kind: "error",
|
|
12228
|
+
id: crypto.randomUUID(),
|
|
12229
|
+
text: `couldn't share machine: ${errorDetail(err)}`
|
|
12230
|
+
}]);
|
|
12231
|
+
});
|
|
11294
12232
|
}, [
|
|
11295
12233
|
portalClient,
|
|
11296
12234
|
agent.id,
|
|
11297
12235
|
agent.name,
|
|
11298
|
-
setPortalPrompt
|
|
12236
|
+
setPortalPrompt,
|
|
12237
|
+
conversationId
|
|
11299
12238
|
]);
|
|
11300
12239
|
const declineGrant = useCallback(() => {
|
|
11301
12240
|
declinedRef.current.add(agent.id);
|
|
@@ -11438,6 +12377,9 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11438
12377
|
})
|
|
11439
12378
|
}]);
|
|
11440
12379
|
break;
|
|
12380
|
+
case "computer":
|
|
12381
|
+
runComputerStats();
|
|
12382
|
+
break;
|
|
11441
12383
|
case "plan": {
|
|
11442
12384
|
if (!(!!todos && todos.length > 0)) {
|
|
11443
12385
|
useStore.getState().showToast({ message: "no plan to show yet" });
|
|
@@ -11490,6 +12432,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11490
12432
|
history,
|
|
11491
12433
|
runLocalShell,
|
|
11492
12434
|
runSandboxExec,
|
|
12435
|
+
runComputerStats,
|
|
11493
12436
|
runSandboxPty,
|
|
11494
12437
|
archiveConversation,
|
|
11495
12438
|
renameConversation,
|
|
@@ -11535,6 +12478,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11535
12478
|
if (portal.grantedAgentIds.includes(agent.id)) return;
|
|
11536
12479
|
if (portal.declinedAgentIds.includes(agent.id)) return;
|
|
11537
12480
|
if (declinedRef.current.has(agent.id)) return;
|
|
12481
|
+
if (grantInFlightRef.current.has(agent.id)) return;
|
|
11538
12482
|
if (portal.prompt?.agentId === agent.id) return;
|
|
11539
12483
|
setPortalPrompt({
|
|
11540
12484
|
agentId: agent.id,
|
|
@@ -11558,6 +12502,14 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11558
12502
|
setPortalPrompt,
|
|
11559
12503
|
agentHost
|
|
11560
12504
|
]);
|
|
12505
|
+
useEffect(() => {
|
|
12506
|
+
for (const agentId of portal.grantedAgentIds) grantInFlightRef.current.delete(agentId);
|
|
12507
|
+
if (portal.prompt && portal.grantedAgentIds.includes(portal.prompt.agentId)) setPortalPrompt(null);
|
|
12508
|
+
}, [
|
|
12509
|
+
portal.grantedAgentIds,
|
|
12510
|
+
portal.prompt,
|
|
12511
|
+
setPortalPrompt
|
|
12512
|
+
]);
|
|
11561
12513
|
useKeyboard((key) => {
|
|
11562
12514
|
if (reviewOpen) {
|
|
11563
12515
|
if (key.name === "g" && key.ctrl) {
|
|
@@ -11747,10 +12699,38 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11747
12699
|
}
|
|
11748
12700
|
});
|
|
11749
12701
|
const hasPendingSteer = items.some((m) => m.kind === "pending-steer");
|
|
12702
|
+
const latestComputerId = useMemo(() => {
|
|
12703
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
12704
|
+
const item = items[i];
|
|
12705
|
+
if (item && item.kind === "computer") return item.id;
|
|
12706
|
+
}
|
|
12707
|
+
return null;
|
|
12708
|
+
}, [items]);
|
|
12709
|
+
const boundCollectSandboxStats = useMemo(() => {
|
|
12710
|
+
if (!appUrl || !sessionToken) return null;
|
|
12711
|
+
return () => collectSandboxStats({
|
|
12712
|
+
appUrl,
|
|
12713
|
+
sessionToken,
|
|
12714
|
+
agentId: agent.id
|
|
12715
|
+
});
|
|
12716
|
+
}, [
|
|
12717
|
+
appUrl,
|
|
12718
|
+
sessionToken,
|
|
12719
|
+
agent.id
|
|
12720
|
+
]);
|
|
11750
12721
|
const conversationContext = useMemo(() => ({
|
|
11751
12722
|
rest,
|
|
11752
|
-
conversationId
|
|
11753
|
-
|
|
12723
|
+
conversationId,
|
|
12724
|
+
collectSandboxStats: boundCollectSandboxStats,
|
|
12725
|
+
latestComputerId,
|
|
12726
|
+
scrolledUp
|
|
12727
|
+
}), [
|
|
12728
|
+
rest,
|
|
12729
|
+
conversationId,
|
|
12730
|
+
boundCollectSandboxStats,
|
|
12731
|
+
latestComputerId,
|
|
12732
|
+
scrolledUp
|
|
12733
|
+
]);
|
|
11754
12734
|
const actionableCards = useMemo(() => items.filter(isActionableCard), [items]);
|
|
11755
12735
|
const statusText = useMemo(() => formatChatFooterStatus({
|
|
11756
12736
|
runKind: run.kind,
|
|
@@ -11899,13 +12879,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11899
12879
|
children: [!historyLoaded ? /* @__PURE__ */ jsx("text", {
|
|
11900
12880
|
fg: theme.muted,
|
|
11901
12881
|
children: "loading history…"
|
|
11902
|
-
}) : items.length === 0 ? /* @__PURE__ */
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
" below."
|
|
11908
|
-
]
|
|
12882
|
+
}) : items.length === 0 ? /* @__PURE__ */ jsx(NewChatSplash, {
|
|
12883
|
+
agentName: agent.name,
|
|
12884
|
+
agentTitle: agent.title ?? null,
|
|
12885
|
+
width: reviewView && reviewPlacement === "side" ? Math.max(1, bodyWidth - reviewWidth - 1) : bodyWidth,
|
|
12886
|
+
height: Math.max(1, scrollHeight - 1)
|
|
11909
12887
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [transcriptWindow.hiddenCount > 0 ? /* @__PURE__ */ jsx("box", {
|
|
11910
12888
|
style: {
|
|
11911
12889
|
paddingLeft: 1,
|
|
@@ -12359,9 +13337,12 @@ function ToastView() {
|
|
|
12359
13337
|
|
|
12360
13338
|
//#endregion
|
|
12361
13339
|
//#region src/chat/tui/app.tsx
|
|
12362
|
-
function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation }) {
|
|
13340
|
+
function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
|
|
12363
13341
|
const renderer = useRenderer();
|
|
12364
13342
|
const screen = useStore((s) => s.screen);
|
|
13343
|
+
useEffect(() => {
|
|
13344
|
+
if (screen.kind === "chat") recordDefaultAgent(screen.agent.id);
|
|
13345
|
+
}, [screen]);
|
|
12365
13346
|
const chatTitle = useStore((s) => s.chatTitle);
|
|
12366
13347
|
useStore((s) => s.themeId);
|
|
12367
13348
|
const rest = useStore((s) => s.rest);
|
|
@@ -12444,7 +13425,9 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12444
13425
|
rest,
|
|
12445
13426
|
agentSelector,
|
|
12446
13427
|
conversationId,
|
|
12447
|
-
newConversation: newConversation || useStore.getState().seedPrompt !== null
|
|
13428
|
+
newConversation: newConversation || useStore.getState().seedPrompt !== null,
|
|
13429
|
+
defaultAgentSelector,
|
|
13430
|
+
forceOnboarding
|
|
12448
13431
|
});
|
|
12449
13432
|
if (cancelled) return;
|
|
12450
13433
|
goTo(screen);
|
|
@@ -12467,6 +13450,8 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12467
13450
|
agentSelector,
|
|
12468
13451
|
conversationId,
|
|
12469
13452
|
newConversation,
|
|
13453
|
+
defaultAgentSelector,
|
|
13454
|
+
forceOnboarding,
|
|
12470
13455
|
goTo,
|
|
12471
13456
|
setClients,
|
|
12472
13457
|
fail
|
|
@@ -12507,6 +13492,7 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
|
|
|
12507
13492
|
screen.kind === "error" && /* @__PURE__ */ jsx(ErrorScreen, { message: screen.message }),
|
|
12508
13493
|
screen.kind === "agent-picker" && rest && /* @__PURE__ */ jsx(AgentPickerScreen, {}),
|
|
12509
13494
|
screen.kind === "agent-create" && rest && /* @__PURE__ */ jsx(AgentCreateScreen, {}),
|
|
13495
|
+
screen.kind === "first-run" && rest && /* @__PURE__ */ jsx(FirstRunScreen, {}),
|
|
12510
13496
|
screen.kind === "workspace-picker" && /* @__PURE__ */ jsx(WorkspacePicker, {
|
|
12511
13497
|
appUrl,
|
|
12512
13498
|
sessionToken,
|
|
@@ -13045,7 +14031,7 @@ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, comm
|
|
|
13045
14031
|
* module — and the heavy OpenTUI dependency graph it pulls in — is
|
|
13046
14032
|
* dynamically imported and never loaded on the Node management path.
|
|
13047
14033
|
*/
|
|
13048
|
-
async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, seedPrompt }) {
|
|
14034
|
+
async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding, seedPrompt }) {
|
|
13049
14035
|
await registerGrammars();
|
|
13050
14036
|
const watchdog = startMemoryWatchdog();
|
|
13051
14037
|
installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
|
|
@@ -13060,7 +14046,8 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
13060
14046
|
authKind,
|
|
13061
14047
|
seedPrompt: seedPrompt ?? null,
|
|
13062
14048
|
autoGrantMachine: Boolean(seedPrompt && shareMachine),
|
|
13063
|
-
newConversationIntent: newConversation
|
|
14049
|
+
newConversationIntent: newConversation,
|
|
14050
|
+
forcedOnboarding: forceOnboarding
|
|
13064
14051
|
});
|
|
13065
14052
|
const store = useStore.getState();
|
|
13066
14053
|
if (noColorRequested()) {
|
|
@@ -13086,7 +14073,9 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
|
|
|
13086
14073
|
notifications,
|
|
13087
14074
|
agentSelector: agentSelector ?? null,
|
|
13088
14075
|
conversationId: conversationId ?? null,
|
|
13089
|
-
newConversation
|
|
14076
|
+
newConversation,
|
|
14077
|
+
defaultAgentSelector: defaultAgentSelector ?? null,
|
|
14078
|
+
forceOnboarding
|
|
13090
14079
|
})
|
|
13091
14080
|
}));
|
|
13092
14081
|
}
|