skydive-cli 0.5.0-beta.48 → 0.5.0-beta.52

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.
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-B-dizrBw.mjs";
3
- import { A as getSavedTheme, F as recordDefaultAgent, V as resolveWebUrl, W as saveTheme, b as DEFAULT_API_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, g as specKeyFor, h as parseConnectCard, i as resolveAgent, k as getReviewStateDir, l as parseExternalOauthConnectParams, m as computeSettledLabel, p as composeQuestionAnswer, s as MASK_CHAR, u as parseOauthConnectParams, w as getConfigPath, x as DEFAULT_APP_URL } from "./print-SD_y9CTF.mjs";
4
- import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-BOtarTEl.mjs";
2
+ import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-DkuT5_oX.mjs";
3
+ import { A as getSavedTheme, F as recordDefaultAgent, V as resolveWebUrl, W as saveTheme, b as DEFAULT_API_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, g as specKeyFor, h as parseConnectCard, i as resolveAgent, k as getReviewStateDir, l as parseExternalOauthConnectParams, m as computeSettledLabel, p as composeQuestionAnswer, s as MASK_CHAR, u as parseOauthConnectParams, w as getConfigPath, x as DEFAULT_APP_URL } from "./print-DR6Gas-M.mjs";
4
+ import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-BBplm-Zp.mjs";
5
5
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-D3l5kJlX.mjs";
7
- import { t as PortalClient } from "./client-C2lPer4b.mjs";
8
- import "./daemon-B7Wv4P6W.mjs";
7
+ import { t as PortalClient } from "./client-aFwHzCPG.mjs";
8
+ import "./daemon-uzpOdRPL.mjs";
9
9
  import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
10
10
  import "./api-TwLD7ibI.mjs";
11
11
  import { t as SandboxStream } from "./client-Btq6bMzX.mjs";
12
- import { t as PortalDaemonClient } from "./daemon-client-Btr9rmKl.mjs";
12
+ import { t as PortalDaemonClient } from "./daemon-client-Cfi66Xy9.mjs";
13
13
  import { t as runRawPtyPassthrough } from "./raw-pty-D5PhKZSl.mjs";
14
14
  import * as os$1 from "node:os";
15
15
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -1565,82 +1565,228 @@ function hostOf(url) {
1565
1565
  }
1566
1566
 
1567
1567
  //#endregion
1568
- //#region src/chat/tui/status-bar.tsx
1568
+ //#region src/chat/tui/chunks/frame.tsx
1569
+ /** Status glyph + color for a tool's lifecycle state. */
1570
+ function toolGlyph(state) {
1571
+ switch (state) {
1572
+ case "streaming":
1573
+ case "input-ready": return {
1574
+ ch: "◐",
1575
+ color: theme.warning
1576
+ };
1577
+ case "output-ready": return {
1578
+ ch: "✓",
1579
+ color: theme.success
1580
+ };
1581
+ case "error": return {
1582
+ ch: "✗",
1583
+ color: theme.error
1584
+ };
1585
+ }
1586
+ }
1587
+ /** Truncate a single line with an ellipsis. */
1588
+ function truncate(line, maxLen) {
1589
+ return line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line;
1590
+ }
1569
1591
  /**
1570
- * Top-of-screen status line. Mirrors what's in the title of most
1571
- * editor TUIs: who you are, where you're connected, what you're
1572
- * looking at.
1592
+ * Compact a block of tool output for the transcript: truncate over-long
1593
+ * lines (so a giant single-line JSON blob doesn't wrap to a wall) and cap
1594
+ * the line count, with a "+N more" tail that doubles as the click-to-expand
1595
+ * affordance. Expanded blocks render through `ExpandedLines` instead —
1596
+ * full content, word-wrapped, nothing clipped.
1573
1597
  */
1574
- function StatusBar({ rightHint }) {
1575
- const appUrl = useStore((s) => s.appUrl);
1576
- const workspaceName = useStore((s) => s.workspaceName);
1577
- const screen = useStore((s) => s.screen);
1578
- const chatTitle = useStore((s) => s.chatTitle);
1579
- const portal = useStore((s) => s.portal);
1580
- const left = headerLeftLabel(appUrl, workspaceName);
1581
- const middle = screenLabel(screen, chatTitle);
1582
- const share = portalIndicator(portal, screenAgentId(screen));
1598
+ function clipLines(text, { maxLines, maxLineLen }) {
1599
+ const lines = text.replace(/\t/g, " ").split("\n");
1600
+ const out = lines.slice(0, maxLines).map((line) => truncate(line, maxLineLen));
1601
+ if (lines.length > maxLines) out.push(`… +${lines.length - maxLines} more lines · click to expand`);
1602
+ return out;
1603
+ }
1604
+ /**
1605
+ * Split a block for the expanded view: tabs normalised, no line cap, no
1606
+ * per-line truncation rendering word-wraps instead. Blank lines become a
1607
+ * single space so they keep their row (an empty <text> collapses).
1608
+ */
1609
+ function expandLines(text) {
1610
+ return text.replace(/\t/g, " ").split("\n").map((line) => line.length === 0 ? " " : line);
1611
+ }
1612
+ /**
1613
+ * Full, untruncated block content for an expanded view: every line kept and
1614
+ * word-wrapped by the terminal instead of ellipsis-clipped.
1615
+ */
1616
+ function ExpandedLines({ text, fg }) {
1617
+ return /* @__PURE__ */ jsx("box", {
1618
+ style: { flexDirection: "column" },
1619
+ children: expandLines(text).map((line, idx) => /* @__PURE__ */ jsx("text", {
1620
+ fg,
1621
+ style: { wrapMode: "word" },
1622
+ children: line
1623
+ }, idx))
1624
+ });
1625
+ }
1626
+ /** Whether a block of text has lines hidden by the collapsed line cap. */
1627
+ function isClipped(text, maxLines) {
1628
+ let count = 1;
1629
+ for (let i = 0; i < text.length; i++) {
1630
+ if (text.charCodeAt(i) === 10) count++;
1631
+ if (count > maxLines) return true;
1632
+ }
1633
+ return false;
1634
+ }
1635
+ /** Dim tail line under an expanded block: click anywhere on it to fold. */
1636
+ function CollapseHint() {
1637
+ return /* @__PURE__ */ jsx("text", {
1638
+ fg: theme.dim,
1639
+ children: "· click to collapse"
1640
+ });
1641
+ }
1642
+ /** Collapsed line cap for tool error output. */
1643
+ const maxErrorLines = 8;
1644
+ /**
1645
+ * Character budget for a collapsed error block, sized so the block still
1646
+ * costs about `maxErrorLines` rows on a conventional 80-column terminal.
1647
+ * Error lines wrap instead of clipping, so without this a single
1648
+ * multi-kilobyte line (a raw JSON body, a minified stack) would wrap into a
1649
+ * wall of scrollback.
1650
+ */
1651
+ const maxErrorChars = maxErrorLines * 80;
1652
+ /**
1653
+ * Squeeze the vertical dead space out of a block: leading and trailing blank
1654
+ * lines dropped, runs of blank lines collapsed to one. A failing command's
1655
+ * error text arrives as stderr (newline-terminated) plus a blank-line-
1656
+ * separated `Command exited with code N`, which otherwise renders as a
1657
+ * two-row hole in the middle of the block.
1658
+ */
1659
+ function tightenBlanks(text) {
1660
+ return text.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\s+$/, "");
1661
+ }
1662
+ /**
1663
+ * Collapsed form of an error block: whole lines, up to the line cap and the
1664
+ * character budget, left for the renderer to word-wrap. Unlike ok output,
1665
+ * errors are not clipped at a fixed column — the reason a command failed is
1666
+ * usually in the tail of the message, so an ellipsis at column 100 hides the
1667
+ * one thing the block exists to say.
1668
+ */
1669
+ function collapseError(text) {
1670
+ const source = expandLines(text);
1671
+ const lines = [];
1672
+ let room = maxErrorChars;
1673
+ let cut = false;
1674
+ for (const line of source) {
1675
+ if (lines.length === maxErrorLines || room <= 0) break;
1676
+ if (line.length > room) {
1677
+ lines.push(truncate(line, room));
1678
+ cut = true;
1679
+ break;
1680
+ }
1681
+ lines.push(line);
1682
+ room -= line.length;
1683
+ }
1684
+ const hidden = source.length - lines.length;
1685
+ return {
1686
+ lines,
1687
+ hidden,
1688
+ clipped: cut || hidden > 0
1689
+ };
1690
+ }
1691
+ /** Whether a collapsed error block hides any of its text. */
1692
+ function isErrorClipped(text) {
1693
+ return collapseError(tightenBlanks(text)).clipped;
1694
+ }
1695
+ /**
1696
+ * Tool error output, clamped like ok output: collapsed it clips to
1697
+ * `maxErrorLines` with the "+N more · click to expand" tail; expanded it
1698
+ * renders in full. Same click-to-toggle box as the rest of the block — a
1699
+ * failing command's output shouldn't flood the scrollback just because it
1700
+ * arrived on the error channel.
1701
+ */
1702
+ function ErrorLines({ text, expanded }) {
1703
+ const tidy = tightenBlanks(text);
1704
+ if (expanded) return /* @__PURE__ */ jsx(ExpandedLines, {
1705
+ text: tidy,
1706
+ fg: theme.error
1707
+ });
1708
+ const { lines, hidden, clipped } = collapseError(tidy);
1583
1709
  return /* @__PURE__ */ jsxs("box", {
1710
+ style: { flexDirection: "column" },
1711
+ children: [lines.map((line, idx) => /* @__PURE__ */ jsx("text", {
1712
+ fg: theme.error,
1713
+ style: { wrapMode: "word" },
1714
+ children: line
1715
+ }, idx)), clipped && /* @__PURE__ */ jsx("text", {
1716
+ fg: theme.error,
1717
+ children: hidden > 0 ? `… +${hidden} more lines · click to expand` : "… click to expand"
1718
+ })]
1719
+ });
1720
+ }
1721
+ /** Indents tool output two columns under its header — no rule, just space. */
1722
+ function Indented({ children }) {
1723
+ return /* @__PURE__ */ jsx("box", {
1584
1724
  style: {
1585
- flexDirection: "row",
1586
- paddingLeft: 1,
1587
- paddingRight: 1,
1588
- height: 1,
1589
- backgroundColor: theme.surface ?? "#1a1a1a"
1725
+ flexDirection: "column",
1726
+ paddingLeft: 2
1590
1727
  },
1591
- children: [
1592
- /* @__PURE__ */ jsx("text", {
1593
- fg: theme.accent,
1594
- children: left
1595
- }),
1596
- /* @__PURE__ */ jsx("box", {
1597
- style: { flexGrow: 1 },
1598
- children: /* @__PURE__ */ jsxs("text", {
1599
- fg: theme.muted,
1600
- children: [" ", middle]
1601
- })
1602
- }),
1603
- share ? /* @__PURE__ */ jsxs("text", {
1604
- fg: share.color,
1605
- children: [share.text, " "]
1606
- }) : null,
1607
- rightHint ? /* @__PURE__ */ jsx("text", {
1608
- fg: theme.muted,
1609
- children: rightHint
1610
- }) : null
1611
- ]
1728
+ children
1612
1729
  });
1613
1730
  }
1731
+
1732
+ //#endregion
1733
+ //#region src/chat/tui/status-bar-label.ts
1614
1734
  /**
1615
- * The share indicator, which answers exactly one question: can the agent on
1616
- * screen run commands on this machine right now? Not "is the portal
1617
- * connected" — the connection is a per-machine fact, access is per agent, and
1618
- * a badge that tracks the former tells a user who just declined that the agent
1619
- * is in.
1735
+ * The portal indicator: a standing answer to "can the agent on screen run
1736
+ * commands on my machine right now, and if not, what is in the way?"
1620
1737
  *
1621
- * So it is scoped to the current agent and shown only once that agent holds a
1622
- * grant. Everything else no agent on screen, portal off, connected but this
1623
- * agent ungranted, still connecting renders as the off state (nothing), which
1624
- * is the truthful reading: this agent cannot reach the machine.
1738
+ * Two independent facts decide that, and collapsing them is the trap. The
1739
+ * connection is per MACHINE (the daemon is dialed or it isn't); the grant is
1740
+ * per AGENT (this agent is allowed or it isn't). A badge that tracked only the
1741
+ * connection would show green to someone who had just declined. So it reports
1742
+ * the agent-scoped answer wherever a screen names an agent, and the
1743
+ * machine-level fact on the screens that don't (the pickers), where no grant
1744
+ * is in question.
1625
1745
  *
1626
- * The one exception is an error, and only for an agent that IS granted: its
1627
- * access is supposed to be live and isn't, which the user needs to see.
1628
- */
1629
- function portalIndicator(portal, agentId) {
1630
- if (!agentId || !portal.grantedAgentIds.includes(agentId)) return null;
1746
+ * It always renders something. Silence used to stand for five different
1747
+ * situations at once unavailable, off, still connecting, connected but this
1748
+ * agent ungranted, or no agent on screen — which is exactly when a user needs
1749
+ * to be told which one they are in.
1750
+ */
1751
+ function portalIndicator({ portal, agentId, available }) {
1752
+ if (!available) return {
1753
+ text: "○ portal unavailable",
1754
+ color: theme.muted
1755
+ };
1631
1756
  switch (portal.status) {
1632
- case "off":
1633
- case "connecting": return null;
1634
- case "connected": return {
1635
- text: "● machine connected",
1636
- color: theme.success
1757
+ case "off": return {
1758
+ text: " portal off",
1759
+ color: theme.muted
1760
+ };
1761
+ case "connecting": return {
1762
+ text: "○ portal connecting",
1763
+ color: theme.muted
1637
1764
  };
1638
1765
  case "error": return {
1639
- text: "● machine error",
1766
+ text: "● portal error",
1640
1767
  color: theme.error
1641
1768
  };
1769
+ case "connected": return agentId && portal.grantedAgentIds.includes(agentId) ? {
1770
+ text: "● portal shared",
1771
+ color: theme.success
1772
+ } : {
1773
+ text: "○ portal on",
1774
+ color: theme.muted
1775
+ };
1642
1776
  }
1643
1777
  }
1778
+ /**
1779
+ * Clip the screen label to whatever the fixed parts leave, so the bar degrades
1780
+ * by shortening the one piece that has somewhere else to be (the conversation
1781
+ * title is also the terminal's own title) rather than by chopping a word off
1782
+ * the portal badge. Below a few columns the label is dropped outright: a lone
1783
+ * ellipsis is noise, and the badge and hint still have to read.
1784
+ */
1785
+ function fitMiddle({ label, width, fixed }) {
1786
+ const budget = width - fixed - 5;
1787
+ if (budget < 4) return "";
1788
+ return truncate(label, budget);
1789
+ }
1644
1790
  /** The agent whose access the indicator speaks for, if a screen names one. */
1645
1791
  function screenAgentId(screen) {
1646
1792
  return screen.kind === "chat" || screen.kind === "conversation-picker" ? screen.agent.id : null;
@@ -1696,6 +1842,67 @@ function isNewConv(c) {
1696
1842
  return "kind" in c && c.kind === "new";
1697
1843
  }
1698
1844
 
1845
+ //#endregion
1846
+ //#region src/chat/tui/status-bar.tsx
1847
+ /**
1848
+ * Top-of-screen status line. Mirrors what's in the title of most
1849
+ * editor TUIs: who you are, where you're connected, what you're
1850
+ * looking at.
1851
+ */
1852
+ function StatusBar({ rightHint }) {
1853
+ const appUrl = useStore((s) => s.appUrl);
1854
+ const workspaceName = useStore((s) => s.workspaceName);
1855
+ const screen = useStore((s) => s.screen);
1856
+ const chatTitle = useStore((s) => s.chatTitle);
1857
+ const portal = useStore((s) => s.portal);
1858
+ const authKind = useStore((s) => s.authKind);
1859
+ const { width } = useTerminalDimensions();
1860
+ const left = headerLeftLabel(appUrl, workspaceName);
1861
+ const badge = portalIndicator({
1862
+ portal,
1863
+ agentId: screenAgentId(screen),
1864
+ available: authKind === "session"
1865
+ });
1866
+ const middle = fitMiddle({
1867
+ label: screenLabel(screen, chatTitle),
1868
+ width,
1869
+ fixed: left.length + badge.text.length + (rightHint?.length ?? 0)
1870
+ });
1871
+ return /* @__PURE__ */ jsxs("box", {
1872
+ style: {
1873
+ flexDirection: "row",
1874
+ paddingLeft: 1,
1875
+ paddingRight: 1,
1876
+ height: 1,
1877
+ backgroundColor: theme.surface ?? "#1a1a1a"
1878
+ },
1879
+ children: [
1880
+ /* @__PURE__ */ jsx("text", {
1881
+ fg: theme.accent,
1882
+ children: left
1883
+ }),
1884
+ /* @__PURE__ */ jsx("box", {
1885
+ style: {
1886
+ flexGrow: 1,
1887
+ flexShrink: 1
1888
+ },
1889
+ children: /* @__PURE__ */ jsxs("text", {
1890
+ fg: theme.muted,
1891
+ children: [" ", middle]
1892
+ })
1893
+ }),
1894
+ /* @__PURE__ */ jsxs("text", {
1895
+ fg: badge.color,
1896
+ children: [badge.text, " "]
1897
+ }),
1898
+ rightHint ? /* @__PURE__ */ jsx("text", {
1899
+ fg: theme.muted,
1900
+ children: rightHint
1901
+ }) : null
1902
+ ]
1903
+ });
1904
+ }
1905
+
1699
1906
  //#endregion
1700
1907
  //#region src/chat/tui/list-window.ts
1701
1908
  /** Keeps the highlighted row inside the visible window as the user moves. */
@@ -2716,171 +2923,6 @@ function WorkspacePicker({ appUrl, sessionToken, onSelect, onCancel }) {
2716
2923
  });
2717
2924
  }
2718
2925
 
2719
- //#endregion
2720
- //#region src/chat/tui/chunks/frame.tsx
2721
- /** Status glyph + color for a tool's lifecycle state. */
2722
- function toolGlyph(state) {
2723
- switch (state) {
2724
- case "streaming":
2725
- case "input-ready": return {
2726
- ch: "◐",
2727
- color: theme.warning
2728
- };
2729
- case "output-ready": return {
2730
- ch: "✓",
2731
- color: theme.success
2732
- };
2733
- case "error": return {
2734
- ch: "✗",
2735
- color: theme.error
2736
- };
2737
- }
2738
- }
2739
- /** Truncate a single line with an ellipsis. */
2740
- function truncate(line, maxLen) {
2741
- return line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line;
2742
- }
2743
- /**
2744
- * Compact a block of tool output for the transcript: truncate over-long
2745
- * lines (so a giant single-line JSON blob doesn't wrap to a wall) and cap
2746
- * the line count, with a "+N more" tail that doubles as the click-to-expand
2747
- * affordance. Expanded blocks render through `ExpandedLines` instead —
2748
- * full content, word-wrapped, nothing clipped.
2749
- */
2750
- function clipLines(text, { maxLines, maxLineLen }) {
2751
- const lines = text.replace(/\t/g, " ").split("\n");
2752
- const out = lines.slice(0, maxLines).map((line) => truncate(line, maxLineLen));
2753
- if (lines.length > maxLines) out.push(`… +${lines.length - maxLines} more lines · click to expand`);
2754
- return out;
2755
- }
2756
- /**
2757
- * Split a block for the expanded view: tabs normalised, no line cap, no
2758
- * per-line truncation — rendering word-wraps instead. Blank lines become a
2759
- * single space so they keep their row (an empty <text> collapses).
2760
- */
2761
- function expandLines(text) {
2762
- return text.replace(/\t/g, " ").split("\n").map((line) => line.length === 0 ? " " : line);
2763
- }
2764
- /**
2765
- * Full, untruncated block content for an expanded view: every line kept and
2766
- * word-wrapped by the terminal instead of ellipsis-clipped.
2767
- */
2768
- function ExpandedLines({ text, fg }) {
2769
- return /* @__PURE__ */ jsx("box", {
2770
- style: { flexDirection: "column" },
2771
- children: expandLines(text).map((line, idx) => /* @__PURE__ */ jsx("text", {
2772
- fg,
2773
- style: { wrapMode: "word" },
2774
- children: line
2775
- }, idx))
2776
- });
2777
- }
2778
- /** Whether a block of text has lines hidden by the collapsed line cap. */
2779
- function isClipped(text, maxLines) {
2780
- let count = 1;
2781
- for (let i = 0; i < text.length; i++) {
2782
- if (text.charCodeAt(i) === 10) count++;
2783
- if (count > maxLines) return true;
2784
- }
2785
- return false;
2786
- }
2787
- /** Dim tail line under an expanded block: click anywhere on it to fold. */
2788
- function CollapseHint() {
2789
- return /* @__PURE__ */ jsx("text", {
2790
- fg: theme.dim,
2791
- children: "· click to collapse"
2792
- });
2793
- }
2794
- /** Collapsed line cap for tool error output. */
2795
- const maxErrorLines = 8;
2796
- /**
2797
- * Character budget for a collapsed error block, sized so the block still
2798
- * costs about `maxErrorLines` rows on a conventional 80-column terminal.
2799
- * Error lines wrap instead of clipping, so without this a single
2800
- * multi-kilobyte line (a raw JSON body, a minified stack) would wrap into a
2801
- * wall of scrollback.
2802
- */
2803
- const maxErrorChars = maxErrorLines * 80;
2804
- /**
2805
- * Squeeze the vertical dead space out of a block: leading and trailing blank
2806
- * lines dropped, runs of blank lines collapsed to one. A failing command's
2807
- * error text arrives as stderr (newline-terminated) plus a blank-line-
2808
- * separated `Command exited with code N`, which otherwise renders as a
2809
- * two-row hole in the middle of the block.
2810
- */
2811
- function tightenBlanks(text) {
2812
- return text.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\s+$/, "");
2813
- }
2814
- /**
2815
- * Collapsed form of an error block: whole lines, up to the line cap and the
2816
- * character budget, left for the renderer to word-wrap. Unlike ok output,
2817
- * errors are not clipped at a fixed column — the reason a command failed is
2818
- * usually in the tail of the message, so an ellipsis at column 100 hides the
2819
- * one thing the block exists to say.
2820
- */
2821
- function collapseError(text) {
2822
- const source = expandLines(text);
2823
- const lines = [];
2824
- let room = maxErrorChars;
2825
- let cut = false;
2826
- for (const line of source) {
2827
- if (lines.length === maxErrorLines || room <= 0) break;
2828
- if (line.length > room) {
2829
- lines.push(truncate(line, room));
2830
- cut = true;
2831
- break;
2832
- }
2833
- lines.push(line);
2834
- room -= line.length;
2835
- }
2836
- const hidden = source.length - lines.length;
2837
- return {
2838
- lines,
2839
- hidden,
2840
- clipped: cut || hidden > 0
2841
- };
2842
- }
2843
- /** Whether a collapsed error block hides any of its text. */
2844
- function isErrorClipped(text) {
2845
- return collapseError(tightenBlanks(text)).clipped;
2846
- }
2847
- /**
2848
- * Tool error output, clamped like ok output: collapsed it clips to
2849
- * `maxErrorLines` with the "+N more · click to expand" tail; expanded it
2850
- * renders in full. Same click-to-toggle box as the rest of the block — a
2851
- * failing command's output shouldn't flood the scrollback just because it
2852
- * arrived on the error channel.
2853
- */
2854
- function ErrorLines({ text, expanded }) {
2855
- const tidy = tightenBlanks(text);
2856
- if (expanded) return /* @__PURE__ */ jsx(ExpandedLines, {
2857
- text: tidy,
2858
- fg: theme.error
2859
- });
2860
- const { lines, hidden, clipped } = collapseError(tidy);
2861
- return /* @__PURE__ */ jsxs("box", {
2862
- style: { flexDirection: "column" },
2863
- children: [lines.map((line, idx) => /* @__PURE__ */ jsx("text", {
2864
- fg: theme.error,
2865
- style: { wrapMode: "word" },
2866
- children: line
2867
- }, idx)), clipped && /* @__PURE__ */ jsx("text", {
2868
- fg: theme.error,
2869
- children: hidden > 0 ? `… +${hidden} more lines · click to expand` : "… click to expand"
2870
- })]
2871
- });
2872
- }
2873
- /** Indents tool output two columns under its header — no rule, just space. */
2874
- function Indented({ children }) {
2875
- return /* @__PURE__ */ jsx("box", {
2876
- style: {
2877
- flexDirection: "column",
2878
- paddingLeft: 2
2879
- },
2880
- children
2881
- });
2882
- }
2883
-
2884
2926
  //#endregion
2885
2927
  //#region src/chat/tui/list-line.ts
2886
2928
  /**
@@ -7281,13 +7323,14 @@ function useCompaction({ rest, conversationId }) {
7281
7323
  //#endregion
7282
7324
  //#region src/chat/tui/chat/footer-hints.ts
7283
7325
  /** Build the chat footer copy independently of its colors and layout. */
7284
- function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer, actionableCardCount, nonWebChannel }) {
7326
+ function formatChatFooterStatus({ runKind, ctrlCArmed, portalOpen, hasPendingSteer, actionableCardCount, nonWebChannel }) {
7327
+ const portalHint = `ctrl+t ${portalOpen ? "close portal" : "portal"}`;
7285
7328
  const connectHint = actionableCardCount > 1 ? `ctrl+r connect (${actionableCardCount}) · ` : actionableCardCount === 1 ? "ctrl+r connect · " : "";
7286
7329
  switch (runKind) {
7287
7330
  case "idle":
7288
7331
  if (ctrlCArmed) return "press ctrl+c again to quit";
7289
- if (nonWebChannel) return `esc back · ctrl+c quit · ctrl+t ${isShared ? "revoke access" : "local access"} · ? keybinds`;
7290
- return `${connectHint}↵ send · / commands · esc back · ctrl+c quit · ctrl+t ${isShared ? "revoke access" : "local access"} · ? keybinds`;
7332
+ if (nonWebChannel) return `esc back · ctrl+c quit · ${portalHint} · ? keybinds`;
7333
+ return `${connectHint}↵ send · / commands · esc back · ctrl+c quit · ${portalHint} · ? keybinds`;
7291
7334
  case "sending": return "sending…";
7292
7335
  case "streaming": return hasPendingSteer ? `${connectHint}↵ steer · esc cancel steer · ctrl+c cancel run` : `${connectHint}↵ steer · esc leave (keeps running) · ctrl+c cancel`;
7293
7336
  }
@@ -7309,7 +7352,7 @@ function formatModelHint(model, effort) {
7309
7352
  * bar and footer can't fit, one aligned `label value` row each. Pure so the
7310
7353
  * layout is testable without a renderer.
7311
7354
  */
7312
- function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, effort, share }) {
7355
+ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, effort, portal }) {
7313
7356
  const rows = [
7314
7357
  ["app", appUrl ?? "unknown"],
7315
7358
  ["workspace", workspaceName ?? "unknown"],
@@ -7318,14 +7361,20 @@ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, mo
7318
7361
  ["model", model ?? "default"]
7319
7362
  ];
7320
7363
  if (effort) rows.push(["effort", effort]);
7321
- rows.push(["local access", formatShare(share)]);
7364
+ rows.push(["portal", formatPortal(portal)]);
7322
7365
  const width = Math.max(...rows.map(([label]) => label.length));
7323
7366
  return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
7324
7367
  }
7325
- function formatShare(share) {
7326
- if (share.status === "off") return "off (ctrl+t to share this machine)";
7327
- if (share.status !== "connected") return share.status;
7328
- return share.grantedToAgent ? "shared (this agent can run commands here)" : "shared (this agent is not granted)";
7368
+ /**
7369
+ * Spell out both halves of the portal's state, because "can this agent reach
7370
+ * my machine" is the conjunction of a per-machine connection and a per-agent
7371
+ * grant and connected-but-ungranted is exactly the case a one-word status
7372
+ * loses.
7373
+ */
7374
+ function formatPortal(portal) {
7375
+ if (portal.status === "off") return "off (ctrl+t to open it)";
7376
+ if (portal.status !== "connected") return portal.status;
7377
+ return portal.grantedToAgent ? "shared (this agent can run commands on this machine)" : "on (this agent is not granted)";
7329
7378
  }
7330
7379
 
7331
7380
  //#endregion
@@ -7760,7 +7809,7 @@ const keybindGroups = [
7760
7809
  },
7761
7810
  {
7762
7811
  keys: "ctrl+t",
7763
- action: "local access: grant / revoke this machine"
7812
+ action: "open / close the portal on this machine"
7764
7813
  },
7765
7814
  {
7766
7815
  keys: "ctrl+l",
@@ -8013,7 +8062,7 @@ const keybindGroups = [
8013
8062
  }]
8014
8063
  },
8015
8064
  {
8016
- title: "local access prompt",
8065
+ title: "portal prompt",
8017
8066
  binds: [{
8018
8067
  keys: "y",
8019
8068
  action: "allow the agent to run commands"
@@ -8330,7 +8379,7 @@ const slashCommands = [
8330
8379
  name: "share",
8331
8380
  aliases: [],
8332
8381
  argsHint: null,
8333
- description: "local access: grant / revoke this machine",
8382
+ description: "open / close the portal on this machine",
8334
8383
  action: { kind: "share" }
8335
8384
  },
8336
8385
  {
@@ -8358,7 +8407,7 @@ const slashCommands = [
8358
8407
  name: "portal",
8359
8408
  aliases: ["local"],
8360
8409
  argsHint: null,
8361
- description: "allow this agent to access your computer",
8410
+ description: "give this agent access to your machine over the portal",
8362
8411
  action: { kind: "portal" }
8363
8412
  },
8364
8413
  {
@@ -12226,7 +12275,7 @@ function ChatScreen({ agent, conversation, attachRunId }) {
12226
12275
  const declinedRef = useRef(/* @__PURE__ */ new Set());
12227
12276
  const grantInFlightRef = useRef(/* @__PURE__ */ new Set());
12228
12277
  const grantPrompt = portal.prompt && portal.prompt.agentId === agent.id ? portal.prompt : null;
12229
- const isShared = portal.status !== "off";
12278
+ const portalOpen = portal.status !== "off";
12230
12279
  const initialConversationId = isNewConversation(conversation) ? null : conversation.id;
12231
12280
  const agentHost = useAgentHost();
12232
12281
  const nonWebChannel = nonWebChannelLabel(conversation);
@@ -13537,7 +13586,7 @@ function ChatScreen({ agent, conversation, attachRunId }) {
13537
13586
  ]);
13538
13587
  const toggleShare = useCallback(() => {
13539
13588
  if (!portalClient) {
13540
- useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
13589
+ useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "the portal needs a browser session; run `skydive auth login --web`" : "the portal is unavailable in this session" });
13541
13590
  return;
13542
13591
  }
13543
13592
  if (portal.status === "off") {
@@ -13591,7 +13640,7 @@ function ChatScreen({ agent, conversation, attachRunId }) {
13591
13640
  ]);
13592
13641
  const openPortalAccess = useCallback(() => {
13593
13642
  if (!portalClient) {
13594
- useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
13643
+ useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "the portal needs a browser session; run `skydive auth login --web`" : "the portal is unavailable in this session" });
13595
13644
  return;
13596
13645
  }
13597
13646
  switch (portal.status) {
@@ -13599,10 +13648,10 @@ function ChatScreen({ agent, conversation, attachRunId }) {
13599
13648
  declinedRef.current.delete(agent.id);
13600
13649
  portalClient.decline(agent.id, false);
13601
13650
  portalClient.enable();
13602
- useStore.getState().showToast({ message: "enabling local access on this machine…" });
13651
+ useStore.getState().showToast({ message: "opening the portal to this machine…" });
13603
13652
  break;
13604
13653
  case "connecting":
13605
- useStore.getState().showToast({ message: "local access is still connecting…" });
13654
+ useStore.getState().showToast({ message: "the portal is still connecting…" });
13606
13655
  break;
13607
13656
  case "connected":
13608
13657
  if (portal.grantedAgentIds.includes(agent.id)) {
@@ -13617,7 +13666,7 @@ function ChatScreen({ agent, conversation, attachRunId }) {
13617
13666
  });
13618
13667
  break;
13619
13668
  case "error":
13620
- useStore.getState().showToast({ message: portal.error ? `local access error: ${portal.error}` : "local access hit an error" });
13669
+ useStore.getState().showToast({ message: portal.error ? `portal error: ${portal.error}` : "the portal hit an error" });
13621
13670
  break;
13622
13671
  default: {
13623
13672
  const exhaustive = portal.status;
@@ -13730,7 +13779,7 @@ function ChatScreen({ agent, conversation, attachRunId }) {
13730
13779
  conversationId,
13731
13780
  model,
13732
13781
  effort,
13733
- share: {
13782
+ portal: {
13734
13783
  status: portal.status,
13735
13784
  grantedToAgent: portal.grantedAgentIds.includes(agent.id)
13736
13785
  }
@@ -14125,14 +14174,14 @@ function ChatScreen({ agent, conversation, attachRunId }) {
14125
14174
  const statusText = useMemo(() => formatChatFooterStatus({
14126
14175
  runKind: run.kind,
14127
14176
  ctrlCArmed,
14128
- isShared,
14177
+ portalOpen,
14129
14178
  hasPendingSteer,
14130
14179
  actionableCardCount: actionableCards.length,
14131
14180
  nonWebChannel: nonWebChannel !== null
14132
14181
  }), [
14133
14182
  run.kind,
14134
14183
  ctrlCArmed,
14135
- isShared,
14184
+ portalOpen,
14136
14185
  hasPendingSteer,
14137
14186
  actionableCards.length,
14138
14187
  nonWebChannel