skydive-cli 0.1.0-beta.291 → 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.291";
19
+ var version$1 = "0.1.0-beta.311";
20
20
 
21
21
  //#endregion
22
22
  //#region src/types.ts
@@ -1023,7 +1023,12 @@ function createRestClient({ appUrl, sessionToken }) {
1023
1023
  }, sendResultSchema),
1024
1024
  activeRun: async ({ conversationId }) => {
1025
1025
  const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
1026
- return run;
1026
+ if (!run) return null;
1027
+ return {
1028
+ runId: run.runId,
1029
+ agentId: run.agentId ?? "",
1030
+ agentName: run.agentName ?? ""
1031
+ };
1027
1032
  },
1028
1033
  cancelRun: async ({ runId }) => {
1029
1034
  await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
@@ -1187,7 +1192,11 @@ const uiMessagePartSchema = z.union([
1187
1192
  const uiMessageSchema = z.object({
1188
1193
  id: z.string(),
1189
1194
  role: z.string(),
1190
- parts: z.array(uiMessagePartSchema)
1195
+ parts: z.array(uiMessagePartSchema),
1196
+ metadata: z.object({ custom: z.object({
1197
+ agentId: z.string().nullish(),
1198
+ agentName: z.string().nullish()
1199
+ }).passthrough().optional() }).passthrough().optional()
1191
1200
  });
1192
1201
  const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nullable() });
1193
1202
  const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
@@ -1196,6 +1205,8 @@ const sendResultSchema = z.object({
1196
1205
  messageId: z.string().uuid().nullish(),
1197
1206
  conversationId: z.string().uuid(),
1198
1207
  isNewConversation: z.boolean(),
1208
+ agentId: z.string().nullish(),
1209
+ agentName: z.string().nullish(),
1199
1210
  steered: z.boolean().optional(),
1200
1211
  directive: z.object({ id: z.string() }).passthrough().optional()
1201
1212
  });
@@ -1210,7 +1221,11 @@ const finalizeResponseSchema = z.object({
1210
1221
  mediaType: z.string(),
1211
1222
  sizeBytes: z.number().nullable().optional()
1212
1223
  });
1213
- const activeRunResponseSchema = z.object({ run: z.object({ runId: z.string() }).nullable() });
1224
+ const activeRunResponseSchema = z.object({ run: z.object({
1225
+ runId: z.string(),
1226
+ agentId: z.string().nullish(),
1227
+ agentName: z.string().nullish()
1228
+ }).nullable() });
1214
1229
  const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
1215
1230
  const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
1216
1231
  const runStreamEventSchema = z.union([z.object({
@@ -2592,7 +2607,7 @@ const chatCommand = {
2592
2607
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2593
2608
  process.exit(1);
2594
2609
  }
2595
- const { runChat } = await import("./boot-fBjqXxKo.mjs");
2610
+ const { runChat } = await import("./boot-DEX6CwVM.mjs");
2596
2611
  await runChat({
2597
2612
  appUrl,
2598
2613
  sessionToken: session.value.sessionToken,
@@ -3375,7 +3390,7 @@ const switchCommand = {
3375
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.");
3376
3391
  process.exit(1);
3377
3392
  }
3378
- const { runWorkspacePicker } = await import("./boot-fBjqXxKo.mjs");
3393
+ const { runWorkspacePicker } = await import("./boot-DEX6CwVM.mjs");
3379
3394
  await runWorkspacePicker(session);
3380
3395
  return;
3381
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", {
@@ -3027,16 +3042,23 @@ function str(chunk, key) {
3027
3042
  const value = chunk[key];
3028
3043
  return typeof value === "string" ? value : null;
3029
3044
  }
3030
- function applyChunk(items, chunk) {
3045
+ /**
3046
+ * `agentName` is the display name of the agent producing this run's stream.
3047
+ * It's stamped onto each assistant-origin item so a multi-agent conversation
3048
+ * can attribute live turns; pass null in a solo thread to leave items
3049
+ * un-attributed (the chat screen only renders the name when the thread has
3050
+ * more than one agent).
3051
+ */
3052
+ function applyChunk(items, chunk, agentName = null) {
3031
3053
  const type = str(chunk, "type");
3032
3054
  if (!type) return [...items];
3033
3055
  switch (type) {
3034
- case "text-start": return appendAssistantTextStart(items, str(chunk, "id") ?? cryptoId());
3056
+ case "text-start": return appendAssistantTextStart(items, str(chunk, "id") ?? cryptoId(), agentName);
3035
3057
  case "text-delta": {
3036
3058
  const id = str(chunk, "id");
3037
3059
  const delta = str(chunk, "delta") ?? str(chunk, "text") ?? "";
3038
3060
  if (!id || !delta) return [...items];
3039
- return appendAssistantTextDelta(items, id, delta);
3061
+ return appendAssistantTextDelta(items, id, delta, agentName);
3040
3062
  }
3041
3063
  case "text-end": {
3042
3064
  const id = str(chunk, "id");
@@ -3049,14 +3071,15 @@ function applyChunk(items, chunk) {
3049
3071
  kind: "reasoning",
3050
3072
  id: bubbleId,
3051
3073
  text: "",
3052
- done: false
3074
+ done: false,
3075
+ agentName
3053
3076
  }];
3054
3077
  }
3055
3078
  case "reasoning-delta": {
3056
3079
  const id = str(chunk, "id");
3057
3080
  const delta = str(chunk, "delta") ?? str(chunk, "text") ?? "";
3058
3081
  if (!id || !delta) return [...items];
3059
- return appendReasoningDelta(items, id, delta);
3082
+ return appendReasoningDelta(items, id, delta, agentName);
3060
3083
  }
3061
3084
  case "reasoning-end": {
3062
3085
  const id = str(chunk, "id");
@@ -3079,7 +3102,8 @@ function applyChunk(items, chunk) {
3079
3102
  inputText: "",
3080
3103
  input: null,
3081
3104
  output: null,
3082
- state: "streaming"
3105
+ state: "streaming",
3106
+ agentName
3083
3107
  }];
3084
3108
  }
3085
3109
  case "tool-input-delta": {
@@ -3108,7 +3132,8 @@ function applyChunk(items, chunk) {
3108
3132
  inputText: "",
3109
3133
  input,
3110
3134
  output: null,
3111
- state: "input-ready"
3135
+ state: "input-ready",
3136
+ agentName
3112
3137
  }];
3113
3138
  }
3114
3139
  case "tool-output-available": {
@@ -3165,7 +3190,8 @@ function applyChunk(items, chunk) {
3165
3190
  id,
3166
3191
  card,
3167
3192
  status: "idle",
3168
- detail: null
3193
+ detail: null,
3194
+ agentName
3169
3195
  };
3170
3196
  if (items.some((m) => m.kind === "card" && m.id === id)) return items.map((m) => m.kind === "card" && m.id === id ? next : m);
3171
3197
  return [...items, next];
@@ -3206,16 +3232,17 @@ function openTextIndex(items, id, kind) {
3206
3232
  }
3207
3233
  return -1;
3208
3234
  }
3209
- function appendAssistantTextStart(items, id) {
3235
+ function appendAssistantTextStart(items, id, agentName) {
3210
3236
  const bubbleId = freshTextId(items, id, "assistant-text");
3211
3237
  return [...items, {
3212
3238
  kind: "assistant-text",
3213
3239
  id: bubbleId,
3214
3240
  text: "",
3215
- done: false
3241
+ done: false,
3242
+ agentName
3216
3243
  }];
3217
3244
  }
3218
- function appendAssistantTextDelta(items, id, delta) {
3245
+ function appendAssistantTextDelta(items, id, delta, agentName) {
3219
3246
  const idx = openTextIndex(items, id, "assistant-text");
3220
3247
  if (idx === -1) {
3221
3248
  const bubbleId = freshTextId(items, id, "assistant-text");
@@ -3223,7 +3250,8 @@ function appendAssistantTextDelta(items, id, delta) {
3223
3250
  kind: "assistant-text",
3224
3251
  id: bubbleId,
3225
3252
  text: delta,
3226
- done: false
3253
+ done: false,
3254
+ agentName
3227
3255
  }];
3228
3256
  }
3229
3257
  return items.map((m, i) => i === idx && m.kind === "assistant-text" ? {
@@ -3231,7 +3259,7 @@ function appendAssistantTextDelta(items, id, delta) {
3231
3259
  text: m.text + delta
3232
3260
  } : m);
3233
3261
  }
3234
- function appendReasoningDelta(items, id, delta) {
3262
+ function appendReasoningDelta(items, id, delta, agentName) {
3235
3263
  const idx = openTextIndex(items, id, "reasoning");
3236
3264
  if (idx === -1) {
3237
3265
  const bubbleId = freshTextId(items, id, "reasoning");
@@ -3239,7 +3267,8 @@ function appendReasoningDelta(items, id, delta) {
3239
3267
  kind: "reasoning",
3240
3268
  id: bubbleId,
3241
3269
  text: delta,
3242
- done: false
3270
+ done: false,
3271
+ agentName
3243
3272
  }];
3244
3273
  }
3245
3274
  return items.map((m, i) => i === idx && m.kind === "reasoning" ? {
@@ -3269,6 +3298,59 @@ function cryptoId() {
3269
3298
  return crypto.randomUUID();
3270
3299
  }
3271
3300
 
3301
+ //#endregion
3302
+ //#region src/chat/tui/chat/attribution.ts
3303
+ /**
3304
+ * The agent that authored an item, or null for items with no agent (the user's
3305
+ * own turns, errors, the recap).
3306
+ */
3307
+ function itemAgentName(m) {
3308
+ return "agentName" in m ? m.agentName ?? null : null;
3309
+ }
3310
+ /**
3311
+ * Items that end an agent's turn: the human speaking or acting. The next
3312
+ * assistant item after one of these starts a new turn and is re-headed with its
3313
+ * author, even when the same agent replies again — in a multi-agent thread the
3314
+ * reader can't be asked to scan back past their own message to work out who is
3315
+ * answering.
3316
+ */
3317
+ function isTurnSeparator(m) {
3318
+ return m.kind === "user" || m.kind === "pending-steer" || m.kind === "shell";
3319
+ }
3320
+ /**
3321
+ * Every agent that has spoken in the transcript, plus the agent on screen.
3322
+ * Attribution renders only when this holds more than one name, mirroring web's
3323
+ * group-chat gate so a solo thread carries no attribution chrome at all.
3324
+ */
3325
+ function conversationAgentNames(items, onScreenAgentName) {
3326
+ const names = new Set([onScreenAgentName]);
3327
+ for (const item of items) {
3328
+ const name = itemAgentName(item);
3329
+ if (name) names.add(name);
3330
+ }
3331
+ return names;
3332
+ }
3333
+ /**
3334
+ * The agent-name label to render above `items[i]`, or null for no label. Every
3335
+ * assistant turn is headed with its author's name. Within one turn the items
3336
+ * (text, reasoning, tool calls) share the single header, so a turn isn't
3337
+ * re-labeled on each tool row; `error` and `recap` items are transparent and
3338
+ * don't break that grouping.
3339
+ */
3340
+ function agentLabelFor(items, i) {
3341
+ const current = items[i];
3342
+ const name = current ? itemAgentName(current) : null;
3343
+ if (!name) return null;
3344
+ for (let j = i - 1; j >= 0; j--) {
3345
+ const prevItem = items[j];
3346
+ if (!prevItem) continue;
3347
+ if (isTurnSeparator(prevItem)) return name;
3348
+ const prev = itemAgentName(prevItem);
3349
+ if (prev !== null) return prev === name ? null : name;
3350
+ }
3351
+ return name;
3352
+ }
3353
+
3272
3354
  //#endregion
3273
3355
  //#region src/chat/tui/chat/history.ts
3274
3356
  function isDynamicTool(part) {
@@ -3297,6 +3379,7 @@ function uiMessagesToItems(messages) {
3297
3379
  continue;
3298
3380
  }
3299
3381
  if (m.role === "assistant") {
3382
+ const agentName = m.metadata?.custom?.agentName ?? null;
3300
3383
  let textBuf = "";
3301
3384
  let textIdx = 0;
3302
3385
  for (const part of m.parts) {
@@ -3309,7 +3392,8 @@ function uiMessagesToItems(messages) {
3309
3392
  kind: "assistant-text",
3310
3393
  id: `${m.id}#t${textIdx++}`,
3311
3394
  text: textBuf,
3312
- done: true
3395
+ done: true,
3396
+ agentName
3313
3397
  });
3314
3398
  textBuf = "";
3315
3399
  }
@@ -3318,7 +3402,8 @@ function uiMessagesToItems(messages) {
3318
3402
  kind: "reasoning",
3319
3403
  id: `${m.id}#r${textIdx++}`,
3320
3404
  text: "text" in part && typeof part.text === "string" ? part.text : "",
3321
- done: true
3405
+ done: true,
3406
+ agentName
3322
3407
  });
3323
3408
  continue;
3324
3409
  }
@@ -3332,7 +3417,8 @@ function uiMessagesToItems(messages) {
3332
3417
  id: toolCallId ?? `${m.id}#c${textIdx++}`,
3333
3418
  card,
3334
3419
  status: "idle",
3335
- detail: null
3420
+ detail: null,
3421
+ agentName
3336
3422
  });
3337
3423
  }
3338
3424
  continue;
@@ -3352,7 +3438,8 @@ function uiMessagesToItems(messages) {
3352
3438
  kind: "ok",
3353
3439
  value: part.output
3354
3440
  } : null,
3355
- state
3441
+ state,
3442
+ agentName
3356
3443
  });
3357
3444
  }
3358
3445
  }
@@ -3360,7 +3447,8 @@ function uiMessagesToItems(messages) {
3360
3447
  kind: "assistant-text",
3361
3448
  id: `${m.id}#t${textIdx++}`,
3362
3449
  text: textBuf,
3363
- done: true
3450
+ done: true,
3451
+ agentName
3364
3452
  });
3365
3453
  }
3366
3454
  }
@@ -3381,7 +3469,7 @@ function mapToolState(raw) {
3381
3469
  function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer, hasActionableCard }) {
3382
3470
  const connectHint = hasActionableCard ? "ctrl+r connect · " : "";
3383
3471
  switch (runKind) {
3384
- 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`;
3385
3473
  case "sending": return "sending…";
3386
3474
  case "streaming": return hasPendingSteer ? `${connectHint}↵ steer · esc cancel steer · ctrl+c cancel run` : `${connectHint}↵ steer · ctrl+c cancel`;
3387
3475
  }
@@ -3391,6 +3479,31 @@ function formatModelHint(model) {
3391
3479
  return `ctrl+p · ${model ?? "default"}`;
3392
3480
  }
3393
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
+
3394
3507
  //#endregion
3395
3508
  //#region src/chat/notification-preview.ts
3396
3509
  const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
@@ -3677,12 +3790,37 @@ const keybindGroups = [
3677
3790
  keys: "pgup / pgdn",
3678
3791
  action: "scroll transcript"
3679
3792
  },
3793
+ {
3794
+ keys: "/",
3795
+ action: "open the command menu (start of message)"
3796
+ },
3680
3797
  {
3681
3798
  keys: "?",
3682
3799
  action: "open this help (composer empty)"
3683
3800
  }
3684
3801
  ]
3685
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
+ },
3686
3824
  {
3687
3825
  title: "agent picker",
3688
3826
  binds: [
@@ -3933,6 +4071,182 @@ function HelpModal({ onClose }) {
3933
4071
  });
3934
4072
  }
3935
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
+
3936
4250
  //#endregion
3937
4251
  //#region src/chat/tui/screens/model-picker.tsx
3938
4252
  /**
@@ -4759,28 +5073,35 @@ function routeInput(raw) {
4759
5073
  command
4760
5074
  } : { kind: "noop" };
4761
5075
  }
4762
- const sandbox = matchSlash(line, ["sandbox", "sbx"]);
4763
- if (sandbox !== null) return sandbox ? {
4764
- kind: "sandbox-exec",
4765
- command: sandbox
4766
- } : { 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
+ }
4767
5099
  if (line.trim().toLowerCase() === "exit") return { kind: "quit" };
4768
5100
  return {
4769
5101
  kind: "message",
4770
5102
  content: raw
4771
5103
  };
4772
5104
  }
4773
- /**
4774
- * If `line` is `/<name>` or `/<name> <rest>` for one of `names`, return the
4775
- * trimmed rest (possibly ''); otherwise null. Case-insensitive on the name.
4776
- */
4777
- function matchSlash(line, names) {
4778
- if (!line.startsWith("/")) return null;
4779
- const sp = line.indexOf(" ");
4780
- const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
4781
- if (!names.includes(name)) return null;
4782
- return sp === -1 ? "" : line.slice(sp + 1).trim();
4783
- }
4784
5105
  /** Max characters of `!` output we forward to the agent as context. */
4785
5106
  const SHELL_CONTEXT_OUTPUT_CAP = 16e3;
4786
5107
  /**
@@ -5066,6 +5387,7 @@ function ChatScreen({ agent, conversation }) {
5066
5387
  const sessionToken = useStore((s) => s.sessionToken);
5067
5388
  const goTo = useStore((s) => s.goTo);
5068
5389
  const setChatTitle = useStore((s) => s.setChatTitle);
5390
+ const workspaceName = useStore((s) => s.workspaceName);
5069
5391
  const portal = useStore((s) => s.portal);
5070
5392
  const portalClient = useStore((s) => s.portalClient);
5071
5393
  const setPortalPrompt = useStore((s) => s.setPortalPrompt);
@@ -5089,6 +5411,12 @@ function ChatScreen({ agent, conversation }) {
5089
5411
  const uploadsRef = useRef(/* @__PURE__ */ new Map());
5090
5412
  const [credPrompt, setCredPrompt] = useState(null);
5091
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));
5092
5420
  const runRef = useRef(run);
5093
5421
  runRef.current = run;
5094
5422
  const itemsRef = useRef(items);
@@ -5118,7 +5446,8 @@ function ChatScreen({ agent, conversation }) {
5118
5446
  const pendingRows = pendingVisible ? attachments.length + 2 : 0;
5119
5447
  const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
5120
5448
  const bottomBoxHeight = credPrompt ? credPromptHeight : composerBoxHeight;
5121
- 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);
5122
5451
  useEffect(() => {
5123
5452
  const composer = composerRef.current;
5124
5453
  if (!composer) return;
@@ -5168,10 +5497,11 @@ function ChatScreen({ agent, conversation }) {
5168
5497
  cancelled = true;
5169
5498
  };
5170
5499
  }, [rest, initialConversationId]);
5171
- const attachToRun = useCallback((runId) => {
5500
+ const attachToRun = useCallback((runId, runAgentName) => {
5172
5501
  if (!rest) return;
5173
5502
  const abort = new AbortController();
5174
5503
  const responsePreview = createResponsePreview();
5504
+ const streamAgentName = runAgentName ?? agent.name;
5175
5505
  setRun({
5176
5506
  kind: "streaming",
5177
5507
  runId,
@@ -5183,7 +5513,7 @@ function ChatScreen({ agent, conversation }) {
5183
5513
  onEvent: (event) => {
5184
5514
  if (event.kind === "chunk") {
5185
5515
  responsePreview.addChunk(event.chunk);
5186
- setItems((prev) => applyChunk(prev, event.chunk));
5516
+ setItems((prev) => applyChunk(prev, event.chunk, streamAgentName));
5187
5517
  return;
5188
5518
  }
5189
5519
  const errorText = event.error;
@@ -5221,6 +5551,7 @@ function ChatScreen({ agent, conversation }) {
5221
5551
  agent.name,
5222
5552
  agentHost
5223
5553
  ]);
5554
+ const multiAgent = useMemo(() => conversationAgentNames(items, agent.name), [items, agent.name]).size > 1;
5224
5555
  useEffect(() => {
5225
5556
  if (!rest || !conversationId) return;
5226
5557
  const abort = new AbortController();
@@ -5244,7 +5575,7 @@ function ChatScreen({ agent, conversation }) {
5244
5575
  try {
5245
5576
  const active = await rest.activeRun({ conversationId });
5246
5577
  if (cancelled || !active) return;
5247
- attachToRun(active.runId);
5578
+ attachToRun(active.runId, active.agentName || null);
5248
5579
  } catch (_error) {}
5249
5580
  })();
5250
5581
  return () => {
@@ -5311,10 +5642,10 @@ function ChatScreen({ agent, conversation }) {
5311
5642
  text: trimmed
5312
5643
  } : m));
5313
5644
  const current = runRef.current;
5314
- if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId);
5645
+ if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId, result.agentName ?? null);
5315
5646
  return;
5316
5647
  }
5317
- attachToRun(result.runId);
5648
+ attachToRun(result.runId, result.agentName ?? null);
5318
5649
  } catch (err) {
5319
5650
  setItems((prev) => [...prev.filter((m) => m.id !== optimisticId), {
5320
5651
  kind: "error",
@@ -5449,6 +5780,12 @@ function ChatScreen({ agent, conversation }) {
5449
5780
  portalClient,
5450
5781
  portal.status
5451
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]);
5452
5789
  const handleCredInput = useCallback((displayed) => {
5453
5790
  setCredPrompt((prev) => {
5454
5791
  if (!prev || prev.submitting) return prev;
@@ -5625,42 +5962,12 @@ function ChatScreen({ agent, conversation }) {
5625
5962
  agentHost,
5626
5963
  renderer
5627
5964
  ]);
5628
- const submit = useCallback(() => {
5629
- const content = input.trim();
5630
- const staged = attachments.filter((a) => a.status !== "error");
5631
- if (!content && staged.length === 0) return;
5632
- const routed = routeInput(content);
5633
- if (routed.kind !== "message") {
5634
- history.append(content);
5635
- setInput("");
5636
- composerRef.current?.clear();
5637
- setComposerRows(1);
5638
- if (routed.kind === "local-shell") runLocalShell(routed.command);
5639
- else if (routed.kind === "sandbox-exec") runSandboxExec(routed.command);
5640
- else if (routed.kind === "sandbox-pty") runSandboxPty();
5641
- else if (routed.kind === "quit") quit();
5642
- return;
5643
- }
5644
- history.append(content);
5645
- setInput("");
5646
- composerRef.current?.clear();
5647
- setComposerRows(1);
5648
- setAttachments([]);
5649
- sendContent(content, staged);
5650
- }, [
5651
- input,
5652
- attachments,
5653
- sendContent,
5654
- history,
5655
- runLocalShell,
5656
- runSandboxExec,
5657
- runSandboxPty,
5658
- quit
5659
- ]);
5660
5965
  const handleComposerChange = useCallback(() => {
5661
5966
  const composer = composerRef.current;
5662
5967
  if (!composer) return;
5663
5968
  setInput(composer.plainText);
5969
+ setMenuHighlight(0);
5970
+ if (detectCommandTrigger(composer.plainText) === null) setMenuDismissed(false);
5664
5971
  setComposerRows(Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), maxComposerRows));
5665
5972
  }, []);
5666
5973
  /**
@@ -5840,6 +6147,126 @@ function ChatScreen({ agent, conversation }) {
5840
6147
  setPortalPrompt(null);
5841
6148
  useStore.getState().showToast({ message: "kept this machine private" });
5842
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]);
5843
6270
  useEffect(() => {
5844
6271
  if (!portalClient || portal.status !== "connected") return;
5845
6272
  if (portal.grantedAgentIds.includes(agent.id)) return;
@@ -5885,6 +6312,38 @@ function ChatScreen({ agent, conversation }) {
5885
6312
  }
5886
6313
  if (!(key.name === "c" && key.ctrl || key.name === "pageup" || key.name === "pgup" || key.name === "pagedown" || key.name === "pgdn")) return;
5887
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
+ }
5888
6347
  const composer = composerRef.current;
5889
6348
  if (composer && !grantPrompt && !credPrompt && (key.name === "up" || key.name === "down")) {
5890
6349
  if (key.name === "up") {
@@ -5898,6 +6357,7 @@ function ChatScreen({ agent, conversation }) {
5898
6357
  const text = history.previous(composer.plainText);
5899
6358
  if (text === null) return;
5900
6359
  applyComposerText(text, "start");
6360
+ if (detectCommandTrigger(text) !== null) setMenuDismissed(true);
5901
6361
  key.preventDefault();
5902
6362
  return;
5903
6363
  }
@@ -5912,6 +6372,7 @@ function ChatScreen({ agent, conversation }) {
5912
6372
  const text = history.next(composer.plainText);
5913
6373
  if (text === null) return;
5914
6374
  applyComposerText(text, "end");
6375
+ if (detectCommandTrigger(text) !== null) setMenuDismissed(true);
5915
6376
  key.preventDefault();
5916
6377
  return;
5917
6378
  }
@@ -5920,8 +6381,7 @@ function ChatScreen({ agent, conversation }) {
5920
6381
  return;
5921
6382
  }
5922
6383
  if (key.name === "r" && key.ctrl) {
5923
- const target = [...itemsRef.current].reverse().find(isActionableCard);
5924
- if (target) runCardAction(target);
6384
+ runNewestCard();
5925
6385
  return;
5926
6386
  }
5927
6387
  if (key.name === "t" && key.ctrl) {
@@ -6045,7 +6505,22 @@ function ChatScreen({ agent, conversation }) {
6045
6505
  agent.name,
6046
6506
  " below."
6047
6507
  ]
6048
- }) : /* @__PURE__ */ jsx(Fragment, { children: items.map((item) => /* @__PURE__ */ jsx(RenderItem, { item }, item.id)) }), run.kind !== "idle" ? /* @__PURE__ */ jsx(ActivityRow, { label: run.kind === "sending" ? "sending" : "working" }) : null]
6508
+ }) : /* @__PURE__ */ jsx(Fragment, { children: items.map((item, i) => {
6509
+ const label = multiAgent ? agentLabelFor(items, i) : null;
6510
+ return /* @__PURE__ */ jsxs("box", {
6511
+ style: { flexDirection: "column" },
6512
+ children: [label ? /* @__PURE__ */ jsx("box", {
6513
+ style: {
6514
+ paddingLeft: 3,
6515
+ paddingRight: 1
6516
+ },
6517
+ children: /* @__PURE__ */ jsx("text", {
6518
+ fg: theme.muted,
6519
+ children: /* @__PURE__ */ jsx("b", { children: label })
6520
+ })
6521
+ }) : null, /* @__PURE__ */ jsx(RenderItem, { item })]
6522
+ }, item.id);
6523
+ }) }), run.kind !== "idle" ? /* @__PURE__ */ jsx(ActivityRow, { label: run.kind === "sending" ? "sending" : "working" }) : null]
6049
6524
  })
6050
6525
  }),
6051
6526
  pendingVisible ? /* @__PURE__ */ jsxs("box", {
@@ -6071,6 +6546,15 @@ function ChatScreen({ agent, conversation }) {
6071
6546
  children: "ctrl+x remove last"
6072
6547
  })]
6073
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,
6074
6558
  credPrompt ? /* @__PURE__ */ jsx(CredentialPromptBox, {
6075
6559
  prompt: credPrompt,
6076
6560
  height: credPromptHeight,
@@ -6441,7 +6925,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6441
6925
  screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
6442
6926
  agent: screen.agent,
6443
6927
  conversation: screen.conversation
6444
- })
6928
+ }, "id" in screen.conversation ? screen.conversation.id : screen.conversation.nonce)
6445
6929
  ]
6446
6930
  })
6447
6931
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.291",
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",