skydive-cli 0.2.0-beta.497 → 0.2.0-beta.530

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/README.md CHANGED
@@ -176,6 +176,13 @@ internal API. On launch it walks you through:
176
176
  formatting, collapsed reasoning, and rich rendering of tool calls
177
177
  (`bash`, `edit`/`write` diffs, `read`, `grep`/`glob`).
178
178
 
179
+ You can change conversation without leaving chat. `/conversation` (aliases
180
+ `/conversations`, `/conv`) reopens the conversation list, and
181
+ `/conversation <id>` — an id, its `/c/<id>` link, or a pasted
182
+ `skydive chat --resume <id>` line — jumps straight into that thread, including
183
+ one belonging to a **different** agent. Either way the run you leave behind
184
+ keeps going server-side; reopening it reattaches to the stream.
185
+
179
186
  Runs under **Node** like every other command. The TUI renders through
180
187
  OpenTUI, whose native core needs the [Bun](https://bun.sh) runtime, so the
181
188
  first `chat` run downloads a private, pinned Bun and re-execs under it
package/dist/js/bin.mjs CHANGED
@@ -20,7 +20,7 @@ import semver from "semver";
20
20
 
21
21
  //#region package.json
22
22
  var name = "skydive-cli";
23
- var version = "0.2.0-beta.497";
23
+ var version = "0.2.0-beta.530";
24
24
 
25
25
  //#endregion
26
26
  //#region src/types.ts
@@ -1200,7 +1200,7 @@ const importCommand = {
1200
1200
  process.exit(1);
1201
1201
  }
1202
1202
  }
1203
- const { runChat } = await import("./boot-DlptSLP6.mjs");
1203
+ const { runChat } = await import("./boot-C-wL4WoP.mjs");
1204
1204
  await runChat({
1205
1205
  appUrl,
1206
1206
  sessionToken: session.value.sessionToken,
@@ -1511,7 +1511,7 @@ const chatCommand = {
1511
1511
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1512
1512
  process.exit(1);
1513
1513
  }
1514
- const { runChat } = await import("./boot-DlptSLP6.mjs");
1514
+ const { runChat } = await import("./boot-C-wL4WoP.mjs");
1515
1515
  await runChat({
1516
1516
  appUrl,
1517
1517
  sessionToken: session.value.sessionToken,
@@ -1810,7 +1810,7 @@ const switchCommand = {
1810
1810
  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.");
1811
1811
  process.exit(1);
1812
1812
  }
1813
- const { runWorkspacePicker } = await import("./boot-DlptSLP6.mjs");
1813
+ const { runWorkspacePicker } = await import("./boot-C-wL4WoP.mjs");
1814
1814
  await runWorkspacePicker(session);
1815
1815
  return;
1816
1816
  }
@@ -2003,6 +2003,33 @@ function relativeTime(iso) {
2003
2003
  return `${Math.floor(diffMs / 864e5)}d ago`;
2004
2004
  }
2005
2005
 
2006
+ //#endregion
2007
+ //#region src/chat/conversation-ref.ts
2008
+ /**
2009
+ * Parsing the conversation reference a user hands `/conversation`.
2010
+ *
2011
+ * The whole point of the command is that you paste whatever you already have
2012
+ * in front of you, so every form the platform hands out has to work: a bare
2013
+ * id, the `https://skydive.com/c/<id>` link an agent replies with, or the
2014
+ * `skydive chat --resume <id>` line the TUI prints on exit. All three carry
2015
+ * exactly one id, so we pull the first uuid out of the string rather than
2016
+ * teaching this about URL shapes and command syntax.
2017
+ *
2018
+ * Validating here (instead of letting the API answer) is what turns a typo
2019
+ * into "not a conversation id" in the composer rather than a request that
2020
+ * cannot succeed.
2021
+ */
2022
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
2023
+ /**
2024
+ * The conversation id in `input`, lowercased, or null when there isn't one.
2025
+ * Trailing punctuation, query strings and surrounding prose are tolerated —
2026
+ * a pasted link or command line is the expected input, not the exception.
2027
+ */
2028
+ function parseConversationRef(input) {
2029
+ const match = uuidPattern.exec(input);
2030
+ return match ? match[0].toLowerCase() : null;
2031
+ }
2032
+
2006
2033
  //#endregion
2007
2034
  //#region src/chat/paste.ts
2008
2035
  const MAX_PATH_PASTE_CHARS = 4096;
@@ -4887,6 +4914,13 @@ const slashCommands = [
4887
4914
  description: "start a new conversation with this agent",
4888
4915
  action: { kind: "new-conversation" }
4889
4916
  },
4917
+ {
4918
+ name: "conversation",
4919
+ aliases: ["conversations", "conv"],
4920
+ argsHint: "[id or link]",
4921
+ description: "switch conversation (no id: pick from the list)",
4922
+ action: { kind: "conversation" }
4923
+ },
4890
4924
  {
4891
4925
  name: "model",
4892
4926
  aliases: [],
@@ -4951,7 +4985,7 @@ const slashCommands = [
4951
4985
  *
4952
4986
  * An argument-less command only matches as the exact line — `/help me write
4953
4987
  * a plan` stays an ordinary message rather than swallowing the user's text
4954
- * into the help modal. Only `sandbox` accepts a rest.
4988
+ * into the help modal. Only `sandbox` and `conversation` accept a rest.
4955
4989
  */
4956
4990
  function matchSlashCommand(line) {
4957
4991
  if (!line.startsWith("/")) return null;
@@ -7969,6 +8003,10 @@ function routeInput(raw) {
7969
8003
  kind: "sandbox-exec",
7970
8004
  command: match.rest
7971
8005
  } : { kind: "sandbox-pty" };
8006
+ case "conversation": return match.rest ? {
8007
+ kind: "open-conversation",
8008
+ ref: match.rest
8009
+ } : { kind: "conversation-picker" };
7972
8010
  case "new-conversation":
7973
8011
  case "model-picker":
7974
8012
  case "theme-picker":
@@ -9017,6 +9055,64 @@ function ChatScreen({ agent, conversation }) {
9017
9055
  agent,
9018
9056
  goTo
9019
9057
  ]);
9058
+ /**
9059
+ * Leave this conversation for another screen without ending its run: drop
9060
+ * our own SSE subscription and nothing else — never `rest.cancelRun` — so
9061
+ * the agent keeps working server-side and reopening the thread reattaches.
9062
+ * Same contract as `esc`, which is why `/conversation` is allowed mid-run
9063
+ * where `/new` is not: what you land on still lists the thread you left,
9064
+ * so the run is never stranded out of reach.
9065
+ */
9066
+ const leaveForScreen = useCallback((next) => {
9067
+ const current = runRef.current;
9068
+ if (current.kind === "streaming") current.abort.abort();
9069
+ if (current.kind !== "idle") useStore.getState().showToast({ message: `${agent.name} keeps working — reopen to reattach` });
9070
+ goTo(next);
9071
+ }, [agent.name, goTo]);
9072
+ /**
9073
+ * `/conversation <id or link>`: open that conversation without leaving the
9074
+ * TUI — the point being that the id usually arrives *from* an agent, in the
9075
+ * session you started to go find it.
9076
+ *
9077
+ * Resolution goes through `resolveInitialScreen`, the same path
9078
+ * `--resume <id>` takes, so an id belonging to a different agent opens
9079
+ * under that agent and the not-found / no-access wording lives in one
9080
+ * place.
9081
+ */
9082
+ const openConversationRef = useCallback(async (ref) => {
9083
+ const target = parseConversationRef(ref);
9084
+ if (!target) {
9085
+ setItems((prev) => [...prev, {
9086
+ kind: "error",
9087
+ id: crypto.randomUUID(),
9088
+ text: `not a conversation id: ${ref} — pass an id, its /c/<id> link, or run /conversation on its own to pick from the list`
9089
+ }]);
9090
+ return;
9091
+ }
9092
+ if (!rest) return;
9093
+ if (target === conversationId) {
9094
+ useStore.getState().showToast({ message: "already in this conversation" });
9095
+ return;
9096
+ }
9097
+ try {
9098
+ leaveForScreen(await resolveInitialScreen({
9099
+ rest,
9100
+ agentSelector: null,
9101
+ conversationId: target,
9102
+ newConversation: false
9103
+ }));
9104
+ } catch (err) {
9105
+ setItems((prev) => [...prev, {
9106
+ kind: "error",
9107
+ id: crypto.randomUUID(),
9108
+ text: `couldn't open ${target}: ${errorMessage(err)}`
9109
+ }]);
9110
+ }
9111
+ }, [
9112
+ rest,
9113
+ conversationId,
9114
+ leaveForScreen
9115
+ ]);
9020
9116
  const toggleShare = useCallback(() => {
9021
9117
  if (!portalClient) return;
9022
9118
  if (portal.status === "off") {
@@ -9088,6 +9184,15 @@ function ChatScreen({ agent, conversation }) {
9088
9184
  }
9089
9185
  });
9090
9186
  break;
9187
+ case "conversation-picker":
9188
+ leaveForScreen({
9189
+ kind: "conversation-picker",
9190
+ agent
9191
+ });
9192
+ break;
9193
+ case "open-conversation":
9194
+ openConversationRef(routed.ref);
9195
+ break;
9091
9196
  case "model-picker":
9092
9197
  setModelPickerOpen(true);
9093
9198
  break;
@@ -9146,6 +9251,8 @@ function ChatScreen({ agent, conversation }) {
9146
9251
  runLocalShell,
9147
9252
  runSandboxExec,
9148
9253
  runSandboxPty,
9254
+ leaveForScreen,
9255
+ openConversationRef,
9149
9256
  toggleShare,
9150
9257
  openInBrowser,
9151
9258
  quit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.497",
3
+ "version": "0.2.0-beta.530",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",