cvc-tui 0.4.4 → 0.4.7

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.
Files changed (142) hide show
  1. package/NOTICES.md +13 -0
  2. package/dist/app/completion.js +102 -0
  3. package/dist/app/createGatewayEventHandler.js +508 -0
  4. package/dist/app/createSlashHandler.js +101 -0
  5. package/dist/app/delegationStore.js +51 -0
  6. package/dist/app/gatewayContext.js +17 -0
  7. package/dist/app/historyStore.js +123 -0
  8. package/dist/app/inputBuffer.js +120 -0
  9. package/dist/app/inputSelectionStore.js +8 -0
  10. package/dist/app/inputStore.js +28 -0
  11. package/dist/app/interfaces.js +6 -0
  12. package/dist/app/overlayStore.js +40 -0
  13. package/dist/app/promptStore.js +44 -0
  14. package/dist/app/queueStore.js +25 -0
  15. package/dist/app/scroll.js +44 -0
  16. package/dist/app/setupHandoff.js +28 -0
  17. package/dist/app/slash/commands/core.js +479 -0
  18. package/dist/app/slash/commands/debug.js +44 -0
  19. package/dist/app/slash/commands/ops.js +512 -0
  20. package/dist/app/slash/commands/session.js +431 -0
  21. package/dist/app/slash/commands/setup.js +20 -0
  22. package/dist/app/slash/commands/toggles.js +40 -0
  23. package/dist/app/slash/registry.js +18 -0
  24. package/dist/app/slash/types.js +1 -0
  25. package/dist/app/spawnHistoryStore.js +105 -0
  26. package/dist/app/turnController.js +650 -0
  27. package/dist/app/turnStore.js +48 -0
  28. package/dist/app/uiStore.js +36 -0
  29. package/dist/app/useComposerState.js +265 -0
  30. package/dist/app/useConfigSync.js +144 -0
  31. package/dist/app/useInputHandlers.js +403 -0
  32. package/dist/app/useLongRunToolCharms.js +50 -0
  33. package/dist/app/useMainApp.js +638 -0
  34. package/dist/app/useSessionLifecycle.js +175 -0
  35. package/dist/app/useSubmission.js +287 -0
  36. package/dist/app.js +15 -0
  37. package/dist/banner.js +63 -0
  38. package/dist/components/agentsOverlay.js +474 -0
  39. package/dist/components/appChrome.js +252 -0
  40. package/dist/components/appLayout.js +122 -0
  41. package/dist/components/appOverlays.js +65 -0
  42. package/dist/components/branding.js +97 -0
  43. package/dist/components/fpsOverlay.js +22 -0
  44. package/dist/components/helpHint.js +21 -0
  45. package/dist/components/markdown.js +501 -0
  46. package/dist/components/maskedPrompt.js +12 -0
  47. package/dist/components/messageLine.js +82 -0
  48. package/dist/components/modelPicker.js +254 -0
  49. package/dist/components/overlayControls.js +30 -0
  50. package/dist/components/overlays/confirmPrompt.js +25 -0
  51. package/dist/components/overlays/helpOverlay.js +76 -0
  52. package/dist/components/overlays/historySearch.js +49 -0
  53. package/dist/components/overlays/modelPicker.js +60 -0
  54. package/dist/components/overlays/overlayUtils.js +19 -0
  55. package/dist/components/overlays/secretPrompt.js +36 -0
  56. package/dist/components/overlays/sessionPicker.js +93 -0
  57. package/dist/components/overlays/skillsHub.js +71 -0
  58. package/dist/components/prompts.js +95 -0
  59. package/dist/components/queuedMessages.js +24 -0
  60. package/dist/components/sessionPicker.js +130 -0
  61. package/dist/components/skillsHub.js +165 -0
  62. package/dist/components/streamingAssistant.js +35 -0
  63. package/dist/components/streamingMarkdown.js +144 -0
  64. package/dist/components/textInput.js +794 -0
  65. package/dist/components/themed.js +12 -0
  66. package/dist/components/thinking.js +496 -0
  67. package/dist/components/todoPanel.js +40 -0
  68. package/dist/components/transcript.js +22 -0
  69. package/dist/config/env.js +18 -0
  70. package/dist/config/limits.js +22 -0
  71. package/dist/config/timing.js +25 -0
  72. package/dist/content/charms.js +5 -0
  73. package/dist/content/faces.js +21 -0
  74. package/dist/content/fortunes.js +29 -0
  75. package/dist/content/hotkeys.js +38 -0
  76. package/dist/content/placeholders.js +15 -0
  77. package/dist/content/setup.js +14 -0
  78. package/dist/content/verbs.js +41 -0
  79. package/dist/domain/details.js +53 -0
  80. package/dist/domain/messages.js +63 -0
  81. package/dist/domain/paths.js +16 -0
  82. package/dist/domain/providers.js +11 -0
  83. package/dist/domain/roles.js +6 -0
  84. package/dist/domain/slash.js +11 -0
  85. package/dist/domain/usage.js +1 -0
  86. package/dist/domain/viewport.js +33 -0
  87. package/dist/entry.js +64 -70236
  88. package/dist/gateway/client.js +312 -0
  89. package/dist/gatewayClient.js +574 -0
  90. package/dist/gatewayTypes.js +1 -0
  91. package/dist/hooks/useCompletion.js +86 -0
  92. package/dist/hooks/useGitBranch.js +58 -0
  93. package/dist/hooks/useInputHistory.js +12 -0
  94. package/dist/hooks/useQueue.js +57 -0
  95. package/dist/hooks/useVirtualHistory.js +401 -0
  96. package/dist/lib/circularBuffer.js +43 -0
  97. package/dist/lib/clipboard.js +126 -0
  98. package/dist/lib/editor.js +41 -0
  99. package/dist/lib/editor.test.js +58 -0
  100. package/dist/lib/emoji.js +49 -0
  101. package/dist/lib/externalCli.js +11 -0
  102. package/dist/lib/forceTruecolor.js +26 -0
  103. package/dist/lib/fpsStore.js +36 -0
  104. package/dist/lib/gracefulExit.js +29 -0
  105. package/dist/lib/history.js +69 -0
  106. package/dist/lib/inputMetrics.js +143 -0
  107. package/dist/lib/liveProgress.js +51 -0
  108. package/dist/lib/liveProgress.test.js +89 -0
  109. package/dist/lib/localSessionInfo.js +116 -0
  110. package/dist/lib/mathUnicode.js +685 -0
  111. package/dist/lib/memory.js +123 -0
  112. package/dist/lib/memoryMonitor.js +76 -0
  113. package/dist/lib/messages.js +3 -0
  114. package/dist/lib/messages.test.js +25 -0
  115. package/dist/lib/osc52.js +53 -0
  116. package/dist/lib/perfPane.js +94 -0
  117. package/dist/lib/platform.js +312 -0
  118. package/dist/lib/precisionWheel.js +25 -0
  119. package/dist/lib/react-devtools-stub.js +12 -0
  120. package/dist/lib/reasoning.js +39 -0
  121. package/dist/lib/rpc.js +26 -0
  122. package/dist/lib/subagentTree.js +287 -0
  123. package/dist/lib/syntax.js +89 -0
  124. package/dist/lib/terminalModes.js +46 -0
  125. package/dist/lib/terminalParity.js +48 -0
  126. package/dist/lib/terminalSetup.js +321 -0
  127. package/dist/lib/text.js +203 -0
  128. package/dist/lib/text.test.js +18 -0
  129. package/dist/lib/todo.js +2 -0
  130. package/dist/lib/todo.test.js +22 -0
  131. package/dist/lib/viewportStore.js +82 -0
  132. package/dist/lib/virtualHeights.js +61 -0
  133. package/dist/lib/wheelAccel.js +143 -0
  134. package/dist/protocol/interpolation.js +4 -0
  135. package/dist/protocol/paste.js +3 -0
  136. package/dist/theme.js +398 -0
  137. package/dist/types.js +1 -0
  138. package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
  139. package/dist/vendor/cvc-ink/index.js +1 -0
  140. package/dist/vendor/cvc-ink/package.json +9 -0
  141. package/dist/vendor/cvc-ink/text-input.js +1 -0
  142. package/package.json +9 -9
@@ -0,0 +1,252 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ import { Box, Text } from '../vendor/cvc-ink/index.js';
7
+ import { useStore } from '@nanostores/react';
8
+ import { useEffect, useMemo, useRef, useState } from 'react';
9
+ import unicodeSpinners from 'unicode-animations';
10
+ import { $delegationState } from '../app/delegationStore.js';
11
+ import { useTurnSelector } from '../app/turnStore.js';
12
+ import { $uiState } from '../app/uiStore.js';
13
+ import { FACES } from '../content/faces.js';
14
+ import { VERBS } from '../content/verbs.js';
15
+ import { fmtDuration } from '../domain/messages.js';
16
+ import { stickyPromptFromViewport } from '../domain/viewport.js';
17
+ import { buildSubagentTree, treeTotals, widthByDepth } from '../lib/subagentTree.js';
18
+ import { fmtK } from '../lib/text.js';
19
+ import { useScrollbarSnapshot, useViewportSnapshot } from '../lib/viewportStore.js';
20
+ const FACE_TICK_MS = 2500;
21
+ const HEART_COLORS = ['#ff5fa2', '#ff4d6d'];
22
+ // Keep verb segment width stable so status-bar content to the right doesn't
23
+ // jitter when the ticker rotates between short/long verbs.
24
+ export const VERB_PAD_LEN = VERBS.reduce((max, v) => Math.max(max, v.length), 0) + 1; // + ellipsis
25
+ export const padVerb = (verb) => `${verb}…`.padEnd(VERB_PAD_LEN, ' ');
26
+ // Compact alternates for the `emoji` and `ascii` indicator styles.
27
+ // Each entry is a fixed-width (display-width) glyph.
28
+ const EMOJI_FRAMES = ['⚕ ', '🌀', '🤔', '✨', '🍵', '🔮'];
29
+ const ASCII_FRAMES = ['|', '/', '-', '\\'];
30
+ // Faster tick for spinner-style indicators — they read as motion only
31
+ // at frame rates closer to their authored interval.
32
+ const SPINNER_TICK_MS = 100;
33
+ const renderIndicator = (style, tick) => {
34
+ if (style === 'kaomoji') {
35
+ return { frame: FACES[tick % FACES.length] ?? '', intervalMs: FACE_TICK_MS, showVerb: true };
36
+ }
37
+ if (style === 'emoji') {
38
+ return {
39
+ frame: EMOJI_FRAMES[tick % EMOJI_FRAMES.length] ?? '⚕ ',
40
+ intervalMs: SPINNER_TICK_MS * 6,
41
+ showVerb: true
42
+ };
43
+ }
44
+ if (style === 'ascii') {
45
+ return {
46
+ frame: ASCII_FRAMES[tick % ASCII_FRAMES.length] ?? '|',
47
+ intervalMs: SPINNER_TICK_MS,
48
+ showVerb: true
49
+ };
50
+ }
51
+ // 'unicode' — braille spinner (fixed 1-col). Authored interval is
52
+ // ~80ms; honour it but bound below at a safe minimum so React
53
+ // re-renders stay reasonable. This style is for users who want
54
+ // the cleanest possible status, so no verb rotation either.
55
+ const spinner = unicodeSpinners.braille;
56
+ const frame = spinner.frames[tick % spinner.frames.length] ?? '⠋';
57
+ return { frame, intervalMs: Math.max(SPINNER_TICK_MS, spinner.interval), showVerb: false };
58
+ };
59
+ function FaceTicker({ color, startedAt }) {
60
+ const ui = useStore($uiState);
61
+ const style = ui.indicatorStyle;
62
+ const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000));
63
+ const [verbTick, setVerbTick] = useState(() => Math.floor(Math.random() * VERBS.length));
64
+ const [now, setNow] = useState(() => Date.now());
65
+ // Pre-compute cadence + verb-visibility for the active style so an
66
+ // `/indicator` switch re-arms the interval (and skips the verb timer
67
+ // for verb-less styles like `unicode`) without leaving the previous
68
+ // timer dangling.
69
+ const { intervalMs, showVerb } = renderIndicator(style, 0);
70
+ useEffect(() => {
71
+ const glyph = setInterval(() => setTick(n => n + 1), intervalMs);
72
+ const clock = setInterval(() => setNow(Date.now()), 1000);
73
+ // Verb timer is gated on `showVerb` — `unicode` style hides the verb
74
+ // entirely, so cycling `verbTick` would be an avoidable re-render.
75
+ const verb = showVerb ? setInterval(() => setVerbTick(n => n + 1), FACE_TICK_MS) : null;
76
+ return () => {
77
+ clearInterval(glyph);
78
+ clearInterval(clock);
79
+ if (verb !== null) {
80
+ clearInterval(verb);
81
+ }
82
+ };
83
+ }, [intervalMs, showVerb]);
84
+ const { frame } = renderIndicator(style, tick);
85
+ const verb = VERBS[verbTick % VERBS.length] ?? '';
86
+ const verbSegment = showVerb ? ` ${padVerb(verb)}` : '';
87
+ // Leading space keeps a gap between the frame and the duration when the
88
+ // verb segment is hidden (e.g. `unicode` spinner style). When the verb
89
+ // IS shown, its trailing padding already provides the gap, so the extra
90
+ // space is harmless.
91
+ const durationSegment = startedAt ? ` · ${fmtDuration(now - startedAt)}` : '';
92
+ return (_jsxs(Text, { color: color, children: [frame, verbSegment, durationSegment] }));
93
+ }
94
+ function ctxBarColor(pct, t) {
95
+ if (pct == null) {
96
+ return t.color.muted;
97
+ }
98
+ if (pct >= 95) {
99
+ return t.color.statusCritical;
100
+ }
101
+ if (pct > 80) {
102
+ return t.color.statusBad;
103
+ }
104
+ if (pct >= 50) {
105
+ return t.color.statusWarn;
106
+ }
107
+ return t.color.statusGood;
108
+ }
109
+ function ctxBar(pct, w = 10) {
110
+ const p = Math.max(0, Math.min(100, pct ?? 0));
111
+ const filled = Math.round((p / 100) * w);
112
+ return '█'.repeat(filled) + '░'.repeat(w - filled);
113
+ }
114
+ function SpawnHud({ t }) {
115
+ // Tight HUD that only appears when the session is actually fanning out.
116
+ // Colour escalates to warn/error as depth or concurrency approaches the cap.
117
+ const delegation = useStore($delegationState);
118
+ const subagents = useTurnSelector(state => state.subagents);
119
+ const tree = useMemo(() => buildSubagentTree(subagents), [subagents]);
120
+ const totals = useMemo(() => treeTotals(tree), [tree]);
121
+ if (!totals.descendantCount && !delegation.paused) {
122
+ return null;
123
+ }
124
+ const maxDepth = delegation.maxSpawnDepth;
125
+ const maxConc = delegation.maxConcurrentChildren;
126
+ const depth = Math.max(0, totals.maxDepthFromHere);
127
+ const active = totals.activeCount;
128
+ // `max_concurrent_children` is a per-parent cap, not a global one.
129
+ // `activeCount` sums every running agent across the tree and would
130
+ // over-warn for multi-orchestrator runs. The widest level of the tree
131
+ // is a closer proxy to "most concurrent spawns that could be hitting a
132
+ // single parent's slot budget".
133
+ const widestLevel = widthByDepth(tree).reduce((a, b) => Math.max(a, b), 0);
134
+ const depthRatio = maxDepth ? depth / maxDepth : 0;
135
+ const concRatio = maxConc ? widestLevel / maxConc : 0;
136
+ const ratio = Math.max(depthRatio, concRatio);
137
+ const color = delegation.paused || ratio >= 1 ? t.color.error : ratio >= 0.66 ? t.color.warn : t.color.muted;
138
+ const pieces = [];
139
+ if (delegation.paused) {
140
+ pieces.push('⏸ paused');
141
+ }
142
+ if (totals.descendantCount > 0) {
143
+ const depthLabel = maxDepth ? `${depth}/${maxDepth}` : `${depth}`;
144
+ pieces.push(`d${depthLabel}`);
145
+ if (active > 0) {
146
+ // Label pairs the widest-level count (drives concRatio above) with
147
+ // the total active count for context. `W/cap` triggers the warn,
148
+ // `+N` is everything else currently running across the tree.
149
+ const extra = Math.max(0, active - widestLevel);
150
+ const widthLabel = maxConc ? `${widestLevel}/${maxConc}` : `${widestLevel}`;
151
+ const suffix = extra > 0 ? `+${extra}` : '';
152
+ pieces.push(`⚡${widthLabel}${suffix}`);
153
+ }
154
+ }
155
+ const atCap = depthRatio >= 1 || concRatio >= 1;
156
+ return (_jsxs(Text, { color: color, children: [atCap ? ' │ ⚠ ' : ' │ ', pieces.join(' ')] }));
157
+ }
158
+ function SessionDuration({ startedAt }) {
159
+ const [now, setNow] = useState(() => Date.now());
160
+ useEffect(() => {
161
+ setNow(Date.now());
162
+ const id = setInterval(() => setNow(Date.now()), 1000);
163
+ return () => clearInterval(id);
164
+ }, [startedAt]);
165
+ return fmtDuration(now - startedAt);
166
+ }
167
+ const effortLabel = (effort) => {
168
+ const value = String(effort ?? '')
169
+ .trim()
170
+ .toLowerCase();
171
+ return value && value !== 'medium' && value !== 'normal' && value !== 'default' ? value : '';
172
+ };
173
+ const shortModelLabel = (model) => model
174
+ .split('/')
175
+ .pop()
176
+ .replace(/^claude[-_]/, '')
177
+ .replace(/^anthropic[-_]/, '')
178
+ .replace(/[-_]/g, ' ')
179
+ .replace(/\b(\d+)\s+(\d+)\b/g, '$1.$2')
180
+ .trim();
181
+ const modelLabel = (model, effort, fast) => [shortModelLabel(model), effortLabel(effort), fast ? 'fast' : ''].filter(Boolean).join(' ');
182
+ export function GoodVibesHeart({ tick, t }) {
183
+ const [active, setActive] = useState(false);
184
+ const [color, setColor] = useState(t.color.accent);
185
+ useEffect(() => {
186
+ if (tick <= 0) {
187
+ return;
188
+ }
189
+ const palette = [t.color.error, t.color.warn, t.color.accent];
190
+ setColor(palette[Math.floor(Math.random() * palette.length)]);
191
+ setActive(true);
192
+ const id = setTimeout(() => setActive(false), 650);
193
+ return () => clearTimeout(id);
194
+ }, [t.color.accent, tick]);
195
+ if (!active) {
196
+ return null;
197
+ }
198
+ return _jsx(Text, { color: color, children: "\u2665" });
199
+ }
200
+ export function StatusRule({ cwdLabel, cols, busy, status, statusColor, model, modelFast, modelReasoningEffort, usage, bgCount, sessionStartedAt, showCost, turnStartedAt, voiceLabel, t }) {
201
+ const pct = usage.context_percent;
202
+ const barColor = ctxBarColor(pct, t);
203
+ const ctxLabel = usage.context_max
204
+ ? `${fmtK(usage.context_used ?? 0)}/${fmtK(usage.context_max)}`
205
+ : usage.total > 0
206
+ ? `${fmtK(usage.total)} tok`
207
+ : '';
208
+ const bar = usage.context_max ? ctxBar(pct) : '';
209
+ const leftWidth = Math.max(12, cols - cwdLabel.length - 3);
210
+ return (_jsxs(Box, { height: 1, children: [_jsx(Box, { flexShrink: 1, width: leftWidth, children: _jsxs(Text, { color: t.color.border, wrap: "truncate-end", children: ['─ ', busy ? (_jsx(FaceTicker, { color: statusColor, startedAt: turnStartedAt })) : (_jsx(Text, { color: statusColor, children: status })), _jsxs(Text, { color: t.color.muted, children: [" \u2502 ", modelLabel(model, modelReasoningEffort, modelFast)] }), ctxLabel ? _jsxs(Text, { color: t.color.muted, children: [" \u2502 ", ctxLabel] }) : null, bar ? (_jsxs(Text, { color: t.color.muted, children: [' │ ', _jsxs(Text, { color: barColor, children: ["[", bar, "]"] }), " ", _jsx(Text, { color: barColor, children: pct != null ? `${pct}%` : '' })] })) : null, sessionStartedAt ? (_jsxs(Text, { color: t.color.muted, children: [' │ ', _jsx(SessionDuration, { startedAt: sessionStartedAt })] })) : null, typeof usage.compressions === 'number' && usage.compressions > 0 ? (_jsxs(Text, { color: t.color.muted, children: [' │ ', _jsxs(Text, { color: usage.compressions >= 10 ? t.color.error : usage.compressions >= 5 ? t.color.warn : t.color.muted, children: ["cmp ", usage.compressions] })] })) : null, _jsx(SpawnHud, { t: t }), voiceLabel ? (_jsxs(Text, { color: voiceLabel.startsWith('●') ? t.color.error : voiceLabel.startsWith('◉') ? t.color.warn : t.color.muted, children: [' │ ', voiceLabel] })) : null, bgCount > 0 ? _jsxs(Text, { color: t.color.muted, children: [" \u2502 ", bgCount, " bg"] }) : null, showCost && typeof usage.cost_usd === 'number' ? (_jsxs(Text, { color: t.color.muted, children: [" \u2502 $", usage.cost_usd.toFixed(4)] })) : null] }) }), _jsx(Text, { color: t.color.border, children: " \u2500 " }), _jsx(Text, { color: t.color.label, children: cwdLabel })] }));
211
+ }
212
+ export function FloatBox({ children, color }) {
213
+ return (_jsx(Box, { alignSelf: "flex-start", borderColor: color, borderStyle: "double", flexDirection: "column", marginTop: 1, opaque: true, paddingX: 1, children: children }));
214
+ }
215
+ export function StickyPromptTracker({ messages, offsets, scrollRef, onChange }) {
216
+ const { atBottom, bottom, top } = useViewportSnapshot(scrollRef);
217
+ const text = stickyPromptFromViewport(messages, offsets, top, bottom, atBottom);
218
+ useEffect(() => onChange(text), [onChange, text]);
219
+ return null;
220
+ }
221
+ export function TranscriptScrollbar({ scrollRef, t }) {
222
+ const [hover, setHover] = useState(false);
223
+ const [grab, setGrab] = useState(null);
224
+ const grabRef = useRef(null);
225
+ const { scrollHeight: total, top: pos, viewportHeight: vp } = useScrollbarSnapshot(scrollRef);
226
+ if (!vp) {
227
+ return _jsx(Box, { width: 1 });
228
+ }
229
+ const s = scrollRef.current;
230
+ const scrollable = total > vp;
231
+ const thumb = scrollable ? Math.max(1, Math.round((vp * vp) / total)) : vp;
232
+ const travel = Math.max(1, vp - thumb);
233
+ const thumbTop = scrollable ? Math.round((pos / Math.max(1, total - vp)) * travel) : 0;
234
+ const thumbColor = grab !== null ? t.color.primary : hover ? t.color.accent : t.color.border;
235
+ const trackColor = hover ? t.color.border : t.color.muted;
236
+ const jump = (row, offset) => {
237
+ if (!s || !scrollable) {
238
+ return;
239
+ }
240
+ s.scrollTo(Math.round((Math.max(0, Math.min(travel, row - offset)) / travel) * Math.max(0, total - vp)));
241
+ };
242
+ return (_jsx(Box, { flexDirection: "column", onMouseDown: (e) => {
243
+ const row = Math.max(0, Math.min(vp - 1, e.localRow ?? 0));
244
+ const off = row >= thumbTop && row < thumbTop + thumb ? row - thumbTop : Math.floor(thumb / 2);
245
+ grabRef.current = off;
246
+ setGrab(off);
247
+ jump(row, off);
248
+ }, onMouseDrag: (e) => jump(Math.max(0, Math.min(vp - 1, e.localRow ?? 0)), grabRef.current ?? Math.floor(thumb / 2)), onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), onMouseUp: () => {
249
+ grabRef.current = null;
250
+ setGrab(null);
251
+ }, width: 1, children: !scrollable ? (_jsxs(Text, { color: trackColor, dim: true, children: [' \n'.repeat(Math.max(0, vp - 1)), ' '] })) : (_jsxs(_Fragment, { children: [thumbTop > 0 ? (_jsx(Text, { color: trackColor, dim: !hover, children: `${'│\n'.repeat(Math.max(0, thumbTop - 1))}${thumbTop > 0 ? '│' : ''}` })) : null, thumb > 0 ? (_jsx(Text, { color: thumbColor, children: `${'┃\n'.repeat(Math.max(0, thumb - 1))}${thumb > 0 ? '┃' : ''}` })) : null, vp - thumbTop - thumb > 0 ? (_jsx(Text, { color: trackColor, dim: !hover, children: `${'│\n'.repeat(Math.max(0, vp - thumbTop - thumb - 1))}${vp - thumbTop - thumb > 0 ? '│' : ''}` })) : null] })) }));
252
+ }
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '../vendor/cvc-ink/index.js';
7
+ import { useStore } from '@nanostores/react';
8
+ import { Fragment, memo, useMemo, useRef } from 'react';
9
+ import { useGateway } from '../app/gatewayContext.js';
10
+ import { $isBlocked, $overlayState, patchOverlayState } from '../app/overlayStore.js';
11
+ import { $uiState } from '../app/uiStore.js';
12
+ import { INLINE_MODE, SHOW_FPS } from '../config/env.js';
13
+ import { FULL_RENDER_TAIL_ITEMS } from '../config/limits.js';
14
+ import { PLACEHOLDER } from '../content/placeholders.js';
15
+ import { COMPOSER_PROMPT_GAP_WIDTH, composerPromptWidth, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js';
16
+ import { PerfPane } from '../lib/perfPane.js';
17
+ import { AgentsOverlay } from './agentsOverlay.js';
18
+ import { GoodVibesHeart, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js';
19
+ import { FloatingOverlays, PromptZone } from './appOverlays.js';
20
+ import { Banner, Panel, SessionPanel } from './branding.js';
21
+ import { buildLocalSessionInfo } from '../lib/localSessionInfo.js';
22
+ import { FpsOverlay } from './fpsOverlay.js';
23
+ import { HelpHint } from './helpHint.js';
24
+ import { MessageLine } from './messageLine.js';
25
+ import { QueuedMessages } from './queuedMessages.js';
26
+ import { LiveTodoPanel, StreamingAssistant } from './streamingAssistant.js';
27
+ import { TextInput } from './textInput.js';
28
+ const PromptPrefix = memo(function PromptPrefix({ bold = false, color, promptText, width }) {
29
+ const glyphWidth = Math.max(1, width - COMPOSER_PROMPT_GAP_WIDTH);
30
+ return (_jsxs(Box, { width: width, children: [_jsx(Box, { width: glyphWidth, children: _jsx(Text, { bold: bold, color: color, children: promptText }) }), _jsx(Box, { width: COMPOSER_PROMPT_GAP_WIDTH })] }));
31
+ });
32
+ const TranscriptPane = memo(function TranscriptPane({ actions, composer, progress, transcript }) {
33
+ const ui = useStore($uiState);
34
+ // LiveTodoPanel rides as a child of the latest user-message row so it
35
+ // visually belongs to the prompt and follows it during scroll. -1 when
36
+ // empty → row.index === -1 is always false → no render.
37
+ const lastUserIdx = useMemo(() => {
38
+ const items = transcript.historyItems;
39
+ for (let i = items.length - 1; i >= 0; i--) {
40
+ if (items[i].role === 'user') {
41
+ return i;
42
+ }
43
+ }
44
+ return -1;
45
+ }, [transcript.historyItems]);
46
+ // Index of the first user-role message; every later user message gets a
47
+ // small dash above it so multi-turn transcripts visually segment by
48
+ // turn. -1 when no user message has been sent yet → no separator ever
49
+ // renders.
50
+ const firstUserIdx = useMemo(() => transcript.historyItems.findIndex(m => m.role === 'user'), [transcript.historyItems]);
51
+ return (_jsxs(_Fragment, { children: [_jsx(ScrollBox, { flexDirection: "column", flexGrow: 1, flexShrink: 1, onClick: (e) => {
52
+ if (e.cellIsBlank) {
53
+ actions.clearSelection();
54
+ }
55
+ }, ref: transcript.scrollRef, stickyScroll: true, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcript.virtualHistory.topSpacer > 0 ? _jsx(Box, { height: transcript.virtualHistory.topSpacer }) : null, transcript.virtualRows.slice(transcript.virtualHistory.start, transcript.virtualHistory.end).map(row => (_jsxs(Box, { flexDirection: "column", ref: transcript.virtualHistory.measureRef(row.key), children: [row.msg.role === 'user' && firstUserIdx >= 0 && row.index > firstUserIdx && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: ui.theme.color.border, children: "\u2500\u2500\u2500" }) })), row.msg.kind === 'intro' ? (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Banner, { t: ui.theme }), _jsx(SessionPanel, { info: row.msg.info ?? buildLocalSessionInfo(), sid: ui.sid, t: ui.theme })] })) : row.msg.kind === 'panel' && row.msg.panelData ? (_jsx(Panel, { sections: row.msg.panelData.sections, t: ui.theme, title: row.msg.panelData.title })) : (_jsx(MessageLine, { cols: composer.cols, compact: ui.compact, detailsMode: ui.detailsMode, detailsModeCommandOverride: ui.detailsModeCommandOverride, limitHistoryRender: row.index < transcript.historyItems.length - FULL_RENDER_TAIL_ITEMS, msg: row.msg, sections: ui.sections, t: ui.theme })), row.index === lastUserIdx && _jsx(LiveTodoPanel, {})] }, row.key))), transcript.virtualHistory.bottomSpacer > 0 ? _jsx(Box, { height: transcript.virtualHistory.bottomSpacer }) : null, _jsx(StreamingAssistant, { cols: composer.cols, compact: ui.compact, detailsMode: ui.detailsMode, detailsModeCommandOverride: ui.detailsModeCommandOverride, progress: progress, sections: ui.sections })] }) }), _jsx(NoSelect, { flexShrink: 0, marginLeft: 1, children: _jsx(TranscriptScrollbar, { scrollRef: transcript.scrollRef, t: ui.theme }) }), _jsx(StickyPromptTracker, { messages: transcript.historyItems, offsets: transcript.virtualHistory.offsets, onChange: actions.setStickyPrompt, scrollRef: transcript.scrollRef })] }));
56
+ });
57
+ const ComposerPane = memo(function ComposerPane({ actions, composer, status }) {
58
+ const ui = useStore($uiState);
59
+ const isBlocked = useStore($isBlocked);
60
+ const sh = (composer.inputBuf[0] ?? composer.input).startsWith('!');
61
+ const promptText = sh ? '$' : ui.theme.brand.prompt;
62
+ const promptWidth = composerPromptWidth(promptText);
63
+ const promptBlank = ' '.repeat(promptWidth);
64
+ const inputColumns = stableComposerColumns(composer.cols, promptWidth);
65
+ const inputHeight = inputVisualHeight(composer.input, inputColumns);
66
+ const inputMouseRef = useRef(null);
67
+ const captureInputDrag = (e) => {
68
+ if (e.button !== 0) {
69
+ return;
70
+ }
71
+ e.stopImmediatePropagation?.();
72
+ inputMouseRef.current?.startAtBeginning();
73
+ };
74
+ // Drag origin matches the input box's top-left, so localRow / localCol
75
+ // map directly into TextInput coords (after backing out the prompt cell).
76
+ const dragFromPromptRow = (e) => {
77
+ if (e.button !== 0) {
78
+ return;
79
+ }
80
+ e.stopImmediatePropagation?.();
81
+ inputMouseRef.current?.dragAt(e.localRow ?? 0, (e.localCol ?? 0) - promptWidth);
82
+ };
83
+ // Spacer rows live on a different vertical origin; only the column is
84
+ // parent-aligned with the input. Force row=0 so vertical drags can't
85
+ // jump the cursor to the wrong wrapped line.
86
+ const dragFromSpacer = (e) => {
87
+ if (e.button !== 0) {
88
+ return;
89
+ }
90
+ e.stopImmediatePropagation?.();
91
+ inputMouseRef.current?.dragAt(0, (e.localCol ?? 0) - promptWidth);
92
+ };
93
+ const endInputDrag = () => inputMouseRef.current?.end();
94
+ return (_jsxs(NoSelect, { flexDirection: "column", flexShrink: 0, fromLeftEdge: true, onClick: (e) => {
95
+ if (e.cellIsBlank) {
96
+ actions.clearSelection();
97
+ }
98
+ }, paddingX: 1, children: [_jsx(QueuedMessages, { cols: composer.cols, queued: composer.queuedDisplay, queueEditIdx: composer.queueEditIdx, t: ui.theme }), ui.bgTasks.size > 0 && (_jsxs(Text, { color: ui.theme.color.muted, children: [ui.bgTasks.size, " background ", ui.bgTasks.size === 1 ? 'task' : 'tasks', " running"] })), status.showStickyPrompt ? (_jsxs(Text, { color: ui.theme.color.muted, wrap: "truncate-end", children: [_jsx(Text, { color: ui.theme.color.label, children: "\u21B3 " }), status.stickyPrompt] })) : (_jsx(Box, { height: 1, onMouseDown: captureInputDrag, onMouseDrag: dragFromSpacer, onMouseUp: endInputDrag })), _jsx(StatusRulePane, { at: "top", composer: composer, status: status }), _jsxs(Box, { flexDirection: "column", marginTop: ui.statusBar === 'top' ? 0 : 1, position: "relative", children: [_jsx(FloatingOverlays, { cols: composer.cols, compIdx: composer.compIdx, completions: composer.completions, onModelSelect: actions.onModelSelect, onPickerSelect: actions.resumeById, pagerPageSize: composer.pagerPageSize }), composer.input === '?' && !composer.inputBuf.length && _jsx(HelpHint, { t: ui.theme }), !isBlocked && (_jsxs(_Fragment, { children: [composer.inputBuf.map((line, i) => (_jsxs(Box, { children: [_jsx(Box, { width: promptWidth, children: i === 0 ? (_jsx(PromptPrefix, { color: ui.theme.color.muted, promptText: promptText, width: promptWidth })) : (_jsx(Text, { color: ui.theme.color.muted, children: promptBlank })) }), _jsx(Text, { color: ui.theme.color.text, children: line || ' ' })] }, i))), _jsxs(Box, { onMouseDown: captureInputDrag, onMouseDrag: dragFromPromptRow, onMouseUp: endInputDrag, position: "relative", width: Math.max(1, composer.cols - 2), children: [_jsx(Box, { width: promptWidth, children: sh ? (_jsx(PromptPrefix, { color: ui.theme.color.shellDollar, promptText: promptText, width: promptWidth })) : composer.inputBuf.length ? (_jsx(Text, { color: ui.theme.color.prompt, children: promptBlank })) : (_jsx(PromptPrefix, { bold: true, color: ui.theme.color.prompt, promptText: promptText, width: promptWidth })) }), _jsx(Box, { flexGrow: 0, flexShrink: 0, height: inputHeight, width: inputColumns, children: _jsx(TextInput, { columns: inputColumns, mouseApiRef: inputMouseRef, onChange: composer.updateInput, onPaste: composer.handleTextPaste, onSubmit: composer.submit, placeholder: composer.empty ? PLACEHOLDER : ui.busy ? 'Ctrl+C to interrupt…' : '', value: composer.input, voiceRecordKey: composer.voiceRecordKey }) }), _jsx(Box, { position: "absolute", right: 0, children: _jsx(GoodVibesHeart, { t: ui.theme, tick: status.goodVibesTick }) })] })] }))] }), !composer.empty && !ui.sid && _jsxs(Text, { color: ui.theme.color.muted, children: ["\u2695 ", ui.status] }), _jsx(StatusRulePane, { at: "bottom", composer: composer, status: status })] }));
99
+ });
100
+ const AgentsOverlayPane = memo(function AgentsOverlayPane() {
101
+ const { gw } = useGateway();
102
+ const ui = useStore($uiState);
103
+ const overlay = useStore($overlayState);
104
+ return (_jsx(AgentsOverlay, { gw: gw, initialHistoryIndex: overlay.agentsInitialHistoryIndex, onClose: () => patchOverlayState({ agents: false, agentsInitialHistoryIndex: 0 }), t: ui.theme }));
105
+ });
106
+ const StatusRulePane = memo(function StatusRulePane({ at, composer, status }) {
107
+ const ui = useStore($uiState);
108
+ if (ui.statusBar !== at) {
109
+ return null;
110
+ }
111
+ return (_jsx(Box, { marginTop: at === 'top' ? 1 : 0, children: _jsx(StatusRule, { bgCount: ui.bgTasks.size, busy: ui.busy, cols: composer.cols, cwdLabel: status.cwdLabel, model: ui.info?.model ?? '', modelFast: ui.info?.fast || ui.info?.service_tier === 'priority', modelReasoningEffort: ui.info?.reasoning_effort, sessionStartedAt: status.sessionStartedAt, showCost: ui.showCost, status: ui.status, statusColor: status.statusColor, t: ui.theme, turnStartedAt: status.turnStartedAt, usage: ui.usage, voiceLabel: status.voiceLabel }) }));
112
+ });
113
+ export const AppLayout = memo(function AppLayout({ actions, composer, mouseTracking, progress, status, transcript }) {
114
+ const overlay = useStore($overlayState);
115
+ const ui = useStore($uiState);
116
+ // Inline mode skips AlternateScreen so the host terminal's native
117
+ // scrollback captures rows scrolled off the top; composer + progress
118
+ // stay anchored via normal flex-column flow.
119
+ const Shell = INLINE_MODE ? Fragment : AlternateScreen;
120
+ const shellProps = INLINE_MODE ? {} : { mouseTracking };
121
+ return (_jsx(Shell, { ...shellProps, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { flexDirection: "row", flexGrow: 1, children: overlay.agents ? (_jsx(PerfPane, { id: "agents", children: _jsx(AgentsOverlayPane, {}) })) : (_jsx(PerfPane, { id: "transcript", children: _jsx(TranscriptPane, { actions: actions, composer: composer, progress: progress, transcript: transcript }) })) }), !overlay.agents && (_jsxs(_Fragment, { children: [_jsx(PerfPane, { id: "prompt", children: _jsx(PromptZone, { cols: composer.cols, onApprovalChoice: actions.answerApproval, onClarifyAnswer: actions.answerClarify, onSecretSubmit: actions.answerSecret, onSudoSubmit: actions.answerSudo }) }), _jsx(PerfPane, { id: "composer", children: _jsx(ComposerPane, { actions: actions, composer: composer, status: status }) }), SHOW_FPS && (_jsx(Box, { flexShrink: 0, justifyContent: "flex-end", paddingRight: 1, children: _jsx(FpsOverlay, { t: ui.theme }) }))] }))] }) }));
122
+ });
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ import { Box, Text } from '../vendor/cvc-ink/index.js';
7
+ import { useStore } from '@nanostores/react';
8
+ import { useGateway } from '../app/gatewayContext.js';
9
+ import { $overlayState, patchOverlayState } from '../app/overlayStore.js';
10
+ import { $uiSessionId, $uiTheme } from '../app/uiStore.js';
11
+ import { FloatBox } from './appChrome.js';
12
+ import { MaskedPrompt } from './maskedPrompt.js';
13
+ import { ModelPicker } from './modelPicker.js';
14
+ import { OverlayHint } from './overlayControls.js';
15
+ import { ApprovalPrompt, ClarifyPrompt, ConfirmPrompt } from './prompts.js';
16
+ import { SessionPicker } from './sessionPicker.js';
17
+ import { SkillsHub } from './skillsHub.js';
18
+ const COMPLETION_WINDOW = 16;
19
+ export function PromptZone({ cols, onApprovalChoice, onClarifyAnswer, onSecretSubmit, onSudoSubmit }) {
20
+ const overlay = useStore($overlayState);
21
+ const theme = useStore($uiTheme);
22
+ if (overlay.approval) {
23
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, paddingY: 1, children: _jsx(ApprovalPrompt, { onChoice: onApprovalChoice, req: overlay.approval, t: theme }) }));
24
+ }
25
+ if (overlay.confirm) {
26
+ const req = overlay.confirm;
27
+ const onConfirm = () => {
28
+ patchOverlayState({ confirm: null });
29
+ req.onConfirm();
30
+ };
31
+ const onCancel = () => patchOverlayState({ confirm: null });
32
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, paddingY: 1, children: _jsx(ConfirmPrompt, { onCancel: onCancel, onConfirm: onConfirm, req: req, t: theme }) }));
33
+ }
34
+ if (overlay.clarify) {
35
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, paddingY: 1, children: _jsx(ClarifyPrompt, { cols: cols, onAnswer: onClarifyAnswer, onCancel: () => onClarifyAnswer(''), req: overlay.clarify, t: theme }) }));
36
+ }
37
+ if (overlay.sudo) {
38
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, paddingY: 1, children: _jsx(MaskedPrompt, { cols: cols, icon: "\uD83D\uDD10", label: "sudo password required", onSubmit: onSudoSubmit, t: theme }) }));
39
+ }
40
+ if (overlay.secret) {
41
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, paddingY: 1, children: _jsx(MaskedPrompt, { cols: cols, icon: "\uD83D\uDD11", label: overlay.secret.prompt, onSubmit: onSecretSubmit, sub: `for ${overlay.secret.envVar}`, t: theme }) }));
42
+ }
43
+ return null;
44
+ }
45
+ export function FloatingOverlays({ cols, compIdx, completions, onModelSelect, onPickerSelect, pagerPageSize }) {
46
+ const { gw } = useGateway();
47
+ const overlay = useStore($overlayState);
48
+ const sid = useStore($uiSessionId);
49
+ const theme = useStore($uiTheme);
50
+ const hasAny = overlay.modelPicker || overlay.pager || overlay.picker || overlay.skillsHub || completions.length;
51
+ if (!hasAny) {
52
+ return null;
53
+ }
54
+ // Fixed viewport centered on compIdx — previously the slice end was
55
+ // compIdx + 8 so the dropdown grew from 8 rows to 16 as the user scrolled
56
+ // down, bouncing the height on every keystroke.
57
+ const viewportSize = Math.min(COMPLETION_WINDOW, completions.length);
58
+ const start = Math.max(0, Math.min(compIdx - Math.floor(COMPLETION_WINDOW / 2), completions.length - viewportSize));
59
+ return (_jsxs(Box, { alignItems: "flex-start", bottom: "100%", flexDirection: "column", left: 0, position: "absolute", right: 0, children: [overlay.picker && (_jsx(FloatBox, { color: theme.color.border, children: _jsx(SessionPicker, { gw: gw, onCancel: () => patchOverlayState({ picker: false }), onSelect: onPickerSelect, t: theme }) })), overlay.modelPicker && (_jsx(FloatBox, { color: theme.color.border, children: _jsx(ModelPicker, { gw: gw, onCancel: () => patchOverlayState({ modelPicker: false }), onSelect: onModelSelect, sessionId: sid, t: theme }) })), overlay.skillsHub && (_jsx(FloatBox, { color: theme.color.border, children: _jsx(SkillsHub, { gw: gw, onClose: () => patchOverlayState({ skillsHub: false }), t: theme }) })), overlay.pager && (_jsx(FloatBox, { color: theme.color.border, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [overlay.pager.title && (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.color.primary, children: overlay.pager.title }) })), overlay.pager.lines.slice(overlay.pager.offset, overlay.pager.offset + pagerPageSize).map((line, i) => (_jsx(Text, { children: line }, i))), _jsx(Box, { marginTop: 1, children: _jsx(OverlayHint, { t: theme, children: overlay.pager.offset + pagerPageSize < overlay.pager.lines.length
60
+ ? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · Esc/q close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})`
61
+ : `end · ↑↓/jk · b/PgUp back · g top · Esc/q close (${overlay.pager.lines.length} lines)` }) })] }) })), !!completions.length && (_jsx(FloatBox, { color: theme.color.primary, children: _jsx(Box, { flexDirection: "column", width: Math.max(28, cols - 6), children: completions.slice(start, start + viewportSize).map((item, i) => {
62
+ const active = start + i === compIdx;
63
+ return (_jsxs(Box, { backgroundColor: active ? theme.color.completionCurrentBg : theme.color.completionBg, flexDirection: "row", width: "100%", children: [_jsxs(Text, { bold: true, color: theme.color.label, children: [' ', item.display] }), item.meta ? (_jsxs(Text, { backgroundColor: active ? theme.color.completionMetaCurrentBg : theme.color.completionMetaBg, color: theme.color.muted, children: [' ', item.meta] })) : null] }, `${start + i}:${item.text}:${item.display}:${item.meta ?? ''}`));
64
+ }) }) }))] }));
65
+ }
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ import { Box, Text, useStdout } from '../vendor/cvc-ink/index.js';
7
+ import { useEffect, useState } from 'react';
8
+ import unicodeSpinners from 'unicode-animations';
9
+ import { artWidth, caduceus, CADUCEUS_WIDTH, logo, LOGO_WIDTH } from '../banner.js';
10
+ import { flat } from '../lib/text.js';
11
+ const LOADER_TICK_MS = 120;
12
+ function InlineLoader({ label, t }) {
13
+ const [tick, setTick] = useState(0);
14
+ const spinner = unicodeSpinners.braille;
15
+ const frame = spinner.frames[tick % spinner.frames.length] ?? '⠋';
16
+ useEffect(() => {
17
+ const id = setInterval(() => setTick(n => n + 1), Math.max(LOADER_TICK_MS, spinner.interval));
18
+ return () => clearInterval(id);
19
+ }, [spinner.interval]);
20
+ return (_jsxs(Text, { color: t.color.muted, wrap: "truncate", children: [_jsx(Text, { color: t.color.accent, children: frame }), " ", label] }));
21
+ }
22
+ export function ArtLines({ lines }) {
23
+ return (_jsx(_Fragment, { children: lines.map(([c, text], i) => (_jsx(Text, { color: c, children: text }, i))) }));
24
+ }
25
+ export function Banner({ t }) {
26
+ const cols = useStdout().stdout?.columns ?? 80;
27
+ const logoLines = logo(t.color, t.bannerLogo || undefined);
28
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (_jsx(ArtLines, { lines: logoLines })) : (_jsx(Text, { bold: true, color: t.color.primary, children: `${t.brand.icon} CVC` })), _jsx(Text, { color: t.color.muted, children: `${t.brand.icon} Cognitive Version Control · git for your AI's mind` })] }));
29
+ }
30
+ // ── Collapsible helpers ──────────────────────────────────────────────
31
+ function CollapseToggle({ count, open, suffix, t, title, onToggle }) {
32
+ return (_jsxs(Box, { onClick: onToggle, children: [_jsx(Text, { color: t.color.accent, children: open ? '▾ ' : '▸ ' }), _jsx(Text, { bold: true, color: t.color.accent, children: title }), typeof count === 'number' ? (_jsxs(Text, { color: t.color.muted, children: [" (", count, ")"] })) : null, suffix ? (_jsxs(Text, { color: t.color.muted, children: [" ", suffix] })) : null] }));
33
+ }
34
+ // ── SessionPanel ─────────────────────────────────────────────────────
35
+ const SKILLS_MAX = 8;
36
+ const TOOLSETS_MAX = 8;
37
+ export function SessionPanel({ info, sid, t }) {
38
+ const cols = useStdout().stdout?.columns ?? 100;
39
+ const heroLines = caduceus(t.color, t.bannerHero || undefined);
40
+ const leftW = Math.min((artWidth(heroLines) || CADUCEUS_WIDTH) + 4, Math.floor(cols * 0.4));
41
+ const wide = cols >= 90 && leftW + 40 < cols;
42
+ const w = Math.max(20, wide ? cols - leftW - 14 : cols - 12);
43
+ const lineBudget = Math.max(12, w - 2);
44
+ const strip = (s) => (s.endsWith('_tools') ? s.slice(0, -6) : s);
45
+ // ── Local collapse state for each section ──
46
+ const [toolsOpen, setToolsOpen] = useState(true);
47
+ const [skillsOpen, setSkillsOpen] = useState(false);
48
+ const [systemOpen, setSystemOpen] = useState(false);
49
+ const [mcpOpen, setMcpOpen] = useState(false);
50
+ const truncLine = (pfx, items) => {
51
+ let line = '';
52
+ let shown = 0;
53
+ for (const item of [...items].sort()) {
54
+ const next = line ? `${line}, ${item}` : item;
55
+ if (pfx.length + next.length > lineBudget) {
56
+ return line ? `${line}, …+${items.length - shown}` : `${item}, …`;
57
+ }
58
+ line = next;
59
+ shown++;
60
+ }
61
+ return line;
62
+ };
63
+ // ── Collapsible skills section ──
64
+ const skillEntries = Object.entries(info.skills).sort();
65
+ const skillsTotal = flat(info.skills).length;
66
+ const skillsCatCount = skillEntries.length;
67
+ const skillsBody = () => {
68
+ if (info.lazy && skillEntries.length === 0) {
69
+ return _jsx(InlineLoader, { label: "scanning skills", t: t });
70
+ }
71
+ const shown = skillEntries.slice(0, SKILLS_MAX);
72
+ const overflow = skillEntries.length - SKILLS_MAX;
73
+ return (_jsxs(_Fragment, { children: [shown.map(([k, vs]) => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: t.color.muted, children: [strip(k), ": "] }), _jsx(Text, { color: t.color.text, children: truncLine(strip(k) + ': ', vs) })] }, k))), overflow > 0 && (_jsxs(Text, { color: t.color.muted, children: ["(and ", overflow, " more categories\u2026)"] }))] }));
74
+ };
75
+ // ── Collapsible tools section ──
76
+ const toolEntries = Object.entries(info.tools).sort();
77
+ const toolsTotal = flat(info.tools).length;
78
+ const toolsBody = () => {
79
+ const shown = toolEntries.slice(0, TOOLSETS_MAX);
80
+ const overflow = toolEntries.length - TOOLSETS_MAX;
81
+ return (_jsxs(_Fragment, { children: [shown.map(([k, vs]) => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: t.color.muted, children: [strip(k), ": "] }), _jsx(Text, { color: t.color.text, children: truncLine(strip(k) + ': ', vs) })] }, k))), overflow > 0 && (_jsxs(Text, { color: t.color.muted, children: ["(and ", overflow, " more toolsets\u2026)"] }))] }));
82
+ };
83
+ // ── Collapsible MCP section ──
84
+ const mcpBody = () => (_jsx(_Fragment, { children: (info.mcp_servers ?? []).map(s => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: t.color.muted, children: ` ${s.name} ` }), _jsx(Text, { color: t.color.muted, children: `[${s.transport}]` }), _jsx(Text, { color: t.color.muted, children: ": " }), s.connected ? (_jsxs(Text, { color: t.color.text, children: [s.tools, " tool", s.tools === 1 ? '' : 's'] })) : (_jsx(Text, { color: t.color.error, children: "failed" }))] }, s.name))) }));
85
+ // ── System prompt body ──
86
+ const sysPromptLen = (info.system_prompt ?? '').length;
87
+ const systemBody = () => {
88
+ if (sysPromptLen === 0) {
89
+ return _jsx(Text, { color: t.color.muted, children: "No system prompt loaded." });
90
+ }
91
+ return (_jsx(Text, { color: t.color.muted, children: info.system_prompt }));
92
+ };
93
+ return (_jsxs(Box, { borderColor: t.color.border, borderStyle: "round", marginBottom: 1, paddingX: 2, paddingY: 1, children: [wide && (_jsxs(Box, { flexDirection: "column", marginRight: 2, width: leftW, children: [_jsx(ArtLines, { lines: heroLines }), _jsx(Text, {}), _jsxs(Text, { color: t.color.accent, children: [info.model.split('/').pop(), _jsx(Text, { color: t.color.muted, children: " \u00B7 Nous Research" })] }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: info.cwd || process.cwd() }), sid && (_jsxs(Text, { children: [_jsx(Text, { color: t.color.sessionLabel, children: "Session: " }), _jsx(Text, { color: t.color.sessionBorder, children: sid })] }))] })), _jsxs(Box, { flexDirection: "column", width: w, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Text, { bold: true, color: t.color.primary, children: [t.brand.name, info.version ? ` v${info.version}` : '', info.release_date ? ` (${info.release_date})` : ''] }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(CollapseToggle, { onToggle: () => setToolsOpen(v => !v), open: toolsOpen, t: t, title: "Available Tools" }), toolsOpen && toolsBody()] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(CollapseToggle, { count: skillsTotal, onToggle: () => setSkillsOpen(v => !v), open: skillsOpen, suffix: skillsCatCount > 0 ? `in ${skillsCatCount} categor${skillsCatCount === 1 ? 'y' : 'ies'}` : undefined, t: t, title: "Available Skills" }), skillsOpen && skillsBody()] }), sysPromptLen > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(CollapseToggle, { onToggle: () => setSystemOpen(v => !v), open: systemOpen, suffix: `— ${sysPromptLen.toLocaleString()} chars`, t: t, title: "System Prompt" }), systemOpen && systemBody()] })), info.mcp_servers && info.mcp_servers.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(CollapseToggle, { count: info.mcp_servers.length, onToggle: () => setMcpOpen(v => !v), open: mcpOpen, suffix: "connected", t: t, title: "MCP Servers" }), mcpOpen && mcpBody()] })), _jsx(Text, {}), _jsxs(Text, { color: t.color.text, children: [toolsTotal, " tools", ' · ', skillsTotal, " skills", info.mcp_servers?.length ? ` · ${info.mcp_servers.length} MCP` : '', ' · ', _jsx(Text, { color: t.color.muted, children: "/help for commands" })] }), typeof info.update_behind === 'number' && info.update_behind > 0 && (_jsxs(Text, { bold: true, color: t.color.warn, children: ["! ", info.update_behind, " ", info.update_behind === 1 ? 'commit' : 'commits', " behind", _jsxs(Text, { bold: false, color: t.color.warn, dimColor: true, children: [' ', "- run", ' '] }), _jsx(Text, { bold: true, color: t.color.warn, children: info.update_command || 'hermes update' }), _jsxs(Text, { bold: false, color: t.color.warn, dimColor: true, children: [' ', "to update"] })] }))] })] }));
94
+ }
95
+ export function Panel({ sections, t, title }) {
96
+ return (_jsxs(Box, { borderColor: t.color.border, borderStyle: "round", flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: t.color.primary, children: title }) }), sections.map((sec, si) => (_jsxs(Box, { flexDirection: "column", marginTop: si > 0 ? 1 : 0, children: [sec.title && (_jsx(Text, { bold: true, color: t.color.accent, children: sec.title })), sec.rows?.map(([k, v], ri) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: t.color.muted, children: k.padEnd(20) }), _jsx(Text, { color: t.color.text, children: v })] }, ri))), sec.items?.map((item, ii) => (_jsx(Text, { color: t.color.text, wrap: "truncate", children: item }, ii))), sec.text && _jsx(Text, { color: t.color.muted, children: sec.text })] }, si)))] }));
97
+ }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ // FPS counter overlay (CVC_TUI_FPS=1). Zero-cost when disabled.
7
+ import { Text } from '../vendor/cvc-ink/index.js';
8
+ import { useStore } from '@nanostores/react';
9
+ import { SHOW_FPS } from '../config/env.js';
10
+ import { $fpsState } from '../lib/fpsStore.js';
11
+ const fpsColor = (fps, t) => fps >= 50 ? t.color.statusGood : fps >= 30 ? t.color.statusWarn : t.color.error;
12
+ export function FpsOverlay({ t }) {
13
+ if (!SHOW_FPS) {
14
+ return null;
15
+ }
16
+ return _jsx(FpsOverlayInner, { t: t });
17
+ }
18
+ function FpsOverlayInner({ t }) {
19
+ const { fps, lastDurationMs, totalFrames } = useStore($fpsState);
20
+ // Zero-pad widths so digit churn doesn't jitter the corner.
21
+ return (_jsxs(Text, { color: fpsColor(fps, t), children: [fps.toFixed(1).padStart(5), "fps \u00B7 ", lastDurationMs.toFixed(1).padStart(5), "ms \u00B7 #", totalFrames] }));
22
+ }
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @ts-nocheck
3
+ // SPDX-License-Identifier: MIT
4
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
5
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
6
+ import { Box, Text } from '../vendor/cvc-ink/index.js';
7
+ import { HOTKEYS } from '../content/hotkeys.js';
8
+ const COMMON_COMMANDS = [
9
+ ['/help', 'full list of commands + hotkeys'],
10
+ ['/clear', 'start a new session'],
11
+ ['/resume', 'resume a prior session'],
12
+ ['/details', 'control transcript detail level'],
13
+ ['/copy', 'copy selection or last assistant message'],
14
+ ['/quit', 'exit hermes']
15
+ ];
16
+ const HOTKEY_PREVIEW = HOTKEYS.slice(0, 8);
17
+ export function HelpHint({ t }) {
18
+ const labelW = Math.max(...COMMON_COMMANDS.map(([k]) => k.length), ...HOTKEY_PREVIEW.map(([k]) => k.length));
19
+ const pad = (s) => s + ' '.repeat(Math.max(0, labelW - s.length + 2));
20
+ return (_jsx(Box, { alignItems: "flex-start", bottom: "100%", flexDirection: "column", left: 0, position: "absolute", right: 0, children: _jsxs(Box, { alignSelf: "flex-start", borderColor: t.color.primary, borderStyle: "round", flexDirection: "column", marginBottom: 1, opaque: true, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: t.color.primary, children: "? quick help" }), _jsx(Text, { color: t.color.muted, children: ' · type /help for the full panel · backspace to dismiss' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: t.color.accent, children: "Common commands" }) }), COMMON_COMMANDS.map(([k, v]) => (_jsxs(Text, { children: [_jsx(Text, { color: t.color.label, children: pad(k) }), _jsx(Text, { color: t.color.muted, children: v })] }, k))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: t.color.accent, children: "Hotkeys" }) }), HOTKEY_PREVIEW.map(([k, v]) => (_jsxs(Text, { children: [_jsx(Text, { color: t.color.label, children: pad(k) }), _jsx(Text, { color: t.color.muted, children: v })] }, k)))] }) }));
21
+ }