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 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.340";
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 conversationTitleSchema = z.object({
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: conversationTitleSchema });
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: "For -p: continue an existing conversation by id instead of starting a new one."
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-BQZrbJAh.mjs");
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-BQZrbJAh.mjs");
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 `--agent` selector we open the agent picker, exactly as before.
141
- * With a selector (`skydive chat --agent grace`, no `-p`) we resolve it
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
- confirmConv ? /* @__PURE__ */ jsxs("text", {
1516
- fg: theme.error,
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: [selected ? "› " : " ", "+ new conversation"]
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
- selected ? "› " : " ",
1720
+ marker,
1557
1721
  /* @__PURE__ */ jsx(FuzzyText, {
1558
- text: row.hit.item.title ?? "(untitled)",
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: [" ", previewLine(row.hit.item)]
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.340",
3
+ "version": "0.1.0-beta.342",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",