skydive-cli 0.6.0-beta.48 → 0.6.0-beta.59
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 +4 -0
- package/dist/js/bin.mjs +543 -70
- package/dist/js/{boot-CgKBdegB.mjs → boot-oH6ZHYdH.mjs} +390 -63
- package/dist/js/{client-CFnT0cAy.mjs → client-CAX6sE_Y.mjs} +1 -1
- package/dist/js/{client-jTc5DmAj.mjs → client-CnvxmYZ6.mjs} +1 -1
- package/dist/js/{client-DzD76Qd7.mjs → client-CwrwJrXX.mjs} +2 -1
- package/dist/js/{daemon-DcACMRe7.mjs → daemon-Dr2kCY9V.mjs} +2 -2
- package/dist/js/{daemon-D7hURPaU.mjs → daemon-DyzekuZq.mjs} +55 -15
- package/dist/js/{daemon-client-GaxCwNSy.mjs → daemon-client-BYB_2WYU.mjs} +1 -1
- package/dist/js/daemon-client-BpkvG_0G.mjs +8 -0
- package/dist/js/{install-CBFdOTjb.mjs → install-aadv_dwX.mjs} +49 -3
- package/dist/js/{print-2hTs7Wb6.mjs → print-BbRVB3lw.mjs} +2 -2
- package/dist/js/{print-B38Wx3CM.mjs → print-CDDNlUPH.mjs} +2 -2
- package/dist/js/{print-share-R1p66Gev.mjs → print-share-BZCjBNpt.mjs} +1 -1
- package/dist/js/raw-pty-BF3tEnca.mjs +5 -0
- package/dist/js/{raw-pty-Cj-O0pyb.mjs → raw-pty-BcZNOOGh.mjs} +1 -1
- package/dist/js/{rest-BUR9emV8.mjs → rest-CnpAZq0e.mjs} +1 -1
- package/dist/js/{rest-UJz2FS1U.mjs → rest-DdTf06cT.mjs} +36 -2
- package/package.json +8 -7
- package/dist/js/daemon-client-JTG8fkSV.mjs +0 -8
- package/dist/js/raw-pty-HlNGqFLC.mjs +0 -5
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as errorMessage, i as sendErrorMessage, n as errorDetail, o as isRecord, t as createRestClient } from "./rest-
|
|
3
|
-
import { C as DEFAULT_API_URL, D as getConfigPath, M as getReviewStateDir, N as getSavedTheme, R as recordDefaultAgent, W as resolveWebUrl, _ as computeSettledLabel, c as actionableCardVerb, d as parseOauthConnectParams, f as prioritizeActionableCards, g as composeQuestionAnswerEntries, h as composeQuestionAnswer, i as resolveAgent, l as cardActionErrorMessage, m as resolveConnectUrl, p as reconcileMaskedInput, q as saveTheme, s as MASK_CHAR, u as parseExternalOauthConnectParams, v as parseConnectCard, w as DEFAULT_APP_URL, y as specKeyFor } from "./print-
|
|
4
|
-
import { A as
|
|
2
|
+
import { a as errorMessage, i as sendErrorMessage, n as errorDetail, o as isRecord, t as createRestClient } from "./rest-DdTf06cT.mjs";
|
|
3
|
+
import { C as DEFAULT_API_URL, D as getConfigPath, M as getReviewStateDir, N as getSavedTheme, R as recordDefaultAgent, W as resolveWebUrl, _ as computeSettledLabel, c as actionableCardVerb, d as parseOauthConnectParams, f as prioritizeActionableCards, g as composeQuestionAnswerEntries, h as composeQuestionAnswer, i as resolveAgent, l as cardActionErrorMessage, m as resolveConnectUrl, p as reconcileMaskedInput, q as saveTheme, s as MASK_CHAR, u as parseExternalOauthConnectParams, v as parseConnectCard, w as DEFAULT_APP_URL, y as specKeyFor } from "./print-CDDNlUPH.mjs";
|
|
4
|
+
import { A as aliasActivationHint, C as themeMode, D as themesForMode, L as detectShell, M as installAgentAlias, N as slugifyAliasName, P as takenAliasNames, S as themeForMode, T as themeVersion, U as setActiveWorkspace, V as listWorkspaces, _ as applyTheme, a as writeCrashReport, b as noColorRequested, g as DEFAULT_THEME_ID, h as writeArtifact, i as buildCrashReport, j as dedupeAliasName, k as machineOsFromPlatform, l as splashFitsWidth, m as record, n as disableTerminalAutowrap, o as MARK_CELLS, p as profilingEnabled, r as restoreTerminalAutowrap, s as WORDMARK, t as installCrashHandler, v as findTheme, w as themeModeFromColorFgBg, x as theme, y as monoTheme, z as getActiveWorkspaceId } from "./install-aadv_dwX.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 { n as PortalClient } from "./client-
|
|
7
|
+
import { n as PortalClient } from "./client-CwrwJrXX.mjs";
|
|
8
8
|
import { t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
|
|
9
9
|
import "./api-BFQ4PQDA.mjs";
|
|
10
|
-
import "./daemon-
|
|
11
|
-
import { t as SandboxStream } from "./client-
|
|
12
|
-
import { t as PortalDaemonClient } from "./daemon-client-
|
|
13
|
-
import { t as runRawPtyPassthrough } from "./raw-pty-
|
|
10
|
+
import "./daemon-DyzekuZq.mjs";
|
|
11
|
+
import { t as SandboxStream } from "./client-CAX6sE_Y.mjs";
|
|
12
|
+
import { t as PortalDaemonClient } from "./daemon-client-BYB_2WYU.mjs";
|
|
13
|
+
import { t as runRawPtyPassthrough } from "./raw-pty-BcZNOOGh.mjs";
|
|
14
14
|
import * as os$1 from "node:os";
|
|
15
15
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
16
16
|
import path, { basename, extname, isAbsolute, join, posix, win32 } from "node:path";
|
|
17
|
+
import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
17
18
|
import { z } from "zod";
|
|
18
|
-
import open from "open";
|
|
19
|
+
import open$1 from "open";
|
|
19
20
|
import { constants } from "node:fs";
|
|
20
21
|
import { execFile, spawn } from "node:child_process";
|
|
21
22
|
import { createHash } from "node:crypto";
|
|
22
23
|
import { connect, createConnection } from "node:net";
|
|
23
|
-
import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
24
24
|
import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, addDefaultParsers, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
|
|
25
25
|
import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
26
26
|
import { Profiler, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -131,6 +131,7 @@ const useStore = create((set, get) => ({
|
|
|
131
131
|
rest: null,
|
|
132
132
|
promptHistoryPath: null,
|
|
133
133
|
chatTitle: null,
|
|
134
|
+
chatSpend: null,
|
|
134
135
|
chatBackTarget: null,
|
|
135
136
|
portal: initialPortalUi,
|
|
136
137
|
portalClient: null,
|
|
@@ -143,11 +144,13 @@ const useStore = create((set, get) => ({
|
|
|
143
144
|
goTo: (screen) => set({
|
|
144
145
|
screen,
|
|
145
146
|
chatTitle: initialChatTitle(screen),
|
|
147
|
+
chatSpend: null,
|
|
146
148
|
chatBackTarget: null
|
|
147
149
|
}),
|
|
148
150
|
drillTo: (screen, back) => set({
|
|
149
151
|
screen,
|
|
150
152
|
chatTitle: initialChatTitle(screen),
|
|
153
|
+
chatSpend: null,
|
|
151
154
|
chatBackTarget: back
|
|
152
155
|
}),
|
|
153
156
|
setThemeMode: (mode) => {
|
|
@@ -182,13 +185,18 @@ const useStore = create((set, get) => ({
|
|
|
182
185
|
promptHistoryPath
|
|
183
186
|
}),
|
|
184
187
|
setChatTitle: (title) => set((state) => state.screen.kind === "chat" ? { chatTitle: title } : {}),
|
|
188
|
+
setChatSpend: (cents, usage) => set((state) => state.screen.kind === "chat" ? { chatSpend: {
|
|
189
|
+
cents,
|
|
190
|
+
usage
|
|
191
|
+
} } : {}),
|
|
185
192
|
setWorkspaceName: (workspaceName) => set({ workspaceName }),
|
|
186
193
|
fail: (message) => set({
|
|
187
194
|
screen: {
|
|
188
195
|
kind: "error",
|
|
189
196
|
message
|
|
190
197
|
},
|
|
191
|
-
chatTitle: null
|
|
198
|
+
chatTitle: null,
|
|
199
|
+
chatSpend: null
|
|
192
200
|
}),
|
|
193
201
|
setPortalClient: (portalClient) => set({ portalClient }),
|
|
194
202
|
syncPortal: ({ status, machineName, error, grantedAgentIds, declinedAgentIds }) => set((state) => ({ portal: {
|
|
@@ -402,7 +410,7 @@ function createPortalClient(opts) {
|
|
|
402
410
|
persistedMachineName: null,
|
|
403
411
|
onMachineName: () => {},
|
|
404
412
|
onState: (state) => opts.onState({
|
|
405
|
-
status: state.status,
|
|
413
|
+
status: state.status === "stand-down" ? "connecting" : state.status,
|
|
406
414
|
machineName: state.machineName,
|
|
407
415
|
friendlyName: state.friendlyName,
|
|
408
416
|
error: state.error,
|
|
@@ -1581,6 +1589,96 @@ function hostOf(url) {
|
|
|
1581
1589
|
}
|
|
1582
1590
|
}
|
|
1583
1591
|
|
|
1592
|
+
//#endregion
|
|
1593
|
+
//#region src/chat/tui/chat/spend.ts
|
|
1594
|
+
/**
|
|
1595
|
+
* Cents → `$1.23`. Spend that rounds to zero but isn't ("<$0.01") stays
|
|
1596
|
+
* visibly non-zero so a cheap turn never reads as a free one.
|
|
1597
|
+
*/
|
|
1598
|
+
function formatCents(cents) {
|
|
1599
|
+
if (cents === 0) return "$0.00";
|
|
1600
|
+
if (cents < 1) return "<$0.01";
|
|
1601
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* The status-bar readout: the conversation's spend so far including any
|
|
1605
|
+
* subagents it spawned. Prefixed `≈` because the compute share is an
|
|
1606
|
+
* estimate (see the api's conversationUsage) and the figure trails a live
|
|
1607
|
+
* turn by the telemetry lag. Null before the first read lands.
|
|
1608
|
+
*/
|
|
1609
|
+
function formatSpendBadge(totalCents) {
|
|
1610
|
+
if (totalCents === null) return null;
|
|
1611
|
+
return `≈${formatCents(totalCents)}`;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Body text for the `/cost` transcript block, for every outcome of the read.
|
|
1615
|
+
* `null` is "no conversation yet" (nothing has been sent).
|
|
1616
|
+
*/
|
|
1617
|
+
function formatCostRead(read) {
|
|
1618
|
+
if (read === null) return "Nothing spent yet — this conversation is created when you send the first message.";
|
|
1619
|
+
switch (read.kind) {
|
|
1620
|
+
case "usage": return formatCostInfo(read.usage);
|
|
1621
|
+
case "off": return "Spend tracking is not turned on for this workspace yet.";
|
|
1622
|
+
case "unavailable": return "Couldn't read this conversation's spend right now — try again in a moment.";
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Inference by model, the subagents' share, the compute estimate, and the
|
|
1627
|
+
* total, one aligned `label value` row each. Pure so the layout is testable
|
|
1628
|
+
* without a renderer.
|
|
1629
|
+
*/
|
|
1630
|
+
function formatCostInfo(usage) {
|
|
1631
|
+
const rows = [];
|
|
1632
|
+
if (usage.own.byModel.length === 0) rows.push(["inference", formatCents(0)]);
|
|
1633
|
+
for (const line of usage.own.byModel) rows.push([line.model ?? "unknown model", `${formatCents(line.cents)} ${formatTokens(line)}`]);
|
|
1634
|
+
if (usage.children.conversationCount > 0) {
|
|
1635
|
+
const n = usage.children.conversationCount;
|
|
1636
|
+
rows.push(["subagents", `${formatCents(usage.children.inferenceCents)} inference across ${n} ${n === 1 ? "conversation" : "conversations"}`]);
|
|
1637
|
+
}
|
|
1638
|
+
rows.push(["compute", `${formatCents(usage.compute.cents)} est. from ${formatVcpuHours(usage.compute.vcpuSeconds)} of run time`]);
|
|
1639
|
+
rows.push(["total", formatCents(usage.totalCents)]);
|
|
1640
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
1641
|
+
const body = rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
|
|
1642
|
+
const notes = ["as of the last metered call; compute is the time this conversation's runs (subagents included) kept the agent's computer busy, priced at your rates — idle time is not counted"];
|
|
1643
|
+
if (usage.unpriced.length > 0) notes.push(`no rate on file for ${usage.unpriced.join(", ")} — that usage is in the counts above but priced at $0, so the total understates`);
|
|
1644
|
+
return `${body}\n\n${notes.join("\n")}`;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Compact lines for the side panel's costs section: the total first, then
|
|
1648
|
+
* the three buckets behind it. Wider notes live in `/cost`; the panel
|
|
1649
|
+
* names the caveat in one short line when a model has no rate on file.
|
|
1650
|
+
* Pure so the layout is testable without a renderer.
|
|
1651
|
+
*/
|
|
1652
|
+
function formatSpendSection(usage) {
|
|
1653
|
+
const lines = [`${formatCents(usage.totalCents)} total`];
|
|
1654
|
+
if (usage.own.byModel.length === 0) lines.push(`inference ${formatCents(0)}`);
|
|
1655
|
+
else {
|
|
1656
|
+
lines.push(`inference ${formatCents(usage.own.inferenceCents)}${usage.own.byModel.length > 1 ? ` (${usage.own.byModel.length} models)` : ""}`);
|
|
1657
|
+
for (const line of usage.own.byModel) lines.push(` ${line.model ?? "unknown"} ${formatCents(line.cents)}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (usage.children.conversationCount > 0) lines.push(`subagents ${formatCents(usage.children.inferenceCents)}`);
|
|
1660
|
+
lines.push(`compute ${formatCents(usage.compute.cents)} est. (${formatVcpuHours(usage.compute.vcpuSeconds)})`);
|
|
1661
|
+
if (usage.unpriced.length > 0) lines.push(`${usage.unpriced.join(", ")} unpriced — total understates (/cost)`);
|
|
1662
|
+
return lines;
|
|
1663
|
+
}
|
|
1664
|
+
function formatTokens(line) {
|
|
1665
|
+
const parts = [`${formatCount(line.promptTokens)} in`, `${formatCount(line.completionTokens)} out`];
|
|
1666
|
+
const cached = line.cacheReadTokens + line.cacheCreationTokens;
|
|
1667
|
+
if (cached > 0) parts.push(`${formatCount(cached)} cached`);
|
|
1668
|
+
return parts.join(", ");
|
|
1669
|
+
}
|
|
1670
|
+
function formatCount(n) {
|
|
1671
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1672
|
+
if (n >= 1e4) return `${Math.round(n / 1e3)}k`;
|
|
1673
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
1674
|
+
return String(n);
|
|
1675
|
+
}
|
|
1676
|
+
function formatVcpuHours(vcpuSeconds) {
|
|
1677
|
+
if (vcpuSeconds < 60) return `${Math.round(vcpuSeconds)} vCPU-s`;
|
|
1678
|
+
if (vcpuSeconds < 3600) return `${(vcpuSeconds / 60).toFixed(1)} vCPU-min`;
|
|
1679
|
+
return `${(vcpuSeconds / 3600).toFixed(2)} vCPU-h`;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1584
1682
|
//#endregion
|
|
1585
1683
|
//#region src/chat/tui/chunks/frame.tsx
|
|
1586
1684
|
/** Status glyph + color for a tool's lifecycle state. */
|
|
@@ -1775,7 +1873,8 @@ function portalIndicator({ portal, agentId, available }) {
|
|
|
1775
1873
|
text: "○ portal off",
|
|
1776
1874
|
color: theme.muted
|
|
1777
1875
|
};
|
|
1778
|
-
case "connecting":
|
|
1876
|
+
case "connecting":
|
|
1877
|
+
case "stand-down": return {
|
|
1779
1878
|
text: "○ portal connecting",
|
|
1780
1879
|
color: theme.muted
|
|
1781
1880
|
};
|
|
@@ -1869,6 +1968,7 @@ function StatusBar({ rightHint }) {
|
|
|
1869
1968
|
const workspaceName = useStore((s) => s.workspaceName);
|
|
1870
1969
|
const screen = useStore((s) => s.screen);
|
|
1871
1970
|
const chatTitle = useStore((s) => s.chatTitle);
|
|
1971
|
+
const chatSpend = useStore((s) => s.chatSpend);
|
|
1872
1972
|
const portal = useStore((s) => s.portal);
|
|
1873
1973
|
const authKind = useStore((s) => s.authKind);
|
|
1874
1974
|
const { width } = useTerminalDimensions();
|
|
@@ -1878,10 +1978,11 @@ function StatusBar({ rightHint }) {
|
|
|
1878
1978
|
agentId: screenAgentId(screen),
|
|
1879
1979
|
available: authKind === "session"
|
|
1880
1980
|
});
|
|
1981
|
+
const right = formatSpendBadge(chatSpend?.cents ?? null) ?? rightHint;
|
|
1881
1982
|
const middle = fitMiddle({
|
|
1882
1983
|
label: screenLabel(screen, chatTitle),
|
|
1883
1984
|
width,
|
|
1884
|
-
fixed: left.length + badge.text.length + (
|
|
1985
|
+
fixed: left.length + badge.text.length + (right?.length ?? 0)
|
|
1885
1986
|
});
|
|
1886
1987
|
return /* @__PURE__ */ jsxs("box", {
|
|
1887
1988
|
style: {
|
|
@@ -1910,9 +2011,9 @@ function StatusBar({ rightHint }) {
|
|
|
1910
2011
|
fg: badge.color,
|
|
1911
2012
|
children: [badge.text, " "]
|
|
1912
2013
|
}),
|
|
1913
|
-
|
|
2014
|
+
right ? /* @__PURE__ */ jsx("text", {
|
|
1914
2015
|
fg: theme.muted,
|
|
1915
|
-
children:
|
|
2016
|
+
children: right
|
|
1916
2017
|
}) : null
|
|
1917
2018
|
]
|
|
1918
2019
|
});
|
|
@@ -2296,6 +2397,7 @@ async function loadFreeFloor(rest) {
|
|
|
2296
2397
|
return NO_FREE_FLOOR;
|
|
2297
2398
|
}
|
|
2298
2399
|
}
|
|
2400
|
+
const SIGNAL_STEPS = 6;
|
|
2299
2401
|
/**
|
|
2300
2402
|
* A 404 from the pin route means the server predates it, not that the pick
|
|
2301
2403
|
* failed: a canary CLI publishes minutes after its release push while the
|
|
@@ -2599,7 +2701,7 @@ function ModelList({ models, floor, currentModel, modelLocked, trainingOptedOut,
|
|
|
2599
2701
|
const hits = fuzzyFilter(query, models, modelKeys);
|
|
2600
2702
|
const filtered = hits.map((h) => h.item);
|
|
2601
2703
|
const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
|
|
2602
|
-
const visibleRows = Math.max(3, height - 8);
|
|
2704
|
+
const visibleRows = Math.max(3, height - 8 - (filtered[clamped]?.signals ? 1 : 0));
|
|
2603
2705
|
const start = windowStart(clamped, filtered.length, visibleRows);
|
|
2604
2706
|
const windowed = hits.slice(start, start + visibleRows);
|
|
2605
2707
|
useKeyboard((key) => {
|
|
@@ -2723,6 +2825,7 @@ function ModelList({ models, floor, currentModel, modelLocked, trainingOptedOut,
|
|
|
2723
2825
|
}, m.id);
|
|
2724
2826
|
})
|
|
2725
2827
|
}),
|
|
2828
|
+
highlighted?.signals ? /* @__PURE__ */ jsx(ModelSignals, { signals: highlighted.signals }) : null,
|
|
2726
2829
|
/* @__PURE__ */ jsx("text", {
|
|
2727
2830
|
fg: saveError ? theme.error : theme.dim,
|
|
2728
2831
|
children: statusLine
|
|
@@ -2730,6 +2833,46 @@ function ModelList({ models, floor, currentModel, modelLocked, trainingOptedOut,
|
|
|
2730
2833
|
]
|
|
2731
2834
|
});
|
|
2732
2835
|
}
|
|
2836
|
+
function ModelSignals({ signals }) {
|
|
2837
|
+
return /* @__PURE__ */ jsx("box", {
|
|
2838
|
+
style: { flexDirection: "column" },
|
|
2839
|
+
children: /* @__PURE__ */ jsxs("box", {
|
|
2840
|
+
style: {
|
|
2841
|
+
flexDirection: "row",
|
|
2842
|
+
height: 1,
|
|
2843
|
+
gap: 2
|
|
2844
|
+
},
|
|
2845
|
+
children: [
|
|
2846
|
+
/* @__PURE__ */ jsx(Signal, {
|
|
2847
|
+
label: "Intelligence",
|
|
2848
|
+
value: signals.intelligence
|
|
2849
|
+
}),
|
|
2850
|
+
/* @__PURE__ */ jsx(Signal, {
|
|
2851
|
+
label: "Speed",
|
|
2852
|
+
value: signals.speed
|
|
2853
|
+
}),
|
|
2854
|
+
/* @__PURE__ */ jsx(Signal, {
|
|
2855
|
+
label: "Cost",
|
|
2856
|
+
value: signals.cost
|
|
2857
|
+
})
|
|
2858
|
+
]
|
|
2859
|
+
})
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
function Signal({ label, value }) {
|
|
2863
|
+
if (value === null) return null;
|
|
2864
|
+
return /* @__PURE__ */ jsxs("text", { children: [
|
|
2865
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2866
|
+
fg: theme.muted,
|
|
2867
|
+
children: [label, " "]
|
|
2868
|
+
}),
|
|
2869
|
+
/* @__PURE__ */ jsx("span", { children: "●".repeat(value) }),
|
|
2870
|
+
/* @__PURE__ */ jsx("span", {
|
|
2871
|
+
fg: theme.dim,
|
|
2872
|
+
children: "○".repeat(SIGNAL_STEPS - value)
|
|
2873
|
+
})
|
|
2874
|
+
] });
|
|
2875
|
+
}
|
|
2733
2876
|
function EffortList({ model, activeEffort, isCurrentModel, saving, saveError, onSelect, onBack }) {
|
|
2734
2877
|
const levels = effortLevels(model);
|
|
2735
2878
|
const [highlight, setHighlight] = useState(() => {
|
|
@@ -2810,6 +2953,10 @@ function parseApiError(body) {
|
|
|
2810
2953
|
return null;
|
|
2811
2954
|
}
|
|
2812
2955
|
|
|
2956
|
+
//#endregion
|
|
2957
|
+
//#region ../agents/src/name.ts
|
|
2958
|
+
const AGENT_NAME_MAX_LENGTH = 120;
|
|
2959
|
+
|
|
2813
2960
|
//#endregion
|
|
2814
2961
|
//#region src/chat/tui/agent-name.ts
|
|
2815
2962
|
function parseAgentName(value) {
|
|
@@ -2818,9 +2965,9 @@ function parseAgentName(value) {
|
|
|
2818
2965
|
ok: false,
|
|
2819
2966
|
message: "Enter a name."
|
|
2820
2967
|
};
|
|
2821
|
-
if (name.length >
|
|
2968
|
+
if (name.length > AGENT_NAME_MAX_LENGTH) return {
|
|
2822
2969
|
ok: false,
|
|
2823
|
-
message:
|
|
2970
|
+
message: `Use ${AGENT_NAME_MAX_LENGTH} characters or fewer.`
|
|
2824
2971
|
};
|
|
2825
2972
|
return {
|
|
2826
2973
|
ok: true,
|
|
@@ -4644,9 +4791,12 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
4644
4791
|
};
|
|
4645
4792
|
const identityRows = height >= 24 ? 3 : 0;
|
|
4646
4793
|
const trayRows = attachmentTrayRows(attachments.length);
|
|
4647
|
-
const
|
|
4648
|
-
|
|
4649
|
-
|
|
4794
|
+
const { composerRows, visibleRows, listGapRows } = pickerRowBudget({
|
|
4795
|
+
height,
|
|
4796
|
+
fixedRows: 6 + brandHeaderRows(height, width) + identityRows + trayRows,
|
|
4797
|
+
composerLines,
|
|
4798
|
+
listRowCount: rows.length
|
|
4799
|
+
});
|
|
4650
4800
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
4651
4801
|
const windowed = rows.slice(start, start + visibleRows);
|
|
4652
4802
|
const openHighlighted = () => {
|
|
@@ -4958,6 +5108,7 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
4958
5108
|
fg: status.fg,
|
|
4959
5109
|
children: status.text
|
|
4960
5110
|
}),
|
|
5111
|
+
/* @__PURE__ */ jsx("box", { style: { height: listGapRows } }),
|
|
4961
5112
|
/* @__PURE__ */ jsx("box", {
|
|
4962
5113
|
style: {
|
|
4963
5114
|
flexDirection: "column",
|
|
@@ -5013,6 +5164,29 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
5013
5164
|
});
|
|
5014
5165
|
}
|
|
5015
5166
|
const MIN_LIST_ROWS = 3;
|
|
5167
|
+
const LIST_GAP_ROWS = 5;
|
|
5168
|
+
/**
|
|
5169
|
+
* The picker's vertical budget in one place, so the composer cap, the list
|
|
5170
|
+
* window, and the gap can't drift apart into competing formulas. Priority,
|
|
5171
|
+
* top of the screen down: the composer grows to fit its content first (capped
|
|
5172
|
+
* so it can't eat the gap — past the cap the textarea scrolls instead of
|
|
5173
|
+
* growing), the gap then always keeps its LIST_GAP_ROWS empty rows, and the
|
|
5174
|
+
* list window gets the remaining spare (floored at MIN_LIST_ROWS for
|
|
5175
|
+
* terminals too short to afford both). The box is content-height, so a short
|
|
5176
|
+
* list's leftover spare becomes extra breathing room on top of the reserved
|
|
5177
|
+
* gap.
|
|
5178
|
+
*/
|
|
5179
|
+
function pickerRowBudget(input) {
|
|
5180
|
+
const composerRows = Math.max(1, Math.min(input.composerLines, input.height - input.fixedRows - 2 - LIST_GAP_ROWS));
|
|
5181
|
+
const spareRows = input.height - input.fixedRows - (composerRows + 2);
|
|
5182
|
+
const visibleRows = Math.max(MIN_LIST_ROWS, spareRows - LIST_GAP_ROWS);
|
|
5183
|
+
const paintedRows = Math.min(Math.max(input.listRowCount, 1), visibleRows);
|
|
5184
|
+
return {
|
|
5185
|
+
composerRows,
|
|
5186
|
+
visibleRows,
|
|
5187
|
+
listGapRows: Math.max(0, Math.min(LIST_GAP_ROWS, spareRows - paintedRows))
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5016
5190
|
const composerKeyBindings$1 = [
|
|
5017
5191
|
{
|
|
5018
5192
|
name: "return",
|
|
@@ -8483,17 +8657,19 @@ function formatChatFooterStatus({ runKind, ctrlCArmed, portalOpen, hasPendingSte
|
|
|
8483
8657
|
* claim the footer can't back.
|
|
8484
8658
|
*
|
|
8485
8659
|
* A thread that serves its own model reads differently from one following the
|
|
8486
|
-
* agent (ANY-8702): a differing `agentModel` gets a `thread` marker
|
|
8487
|
-
*
|
|
8488
|
-
*
|
|
8489
|
-
*
|
|
8490
|
-
*
|
|
8491
|
-
*
|
|
8660
|
+
* agent (ANY-8702): a differing `agentModel` gets a `thread` marker — the "set
|
|
8661
|
+
* and not the same as the default" case James asked the footer to surface.
|
|
8662
|
+
* Provenance is derived from the pair, not carried as a flag (James, #24018
|
|
8663
|
+
* review). The marker is the whole message: naming the agent's default beside
|
|
8664
|
+
* it read as a second model the footer might switch to (Marcus, 2026-10-03),
|
|
8665
|
+
* so the default is not spelled out. When the thread follows the default —
|
|
8666
|
+
* or the server predates pins and sends no `agentModel` — the model renders
|
|
8667
|
+
* bare exactly as before.
|
|
8492
8668
|
*/
|
|
8493
8669
|
function formatModelHint({ model, effort, agentModel }) {
|
|
8494
8670
|
const effortPart = effort ? ` · ${effort}` : "";
|
|
8495
8671
|
if (model === null || agentModel === null || agentModel === model) return `ctrl+p · ${model ?? "default"}${effortPart}`;
|
|
8496
|
-
return `ctrl+p · ${model} (thread)
|
|
8672
|
+
return `ctrl+p · ${model} (thread)${effortPart}`;
|
|
8497
8673
|
}
|
|
8498
8674
|
|
|
8499
8675
|
//#endregion
|
|
@@ -8558,14 +8734,12 @@ function collectSandboxStats(cfg) {
|
|
|
8558
8734
|
* conversation.
|
|
8559
8735
|
*/
|
|
8560
8736
|
/**
|
|
8561
|
-
* Wire-off for the whole panel (binding, help entry, render).
|
|
8562
|
-
*
|
|
8563
|
-
*
|
|
8564
|
-
*
|
|
8565
|
-
* on this directly; the render is unreachable because sidePanelOpen can only
|
|
8566
|
-
* flip through the gated binding.
|
|
8737
|
+
* Wire-off for the whole panel (binding, help entry, render). Costs and the
|
|
8738
|
+
* sandbox gauges are both in, so the panel has enough content to earn the
|
|
8739
|
+
* screen column: the gate stays as the one-line home for a future panel-wide
|
|
8740
|
+
* disable.
|
|
8567
8741
|
*/
|
|
8568
|
-
const SIDE_PANEL_ENABLED =
|
|
8742
|
+
const SIDE_PANEL_ENABLED = true;
|
|
8569
8743
|
/** Fixed panel width, matching OpenCode's sidebar (42 cols minus padding). */
|
|
8570
8744
|
const SIDE_PANEL_WIDTH = 40;
|
|
8571
8745
|
/**
|
|
@@ -8623,27 +8797,59 @@ function SidePanel({ open, width, height, agentName, title, collectSandboxStats,
|
|
|
8623
8797
|
gap: 2,
|
|
8624
8798
|
flexShrink: 0
|
|
8625
8799
|
},
|
|
8626
|
-
children: [
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8800
|
+
children: [
|
|
8801
|
+
/* @__PURE__ */ jsxs("box", {
|
|
8802
|
+
style: {
|
|
8803
|
+
flexDirection: "column",
|
|
8804
|
+
flexShrink: 0
|
|
8805
|
+
},
|
|
8806
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
8807
|
+
fg: theme.fg,
|
|
8808
|
+
children: /* @__PURE__ */ jsx("b", { children: title || agentName })
|
|
8809
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
8810
|
+
fg: theme.dim,
|
|
8811
|
+
children: agentName
|
|
8812
|
+
})]
|
|
8813
|
+
}),
|
|
8814
|
+
/* @__PURE__ */ jsx(CostsSection, {}),
|
|
8815
|
+
/* @__PURE__ */ jsx(ComputerSection, {
|
|
8816
|
+
visible: placement === "side",
|
|
8817
|
+
collectSandboxStats,
|
|
8818
|
+
pollMs
|
|
8819
|
+
})
|
|
8820
|
+
]
|
|
8643
8821
|
})
|
|
8644
8822
|
}, "side-panel");
|
|
8645
8823
|
}
|
|
8646
8824
|
/**
|
|
8825
|
+
* The panel's Costs section: this conversation's spend so far, fed by the
|
|
8826
|
+
* same store read the status-bar badge shows — the chat screen's
|
|
8827
|
+
* useConversationSpend hook keeps it current (live poll while a run is
|
|
8828
|
+
* running, settle read after, slow poll while idle), so the section adds no
|
|
8829
|
+
* reads of its own. `usage: null` is the org-targeting answer: tracking
|
|
8830
|
+
* simply isn't on here.
|
|
8831
|
+
*/
|
|
8832
|
+
function CostsSection() {
|
|
8833
|
+
const chatSpend = useStore((s) => s.chatSpend);
|
|
8834
|
+
const usage = chatSpend?.usage ?? null;
|
|
8835
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
8836
|
+
style: {
|
|
8837
|
+
flexDirection: "column",
|
|
8838
|
+
flexShrink: 0
|
|
8839
|
+
},
|
|
8840
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
8841
|
+
fg: theme.muted,
|
|
8842
|
+
children: /* @__PURE__ */ jsx("b", { children: "costs" })
|
|
8843
|
+
}), usage === null ? /* @__PURE__ */ jsx("text", {
|
|
8844
|
+
fg: theme.dim,
|
|
8845
|
+
children: chatSpend === null ? "reading spend…" : "spend tracking is off for this workspace"
|
|
8846
|
+
}) : formatSpendSection(usage).map((line, i) => /* @__PURE__ */ jsx("text", {
|
|
8847
|
+
fg: i === 0 ? theme.fg : theme.muted,
|
|
8848
|
+
children: line
|
|
8849
|
+
}, i))]
|
|
8850
|
+
});
|
|
8851
|
+
}
|
|
8852
|
+
/**
|
|
8647
8853
|
* The panel's Computer section: the sandbox cpu / memory / disk gauges, fed by
|
|
8648
8854
|
* the same api-backed collector as the /computer card (no exec, no wake; an
|
|
8649
8855
|
* idle agent gets an idle line at once). Polls on the card's 30s cadence while
|
|
@@ -9813,6 +10019,13 @@ const slashCommands = [
|
|
|
9813
10019
|
description: "show the agent sandbox's cpu/memory/disk (restart: reboot it)",
|
|
9814
10020
|
action: { kind: "computer" }
|
|
9815
10021
|
},
|
|
10022
|
+
{
|
|
10023
|
+
name: "cost",
|
|
10024
|
+
aliases: [],
|
|
10025
|
+
argsHint: null,
|
|
10026
|
+
description: "what this conversation has cost so far: inference by model, compute",
|
|
10027
|
+
action: { kind: "cost" }
|
|
10028
|
+
},
|
|
9816
10029
|
{
|
|
9817
10030
|
name: "plan",
|
|
9818
10031
|
aliases: ["todos", "todo"],
|
|
@@ -9970,6 +10183,7 @@ const TERMINAL_RESTORE = [
|
|
|
9970
10183
|
"\x1B[?1015l",
|
|
9971
10184
|
"\x1B[<u",
|
|
9972
10185
|
"\x1B[?1049l",
|
|
10186
|
+
"\x1B[?7h",
|
|
9973
10187
|
"\x1B[0m"
|
|
9974
10188
|
].join("");
|
|
9975
10189
|
/**
|
|
@@ -12837,7 +13051,7 @@ function unfoldSettled(current) {
|
|
|
12837
13051
|
* shortcut), which would otherwise shadow it inside the component body.
|
|
12838
13052
|
*/
|
|
12839
13053
|
async function openUrlInBrowser(url) {
|
|
12840
|
-
await open(url).catch(() => void 0);
|
|
13054
|
+
await open$1(url).catch(() => void 0);
|
|
12841
13055
|
}
|
|
12842
13056
|
function isActionableCard(m) {
|
|
12843
13057
|
return m.kind === "card" && m.status !== "done" && m.status !== "busy" && m.card.button !== null;
|
|
@@ -15179,6 +15393,7 @@ function routeInput(raw) {
|
|
|
15179
15393
|
case "theme-picker":
|
|
15180
15394
|
case "pr-picker":
|
|
15181
15395
|
case "status":
|
|
15396
|
+
case "cost":
|
|
15182
15397
|
case "share":
|
|
15183
15398
|
case "browser":
|
|
15184
15399
|
case "connect":
|
|
@@ -15227,7 +15442,7 @@ function formatShellContext(command, output, exitCode) {
|
|
|
15227
15442
|
* bar and footer can't fit, one aligned `label value` row each. Pure so the
|
|
15228
15443
|
* layout is testable without a renderer.
|
|
15229
15444
|
*/
|
|
15230
|
-
function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, effort, portal }) {
|
|
15445
|
+
function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, effort, portal, spendCents }) {
|
|
15231
15446
|
const rows = [
|
|
15232
15447
|
["app", appUrl ?? "unknown"],
|
|
15233
15448
|
["workspace", workspaceName ?? "unknown"],
|
|
@@ -15237,6 +15452,7 @@ function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, mo
|
|
|
15237
15452
|
];
|
|
15238
15453
|
if (effort) rows.push(["effort", effort]);
|
|
15239
15454
|
rows.push(["portal", formatPortal(portal)]);
|
|
15455
|
+
if (spendCents !== null) rows.push(["spend", `${formatSpendBadge(spendCents)} so far (/cost for the breakdown)`]);
|
|
15240
15456
|
const width = Math.max(...rows.map(([label]) => label.length));
|
|
15241
15457
|
return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
|
|
15242
15458
|
}
|
|
@@ -15258,7 +15474,7 @@ function formatPortal(portal) {
|
|
|
15258
15474
|
* The slash-command dispatcher for the chat screen, extracted from ChatScreen
|
|
15259
15475
|
* (ANY-8935 slice 6). Verbatim code motion; comments carried.
|
|
15260
15476
|
*/
|
|
15261
|
-
function useExecuteLine({ agent, rest, conversationId, appUrl, goTo, history, setInput, setComposerRows, setItems, composerRef, runLocalShell, runSandboxExec, runSandboxPty, archiveConversation, renameConversation, forkAndContinue, compactConversation, openInBrowser, copyLastMessage, exportTranscript, sendContent, leaveForScreen, openConversationRef, toggleShare, openPortalAccess, quit, forkingRef, shellSessionRef, prLinks, subagentMinimized, planMinimized, anchoredSubagentBatchRef, clearAttachments, model, effort, planHidden, setPlanHidden, setPlanExpanded, setPlanMinimizedBy, portal, promptForCards, runComputerStats, runRef, setHelpOpen, setModelPickerView, setPrPickerOpen, setRestartConfirm, setSubagentMinimizedBy, setSubagentSelection, setThemePickerOpen, todos, workspaceName }) {
|
|
15477
|
+
function useExecuteLine({ agent, rest, conversationId, appUrl, goTo, history, setInput, setComposerRows, setItems, composerRef, runLocalShell, runSandboxExec, runSandboxPty, archiveConversation, renameConversation, forkAndContinue, compactConversation, openInBrowser, copyLastMessage, exportTranscript, sendContent, leaveForScreen, openConversationRef, toggleShare, openPortalAccess, quit, forkingRef, shellSessionRef, prLinks, subagentMinimized, planMinimized, anchoredSubagentBatchRef, clearAttachments, model, effort, planHidden, setPlanHidden, setPlanExpanded, setPlanMinimizedBy, portal, promptForCards, runComputerStats, showCost, runRef, setHelpOpen, setModelPickerView, setPrPickerOpen, setRestartConfirm, setSubagentMinimizedBy, setSubagentSelection, setThemePickerOpen, todos, workspaceName }) {
|
|
15262
15478
|
return { executeLine: useCallback((content, staged) => {
|
|
15263
15479
|
if (!content && staged.length === 0) return;
|
|
15264
15480
|
if (forkingRef.current) {
|
|
@@ -15365,18 +15581,23 @@ function useExecuteLine({ agent, rest, conversationId, appUrl, goTo, history, se
|
|
|
15365
15581
|
workspaceName,
|
|
15366
15582
|
agentName: agent.name,
|
|
15367
15583
|
conversationId,
|
|
15584
|
+
spendCents: useStore.getState().chatSpend?.cents ?? null,
|
|
15368
15585
|
model,
|
|
15369
15586
|
effort,
|
|
15370
15587
|
portal: {
|
|
15371
15588
|
status: portal.status,
|
|
15372
15589
|
grantedToAgent: portal.grantedAgentIds.includes(agent.id)
|
|
15373
15590
|
}
|
|
15374
|
-
})
|
|
15591
|
+
}),
|
|
15592
|
+
spendCents: useStore.getState().chatSpend?.cents ?? null
|
|
15375
15593
|
}]);
|
|
15376
15594
|
break;
|
|
15377
15595
|
case "computer":
|
|
15378
15596
|
runComputerStats();
|
|
15379
15597
|
break;
|
|
15598
|
+
case "cost":
|
|
15599
|
+
showCost();
|
|
15600
|
+
break;
|
|
15380
15601
|
case "computer-restart":
|
|
15381
15602
|
setRestartConfirm(true);
|
|
15382
15603
|
break;
|
|
@@ -15478,6 +15699,7 @@ function useExecuteLine({ agent, rest, conversationId, appUrl, goTo, history, se
|
|
|
15478
15699
|
runLocalShell,
|
|
15479
15700
|
runSandboxExec,
|
|
15480
15701
|
runComputerStats,
|
|
15702
|
+
showCost,
|
|
15481
15703
|
runSandboxPty,
|
|
15482
15704
|
archiveConversation,
|
|
15483
15705
|
renameConversation,
|
|
@@ -15661,7 +15883,7 @@ function useConversationActions({ rest, appUrl, conversationId, agent, goTo, dri
|
|
|
15661
15883
|
return;
|
|
15662
15884
|
}
|
|
15663
15885
|
const url = `${resolveWebUrl(appUrl)}/c/${encodeURIComponent(conversationId)}`;
|
|
15664
|
-
open(url).then(() => useStore.getState().showToast({
|
|
15886
|
+
open$1(url).then(() => useStore.getState().showToast({
|
|
15665
15887
|
message: `opened in browser: ${url}`,
|
|
15666
15888
|
variant: "success"
|
|
15667
15889
|
})).catch((err) => setItems((prev) => [...prev, {
|
|
@@ -15675,7 +15897,7 @@ function useConversationActions({ rest, appUrl, conversationId, agent, goTo, dri
|
|
|
15675
15897
|
const target = itemsRef.current.find((m) => m.kind === "agent-file" && m.id === id);
|
|
15676
15898
|
if (!target || target.status === "saving") return;
|
|
15677
15899
|
if (target.status === "saved" && target.savedPath) {
|
|
15678
|
-
open(target.savedPath).catch(() => void 0);
|
|
15900
|
+
open$1(target.savedPath).catch(() => void 0);
|
|
15679
15901
|
useStore.getState().showToast({
|
|
15680
15902
|
message: `opened ${basename(target.savedPath)}`,
|
|
15681
15903
|
variant: "success"
|
|
@@ -15706,7 +15928,7 @@ function useConversationActions({ rest, appUrl, conversationId, agent, goTo, dri
|
|
|
15706
15928
|
message: `saved to ${path}`,
|
|
15707
15929
|
variant: "success"
|
|
15708
15930
|
});
|
|
15709
|
-
open(path).catch(() => void 0);
|
|
15931
|
+
open$1(path).catch(() => void 0);
|
|
15710
15932
|
} catch (err) {
|
|
15711
15933
|
patch({
|
|
15712
15934
|
status: "error",
|
|
@@ -15999,6 +16221,7 @@ function useConversationActions({ rest, appUrl, conversationId, agent, goTo, dri
|
|
|
15999
16221
|
useStore.getState().showToast({ message: "opening the portal to this machine…" });
|
|
16000
16222
|
break;
|
|
16001
16223
|
case "connecting":
|
|
16224
|
+
case "stand-down":
|
|
16002
16225
|
useStore.getState().showToast({ message: "the portal is still connecting…" });
|
|
16003
16226
|
break;
|
|
16004
16227
|
case "connected":
|
|
@@ -16222,6 +16445,72 @@ function useLocalCommands({ setItems, shellSessionRef, bindPortalCwd, sendConten
|
|
|
16222
16445
|
};
|
|
16223
16446
|
}
|
|
16224
16447
|
|
|
16448
|
+
//#endregion
|
|
16449
|
+
//#region src/chat/tui/chat/use-conversation-spend.ts
|
|
16450
|
+
const LIVE_POLL_MS = 1e4;
|
|
16451
|
+
const SETTLE_AFTER_RUN_MS = 8e3;
|
|
16452
|
+
const IDLE_POLL_MS = 6e4;
|
|
16453
|
+
/**
|
|
16454
|
+
* Keeps the store's `chatSpend` — what the status bar shows and the side
|
|
16455
|
+
* panel's costs section reads — current for the open conversation: one read
|
|
16456
|
+
* when it opens, a slow poll while a run is live, a read at run completion
|
|
16457
|
+
* plus one settle read for the trailing telemetry, and a slow background
|
|
16458
|
+
* poll so a panel opened mid-idle converges. The store is the single copy
|
|
16459
|
+
* of the figure; `/status` reads it from there and `/cost` calls
|
|
16460
|
+
* {@link refresh} for a fresh breakdown.
|
|
16461
|
+
*
|
|
16462
|
+
* Reads are best effort — a failed one keeps the last figure rather than
|
|
16463
|
+
* surfacing an error in the transcript; `/cost` is where a user asks
|
|
16464
|
+
* explicitly and gets told.
|
|
16465
|
+
*/
|
|
16466
|
+
function useConversationSpend({ rest, conversationId, runActive }) {
|
|
16467
|
+
const setChatSpend = useStore((s) => s.setChatSpend);
|
|
16468
|
+
const conversationIdRef = useRef(conversationId);
|
|
16469
|
+
conversationIdRef.current = conversationId;
|
|
16470
|
+
const refresh = useCallback(async () => {
|
|
16471
|
+
if (!rest || !conversationId) return { kind: "unavailable" };
|
|
16472
|
+
let usage;
|
|
16473
|
+
try {
|
|
16474
|
+
usage = await rest.getConversationUsage({ conversationId });
|
|
16475
|
+
} catch (_error) {
|
|
16476
|
+
return { kind: "unavailable" };
|
|
16477
|
+
}
|
|
16478
|
+
if (conversationIdRef.current !== conversationId) return { kind: "unavailable" };
|
|
16479
|
+
if (!usage) return { kind: "off" };
|
|
16480
|
+
setChatSpend(usage.totalCents, usage);
|
|
16481
|
+
return {
|
|
16482
|
+
kind: "usage",
|
|
16483
|
+
usage
|
|
16484
|
+
};
|
|
16485
|
+
}, [
|
|
16486
|
+
rest,
|
|
16487
|
+
conversationId,
|
|
16488
|
+
setChatSpend
|
|
16489
|
+
]);
|
|
16490
|
+
useEffect(() => {
|
|
16491
|
+
setChatSpend(null, null);
|
|
16492
|
+
refresh();
|
|
16493
|
+
}, [refresh, setChatSpend]);
|
|
16494
|
+
const wasActiveRef = useRef(runActive);
|
|
16495
|
+
useEffect(() => {
|
|
16496
|
+
if (runActive) {
|
|
16497
|
+
const timer = setInterval(() => void refresh(), LIVE_POLL_MS);
|
|
16498
|
+
wasActiveRef.current = true;
|
|
16499
|
+
return () => clearInterval(timer);
|
|
16500
|
+
}
|
|
16501
|
+
if (!wasActiveRef.current) return;
|
|
16502
|
+
wasActiveRef.current = false;
|
|
16503
|
+
refresh();
|
|
16504
|
+
const settle = setTimeout(() => void refresh(), SETTLE_AFTER_RUN_MS);
|
|
16505
|
+
return () => clearTimeout(settle);
|
|
16506
|
+
}, [runActive, refresh]);
|
|
16507
|
+
useEffect(() => {
|
|
16508
|
+
const timer = setInterval(() => void refresh(), IDLE_POLL_MS);
|
|
16509
|
+
return () => clearInterval(timer);
|
|
16510
|
+
}, [refresh]);
|
|
16511
|
+
return { refresh };
|
|
16512
|
+
}
|
|
16513
|
+
|
|
16225
16514
|
//#endregion
|
|
16226
16515
|
//#region src/chat/tui/screens/chat/transcript-row.tsx
|
|
16227
16516
|
/**
|
|
@@ -16379,6 +16668,11 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
16379
16668
|
const [historyLoaded, setHistoryLoaded] = useState(initialConversationId === null);
|
|
16380
16669
|
const [input, setInput] = useState(() => useStore.getState().takeEarlyInput());
|
|
16381
16670
|
const [run, setRun] = useState({ kind: "idle" });
|
|
16671
|
+
const spend = useConversationSpend({
|
|
16672
|
+
rest,
|
|
16673
|
+
conversationId,
|
|
16674
|
+
runActive: run.kind !== "idle"
|
|
16675
|
+
});
|
|
16382
16676
|
const [model, setModel] = useState(agent.model ?? null);
|
|
16383
16677
|
const [effort, setEffort] = useState(agent.thinkingLevel ?? null);
|
|
16384
16678
|
const [modelLocked, setModelLocked] = useState(agent.modelLocked ?? false);
|
|
@@ -16871,7 +17165,20 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
16871
17165
|
rest,
|
|
16872
17166
|
renderer
|
|
16873
17167
|
});
|
|
17168
|
+
const refreshSpend = spend.refresh;
|
|
17169
|
+
const showCost = useCallback(async () => {
|
|
17170
|
+
const read = conversationId ? await refreshSpend() : null;
|
|
17171
|
+
setItems((prev) => [...prev, {
|
|
17172
|
+
kind: "info",
|
|
17173
|
+
id: crypto.randomUUID(),
|
|
17174
|
+
title: "cost",
|
|
17175
|
+
text: formatCostRead(read)
|
|
17176
|
+
}]);
|
|
17177
|
+
}, [conversationId, refreshSpend]);
|
|
16874
17178
|
const { executeLine } = useExecuteLine({
|
|
17179
|
+
showCost: () => {
|
|
17180
|
+
showCost();
|
|
17181
|
+
},
|
|
16875
17182
|
agent,
|
|
16876
17183
|
rest,
|
|
16877
17184
|
conversationId,
|
|
@@ -18459,6 +18766,22 @@ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, comm
|
|
|
18459
18766
|
//#endregion
|
|
18460
18767
|
//#region src/chat/boot.tsx
|
|
18461
18768
|
/**
|
|
18769
|
+
* Disable host autowrap and create the renderer that owns restoring it.
|
|
18770
|
+
* If `createCliRenderer` throws (bad TERM, boot-time init failure), its
|
|
18771
|
+
* `onDestroy` never runs, so the disable is undone here before the error
|
|
18772
|
+
* propagates — otherwise the terminal is left without autowrap for
|
|
18773
|
+
* whatever prints next (see tui/autowrap.ts).
|
|
18774
|
+
*/
|
|
18775
|
+
async function bootRenderer(options) {
|
|
18776
|
+
disableTerminalAutowrap();
|
|
18777
|
+
try {
|
|
18778
|
+
return await createCliRenderer(options);
|
|
18779
|
+
} catch (err) {
|
|
18780
|
+
restoreTerminalAutowrap();
|
|
18781
|
+
throw err;
|
|
18782
|
+
}
|
|
18783
|
+
}
|
|
18784
|
+
/**
|
|
18462
18785
|
* Boots the OpenTUI chat interface. Called by the `chat` command only
|
|
18463
18786
|
* after it has confirmed a Bun runtime and resolved a session, so this
|
|
18464
18787
|
* module — and the heavy OpenTUI dependency graph it pulls in — is
|
|
@@ -18468,9 +18791,10 @@ async function runChat({ appUrl, sessionToken, authKind = "session", workspaceId
|
|
|
18468
18791
|
await registerGrammars();
|
|
18469
18792
|
const watchdog = startMemoryWatchdog();
|
|
18470
18793
|
installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
|
|
18471
|
-
const renderer = await
|
|
18794
|
+
const renderer = await bootRenderer({
|
|
18472
18795
|
exitOnCtrlC: false,
|
|
18473
|
-
...profilingRendererOptions()
|
|
18796
|
+
...profilingRendererOptions(),
|
|
18797
|
+
onDestroy: restoreTerminalAutowrap
|
|
18474
18798
|
});
|
|
18475
18799
|
startRendererStatsCapture(renderer);
|
|
18476
18800
|
startStateCapture(useStore);
|
|
@@ -18514,7 +18838,10 @@ async function runChat({ appUrl, sessionToken, authKind = "session", workspaceId
|
|
|
18514
18838
|
}
|
|
18515
18839
|
/** Open the same workspace picker used by the initial agent screen. */
|
|
18516
18840
|
async function runWorkspacePicker({ appUrl, sessionToken }) {
|
|
18517
|
-
const renderer = await
|
|
18841
|
+
const renderer = await bootRenderer({
|
|
18842
|
+
exitOnCtrlC: false,
|
|
18843
|
+
onDestroy: restoreTerminalAutowrap
|
|
18844
|
+
});
|
|
18518
18845
|
await settleBootThemeMode({
|
|
18519
18846
|
source: renderer,
|
|
18520
18847
|
applyThemeMode: (mode) => useStore.getState().setThemeMode(mode)
|