skydive-cli 0.1.0-beta.340 → 0.1.0-beta.342
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/dist/js/bin.mjs +15 -9
- package/dist/js/{boot-BQZrbJAh.mjs → boot-BcuOicbb.mjs} +242 -125
- package/package.json +1 -1
package/dist/js/bin.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import zlib from "node:zlib";
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
|
|
18
18
|
//#region package.json
|
|
19
|
-
var version$1 = "0.1.0-beta.
|
|
19
|
+
var version$1 = "0.1.0-beta.342";
|
|
20
20
|
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/types.ts
|
|
@@ -1171,11 +1171,14 @@ const conversationSummarySchema = z.object({
|
|
|
1171
1171
|
title: z.string().nullable().optional()
|
|
1172
1172
|
})
|
|
1173
1173
|
});
|
|
1174
|
-
const
|
|
1174
|
+
const conversationDetailSchema = z.object({
|
|
1175
1175
|
id: z.string().uuid(),
|
|
1176
|
-
title: z.string().nullable()
|
|
1176
|
+
title: z.string().nullable(),
|
|
1177
|
+
agentId: z.string().uuid(),
|
|
1178
|
+
createdAt: z.string(),
|
|
1179
|
+
updatedAt: z.string()
|
|
1177
1180
|
});
|
|
1178
|
-
const getConversationResponseSchema = z.object({ conversation:
|
|
1181
|
+
const getConversationResponseSchema = z.object({ conversation: conversationDetailSchema });
|
|
1179
1182
|
const listConversationsResponseSchema = z.object({
|
|
1180
1183
|
conversations: z.array(conversationSummarySchema),
|
|
1181
1184
|
nextCursor: z.string().nullable().optional(),
|
|
@@ -2570,8 +2573,9 @@ const chatCommand = {
|
|
|
2570
2573
|
type: "string",
|
|
2571
2574
|
describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
|
|
2572
2575
|
}).option("conversation", {
|
|
2576
|
+
alias: "resume",
|
|
2573
2577
|
type: "string",
|
|
2574
|
-
describe: "
|
|
2578
|
+
describe: "Continue an existing conversation by id. With -p, the one-shot lands in that conversation; without -p, the TUI opens directly into it (the agent is resolved from the conversation, no --agent needed)."
|
|
2575
2579
|
}).option("share-machine", {
|
|
2576
2580
|
type: "boolean",
|
|
2577
2581
|
describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run). Defaults to `shareMachineDefault` in the CLI config file; --no-share-machine disables for this invocation."
|
|
@@ -2582,7 +2586,7 @@ const chatCommand = {
|
|
|
2582
2586
|
type: "boolean",
|
|
2583
2587
|
default: true,
|
|
2584
2588
|
describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
|
|
2585
|
-
}).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
|
|
2589
|
+
}).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)").example("skydive chat --resume 0197e0f3-…", "Reopen a previous conversation in the TUI (the id is printed when you quit a chat)"),
|
|
2586
2590
|
handler: async (argv) => {
|
|
2587
2591
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2588
2592
|
if (argv.print !== void 0) {
|
|
@@ -2618,7 +2622,7 @@ const chatCommand = {
|
|
|
2618
2622
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2619
2623
|
process.exit(1);
|
|
2620
2624
|
}
|
|
2621
|
-
const { runChat } = await import("./boot-
|
|
2625
|
+
const { runChat } = await import("./boot-BcuOicbb.mjs");
|
|
2622
2626
|
await runChat({
|
|
2623
2627
|
appUrl,
|
|
2624
2628
|
sessionToken: session.value.sessionToken,
|
|
@@ -2626,7 +2630,8 @@ const chatCommand = {
|
|
|
2626
2630
|
promptHistoryPath: getPromptHistoryPath(),
|
|
2627
2631
|
theme: themeId,
|
|
2628
2632
|
notifications: argv.notify,
|
|
2629
|
-
agentSelector: argv.agent ?? null
|
|
2633
|
+
agentSelector: argv.agent ?? null,
|
|
2634
|
+
conversationId: argv.conversation ?? null
|
|
2630
2635
|
});
|
|
2631
2636
|
}
|
|
2632
2637
|
};
|
|
@@ -3411,7 +3416,7 @@ const switchCommand = {
|
|
|
3411
3416
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
3412
3417
|
process.exit(1);
|
|
3413
3418
|
}
|
|
3414
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3419
|
+
const { runWorkspacePicker } = await import("./boot-BcuOicbb.mjs");
|
|
3415
3420
|
await runWorkspacePicker(session);
|
|
3416
3421
|
return;
|
|
3417
3422
|
}
|
|
@@ -3795,6 +3800,7 @@ const VALUE_FLAGS = new Set([
|
|
|
3795
3800
|
"-p",
|
|
3796
3801
|
"--agent",
|
|
3797
3802
|
"--conversation",
|
|
3803
|
+
"--resume",
|
|
3798
3804
|
"--theme",
|
|
3799
3805
|
"--api-url"
|
|
3800
3806
|
]);
|
|
@@ -137,18 +137,56 @@ const useStore = create((set, get) => ({
|
|
|
137
137
|
/**
|
|
138
138
|
* Decide the screen the chat TUI boots into.
|
|
139
139
|
*
|
|
140
|
-
* With no
|
|
141
|
-
*
|
|
140
|
+
* With no selector we open the agent picker, exactly as before.
|
|
141
|
+
*
|
|
142
|
+
* With `--agent` (`skydive chat --agent grace`, no `-p`) we resolve it
|
|
142
143
|
* against the org roster and open that agent's conversation list, skipping
|
|
143
144
|
* the picker. Resolution reuses the same `resolveAgent` rules as the `-p`
|
|
144
145
|
* path so a given selector behaves identically in both modes: an id, or a
|
|
145
146
|
* unique case-insensitive slug/name; ambiguous or unknown selectors throw
|
|
146
147
|
* with the candidate list.
|
|
147
148
|
*
|
|
149
|
+
* With `--resume <conversation-id>` we skip both pickers and boot straight
|
|
150
|
+
* into that conversation. The id alone is enough — the owning agent is
|
|
151
|
+
* recovered from the conversation itself — so the "Resume this conversation
|
|
152
|
+
* with: …" hint printed on exit round-trips without the user also passing
|
|
153
|
+
* `--agent`. When both are given, the conversation wins (it pins the agent
|
|
154
|
+
* anyway).
|
|
155
|
+
*
|
|
148
156
|
* Kept out of the React component so it's unit-testable under Node (the
|
|
149
157
|
* component itself can't render without Bun + OpenTUI).
|
|
150
158
|
*/
|
|
151
|
-
async function resolveInitialScreen({ rest, agentSelector }) {
|
|
159
|
+
async function resolveInitialScreen({ rest, agentSelector, conversationId }) {
|
|
160
|
+
if (conversationId) {
|
|
161
|
+
const [conversation, agents] = await Promise.all([rest.getConversation({ conversationId }).catch((err) => {
|
|
162
|
+
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.`);
|
|
163
|
+
throw err;
|
|
164
|
+
}), rest.listAgents({
|
|
165
|
+
scope: "org",
|
|
166
|
+
onPage: null
|
|
167
|
+
})]);
|
|
168
|
+
const agent = agents.find((a) => a.id === conversation.agentId);
|
|
169
|
+
if (!agent) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
|
|
170
|
+
return {
|
|
171
|
+
kind: "chat",
|
|
172
|
+
agent,
|
|
173
|
+
conversation: {
|
|
174
|
+
id: conversation.id,
|
|
175
|
+
title: conversation.title,
|
|
176
|
+
createdAt: conversation.createdAt,
|
|
177
|
+
updatedAt: conversation.updatedAt,
|
|
178
|
+
preview: null,
|
|
179
|
+
channel: null,
|
|
180
|
+
channelLabel: null,
|
|
181
|
+
agent: {
|
|
182
|
+
id: agent.id,
|
|
183
|
+
name: agent.name,
|
|
184
|
+
slug: agent.slug ?? null,
|
|
185
|
+
title: agent.title ?? null
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
152
190
|
if (!agentSelector) return { kind: "agent-picker" };
|
|
153
191
|
return {
|
|
154
192
|
kind: "conversation-picker",
|
|
@@ -1329,6 +1367,132 @@ function WorkspacePicker({ appUrl, sessionToken, onSelect, onCancel }) {
|
|
|
1329
1367
|
});
|
|
1330
1368
|
}
|
|
1331
1369
|
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region src/chat/tui/chunks/frame.tsx
|
|
1372
|
+
/** Status glyph + color for a tool's lifecycle state. */
|
|
1373
|
+
function toolGlyph(state) {
|
|
1374
|
+
switch (state) {
|
|
1375
|
+
case "streaming":
|
|
1376
|
+
case "input-ready": return {
|
|
1377
|
+
ch: "◐",
|
|
1378
|
+
color: theme.warning
|
|
1379
|
+
};
|
|
1380
|
+
case "output-ready": return {
|
|
1381
|
+
ch: "✓",
|
|
1382
|
+
color: theme.success
|
|
1383
|
+
};
|
|
1384
|
+
case "error": return {
|
|
1385
|
+
ch: "✗",
|
|
1386
|
+
color: theme.error
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/** Truncate a single line with an ellipsis. */
|
|
1391
|
+
function truncate(line, maxLen) {
|
|
1392
|
+
return line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Compact a block of tool output for the transcript: truncate over-long
|
|
1396
|
+
* lines (so a giant single-line JSON blob doesn't wrap to a wall) and cap
|
|
1397
|
+
* the line count, with a "+N more" tail that doubles as the click-to-expand
|
|
1398
|
+
* affordance. Expanded blocks render through `ExpandedLines` instead —
|
|
1399
|
+
* full content, word-wrapped, nothing clipped.
|
|
1400
|
+
*/
|
|
1401
|
+
function clipLines(text, { maxLines, maxLineLen }) {
|
|
1402
|
+
const lines = text.replace(/\t/g, " ").split("\n");
|
|
1403
|
+
const out = lines.slice(0, maxLines).map((line) => truncate(line, maxLineLen));
|
|
1404
|
+
if (lines.length > maxLines) out.push(`… +${lines.length - maxLines} more lines · click to expand`);
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Split a block for the expanded view: tabs normalised, no line cap, no
|
|
1409
|
+
* per-line truncation — rendering word-wraps instead. Blank lines become a
|
|
1410
|
+
* single space so they keep their row (an empty <text> collapses).
|
|
1411
|
+
*/
|
|
1412
|
+
function expandLines(text) {
|
|
1413
|
+
return text.replace(/\t/g, " ").split("\n").map((line) => line.length === 0 ? " " : line);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Full, untruncated block content for an expanded view: every line kept and
|
|
1417
|
+
* word-wrapped by the terminal instead of ellipsis-clipped.
|
|
1418
|
+
*/
|
|
1419
|
+
function ExpandedLines({ text, fg }) {
|
|
1420
|
+
return /* @__PURE__ */ jsx("box", {
|
|
1421
|
+
style: { flexDirection: "column" },
|
|
1422
|
+
children: expandLines(text).map((line, idx) => /* @__PURE__ */ jsx("text", {
|
|
1423
|
+
fg,
|
|
1424
|
+
style: { wrapMode: "word" },
|
|
1425
|
+
children: line
|
|
1426
|
+
}, idx))
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
/** Whether a block of text has lines hidden by the collapsed line cap. */
|
|
1430
|
+
function isClipped(text, maxLines) {
|
|
1431
|
+
let count = 1;
|
|
1432
|
+
for (let i = 0; i < text.length; i++) {
|
|
1433
|
+
if (text.charCodeAt(i) === 10) count++;
|
|
1434
|
+
if (count > maxLines) return true;
|
|
1435
|
+
}
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
/** Dim tail line under an expanded block: click anywhere on it to fold. */
|
|
1439
|
+
function CollapseHint() {
|
|
1440
|
+
return /* @__PURE__ */ jsx("text", {
|
|
1441
|
+
fg: theme.dim,
|
|
1442
|
+
children: "· click to collapse"
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/** Indents tool output two columns under its header — no rule, just space. */
|
|
1446
|
+
function Indented({ children }) {
|
|
1447
|
+
return /* @__PURE__ */ jsx("box", {
|
|
1448
|
+
style: {
|
|
1449
|
+
flexDirection: "column",
|
|
1450
|
+
paddingLeft: 2
|
|
1451
|
+
},
|
|
1452
|
+
children
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region src/chat/tui/list-line.ts
|
|
1458
|
+
/**
|
|
1459
|
+
* Fit a row's title and trailing detail into `width` columns. The title
|
|
1460
|
+
* identifies the row so it gets the space first; the detail takes what is
|
|
1461
|
+
* left and is dropped when nothing is.
|
|
1462
|
+
*/
|
|
1463
|
+
function fitRowText(title, detail, width) {
|
|
1464
|
+
if (width <= 0) return {
|
|
1465
|
+
title: "",
|
|
1466
|
+
detail: ""
|
|
1467
|
+
};
|
|
1468
|
+
if ((detail ? title.length + 1 + detail.length : title.length) <= width) return {
|
|
1469
|
+
title,
|
|
1470
|
+
detail
|
|
1471
|
+
};
|
|
1472
|
+
const room = width - title.length - 1;
|
|
1473
|
+
if (room < 2) return {
|
|
1474
|
+
title: truncate(title, width),
|
|
1475
|
+
detail: ""
|
|
1476
|
+
};
|
|
1477
|
+
return {
|
|
1478
|
+
title,
|
|
1479
|
+
detail: truncate(detail, room)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Join hint segments with " · ", dropping trailing segments that do not fit
|
|
1484
|
+
* rather than wrapping onto a second line — which would cost the list a row.
|
|
1485
|
+
* The first segment is always kept (truncated if it has to be).
|
|
1486
|
+
*/
|
|
1487
|
+
function fitHintLine(segments, width) {
|
|
1488
|
+
const kept = [];
|
|
1489
|
+
for (const segment of segments) {
|
|
1490
|
+
if (kept.length > 0 && [...kept, segment].join(" · ").length > width) break;
|
|
1491
|
+
kept.push(segment);
|
|
1492
|
+
}
|
|
1493
|
+
return truncate(kept.join(" · "), width);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1332
1496
|
//#endregion
|
|
1333
1497
|
//#region src/chat/tui/screens/conversation-picker.tsx
|
|
1334
1498
|
const humanChannels = [
|
|
@@ -1432,6 +1596,27 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
1432
1596
|
kind: "conv",
|
|
1433
1597
|
hit
|
|
1434
1598
|
}))];
|
|
1599
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1600
|
+
const rowWidth = Math.max(1, innerWidth - 2);
|
|
1601
|
+
const confirmConv = confirmId !== null ? list.find((c) => c.id === confirmId) : void 0;
|
|
1602
|
+
const status = confirmConv ? {
|
|
1603
|
+
fg: theme.error,
|
|
1604
|
+
text: truncate(`delete “${confirmConv.title ?? "(untitled)"}”? y/n`, innerWidth)
|
|
1605
|
+
} : error ? {
|
|
1606
|
+
fg: theme.error,
|
|
1607
|
+
text: truncate(`delete failed: ${error}`, innerWidth)
|
|
1608
|
+
} : {
|
|
1609
|
+
fg: theme.dim,
|
|
1610
|
+
text: fitHintLine([
|
|
1611
|
+
`${hits.length}/${list.length}${complete ? "" : " (loading…)"}`,
|
|
1612
|
+
showAutomated ? "incl. agent runs" : "yours",
|
|
1613
|
+
`tab ${showAutomated ? "hide" : "show"} agent runs`,
|
|
1614
|
+
"↑/↓ move",
|
|
1615
|
+
"↵ open",
|
|
1616
|
+
"ctrl+d delete",
|
|
1617
|
+
"esc back"
|
|
1618
|
+
], innerWidth)
|
|
1619
|
+
};
|
|
1435
1620
|
const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
|
|
1436
1621
|
const visibleRows = Math.max(3, height - 6);
|
|
1437
1622
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
@@ -1487,7 +1672,6 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
1487
1672
|
if (row) open(row);
|
|
1488
1673
|
}
|
|
1489
1674
|
});
|
|
1490
|
-
const confirmConv = confirmId !== null ? list.find((c) => c.id === confirmId) : void 0;
|
|
1491
1675
|
return /* @__PURE__ */ jsxs("box", {
|
|
1492
1676
|
style: {
|
|
1493
1677
|
flexDirection: "column",
|
|
@@ -1512,31 +1696,9 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
1512
1696
|
}
|
|
1513
1697
|
})]
|
|
1514
1698
|
}),
|
|
1515
|
-
|
|
1516
|
-
fg:
|
|
1517
|
-
children:
|
|
1518
|
-
"delete “",
|
|
1519
|
-
confirmConv.title ?? "(untitled)",
|
|
1520
|
-
"”? y/n"
|
|
1521
|
-
]
|
|
1522
|
-
}) : error ? /* @__PURE__ */ jsxs("text", {
|
|
1523
|
-
fg: theme.error,
|
|
1524
|
-
children: ["delete failed: ", error]
|
|
1525
|
-
}) : /* @__PURE__ */ jsxs("text", {
|
|
1526
|
-
fg: theme.dim,
|
|
1527
|
-
children: [
|
|
1528
|
-
hits.length,
|
|
1529
|
-
"/",
|
|
1530
|
-
list.length,
|
|
1531
|
-
complete ? "" : " (loading…)",
|
|
1532
|
-
" ·",
|
|
1533
|
-
" ",
|
|
1534
|
-
showAutomated ? "incl. agent runs" : "yours",
|
|
1535
|
-
" · tab",
|
|
1536
|
-
" ",
|
|
1537
|
-
showAutomated ? "hide" : "show",
|
|
1538
|
-
" agent runs · ↑/↓ move · ↵ open · ctrl+d delete · esc back"
|
|
1539
|
-
]
|
|
1699
|
+
/* @__PURE__ */ jsx("text", {
|
|
1700
|
+
fg: status.fg,
|
|
1701
|
+
children: status.text
|
|
1540
1702
|
}),
|
|
1541
1703
|
/* @__PURE__ */ jsx("box", {
|
|
1542
1704
|
style: {
|
|
@@ -1546,22 +1708,24 @@ function ConversationList({ agent, conversations, complete, showAutomated, onTog
|
|
|
1546
1708
|
children: windowed.map((row, i) => {
|
|
1547
1709
|
const selected = start + i === clamped;
|
|
1548
1710
|
const fg = selected ? theme.accent : theme.fg;
|
|
1711
|
+
const marker = selected ? "› " : " ";
|
|
1549
1712
|
if (row.kind === "new") return /* @__PURE__ */ jsxs("text", {
|
|
1550
1713
|
fg,
|
|
1551
|
-
children: [
|
|
1714
|
+
children: [marker, truncate("+ new conversation", rowWidth)]
|
|
1552
1715
|
}, "new");
|
|
1716
|
+
const { title, detail } = fitRowText(row.hit.item.title ?? "(untitled)", previewLine(row.hit.item), rowWidth);
|
|
1553
1717
|
return /* @__PURE__ */ jsxs("text", {
|
|
1554
1718
|
fg,
|
|
1555
1719
|
children: [
|
|
1556
|
-
|
|
1720
|
+
marker,
|
|
1557
1721
|
/* @__PURE__ */ jsx(FuzzyText, {
|
|
1558
|
-
text:
|
|
1722
|
+
text: title,
|
|
1559
1723
|
indexes: row.hit.item.title ? row.hit.highlights[0] : null
|
|
1560
1724
|
}),
|
|
1561
|
-
/* @__PURE__ */ jsxs("span", {
|
|
1725
|
+
detail ? /* @__PURE__ */ jsxs("span", {
|
|
1562
1726
|
fg: theme.dim,
|
|
1563
|
-
children: [" ",
|
|
1564
|
-
})
|
|
1727
|
+
children: [" ", detail]
|
|
1728
|
+
}) : null
|
|
1565
1729
|
]
|
|
1566
1730
|
}, row.hit.item.id);
|
|
1567
1731
|
})
|
|
@@ -1718,92 +1882,6 @@ async function resolveDroppedFiles(paths) {
|
|
|
1718
1882
|
}
|
|
1719
1883
|
}
|
|
1720
1884
|
|
|
1721
|
-
//#endregion
|
|
1722
|
-
//#region src/chat/tui/chunks/frame.tsx
|
|
1723
|
-
/** Status glyph + color for a tool's lifecycle state. */
|
|
1724
|
-
function toolGlyph(state) {
|
|
1725
|
-
switch (state) {
|
|
1726
|
-
case "streaming":
|
|
1727
|
-
case "input-ready": return {
|
|
1728
|
-
ch: "◐",
|
|
1729
|
-
color: theme.warning
|
|
1730
|
-
};
|
|
1731
|
-
case "output-ready": return {
|
|
1732
|
-
ch: "✓",
|
|
1733
|
-
color: theme.success
|
|
1734
|
-
};
|
|
1735
|
-
case "error": return {
|
|
1736
|
-
ch: "✗",
|
|
1737
|
-
color: theme.error
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
/** Truncate a single line with an ellipsis. */
|
|
1742
|
-
function truncate(line, maxLen) {
|
|
1743
|
-
return line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line;
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Compact a block of tool output for the transcript: truncate over-long
|
|
1747
|
-
* lines (so a giant single-line JSON blob doesn't wrap to a wall) and cap
|
|
1748
|
-
* the line count, with a "+N more" tail that doubles as the click-to-expand
|
|
1749
|
-
* affordance. Expanded blocks render through `ExpandedLines` instead —
|
|
1750
|
-
* full content, word-wrapped, nothing clipped.
|
|
1751
|
-
*/
|
|
1752
|
-
function clipLines(text, { maxLines, maxLineLen }) {
|
|
1753
|
-
const lines = text.replace(/\t/g, " ").split("\n");
|
|
1754
|
-
const out = lines.slice(0, maxLines).map((line) => truncate(line, maxLineLen));
|
|
1755
|
-
if (lines.length > maxLines) out.push(`… +${lines.length - maxLines} more lines · click to expand`);
|
|
1756
|
-
return out;
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Split a block for the expanded view: tabs normalised, no line cap, no
|
|
1760
|
-
* per-line truncation — rendering word-wraps instead. Blank lines become a
|
|
1761
|
-
* single space so they keep their row (an empty <text> collapses).
|
|
1762
|
-
*/
|
|
1763
|
-
function expandLines(text) {
|
|
1764
|
-
return text.replace(/\t/g, " ").split("\n").map((line) => line.length === 0 ? " " : line);
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Full, untruncated block content for an expanded view: every line kept and
|
|
1768
|
-
* word-wrapped by the terminal instead of ellipsis-clipped.
|
|
1769
|
-
*/
|
|
1770
|
-
function ExpandedLines({ text, fg }) {
|
|
1771
|
-
return /* @__PURE__ */ jsx("box", {
|
|
1772
|
-
style: { flexDirection: "column" },
|
|
1773
|
-
children: expandLines(text).map((line, idx) => /* @__PURE__ */ jsx("text", {
|
|
1774
|
-
fg,
|
|
1775
|
-
style: { wrapMode: "word" },
|
|
1776
|
-
children: line
|
|
1777
|
-
}, idx))
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
/** Whether a block of text has lines hidden by the collapsed line cap. */
|
|
1781
|
-
function isClipped(text, maxLines) {
|
|
1782
|
-
let count = 1;
|
|
1783
|
-
for (let i = 0; i < text.length; i++) {
|
|
1784
|
-
if (text.charCodeAt(i) === 10) count++;
|
|
1785
|
-
if (count > maxLines) return true;
|
|
1786
|
-
}
|
|
1787
|
-
return false;
|
|
1788
|
-
}
|
|
1789
|
-
/** Dim tail line under an expanded block: click anywhere on it to fold. */
|
|
1790
|
-
function CollapseHint() {
|
|
1791
|
-
return /* @__PURE__ */ jsx("text", {
|
|
1792
|
-
fg: theme.dim,
|
|
1793
|
-
children: "· click to collapse"
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
/** Indents tool output two columns under its header — no rule, just space. */
|
|
1797
|
-
function Indented({ children }) {
|
|
1798
|
-
return /* @__PURE__ */ jsx("box", {
|
|
1799
|
-
style: {
|
|
1800
|
-
flexDirection: "column",
|
|
1801
|
-
paddingLeft: 2
|
|
1802
|
-
},
|
|
1803
|
-
children
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
1885
|
//#endregion
|
|
1808
1886
|
//#region src/chat/tui/chunks/syntax.ts
|
|
1809
1887
|
let cached = null;
|
|
@@ -4297,6 +4375,39 @@ function filterSlashCommands(query) {
|
|
|
4297
4375
|
return [...prefix, ...substring];
|
|
4298
4376
|
}
|
|
4299
4377
|
|
|
4378
|
+
//#endregion
|
|
4379
|
+
//#region src/chat/tui/resume-hint.ts
|
|
4380
|
+
/**
|
|
4381
|
+
* The "Resume this conversation with: …" hint printed when the TUI exits.
|
|
4382
|
+
*
|
|
4383
|
+
* There are two ways out of the app — ChatScreen's quit (double ctrl+c or a
|
|
4384
|
+
* bare `exit`/`quit` line) and the app-root global ctrl+c on non-chat
|
|
4385
|
+
* screens (after `esc` back to the picker) — and both must print the hint:
|
|
4386
|
+
* esc'ing out of a live run then quitting is exactly the case where the id
|
|
4387
|
+
* is the only way back in (the agent keeps working server-side). So the
|
|
4388
|
+
* last-opened conversation is tracked here, module-scoped, and both
|
|
4389
|
+
* teardown sites call `printResumeHint()`.
|
|
4390
|
+
*
|
|
4391
|
+
* The write happens synchronously AFTER `renderer.destroy()` returns, never
|
|
4392
|
+
* on a process 'exit' hook — the native renderer teardown can end the
|
|
4393
|
+
* process without firing JS exit listeners (observed on darwin). By the
|
|
4394
|
+
* time destroy() returns, stdout is unhooked and the alt screen restored,
|
|
4395
|
+
* so the lines land in normal scrollback.
|
|
4396
|
+
*/
|
|
4397
|
+
let lastConversationId = null;
|
|
4398
|
+
/** Record the conversation currently open in the chat screen (null while a
|
|
4399
|
+
* brand-new conversation hasn't been materialized on the server yet). */
|
|
4400
|
+
function setResumeConversation(conversationId) {
|
|
4401
|
+
lastConversationId = conversationId;
|
|
4402
|
+
}
|
|
4403
|
+
/** Print the resume hint for the last opened conversation, if any. Call
|
|
4404
|
+
* after `renderer.destroy()`. Prints at most once per process. */
|
|
4405
|
+
function printResumeHint(write = (text) => process.stdout.write(text)) {
|
|
4406
|
+
if (!lastConversationId) return;
|
|
4407
|
+
write(`\nResume this conversation with:\n skydive chat --resume ${lastConversationId}\n`);
|
|
4408
|
+
lastConversationId = null;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4300
4411
|
//#endregion
|
|
4301
4412
|
//#region src/chat/tui/screens/model-picker.tsx
|
|
4302
4413
|
/**
|
|
@@ -5494,6 +5605,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5494
5605
|
}, [agentHost]);
|
|
5495
5606
|
useEffect(() => {
|
|
5496
5607
|
agentHost.setSession(conversationId);
|
|
5608
|
+
setResumeConversation(conversationId);
|
|
5497
5609
|
}, [agentHost, conversationId]);
|
|
5498
5610
|
const scrollRef = useRef(null);
|
|
5499
5611
|
const composerRef = useRef(null);
|
|
@@ -6013,6 +6125,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
6013
6125
|
portalClient?.dispose();
|
|
6014
6126
|
agentHost.dispose();
|
|
6015
6127
|
renderer.destroy();
|
|
6128
|
+
printResumeHint();
|
|
6016
6129
|
}, [
|
|
6017
6130
|
portalClient,
|
|
6018
6131
|
agentHost,
|
|
@@ -6849,7 +6962,7 @@ function ToastView() {
|
|
|
6849
6962
|
|
|
6850
6963
|
//#endregion
|
|
6851
6964
|
//#region src/chat/tui/app.tsx
|
|
6852
|
-
function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector }) {
|
|
6965
|
+
function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId }) {
|
|
6853
6966
|
const renderer = useRenderer();
|
|
6854
6967
|
const screen = useStore((s) => s.screen);
|
|
6855
6968
|
useStore((s) => s.themeId);
|
|
@@ -6921,7 +7034,8 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
|
|
|
6921
7034
|
try {
|
|
6922
7035
|
const screen = await resolveInitialScreen({
|
|
6923
7036
|
rest,
|
|
6924
|
-
agentSelector
|
|
7037
|
+
agentSelector,
|
|
7038
|
+
conversationId
|
|
6925
7039
|
});
|
|
6926
7040
|
if (cancelled) return;
|
|
6927
7041
|
goTo(screen);
|
|
@@ -6942,6 +7056,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
|
|
|
6942
7056
|
sessionToken,
|
|
6943
7057
|
promptHistoryPath,
|
|
6944
7058
|
agentSelector,
|
|
7059
|
+
conversationId,
|
|
6945
7060
|
goTo,
|
|
6946
7061
|
setClients,
|
|
6947
7062
|
fail
|
|
@@ -6951,6 +7066,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
|
|
|
6951
7066
|
useStore.getState().portalClient?.dispose();
|
|
6952
7067
|
agentHost.dispose();
|
|
6953
7068
|
renderer.destroy();
|
|
7069
|
+
printResumeHint();
|
|
6954
7070
|
}
|
|
6955
7071
|
});
|
|
6956
7072
|
return /* @__PURE__ */ jsx(AgentHostContext.Provider, {
|
|
@@ -7244,7 +7360,7 @@ var LinkifiedMarkdownRenderable = class extends MarkdownRenderable {
|
|
|
7244
7360
|
* module — and the heavy OpenTUI dependency graph it pulls in — is
|
|
7245
7361
|
* dynamically imported and never loaded on the Node management path.
|
|
7246
7362
|
*/
|
|
7247
|
-
async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector }) {
|
|
7363
|
+
async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId }) {
|
|
7248
7364
|
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
|
7249
7365
|
extend({ markdown: LinkifiedMarkdownRenderable });
|
|
7250
7366
|
const store = useStore.getState();
|
|
@@ -7268,7 +7384,8 @@ async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath,
|
|
|
7268
7384
|
shareMachine,
|
|
7269
7385
|
promptHistoryPath,
|
|
7270
7386
|
notifications,
|
|
7271
|
-
agentSelector: agentSelector ?? null
|
|
7387
|
+
agentSelector: agentSelector ?? null,
|
|
7388
|
+
conversationId: conversationId ?? null
|
|
7272
7389
|
}));
|
|
7273
7390
|
}
|
|
7274
7391
|
/** Open the same workspace picker used by the initial agent screen. */
|