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,501 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } 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, Link, Text } from '../vendor/cvc-ink/index.js';
7
+ import { Fragment, memo, useMemo } from 'react';
8
+ import { ensureEmojiPresentation } from '../lib/emoji.js';
9
+ import { BOX_CLOSE, BOX_OPEN, texToUnicode } from '../lib/mathUnicode.js';
10
+ import { highlightLine, isHighlightable } from '../lib/syntax.js';
11
+ // `\boxed{X}` regions in `texToUnicode` output are marked with the
12
+ // non-printable U+0001 / U+0002 sentinels. Split on them and render the
13
+ // boxed segment with `inverse + bold` so it reads as a highlighter-pen
14
+ // emphasis on top of whatever color the parent `<Text>` is using (the
15
+ // theme accent for math). The leading / trailing space inside the
16
+ // highlight gives a one-cell visual margin so the highlight reads as a
17
+ // block, not a hug.
18
+ const renderMath = (text) => {
19
+ if (!text.includes(BOX_OPEN)) {
20
+ return text;
21
+ }
22
+ const out = [];
23
+ let i = 0;
24
+ let key = 0;
25
+ while (i < text.length) {
26
+ const start = text.indexOf(BOX_OPEN, i);
27
+ if (start < 0) {
28
+ out.push(text.slice(i));
29
+ break;
30
+ }
31
+ if (start > i) {
32
+ out.push(text.slice(i, start));
33
+ }
34
+ const end = text.indexOf(BOX_CLOSE, start + 1);
35
+ if (end < 0) {
36
+ out.push(text.slice(start));
37
+ break;
38
+ }
39
+ out.push(_jsxs(Text, { bold: true, inverse: true, children: [' ', text.slice(start + 1, end), ' '] }, key++));
40
+ i = end + 1;
41
+ }
42
+ return out;
43
+ };
44
+ const FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
45
+ const FENCE_CLOSE_RE = /^\s*(`{3,}|~{3,})\s*$/;
46
+ const HR_RE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
47
+ const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*?)(?:\s+#+\s*)?$/;
48
+ const SETEXT_RE = /^\s{0,3}(=+|-+)\s*$/;
49
+ const FOOTNOTE_RE = /^\[\^([^\]]+)\]:\s*(.*)$/;
50
+ const DEF_RE = /^\s*:\s+(.+)$/;
51
+ const BULLET_RE = /^(\s*)[-+*]\s+(.*)$/;
52
+ const TASK_RE = /^\[( |x|X)\]\s+(.*)$/;
53
+ const NUMBERED_RE = /^(\s*)(\d+)[.)]\s+(.*)$/;
54
+ const QUOTE_RE = /^\s*(?:>\s*)+/;
55
+ const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/;
56
+ const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)';
57
+ // Display math openers: `$$ ... $$` (TeX) and `\[ ... \]` (LaTeX). The
58
+ // opener is matched only when `$$` / `\[` appears at the very start of the
59
+ // trimmed line — `startsWith('$$')` used to fire on prose like
60
+ // `$$x+y$$ followed by more`, opening a block that never closed because the
61
+ // trailing `$$` on the same line was invisible to the close-scan loop.
62
+ const MATH_BLOCK_OPEN_RE = /^\s*(\$\$|\\\[)(.*)$/;
63
+ const MATH_BLOCK_CLOSE_DOLLAR_RE = /^(.*?)\$\$\s*$/;
64
+ const MATH_BLOCK_CLOSE_BRACKET_RE = /^(.*?)\\\]\s*$/;
65
+ export const MEDIA_LINE_RE = /^\s*[`"']?MEDIA:\s*(\S+?)[`"']?\s*$/;
66
+ export const AUDIO_DIRECTIVE_RE = /^\s*\[\[audio_as_voice\]\]\s*$/;
67
+ // Inline markdown tokens, in priority order. The outer regex picks the
68
+ // leftmost match at each position, preferring earlier alternatives on tie —
69
+ // so `**` must come before `*`, `__` before `_`, etc. Each pattern owns its
70
+ // own capture groups; MdInline dispatches on which group matched.
71
+ //
72
+ // Subscript (`~x~`) is restricted to short alphanumeric runs so prose like
73
+ // `thing ~! more ~?` from Kimi / Qwen / GLM (kaomoji-style decorators)
74
+ // doesn't pair up the first `~` with the next one on the line and swallow
75
+ // the text between them as a dim `_`-prefixed span.
76
+ //
77
+ // Inline math (`$x$` and `\(x\)`) takes precedence over emphasis at the
78
+ // same start position because regex alternation is leftmost-first; a
79
+ // dollar-delimited span at column N wins over a `*` at column N+1, so
80
+ // `$P=a*b*c$` renders as math instead of having `*b*` corrupted into
81
+ // italics. Single-character minimums and "no space adjacent to delimiter"
82
+ // rules keep currency prose like `$5 to $10` from being swallowed.
83
+ export const INLINE_RE = new RegExp([
84
+ `!\\[(.*?)\\]\\(${MD_URL_RE}\\)`, // 1,2 image
85
+ `\\[(.+?)\\]\\(${MD_URL_RE}\\)`, // 3,4 link
86
+ `<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>`, // 5 autolink
87
+ `~~(.+?)~~`, // 6 strike
88
+ `\`([^\\\`]+)\``, // 7 code
89
+ `\\*\\*(.+?)\\*\\*`, // 8 bold *
90
+ `(?<!\\w)__(.+?)__(?!\\w)`, // 9 bold _
91
+ `\\*(.+?)\\*`, // 10 italic *
92
+ `(?<!\\w)_(.+?)_(?!\\w)`, // 11 italic _
93
+ `==(.+?)==`, // 12 highlight
94
+ `\\[\\^([^\\]]+)\\]`, // 13 footnote ref
95
+ `\\^([^^\\s][^^]*?)\\^`, // 14 superscript
96
+ `~([A-Za-z0-9]{1,8})~`, // 15 subscript
97
+ `(https?:\\/\\/[^\\s<]+)`, // 16 bare URL — wrapped so it owns its own
98
+ // capture group; without this, the math
99
+ // spans below would land in m[16] and the
100
+ // MdInline dispatcher would treat them as
101
+ // bare URLs and render them as autolinks.
102
+ `(?<!\\$)\\$([^\\s$](?:[^$\\n]*?[^\\s$])?)\\$(?!\\$)`, // 17 inline math $...$
103
+ `\\\\\\(([^\\n]+?)\\\\\\)` // 18 inline math \(...\)
104
+ ].join('|'), 'g');
105
+ const indentDepth = (s) => Math.floor(s.replace(/\t/g, ' ').length / 2);
106
+ const splitRow = (row) => row
107
+ .trim()
108
+ .replace(/^\|/, '')
109
+ .replace(/\|$/, '')
110
+ .split('|')
111
+ .map(c => c.trim());
112
+ const isTableDivider = (row) => {
113
+ const cells = splitRow(row);
114
+ return cells.length > 1 && cells.every(c => TABLE_DIVIDER_CELL_RE.test(c));
115
+ };
116
+ const autolinkUrl = (raw) => raw.startsWith('mailto:') || raw.startsWith('http') || !raw.includes('@') ? raw : `mailto:${raw}`;
117
+ const renderAutolink = (k, t, raw) => (_jsx(Link, { url: autolinkUrl(raw), children: _jsx(Text, { color: t.color.accent, underline: true, children: raw.replace(/^mailto:/, '') }) }, k));
118
+ export const stripInlineMarkup = (v) => v
119
+ .replace(/!\[(.*?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '[image: $1] $2')
120
+ .replace(/\[(.+?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '$1')
121
+ .replace(/<((?:https?:\/\/|mailto:)[^>\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, '$1')
122
+ .replace(/~~(.+?)~~/g, '$1')
123
+ .replace(/`([^`]+)`/g, '$1')
124
+ .replace(/\*\*(.+?)\*\*/g, '$1')
125
+ .replace(/(?<!\w)__(.+?)__(?!\w)/g, '$1')
126
+ .replace(/\*(.+?)\*/g, '$1')
127
+ .replace(/(?<!\w)_(.+?)_(?!\w)/g, '$1')
128
+ .replace(/==(.+?)==/g, '$1')
129
+ .replace(/\[\^([^\]]+)\]/g, '[$1]')
130
+ .replace(/\^([^^\s][^^]*?)\^/g, '^$1')
131
+ .replace(/~([A-Za-z0-9]{1,8})~/g, '_$1')
132
+ .replace(/(?<!\$)\$([^\s$](?:[^$\n]*?[^\s$])?)\$(?!\$)/g, '$1')
133
+ .replace(/\\\(([^\n]+?)\\\)/g, '$1');
134
+ const renderTable = (k, rows, t) => {
135
+ const widths = rows[0].map((_, ci) => Math.max(...rows.map(r => stripInlineMarkup(r[ci] ?? '').length)));
136
+ // Thin divider under the header. Without it tables look like prose
137
+ // with extra spacing because the header is just accent-coloured text
138
+ // (#15534). We avoid full borders on purpose — column widths come
139
+ // from `stripInlineMarkup(...).length` (UTF-16 code units, not
140
+ // display width), so a real outline often misaligns on emoji and
141
+ // East-Asian wide characters; one dim solid rule (`─`) under row 0
142
+ // plus tab-style column gaps reads cleanly on every terminal we
143
+ // tested.
144
+ const sep = widths.map(w => '─'.repeat(Math.max(1, w))).join(' ');
145
+ return (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: rows.map((row, ri) => (_jsxs(Fragment, { children: [_jsx(Box, { children: widths.map((w, ci) => (_jsxs(Text, { bold: ri === 0, color: ri === 0 ? t.color.accent : undefined, children: [_jsx(MdInline, { t: t, text: row[ci] ?? '' }), ' '.repeat(Math.max(0, w - stripInlineMarkup(row[ci] ?? '').length)), ci < widths.length - 1 ? ' ' : ''] }, ci))) }), ri === 0 && rows.length > 1 ? (_jsx(Text, { color: t.color.muted, dimColor: true, children: sep })) : null] }, ri))) }, k));
146
+ };
147
+ function MdInline({ t, text }) {
148
+ const parts = [];
149
+ let last = 0;
150
+ for (const m of text.matchAll(INLINE_RE)) {
151
+ const i = m.index ?? 0;
152
+ const k = parts.length;
153
+ if (i > last) {
154
+ parts.push(_jsx(Text, { children: text.slice(last, i) }, k));
155
+ }
156
+ if (m[1] && m[2]) {
157
+ parts.push(_jsxs(Text, { color: t.color.muted, children: ["[image: ", m[1], "] ", m[2]] }, parts.length));
158
+ }
159
+ else if (m[3] && m[4]) {
160
+ parts.push(_jsx(Link, { url: m[4], children: _jsx(Text, { color: t.color.accent, underline: true, children: m[3] }) }, parts.length));
161
+ }
162
+ else if (m[5]) {
163
+ parts.push(renderAutolink(parts.length, t, m[5]));
164
+ }
165
+ else if (m[6]) {
166
+ parts.push(_jsx(Text, { strikethrough: true, children: _jsx(MdInline, { t: t, text: m[6] }) }, parts.length));
167
+ }
168
+ else if (m[7]) {
169
+ // Code is the one wrap that does NOT recurse — inline `code` spans
170
+ // are verbatim by definition. Letting MdInline reprocess them
171
+ // would corrupt regex examples and shell snippets.
172
+ parts.push(_jsx(Text, { color: t.color.accent, dimColor: true, children: m[7] }, parts.length));
173
+ }
174
+ else if (m[8] ?? m[9]) {
175
+ // Recurse into bold / italic / strike / highlight so nested
176
+ // `$...$` math (and other inline tokens) inside a `**bolded
177
+ // statement with $\mathbb{Z}$ math**` actually render. Without
178
+ // this the inner content is dropped into a single `<Text bold>`
179
+ // verbatim and the math renderer never sees it.
180
+ parts.push(_jsx(Text, { bold: true, children: _jsx(MdInline, { t: t, text: m[8] ?? m[9] }) }, parts.length));
181
+ }
182
+ else if (m[10] ?? m[11]) {
183
+ parts.push(_jsx(Text, { italic: true, children: _jsx(MdInline, { t: t, text: m[10] ?? m[11] }) }, parts.length));
184
+ }
185
+ else if (m[12]) {
186
+ parts.push(_jsx(Text, { backgroundColor: t.color.diffAdded, color: t.color.diffAddedWord, children: _jsx(MdInline, { t: t, text: m[12] }) }, parts.length));
187
+ }
188
+ else if (m[13]) {
189
+ parts.push(_jsxs(Text, { color: t.color.muted, children: ["[", m[13], "]"] }, parts.length));
190
+ }
191
+ else if (m[14]) {
192
+ parts.push(_jsxs(Text, { color: t.color.muted, children: ["^", m[14]] }, parts.length));
193
+ }
194
+ else if (m[15]) {
195
+ parts.push(_jsxs(Text, { color: t.color.muted, children: ["_", m[15]] }, parts.length));
196
+ }
197
+ else if (m[16]) {
198
+ // Bare URL — trim trailing prose punctuation into a sibling text node
199
+ // so `see https://x.com/, which…` keeps the comma outside the link.
200
+ const url = m[16].replace(/[),.;:!?]+$/g, '');
201
+ parts.push(renderAutolink(parts.length, t, url));
202
+ if (url.length < m[16].length) {
203
+ parts.push(_jsx(Text, { children: m[16].slice(url.length) }, parts.length));
204
+ }
205
+ }
206
+ else if (m[17] ?? m[18]) {
207
+ // Inline math is run through `texToUnicode` (Greek letters, ℕℤℚℝ,
208
+ // operators, sub/superscripts, fractions) and rendered in italic
209
+ // accent. Italic is the disambiguator — links use accent+underline,
210
+ // so without italic readers can't tell `\mathbb{R}` (math) from a
211
+ // hyperlinked word. Anything `texToUnicode` doesn't recognise is
212
+ // preserved verbatim, so unfamiliar commands just look like their
213
+ // raw LaTeX rather than vanishing.
214
+ parts.push(_jsx(Text, { color: t.color.accent, italic: true, children: renderMath(texToUnicode(m[17] ?? m[18])) }, parts.length));
215
+ }
216
+ last = i + m[0].length;
217
+ }
218
+ if (last < text.length) {
219
+ parts.push(_jsx(Text, { children: text.slice(last) }, parts.length));
220
+ }
221
+ return _jsx(Text, { wrap: "wrap-trim", children: parts.length ? parts : text });
222
+ }
223
+ // Cross-instance parsed-children cache: useMemo's per-instance cache dies
224
+ // on remount, so virtualization re-parses every row that scrolls back into
225
+ // view. Theme-keyed WeakMap drops stale palettes; inner Map is LRU-bounded.
226
+ const MD_CACHE_LIMIT = 512;
227
+ const mdCache = new WeakMap();
228
+ const cacheBucket = (t) => {
229
+ const b = mdCache.get(t);
230
+ if (b) {
231
+ return b;
232
+ }
233
+ const fresh = new Map();
234
+ mdCache.set(t, fresh);
235
+ return fresh;
236
+ };
237
+ const cacheGet = (b, key) => {
238
+ const v = b.get(key);
239
+ if (v) {
240
+ b.delete(key);
241
+ b.set(key, v);
242
+ }
243
+ return v;
244
+ };
245
+ const cacheSet = (b, key, v) => {
246
+ b.set(key, v);
247
+ if (b.size > MD_CACHE_LIMIT) {
248
+ b.delete(b.keys().next().value);
249
+ }
250
+ };
251
+ function MdImpl({ compact, t, text }) {
252
+ const nodes = useMemo(() => {
253
+ const bucket = cacheBucket(t);
254
+ const cacheKey = `${compact ? '1' : '0'}|${text}`;
255
+ const cached = cacheGet(bucket, cacheKey);
256
+ if (cached) {
257
+ return cached;
258
+ }
259
+ const lines = ensureEmojiPresentation(text).split('\n');
260
+ const nodes = [];
261
+ let prevKind = null;
262
+ let i = 0;
263
+ const gap = () => {
264
+ if (nodes.length && prevKind !== 'blank') {
265
+ nodes.push(_jsx(Text, { children: " " }, `gap-${nodes.length}`));
266
+ prevKind = 'blank';
267
+ }
268
+ };
269
+ const start = (kind) => {
270
+ if (prevKind && prevKind !== 'blank' && prevKind !== kind) {
271
+ gap();
272
+ }
273
+ prevKind = kind;
274
+ };
275
+ while (i < lines.length) {
276
+ const line = lines[i];
277
+ const key = nodes.length;
278
+ if (!line.trim()) {
279
+ if (!compact) {
280
+ gap();
281
+ }
282
+ i++;
283
+ continue;
284
+ }
285
+ if (AUDIO_DIRECTIVE_RE.test(line)) {
286
+ i++;
287
+ continue;
288
+ }
289
+ const media = line.match(MEDIA_LINE_RE)?.[1];
290
+ if (media) {
291
+ start('paragraph');
292
+ nodes.push(_jsxs(Text, { color: t.color.muted, wrap: "wrap-trim", children: ['▸ ', _jsx(Link, { url: /^(?:\/|[a-z]:[\\/])/i.test(media) ? `file://${media}` : media, children: _jsx(Text, { color: t.color.accent, underline: true, children: media }) })] }, key));
293
+ i++;
294
+ continue;
295
+ }
296
+ const fence = line.match(FENCE_RE);
297
+ if (fence) {
298
+ const char = fence[1][0];
299
+ const len = fence[1].length;
300
+ const lang = fence[2].trim().toLowerCase();
301
+ const block = [];
302
+ for (i++; i < lines.length; i++) {
303
+ const close = lines[i].match(FENCE_CLOSE_RE)?.[1];
304
+ if (close && close[0] === char && close.length >= len) {
305
+ break;
306
+ }
307
+ block.push(lines[i]);
308
+ }
309
+ if (i < lines.length) {
310
+ i++;
311
+ }
312
+ if (['md', 'markdown'].includes(lang)) {
313
+ start('paragraph');
314
+ nodes.push(_jsx(Md, { compact: compact, t: t, text: block.join('\n') }, key));
315
+ continue;
316
+ }
317
+ start('code');
318
+ const isDiff = lang === 'diff';
319
+ const highlighted = !isDiff && isHighlightable(lang);
320
+ nodes.push(_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [lang && !isDiff && _jsx(Text, { color: t.color.muted, children: '─ ' + lang }), block.map((l, j) => {
321
+ if (highlighted) {
322
+ return (_jsx(Text, { children: highlightLine(l, lang, t).map(([color, text], kk) => color ? (_jsx(Text, { color: color, children: text }, kk)) : (_jsx(Text, { children: text }, kk))) }, j));
323
+ }
324
+ const add = isDiff && l.startsWith('+');
325
+ const del = isDiff && l.startsWith('-');
326
+ const hunk = isDiff && l.startsWith('@@');
327
+ return (_jsx(Text, { backgroundColor: add ? t.color.diffAdded : del ? t.color.diffRemoved : undefined, color: add ? t.color.diffAddedWord : del ? t.color.diffRemovedWord : hunk ? t.color.muted : undefined, dimColor: isDiff && !add && !del && !hunk && l.startsWith(' '), children: l }, j));
328
+ })] }, key));
329
+ continue;
330
+ }
331
+ const mathOpen = line.match(MATH_BLOCK_OPEN_RE);
332
+ if (mathOpen) {
333
+ const opener = mathOpen[1];
334
+ const closeRe = opener === '$$' ? MATH_BLOCK_CLOSE_DOLLAR_RE : MATH_BLOCK_CLOSE_BRACKET_RE;
335
+ const headRest = mathOpen[2] ?? '';
336
+ const block = [];
337
+ // Single-line block: `$$x + y = z$$` or `\[x\]`. Capture inner content
338
+ // and emit the block immediately. Without this, the close-scan loop
339
+ // skips line `i` and treats the next opener as our closer, swallowing
340
+ // every paragraph in between.
341
+ const sameLineClose = headRest.match(closeRe);
342
+ if (sameLineClose) {
343
+ const inner = sameLineClose[1].trim();
344
+ start('code');
345
+ nodes.push(_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: inner ? _jsx(Text, { color: t.color.accent, children: renderMath(texToUnicode(inner)) }) : null }, key));
346
+ i++;
347
+ continue;
348
+ }
349
+ // Multi-line block: scan ahead for a real closer before committing.
350
+ // If none exists in the rest of the document, render this line as a
351
+ // paragraph instead of consuming everything that follows.
352
+ let closeIdx = -1;
353
+ for (let j = i + 1; j < lines.length; j++) {
354
+ if (closeRe.test(lines[j])) {
355
+ closeIdx = j;
356
+ break;
357
+ }
358
+ }
359
+ if (closeIdx < 0) {
360
+ start('paragraph');
361
+ nodes.push(_jsx(MdInline, { t: t, text: line }, key));
362
+ i++;
363
+ continue;
364
+ }
365
+ if (headRest.trim()) {
366
+ block.push(headRest);
367
+ }
368
+ for (let j = i + 1; j < closeIdx; j++) {
369
+ block.push(lines[j]);
370
+ }
371
+ const tail = lines[closeIdx].match(closeRe)[1].trimEnd();
372
+ if (tail.trim()) {
373
+ block.push(tail);
374
+ }
375
+ start('code');
376
+ nodes.push(_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: block.map((l, j) => (_jsx(Text, { color: t.color.accent, children: renderMath(texToUnicode(l)) }, j))) }, key));
377
+ i = closeIdx + 1;
378
+ continue;
379
+ }
380
+ const heading = line.match(HEADING_RE)?.[2];
381
+ if (heading) {
382
+ start('heading');
383
+ nodes.push(_jsx(Text, { bold: true, color: t.color.accent, wrap: "wrap-trim", children: _jsx(MdInline, { t: t, text: heading }) }, key));
384
+ i++;
385
+ continue;
386
+ }
387
+ if (i + 1 < lines.length && SETEXT_RE.test(lines[i + 1])) {
388
+ start('heading');
389
+ nodes.push(_jsx(Text, { bold: true, color: t.color.accent, wrap: "wrap-trim", children: _jsx(MdInline, { t: t, text: line.trim() }) }, key));
390
+ i += 2;
391
+ continue;
392
+ }
393
+ if (HR_RE.test(line)) {
394
+ start('rule');
395
+ nodes.push(_jsx(Text, { color: t.color.muted, children: '─'.repeat(36) }, key));
396
+ i++;
397
+ continue;
398
+ }
399
+ const footnote = line.match(FOOTNOTE_RE);
400
+ if (footnote) {
401
+ start('list');
402
+ nodes.push(_jsxs(Text, { color: t.color.muted, wrap: "wrap-trim", children: ["[", footnote[1], "] ", _jsx(MdInline, { t: t, text: footnote[2] ?? '' })] }, key));
403
+ i++;
404
+ while (i < lines.length && /^\s{2,}\S/.test(lines[i])) {
405
+ nodes.push(_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: t.color.muted, wrap: "wrap-trim", children: _jsx(MdInline, { t: t, text: lines[i].trim() }) }) }, `${key}-cont-${i}`));
406
+ i++;
407
+ }
408
+ continue;
409
+ }
410
+ if (i + 1 < lines.length && DEF_RE.test(lines[i + 1])) {
411
+ start('list');
412
+ nodes.push(_jsx(Text, { bold: true, wrap: "wrap-trim", children: line.trim() }, key));
413
+ i++;
414
+ while (i < lines.length) {
415
+ const def = lines[i].match(DEF_RE)?.[1];
416
+ if (!def) {
417
+ break;
418
+ }
419
+ nodes.push(_jsxs(Text, { wrap: "wrap-trim", children: [_jsx(Text, { color: t.color.muted, children: " \u00B7 " }), _jsx(MdInline, { t: t, text: def })] }, `${key}-def-${i}`));
420
+ i++;
421
+ }
422
+ continue;
423
+ }
424
+ const bullet = line.match(BULLET_RE);
425
+ if (bullet) {
426
+ start('list');
427
+ const task = bullet[2].match(TASK_RE);
428
+ const marker = task ? (task[1].toLowerCase() === 'x' ? '☑' : '☐') : '•';
429
+ nodes.push(_jsx(Box, { paddingLeft: indentDepth(bullet[1]) * 2, children: _jsxs(Text, { wrap: "wrap-trim", children: [_jsxs(Text, { color: t.color.muted, children: [marker, " "] }), _jsx(MdInline, { t: t, text: task ? task[2] : bullet[2] })] }) }, key));
430
+ i++;
431
+ continue;
432
+ }
433
+ const numbered = line.match(NUMBERED_RE);
434
+ if (numbered) {
435
+ start('list');
436
+ nodes.push(_jsx(Box, { paddingLeft: indentDepth(numbered[1]) * 2, children: _jsxs(Text, { wrap: "wrap-trim", children: [_jsxs(Text, { color: t.color.muted, children: [numbered[2], ". "] }), _jsx(MdInline, { t: t, text: numbered[3] })] }) }, key));
437
+ i++;
438
+ continue;
439
+ }
440
+ if (QUOTE_RE.test(line)) {
441
+ start('quote');
442
+ const quoteLines = [];
443
+ while (i < lines.length && QUOTE_RE.test(lines[i])) {
444
+ const prefix = lines[i].match(QUOTE_RE)?.[0] ?? '';
445
+ quoteLines.push({ depth: (prefix.match(/>/g) ?? []).length, text: lines[i].slice(prefix.length) });
446
+ i++;
447
+ }
448
+ nodes.push(_jsx(Box, { flexDirection: "column", children: quoteLines.map((ql, qi) => (_jsx(Box, { paddingLeft: Math.max(0, ql.depth - 1) * 2, children: _jsxs(Text, { color: t.color.muted, wrap: "wrap-trim", children: ["\u2502 ", _jsx(MdInline, { t: t, text: ql.text })] }) }, qi))) }, key));
449
+ continue;
450
+ }
451
+ if (line.includes('|') && i + 1 < lines.length && isTableDivider(lines[i + 1])) {
452
+ start('table');
453
+ const rows = [splitRow(line)];
454
+ for (i += 2; i < lines.length && lines[i].includes('|') && lines[i].trim(); i++) {
455
+ rows.push(splitRow(lines[i]));
456
+ }
457
+ nodes.push(renderTable(key, rows, t));
458
+ continue;
459
+ }
460
+ if (/^<\/?details\b/i.test(line)) {
461
+ i++;
462
+ continue;
463
+ }
464
+ const summary = line.match(/^<summary>(.*?)<\/summary>$/i)?.[1];
465
+ if (summary) {
466
+ start('paragraph');
467
+ nodes.push(_jsxs(Text, { color: t.color.muted, wrap: "wrap-trim", children: ["\u25B6 ", summary] }, key));
468
+ i++;
469
+ continue;
470
+ }
471
+ if (/^<\/?[^>]+>$/.test(line.trim())) {
472
+ start('paragraph');
473
+ nodes.push(_jsx(Text, { color: t.color.muted, wrap: "wrap-trim", children: line.trim() }, key));
474
+ i++;
475
+ continue;
476
+ }
477
+ if (line.includes('|') && line.trim().startsWith('|')) {
478
+ start('table');
479
+ const rows = [];
480
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
481
+ const row = lines[i].trim();
482
+ if (!/^[|\s:-]+$/.test(row)) {
483
+ rows.push(splitRow(row));
484
+ }
485
+ i++;
486
+ }
487
+ if (rows.length) {
488
+ nodes.push(renderTable(key, rows, t));
489
+ }
490
+ continue;
491
+ }
492
+ start('paragraph');
493
+ nodes.push(_jsx(MdInline, { t: t, text: line }, key));
494
+ i++;
495
+ }
496
+ cacheSet(bucket, cacheKey, nodes);
497
+ return nodes;
498
+ }, [compact, t, text]);
499
+ return _jsx(Box, { flexDirection: "column", children: nodes });
500
+ }
501
+ export const Md = memo(MdImpl);
@@ -0,0 +1,12 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } 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 { useState } from 'react';
8
+ import { TextInput } from './textInput.js';
9
+ export function MaskedPrompt({ cols = 80, icon, label, onSubmit, sub, t }) {
10
+ const [value, setValue] = useState('');
11
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: t.color.warn, children: [icon, " ", label] }), sub && _jsxs(Text, { color: t.color.muted, children: [" ", sub] }), _jsxs(Box, { children: [_jsx(Text, { color: t.color.label, children: '> ' }), _jsx(TextInput, { columns: Math.max(20, cols - 6), mask: "*", onChange: setValue, onSubmit: onSubmit, value: value })] })] }));
12
+ }
@@ -0,0 +1,82 @@
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 { Ansi, Box, NoSelect, Text } from '../vendor/cvc-ink/index.js';
7
+ import { memo, useState } from 'react';
8
+ import { LONG_MSG } from '../config/limits.js';
9
+ import { sectionMode } from '../domain/details.js';
10
+ import { userDisplay } from '../domain/messages.js';
11
+ import { ROLE } from '../domain/roles.js';
12
+ import { transcriptBodyWidth, transcriptGutterWidth } from '../lib/inputMetrics.js';
13
+ import { boundedHistoryRenderText, boundedLiveRenderText, compactPreview, hasAnsi, isPasteBackedText, stripAnsi } from '../lib/text.js';
14
+ import { Md } from './markdown.js';
15
+ import { StreamingMd } from './streamingMarkdown.js';
16
+ import { ToolTrail } from './thinking.js';
17
+ import { TodoPanel } from './todoPanel.js';
18
+ // Collapse threshold for long system messages (system prompt etc.)
19
+ const SYSTEM_COLLAPSE_CHARS = 400;
20
+ export const MessageLine = memo(function MessageLine({ cols, compact, detailsMode = 'collapsed', detailsModeCommandOverride = false, isStreaming = false, limitHistoryRender = false, msg, sections, t, tools = [] }) {
21
+ // Per-section overrides win over the global mode, so resolve each section
22
+ // we might consume here once and gate visibility on the *content-bearing*
23
+ // sections only — never on the global mode. A `trail` message feeds Tool
24
+ // calls + Activity; an assistant message with thinking/tools metadata
25
+ // feeds Thinking + Tool calls. Gating on every section would let
26
+ // `thinking` (expanded by default) keep an empty wrapper alive when only
27
+ // `tools` is hidden — exactly the empty-Box bug Copilot caught.
28
+ const thinkingMode = sectionMode('thinking', detailsMode, sections, detailsModeCommandOverride);
29
+ const toolsMode = sectionMode('tools', detailsMode, sections, detailsModeCommandOverride);
30
+ const activityMode = sectionMode('activity', detailsMode, sections, detailsModeCommandOverride);
31
+ const thinking = msg.thinking?.trim() ?? '';
32
+ // Collapse toggle for long system messages
33
+ const systemIsLong = msg.role === 'system' && msg.text.length > SYSTEM_COLLAPSE_CHARS;
34
+ const [systemOpen, setSystemOpen] = useState(false);
35
+ if (msg.kind === 'trail' && msg.todos?.length) {
36
+ return (_jsx(TodoPanel, { defaultCollapsed: msg.todoCollapsedByDefault, incomplete: msg.todoIncomplete, t: t, todos: msg.todos }));
37
+ }
38
+ if (msg.kind === 'trail' && (msg.tools?.length || tools.length || thinking)) {
39
+ return thinkingMode !== 'hidden' || toolsMode !== 'hidden' || activityMode !== 'hidden' ? (_jsx(Box, { flexDirection: "column", children: _jsx(ToolTrail, { commandOverride: detailsModeCommandOverride, detailsMode: detailsMode, reasoning: thinking, reasoningTokens: msg.thinkingTokens, sections: sections, t: t, tools: tools, toolTokens: msg.toolTokens, trail: msg.tools ?? [] }) })) : null;
40
+ }
41
+ if (msg.role === 'tool') {
42
+ const maxChars = Math.max(24, cols - 14);
43
+ const stripped = hasAnsi(msg.text) ? stripAnsi(msg.text) : msg.text;
44
+ const preview = compactPreview(stripped, maxChars) || '(empty tool result)';
45
+ return (_jsx(Box, { alignSelf: "flex-start", borderColor: t.color.muted, borderStyle: "round", marginLeft: 3, paddingX: 1, children: hasAnsi(msg.text) ? (_jsx(Text, { wrap: "truncate-end", children: _jsx(Ansi, { children: msg.text }) })) : (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: preview })) }));
46
+ }
47
+ const { body, glyph, prefix } = ROLE[msg.role](t);
48
+ const gutterWidth = transcriptGutterWidth(msg.role, t.brand.prompt);
49
+ const showDetails = (toolsMode !== 'hidden' && Boolean(msg.tools?.length)) || (thinkingMode !== 'hidden' && Boolean(thinking));
50
+ const content = (() => {
51
+ if (msg.kind === 'slash') {
52
+ return _jsx(Text, { color: t.color.muted, children: msg.text });
53
+ }
54
+ // ── Collapsible long system message (system prompt, AGENTS.md, etc.) ──
55
+ // MUST come before the hasAnsi check — system messages from the backend
56
+ // contain Rich markup escape codes that would otherwise hit <Ansi> full render.
57
+ if (systemIsLong) {
58
+ const firstLine = (msg.text.split('\n')[0] ?? '').trim().slice(0, 120) || '(system message)';
59
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { onClick: () => setSystemOpen(v => !v), children: [_jsx(Text, { color: t.color.accent, children: systemOpen ? '▾ ' : '▸ ' }), _jsx(Text, { color: t.color.muted, children: firstLine }), _jsxs(Text, { color: t.color.muted, dimColor: true, children: [' — ', msg.text.length.toLocaleString(), " chars"] })] }), systemOpen && _jsx(Ansi, { children: msg.text })] }));
60
+ }
61
+ if (msg.role !== 'user' && hasAnsi(msg.text)) {
62
+ return _jsx(Ansi, { children: msg.text });
63
+ }
64
+ if (msg.role === 'assistant') {
65
+ return isStreaming ? (
66
+ // Incremental markdown: split at the last stable block boundary so
67
+ // only the in-flight tail re-tokenizes per delta. See
68
+ // streamingMarkdown.tsx for the cost model.
69
+ _jsx(StreamingMd, { compact: compact, t: t, text: boundedLiveRenderText(msg.text) })) : (_jsx(Md, { compact: compact, t: t, text: limitHistoryRender ? boundedHistoryRenderText(msg.text) : msg.text }));
70
+ }
71
+ if (msg.role === 'user' && msg.text.length > LONG_MSG && isPasteBackedText(msg.text)) {
72
+ const [head, ...rest] = userDisplay(msg.text).split('[long message]');
73
+ return (_jsxs(Text, { color: body, children: [head, _jsx(Text, { color: t.color.muted, dimColor: true, children: "[long message]" }), rest.join('')] }));
74
+ }
75
+ return _jsx(Text, { ...(body ? { color: body } : {}), children: msg.text });
76
+ })();
77
+ // Diff segments (emitted by pushInlineDiffSegment between narration
78
+ // segments) need a blank line on both sides so the patch doesn't butt up
79
+ // against the prose around it.
80
+ const isDiffSegment = msg.kind === 'diff';
81
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: msg.role === 'user' || isDiffSegment ? 1 : 0, marginTop: msg.role === 'user' || msg.kind === 'slash' || isDiffSegment ? 1 : 0, children: [showDetails && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(ToolTrail, { commandOverride: detailsModeCommandOverride, detailsMode: detailsMode, reasoning: thinking, reasoningTokens: msg.thinkingTokens, sections: sections, t: t, toolTokens: msg.toolTokens, trail: msg.tools }) })), _jsxs(Box, { children: [_jsx(NoSelect, { flexShrink: 0, fromLeftEdge: true, width: gutterWidth, children: _jsxs(Text, { bold: msg.role === 'user', color: prefix, children: [glyph, ' '] }) }), _jsx(Box, { width: transcriptBodyWidth(cols, msg.role, t.brand.prompt), children: content })] })] }));
82
+ });