skydive-cli 0.1.0-beta.297 → 0.1.0-beta.311

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.297";
19
+ var version$1 = "0.1.0-beta.311";
20
20
 
21
21
  //#endregion
22
22
  //#region src/types.ts
@@ -2607,7 +2607,7 @@ const chatCommand = {
2607
2607
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2608
2608
  process.exit(1);
2609
2609
  }
2610
- const { runChat } = await import("./boot-DYzw1U0y.mjs");
2610
+ const { runChat } = await import("./boot-DEX6CwVM.mjs");
2611
2611
  await runChat({
2612
2612
  appUrl,
2613
2613
  sessionToken: session.value.sessionToken,
@@ -3390,7 +3390,7 @@ const switchCommand = {
3390
3390
  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.");
3391
3391
  process.exit(1);
3392
3392
  }
3393
- const { runWorkspacePicker } = await import("./boot-DYzw1U0y.mjs");
3393
+ const { runWorkspacePicker } = await import("./boot-DEX6CwVM.mjs");
3394
3394
  await runWorkspacePicker(session);
3395
3395
  return;
3396
3396
  }
@@ -1096,7 +1096,8 @@ function AgentCreateScreen() {
1096
1096
  agent,
1097
1097
  conversation: {
1098
1098
  kind: "new",
1099
- agentId: agent.id
1099
+ agentId: agent.id,
1100
+ nonce: crypto.randomUUID()
1100
1101
  }
1101
1102
  });
1102
1103
  } catch (err) {
@@ -1406,7 +1407,8 @@ function ConversationList({ agent, conversations, complete, onPick }) {
1406
1407
  agent,
1407
1408
  conversation: {
1408
1409
  kind: "new",
1409
- agentId: agent.id
1410
+ agentId: agent.id,
1411
+ nonce: crypto.randomUUID()
1410
1412
  }
1411
1413
  });
1412
1414
  else onPick({
@@ -2905,6 +2907,19 @@ function RenderItem({ item }) {
2905
2907
  barColor: null,
2906
2908
  children: /* @__PURE__ */ jsx(CardItem, { item })
2907
2909
  });
2910
+ case "info": return /* @__PURE__ */ jsx(Row, {
2911
+ barColor: null,
2912
+ children: /* @__PURE__ */ jsxs("box", {
2913
+ style: { flexDirection: "column" },
2914
+ children: [/* @__PURE__ */ jsx("text", {
2915
+ fg: theme.muted,
2916
+ children: /* @__PURE__ */ jsx("b", { children: item.title })
2917
+ }), /* @__PURE__ */ jsx("text", {
2918
+ fg: theme.muted,
2919
+ children: item.text
2920
+ })]
2921
+ })
2922
+ });
2908
2923
  case "recap": return /* @__PURE__ */ jsx(Row, {
2909
2924
  barColor: null,
2910
2925
  children: /* @__PURE__ */ jsxs("text", {
@@ -3454,7 +3469,7 @@ function mapToolState(raw) {
3454
3469
  function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer, hasActionableCard }) {
3455
3470
  const connectHint = hasActionableCard ? "ctrl+r connect · " : "";
3456
3471
  switch (runKind) {
3457
- case "idle": return ctrlCArmed ? "press ctrl+c again to quit" : `${connectHint}↵ send · esc back · ctrl+c quit · ctrl+t ${isShared ? "revoke access" : "local access"} · ? keybinds`;
3472
+ case "idle": return ctrlCArmed ? "press ctrl+c again to quit" : `${connectHint}↵ send · / commands · esc back · ctrl+c quit · ctrl+t ${isShared ? "revoke access" : "local access"} · ? keybinds`;
3458
3473
  case "sending": return "sending…";
3459
3474
  case "streaming": return hasPendingSteer ? `${connectHint}↵ steer · esc cancel steer · ctrl+c cancel run` : `${connectHint}↵ steer · ctrl+c cancel`;
3460
3475
  }
@@ -3464,6 +3479,31 @@ function formatModelHint(model) {
3464
3479
  return `ctrl+p · ${model ?? "default"}`;
3465
3480
  }
3466
3481
 
3482
+ //#endregion
3483
+ //#region src/chat/tui/chat/status-info.ts
3484
+ /**
3485
+ * Body text for the `/status` transcript block: the session facts the status
3486
+ * bar and footer can't fit, one aligned `label value` row each. Pure so the
3487
+ * layout is testable without a renderer.
3488
+ */
3489
+ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, share }) {
3490
+ const rows = [
3491
+ ["app", appUrl ?? "unknown"],
3492
+ ["workspace", workspaceName ?? "unknown"],
3493
+ ["agent", agentName],
3494
+ ["conversation", conversationId ?? "new — created when you send the first message"],
3495
+ ["model", model ?? "default"],
3496
+ ["local access", formatShare(share)]
3497
+ ];
3498
+ const width = Math.max(...rows.map(([label]) => label.length));
3499
+ return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
3500
+ }
3501
+ function formatShare(share) {
3502
+ if (share.status === "off") return "off — ctrl+t to share this machine";
3503
+ if (share.status !== "connected") return share.status;
3504
+ return share.grantedToAgent ? "shared — this agent can run commands here" : "shared — this agent is not granted";
3505
+ }
3506
+
3467
3507
  //#endregion
3468
3508
  //#region src/chat/notification-preview.ts
3469
3509
  const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
@@ -3750,12 +3790,37 @@ const keybindGroups = [
3750
3790
  keys: "pgup / pgdn",
3751
3791
  action: "scroll transcript"
3752
3792
  },
3793
+ {
3794
+ keys: "/",
3795
+ action: "open the command menu (start of message)"
3796
+ },
3753
3797
  {
3754
3798
  keys: "?",
3755
3799
  action: "open this help (composer empty)"
3756
3800
  }
3757
3801
  ]
3758
3802
  },
3803
+ {
3804
+ title: "command menu (type / to open)",
3805
+ binds: [
3806
+ {
3807
+ keys: "↑ / ↓",
3808
+ action: "move selection"
3809
+ },
3810
+ {
3811
+ keys: "tab",
3812
+ action: "complete the command"
3813
+ },
3814
+ {
3815
+ keys: "↵",
3816
+ action: "run (or complete) the selected command"
3817
+ },
3818
+ {
3819
+ keys: "esc",
3820
+ action: "dismiss the menu"
3821
+ }
3822
+ ]
3823
+ },
3759
3824
  {
3760
3825
  title: "agent picker",
3761
3826
  binds: [
@@ -4006,6 +4071,182 @@ function HelpModal({ onClose }) {
4006
4071
  });
4007
4072
  }
4008
4073
 
4074
+ //#endregion
4075
+ //#region src/chat/tui/autocomplete/completion-menu.tsx
4076
+ const MAX_COMPLETION_ROWS = 6;
4077
+ /**
4078
+ * Rows the menu occupies in the chat screen's fixed layout budget: the
4079
+ * windowed items (min 1 for the empty row) plus the border. The chat screen
4080
+ * subtracts this from the transcript height, mirroring the composer and
4081
+ * staged-attachment panels.
4082
+ */
4083
+ function completionMenuHeight(itemCount) {
4084
+ return Math.min(Math.max(itemCount, 1), MAX_COMPLETION_ROWS) + 2;
4085
+ }
4086
+ /**
4087
+ * The inline autocomplete dropdown, rendered directly above the composer
4088
+ * (never as a screen overlay — the composer must stay mounted and focused).
4089
+ * Pure presentation: the chat screen owns the open/highlight state and the
4090
+ * keyboard handling.
4091
+ */
4092
+ function CompletionMenu({ items, highlight }) {
4093
+ const start = windowStart(highlight, items.length, MAX_COMPLETION_ROWS);
4094
+ const windowed = items.slice(start, start + MAX_COMPLETION_ROWS);
4095
+ return /* @__PURE__ */ jsx("box", {
4096
+ style: {
4097
+ border: true,
4098
+ borderColor: theme.faint,
4099
+ paddingLeft: 1,
4100
+ paddingRight: 1,
4101
+ flexShrink: 0,
4102
+ marginTop: 1,
4103
+ flexDirection: "column"
4104
+ },
4105
+ children: windowed.length === 0 ? /* @__PURE__ */ jsx("text", {
4106
+ fg: theme.muted,
4107
+ children: "no matching command — ↵ sends as a message"
4108
+ }) : windowed.map((item, i) => {
4109
+ const selected = start + i === highlight;
4110
+ return /* @__PURE__ */ jsxs("text", {
4111
+ fg: selected ? theme.accent : theme.fg,
4112
+ children: [
4113
+ selected ? "› " : " ",
4114
+ item.label,
4115
+ item.hint ? /* @__PURE__ */ jsx("span", {
4116
+ fg: theme.dim,
4117
+ children: ` ${item.hint}`
4118
+ }) : null,
4119
+ /* @__PURE__ */ jsx("span", {
4120
+ fg: theme.muted,
4121
+ children: ` ${item.description}`
4122
+ })
4123
+ ]
4124
+ }, item.id);
4125
+ })
4126
+ });
4127
+ }
4128
+
4129
+ //#endregion
4130
+ //#region src/chat/slash-commands.ts
4131
+ const slashCommands = [
4132
+ {
4133
+ name: "sandbox",
4134
+ aliases: ["sbx"],
4135
+ argsHint: "[command]",
4136
+ description: "run in the agent's sandbox (no command: open a terminal)",
4137
+ action: { kind: "sandbox" }
4138
+ },
4139
+ {
4140
+ name: "new",
4141
+ aliases: [],
4142
+ argsHint: null,
4143
+ description: "start a new conversation with this agent",
4144
+ action: { kind: "new-conversation" }
4145
+ },
4146
+ {
4147
+ name: "model",
4148
+ aliases: [],
4149
+ argsHint: null,
4150
+ description: "switch the model for the next run",
4151
+ action: { kind: "model-picker" }
4152
+ },
4153
+ {
4154
+ name: "theme",
4155
+ aliases: [],
4156
+ argsHint: null,
4157
+ description: "switch the color theme",
4158
+ action: { kind: "theme-picker" }
4159
+ },
4160
+ {
4161
+ name: "status",
4162
+ aliases: [],
4163
+ argsHint: null,
4164
+ description: "show connection and session status",
4165
+ action: { kind: "status" }
4166
+ },
4167
+ {
4168
+ name: "share",
4169
+ aliases: [],
4170
+ argsHint: null,
4171
+ description: "local access: grant / revoke this machine",
4172
+ action: { kind: "share" }
4173
+ },
4174
+ {
4175
+ name: "browser",
4176
+ aliases: [],
4177
+ argsHint: null,
4178
+ description: "open this conversation in the browser",
4179
+ action: { kind: "browser" }
4180
+ },
4181
+ {
4182
+ name: "connect",
4183
+ aliases: [],
4184
+ argsHint: null,
4185
+ description: "run the newest connect card (auth cards)",
4186
+ action: { kind: "connect" }
4187
+ },
4188
+ {
4189
+ name: "help",
4190
+ aliases: [],
4191
+ argsHint: null,
4192
+ description: "show keybinds and commands",
4193
+ action: { kind: "help" }
4194
+ },
4195
+ {
4196
+ name: "exit",
4197
+ aliases: ["quit"],
4198
+ argsHint: null,
4199
+ description: "quit the chat",
4200
+ action: { kind: "quit" }
4201
+ }
4202
+ ];
4203
+ /**
4204
+ * Match a submitted line against the registry: `/<name>` or `/<name> <rest>`,
4205
+ * case-insensitive on the name. Returns the command plus the trimmed rest,
4206
+ * or null when the line isn't a known slash command.
4207
+ *
4208
+ * An argument-less command only matches as the exact line — `/help me write
4209
+ * a plan` stays an ordinary message rather than swallowing the user's text
4210
+ * into the help modal. Only `sandbox` accepts a rest.
4211
+ */
4212
+ function matchSlashCommand(line) {
4213
+ if (!line.startsWith("/")) return null;
4214
+ const sp = line.indexOf(" ");
4215
+ const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
4216
+ const command = slashCommands.find((c) => c.name === name || c.aliases.includes(name));
4217
+ if (!command) return null;
4218
+ const rest = sp === -1 ? "" : line.slice(sp + 1).trim();
4219
+ if (command.argsHint === null && rest !== "") return null;
4220
+ return {
4221
+ command,
4222
+ rest
4223
+ };
4224
+ }
4225
+ /**
4226
+ * The in-progress command token the completion menu should open for: the
4227
+ * composer text is a lone `/<partial>` with no whitespace yet (whitespace
4228
+ * means the name is settled and the user is typing arguments — or prose).
4229
+ * Returns the partial name ('' right after `/`), or null when no menu
4230
+ * should show.
4231
+ */
4232
+ function detectCommandTrigger(text) {
4233
+ if (!text.startsWith("/")) return null;
4234
+ if (/\s/.test(text)) return null;
4235
+ return text.slice(1);
4236
+ }
4237
+ /**
4238
+ * Menu ordering: prefix matches (registry order) ahead of substring matches,
4239
+ * checked against names and aliases. Empty query lists everything.
4240
+ */
4241
+ function filterSlashCommands(query) {
4242
+ const q = query.trim().toLowerCase();
4243
+ if (!q) return [...slashCommands];
4244
+ const names = (c) => [c.name, ...c.aliases];
4245
+ const prefix = slashCommands.filter((c) => names(c).some((n) => n.startsWith(q)));
4246
+ const substring = slashCommands.filter((c) => !prefix.includes(c) && names(c).some((n) => n.includes(q)));
4247
+ return [...prefix, ...substring];
4248
+ }
4249
+
4009
4250
  //#endregion
4010
4251
  //#region src/chat/tui/screens/model-picker.tsx
4011
4252
  /**
@@ -4832,28 +5073,35 @@ function routeInput(raw) {
4832
5073
  command
4833
5074
  } : { kind: "noop" };
4834
5075
  }
4835
- const sandbox = matchSlash(line, ["sandbox", "sbx"]);
4836
- if (sandbox !== null) return sandbox ? {
4837
- kind: "sandbox-exec",
4838
- command: sandbox
4839
- } : { kind: "sandbox-pty" };
5076
+ const match = matchSlashCommand(line);
5077
+ if (match) {
5078
+ const { action } = match.command;
5079
+ switch (action.kind) {
5080
+ case "sandbox": return match.rest ? {
5081
+ kind: "sandbox-exec",
5082
+ command: match.rest
5083
+ } : { kind: "sandbox-pty" };
5084
+ case "new-conversation":
5085
+ case "model-picker":
5086
+ case "theme-picker":
5087
+ case "status":
5088
+ case "share":
5089
+ case "browser":
5090
+ case "connect":
5091
+ case "help":
5092
+ case "quit": return { kind: action.kind };
5093
+ default: {
5094
+ const exhaustive = action;
5095
+ throw new Error(`unhandled slash action: ${JSON.stringify(exhaustive)}`);
5096
+ }
5097
+ }
5098
+ }
4840
5099
  if (line.trim().toLowerCase() === "exit") return { kind: "quit" };
4841
5100
  return {
4842
5101
  kind: "message",
4843
5102
  content: raw
4844
5103
  };
4845
5104
  }
4846
- /**
4847
- * If `line` is `/<name>` or `/<name> <rest>` for one of `names`, return the
4848
- * trimmed rest (possibly ''); otherwise null. Case-insensitive on the name.
4849
- */
4850
- function matchSlash(line, names) {
4851
- if (!line.startsWith("/")) return null;
4852
- const sp = line.indexOf(" ");
4853
- const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
4854
- if (!names.includes(name)) return null;
4855
- return sp === -1 ? "" : line.slice(sp + 1).trim();
4856
- }
4857
5105
  /** Max characters of `!` output we forward to the agent as context. */
4858
5106
  const SHELL_CONTEXT_OUTPUT_CAP = 16e3;
4859
5107
  /**
@@ -5139,6 +5387,7 @@ function ChatScreen({ agent, conversation }) {
5139
5387
  const sessionToken = useStore((s) => s.sessionToken);
5140
5388
  const goTo = useStore((s) => s.goTo);
5141
5389
  const setChatTitle = useStore((s) => s.setChatTitle);
5390
+ const workspaceName = useStore((s) => s.workspaceName);
5142
5391
  const portal = useStore((s) => s.portal);
5143
5392
  const portalClient = useStore((s) => s.portalClient);
5144
5393
  const setPortalPrompt = useStore((s) => s.setPortalPrompt);
@@ -5162,6 +5411,12 @@ function ChatScreen({ agent, conversation }) {
5162
5411
  const uploadsRef = useRef(/* @__PURE__ */ new Map());
5163
5412
  const [credPrompt, setCredPrompt] = useState(null);
5164
5413
  const credPromptOpen = credPrompt !== null;
5414
+ const [menuDismissed, setMenuDismissed] = useState(false);
5415
+ const [menuHighlight, setMenuHighlight] = useState(0);
5416
+ const commandQuery = grantPrompt || credPrompt ? null : detectCommandTrigger(input);
5417
+ const menuCommands = commandQuery === null ? [] : filterSlashCommands(commandQuery);
5418
+ const menuOpen = commandQuery !== null && !menuDismissed;
5419
+ const menuHighlightClamped = Math.min(menuHighlight, Math.max(0, menuCommands.length - 1));
5165
5420
  const runRef = useRef(run);
5166
5421
  runRef.current = run;
5167
5422
  const itemsRef = useRef(items);
@@ -5191,7 +5446,8 @@ function ChatScreen({ agent, conversation }) {
5191
5446
  const pendingRows = pendingVisible ? attachments.length + 2 : 0;
5192
5447
  const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
5193
5448
  const bottomBoxHeight = credPrompt ? credPromptHeight : composerBoxHeight;
5194
- const scrollHeight = Math.max(3, height - 5 - bottomBoxHeight - pendingRows);
5449
+ const menuRows = menuOpen ? completionMenuHeight(menuCommands.length) : 0;
5450
+ const scrollHeight = Math.max(3, height - 5 - bottomBoxHeight - pendingRows - menuRows);
5195
5451
  useEffect(() => {
5196
5452
  const composer = composerRef.current;
5197
5453
  if (!composer) return;
@@ -5524,6 +5780,12 @@ function ChatScreen({ agent, conversation }) {
5524
5780
  portalClient,
5525
5781
  portal.status
5526
5782
  ]);
5783
+ const runNewestCard = useCallback(() => {
5784
+ const target = [...itemsRef.current].reverse().find(isActionableCard);
5785
+ if (!target) return false;
5786
+ runCardAction(target);
5787
+ return true;
5788
+ }, [runCardAction]);
5527
5789
  const handleCredInput = useCallback((displayed) => {
5528
5790
  setCredPrompt((prev) => {
5529
5791
  if (!prev || prev.submitting) return prev;
@@ -5700,42 +5962,12 @@ function ChatScreen({ agent, conversation }) {
5700
5962
  agentHost,
5701
5963
  renderer
5702
5964
  ]);
5703
- const submit = useCallback(() => {
5704
- const content = input.trim();
5705
- const staged = attachments.filter((a) => a.status !== "error");
5706
- if (!content && staged.length === 0) return;
5707
- const routed = routeInput(content);
5708
- if (routed.kind !== "message") {
5709
- history.append(content);
5710
- setInput("");
5711
- composerRef.current?.clear();
5712
- setComposerRows(1);
5713
- if (routed.kind === "local-shell") runLocalShell(routed.command);
5714
- else if (routed.kind === "sandbox-exec") runSandboxExec(routed.command);
5715
- else if (routed.kind === "sandbox-pty") runSandboxPty();
5716
- else if (routed.kind === "quit") quit();
5717
- return;
5718
- }
5719
- history.append(content);
5720
- setInput("");
5721
- composerRef.current?.clear();
5722
- setComposerRows(1);
5723
- setAttachments([]);
5724
- sendContent(content, staged);
5725
- }, [
5726
- input,
5727
- attachments,
5728
- sendContent,
5729
- history,
5730
- runLocalShell,
5731
- runSandboxExec,
5732
- runSandboxPty,
5733
- quit
5734
- ]);
5735
5965
  const handleComposerChange = useCallback(() => {
5736
5966
  const composer = composerRef.current;
5737
5967
  if (!composer) return;
5738
5968
  setInput(composer.plainText);
5969
+ setMenuHighlight(0);
5970
+ if (detectCommandTrigger(composer.plainText) === null) setMenuDismissed(false);
5739
5971
  setComposerRows(Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), maxComposerRows));
5740
5972
  }, []);
5741
5973
  /**
@@ -5915,6 +6147,126 @@ function ChatScreen({ agent, conversation }) {
5915
6147
  setPortalPrompt(null);
5916
6148
  useStore.getState().showToast({ message: "kept this machine private" });
5917
6149
  }, [agent.id, setPortalPrompt]);
6150
+ const executeLine = useCallback((content, staged) => {
6151
+ if (!content && staged.length === 0) return;
6152
+ const routed = routeInput(content);
6153
+ if (routed.kind !== "message") {
6154
+ history.append(content);
6155
+ setInput("");
6156
+ composerRef.current?.clear();
6157
+ setComposerRows(1);
6158
+ switch (routed.kind) {
6159
+ case "local-shell":
6160
+ runLocalShell(routed.command);
6161
+ break;
6162
+ case "sandbox-exec":
6163
+ runSandboxExec(routed.command);
6164
+ break;
6165
+ case "sandbox-pty":
6166
+ runSandboxPty();
6167
+ break;
6168
+ case "new-conversation":
6169
+ if (runRef.current.kind !== "idle") {
6170
+ useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
6171
+ break;
6172
+ }
6173
+ goTo({
6174
+ kind: "chat",
6175
+ agent,
6176
+ conversation: {
6177
+ kind: "new",
6178
+ agentId: agent.id,
6179
+ nonce: crypto.randomUUID()
6180
+ }
6181
+ });
6182
+ break;
6183
+ case "model-picker":
6184
+ setModelPickerOpen(true);
6185
+ break;
6186
+ case "theme-picker":
6187
+ setThemePickerOpen(true);
6188
+ break;
6189
+ case "status":
6190
+ setItems((prev) => [...prev, {
6191
+ kind: "info",
6192
+ id: crypto.randomUUID(),
6193
+ title: "status",
6194
+ text: formatStatusInfo({
6195
+ appUrl,
6196
+ workspaceName,
6197
+ agentName: agent.name,
6198
+ conversationId,
6199
+ model,
6200
+ share: {
6201
+ status: portal.status,
6202
+ grantedToAgent: portal.grantedAgentIds.includes(agent.id)
6203
+ }
6204
+ })
6205
+ }]);
6206
+ break;
6207
+ case "share":
6208
+ toggleShare();
6209
+ break;
6210
+ case "browser":
6211
+ openInBrowser();
6212
+ break;
6213
+ case "connect":
6214
+ if (!runNewestCard()) useStore.getState().showToast({ message: "no connect card waiting for action" });
6215
+ break;
6216
+ case "help":
6217
+ setHelpOpen(true);
6218
+ break;
6219
+ case "quit":
6220
+ quit();
6221
+ break;
6222
+ case "noop": break;
6223
+ default: {
6224
+ const exhaustive = routed;
6225
+ throw new Error(`unhandled route: ${JSON.stringify(exhaustive)}`);
6226
+ }
6227
+ }
6228
+ return;
6229
+ }
6230
+ history.append(content);
6231
+ setInput("");
6232
+ composerRef.current?.clear();
6233
+ setComposerRows(1);
6234
+ setAttachments([]);
6235
+ sendContent(content, staged);
6236
+ }, [
6237
+ history,
6238
+ runLocalShell,
6239
+ runSandboxExec,
6240
+ runSandboxPty,
6241
+ toggleShare,
6242
+ openInBrowser,
6243
+ quit,
6244
+ sendContent,
6245
+ goTo,
6246
+ agent,
6247
+ appUrl,
6248
+ workspaceName,
6249
+ conversationId,
6250
+ model,
6251
+ portal.status,
6252
+ portal.grantedAgentIds,
6253
+ runNewestCard
6254
+ ]);
6255
+ const submit = useCallback(() => {
6256
+ const staged = attachments.filter((a) => a.status !== "error");
6257
+ executeLine(input.trim(), staged);
6258
+ }, [
6259
+ executeLine,
6260
+ input,
6261
+ attachments
6262
+ ]);
6263
+ const acceptCompletion = useCallback((command, execute) => {
6264
+ if (execute && command.argsHint === null) {
6265
+ executeLine(`/${command.name}`, []);
6266
+ return;
6267
+ }
6268
+ applyComposerText(`/${command.name} `, "end");
6269
+ }, [executeLine, applyComposerText]);
5918
6270
  useEffect(() => {
5919
6271
  if (!portalClient || portal.status !== "connected") return;
5920
6272
  if (portal.grantedAgentIds.includes(agent.id)) return;
@@ -5960,6 +6312,38 @@ function ChatScreen({ agent, conversation }) {
5960
6312
  }
5961
6313
  if (!(key.name === "c" && key.ctrl || key.name === "pageup" || key.name === "pgup" || key.name === "pagedown" || key.name === "pgdn")) return;
5962
6314
  }
6315
+ if (menuOpen) {
6316
+ if (key.name === "escape") {
6317
+ setMenuDismissed(true);
6318
+ return;
6319
+ }
6320
+ const selected = menuCommands[menuHighlightClamped];
6321
+ if (selected) {
6322
+ if (key.name === "up") {
6323
+ setMenuHighlight((h) => Math.max(0, Math.min(h, menuCommands.length - 1) - 1));
6324
+ key.preventDefault();
6325
+ return;
6326
+ }
6327
+ if (key.name === "down") {
6328
+ setMenuHighlight((h) => Math.min(menuCommands.length - 1, h + 1));
6329
+ key.preventDefault();
6330
+ return;
6331
+ }
6332
+ if (key.name === "tab") {
6333
+ acceptCompletion(selected, false);
6334
+ key.preventDefault();
6335
+ return;
6336
+ }
6337
+ if ((key.name === "return" || key.name === "kpenter") && !key.shift) {
6338
+ const query = (commandQuery ?? "").toLowerCase();
6339
+ if (!(query === selected.name || selected.aliases.includes(query)) || selected.argsHint === null) {
6340
+ acceptCompletion(selected, true);
6341
+ key.preventDefault();
6342
+ return;
6343
+ }
6344
+ }
6345
+ }
6346
+ }
5963
6347
  const composer = composerRef.current;
5964
6348
  if (composer && !grantPrompt && !credPrompt && (key.name === "up" || key.name === "down")) {
5965
6349
  if (key.name === "up") {
@@ -5973,6 +6357,7 @@ function ChatScreen({ agent, conversation }) {
5973
6357
  const text = history.previous(composer.plainText);
5974
6358
  if (text === null) return;
5975
6359
  applyComposerText(text, "start");
6360
+ if (detectCommandTrigger(text) !== null) setMenuDismissed(true);
5976
6361
  key.preventDefault();
5977
6362
  return;
5978
6363
  }
@@ -5987,6 +6372,7 @@ function ChatScreen({ agent, conversation }) {
5987
6372
  const text = history.next(composer.plainText);
5988
6373
  if (text === null) return;
5989
6374
  applyComposerText(text, "end");
6375
+ if (detectCommandTrigger(text) !== null) setMenuDismissed(true);
5990
6376
  key.preventDefault();
5991
6377
  return;
5992
6378
  }
@@ -5995,8 +6381,7 @@ function ChatScreen({ agent, conversation }) {
5995
6381
  return;
5996
6382
  }
5997
6383
  if (key.name === "r" && key.ctrl) {
5998
- const target = [...itemsRef.current].reverse().find(isActionableCard);
5999
- if (target) runCardAction(target);
6384
+ runNewestCard();
6000
6385
  return;
6001
6386
  }
6002
6387
  if (key.name === "t" && key.ctrl) {
@@ -6161,6 +6546,15 @@ function ChatScreen({ agent, conversation }) {
6161
6546
  children: "ctrl+x remove last"
6162
6547
  })]
6163
6548
  }) : null,
6549
+ menuOpen ? /* @__PURE__ */ jsx(CompletionMenu, {
6550
+ items: menuCommands.map((c) => ({
6551
+ id: c.name,
6552
+ label: `/${c.name}`,
6553
+ hint: c.argsHint,
6554
+ description: c.description
6555
+ })),
6556
+ highlight: menuHighlightClamped
6557
+ }) : null,
6164
6558
  credPrompt ? /* @__PURE__ */ jsx(CredentialPromptBox, {
6165
6559
  prompt: credPrompt,
6166
6560
  height: credPromptHeight,
@@ -6531,7 +6925,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6531
6925
  screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
6532
6926
  agent: screen.agent,
6533
6927
  conversation: screen.conversation
6534
- })
6928
+ }, "id" in screen.conversation ? screen.conversation.id : screen.conversation.nonce)
6535
6929
  ]
6536
6930
  })
6537
6931
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.297",
3
+ "version": "0.1.0-beta.311",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",