zidane 5.1.11 → 5.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-D0pXl4CO.d.ts → agent-skiQGYs2.d.ts} +8 -2
- package/dist/agent-skiQGYs2.d.ts.map +1 -0
- package/dist/chat.d.ts +43 -6
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{index-n4STKh9s.d.ts → index-CjPh6CRE.d.ts} +2 -2
- package/dist/{index-n4STKh9s.d.ts.map → index-CjPh6CRE.d.ts.map} +1 -1
- package/dist/{index-C9A_Ah4R.d.ts → index-YM7SipFz.d.ts} +2 -2
- package/dist/{index-C9A_Ah4R.d.ts.map → index-YM7SipFz.d.ts.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -6
- package/dist/{login-CQNaKTLJ.js → login-Cc6Q-Fpu.js} +2 -2
- package/dist/{login-CQNaKTLJ.js.map → login-Cc6Q-Fpu.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/{messages-DiAiNhxA.js → messages-CIkO_aCH.js} +40 -4
- package/dist/messages-CIkO_aCH.js.map +1 -0
- package/dist/{presets-CYNTGGXg.js → presets-Ce79MK4J.js} +2 -2
- package/dist/{presets-CYNTGGXg.js.map → presets-Ce79MK4J.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-6bqfXUd1.js → providers-CvriFHFU.js} +27 -8
- package/dist/providers-CvriFHFU.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-pS4Vt4dl.js → session-DtLD1Sl1.js} +2 -1
- package/dist/{session-pS4Vt4dl.js.map → session-DtLD1Sl1.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/{tool-formatters-BkbbrFyr.d.ts → tool-formatters-0aOMYbH-.d.ts} +71 -5
- package/dist/tool-formatters-0aOMYbH-.d.ts.map +1 -0
- package/dist/{tools-BoHVy2UM.js → tools-BG2wMa3X.js} +2 -2
- package/dist/{tools-BoHVy2UM.js.map → tools-BG2wMa3X.js.map} +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/tui.d.ts +44 -11
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +945 -276
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-BMGp7jXI.js → turn-operations-CDmQ2h-T.js} +490 -55
- package/dist/turn-operations-CDmQ2h-T.js.map +1 -0
- package/dist/types-Bx_F8jet.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/agent-D0pXl4CO.d.ts.map +0 -1
- package/dist/messages-DiAiNhxA.js.map +0 -1
- package/dist/providers-6bqfXUd1.js.map +0 -1
- package/dist/tool-formatters-BkbbrFyr.d.ts.map +0 -1
- package/dist/turn-operations-BMGp7jXI.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { S as resolvePersistDir, b as cleanupPersistedSession, d as createAgent } from "./tools-
|
|
1
|
+
import { S as resolvePersistDir, b as cleanupPersistedSession, d as createAgent } from "./tools-BG2wMa3X.js";
|
|
2
2
|
import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-CUt-N8zn.js";
|
|
3
|
-
import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-
|
|
3
|
+
import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-Cc6Q-Fpu.js";
|
|
4
4
|
import { n as formatTokenUsage } from "./stats-DgOvY7wd.js";
|
|
5
|
-
import { n as loadSession, t as createSession } from "./session-
|
|
5
|
+
import { n as loadSession, t as createSession } from "./session-DtLD1Sl1.js";
|
|
6
6
|
import { createTuiStore } from "./session/sqlite.js";
|
|
7
|
-
import { $ as getMcpAuthStatus, $t as
|
|
7
|
+
import { $ as getMcpAuthStatus, $t as toolCallPreview, A as isOnSafelist, B as filterModelCatalog, Bn as setProviderCredential, Bt as eventsFromTurns, C as writeSessionExport, Cn as uniqueSkillNamesFromReferences, Ct as SettingsProvider, E as useSafeModeQueue, Ft as ConfigProvider, Gt as listSessionMeta, H as buildMcpServers, Ht as isTurnHighlighted, I as splitPromptSegments, It as useConfig, Jn as getContextWindow, L as runOAuthLogin, Lt as resolveConfig, Mn as tryOpenBrowser, Nn as shouldAutoCompact, O as addToSafelist, Ot as resolveChipColor, P as suggestSafelistEntry, Pn as detectAuth, Q as useMcpAuthState, R as supportsOAuth, Sn as createSkillsCompletionProvider, St as SETTINGS_TOGGLES, T as useSafeModeActions, Tn as createFilesCompletionProvider, Tt as useSettings, Ut as isVisible, V as indexOfEntry, Vt as isEditErrorResult, W as discoverProjectMcps, Wt as lastContextSizeFromTurns, X as McpAuthProvider, Xn as modelSupportsReasoning, Xt as stripSpawnTokensLine, Yt as selectableTurnIds, Z as useMcpAuthDispatch, Zt as sumRunCosts, _ as useStreamBuffer, _t as shortId, a as TOOL_DISPLAY, at as createInteractionTools, b as discoverProjectSkills, br as buildPlanSystem, bt as DEFAULT_SETTINGS, c as ThemeProvider, ct as pendingInteractionsFromTurns, d as useSurfaces, dt as useInteractionsQueue, en as toolResultText, er as piIdOf, g as turnContextSize, gn as matchesBinding, gt as fmtTokens, h as finalizeStreamingMarkdownForOwner, ht as compactPath, i as turnAsText, it as buildResumedToolResultsTurn, jn as useCompletion, k as getSafelist, kt as resolveTheme, l as useColors, m as finalizeStreamingMarkdown, mn as ensureKeybindingsFile, mt as ageString, n as deleteTurnSafely, nn as buildContextualDiff, nt as InteractionsProvider, o as displayNameFor, on as extractEditPayload, or as accentColor, p as useTheme, pt as generateSessionTitle, q as createFileMcpCredentialStore, qt as marginTopFor, r as truncateTurnsAt, rn as buildUnifiedDiff, s as formatToolCall, sn as filetypeFromPath, st as makeRequestInteraction, tn as turnSelectionOwnership, u as useSelectStyle, un as findGitRoot, ut as useInteractionsActions, v as buildSkillsConfig, vt as listProjectFiles, w as SafeModeProvider, wt as clampFps, xt as SETTINGS_CHOICES, y as defaultSkillScanPaths, yr as buildBuildSystem, yt as useEnabledToggleSet, z as buildModelCatalog, zt as deriveSessionTitle } from "./turn-operations-CDmQ2h-T.js";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { Buffer } from "node:buffer";
|
|
10
|
+
import * as fs from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
-
import { RGBA, SyntaxStyle, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, stripAnsiSequences } from "@opentui/core";
|
|
13
|
+
import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, stripAnsiSequences } from "@opentui/core";
|
|
13
14
|
import { createRoot, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from "@opentui/react";
|
|
14
15
|
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
15
16
|
//#region src/tui/modal.tsx
|
|
@@ -317,6 +318,160 @@ function writeToClipboard(text) {
|
|
|
317
318
|
return osc || helper;
|
|
318
319
|
}
|
|
319
320
|
//#endregion
|
|
321
|
+
//#region src/tui/color-gradient.ts
|
|
322
|
+
/** Parse `#rrggbb` (case-insensitive) into `[r, g, b]` 0–255 integers. */
|
|
323
|
+
function parseHex(hex) {
|
|
324
|
+
const h = hex.replace("#", "");
|
|
325
|
+
return [
|
|
326
|
+
Number.parseInt(h.slice(0, 2), 16),
|
|
327
|
+
Number.parseInt(h.slice(2, 4), 16),
|
|
328
|
+
Number.parseInt(h.slice(4, 6), 16)
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
/** Convert sRGB 0–255 → HSL 0–1. */
|
|
332
|
+
function rgbToHsl(r, g, b) {
|
|
333
|
+
r /= 255;
|
|
334
|
+
g /= 255;
|
|
335
|
+
b /= 255;
|
|
336
|
+
const max = Math.max(r, g, b);
|
|
337
|
+
const min = Math.min(r, g, b);
|
|
338
|
+
const l = (max + min) / 2;
|
|
339
|
+
if (max === min) return [
|
|
340
|
+
0,
|
|
341
|
+
0,
|
|
342
|
+
l
|
|
343
|
+
];
|
|
344
|
+
const d = max - min;
|
|
345
|
+
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
346
|
+
let h;
|
|
347
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
348
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
349
|
+
else h = (r - g) / d + 4;
|
|
350
|
+
return [
|
|
351
|
+
h / 6,
|
|
352
|
+
s,
|
|
353
|
+
l
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
/** Convert HSL 0–1 → sRGB 0–255. Standard piecewise formula. */
|
|
357
|
+
function hslToRgb(h, s, l) {
|
|
358
|
+
if (s === 0) return [
|
|
359
|
+
l * 255,
|
|
360
|
+
l * 255,
|
|
361
|
+
l * 255
|
|
362
|
+
];
|
|
363
|
+
const hue2rgb = (p, q, t) => {
|
|
364
|
+
if (t < 0) t += 1;
|
|
365
|
+
if (t > 1) t -= 1;
|
|
366
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
367
|
+
if (t < 1 / 2) return q;
|
|
368
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
369
|
+
return p;
|
|
370
|
+
};
|
|
371
|
+
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
372
|
+
const p = 2 * l - q;
|
|
373
|
+
return [
|
|
374
|
+
hue2rgb(p, q, h + 1 / 3) * 255,
|
|
375
|
+
hue2rgb(p, q, h) * 255,
|
|
376
|
+
hue2rgb(p, q, h - 1 / 3) * 255
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
function toHex(rgb) {
|
|
380
|
+
const pad = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
381
|
+
return `#${pad(rgb[0])}${pad(rgb[1])}${pad(rgb[2])}`;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Blend two hex colors in HSL space with shortest-path hue interpolation.
|
|
385
|
+
* `t` ∈ [0, 1]; `t=0` returns `from`, `t=1` returns `to`.
|
|
386
|
+
*/
|
|
387
|
+
function blendHsl(from, to, t) {
|
|
388
|
+
const [r1, g1, b1] = parseHex(from);
|
|
389
|
+
const [r2, g2, b2] = parseHex(to);
|
|
390
|
+
const [h1, s1, l1] = rgbToHsl(r1, g1, b1);
|
|
391
|
+
const [h2, s2, l2] = rgbToHsl(r2, g2, b2);
|
|
392
|
+
let dh = h2 - h1;
|
|
393
|
+
if (dh > .5) dh -= 1;
|
|
394
|
+
else if (dh < -.5) dh += 1;
|
|
395
|
+
return toHex(hslToRgb((h1 + dh * t + 1) % 1, s1 + (s2 - s1) * t, l1 + (l2 - l1) * t));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Static gradient ramp of length `n` going from `from` (index 0) to
|
|
399
|
+
* `to` (index n-1) in HSL space. For the cycling A→B→A→B ramp the
|
|
400
|
+
* throbber uses, see `buildCycleRamp` in `crush-throbber.tsx`.
|
|
401
|
+
*/
|
|
402
|
+
function buildLinearRamp(from, to, n) {
|
|
403
|
+
if (n <= 0) return [];
|
|
404
|
+
if (n === 1) return [blendHsl(from, to, .5)];
|
|
405
|
+
const ramp = [];
|
|
406
|
+
for (let i = 0; i < n; i++) ramp.push(blendHsl(from, to, i / (n - 1)));
|
|
407
|
+
return ramp;
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/tui/crush-throbber.tsx
|
|
411
|
+
/** @jsxImportSource @opentui/react */
|
|
412
|
+
const CRUSH_RUNES = "0123456789abcdefABCDEF~!@#$£€%^&*()+=_";
|
|
413
|
+
const TICK_MS = 50;
|
|
414
|
+
const ELLIPSIS_FRAMES = [
|
|
415
|
+
".",
|
|
416
|
+
"..",
|
|
417
|
+
"...",
|
|
418
|
+
""
|
|
419
|
+
];
|
|
420
|
+
const ELLIPSIS_TICKS = 8;
|
|
421
|
+
const BIRTH_MAX_MS = 1e3;
|
|
422
|
+
/**
|
|
423
|
+
* Build a gradient ramp of length `n` traversing the keys `from → to →
|
|
424
|
+
* from → to` (matching Crush's `CycleColors=true` mode, which prerenders
|
|
425
|
+
* `width*3` stops across four anchor points so the drift can loop without
|
|
426
|
+
* a visible seam).
|
|
427
|
+
*/
|
|
428
|
+
function buildCycleRamp(from, to, n) {
|
|
429
|
+
const segLen = Math.floor(n / 3);
|
|
430
|
+
const ramp = [];
|
|
431
|
+
for (let i = 0; i < segLen; i++) ramp.push(blendHsl(from, to, i / segLen));
|
|
432
|
+
for (let i = 0; i < segLen; i++) ramp.push(blendHsl(to, from, i / segLen));
|
|
433
|
+
const tail = n - 2 * segLen;
|
|
434
|
+
for (let i = 0; i < tail; i++) ramp.push(blendHsl(from, to, i / Math.max(1, tail)));
|
|
435
|
+
return ramp;
|
|
436
|
+
}
|
|
437
|
+
function CrushThrobber({ label, size = 15, from, to, labelColor }) {
|
|
438
|
+
const COLOR = useColors();
|
|
439
|
+
const cells = Math.max(1, size);
|
|
440
|
+
const [frame, setFrame] = useState(0);
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
const id = setInterval(() => setFrame((f) => f + 1), TICK_MS);
|
|
443
|
+
return () => clearInterval(id);
|
|
444
|
+
}, []);
|
|
445
|
+
const birthOffsets = useMemo(() => Array.from({ length: cells }, () => Math.random() * BIRTH_MAX_MS), [cells]);
|
|
446
|
+
const startRef = useRef(0);
|
|
447
|
+
if (startRef.current === 0) startRef.current = Date.now();
|
|
448
|
+
const ramp = useMemo(() => buildCycleRamp(from, to, cells * 3), [
|
|
449
|
+
from,
|
|
450
|
+
to,
|
|
451
|
+
cells
|
|
452
|
+
]);
|
|
453
|
+
const elapsed = Date.now() - startRef.current;
|
|
454
|
+
const initialized = elapsed >= BIRTH_MAX_MS;
|
|
455
|
+
const rampLen = ramp.length;
|
|
456
|
+
const offset = (frame % rampLen + rampLen) % rampLen;
|
|
457
|
+
const labelTint = labelColor ?? COLOR.dim;
|
|
458
|
+
const ellipsis = initialized && label ? ELLIPSIS_FRAMES[Math.floor(frame / ELLIPSIS_TICKS) % ELLIPSIS_FRAMES.length] : "";
|
|
459
|
+
return /* @__PURE__ */ jsxs("text", { children: [Array.from({ length: cells }, (_, i) => {
|
|
460
|
+
const ch = initialized || elapsed >= birthOffsets[i] ? CRUSH_RUNES[Math.floor(Math.random() * 38)] : ".";
|
|
461
|
+
const fg = ramp[(offset + i) % rampLen];
|
|
462
|
+
return /* @__PURE__ */ jsx("span", {
|
|
463
|
+
fg,
|
|
464
|
+
children: ch
|
|
465
|
+
}, i);
|
|
466
|
+
}), label !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
467
|
+
fg: labelTint,
|
|
468
|
+
children: ` ${label}`
|
|
469
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
470
|
+
fg: labelTint,
|
|
471
|
+
children: ellipsis
|
|
472
|
+
})] })] });
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
320
475
|
//#region src/tui/theme.ts
|
|
321
476
|
/**
|
|
322
477
|
* Convert the renderer-agnostic `Theme.syntax` map (hex strings + plain
|
|
@@ -506,12 +661,15 @@ function onInputSubmit(handler) {
|
|
|
506
661
|
* Width tiering is driven by plain-text length estimates — close enough
|
|
507
662
|
* since the segments are ASCII-heavy.
|
|
508
663
|
*/
|
|
509
|
-
function Footer({ hints, context }) {
|
|
664
|
+
function Footer({ hints, context, cost = null, status = null }) {
|
|
510
665
|
const { width } = useTerminalDimensions();
|
|
666
|
+
const showCost = typeof cost === "number" && cost > 0;
|
|
511
667
|
const inner = Math.max(0, width - 2);
|
|
512
668
|
const hW = hintsLength(hints);
|
|
513
|
-
const
|
|
514
|
-
|
|
669
|
+
const ctxW = context ? contextIndicatorLength(context) : 0;
|
|
670
|
+
const costW = showCost ? costIndicatorLength(cost) : 0;
|
|
671
|
+
const rightW = costW + ctxW + (costW > 0 && ctxW > 0 ? 3 : 0);
|
|
672
|
+
if (hW + (status === "asking" ? 3 : status ? 2 : 0) + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
|
|
515
673
|
style: {
|
|
516
674
|
flexDirection: "row",
|
|
517
675
|
height: 1,
|
|
@@ -520,8 +678,12 @@ function Footer({ hints, context }) {
|
|
|
520
678
|
},
|
|
521
679
|
children: [
|
|
522
680
|
/* @__PURE__ */ jsx(HintsText, { hints }),
|
|
681
|
+
/* @__PURE__ */ jsx(FooterStatusIcon, { status }),
|
|
523
682
|
/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
|
|
524
|
-
|
|
683
|
+
/* @__PURE__ */ jsx(RightStatus, {
|
|
684
|
+
cost: showCost ? cost : null,
|
|
685
|
+
context
|
|
686
|
+
})
|
|
525
687
|
]
|
|
526
688
|
});
|
|
527
689
|
const stackedHints = clipHintsToWidth(hints, inner);
|
|
@@ -531,21 +693,48 @@ function Footer({ hints, context }) {
|
|
|
531
693
|
paddingLeft: 1,
|
|
532
694
|
paddingRight: 1
|
|
533
695
|
},
|
|
534
|
-
children: [stackedHints.length > 0 && /* @__PURE__ */
|
|
696
|
+
children: [(stackedHints.length > 0 || status) && /* @__PURE__ */ jsxs("box", {
|
|
535
697
|
style: {
|
|
536
698
|
flexDirection: "row",
|
|
537
699
|
height: 1
|
|
538
700
|
},
|
|
539
|
-
children: /* @__PURE__ */ jsx(HintsText, { hints: stackedHints })
|
|
540
|
-
}),
|
|
701
|
+
children: [/* @__PURE__ */ jsx(HintsText, { hints: stackedHints }), /* @__PURE__ */ jsx(FooterStatusIcon, { status })]
|
|
702
|
+
}), rightW > 0 && /* @__PURE__ */ jsxs("box", {
|
|
541
703
|
style: {
|
|
542
704
|
flexDirection: "row",
|
|
543
705
|
height: 1
|
|
544
706
|
},
|
|
545
|
-
children: [/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }), /* @__PURE__ */ jsx(
|
|
707
|
+
children: [/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }), /* @__PURE__ */ jsx(RightStatus, {
|
|
708
|
+
cost: showCost ? cost : null,
|
|
709
|
+
context
|
|
710
|
+
})]
|
|
546
711
|
})]
|
|
547
712
|
});
|
|
548
713
|
}
|
|
714
|
+
function RightStatus({ cost, context }) {
|
|
715
|
+
const COLOR = useColors();
|
|
716
|
+
if (cost == null && !context) return null;
|
|
717
|
+
return /* @__PURE__ */ jsxs("text", { children: [
|
|
718
|
+
cost != null && /* @__PURE__ */ jsx(CostIndicator, { cost }),
|
|
719
|
+
cost != null && context && /* @__PURE__ */ jsx("span", {
|
|
720
|
+
fg: COLOR.mute,
|
|
721
|
+
children: " · "
|
|
722
|
+
}),
|
|
723
|
+
context && /* @__PURE__ */ jsx(ContextIndicator, { context })
|
|
724
|
+
] });
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Right-edge status glyph in the Footer's left section. Renders a single
|
|
728
|
+
* cell preceded by a 1ch gap when active; nothing at all when `status` is
|
|
729
|
+
* null. Color / glyph mapping mirrors the old title-overlay logic so the
|
|
730
|
+
* cue reads the same just relocated to the bottom bar.
|
|
731
|
+
*/
|
|
732
|
+
function FooterStatusIcon({ status }) {
|
|
733
|
+
const COLOR = useColors();
|
|
734
|
+
if (!status) return null;
|
|
735
|
+
const glyph = status === "asking" ? /* @__PURE__ */ jsx("span", { children: "❓" }) : status === "busy" ? /* @__PURE__ */ jsx(StatusSpinner, { color: COLOR.warn }) : /* @__PURE__ */ jsx(StatusSpinner, { color: COLOR.accent });
|
|
736
|
+
return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", { children: " " }), glyph] });
|
|
737
|
+
}
|
|
549
738
|
function HintsText({ hints }) {
|
|
550
739
|
const COLOR = useColors();
|
|
551
740
|
return /* @__PURE__ */ jsx("text", {
|
|
@@ -596,27 +785,35 @@ function ContextIndicator({ context }) {
|
|
|
596
785
|
const ratio = context.max > 0 ? context.used / context.max : 0;
|
|
597
786
|
const pct = Math.round(ratio * 100);
|
|
598
787
|
const color = ratio >= .85 ? COLOR.error : ratio >= .6 ? COLOR.warn : COLOR.dim;
|
|
599
|
-
return /* @__PURE__ */ jsxs("
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
788
|
+
return /* @__PURE__ */ jsxs("span", { children: [
|
|
789
|
+
/* @__PURE__ */ jsx("span", {
|
|
790
|
+
fg: COLOR.mute,
|
|
791
|
+
children: "ctx "
|
|
792
|
+
}),
|
|
793
|
+
/* @__PURE__ */ jsx("span", {
|
|
794
|
+
fg: color,
|
|
795
|
+
children: fmtTokens(context.used)
|
|
796
|
+
}),
|
|
797
|
+
/* @__PURE__ */ jsx("span", {
|
|
798
|
+
fg: COLOR.mute,
|
|
799
|
+
children: ` / ${fmtTokens(context.max)} `
|
|
800
|
+
}),
|
|
801
|
+
/* @__PURE__ */ jsx("span", {
|
|
802
|
+
fg: color,
|
|
803
|
+
children: `(${pct}%)`
|
|
804
|
+
})
|
|
805
|
+
] });
|
|
806
|
+
}
|
|
807
|
+
function CostIndicator({ cost }) {
|
|
808
|
+
const COLOR = useColors();
|
|
809
|
+
const fg = COLOR.money ?? COLOR.warn;
|
|
810
|
+
return /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", {
|
|
811
|
+
fg: COLOR.mute,
|
|
812
|
+
children: "$"
|
|
813
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
814
|
+
fg,
|
|
815
|
+
children: formatCost(cost)
|
|
816
|
+
})] });
|
|
620
817
|
}
|
|
621
818
|
/**
|
|
622
819
|
* Width budget reservations applied to the responsive math in
|
|
@@ -629,10 +826,23 @@ const TITLE_OVERLAY_WRAP = 2;
|
|
|
629
826
|
const TITLE_OVERLAY_META_WRAP = 2;
|
|
630
827
|
const TITLE_OVERLAY_GAP = 2;
|
|
631
828
|
/**
|
|
829
|
+
* Render text as a per-character HSL gradient between two hex colors.
|
|
830
|
+
* Used by the title overlay so the chat header sweeps the theme's
|
|
831
|
+
* throbber pair (Charple→Dolly for Crush) instead of sitting flat on
|
|
832
|
+
* the brand color.
|
|
833
|
+
*/
|
|
834
|
+
function renderGradientText(text, from, to) {
|
|
835
|
+
const ramp = buildLinearRamp(from, to, text.length);
|
|
836
|
+
return text.split("").map((ch, i) => /* @__PURE__ */ jsx("span", {
|
|
837
|
+
fg: ramp[i],
|
|
838
|
+
children: ch
|
|
839
|
+
}, i));
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
632
842
|
* Colored title for a full-screen bordered surface. The `title` slot
|
|
633
|
-
* rides `titleColor` (defaults to
|
|
634
|
-
*
|
|
635
|
-
* rides on the RIGHT.
|
|
843
|
+
* rides `titleColor` (defaults to a per-character gradient across the
|
|
844
|
+
* theme's throbber pair, fallback brand→accent) on the LEFT of the top
|
|
845
|
+
* border; the optional `meta` slot rides on the RIGHT.
|
|
636
846
|
*
|
|
637
847
|
* `meta` accepts either:
|
|
638
848
|
* - a plain `string` → rendered entirely in `COLOR.dim` (the simple
|
|
@@ -667,13 +877,16 @@ const TITLE_OVERLAY_GAP = 2;
|
|
|
667
877
|
* </box>
|
|
668
878
|
* ```
|
|
669
879
|
*/
|
|
670
|
-
function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon = null }) {
|
|
880
|
+
function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon = null, statusIconCells = 1 }) {
|
|
671
881
|
const COLOR = useColors();
|
|
672
882
|
const { width: termWidth } = useTerminalDimensions();
|
|
883
|
+
const useGradient = titleColor === void 0;
|
|
884
|
+
const gradientFrom = COLOR.throbber?.to ?? COLOR.accent;
|
|
885
|
+
const gradientTo = COLOR.throbber?.from ?? COLOR.brand;
|
|
673
886
|
const fg = titleColor ?? COLOR.brand;
|
|
674
887
|
const W = Math.max(0, parentWidth ?? termWidth - 2);
|
|
675
888
|
const inner = Math.max(0, W - 2);
|
|
676
|
-
const iconReserve = statusIcon ?
|
|
889
|
+
const iconReserve = statusIcon ? 1 + statusIconCells : 0;
|
|
677
890
|
const metaLen = metaSegmentsLength(meta);
|
|
678
891
|
const showMeta = meta != null && metaLen > 0 && title.length + iconReserve + TITLE_OVERLAY_WRAP + TITLE_OVERLAY_GAP + metaLen + TITLE_OVERLAY_META_WRAP <= inner;
|
|
679
892
|
const titleBudget = (showMeta ? inner - (metaLen + TITLE_OVERLAY_META_WRAP) - TITLE_OVERLAY_GAP - TITLE_OVERLAY_WRAP : inner - TITLE_OVERLAY_WRAP) - iconReserve;
|
|
@@ -689,7 +902,7 @@ function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon
|
|
|
689
902
|
fg: COLOR.mute,
|
|
690
903
|
children: " "
|
|
691
904
|
}),
|
|
692
|
-
/* @__PURE__ */ jsx("span", {
|
|
905
|
+
useGradient && visibleTitle.length > 0 ? renderGradientText(visibleTitle, gradientFrom, gradientTo) : /* @__PURE__ */ jsx("span", {
|
|
693
906
|
fg,
|
|
694
907
|
children: visibleTitle
|
|
695
908
|
}),
|
|
@@ -800,17 +1013,23 @@ function contextIndicatorLength(context) {
|
|
|
800
1013
|
const pct = Math.round(ratio * 100);
|
|
801
1014
|
return 4 + fmtTokens(context.used).length + 3 + fmtTokens(context.max).length + 2 + String(pct).length + 2;
|
|
802
1015
|
}
|
|
1016
|
+
function formatCost(cost) {
|
|
1017
|
+
return cost.toFixed(cost < .01 ? 4 : 2);
|
|
1018
|
+
}
|
|
1019
|
+
function costIndicatorLength(cost) {
|
|
1020
|
+
return 1 + formatCost(cost).length;
|
|
1021
|
+
}
|
|
803
1022
|
const SPINNER_FRAMES = [
|
|
804
|
-
"
|
|
805
|
-
"
|
|
806
|
-
"
|
|
807
|
-
"
|
|
808
|
-
"
|
|
809
|
-
"
|
|
810
|
-
"
|
|
811
|
-
"
|
|
812
|
-
"
|
|
813
|
-
"
|
|
1023
|
+
"▰▱▱▱▱",
|
|
1024
|
+
"▰▰▱▱▱",
|
|
1025
|
+
"▰▰▰▱▱",
|
|
1026
|
+
"▰▰▰▰▱",
|
|
1027
|
+
"▰▰▰▰▰",
|
|
1028
|
+
"▱▰▰▰▰",
|
|
1029
|
+
"▱▱▰▰▰",
|
|
1030
|
+
"▱▱▱▰▰",
|
|
1031
|
+
"▱▱▱▱▰",
|
|
1032
|
+
"▱▱▱▱▱"
|
|
814
1033
|
];
|
|
815
1034
|
const SPINNER_INTERVAL_MS = 80;
|
|
816
1035
|
/**
|
|
@@ -850,9 +1069,11 @@ function StatusSpinner({ color }) {
|
|
|
850
1069
|
children: useSpinnerFrame()
|
|
851
1070
|
});
|
|
852
1071
|
}
|
|
853
|
-
function Transcript({ events, settings, selectedTurnId = null }) {
|
|
1072
|
+
function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
854
1073
|
const COLOR = useColors();
|
|
855
1074
|
const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
|
|
1075
|
+
const showThrobber = busy;
|
|
1076
|
+
const throbberLabel = events.length > 0 && events[events.length - 1].kind === "thinking" ? "Thinking" : void 0;
|
|
856
1077
|
const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
|
|
857
1078
|
const scrollboxRef = useRef(null);
|
|
858
1079
|
const anchors = useMemo(() => computeTurnAnchors(items), [items]);
|
|
@@ -875,8 +1096,8 @@ function Transcript({ events, settings, selectedTurnId = null }) {
|
|
|
875
1096
|
anchors,
|
|
876
1097
|
ownership
|
|
877
1098
|
]);
|
|
878
|
-
if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
|
|
879
|
-
return /* @__PURE__ */
|
|
1099
|
+
if (items.length === 0 && !showThrobber) return /* @__PURE__ */ jsx(EmptyState, {});
|
|
1100
|
+
return /* @__PURE__ */ jsxs("scrollbox", {
|
|
880
1101
|
ref: scrollboxRef,
|
|
881
1102
|
focusable: false,
|
|
882
1103
|
style: {
|
|
@@ -893,7 +1114,7 @@ function Transcript({ events, settings, selectedTurnId = null }) {
|
|
|
893
1114
|
foregroundColor: COLOR.mute
|
|
894
1115
|
}
|
|
895
1116
|
},
|
|
896
|
-
children: items.map((item, i) => item.kind === "event" ? /* @__PURE__ */ jsx(EventLine, {
|
|
1117
|
+
children: [items.map((item, i) => item.kind === "event" ? /* @__PURE__ */ jsx(EventLine, {
|
|
897
1118
|
event: item.event,
|
|
898
1119
|
previous: item.previous,
|
|
899
1120
|
selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
|
|
@@ -903,7 +1124,14 @@ function Transcript({ events, settings, selectedTurnId = null }) {
|
|
|
903
1124
|
previous: item.previous,
|
|
904
1125
|
selectedTurnId,
|
|
905
1126
|
anchorIds: anchors.ids[i]
|
|
906
|
-
}, i))
|
|
1127
|
+
}, i)), showThrobber && /* @__PURE__ */ jsx("box", {
|
|
1128
|
+
style: { marginTop: items.length > 0 ? 1 : 0 },
|
|
1129
|
+
children: /* @__PURE__ */ jsx(CrushThrobber, {
|
|
1130
|
+
label: throbberLabel,
|
|
1131
|
+
from: COLOR.throbber?.from ?? COLOR.brand,
|
|
1132
|
+
to: COLOR.throbber?.to ?? COLOR.accent
|
|
1133
|
+
})
|
|
1134
|
+
})]
|
|
907
1135
|
});
|
|
908
1136
|
}
|
|
909
1137
|
/**
|
|
@@ -1127,7 +1355,6 @@ function EventLineImpl({ event, depthOffset = 0 }) {
|
|
|
1127
1355
|
style: row,
|
|
1128
1356
|
children: /* @__PURE__ */ jsx(MarkdownBlock, {
|
|
1129
1357
|
text: event.text,
|
|
1130
|
-
streaming: event.streaming ?? false,
|
|
1131
1358
|
dim: child
|
|
1132
1359
|
})
|
|
1133
1360
|
});
|
|
@@ -1276,13 +1503,10 @@ function UserPromptBlock({ text, refs }) {
|
|
|
1276
1503
|
};
|
|
1277
1504
|
if (!refs || refs.length === 0) return /* @__PURE__ */ jsx("box", {
|
|
1278
1505
|
style: boxStyle,
|
|
1279
|
-
children: /* @__PURE__ */ jsxs("text", {
|
|
1506
|
+
children: /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
1280
1507
|
fg: COLOR.brand,
|
|
1281
|
-
children:
|
|
1282
|
-
|
|
1283
|
-
children: USER_PROMPT_PREFIX
|
|
1284
|
-
}), text]
|
|
1285
|
-
})
|
|
1508
|
+
children: USER_PROMPT_PREFIX
|
|
1509
|
+
}), text] })
|
|
1286
1510
|
});
|
|
1287
1511
|
const segments = splitPromptSegments(text, refs);
|
|
1288
1512
|
return /* @__PURE__ */ jsx("box", {
|
|
@@ -1296,10 +1520,7 @@ function UserPromptBlock({ text, refs }) {
|
|
|
1296
1520
|
fg: COLOR.brand,
|
|
1297
1521
|
children: USER_PROMPT_PREFIX
|
|
1298
1522
|
}), segments.map((seg, i) => {
|
|
1299
|
-
if (seg.kind === "plain") return /* @__PURE__ */ jsx("text", {
|
|
1300
|
-
fg: COLOR.brand,
|
|
1301
|
-
children: seg.text
|
|
1302
|
-
}, i);
|
|
1523
|
+
if (seg.kind === "plain") return /* @__PURE__ */ jsx("text", { children: seg.text }, i);
|
|
1303
1524
|
const chip = resolveChipColor(SURFACE.chips, seg.providerId);
|
|
1304
1525
|
return /* @__PURE__ */ jsx("text", {
|
|
1305
1526
|
fg: chip.fg,
|
|
@@ -1310,214 +1531,310 @@ function UserPromptBlock({ text, refs }) {
|
|
|
1310
1531
|
})
|
|
1311
1532
|
});
|
|
1312
1533
|
}
|
|
1534
|
+
/** Click-feedback delay before `[copied]` reverts to `[copy]`, in ms. */
|
|
1535
|
+
const COPY_FEEDBACK_MS = 1200;
|
|
1313
1536
|
/**
|
|
1314
|
-
*
|
|
1537
|
+
* BoxRenderable subclass that hosts a `[lang] … [copy]` header above
|
|
1538
|
+
* an inner {@link CodeRenderable} body. Returned from
|
|
1539
|
+
* {@link makeMarkdownRenderNode} for every `code` token so the affordance
|
|
1540
|
+
* lives entirely inside OpenTUI — React doesn't see the wrapper.
|
|
1541
|
+
*
|
|
1542
|
+
* The accessor pairs below mirror the CodeRenderable surface (`content`,
|
|
1543
|
+
* `filetype`, `syntaxStyle`, `fg`, `bg`, `conceal`, `streaming`) so
|
|
1544
|
+
* coalesced mode's `applyCodeBlockRenderable` — which writes those
|
|
1545
|
+
* properties on the wrapper for every delta — lands on the body where
|
|
1546
|
+
* it produces visible output.
|
|
1315
1547
|
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* close" rule. Info-strings (the bit after the opening fence) are kept
|
|
1319
|
-
* as the `lang` hint; any trailing whitespace is trimmed. Closing
|
|
1320
|
-
* fences are detected on a line of their own (whitespace tolerated).
|
|
1548
|
+
* Two of those forwarders deliberately ignore the value the markdown
|
|
1549
|
+
* writes:
|
|
1321
1550
|
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1551
|
+
* - `drawUnstyledText` is pinned to `false` so the body's `content`
|
|
1552
|
+
* setter stays on the "preserve last-styled buffer while async
|
|
1553
|
+
* highlight runs" path. The markdown's `applyCodeBlockRenderable`
|
|
1554
|
+
* writes `!(streaming && concealCode)` here, which evaluates to
|
|
1555
|
+
* `true` for our defaults — that would put the body on the
|
|
1556
|
+
* synchronous `textBuffer.setText` path and reintroduce a per-delta
|
|
1557
|
+
* plain → styled colour swap.
|
|
1326
1558
|
*
|
|
1327
|
-
*
|
|
1559
|
+
* - `marginBottom` is pinned to `0`. The markdown writes
|
|
1560
|
+
* `getInterBlockMargin(code) = 1` here on every delta; combined with
|
|
1561
|
+
* the `marginTop: 1` `renderNode` applies to the next block, it
|
|
1562
|
+
* would stack into a two-row gap below every fence. Pinning to 0
|
|
1563
|
+
* makes the next block's `marginTop` the single source of truth for
|
|
1564
|
+
* inter-block spacing.
|
|
1328
1565
|
*/
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1566
|
+
var CodeBlockWrapper = class extends BoxRenderable {
|
|
1567
|
+
/** Live code body owned by this wrapper. Updates pass through here. */
|
|
1568
|
+
body;
|
|
1569
|
+
/** Header chrome — re-themed on every {@link applyTheme} call. */
|
|
1570
|
+
header;
|
|
1571
|
+
spacer;
|
|
1572
|
+
langLabel;
|
|
1573
|
+
button;
|
|
1574
|
+
/** Bag we belong to. Used for live theme lookups and self-deregistration. */
|
|
1575
|
+
bag;
|
|
1576
|
+
/** Pending feedback-reset timer for the `[copied]` flash. */
|
|
1577
|
+
feedbackTimer = null;
|
|
1578
|
+
/** Whether the button is currently showing `[copied]`. */
|
|
1579
|
+
copied = false;
|
|
1580
|
+
/**
|
|
1581
|
+
* Latest body content, captured every time the markdown writes to
|
|
1582
|
+
* `content`. Read by the click handler so a copy click pushes the
|
|
1583
|
+
* up-to-date code (not whatever was there on first mount).
|
|
1584
|
+
*/
|
|
1585
|
+
latestContent;
|
|
1586
|
+
constructor(ctx, body, options) {
|
|
1587
|
+
const { colors, surfaces } = options.bag;
|
|
1588
|
+
super(ctx, {
|
|
1589
|
+
flexDirection: "column",
|
|
1590
|
+
flexShrink: 0,
|
|
1591
|
+
alignSelf: "stretch",
|
|
1592
|
+
backgroundColor: surfaces.background
|
|
1593
|
+
});
|
|
1594
|
+
this.body = body;
|
|
1595
|
+
this.bag = options.bag;
|
|
1596
|
+
this.latestContent = body.content;
|
|
1597
|
+
body.marginTop = 0;
|
|
1598
|
+
body.marginBottom = 0;
|
|
1599
|
+
body.drawUnstyledText = false;
|
|
1600
|
+
this.header = new BoxRenderable(ctx, {
|
|
1601
|
+
flexDirection: "row",
|
|
1602
|
+
height: 1,
|
|
1603
|
+
alignSelf: "stretch",
|
|
1604
|
+
backgroundColor: surfaces.background
|
|
1605
|
+
});
|
|
1606
|
+
this.langLabel = new TextRenderable(ctx, {
|
|
1607
|
+
content: options.lang && options.lang.length > 0 ? options.lang : "code",
|
|
1608
|
+
fg: colors.mute,
|
|
1609
|
+
selectable: false
|
|
1610
|
+
});
|
|
1611
|
+
this.spacer = new BoxRenderable(ctx, {
|
|
1612
|
+
flexGrow: 1,
|
|
1613
|
+
backgroundColor: surfaces.background
|
|
1614
|
+
});
|
|
1615
|
+
this.button = new TextRenderable(ctx, {
|
|
1616
|
+
content: "[copy]",
|
|
1617
|
+
fg: colors.warn,
|
|
1618
|
+
selectable: false
|
|
1619
|
+
});
|
|
1620
|
+
this.button.onMouseDown = (event) => {
|
|
1621
|
+
event.stopPropagation();
|
|
1622
|
+
event.preventDefault();
|
|
1623
|
+
if (!writeToClipboard(this.latestContent)) return;
|
|
1624
|
+
const live = this.bag.colors;
|
|
1625
|
+
if (!this.copied) {
|
|
1626
|
+
this.copied = true;
|
|
1627
|
+
this.button.content = "[copied]";
|
|
1628
|
+
this.button.fg = live.accent;
|
|
1629
|
+
}
|
|
1630
|
+
if (this.feedbackTimer) clearTimeout(this.feedbackTimer);
|
|
1631
|
+
this.feedbackTimer = setTimeout(() => {
|
|
1632
|
+
this.copied = false;
|
|
1633
|
+
this.feedbackTimer = null;
|
|
1634
|
+
if (this.button.isDestroyed) return;
|
|
1635
|
+
this.button.content = "[copy]";
|
|
1636
|
+
this.button.fg = this.bag.colors.warn;
|
|
1637
|
+
}, COPY_FEEDBACK_MS);
|
|
1638
|
+
};
|
|
1639
|
+
this.header.add(this.langLabel);
|
|
1640
|
+
this.header.add(this.spacer);
|
|
1641
|
+
this.header.add(this.button);
|
|
1642
|
+
this.add(this.header);
|
|
1643
|
+
this.add(this.body);
|
|
1644
|
+
options.bag.wrappers.add(this);
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Re-paint header chrome with the current theme. Called from
|
|
1648
|
+
* {@link MarkdownBlock}'s `useEffect` after every theme switch — the
|
|
1649
|
+
* body's `fg`/`bg` are already re-driven by the markdown's own
|
|
1650
|
+
* `rerenderBlocks` pass through our forwarding accessors, so we only
|
|
1651
|
+
* need to refresh the bits the markdown doesn't manage: the wrapper's
|
|
1652
|
+
* own background, the header row, the spacer, the lang label, and the
|
|
1653
|
+
* `[copy]`/`[copied]` button colour (which depends on `this.copied`).
|
|
1654
|
+
*/
|
|
1655
|
+
applyTheme(colors, surfaces) {
|
|
1656
|
+
this.backgroundColor = surfaces.background;
|
|
1657
|
+
this.header.backgroundColor = surfaces.background;
|
|
1658
|
+
this.spacer.backgroundColor = surfaces.background;
|
|
1659
|
+
this.langLabel.fg = colors.mute;
|
|
1660
|
+
this.button.fg = this.copied ? colors.accent : colors.warn;
|
|
1661
|
+
}
|
|
1662
|
+
get content() {
|
|
1663
|
+
return this.body.content;
|
|
1664
|
+
}
|
|
1665
|
+
set content(value) {
|
|
1666
|
+
this.latestContent = value;
|
|
1667
|
+
this.body.content = value;
|
|
1668
|
+
}
|
|
1669
|
+
get filetype() {
|
|
1670
|
+
return this.body.filetype;
|
|
1671
|
+
}
|
|
1672
|
+
set filetype(value) {
|
|
1673
|
+
this.body.filetype = value;
|
|
1674
|
+
}
|
|
1675
|
+
get syntaxStyle() {
|
|
1676
|
+
return this.body.syntaxStyle;
|
|
1677
|
+
}
|
|
1678
|
+
set syntaxStyle(value) {
|
|
1679
|
+
this.body.syntaxStyle = value;
|
|
1680
|
+
}
|
|
1681
|
+
get fg() {
|
|
1682
|
+
return this.body.fg;
|
|
1683
|
+
}
|
|
1684
|
+
set fg(value) {
|
|
1685
|
+
this.body.fg = value;
|
|
1686
|
+
}
|
|
1687
|
+
get bg() {
|
|
1688
|
+
return this.body.bg;
|
|
1689
|
+
}
|
|
1690
|
+
set bg(value) {
|
|
1691
|
+
this.body.bg = value;
|
|
1692
|
+
}
|
|
1693
|
+
get conceal() {
|
|
1694
|
+
return this.body.conceal;
|
|
1695
|
+
}
|
|
1696
|
+
set conceal(value) {
|
|
1697
|
+
this.body.conceal = value;
|
|
1698
|
+
}
|
|
1699
|
+
get streaming() {
|
|
1700
|
+
return this.body.streaming;
|
|
1701
|
+
}
|
|
1702
|
+
set streaming(value) {
|
|
1703
|
+
this.body.streaming = value;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Always reports `false`: see the setter doc.
|
|
1707
|
+
*/
|
|
1708
|
+
get drawUnstyledText() {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Pinned to `false` regardless of what the markdown writes. See the
|
|
1713
|
+
* class doc — leaving this open to the markdown's default would put
|
|
1714
|
+
* the body's `content` setter on the synchronous-plain-text path on
|
|
1715
|
+
* every delta, replacing the styled buffer with raw text for one
|
|
1716
|
+
* frame and producing a visible colour swap.
|
|
1717
|
+
*/
|
|
1718
|
+
set drawUnstyledText(_value) {
|
|
1719
|
+
if (this.body.drawUnstyledText !== false) this.body.drawUnstyledText = false;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Always reports `0`: see the setter doc.
|
|
1723
|
+
*/
|
|
1724
|
+
get marginBottom() {
|
|
1725
|
+
return 0;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Pinned to `0`. The next block's `marginTop` (which `renderNode`
|
|
1729
|
+
* sets to `1` on every non-first block) is the single source of
|
|
1730
|
+
* inter-block spacing. Letting the markdown also write
|
|
1731
|
+
* `getInterBlockMargin(code) = 1` here on every delta would stack to
|
|
1732
|
+
* a two-row gap below every fence.
|
|
1733
|
+
*/
|
|
1734
|
+
set marginBottom(_value) {}
|
|
1735
|
+
destroy() {
|
|
1736
|
+
this.bag.wrappers.delete(this);
|
|
1737
|
+
if (this.feedbackTimer) {
|
|
1738
|
+
clearTimeout(this.feedbackTimer);
|
|
1739
|
+
this.feedbackTimer = null;
|
|
1370
1740
|
}
|
|
1371
|
-
|
|
1741
|
+
super.destroy();
|
|
1372
1742
|
}
|
|
1373
|
-
if (inFence) prose.push(`${fenceChar.repeat(fenceLen)}${lang}`, ...code);
|
|
1374
|
-
flushProse();
|
|
1375
|
-
return segments;
|
|
1376
|
-
}
|
|
1377
|
-
/** Fade-out delay for the "[copied]" affordance, in ms. */
|
|
1378
|
-
const COPY_FEEDBACK_MS = 1200;
|
|
1379
|
-
/**
|
|
1380
|
-
* Fence info-string → canonical Tree-sitter filetype.
|
|
1381
|
-
*
|
|
1382
|
-
* OpenTUI's built-in parsers register `typescript`, `javascript`,
|
|
1383
|
-
* `markdown`, `zig`; the `<markdown>` renderer has an internal
|
|
1384
|
-
* `infoStringMap` that translates short fence info-strings (`ts`, `js`,
|
|
1385
|
-
* `tsx`, `jsx`, `md`) into those canonical names before dispatching the
|
|
1386
|
-
* embedded fence. Our standalone `<code>` (lifted from the markdown body
|
|
1387
|
-
* so we can pin a copy header) skips that map, so we re-apply the same
|
|
1388
|
-
* translation here. Without it, ` ```ts ` would land as plain text while
|
|
1389
|
-
* ` ```typescript ` would highlight — a regression vs. the pre-copy
|
|
1390
|
-
* render path.
|
|
1391
|
-
*
|
|
1392
|
-
* Custom parsers registered in `tui/tree-sitter.ts` (`python`, `bash`,
|
|
1393
|
-
* `rust`, `go`, `yaml`, `html`, `css`, `json`) carry their own aliases
|
|
1394
|
-
* via `addDefaultParsers`, so the OpenTUI client resolves `py`, `sh`,
|
|
1395
|
-
* `rs`, `golang`, `yml`, `htm`, `zsh` natively — no entry needed here.
|
|
1396
|
-
*/
|
|
1397
|
-
const FILETYPE_ALIASES = {
|
|
1398
|
-
ts: "typescript",
|
|
1399
|
-
tsx: "typescript",
|
|
1400
|
-
typescriptreact: "typescript",
|
|
1401
|
-
js: "javascript",
|
|
1402
|
-
jsx: "javascript",
|
|
1403
|
-
mjs: "javascript",
|
|
1404
|
-
cjs: "javascript",
|
|
1405
|
-
javascriptreact: "javascript",
|
|
1406
|
-
md: "markdown"
|
|
1407
1743
|
};
|
|
1408
1744
|
/**
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
* to
|
|
1745
|
+
* Block-index regex applied to OpenTUI's auto-generated renderable ids
|
|
1746
|
+
* (`${markdown.id}-block-${index}`). The trailing digits give us the
|
|
1747
|
+
* block's position inside the markdown's child list, which is the only
|
|
1748
|
+
* piece of context we need to gate the "non-first block → marginTop: 1"
|
|
1749
|
+
* spacing rule.
|
|
1413
1750
|
*/
|
|
1414
|
-
|
|
1415
|
-
if (!lang) return void 0;
|
|
1416
|
-
const key = lang.toLowerCase().trim();
|
|
1417
|
-
if (!key || key === "text" || key === "plain" || key === "plaintext" || key === "txt") return void 0;
|
|
1418
|
-
return FILETYPE_ALIASES[key] ?? key;
|
|
1419
|
-
}
|
|
1751
|
+
const BLOCK_ID_INDEX_RE = /-block-(\d+)$/;
|
|
1420
1752
|
/**
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
* -
|
|
1753
|
+
* Build a stable `renderNode` callback wired to a live `RenderNodeBag`.
|
|
1754
|
+
*
|
|
1755
|
+
* Returns:
|
|
1756
|
+
* - `code` tokens → {@link CodeBlockWrapper} (hosts the `[lang] …
|
|
1757
|
+
* [copy]` header).
|
|
1758
|
+
* - everything else (prose, heading, list, table, …) → the renderable
|
|
1759
|
+
* we got from `context.defaultRender()`, mutated in place.
|
|
1425
1760
|
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
* over it doesn't extend a selection started elsewhere.
|
|
1761
|
+
* Critical: we MUST return the renderable we mutated, not `undefined`.
|
|
1762
|
+
* OpenTUI's coalesced-mode loop, when `renderNode` returns a falsy
|
|
1763
|
+
* value, creates a fresh default renderable (`markdown.ts ~8851`) — so
|
|
1764
|
+
* anything we mutated via `defaultRender()` is thrown away.
|
|
1431
1765
|
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1766
|
+
* Tables: returning the default `TextTableRenderable` looks like it
|
|
1767
|
+
* should orphan the markdown's `tableContentCache` tuple (incremental
|
|
1768
|
+
* table updates depend on it), but the markdown has a follow-up branch
|
|
1769
|
+
* (`markdown.ts ~8854`) that re-builds the cache from the token when
|
|
1770
|
+
* `renderNode` returned a `TextTableRenderable` without one. So we can
|
|
1771
|
+
* safely mutate + return the table renderable too, and the spacing
|
|
1772
|
+
* rule applies uniformly.
|
|
1773
|
+
*
|
|
1774
|
+
* Every non-first block gets `marginTop: 1`. This reimplements the
|
|
1775
|
+
* inter-block spacing that top-level mode would compute from the
|
|
1776
|
+
* parser's space tokens — coalesced mode + `renderNode` strips them
|
|
1777
|
+
* (see the file-level comment), so we recompute the spacing ourselves.
|
|
1778
|
+
* The result is bit-identical to top-level mode for every
|
|
1779
|
+
* well-formatted markdown source.
|
|
1780
|
+
*/
|
|
1781
|
+
function makeMarkdownRenderNode(bag) {
|
|
1782
|
+
return (token, context) => {
|
|
1783
|
+
const inner = context.defaultRender();
|
|
1784
|
+
if (!inner) return void 0;
|
|
1785
|
+
const topMargin = parseBlockIndex(inner.id) === 0 ? 0 : 1;
|
|
1786
|
+
if (token.type === "code" && inner instanceof CodeRenderable) {
|
|
1787
|
+
const lang = typeof token.lang === "string" ? token.lang : "";
|
|
1788
|
+
const wrapper = new CodeBlockWrapper(bag.current.ctx, inner, {
|
|
1789
|
+
lang,
|
|
1790
|
+
bag: bag.current
|
|
1791
|
+
});
|
|
1792
|
+
wrapper.marginTop = topMargin;
|
|
1793
|
+
return wrapper;
|
|
1794
|
+
}
|
|
1795
|
+
inner.marginTop = topMargin;
|
|
1796
|
+
return inner;
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Pull the trailing block index out of an OpenTUI renderable id. Returns
|
|
1801
|
+
* `0` for any id that doesn't match the expected `…-block-N` shape — the
|
|
1802
|
+
* spacing rule then treats unparseable ids as "first block" (no
|
|
1803
|
+
* `marginTop`), which is the safer side of the rendering edge case.
|
|
1437
1804
|
*/
|
|
1438
|
-
|
|
1805
|
+
function parseBlockIndex(id) {
|
|
1806
|
+
if (!id) return 0;
|
|
1807
|
+
const match = BLOCK_ID_INDEX_RE.exec(id);
|
|
1808
|
+
if (!match) return 0;
|
|
1809
|
+
const parsed = Number.parseInt(match[1] ?? "", 10);
|
|
1810
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1811
|
+
}
|
|
1812
|
+
const MarkdownBlock = memo(({ text, dim }) => {
|
|
1439
1813
|
const COLOR = useColors();
|
|
1814
|
+
const SURFACE = useSurfaces();
|
|
1440
1815
|
const mdStyle = useMdStyle();
|
|
1441
|
-
const
|
|
1816
|
+
const renderer = useRenderer();
|
|
1817
|
+
const bag = useRef({
|
|
1818
|
+
ctx: renderer,
|
|
1819
|
+
colors: COLOR,
|
|
1820
|
+
surfaces: SURFACE,
|
|
1821
|
+
wrappers: /* @__PURE__ */ new Set()
|
|
1822
|
+
});
|
|
1823
|
+
bag.current.ctx = renderer;
|
|
1824
|
+
bag.current.colors = COLOR;
|
|
1825
|
+
bag.current.surfaces = SURFACE;
|
|
1442
1826
|
useEffect(() => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const onCopy = useCallback(() => {
|
|
1448
|
-
setCopied(writeToClipboard(content));
|
|
1449
|
-
}, [content]);
|
|
1450
|
-
const filetype = canonicalFiletype(lang);
|
|
1451
|
-
const label = lang && lang.length > 0 ? lang : "code";
|
|
1452
|
-
const buttonColor = copied ? COLOR.accent : COLOR.warn;
|
|
1453
|
-
const buttonText = copied ? "[copied]" : "[copy]";
|
|
1454
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
1455
|
-
style: {
|
|
1456
|
-
flexDirection: "column",
|
|
1457
|
-
flexShrink: 0,
|
|
1458
|
-
alignSelf: "stretch"
|
|
1459
|
-
},
|
|
1460
|
-
children: [/* @__PURE__ */ jsxs("box", {
|
|
1461
|
-
style: {
|
|
1462
|
-
flexDirection: "row",
|
|
1463
|
-
height: 1,
|
|
1464
|
-
alignSelf: "stretch"
|
|
1465
|
-
},
|
|
1466
|
-
children: [
|
|
1467
|
-
/* @__PURE__ */ jsx("text", {
|
|
1468
|
-
selectable: false,
|
|
1469
|
-
fg: COLOR.mute,
|
|
1470
|
-
children: label
|
|
1471
|
-
}),
|
|
1472
|
-
/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
|
|
1473
|
-
/* @__PURE__ */ jsx("text", {
|
|
1474
|
-
selectable: false,
|
|
1475
|
-
fg: buttonColor,
|
|
1476
|
-
onMouseDown: (e) => {
|
|
1477
|
-
e.stopPropagation();
|
|
1478
|
-
e.preventDefault();
|
|
1479
|
-
onCopy();
|
|
1480
|
-
},
|
|
1481
|
-
children: buttonText
|
|
1482
|
-
})
|
|
1483
|
-
]
|
|
1484
|
-
}), /* @__PURE__ */ jsx("code", {
|
|
1485
|
-
content,
|
|
1486
|
-
...filetype ? { filetype } : {},
|
|
1487
|
-
syntaxStyle: mdStyle,
|
|
1488
|
-
...dim ? { fg: COLOR.dim } : {}
|
|
1489
|
-
})]
|
|
1490
|
-
});
|
|
1491
|
-
});
|
|
1492
|
-
FencedCodeBlock.displayName = "FencedCodeBlock";
|
|
1493
|
-
const MarkdownBlock = memo(({ text, streaming, dim }) => {
|
|
1494
|
-
const COLOR = useColors();
|
|
1495
|
-
const mdStyle = useMdStyle();
|
|
1496
|
-
const segments = useMemo(() => streaming ? null : splitMarkdownCodeBlocks(text), [text, streaming]);
|
|
1497
|
-
if (!segments || segments.length === 0 || !segments.some((s) => s.kind === "code")) return /* @__PURE__ */ jsx("markdown", {
|
|
1827
|
+
for (const wrapper of bag.current.wrappers) wrapper.applyTheme(COLOR, SURFACE);
|
|
1828
|
+
}, [COLOR, SURFACE]);
|
|
1829
|
+
const renderNode = useMemo(() => makeMarkdownRenderNode(bag), []);
|
|
1830
|
+
return /* @__PURE__ */ jsx("markdown", {
|
|
1498
1831
|
content: text,
|
|
1499
1832
|
syntaxStyle: mdStyle,
|
|
1500
|
-
streaming,
|
|
1501
|
-
internalBlockMode:
|
|
1502
|
-
fg: dim ? COLOR.dim : void 0
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
style: {
|
|
1506
|
-
flexDirection: "column",
|
|
1507
|
-
flexShrink: 0,
|
|
1508
|
-
alignSelf: "stretch"
|
|
1509
|
-
},
|
|
1510
|
-
children: segments.map((seg, i) => seg.kind === "code" ? /* @__PURE__ */ jsx(FencedCodeBlock, {
|
|
1511
|
-
content: seg.content,
|
|
1512
|
-
lang: seg.lang,
|
|
1513
|
-
dim
|
|
1514
|
-
}, i) : /* @__PURE__ */ jsx("markdown", {
|
|
1515
|
-
content: seg.content,
|
|
1516
|
-
syntaxStyle: mdStyle,
|
|
1517
|
-
streaming: false,
|
|
1518
|
-
internalBlockMode: "coalesced",
|
|
1519
|
-
fg: dim ? COLOR.dim : void 0
|
|
1520
|
-
}, i))
|
|
1833
|
+
streaming: true,
|
|
1834
|
+
internalBlockMode: "coalesced",
|
|
1835
|
+
fg: dim ? COLOR.dim : void 0,
|
|
1836
|
+
bg: SURFACE.background,
|
|
1837
|
+
renderNode
|
|
1521
1838
|
});
|
|
1522
1839
|
});
|
|
1523
1840
|
MarkdownBlock.displayName = "MarkdownBlock";
|
|
@@ -1607,6 +1924,7 @@ function EditDiffBlock({ payload, dim }) {
|
|
|
1607
1924
|
...SURFACE.diff.contextBg ? { contextBg: SURFACE.diff.contextBg } : {},
|
|
1608
1925
|
addedSignColor: SURFACE.diff.addFg,
|
|
1609
1926
|
removedSignColor: SURFACE.diff.removeFg,
|
|
1927
|
+
treeSitterClient: getTreeSitterClient(),
|
|
1610
1928
|
...dim ? { fg: COLOR.dim } : {}
|
|
1611
1929
|
})]
|
|
1612
1930
|
});
|
|
@@ -2303,6 +2621,206 @@ function CompletionPopup({ state, visibleRows = 6 }) {
|
|
|
2303
2621
|
});
|
|
2304
2622
|
}
|
|
2305
2623
|
//#endregion
|
|
2624
|
+
//#region src/tui/file-edit-approval-modal.tsx
|
|
2625
|
+
const FILE_EDIT_TOOLS = new Set([
|
|
2626
|
+
"edit",
|
|
2627
|
+
"multi_edit",
|
|
2628
|
+
"write_file"
|
|
2629
|
+
]);
|
|
2630
|
+
function isFileEditTool(tool) {
|
|
2631
|
+
return FILE_EDIT_TOOLS.has(tool);
|
|
2632
|
+
}
|
|
2633
|
+
const ACTIONS = [
|
|
2634
|
+
{
|
|
2635
|
+
label: "Allow",
|
|
2636
|
+
decision: "accept-once",
|
|
2637
|
+
shortcut: "a"
|
|
2638
|
+
},
|
|
2639
|
+
{
|
|
2640
|
+
label: "Allow for session",
|
|
2641
|
+
decision: "accept-session",
|
|
2642
|
+
shortcut: "s"
|
|
2643
|
+
},
|
|
2644
|
+
{
|
|
2645
|
+
label: "Always allow",
|
|
2646
|
+
decision: "accept-safelist",
|
|
2647
|
+
shortcut: "p"
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
label: "Deny",
|
|
2651
|
+
decision: "deny",
|
|
2652
|
+
shortcut: "d",
|
|
2653
|
+
destructive: true
|
|
2654
|
+
}
|
|
2655
|
+
];
|
|
2656
|
+
function FileEditApprovalModal({ request, onDecide }) {
|
|
2657
|
+
const COLOR = useColors();
|
|
2658
|
+
const SURFACE = useSurfaces();
|
|
2659
|
+
const mdStyle = useMdStyle();
|
|
2660
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
2661
|
+
const modal = useModal();
|
|
2662
|
+
const targetPath = String(request.input.path ?? "");
|
|
2663
|
+
const [priorContent, priorError] = useMemo(() => {
|
|
2664
|
+
try {
|
|
2665
|
+
return [fs.readFileSync(targetPath, "utf8"), null];
|
|
2666
|
+
} catch (e) {
|
|
2667
|
+
if (e.code === "ENOENT") return ["", null];
|
|
2668
|
+
return ["", e.message];
|
|
2669
|
+
}
|
|
2670
|
+
}, [targetPath]);
|
|
2671
|
+
const payload = useMemo(() => {
|
|
2672
|
+
try {
|
|
2673
|
+
return extractEditPayload(request.tool, request.input, priorContent);
|
|
2674
|
+
} catch {
|
|
2675
|
+
return null;
|
|
2676
|
+
}
|
|
2677
|
+
}, [
|
|
2678
|
+
request.tool,
|
|
2679
|
+
request.input,
|
|
2680
|
+
priorContent
|
|
2681
|
+
]);
|
|
2682
|
+
const diffText = useMemo(() => payload ? buildContextualDiff(payload, priorContent, 6) : "", [payload, priorContent]);
|
|
2683
|
+
const [selected, setSelected] = useState(0);
|
|
2684
|
+
const filetype = useMemo(() => filetypeFromPath(targetPath), [targetPath]);
|
|
2685
|
+
const decide = useCallback((decision) => {
|
|
2686
|
+
onDecide(decision);
|
|
2687
|
+
modal.close();
|
|
2688
|
+
}, [onDecide, modal]);
|
|
2689
|
+
useKeyboard((key) => {
|
|
2690
|
+
if (key.name === "left") {
|
|
2691
|
+
setSelected((i) => (i - 1 + ACTIONS.length) % ACTIONS.length);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (key.name === "right" || key.name === "tab") {
|
|
2695
|
+
setSelected((i) => (i + 1) % ACTIONS.length);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (key.name === "return") {
|
|
2699
|
+
decide(ACTIONS[selected].decision);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (key.name === "escape") {
|
|
2703
|
+
decide("deny");
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const ch = (key.sequence ?? "").toLowerCase();
|
|
2707
|
+
const hit = ACTIONS.find((a) => a.shortcut === ch);
|
|
2708
|
+
if (hit) decide(hit.decision);
|
|
2709
|
+
});
|
|
2710
|
+
const useSplit = termWidth >= 100;
|
|
2711
|
+
const diffWidth = Math.min(termWidth - 6, 140);
|
|
2712
|
+
const diffHeight = Math.max(8, Math.min(28, diffText.split("\n").length + 2));
|
|
2713
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2714
|
+
title: " Permission Request ",
|
|
2715
|
+
bottomTitle: " ←/→ navigate · enter confirm · esc deny · a/s/d shortcuts ",
|
|
2716
|
+
style: {
|
|
2717
|
+
border: true,
|
|
2718
|
+
borderColor: COLOR.brand,
|
|
2719
|
+
backgroundColor: SURFACE.modal,
|
|
2720
|
+
padding: 1,
|
|
2721
|
+
flexDirection: "column",
|
|
2722
|
+
width: diffWidth + 6,
|
|
2723
|
+
flexShrink: 0
|
|
2724
|
+
},
|
|
2725
|
+
children: [
|
|
2726
|
+
/* @__PURE__ */ jsx("box", {
|
|
2727
|
+
style: {
|
|
2728
|
+
flexDirection: "row",
|
|
2729
|
+
height: 1,
|
|
2730
|
+
marginBottom: 1,
|
|
2731
|
+
flexShrink: 0
|
|
2732
|
+
},
|
|
2733
|
+
children: /* @__PURE__ */ jsxs("text", {
|
|
2734
|
+
wrapMode: "none",
|
|
2735
|
+
children: [
|
|
2736
|
+
/* @__PURE__ */ jsx("span", {
|
|
2737
|
+
fg: COLOR.brand,
|
|
2738
|
+
children: request.tool
|
|
2739
|
+
}),
|
|
2740
|
+
/* @__PURE__ */ jsx("span", {
|
|
2741
|
+
fg: COLOR.mute,
|
|
2742
|
+
children: " · "
|
|
2743
|
+
}),
|
|
2744
|
+
/* @__PURE__ */ jsx("span", {
|
|
2745
|
+
fg: COLOR.model,
|
|
2746
|
+
children: targetPath || "(no path)"
|
|
2747
|
+
})
|
|
2748
|
+
]
|
|
2749
|
+
})
|
|
2750
|
+
}),
|
|
2751
|
+
priorError ? /* @__PURE__ */ jsx("box", {
|
|
2752
|
+
style: {
|
|
2753
|
+
height: 3,
|
|
2754
|
+
flexShrink: 0,
|
|
2755
|
+
marginBottom: 1
|
|
2756
|
+
},
|
|
2757
|
+
children: /* @__PURE__ */ jsx("text", {
|
|
2758
|
+
fg: COLOR.error,
|
|
2759
|
+
wrapMode: "word",
|
|
2760
|
+
children: `Couldn't read ${targetPath}: ${priorError}`
|
|
2761
|
+
})
|
|
2762
|
+
}) : /* @__PURE__ */ jsx("box", {
|
|
2763
|
+
style: {
|
|
2764
|
+
border: true,
|
|
2765
|
+
borderColor: COLOR.mute,
|
|
2766
|
+
width: diffWidth + 2,
|
|
2767
|
+
height: diffHeight + 2,
|
|
2768
|
+
flexShrink: 0,
|
|
2769
|
+
marginBottom: 1
|
|
2770
|
+
},
|
|
2771
|
+
children: /* @__PURE__ */ jsx("diff", {
|
|
2772
|
+
diff: diffText,
|
|
2773
|
+
view: useSplit ? "split" : "unified",
|
|
2774
|
+
wrapMode: "word",
|
|
2775
|
+
showLineNumbers: true,
|
|
2776
|
+
syntaxStyle: mdStyle,
|
|
2777
|
+
treeSitterClient: getTreeSitterClient(),
|
|
2778
|
+
...filetype ? { filetype } : {},
|
|
2779
|
+
addedBg: SURFACE.diff.addBg,
|
|
2780
|
+
removedBg: SURFACE.diff.removeBg,
|
|
2781
|
+
...SURFACE.diff.addContentBg ? { addedContentBg: SURFACE.diff.addContentBg } : {},
|
|
2782
|
+
...SURFACE.diff.removeContentBg ? { removedContentBg: SURFACE.diff.removeContentBg } : {},
|
|
2783
|
+
addedSignColor: SURFACE.diff.addFg,
|
|
2784
|
+
removedSignColor: SURFACE.diff.removeFg,
|
|
2785
|
+
style: {
|
|
2786
|
+
width: diffWidth,
|
|
2787
|
+
height: diffHeight
|
|
2788
|
+
}
|
|
2789
|
+
})
|
|
2790
|
+
}),
|
|
2791
|
+
/* @__PURE__ */ jsx("box", {
|
|
2792
|
+
style: {
|
|
2793
|
+
flexDirection: "row",
|
|
2794
|
+
height: 1,
|
|
2795
|
+
flexShrink: 0
|
|
2796
|
+
},
|
|
2797
|
+
children: ACTIONS.map((action, i) => {
|
|
2798
|
+
const isSelected = i === selected;
|
|
2799
|
+
const tint = action.destructive ? COLOR.error : COLOR.brand;
|
|
2800
|
+
const labelText = ` ${action.label} (${action.shortcut}) `;
|
|
2801
|
+
return /* @__PURE__ */ jsx("box", {
|
|
2802
|
+
style: {
|
|
2803
|
+
marginRight: 2,
|
|
2804
|
+
flexShrink: 0
|
|
2805
|
+
},
|
|
2806
|
+
children: isSelected ? /* @__PURE__ */ jsx("text", {
|
|
2807
|
+
bg: tint,
|
|
2808
|
+
fg: SURFACE.background,
|
|
2809
|
+
wrapMode: "none",
|
|
2810
|
+
children: labelText
|
|
2811
|
+
}) : /* @__PURE__ */ jsx("text", {
|
|
2812
|
+
bg: SURFACE.selection,
|
|
2813
|
+
fg: COLOR.dim,
|
|
2814
|
+
wrapMode: "none",
|
|
2815
|
+
children: labelText
|
|
2816
|
+
})
|
|
2817
|
+
}, action.decision);
|
|
2818
|
+
})
|
|
2819
|
+
})
|
|
2820
|
+
]
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
//#endregion
|
|
2306
2824
|
//#region src/tui/interaction-block.tsx
|
|
2307
2825
|
const COMMENT_TEXTAREA_BINDINGS = [
|
|
2308
2826
|
...defaultTextareaKeyBindings.filter((b) => b.name !== "return" && !(b.name === "a" && b.ctrl && !b.shift && !b.meta)),
|
|
@@ -3584,7 +4102,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
3584
4102
|
if (cwdBudget < 6) return countSegs;
|
|
3585
4103
|
const segs = [{
|
|
3586
4104
|
text: compactPath(currentProjectRoot, cwdBudget),
|
|
3587
|
-
color: COLOR.
|
|
4105
|
+
color: COLOR.model
|
|
3588
4106
|
}];
|
|
3589
4107
|
if (showAllProjects) segs.push({
|
|
3590
4108
|
text: " · ",
|
|
@@ -3818,6 +4336,7 @@ function renderProjectLabel(rowProject, currentProject, COLOR) {
|
|
|
3818
4336
|
/** Visible content lines: 1 minimum, 5 maximum (textarea scrolls past 5). */
|
|
3819
4337
|
const MIN_CONTENT_LINES = 1;
|
|
3820
4338
|
const MAX_CONTENT_LINES = 5;
|
|
4339
|
+
const CWD_DISPLAY = compactPath(process.cwd());
|
|
3821
4340
|
/**
|
|
3822
4341
|
* Stable empty-array reference — keeps `queuedMessages` default referentially
|
|
3823
4342
|
* stable across renders so memoized children don't bust their deps. Declared
|
|
@@ -3830,6 +4349,8 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
|
|
|
3830
4349
|
const titleText = session?.title ?? "untitled";
|
|
3831
4350
|
const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction;
|
|
3832
4351
|
const userMessageCount = useMemo(() => events.filter((e) => e.kind === "user-prompt").length, [events]);
|
|
4352
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
4353
|
+
const hasStatusIcon = busy || compacting;
|
|
3833
4354
|
const metaSegments = useMemo(() => {
|
|
3834
4355
|
if (!session) return null;
|
|
3835
4356
|
const turnsSuffix = `turn${session.turnCount === 1 ? "" : "s"}`;
|
|
@@ -3857,15 +4378,28 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
|
|
|
3857
4378
|
text: "ctrl+x",
|
|
3858
4379
|
color: COLOR.warn
|
|
3859
4380
|
}, { text: " session" });
|
|
4381
|
+
const OVERLAY_RESERVED = 6;
|
|
4382
|
+
const iconReserve = hasStatusIcon ? 2 : 0;
|
|
4383
|
+
const statsLen = segments.reduce((sum, s) => sum + s.text.length, 0);
|
|
4384
|
+
const cwdBudget = Math.max(0, termWidth - 4 - titleText.length - iconReserve - OVERLAY_RESERVED - statsLen - 3);
|
|
4385
|
+
if (cwdBudget >= 6) segments.unshift({
|
|
4386
|
+
text: compactPath(CWD_DISPLAY, cwdBudget),
|
|
4387
|
+
color: COLOR.dim
|
|
4388
|
+
}, {
|
|
4389
|
+
text: " · ",
|
|
4390
|
+
color: COLOR.mute
|
|
4391
|
+
});
|
|
3860
4392
|
return segments;
|
|
3861
4393
|
}, [
|
|
3862
4394
|
session,
|
|
3863
4395
|
userMessageCount,
|
|
3864
4396
|
COLOR,
|
|
3865
|
-
showSessionShortcut
|
|
4397
|
+
showSessionShortcut,
|
|
4398
|
+
termWidth,
|
|
4399
|
+
titleText,
|
|
4400
|
+
hasStatusIcon
|
|
3866
4401
|
]);
|
|
3867
4402
|
const userPrompts = useMemo(() => events.filter((e) => e.kind === "user-prompt").map((e) => e.text), [events]);
|
|
3868
|
-
const titleStatusIcon = busy ? /* @__PURE__ */ jsx(StatusSpinner, { color: COLOR.warn }) : compacting ? /* @__PURE__ */ jsx(StatusSpinner, { color: COLOR.accent }) : null;
|
|
3869
4403
|
const [completionPopupOpen, setCompletionPopupOpen] = useState(false);
|
|
3870
4404
|
const handlePopupOpenChange = useCallback((open) => {
|
|
3871
4405
|
setCompletionPopupOpen(open);
|
|
@@ -3887,7 +4421,8 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
|
|
|
3887
4421
|
children: /* @__PURE__ */ jsx(Transcript, {
|
|
3888
4422
|
events,
|
|
3889
4423
|
settings,
|
|
3890
|
-
selectedTurnId: selectedTurnId ?? null
|
|
4424
|
+
selectedTurnId: selectedTurnId ?? null,
|
|
4425
|
+
busy: busy && !pending && !pendingInteraction
|
|
3891
4426
|
})
|
|
3892
4427
|
}),
|
|
3893
4428
|
pending ? /* @__PURE__ */ jsx(ApprovalBlock, {
|
|
@@ -3912,8 +4447,7 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
|
|
|
3912
4447
|
})] }),
|
|
3913
4448
|
/* @__PURE__ */ jsx(TitleOverlay, {
|
|
3914
4449
|
title: titleText,
|
|
3915
|
-
meta: metaSegments
|
|
3916
|
-
statusIcon: titleStatusIcon
|
|
4450
|
+
meta: metaSegments
|
|
3917
4451
|
})
|
|
3918
4452
|
]
|
|
3919
4453
|
});
|
|
@@ -3974,6 +4508,22 @@ function ApprovalBlock({ request, onPick }) {
|
|
|
3974
4508
|
const focused = useModalAwareFocus();
|
|
3975
4509
|
const COLOR = useColors();
|
|
3976
4510
|
const SELECT_THEME = useSelectStyle();
|
|
4511
|
+
const modal = useModal();
|
|
4512
|
+
const isFileEdit = isFileEditTool(request.tool);
|
|
4513
|
+
useEffect(() => {
|
|
4514
|
+
if (!isFileEdit) return;
|
|
4515
|
+
modal.open(/* @__PURE__ */ jsx(FileEditApprovalModal, {
|
|
4516
|
+
request,
|
|
4517
|
+
onDecide: onPick
|
|
4518
|
+
}));
|
|
4519
|
+
return () => modal.close();
|
|
4520
|
+
}, [
|
|
4521
|
+
isFileEdit,
|
|
4522
|
+
request,
|
|
4523
|
+
onPick,
|
|
4524
|
+
modal
|
|
4525
|
+
]);
|
|
4526
|
+
if (isFileEdit) return null;
|
|
3977
4527
|
const summary = useMemo(() => `${request.tool}(${formatApprovalArgs(request.input)})`, [request.tool, request.input]);
|
|
3978
4528
|
const options = useMemo(() => {
|
|
3979
4529
|
return [
|
|
@@ -3982,6 +4532,11 @@ function ApprovalBlock({ request, onPick }) {
|
|
|
3982
4532
|
description: "",
|
|
3983
4533
|
value: "accept-once"
|
|
3984
4534
|
},
|
|
4535
|
+
{
|
|
4536
|
+
name: "accept for session · auto-approve matching calls until you quit",
|
|
4537
|
+
description: "",
|
|
4538
|
+
value: "accept-session"
|
|
4539
|
+
},
|
|
3985
4540
|
{
|
|
3986
4541
|
name: `accept + remember · add "${suggestSafelistEntry(request.tool, request.input)}" to projects.json`,
|
|
3987
4542
|
description: "",
|
|
@@ -6464,6 +7019,10 @@ function AppShell() {
|
|
|
6464
7019
|
useEffect(() => {
|
|
6465
7020
|
persistToolResultsRef.current = settings.persistToolResults;
|
|
6466
7021
|
}, [settings.persistToolResults]);
|
|
7022
|
+
const allowInteractionRef = useRef(settings.allowInteraction);
|
|
7023
|
+
useEffect(() => {
|
|
7024
|
+
allowInteractionRef.current = settings.allowInteraction;
|
|
7025
|
+
}, [settings.allowInteraction]);
|
|
6467
7026
|
const autoCompactRef = useRef(settings.autoCompact);
|
|
6468
7027
|
useEffect(() => {
|
|
6469
7028
|
autoCompactRef.current = settings.autoCompact;
|
|
@@ -6472,6 +7031,10 @@ function AppShell() {
|
|
|
6472
7031
|
useEffect(() => {
|
|
6473
7032
|
autoCompactThresholdRef.current = settings.autoCompactThreshold;
|
|
6474
7033
|
}, [settings.autoCompactThreshold]);
|
|
7034
|
+
const smoothStreamingRef = useRef(settings.smoothStreaming);
|
|
7035
|
+
useEffect(() => {
|
|
7036
|
+
smoothStreamingRef.current = settings.smoothStreaming;
|
|
7037
|
+
}, [settings.smoothStreaming]);
|
|
6475
7038
|
const [projectDir] = useState(() => findGitRoot(process.cwd()) ?? process.cwd());
|
|
6476
7039
|
const safelistRef = useRef(null);
|
|
6477
7040
|
const readSafelist = useCallback(() => {
|
|
@@ -6481,6 +7044,7 @@ function AppShell() {
|
|
|
6481
7044
|
useEffect(() => {
|
|
6482
7045
|
safelistRef.current = null;
|
|
6483
7046
|
}, [dataDir, projectDir]);
|
|
7047
|
+
const sessionSafelistRef = useRef(/* @__PURE__ */ new Set());
|
|
6484
7048
|
const [skillsCatalog, setSkillsCatalog] = useState([]);
|
|
6485
7049
|
const [mcpsCatalog, setMcpsCatalog] = useState([]);
|
|
6486
7050
|
const [mcpsErrors, setMcpsErrors] = useState([]);
|
|
@@ -6576,9 +7140,11 @@ function AppShell() {
|
|
|
6576
7140
|
const gateDecision = useCallback(async (tool, input) => {
|
|
6577
7141
|
if (!safeModeEnabledRef.current) return true;
|
|
6578
7142
|
if (isOnSafelist(readSafelist(), tool, input)) return true;
|
|
7143
|
+
if (isOnSafelist([...sessionSafelistRef.current], tool, input)) return true;
|
|
6579
7144
|
const decision = await requestApproval(tool, input);
|
|
6580
7145
|
if (decision === "deny") return false;
|
|
6581
|
-
if (decision === "accept-
|
|
7146
|
+
if (decision === "accept-session") sessionSafelistRef.current.add(suggestSafelistEntry(tool, input));
|
|
7147
|
+
else if (decision === "accept-safelist") {
|
|
6582
7148
|
addToSafelist(dataDir, projectDir, suggestSafelistEntry(tool, input));
|
|
6583
7149
|
safelistRef.current = null;
|
|
6584
7150
|
}
|
|
@@ -6636,6 +7202,13 @@ function AppShell() {
|
|
|
6636
7202
|
/** Token count from the most recent assistant turn (caching-aware). */
|
|
6637
7203
|
const [lastInputTokens, setLastInputTokens] = useState(0);
|
|
6638
7204
|
/**
|
|
7205
|
+
* Cumulative USD cost across every run in the active session — seeded
|
|
7206
|
+
* from `session.runs` on activation and topped up by each `turn:after`'s
|
|
7207
|
+
* `usage.cost`. Only providers that report cost (currently OpenRouter)
|
|
7208
|
+
* contribute; everywhere else this stays at 0 and the footer hides it.
|
|
7209
|
+
*/
|
|
7210
|
+
const [sessionCost, setSessionCost] = useState(0);
|
|
7211
|
+
/**
|
|
6639
7212
|
* Synchronous mirror of {@link lastInputTokens} for callbacks that need
|
|
6640
7213
|
* to read the freshest value after `await agent.run()` resolves. The
|
|
6641
7214
|
* `turn:after` hook updates both the state (drives the footer) and this
|
|
@@ -6695,7 +7268,7 @@ function AppShell() {
|
|
|
6695
7268
|
* via `useEffect` once `triggerAutoCompactIfNeeded` is defined below.
|
|
6696
7269
|
*/
|
|
6697
7270
|
const triggerAutoCompactRef = useRef(() => {});
|
|
6698
|
-
const stream = useStreamBuffer(setEvents);
|
|
7271
|
+
const stream = useStreamBuffer(setEvents, { getSmooth: useCallback(() => smoothStreamingRef.current, []) });
|
|
6699
7272
|
const makePicked = useCallback((provider, modelId) => {
|
|
6700
7273
|
const descriptor = providerRegistry[provider.key];
|
|
6701
7274
|
if (!descriptor) return null;
|
|
@@ -6711,6 +7284,14 @@ function AppShell() {
|
|
|
6711
7284
|
model
|
|
6712
7285
|
};
|
|
6713
7286
|
}, [providerRegistry, initialState]);
|
|
7287
|
+
const cancelRunOnDenial = useCallback((reason) => {
|
|
7288
|
+
denyAll();
|
|
7289
|
+
interactions.cancelAll(reason);
|
|
7290
|
+
messageQueueRef.current = [];
|
|
7291
|
+
setMessageQueue([]);
|
|
7292
|
+
setQueueSelectionIndex(null);
|
|
7293
|
+
agentRef.current?.abort();
|
|
7294
|
+
}, [denyAll, interactions]);
|
|
6714
7295
|
const buildAgent = useCallback((session, key) => {
|
|
6715
7296
|
const descriptor = providerRegistry[key];
|
|
6716
7297
|
if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
|
|
@@ -6726,11 +7307,13 @@ function AppShell() {
|
|
|
6726
7307
|
discovered: mcpsCatalogRef.current,
|
|
6727
7308
|
enabled: enabledMcpsRef.current
|
|
6728
7309
|
});
|
|
6729
|
-
const
|
|
7310
|
+
const allowInteraction = allowInteractionRef.current !== false;
|
|
7311
|
+
const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
|
|
6730
7312
|
const actualCwd = process.cwd();
|
|
6731
7313
|
const envOpts = {
|
|
6732
7314
|
cwd: actualCwd,
|
|
6733
|
-
...projectDir !== actualCwd ? { projectRoot: projectDir } : {}
|
|
7315
|
+
...projectDir !== actualCwd ? { projectRoot: projectDir } : {},
|
|
7316
|
+
allowInteraction
|
|
6734
7317
|
};
|
|
6735
7318
|
const builtInSystem = profile.id === "build" ? buildBuildSystem(envOpts) : profile.id === "plan" ? buildPlanSystem(envOpts) : null;
|
|
6736
7319
|
const persistDir = resolvePersistDir({
|
|
@@ -6769,6 +7352,7 @@ function AppShell() {
|
|
|
6769
7352
|
if (!await gateDecision(name, input)) {
|
|
6770
7353
|
ctx.block = true;
|
|
6771
7354
|
ctx.reason = "User denied this tool call";
|
|
7355
|
+
cancelRunOnDenial("user denied a tool call");
|
|
6772
7356
|
}
|
|
6773
7357
|
};
|
|
6774
7358
|
agent.hooks.hook("tool:gate", (ctx) => applyGate(ctx.name, ctx.input, ctx));
|
|
@@ -6852,6 +7436,8 @@ function AppShell() {
|
|
|
6852
7436
|
const tokens = turnContextSize(usage);
|
|
6853
7437
|
setLastInputTokens(tokens);
|
|
6854
7438
|
lastInputTokensRef.current = tokens;
|
|
7439
|
+
const turnCost = usage.cost ?? 0;
|
|
7440
|
+
if (turnCost > 0) setSessionCost((prev) => prev + turnCost);
|
|
6855
7441
|
}
|
|
6856
7442
|
stream.flushAndUpdate(finalizeStreamingMarkdown);
|
|
6857
7443
|
});
|
|
@@ -6926,6 +7512,7 @@ function AppShell() {
|
|
|
6926
7512
|
providerRegistry,
|
|
6927
7513
|
stream,
|
|
6928
7514
|
gateDecision,
|
|
7515
|
+
cancelRunOnDenial,
|
|
6929
7516
|
projectDir,
|
|
6930
7517
|
config.prefix,
|
|
6931
7518
|
interactions,
|
|
@@ -6969,6 +7556,7 @@ function AppShell() {
|
|
|
6969
7556
|
setMessageQueue([]);
|
|
6970
7557
|
setQueueSelectionIndex(null);
|
|
6971
7558
|
runningRef.current = false;
|
|
7559
|
+
sessionSafelistRef.current.clear();
|
|
6972
7560
|
}, [
|
|
6973
7561
|
stream,
|
|
6974
7562
|
denyAll,
|
|
@@ -7050,6 +7638,7 @@ function AppShell() {
|
|
|
7050
7638
|
const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
|
|
7051
7639
|
setLastInputTokens(replayedTokens);
|
|
7052
7640
|
lastInputTokensRef.current = replayedTokens;
|
|
7641
|
+
setSessionCost(sumRunCosts(session.runs));
|
|
7053
7642
|
setCurrentSession({
|
|
7054
7643
|
id: session.id,
|
|
7055
7644
|
title: deriveSessionTitle(session.turns, session.metadata),
|
|
@@ -7160,13 +7749,12 @@ function AppShell() {
|
|
|
7160
7749
|
}, []);
|
|
7161
7750
|
const onAbort = useCallback(() => {
|
|
7162
7751
|
if (popupOpenRef.current) return;
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
}, [denyAll, interactions]);
|
|
7752
|
+
cancelRunOnDenial("user aborted run");
|
|
7753
|
+
}, [cancelRunOnDenial]);
|
|
7754
|
+
const onInteractionResolve = useCallback((response) => {
|
|
7755
|
+
interactions.resolveHead(response);
|
|
7756
|
+
if (response.kind === "plan" && response.decision === "reject") cancelRunOnDenial("user rejected a plan");
|
|
7757
|
+
}, [interactions, cancelRunOnDenial]);
|
|
7170
7758
|
const enterQueueSelection = useCallback(() => {
|
|
7171
7759
|
if (messageQueueRef.current.length === 0) return;
|
|
7172
7760
|
setQueueSelectionIndex(messageQueueRef.current.length - 1);
|
|
@@ -7621,6 +8209,7 @@ function AppShell() {
|
|
|
7621
8209
|
const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
|
|
7622
8210
|
setLastInputTokens(replayedTokens);
|
|
7623
8211
|
lastInputTokensRef.current = replayedTokens;
|
|
8212
|
+
setSessionCost(sumRunCosts(session.runs));
|
|
7624
8213
|
setCurrentSession((prev) => prev ? {
|
|
7625
8214
|
...prev,
|
|
7626
8215
|
turnCount: session.turns.length,
|
|
@@ -8044,6 +8633,7 @@ function AppShell() {
|
|
|
8044
8633
|
}
|
|
8045
8634
|
if (key.name !== "escape") return;
|
|
8046
8635
|
if (busy || pendingApproval) return onAbort();
|
|
8636
|
+
if (popupOpenRef.current) return;
|
|
8047
8637
|
if (screen === "chat") return onOpenSessions();
|
|
8048
8638
|
if (screen === "sessions") {
|
|
8049
8639
|
if (currentSession) setScreen("chat");
|
|
@@ -8133,13 +8723,15 @@ function AppShell() {
|
|
|
8133
8723
|
lastInputTokens,
|
|
8134
8724
|
providerRegistry
|
|
8135
8725
|
]);
|
|
8726
|
+
const footerCost = screen === "chat" ? sessionCost : null;
|
|
8136
8727
|
useEffect(() => () => {
|
|
8137
8728
|
teardown();
|
|
8138
8729
|
}, [teardown]);
|
|
8139
8730
|
return /* @__PURE__ */ jsxs("box", {
|
|
8140
8731
|
style: {
|
|
8141
8732
|
flexDirection: "column",
|
|
8142
|
-
flexGrow: 1
|
|
8733
|
+
flexGrow: 1,
|
|
8734
|
+
backgroundColor: SURFACE.background
|
|
8143
8735
|
},
|
|
8144
8736
|
children: [/* @__PURE__ */ jsxs("box", {
|
|
8145
8737
|
style: {
|
|
@@ -8174,7 +8766,7 @@ function AppShell() {
|
|
|
8174
8766
|
pending: pendingApproval,
|
|
8175
8767
|
onApproval: resolveHead,
|
|
8176
8768
|
pendingInteraction,
|
|
8177
|
-
onInteraction:
|
|
8769
|
+
onInteraction: onInteractionResolve,
|
|
8178
8770
|
completionProviders,
|
|
8179
8771
|
onPopupOpenChange,
|
|
8180
8772
|
selectedTurnId,
|
|
@@ -8183,7 +8775,9 @@ function AppShell() {
|
|
|
8183
8775
|
]
|
|
8184
8776
|
}), /* @__PURE__ */ jsx(Footer, {
|
|
8185
8777
|
hints,
|
|
8186
|
-
context: contextUsage
|
|
8778
|
+
context: contextUsage,
|
|
8779
|
+
cost: footerCost,
|
|
8780
|
+
status: pendingInteraction?.kind === "question" ? "asking" : busy ? "busy" : compacting ? "compacting" : null
|
|
8187
8781
|
})]
|
|
8188
8782
|
});
|
|
8189
8783
|
}
|
|
@@ -8359,6 +8953,18 @@ function shortChord(spec) {
|
|
|
8359
8953
|
* https://opentui.com/docs/reference/tree-sitter/#use-local-files).
|
|
8360
8954
|
* - If a download fails (offline / firewall), the language renders as
|
|
8361
8955
|
* plain `markup.raw.block` — no crash, just no syntax color.
|
|
8956
|
+
* - **Self-hosted wasm.** Languages whose upstream grammars don't ship
|
|
8957
|
+
* `.wasm` releases (currently: SQL) are built ahead of time and
|
|
8958
|
+
* vendored under `tui/parsers/`; the TUI binary embeds the file via
|
|
8959
|
+
* `with { type: 'file' }` (see `tui/src/tree-sitter-bundle.ts`),
|
|
8960
|
+
* extracts it to a tmpdir at startup, and points the SDK at the
|
|
8961
|
+
* extracted path through a `ZIDANE_LOCAL_TREE_SITTER_<X>_WASM` env
|
|
8962
|
+
* var — keeps the repo's privacy boundary intact (no
|
|
8963
|
+
* `raw.githubusercontent.com` fetches) and works offline / inside
|
|
8964
|
+
* air-gapped runners. SDK consumers without these env vars set just
|
|
8965
|
+
* skip the affected grammars. See `tui/parsers/README.md` for the
|
|
8966
|
+
* rebuild recipe and `LOCAL_PARSERS` below for the env-var
|
|
8967
|
+
* contract.
|
|
8362
8968
|
*
|
|
8363
8969
|
* Versions are pinned in the WASM URLs so a grammar repo's `master`
|
|
8364
8970
|
* landing a breaking change can't silently affect us.
|
|
@@ -8413,8 +9019,71 @@ const EXTRA_PARSERS = [
|
|
|
8413
9019
|
filetype: "css",
|
|
8414
9020
|
wasm: "https://github.com/tree-sitter/tree-sitter-css/releases/download/v0.23.2/tree-sitter-css.wasm",
|
|
8415
9021
|
queries: { highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-css/v0.23.2/queries/highlights.scm"] }
|
|
9022
|
+
},
|
|
9023
|
+
{
|
|
9024
|
+
filetype: "haskell",
|
|
9025
|
+
aliases: ["hs"],
|
|
9026
|
+
wasm: "https://github.com/tree-sitter/tree-sitter-haskell/releases/download/v0.23.1/tree-sitter-haskell.wasm",
|
|
9027
|
+
queries: { highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-haskell/v0.23.1/queries/highlights.scm"] }
|
|
9028
|
+
},
|
|
9029
|
+
{
|
|
9030
|
+
filetype: "c",
|
|
9031
|
+
wasm: "https://github.com/tree-sitter/tree-sitter-c/releases/download/v0.24.1/tree-sitter-c.wasm",
|
|
9032
|
+
queries: { highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-c/v0.24.1/queries/highlights.scm"] }
|
|
9033
|
+
},
|
|
9034
|
+
{
|
|
9035
|
+
filetype: "cpp",
|
|
9036
|
+
aliases: [
|
|
9037
|
+
"c++",
|
|
9038
|
+
"cxx",
|
|
9039
|
+
"cc"
|
|
9040
|
+
],
|
|
9041
|
+
wasm: "https://github.com/tree-sitter/tree-sitter-cpp/releases/download/v0.23.4/tree-sitter-cpp.wasm",
|
|
9042
|
+
queries: { highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-cpp/v0.23.4/queries/highlights.scm"] }
|
|
9043
|
+
},
|
|
9044
|
+
{
|
|
9045
|
+
filetype: "csharp",
|
|
9046
|
+
aliases: ["cs", "c#"],
|
|
9047
|
+
wasm: "https://github.com/tree-sitter/tree-sitter-c-sharp/releases/download/v0.23.1/tree-sitter-c_sharp.wasm",
|
|
9048
|
+
queries: { highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-c-sharp/v0.23.1/queries/highlights.scm"] }
|
|
9049
|
+
},
|
|
9050
|
+
{
|
|
9051
|
+
filetype: "elixir",
|
|
9052
|
+
aliases: ["ex", "exs"],
|
|
9053
|
+
wasm: "https://github.com/elixir-lang/tree-sitter-elixir/releases/download/v0.3.4/tree-sitter-elixir.wasm",
|
|
9054
|
+
queries: { highlights: ["https://raw.githubusercontent.com/elixir-lang/tree-sitter-elixir/v0.3.4/queries/highlights.scm"] }
|
|
8416
9055
|
}
|
|
8417
9056
|
];
|
|
9057
|
+
const LOCAL_PARSERS = [{
|
|
9058
|
+
template: {
|
|
9059
|
+
filetype: "sql",
|
|
9060
|
+
aliases: [
|
|
9061
|
+
"psql",
|
|
9062
|
+
"mysql",
|
|
9063
|
+
"sqlite",
|
|
9064
|
+
"plsql"
|
|
9065
|
+
],
|
|
9066
|
+
queries: { highlights: ["https://raw.githubusercontent.com/DerekStride/tree-sitter-sql/v0.3.11/queries/highlights.scm"] }
|
|
9067
|
+
},
|
|
9068
|
+
pathEnvVar: "ZIDANE_LOCAL_TREE_SITTER_SQL_WASM"
|
|
9069
|
+
}];
|
|
9070
|
+
/**
|
|
9071
|
+
* Walk {@link LOCAL_PARSERS} and return the parsers whose `pathEnvVar`
|
|
9072
|
+
* resolves to a non-empty string. Stripped entries log a one-shot warning
|
|
9073
|
+
* to stderr so a TUI host with broken bundle setup doesn't fail
|
|
9074
|
+
* silently.
|
|
9075
|
+
*/
|
|
9076
|
+
function resolveLocalParsers() {
|
|
9077
|
+
const resolved = [];
|
|
9078
|
+
for (const { template, pathEnvVar } of LOCAL_PARSERS) {
|
|
9079
|
+
const wasm = process.env[pathEnvVar];
|
|
9080
|
+
if (wasm && wasm.length > 0) resolved.push({
|
|
9081
|
+
...template,
|
|
9082
|
+
wasm
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
return resolved;
|
|
9086
|
+
}
|
|
8418
9087
|
let registered = false;
|
|
8419
9088
|
/**
|
|
8420
9089
|
* Register the extra Tree-sitter parsers + start the worker. Idempotent —
|
|
@@ -8424,7 +9093,7 @@ let registered = false;
|
|
|
8424
9093
|
async function setupTreeSitter() {
|
|
8425
9094
|
if (registered) return;
|
|
8426
9095
|
registered = true;
|
|
8427
|
-
addDefaultParsers(EXTRA_PARSERS);
|
|
9096
|
+
addDefaultParsers([...EXTRA_PARSERS, ...resolveLocalParsers()]);
|
|
8428
9097
|
await getTreeSitterClient().initialize();
|
|
8429
9098
|
}
|
|
8430
9099
|
//#endregion
|