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.
- package/CHANGELOG.md +2 -0
- package/README.md +25 -8
- package/dist/js/bin.mjs +20 -20
- package/dist/js/{boot-BoqVFa4M.mjs → boot-D_VFIRJj.mjs} +301 -252
- package/dist/js/client-CkPQG8M1.mjs +5 -0
- package/dist/js/{client-C2lPer4b.mjs → client-aFwHzCPG.mjs} +99 -8
- package/dist/js/{daemon-B57zYVAN.mjs → daemon-CORAITjj.mjs} +2 -2
- package/dist/js/{daemon-client-Btr9rmKl.mjs → daemon-client-Cfi66Xy9.mjs} +1 -1
- package/dist/js/daemon-client-DJPW9cRp.mjs +8 -0
- package/dist/js/{daemon-B7Wv4P6W.mjs → daemon-uzpOdRPL.mjs} +2 -2
- package/dist/js/{install-BOtarTEl.mjs → install-BBplm-Zp.mjs} +2 -2
- package/dist/js/{print-ejS3eAZz.mjs → print-Cuao6LN6.mjs} +2 -2
- package/dist/js/{print-SD_y9CTF.mjs → print-DR6Gas-M.mjs} +5 -5
- package/dist/js/{print-share-D20YvVbb.mjs → print-share-BypBUrNh.mjs} +2 -2
- package/dist/js/{rest-Cz-aN-ME.mjs → rest-CJkBP2Jz.mjs} +1 -1
- package/dist/js/{rest-B-dizrBw.mjs → rest-DkuT5_oX.mjs} +1 -1
- package/package.json +5 -5
- package/dist/js/client-CSKr4Swa.mjs +0 -5
- package/dist/js/daemon-client-DL3eNr6N.mjs +0 -8
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
8
|
-
import "./daemon-
|
|
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-
|
|
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/
|
|
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
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
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
|
|
1575
|
-
const
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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: "
|
|
1586
|
-
paddingLeft:
|
|
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
|
|
1616
|
-
*
|
|
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
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
* agent
|
|
1624
|
-
*
|
|
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
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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: "●
|
|
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,
|
|
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 ·
|
|
7290
|
-
return `${connectHint}↵ send · / commands · esc back · ctrl+c quit ·
|
|
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,
|
|
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(["
|
|
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
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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" ? "
|
|
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" ? "
|
|
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: "
|
|
13651
|
+
useStore.getState().showToast({ message: "opening the portal to this machine…" });
|
|
13603
13652
|
break;
|
|
13604
13653
|
case "connecting":
|
|
13605
|
-
useStore.getState().showToast({ message: "
|
|
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 ? `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14184
|
+
portalOpen,
|
|
14136
14185
|
hasPendingSteer,
|
|
14137
14186
|
actionableCards.length,
|
|
14138
14187
|
nonWebChannel
|