skydive-cli 0.1.0-beta.276 → 0.1.0-beta.286

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 { createParser } from "eventsource-parser";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region package.json
19
- var version$1 = "0.1.0-beta.276";
19
+ var version$1 = "0.1.0-beta.286";
20
20
 
21
21
  //#endregion
22
22
  //#region src/types.ts
@@ -2137,7 +2137,7 @@ const chatCommand = {
2137
2137
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2138
2138
  process.exit(1);
2139
2139
  }
2140
- const { runChat } = await import("./boot-FPpnUcyM.mjs");
2140
+ const { runChat } = await import("./boot-D23rGRlW.mjs");
2141
2141
  await runChat({
2142
2142
  appUrl,
2143
2143
  sessionToken: session.value.sessionToken,
@@ -3345,7 +3345,7 @@ const switchCommand = {
3345
3345
  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.");
3346
3346
  process.exit(1);
3347
3347
  }
3348
- const { runWorkspacePicker } = await import("./boot-FPpnUcyM.mjs");
3348
+ const { runWorkspacePicker } = await import("./boot-D23rGRlW.mjs");
3349
3349
  await runWorkspacePicker(session);
3350
3350
  return;
3351
3351
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { A as themesForMode, C as monoTheme, D as themeMode, E as themeForMode, F as DEFAULT_APP_URL, I as getConfigPath, L as getSavedTheme, M as listWorkspaces, N as setActiveWorkspace, O as themeModeFromColorFgBg, P as DEFAULT_API_URL, R as resolveWebUrl, S as findTheme, T as theme, _ as isRecord, b as DEFAULT_THEME_ID, c as resolveAgent, d as parseExternalOauthConnectParams, f as parseOauthConnectParams, g as errorMessage, h as parseConnectCard, j as getActiveWorkspaceId, k as themeVersion, l as MASK_CHAR, m as resolveConnectUrl, p as reconcileMaskedInput, u as cardActionErrorMessage, v as HttpError, w as noColorRequested, x as applyTheme, y as createRestClient, z as saveTheme } from "./bin.mjs";
3
3
  import { t as PortalClient } from "./client-_OL8-XGH.mjs";
4
- import path, { basename, isAbsolute, join, win32 } from "node:path";
4
+ import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
5
5
  import { z } from "zod";
6
6
  import open from "open";
7
7
  import { execFile, spawn } from "node:child_process";
@@ -1559,7 +1559,7 @@ function parseDroppedPaths(text) {
1559
1559
  i += 1;
1560
1560
  continue;
1561
1561
  }
1562
- if (/\s/.test(ch)) {
1562
+ if (ch === " " || ch === " ") {
1563
1563
  if (current) {
1564
1564
  tokens.push(current);
1565
1565
  current = "";
@@ -1585,6 +1585,28 @@ function parseDroppedPaths(text) {
1585
1585
  }
1586
1586
  return paths;
1587
1587
  }
1588
+ const TEXT_MIME_BY_EXT = {
1589
+ ".txt": "text/plain",
1590
+ ".md": "text/markdown",
1591
+ ".markdown": "text/markdown",
1592
+ ".csv": "text/csv",
1593
+ ".tsv": "text/tab-separated-values",
1594
+ ".json": "application/json",
1595
+ ".yaml": "application/yaml",
1596
+ ".yml": "application/yaml",
1597
+ ".xml": "application/xml",
1598
+ ".html": "text/html",
1599
+ ".htm": "text/html",
1600
+ ".css": "text/css",
1601
+ ".js": "text/javascript",
1602
+ ".ts": "text/plain",
1603
+ ".tsx": "text/plain",
1604
+ ".jsx": "text/plain",
1605
+ ".py": "text/x-python",
1606
+ ".sh": "text/x-shellscript",
1607
+ ".log": "text/plain",
1608
+ ".svg": "image/svg+xml"
1609
+ };
1588
1610
  /** Decide how to handle a paste. `kind: 'binary'` events carry the mime of
1589
1611
  * bytes the terminal forwarded; text events carry the decoded paste text.
1590
1612
  *
@@ -1610,25 +1632,30 @@ function routePaste(input) {
1610
1632
  return { kind: "text" };
1611
1633
  }
1612
1634
  /**
1613
- * Resolve path candidates into images using the filesystem and file magic,
1614
- * not extensions. The whole batch must be regular image files; otherwise the
1615
- * caller should restore the original paste as text.
1635
+ * Resolve path candidates into attachable files. Every path must be an
1636
+ * existing regular file; otherwise the caller should restore the original
1637
+ * paste as text (an all-or-nothing rule so a paste meant as text — which
1638
+ * merely *looks* path-shaped — is never partially eaten).
1639
+ *
1640
+ * Any file type attaches. The mediaType comes from magic bytes when
1641
+ * detectable, an extension map for the plain-text formats magic can't see,
1642
+ * and application/octet-stream as the last resort — the attachments API
1643
+ * accepts any mediaType (100MB cap enforced server-side at presign).
1616
1644
  */
1617
- async function resolveDroppedImages(paths) {
1645
+ async function resolveDroppedFiles(paths) {
1618
1646
  try {
1619
- const images = await Promise.all(paths.map(async (path) => {
1647
+ const files = await Promise.all(paths.map(async (path) => {
1620
1648
  if (!(await stat(path)).isFile()) return null;
1621
1649
  const data = new Uint8Array(await readFile(path));
1622
- const detected = await fileTypeFromBuffer(data);
1623
- if (!detected?.mime.startsWith("image/")) return null;
1650
+ const mediaType = (await fileTypeFromBuffer(data))?.mime ?? TEXT_MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
1624
1651
  return {
1625
1652
  fileName: basename(path),
1626
- mediaType: detected.mime,
1653
+ mediaType,
1627
1654
  data
1628
1655
  };
1629
1656
  }));
1630
- if (!images.every((image) => image !== null)) return null;
1631
- return images;
1657
+ if (!files.every((file) => file !== null)) return null;
1658
+ return files;
1632
1659
  } catch (_error) {
1633
1660
  return null;
1634
1661
  }
@@ -2774,12 +2801,15 @@ function Row({ barColor, children }) {
2774
2801
  }
2775
2802
  function RenderItem({ item }) {
2776
2803
  switch (item.kind) {
2777
- case "user": return /* @__PURE__ */ jsx(Row, {
2804
+ case "user": return /* @__PURE__ */ jsxs(Row, {
2778
2805
  barColor: theme.accent,
2779
- children: /* @__PURE__ */ jsx("text", {
2806
+ children: [item.text ? /* @__PURE__ */ jsx("text", {
2780
2807
  fg: theme.user,
2781
2808
  children: item.text
2782
- })
2809
+ }) : null, item.attachments && item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
2810
+ fg: theme.muted,
2811
+ children: ["⎘ ", item.attachments.join(" · ")]
2812
+ }) : null]
2783
2813
  });
2784
2814
  case "pending-steer": return /* @__PURE__ */ jsx(Row, {
2785
2815
  barColor: theme.dim,
@@ -3200,10 +3230,15 @@ function uiMessagesToItems(messages) {
3200
3230
  for (const m of messages) {
3201
3231
  if (m.role === "user") {
3202
3232
  const text = m.parts.map((p) => p.type === "text" ? p.text : "").join("").trim();
3203
- if (text) out.push({
3233
+ const attachments = m.parts.filter((p) => p.type === "data-anyone-attachment").map((p) => {
3234
+ const data = isRecord(p) && "data" in p && isRecord(p.data) ? p.data : null;
3235
+ return data && typeof data.fileName === "string" ? data.fileName : "file";
3236
+ });
3237
+ if (text || attachments.length > 0) out.push({
3204
3238
  kind: "user",
3205
3239
  id: m.id,
3206
- text
3240
+ text,
3241
+ ...attachments.length > 0 ? { attachments } : {}
3207
3242
  });
3208
3243
  continue;
3209
3244
  }
@@ -4939,6 +4974,7 @@ const composerKeyBindings = [
4939
4974
  ];
4940
4975
  /** Cap composer growth; past this the textarea scrolls its content instead. */
4941
4976
  const maxComposerRows = 8;
4977
+ const maxAttachments = 10;
4942
4978
  function isNewConversation(c) {
4943
4979
  return "kind" in c && c.kind === "new";
4944
4980
  }
@@ -4947,12 +4983,13 @@ function formatBytes(bytes) {
4947
4983
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
4948
4984
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4949
4985
  }
4950
- /**
4951
- * Builds the transcript bubble text for a sent user message: the typed text
4952
- * plus a line per attachment, so an image-only send still shows something.
4953
- */
4954
- function userItemText(text, attachments) {
4955
- return [text, ...attachments.map((a) => `📎 ${a.fileName}`)].filter((line) => line.length > 0).join("\n");
4986
+ /** Status glyph for a staged attachment chip: uploading / ready / failed. */
4987
+ function attachmentGlyph(a) {
4988
+ switch (a.status) {
4989
+ case "uploading": return "↑";
4990
+ case "ready": return "✓";
4991
+ case "error": return `✗ ${a.errorText ?? "upload failed"}`;
4992
+ }
4956
4993
  }
4957
4994
  /**
4958
4995
  * Open a URL in the user's local browser. Best-effort: over SSH there may be
@@ -4994,14 +5031,16 @@ function ChatScreen({ agent, conversation }) {
4994
5031
  const [helpOpen, setHelpOpen] = useState(false);
4995
5032
  const [ctrlCArmed, setCtrlCArmed] = useState(false);
4996
5033
  const [composerRows, setComposerRows] = useState(1);
4997
- const [pending, setPending] = useState([]);
4998
- const [uploading, setUploading] = useState(false);
5034
+ const [attachments, setAttachments] = useState([]);
5035
+ const uploadsRef = useRef(/* @__PURE__ */ new Map());
4999
5036
  const [credPrompt, setCredPrompt] = useState(null);
5000
5037
  const credPromptOpen = credPrompt !== null;
5001
5038
  const runRef = useRef(run);
5002
5039
  runRef.current = run;
5003
5040
  const itemsRef = useRef(items);
5004
5041
  itemsRef.current = items;
5042
+ const attachmentsRef = useRef(attachments);
5043
+ attachmentsRef.current = attachments;
5005
5044
  const inputRef = useRef(input);
5006
5045
  inputRef.current = input;
5007
5046
  const credPromptRef = useRef(credPrompt);
@@ -5021,8 +5060,8 @@ function ChatScreen({ agent, conversation }) {
5021
5060
  const scrollRef = useRef(null);
5022
5061
  const composerRef = useRef(null);
5023
5062
  const composerBoxHeight = composerRows + 2;
5024
- const pendingVisible = !grantPrompt && !credPrompt && (pending.length > 0 || uploading);
5025
- const pendingRows = pendingVisible ? pending.length + 2 : 0;
5063
+ const pendingVisible = !grantPrompt && !credPrompt && attachments.length > 0;
5064
+ const pendingRows = pendingVisible ? attachments.length + 2 : 0;
5026
5065
  const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
5027
5066
  const bottomBoxHeight = credPrompt ? credPromptHeight : composerBoxHeight;
5028
5067
  const scrollHeight = Math.max(3, height - 5 - bottomBoxHeight - pendingRows);
@@ -5158,34 +5197,64 @@ function ChatScreen({ agent, conversation }) {
5158
5197
  cancelled = true;
5159
5198
  };
5160
5199
  }, []);
5161
- const sendContent = useCallback(async (content, attachments, opts) => {
5200
+ const sendContent = useCallback(async (content, staged, opts) => {
5162
5201
  if (!rest) return;
5163
5202
  const trimmed = content.trim();
5164
- if (!trimmed && attachments.length === 0) return;
5203
+ if (!trimmed && staged.length === 0) return;
5165
5204
  const echo = opts?.echo !== false;
5166
5205
  const optimisticId = crypto.randomUUID();
5206
+ const stagedNames = staged.map((a) => a.fileName);
5167
5207
  if (echo) setItems((prev) => [...prev, {
5168
5208
  kind: "user",
5169
5209
  id: optimisticId,
5170
- text: userItemText(trimmed, attachments)
5210
+ text: trimmed,
5211
+ ...stagedNames.length > 0 ? { attachments: stagedNames } : {}
5171
5212
  }]);
5172
5213
  const wasStreaming = runRef.current.kind === "streaming";
5173
5214
  if (!wasStreaming) setRun({ kind: "sending" });
5174
5215
  try {
5216
+ const attachmentIds = [];
5217
+ const restorable = [];
5218
+ const settled = await Promise.allSettled(staged.map((a) => uploadsRef.current.get(a.tempId)));
5219
+ for (const [i, result] of settled.entries()) {
5220
+ const row = staged[i];
5221
+ if (!row) continue;
5222
+ if (result.status === "fulfilled" && result.value) {
5223
+ attachmentIds.push(result.value.id);
5224
+ restorable.push({
5225
+ ...row,
5226
+ status: "ready",
5227
+ errorText: null
5228
+ });
5229
+ } else {
5230
+ uploadsRef.current.delete(row.tempId);
5231
+ setItems((prev) => [...prev, {
5232
+ kind: "error",
5233
+ id: crypto.randomUUID(),
5234
+ text: `attachment failed: ${row.fileName}${result.status === "rejected" ? ` (${errorMessage(result.reason)})` : ""}`
5235
+ }]);
5236
+ }
5237
+ }
5238
+ if (!trimmed && attachmentIds.length === 0) {
5239
+ setItems((prev) => prev.filter((m) => m.id !== optimisticId));
5240
+ if (!wasStreaming) setRun({ kind: "idle" });
5241
+ return;
5242
+ }
5175
5243
  const result = await rest.sendMessage({
5176
5244
  agentId: agent.id,
5177
5245
  conversationId,
5178
5246
  content: trimmed,
5179
- attachmentIds: attachments.map((a) => a.id),
5247
+ attachmentIds,
5180
5248
  clientSurface: "tui"
5181
5249
  });
5250
+ for (const row of restorable) uploadsRef.current.delete(row.tempId);
5182
5251
  if (result.isNewConversation) setConversationId(result.conversationId);
5183
5252
  if (result.steered) {
5184
5253
  const directiveId = result.directive?.id;
5185
5254
  if (directiveId) setItems((prev) => prev.map((m) => m.id === optimisticId ? {
5186
5255
  kind: "pending-steer",
5187
5256
  id: directiveId,
5188
- text: userItemText(trimmed, attachments)
5257
+ text: trimmed
5189
5258
  } : m));
5190
5259
  const current = runRef.current;
5191
5260
  if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId);
@@ -5198,10 +5267,13 @@ function ChatScreen({ agent, conversation }) {
5198
5267
  id: crypto.randomUUID(),
5199
5268
  text: errorMessage(err)
5200
5269
  }]);
5201
- if (echo) {
5202
- setInput((existing) => existing ? existing : trimmed);
5203
- setPending((prev) => prev.length > 0 ? prev : attachments);
5204
- }
5270
+ if (echo) setInput((existing) => existing ? existing : trimmed);
5271
+ const restore = staged.filter((a) => uploadsRef.current.has(a.tempId));
5272
+ if (restore.length > 0) setAttachments((prev) => [...restore.map((a) => ({
5273
+ ...a,
5274
+ status: "ready",
5275
+ errorText: null
5276
+ })), ...prev]);
5205
5277
  if (!wasStreaming) setRun({ kind: "idle" });
5206
5278
  }
5207
5279
  }, [
@@ -5501,8 +5573,8 @@ function ChatScreen({ agent, conversation }) {
5501
5573
  ]);
5502
5574
  const submit = useCallback(() => {
5503
5575
  const content = input.trim();
5504
- const attachments = pending;
5505
- if (!content && attachments.length === 0) return;
5576
+ const staged = attachments.filter((a) => a.status !== "error");
5577
+ if (!content && staged.length === 0) return;
5506
5578
  const routed = routeInput(content);
5507
5579
  if (routed.kind !== "message") {
5508
5580
  history.append(content);
@@ -5519,11 +5591,11 @@ function ChatScreen({ agent, conversation }) {
5519
5591
  setInput("");
5520
5592
  composerRef.current?.clear();
5521
5593
  setComposerRows(1);
5522
- setPending([]);
5523
- sendContent(content, attachments);
5594
+ setAttachments([]);
5595
+ sendContent(content, staged);
5524
5596
  }, [
5525
5597
  input,
5526
- pending,
5598
+ attachments,
5527
5599
  sendContent,
5528
5600
  history,
5529
5601
  runLocalShell,
@@ -5537,38 +5609,58 @@ function ChatScreen({ agent, conversation }) {
5537
5609
  setInput(composer.plainText);
5538
5610
  setComposerRows(Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), maxComposerRows));
5539
5611
  }, []);
5540
- const stageImages = useCallback(async (images) => {
5541
- if (!rest || uploading || images.length === 0) return;
5542
- setUploading(true);
5543
- try {
5544
- for (const image of images) {
5545
- const uploaded = await rest.uploadAttachment({
5546
- agentId: agent.id,
5547
- fileName: image.fileName,
5548
- mediaType: image.mediaType,
5549
- data: image.data
5550
- });
5551
- setPending((prev) => [...prev, uploaded]);
5552
- }
5553
- } catch (err) {
5612
+ /**
5613
+ * Stage a pasted/dropped file in the composer tray and start its upload
5614
+ * immediately (the web composer's behavior). Submit awaits the in-flight
5615
+ * uploads and carries the resulting attachment ids; the composer never
5616
+ * blocks while an upload runs.
5617
+ */
5618
+ const stageAttachment = useCallback(({ fileName, mediaType, data }) => {
5619
+ if (!rest) return;
5620
+ if (attachmentsRef.current.length >= maxAttachments) {
5554
5621
  setItems((prev) => [...prev, {
5555
5622
  kind: "error",
5556
5623
  id: crypto.randomUUID(),
5557
- text: `couldn't attach image: ${errorMessage(err)}`
5624
+ text: `attachment limit reached (${maxAttachments}) — ${fileName} skipped`
5558
5625
  }]);
5559
- } finally {
5560
- setUploading(false);
5626
+ return;
5561
5627
  }
5562
- }, [
5563
- rest,
5564
- uploading,
5565
- agent.id
5566
- ]);
5628
+ const tempId = crypto.randomUUID();
5629
+ setAttachments((prev) => [...prev, {
5630
+ tempId,
5631
+ fileName,
5632
+ sizeBytes: data.byteLength,
5633
+ status: "uploading",
5634
+ errorText: null
5635
+ }]);
5636
+ const promise = rest.uploadAttachment({
5637
+ agentId: agent.id,
5638
+ fileName,
5639
+ mediaType,
5640
+ data
5641
+ });
5642
+ uploadsRef.current.set(tempId, promise);
5643
+ promise.then(() => {
5644
+ setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
5645
+ ...a,
5646
+ status: "ready"
5647
+ } : a));
5648
+ }).catch((err) => {
5649
+ setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
5650
+ ...a,
5651
+ status: "error",
5652
+ errorText: errorMessage(err)
5653
+ } : a));
5654
+ });
5655
+ }, [rest, agent.id]);
5656
+ const stageFiles = useCallback((files) => {
5657
+ for (const file of files) stageAttachment(file);
5658
+ }, [stageAttachment]);
5567
5659
  const pasteImage = useCallback(async () => {
5568
5660
  const image = await readClipboardImage();
5569
5661
  if (!image) return;
5570
- await stageImages([image]);
5571
- }, [stageImages]);
5662
+ stageAttachment(image);
5663
+ }, [stageAttachment]);
5572
5664
  usePaste((event) => {
5573
5665
  if (modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt) return;
5574
5666
  const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
@@ -5581,7 +5673,7 @@ function ChatScreen({ agent, conversation }) {
5581
5673
  case "binary-image": {
5582
5674
  event.preventDefault();
5583
5675
  const ext = route.mediaType.split("/")[1]?.split("+")[0] ?? "png";
5584
- stageImages([{
5676
+ stageFiles([{
5585
5677
  fileName: `pasted-image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.${ext}`,
5586
5678
  mediaType: route.mediaType,
5587
5679
  data: event.bytes
@@ -5594,9 +5686,9 @@ function ChatScreen({ agent, conversation }) {
5594
5686
  return;
5595
5687
  case "dropped-paths":
5596
5688
  event.preventDefault();
5597
- resolveDroppedImages(route.paths).then((images) => {
5598
- if (images) {
5599
- stageImages(images);
5689
+ resolveDroppedFiles(route.paths).then((files) => {
5690
+ if (files) {
5691
+ stageFiles(files);
5600
5692
  return;
5601
5693
  }
5602
5694
  composerRef.current?.insertText(route.text);
@@ -5786,8 +5878,12 @@ function ChatScreen({ agent, conversation }) {
5786
5878
  pasteImage();
5787
5879
  return;
5788
5880
  }
5789
- if (key.name === "x" && key.ctrl) {
5790
- setPending((prev) => prev.slice(0, -1));
5881
+ if (key.name === "x" && key.ctrl || key.name === "backspace" && attachmentsRef.current.length > 0 && !composerRef.current?.plainText) {
5882
+ const last = attachmentsRef.current.at(-1);
5883
+ if (last) {
5884
+ uploadsRef.current.delete(last.tempId);
5885
+ setAttachments((prev) => prev.filter((a) => a.tempId !== last.tempId));
5886
+ }
5791
5887
  return;
5792
5888
  }
5793
5889
  if (key.name === "l" && key.ctrl) openInBrowser();
@@ -5906,19 +6002,17 @@ function ChatScreen({ agent, conversation }) {
5906
6002
  marginTop: 1,
5907
6003
  flexDirection: "column"
5908
6004
  },
5909
- children: [pending.map((a) => /* @__PURE__ */ jsxs("text", {
5910
- fg: theme.muted,
6005
+ children: [attachments.map((a) => /* @__PURE__ */ jsxs("text", {
6006
+ fg: a.status === "error" ? theme.error : theme.muted,
5911
6007
  children: [
5912
6008
  "📎 ",
5913
6009
  a.fileName,
5914
6010
  " (",
5915
6011
  formatBytes(a.sizeBytes),
5916
- ")"
6012
+ ") ",
6013
+ attachmentGlyph(a)
5917
6014
  ]
5918
- }, a.id)), uploading ? /* @__PURE__ */ jsx("text", {
5919
- fg: theme.dim,
5920
- children: "uploading image…"
5921
- }) : /* @__PURE__ */ jsx("text", {
6015
+ }, a.tempId)), /* @__PURE__ */ jsx("text", {
5922
6016
  fg: theme.dim,
5923
6017
  children: "ctrl+x remove last"
5924
6018
  })]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.276",
3
+ "version": "0.1.0-beta.286",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",