skydive-cli 0.5.0-beta.29 → 0.5.0-beta.30

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
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-BRvwJNwH.mjs";
2
+ import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-BoO7ldO0.mjs";
3
3
  import { A as getShareMachineDefault, C as getConfigPath, D as getPromptHistoryPath, E as getPreference, F as resolveAppUrl, H as saveSession, I as resolveChatAuth, L as resolveConfig, M as getStoredApiKeyWorkspaceName, N as getUpdateCheckDisabled, R as resolveManagementAuth, S as deleteConfig, T as getLastSeenVersion, V as saveConfig, W as setLastSeenVersion, _ as API_KEY_FAMILY_PREFIX, g as API_KEYS_URL, i as resolveAgent$1, j as getStoredApiKeyId, v as API_KEY_PREFIX, w as getDefaultAgent, x as PREFERENCES, y as DEFAULT_API_URL, z as resolveSession } from "./print-DYiQC5IW.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
6
  import { t as createRestClient } from "./rest-xXfqT4SX.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
8
  import { r as resolveMachineIdentity } from "./client-DbqRBquD.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-DBrQbBCG.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-DuxaUuek.mjs";
10
10
  import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
11
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
12
12
  import { hideBin } from "yargs/helpers";
@@ -1584,7 +1584,7 @@ const importCommand = {
1584
1584
  process.exit(1);
1585
1585
  }
1586
1586
  }
1587
- const { runChat } = await import("./boot-DOTGKAyK.mjs");
1587
+ const { runChat } = await import("./boot-0-dpPM1_.mjs");
1588
1588
  await runChat({
1589
1589
  appUrl,
1590
1590
  sessionToken: session.value.sessionToken,
@@ -2029,7 +2029,7 @@ const chatCommand = {
2029
2029
  sessionToken: auth.value.token,
2030
2030
  agentSelector: argv.agent ?? null
2031
2031
  });
2032
- const { runChat } = await import("./boot-DOTGKAyK.mjs");
2032
+ const { runChat } = await import("./boot-0-dpPM1_.mjs");
2033
2033
  await runChat({
2034
2034
  appUrl: auth.value.appUrl,
2035
2035
  sessionToken: auth.value.token,
@@ -2411,7 +2411,7 @@ const switchCommand = {
2411
2411
  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.");
2412
2412
  process.exit(1);
2413
2413
  }
2414
- const { runWorkspacePicker } = await import("./boot-DOTGKAyK.mjs");
2414
+ const { runWorkspacePicker } = await import("./boot-0-dpPM1_.mjs");
2415
2415
  await runWorkspacePicker(session);
2416
2416
  return;
2417
2417
  }
@@ -2490,7 +2490,7 @@ const openCommand = {
2490
2490
  const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
2491
2491
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2492
2492
  const { machineName } = await resolveMachineIdentity(null);
2493
- const { PortalDaemonClient } = await import("./daemon-client-BrOr8CNG.mjs");
2493
+ const { PortalDaemonClient } = await import("./daemon-client-LJdnM_nE.mjs");
2494
2494
  let lastLine = "";
2495
2495
  let signalConnected;
2496
2496
  const connected = new Promise((resolve) => {
@@ -4336,7 +4336,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4336
4336
  process.exit(0);
4337
4337
  }
4338
4338
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4339
- const { runPortalDaemon } = await import("./daemon-BZ150Qid.mjs");
4339
+ const { runPortalDaemon } = await import("./daemon-Ce8WLdGI.mjs");
4340
4340
  runPortalDaemon(process.argv);
4341
4341
  } else runCli();
4342
4342
  function runCli() {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-BRvwJNwH.mjs";
2
+ import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-BoO7ldO0.mjs";
3
3
  import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-DYiQC5IW.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
5
  import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-xXfqT4SX.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
7
  import { t as PortalClient } from "./client-DbqRBquD.mjs";
8
- import "./daemon-DBrQbBCG.mjs";
8
+ import "./daemon-DuxaUuek.mjs";
9
9
  import "./api-DG5W6iwx.mjs";
10
10
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
- import { t as PortalDaemonClient } from "./daemon-client-BpBjNzyD.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client-C4heMWTq.mjs";
12
12
  import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
13
13
  import * as os$1 from "node:os";
14
14
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -4749,299 +4749,6 @@ function parseMatches(output) {
4749
4749
  return null;
4750
4750
  }
4751
4751
 
4752
- //#endregion
4753
- //#region src/chat/tui/chat/conversation-context.tsx
4754
- const ConversationContext = createContext({
4755
- rest: null,
4756
- conversationId: null,
4757
- collectSandboxStats: null,
4758
- latestComputerId: null,
4759
- scrolledUp: false
4760
- });
4761
- function useConversationContext() {
4762
- return useContext(ConversationContext);
4763
- }
4764
-
4765
- //#endregion
4766
- //#region src/chat/tui/spinner-frames.ts
4767
- /** Braille dot-cycle frames — the classic smooth terminal spinner. */
4768
- const SPINNER_FRAMES = [
4769
- "⠋",
4770
- "⠙",
4771
- "⠹",
4772
- "⠸",
4773
- "⠼",
4774
- "⠴",
4775
- "⠦",
4776
- "⠧",
4777
- "⠇",
4778
- "⠏"
4779
- ];
4780
- /** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
4781
- const SPINNER_INTERVAL_MS = 80;
4782
- /** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
4783
- function spinnerFrame(tick) {
4784
- const n = SPINNER_FRAMES.length;
4785
- return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
4786
- }
4787
-
4788
- //#endregion
4789
- //#region src/chat/tui/spinner.tsx
4790
- /**
4791
- * An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
4792
- * a self-owned interval that's cleared on unmount, so the animation runs only
4793
- * while the component is mounted. Returns a <span>, so render it inside a
4794
- * <text> (matching how the rest of the TUI composes inline glyphs).
4795
- */
4796
- function Spinner({ color }) {
4797
- const [tick, setTick] = useState(0);
4798
- useEffect(() => {
4799
- const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
4800
- return () => clearInterval(id);
4801
- }, []);
4802
- return /* @__PURE__ */ jsx("span", {
4803
- fg: color,
4804
- children: spinnerFrame(tick)
4805
- });
4806
- }
4807
-
4808
- //#endregion
4809
- //#region src/chat/tui/chat/subagents.ts
4810
- /** A run the platform hasn't settled yet — still queued or mid-run. */
4811
- function isLiveStatus(status) {
4812
- return status === "queued" || status === "running";
4813
- }
4814
- function toTaskStatus(raw) {
4815
- switch (raw) {
4816
- case "queued":
4817
- case "running":
4818
- case "completed":
4819
- case "failed":
4820
- case "canceled": return raw;
4821
- default: return "queued";
4822
- }
4823
- }
4824
- const FALLBACK_TITLE = "Subagent task";
4825
- /**
4826
- * Each delegated task's display title from the `subagent` call's input, in
4827
- * call order — the short name the tool requires per task. A malformed entry
4828
- * yields a fallback label rather than being dropped, so the row count always
4829
- * matches the fan-out width. Mirrors the web client's `subagentTaskTitles`.
4830
- */
4831
- function subagentTaskTitles(input) {
4832
- if (!isRecord(input)) return [];
4833
- const tasks = input["tasks"];
4834
- if (!Array.isArray(tasks)) return [];
4835
- return tasks.map((task) => {
4836
- if (!isRecord(task)) return FALLBACK_TITLE;
4837
- const title = task["title"];
4838
- return typeof title === "string" && title.trim() ? title.trim() : FALLBACK_TITLE;
4839
- });
4840
- }
4841
- const QUEUED_TASK_ID_LINE = /^\s*-\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):\s/i;
4842
- /**
4843
- * The ids of the runs a `subagent` call queued, read back out of its own
4844
- * output. The call returns the moment the runs exist, so the ids are there
4845
- * long before any child finishes — they are what ties this card to the task
4846
- * rows the status poll returns, and their order is the tasks' call order.
4847
- */
4848
- function queuedSubagentTaskIds(output) {
4849
- const text = flattenOutputText(output);
4850
- if (!text) return [];
4851
- return text.split("\n").map((line) => line.match(QUEUED_TASK_ID_LINE)?.[1] ?? null).filter((id) => id !== null);
4852
- }
4853
- /**
4854
- * Tool output reaches the TUI as `unknown`: the raw string live, possibly
4855
- * wrapped in content parts or arrays when loaded from history. Walk it for
4856
- * string leaves, newline-joined — the PR scanner's `stringifyToolOutput`
4857
- * shape, bounded small because the id list sits at the top of the output.
4858
- */
4859
- function flattenOutputText(value) {
4860
- const MAX_LEN = 2e4;
4861
- const parts = [];
4862
- let total = 0;
4863
- const visit = (v, depth) => {
4864
- if (total >= MAX_LEN || depth > 4) return;
4865
- if (typeof v === "string") {
4866
- parts.push(v);
4867
- total += v.length;
4868
- return;
4869
- }
4870
- if (Array.isArray(v)) {
4871
- for (const item of v) visit(item, depth + 1);
4872
- return;
4873
- }
4874
- if (isRecord(v)) for (const item of Object.values(v)) visit(item, depth + 1);
4875
- };
4876
- visit(value, 0);
4877
- return parts.join("\n");
4878
- }
4879
- /**
4880
- * The card's rows: one per queued run id (call order), each carrying the
4881
- * freshest known state. A task the status fetch hasn't returned yet falls
4882
- * back to the input's title and reads as queued; a fetched row's own title
4883
- * wins over the input's (the server's copy is the one the child actually
4884
- * ran under). Before the ids exist — the call is still streaming — rows come
4885
- * from the input titles alone, so the card never renders empty mid-turn.
4886
- */
4887
- function subagentRows({ titles, ids, fetched }) {
4888
- if (ids.length === 0) return titles.map((title, index) => ({
4889
- id: `queued:${index}`,
4890
- title,
4891
- status: "queued",
4892
- createdAt: null,
4893
- completedAt: null
4894
- }));
4895
- return ids.map((id, index) => {
4896
- const row = fetched?.get(id) ?? null;
4897
- return {
4898
- id,
4899
- title: row?.title ?? titles[index] ?? FALLBACK_TITLE,
4900
- status: row ? toTaskStatus(row.status) : "queued",
4901
- createdAt: row?.createdAt ?? null,
4902
- completedAt: row?.completedAt ?? null
4903
- };
4904
- });
4905
- }
4906
-
4907
- //#endregion
4908
- //#region src/chat/tui/chunks/tool-subagent.tsx
4909
- /** How often a live batch re-reads its tasks' status. */
4910
- const POLL_INTERVAL_MS = 3e3;
4911
- function rowGlyph(status) {
4912
- switch (status) {
4913
- case "completed": return {
4914
- ch: "✔",
4915
- fg: theme.success
4916
- };
4917
- case "failed":
4918
- case "canceled": return {
4919
- ch: "✗",
4920
- fg: theme.error
4921
- };
4922
- case "running": return {
4923
- ch: "▶",
4924
- fg: theme.accent
4925
- };
4926
- case "queued": return {
4927
- ch: "○",
4928
- fg: theme.dim
4929
- };
4930
- }
4931
- }
4932
- function rowColor$1(status) {
4933
- switch (status) {
4934
- case "running": return theme.fg;
4935
- case "completed": return theme.dim;
4936
- default: return theme.muted;
4937
- }
4938
- }
4939
- function headerLabel(rows) {
4940
- const subject = rows.length === 1 ? "Subagent" : `${rows.length} subagents`;
4941
- return rows.some((row) => isLiveStatus(row.status)) ? `${subject} running` : subject;
4942
- }
4943
- /**
4944
- * The transcript block for a `subagent` tool call — the TUI analog of the web
4945
- * chat's fan-out card. One row per delegated run: status glyph, the short
4946
- * title the delegating agent wrote, ticking over live as children settle.
4947
- *
4948
- * Rows are seeded from the call itself (titles from its input, task ids from
4949
- * its own output), so the card renders the moment the call does. Status then
4950
- * comes from the server's task rows, polled while any run is live — the
4951
- * delegated runs outlive the call that spawned them, so the transcript stream
4952
- * carries nothing more about them. The poll stops the moment the batch
4953
- * settles; reopening an old conversation does one fetch and stops there. A
4954
- * failed poll is skipped silently (the next tick retries; stale rows beat an
4955
- * error row in the scrollback).
4956
- */
4957
- function ToolSubagent({ item, pinned = false, onLiveChange }) {
4958
- const { rest, conversationId } = useConversationContext();
4959
- const titles = subagentTaskTitles(item.input);
4960
- const ids = item.output?.kind === "ok" ? queuedSubagentTaskIds(item.output.value) : [];
4961
- const idsKey = ids.join(",");
4962
- const [fetched, setFetched] = useState(null);
4963
- const rows = subagentRows({
4964
- titles,
4965
- ids,
4966
- fetched
4967
- });
4968
- const live = rows.length === 0 || rows.some((row) => isLiveStatus(row.status));
4969
- useEffect(() => {
4970
- onLiveChange?.(live);
4971
- }, [live, onLiveChange]);
4972
- useEffect(() => {
4973
- if (!rest || !conversationId || idsKey.length === 0 || !live) return;
4974
- let cancelled = false;
4975
- let timer = null;
4976
- const tick = async () => {
4977
- try {
4978
- const tasks = await rest.listSubagentTasks({ conversationId });
4979
- if (cancelled) return;
4980
- setFetched(tasks);
4981
- } catch (_error) {}
4982
- if (!cancelled) timer = setTimeout(tick, POLL_INTERVAL_MS);
4983
- };
4984
- tick();
4985
- return () => {
4986
- cancelled = true;
4987
- if (timer !== null) clearTimeout(timer);
4988
- };
4989
- }, [
4990
- rest,
4991
- conversationId,
4992
- idsKey,
4993
- live
4994
- ]);
4995
- if (rows.length === 0) return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
4996
- fg: theme.warning,
4997
- children: "◐ "
4998
- }), /* @__PURE__ */ jsx("span", {
4999
- fg: theme.tool,
5000
- children: "Delegating to subagents…"
5001
- })] });
5002
- if (pinned && !live) return null;
5003
- return /* @__PURE__ */ jsxs("box", {
5004
- style: { flexDirection: "column" },
5005
- children: [/* @__PURE__ */ jsxs("text", { children: [
5006
- live ? /* @__PURE__ */ jsx(Spinner, { color: theme.accent }) : /* @__PURE__ */ jsx("span", {
5007
- fg: theme.success,
5008
- children: "✓"
5009
- }),
5010
- /* @__PURE__ */ jsxs("span", {
5011
- fg: theme.tool,
5012
- children: [" ", /* @__PURE__ */ jsx("b", { children: headerLabel(rows) })]
5013
- }),
5014
- /* @__PURE__ */ jsxs("span", {
5015
- fg: theme.dim,
5016
- children: [
5017
- " ",
5018
- rows.filter((row) => row.status === "completed").length,
5019
- "/",
5020
- rows.length
5021
- ]
5022
- })
5023
- ] }), /* @__PURE__ */ jsx("box", {
5024
- style: {
5025
- flexDirection: "column",
5026
- paddingLeft: 2
5027
- },
5028
- children: rows.map((row) => {
5029
- const glyph = rowGlyph(row.status);
5030
- return /* @__PURE__ */ jsxs("box", {
5031
- style: { flexDirection: "row" },
5032
- children: [/* @__PURE__ */ jsxs("text", {
5033
- fg: glyph.fg,
5034
- children: [glyph.ch, " "]
5035
- }), /* @__PURE__ */ jsx("text", {
5036
- fg: rowColor$1(row.status),
5037
- children: row.title
5038
- })]
5039
- }, row.id);
5040
- })
5041
- })]
5042
- });
5043
- }
5044
-
5045
4752
  //#endregion
5046
4753
  //#region src/chat/tui/chunks/shell-local.tsx
5047
4754
  /**
@@ -5080,6 +4787,19 @@ function ShellLocalItem({ item }) {
5080
4787
  });
5081
4788
  }
5082
4789
 
4790
+ //#endregion
4791
+ //#region src/chat/tui/chat/conversation-context.tsx
4792
+ const ConversationContext = createContext({
4793
+ rest: null,
4794
+ conversationId: null,
4795
+ collectSandboxStats: null,
4796
+ latestComputerId: null,
4797
+ scrolledUp: false
4798
+ });
4799
+ function useConversationContext() {
4800
+ return useContext(ConversationContext);
4801
+ }
4802
+
5083
4803
  //#endregion
5084
4804
  //#region src/chat/tui/chat/sandbox-stats.ts
5085
4805
  /**
@@ -5764,7 +5484,7 @@ function ToolItem({ item }) {
5764
5484
  case "read": return /* @__PURE__ */ jsx(ToolRead, { item });
5765
5485
  case "grep":
5766
5486
  case "glob": return /* @__PURE__ */ jsx(ToolGrep, { item });
5767
- case "subagent": return /* @__PURE__ */ jsx(ToolSubagent, { item });
5487
+ case "subagent": return null;
5768
5488
  default: return /* @__PURE__ */ jsx(ToolGeneric, { item });
5769
5489
  }
5770
5490
  }
@@ -6792,7 +6512,7 @@ function windowTodos(todos, cap = MAX_VISIBLE_ROWS) {
6792
6512
  const sorted = [...todos].sort((a, b) => a.position - b.position);
6793
6513
  let leadingDone = 0;
6794
6514
  while (leadingDone < sorted.length && sorted[leadingDone]?.status === "completed") leadingDone += 1;
6795
- const head = leadingDone >= 2 || sorted.length > cap ? leadingDone : 0;
6515
+ const head = leadingDone === sorted.length && sorted.length > 0 || leadingDone >= 2 || sorted.length > cap ? leadingDone : 0;
6796
6516
  const rest = sorted.slice(head);
6797
6517
  const rowBudget = Math.max(1, head > 0 ? cap - 1 : cap);
6798
6518
  if (rest.length <= rowBudget) return {
@@ -6823,7 +6543,7 @@ function todoCardHeight(todos) {
6823
6543
  }
6824
6544
 
6825
6545
  //#endregion
6826
- //#region src/chat/tui/chat/todo-card.tsx
6546
+ //#region src/chat/tui/chat/task-card.tsx
6827
6547
  function glyph(status) {
6828
6548
  switch (status) {
6829
6549
  case "completed": return {
@@ -6844,46 +6564,26 @@ function glyph(status) {
6844
6564
  };
6845
6565
  }
6846
6566
  }
6847
- function rowColor(status) {
6848
- switch (status) {
6849
- case "completed": return theme.dim;
6850
- case "in_progress": return theme.fg;
6851
- case "failed": return theme.muted;
6852
- default: return theme.muted;
6853
- }
6854
- }
6855
- /**
6856
- * The agent's live todo list, pinned just above the composer — the TUI analog
6857
- * of the web chat's todo card. Rendered only once the agent has actually
6858
- * written a list (`platform todo write`) and until it clears one, so a
6859
- * conversation the agent never wrote todos for grows no chrome for it.
6860
- *
6861
- * The card can't scroll like the web card, so it windows (see `windowTodos`):
6862
- * a leading run of completed todos folds into a `✔ N done` line, the active
6863
- * and upcoming rows fill the rest, and a `+N more` tail counts pending work
6864
- * past the cap. The window follows the in-progress row down as todos complete.
6865
- *
6866
- * `isActive` dims the header while the run is idle: an in-progress row only
6867
- * reads as "happening now" when a run is actually producing output.
6868
- */
6869
- function TodoCardView({ todos, isActive }) {
6870
- if (todos.length === 0) return null;
6871
- const completed = todos.filter((t) => t.status === "completed").length;
6567
+ function rowColor(status) {
6568
+ switch (status) {
6569
+ case "completed": return theme.dim;
6570
+ case "in_progress": return theme.fg;
6571
+ case "failed": return theme.muted;
6572
+ default: return theme.muted;
6573
+ }
6574
+ }
6575
+ /** Shared plan/subagent checklist content. The caller owns outer spacing. */
6576
+ function TaskCardContent({ label, todos, isActive }) {
6577
+ const completed = todos.filter((todo) => todo.status === "completed").length;
6872
6578
  const { doneSummary, rows, moreCount } = windowTodos(todos);
6873
6579
  return /* @__PURE__ */ jsxs("box", {
6874
- style: {
6875
- flexDirection: "column",
6876
- flexShrink: 0,
6877
- marginTop: 1,
6878
- paddingLeft: 1,
6879
- paddingRight: 1
6880
- },
6580
+ style: { flexDirection: "column" },
6881
6581
  children: [
6882
6582
  /* @__PURE__ */ jsxs("box", {
6883
6583
  style: { flexDirection: "row" },
6884
6584
  children: [/* @__PURE__ */ jsx("text", {
6885
6585
  fg: isActive ? theme.accent : theme.muted,
6886
- children: /* @__PURE__ */ jsx("b", { children: "Plan" })
6586
+ children: /* @__PURE__ */ jsx("b", { children: label })
6887
6587
  }), /* @__PURE__ */ jsxs("text", {
6888
6588
  fg: theme.dim,
6889
6589
  children: [
@@ -6928,26 +6628,243 @@ function TodoCardView({ todos, isActive }) {
6928
6628
  ]
6929
6629
  });
6930
6630
  }
6631
+ function taskCardContentHeight(todos) {
6632
+ if (todos.length === 0) return 0;
6633
+ const window = windowTodos(todos);
6634
+ return 1 + (window.doneSummary > 0 ? 1 : 0) + window.rows.length + (window.moreCount > 0 ? 1 : 0);
6635
+ }
6636
+
6637
+ //#endregion
6638
+ //#region src/chat/tui/chat/todo-card.tsx
6639
+ /**
6640
+ * The agent's live todo list, pinned just above the composer — the TUI analog
6641
+ * of the web chat's todo card. Rendered only once the agent has actually
6642
+ * written a list (`platform todo write`) and until it clears one, so a
6643
+ * conversation the agent never wrote todos for grows no chrome for it.
6644
+ *
6645
+ * The card can't scroll like the web card, so it windows completed and pending
6646
+ * work. `isActive` dims the header while the run is idle.
6647
+ */
6648
+ function TodoCardView({ todos, isActive }) {
6649
+ if (todos.length === 0) return null;
6650
+ return /* @__PURE__ */ jsx("box", {
6651
+ style: {
6652
+ flexDirection: "column",
6653
+ flexShrink: 0,
6654
+ marginTop: 1,
6655
+ paddingLeft: 1,
6656
+ paddingRight: 1
6657
+ },
6658
+ children: /* @__PURE__ */ jsx(TaskCardContent, {
6659
+ label: "Plan",
6660
+ todos,
6661
+ isActive
6662
+ })
6663
+ });
6664
+ }
6665
+
6666
+ //#endregion
6667
+ //#region src/chat/tui/chat/subagents.ts
6668
+ /** A run the platform hasn't settled yet — still queued or mid-run. */
6669
+ function isLiveStatus(status) {
6670
+ return status === "queued" || status === "running";
6671
+ }
6672
+ function toTaskStatus(raw) {
6673
+ switch (raw) {
6674
+ case "queued":
6675
+ case "running":
6676
+ case "completed":
6677
+ case "failed":
6678
+ case "canceled": return raw;
6679
+ default: return "queued";
6680
+ }
6681
+ }
6682
+ const FALLBACK_TITLE = "Subagent task";
6683
+ /**
6684
+ * Each delegated task's display title from the `subagent` call's input, in
6685
+ * call order — the short name the tool requires per task. A malformed entry
6686
+ * yields a fallback label rather than being dropped, so the row count always
6687
+ * matches the fan-out width. Mirrors the web client's `subagentTaskTitles`.
6688
+ */
6689
+ function subagentTaskTitles(input) {
6690
+ if (!isRecord(input)) return [];
6691
+ const tasks = input["tasks"];
6692
+ if (!Array.isArray(tasks)) return [];
6693
+ return tasks.map((task) => {
6694
+ if (!isRecord(task)) return FALLBACK_TITLE;
6695
+ const title = task["title"];
6696
+ return typeof title === "string" && title.trim() ? title.trim() : FALLBACK_TITLE;
6697
+ });
6698
+ }
6699
+ const QUEUED_TASK_ID_LINE = /^\s*-\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):\s/i;
6700
+ /**
6701
+ * The ids of the runs a `subagent` call queued, read back out of its own
6702
+ * output. The call returns the moment the runs exist, so the ids are there
6703
+ * long before any child finishes — they are what ties this card to the task
6704
+ * rows the status poll returns, and their order is the tasks' call order.
6705
+ */
6706
+ function queuedSubagentTaskIds(output) {
6707
+ const text = flattenOutputText(output);
6708
+ if (!text) return [];
6709
+ return text.split("\n").map((line) => line.match(QUEUED_TASK_ID_LINE)?.[1] ?? null).filter((id) => id !== null);
6710
+ }
6711
+ /**
6712
+ * Tool output reaches the TUI as `unknown`: the raw string live, possibly
6713
+ * wrapped in content parts or arrays when loaded from history. Walk it for
6714
+ * string leaves, newline-joined — the PR scanner's `stringifyToolOutput`
6715
+ * shape, bounded small because the id list sits at the top of the output.
6716
+ */
6717
+ function flattenOutputText(value) {
6718
+ const MAX_LEN = 2e4;
6719
+ const parts = [];
6720
+ let total = 0;
6721
+ const visit = (v, depth) => {
6722
+ if (total >= MAX_LEN || depth > 4) return;
6723
+ if (typeof v === "string") {
6724
+ parts.push(v);
6725
+ total += v.length;
6726
+ return;
6727
+ }
6728
+ if (Array.isArray(v)) {
6729
+ for (const item of v) visit(item, depth + 1);
6730
+ return;
6731
+ }
6732
+ if (isRecord(v)) for (const item of Object.values(v)) visit(item, depth + 1);
6733
+ };
6734
+ visit(value, 0);
6735
+ return parts.join("\n");
6736
+ }
6737
+ function nextAnchoredSubagentIds({ current, nextId, liveById }) {
6738
+ if (current.includes(nextId)) return current;
6739
+ return current.some((id) => liveById[id] ?? true) ? [...current, nextId] : [nextId];
6740
+ }
6741
+ /**
6742
+ * The card's rows: one per queued run id (call order), each carrying the
6743
+ * freshest known state. A task the status fetch hasn't returned yet falls
6744
+ * back to the input's title and reads as queued; a fetched row's own title
6745
+ * wins over the input's (the server's copy is the one the child actually
6746
+ * ran under). Before the ids exist — the call is still streaming — rows come
6747
+ * from the input titles alone, so the card never renders empty mid-turn.
6748
+ */
6749
+ function subagentRows({ titles, ids, fetched }) {
6750
+ if (ids.length === 0) return titles.map((title, index) => ({
6751
+ id: `queued:${index}`,
6752
+ title,
6753
+ status: "queued",
6754
+ createdAt: null,
6755
+ completedAt: null
6756
+ }));
6757
+ return ids.map((id, index) => {
6758
+ const row = fetched?.get(id) ?? null;
6759
+ return {
6760
+ id,
6761
+ title: row?.title ?? titles[index] ?? FALLBACK_TITLE,
6762
+ status: row ? toTaskStatus(row.status) : "queued",
6763
+ createdAt: row?.createdAt ?? null,
6764
+ completedAt: row?.completedAt ?? null
6765
+ };
6766
+ });
6767
+ }
6768
+
6769
+ //#endregion
6770
+ //#region src/chat/tui/chunks/tool-subagent.tsx
6771
+ /** How often a live batch re-reads its tasks' status. */
6772
+ const POLL_INTERVAL_MS = 3e3;
6773
+ function subagentTodos(rows) {
6774
+ return rows.map((row, position) => ({
6775
+ id: row.id,
6776
+ content: row.title,
6777
+ position,
6778
+ status: row.status === "completed" ? "completed" : row.status === "running" ? "in_progress" : row.status === "failed" || row.status === "canceled" ? "failed" : "pending"
6779
+ }));
6780
+ }
6781
+ /**
6782
+ * The transcript block for a `subagent` tool call — the TUI analog of the web
6783
+ * chat's fan-out card. One row per delegated run: status glyph, the short
6784
+ * title the delegating agent wrote, ticking over live as children settle.
6785
+ *
6786
+ * Rows are seeded from the call itself (titles from its input, task ids from
6787
+ * its own output), so the card renders the moment the call does. Status then
6788
+ * comes from the server's task rows, polled while any run is live — the
6789
+ * delegated runs outlive the call that spawned them, so the transcript stream
6790
+ * carries nothing more about them. The poll stops the moment the batch
6791
+ * settles; reopening an old conversation does one fetch and stops there. A
6792
+ * failed poll is skipped silently (the next tick retries; stale rows beat an
6793
+ * error row in the scrollback).
6794
+ */
6795
+ function ToolSubagent({ item, isActive, onLiveChange, onHeightChange }) {
6796
+ const { rest, conversationId } = useConversationContext();
6797
+ const titles = subagentTaskTitles(item.input);
6798
+ const ids = item.output?.kind === "ok" ? queuedSubagentTaskIds(item.output.value) : [];
6799
+ const idsKey = ids.join(",");
6800
+ const [fetched, setFetched] = useState(null);
6801
+ const rows = subagentRows({
6802
+ titles,
6803
+ ids,
6804
+ fetched
6805
+ });
6806
+ const live = rows.length === 0 || rows.some((row) => isLiveStatus(row.status));
6807
+ const todos = subagentTodos(rows);
6808
+ useEffect(() => {
6809
+ onLiveChange?.(live);
6810
+ }, [live, onLiveChange]);
6811
+ useEffect(() => {
6812
+ onHeightChange?.(taskCardContentHeight(todos));
6813
+ }, [onHeightChange, todos]);
6814
+ useEffect(() => {
6815
+ if (!rest || !conversationId || idsKey.length === 0 || !live) return;
6816
+ let cancelled = false;
6817
+ let timer = null;
6818
+ const tick = async () => {
6819
+ try {
6820
+ const tasks = await rest.listSubagentTasks({ conversationId });
6821
+ if (cancelled) return;
6822
+ setFetched(tasks);
6823
+ } catch (_error) {}
6824
+ if (!cancelled) timer = setTimeout(tick, POLL_INTERVAL_MS);
6825
+ };
6826
+ tick();
6827
+ return () => {
6828
+ cancelled = true;
6829
+ if (timer !== null) clearTimeout(timer);
6830
+ };
6831
+ }, [
6832
+ rest,
6833
+ conversationId,
6834
+ idsKey,
6835
+ live
6836
+ ]);
6837
+ if (rows.length === 0) return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
6838
+ fg: theme.warning,
6839
+ children: "◐ "
6840
+ }), /* @__PURE__ */ jsx("span", {
6841
+ fg: theme.tool,
6842
+ children: "Delegating to subagents…"
6843
+ })] });
6844
+ return /* @__PURE__ */ jsx(TaskCardContent, {
6845
+ label: rows.length === 1 ? "Subagent" : "Subagents",
6846
+ todos,
6847
+ isActive: isActive ?? live
6848
+ });
6849
+ }
6931
6850
 
6932
6851
  //#endregion
6933
6852
  //#region src/chat/tui/chat/subagent-card.tsx
6934
6853
  /**
6935
- * Live delegated work pinned above the plan. Each batch owns its polling in
6936
- * ToolSubagent; settled batches remove themselves here and remain visible only
6937
- * at their original transcript position.
6854
+ * Delegated work pinned above the plan. Each batch owns its status polling and
6855
+ * stays as a dimmed completion summary after every run settles.
6938
6856
  */
6939
- function SubagentCardView({ items, onHeightChange }) {
6940
- const [liveById, setLiveById] = useState({});
6941
- const setBatchLive = useCallback((id, live) => {
6942
- setLiveById((current) => current[id] === live ? current : {
6857
+ function SubagentCardView({ items, isActive, onHeightChange, onBatchLiveChange }) {
6858
+ const [heightById, setHeightById] = useState({});
6859
+ const setBatchHeight = useCallback((id, height) => {
6860
+ setHeightById((current) => current[id] === height ? current : {
6943
6861
  ...current,
6944
- [id]: live
6862
+ [id]: height
6945
6863
  });
6946
6864
  }, []);
6947
- const liveItems = useMemo(() => items.filter((item) => liveById[item.id] ?? true), [items, liveById]);
6948
- const height = liveItems.length === 0 ? 0 : 1 + liveItems.reduce((rows, item) => rows + 1 + subagentTaskTitles(item.input).length, 0) + (liveItems.length - 1);
6865
+ const height = useMemo(() => items.length === 0 ? 0 : 1 + items.reduce((rows, item) => rows + (heightById[item.id] ?? 0), 0) + (items.length - 1), [heightById, items]);
6949
6866
  useEffect(() => onHeightChange?.(height), [height, onHeightChange]);
6950
- if (liveItems.length === 0) return null;
6867
+ if (items.length === 0) return null;
6951
6868
  return /* @__PURE__ */ jsx("box", {
6952
6869
  style: {
6953
6870
  flexDirection: "column",
@@ -6957,10 +6874,11 @@ function SubagentCardView({ items, onHeightChange }) {
6957
6874
  paddingRight: 1,
6958
6875
  gap: 1
6959
6876
  },
6960
- children: liveItems.map((item) => /* @__PURE__ */ jsx(ToolSubagent, {
6877
+ children: items.map((item) => /* @__PURE__ */ jsx(ToolSubagent, {
6961
6878
  item,
6962
- pinned: true,
6963
- onLiveChange: (live) => setBatchLive(item.id, live)
6879
+ isActive,
6880
+ onLiveChange: (live) => onBatchLiveChange?.(item.id, live),
6881
+ onHeightChange: (batchHeight) => setBatchHeight(item.id, batchHeight)
6964
6882
  }, item.id))
6965
6883
  });
6966
6884
  }
@@ -7283,6 +7201,49 @@ function NewChatSplash({ agentName, agentTitle, width, height }) {
7283
7201
  });
7284
7202
  }
7285
7203
 
7204
+ //#endregion
7205
+ //#region src/chat/tui/spinner-frames.ts
7206
+ /** Braille dot-cycle frames — the classic smooth terminal spinner. */
7207
+ const SPINNER_FRAMES = [
7208
+ "⠋",
7209
+ "⠙",
7210
+ "⠹",
7211
+ "⠸",
7212
+ "⠼",
7213
+ "⠴",
7214
+ "⠦",
7215
+ "⠧",
7216
+ "⠇",
7217
+ "⠏"
7218
+ ];
7219
+ /** Milliseconds between frames. ~12.5fps reads as smooth without thrashing. */
7220
+ const SPINNER_INTERVAL_MS = 80;
7221
+ /** Frame for a given tick count. Wraps at the end of the cycle; negative-safe. */
7222
+ function spinnerFrame(tick) {
7223
+ const n = SPINNER_FRAMES.length;
7224
+ return SPINNER_FRAMES[(Math.trunc(tick) % n + n) % n] ?? SPINNER_FRAMES[0];
7225
+ }
7226
+
7227
+ //#endregion
7228
+ //#region src/chat/tui/spinner.tsx
7229
+ /**
7230
+ * An animated spinner glyph. Advances one frame every SPINNER_INTERVAL_MS via
7231
+ * a self-owned interval that's cleared on unmount, so the animation runs only
7232
+ * while the component is mounted. Returns a <span>, so render it inside a
7233
+ * <text> (matching how the rest of the TUI composes inline glyphs).
7234
+ */
7235
+ function Spinner({ color }) {
7236
+ const [tick, setTick] = useState(0);
7237
+ useEffect(() => {
7238
+ const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
7239
+ return () => clearInterval(id);
7240
+ }, []);
7241
+ return /* @__PURE__ */ jsx("span", {
7242
+ fg: color,
7243
+ children: spinnerFrame(tick)
7244
+ });
7245
+ }
7246
+
7286
7247
  //#endregion
7287
7248
  //#region src/chat/tui/prompt-history.ts
7288
7249
  /**
@@ -11418,6 +11379,11 @@ function ChatScreen({ agent, conversation }) {
11418
11379
  const [todos, setTodos] = useState(null);
11419
11380
  const [planHidden, setPlanHidden] = useState(false);
11420
11381
  const [subagentRows, setSubagentRows] = useState(0);
11382
+ const [anchoredSubagentIds, setAnchoredSubagentIds] = useState([]);
11383
+ const anchoredSubagentLiveRef = useRef({});
11384
+ const setAnchoredBatchLive = useCallback((id, live) => {
11385
+ anchoredSubagentLiveRef.current[id] = live;
11386
+ }, []);
11421
11387
  const [scrolledUp, setScrolledUp] = useState(false);
11422
11388
  const hadTodosRef = useRef(false);
11423
11389
  useEffect(() => {
@@ -11560,7 +11526,10 @@ function ChatScreen({ agent, conversation }) {
11560
11526
  const menuRows = menuOpen ? completionMenuHeight(menuCommands.length) : 0;
11561
11527
  const todoCardVisible = !credPrompt && !planHidden && !!todos && todos.length > 0;
11562
11528
  const todoRows = todoCardVisible ? todoCardHeight(todos) : 0;
11563
- const subagentItems = useMemo(() => items.filter((item) => item.kind === "tool" && item.toolName === "subagent"), [items]);
11529
+ const subagentItems = useMemo(() => {
11530
+ const ids = new Set(anchoredSubagentIds);
11531
+ return items.filter((item) => item.kind === "tool" && item.toolName === "subagent" && ids.has(item.id));
11532
+ }, [items, anchoredSubagentIds]);
11564
11533
  const jumpHintVisible = scrolledUp && !credPrompt;
11565
11534
  const jumpHintRows = jumpHintVisible ? 1 : 0;
11566
11535
  const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - jumpHintRows - subagentRows - todoRows);
@@ -11683,6 +11652,18 @@ function ChatScreen({ agent, conversation }) {
11683
11652
  if (event.chunk.type === "error") sawErrorChunk = true;
11684
11653
  responsePreview.addChunk(event.chunk);
11685
11654
  prScanner.addChunk(event.chunk);
11655
+ if ((event.chunk.type === "tool-input-start" || event.chunk.type === "tool-input-available") && event.chunk.toolName === "subagent" && typeof event.chunk.toolCallId === "string") {
11656
+ const id = event.chunk.toolCallId;
11657
+ setAnchoredSubagentIds((current) => {
11658
+ const next = nextAnchoredSubagentIds({
11659
+ current,
11660
+ nextId: id,
11661
+ liveById: anchoredSubagentLiveRef.current
11662
+ });
11663
+ if (next.length === 1 && next[0] === id) anchoredSubagentLiveRef.current = { [id]: true };
11664
+ return next;
11665
+ });
11666
+ }
11686
11667
  if (event.chunk.type === "data-anyone-todos-updated") {
11687
11668
  const next = todosFromPayload(event.chunk.data);
11688
11669
  if (next) setTodos(next.length > 0 ? next : null);
@@ -13363,7 +13344,9 @@ function ChatScreen({ agent, conversation }) {
13363
13344
  }) : null,
13364
13345
  /* @__PURE__ */ jsx(SubagentCardView, {
13365
13346
  items: subagentItems,
13366
- onHeightChange: setSubagentRows
13347
+ isActive: run.kind !== "idle",
13348
+ onHeightChange: setSubagentRows,
13349
+ onBatchLiveChange: setAnchoredBatchLive
13367
13350
  }),
13368
13351
  todoCardVisible && todos ? /* @__PURE__ */ jsx(TodoCardView, {
13369
13352
  todos,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./client-DbqRBquD.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-DBrQbBCG.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-DuxaUuek.mjs";
4
4
  import "./api-DG5W6iwx.mjs";
5
5
 
6
6
  export { runPortalDaemon };
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
20
  * the commit time of the built tree is one monotonic clock they share.
21
21
  */
22
22
  function portalDaemonBuild() {
23
- return "1786750759";
23
+ return "1786755327";
24
24
  }
25
25
  /**
26
26
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-DBrQbBCG.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-DuxaUuek.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DbqRBquD.mjs";
3
+ import "./daemon-DuxaUuek.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client-C4heMWTq.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.5.0-beta.29";
11
+ var version$1 = "0.5.0-beta.30";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.29",
3
+ "version": "0.5.0-beta.30",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DbqRBquD.mjs";
3
- import "./daemon-DBrQbBCG.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-BpBjNzyD.mjs";
6
-
7
- export { PortalDaemonClient };