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,287 @@
1
+ const ROOT_KEY = '__root__';
2
+ /**
3
+ * Reconstruct the subagent spawn tree from a flat event-ordered list.
4
+ *
5
+ * Grouping is by `parentId`; a missing `parentId` (or one pointing at an
6
+ * unknown subagent) is treated as a top-level spawn of the current turn.
7
+ * Children within a parent are sorted by `depth` then `index` — same key
8
+ * used in `turnController.upsertSubagent`, so render order matches spawn
9
+ * order regardless of network reordering of gateway events.
10
+ *
11
+ * Older gateways omit `parentId`; every subagent is then a top-level node
12
+ * and the tree renders flat — matching pre-observability behaviour.
13
+ */
14
+ export function buildSubagentTree(items) {
15
+ if (!items.length) {
16
+ return [];
17
+ }
18
+ const byParent = new Map();
19
+ const known = new Set();
20
+ for (const item of items) {
21
+ known.add(item.id);
22
+ }
23
+ for (const item of items) {
24
+ const parentKey = item.parentId && known.has(item.parentId) ? item.parentId : ROOT_KEY;
25
+ const bucket = byParent.get(parentKey) ?? [];
26
+ bucket.push(item);
27
+ byParent.set(parentKey, bucket);
28
+ }
29
+ for (const bucket of byParent.values()) {
30
+ bucket.sort((a, b) => a.depth - b.depth || a.index - b.index);
31
+ }
32
+ const build = (item) => {
33
+ const kids = byParent.get(item.id) ?? [];
34
+ const children = kids.map(build);
35
+ return { aggregate: aggregate(item, children), children, item };
36
+ };
37
+ return (byParent.get(ROOT_KEY) ?? []).map(build);
38
+ }
39
+ /**
40
+ * Roll up counts for a node's whole subtree. Kept pure so the live view
41
+ * and the post-hoc replay can share the same renderer unchanged.
42
+ *
43
+ * `hotness` = tools per second across the subtree — a crude proxy for
44
+ * "how much work is happening in this branch". Used to colour tree rails
45
+ * in the overlay / inline view so the eye spots the expensive branch.
46
+ */
47
+ export function aggregate(item, children) {
48
+ let totalTools = item.toolCount ?? 0;
49
+ let totalDuration = item.durationSeconds ?? 0;
50
+ let descendantCount = 0;
51
+ let activeCount = isRunning(item) ? 1 : 0;
52
+ let maxDepthFromHere = 0;
53
+ let inputTokens = item.inputTokens ?? 0;
54
+ let outputTokens = item.outputTokens ?? 0;
55
+ let costUsd = item.costUsd ?? 0;
56
+ let filesTouched = (item.filesRead?.length ?? 0) + (item.filesWritten?.length ?? 0);
57
+ for (const child of children) {
58
+ totalTools += child.aggregate.totalTools;
59
+ totalDuration += child.aggregate.totalDuration;
60
+ descendantCount += child.aggregate.descendantCount + 1;
61
+ activeCount += child.aggregate.activeCount;
62
+ maxDepthFromHere = Math.max(maxDepthFromHere, child.aggregate.maxDepthFromHere + 1);
63
+ inputTokens += child.aggregate.inputTokens;
64
+ outputTokens += child.aggregate.outputTokens;
65
+ costUsd += child.aggregate.costUsd;
66
+ filesTouched += child.aggregate.filesTouched;
67
+ }
68
+ const hotness = totalDuration > 0 ? totalTools / totalDuration : 0;
69
+ return {
70
+ activeCount,
71
+ costUsd,
72
+ descendantCount,
73
+ filesTouched,
74
+ hotness,
75
+ inputTokens,
76
+ maxDepthFromHere,
77
+ outputTokens,
78
+ totalDuration,
79
+ totalTools
80
+ };
81
+ }
82
+ /**
83
+ * Count of subagents at each depth level, indexed by depth (0 = top level).
84
+ * Drives the inline sparkline (`▁▃▇▅`) and the status-bar HUD.
85
+ */
86
+ export function widthByDepth(tree) {
87
+ const widths = [];
88
+ const walk = (nodes, depth) => {
89
+ if (!nodes.length) {
90
+ return;
91
+ }
92
+ widths[depth] = (widths[depth] ?? 0) + nodes.length;
93
+ for (const node of nodes) {
94
+ walk(node.children, depth + 1);
95
+ }
96
+ };
97
+ walk(tree, 0);
98
+ return widths;
99
+ }
100
+ /**
101
+ * Flat totals across the full tree — feeds the summary chip header.
102
+ */
103
+ export function treeTotals(tree) {
104
+ let totalTools = 0;
105
+ let totalDuration = 0;
106
+ let descendantCount = 0;
107
+ let activeCount = 0;
108
+ let maxDepthFromHere = 0;
109
+ let inputTokens = 0;
110
+ let outputTokens = 0;
111
+ let costUsd = 0;
112
+ let filesTouched = 0;
113
+ for (const node of tree) {
114
+ totalTools += node.aggregate.totalTools;
115
+ totalDuration += node.aggregate.totalDuration;
116
+ descendantCount += node.aggregate.descendantCount + 1;
117
+ activeCount += node.aggregate.activeCount;
118
+ maxDepthFromHere = Math.max(maxDepthFromHere, node.aggregate.maxDepthFromHere + 1);
119
+ inputTokens += node.aggregate.inputTokens;
120
+ outputTokens += node.aggregate.outputTokens;
121
+ costUsd += node.aggregate.costUsd;
122
+ filesTouched += node.aggregate.filesTouched;
123
+ }
124
+ const hotness = totalDuration > 0 ? totalTools / totalDuration : 0;
125
+ return {
126
+ activeCount,
127
+ costUsd,
128
+ descendantCount,
129
+ filesTouched,
130
+ hotness,
131
+ inputTokens,
132
+ maxDepthFromHere,
133
+ outputTokens,
134
+ totalDuration,
135
+ totalTools
136
+ };
137
+ }
138
+ /**
139
+ * Flatten the tree into visit order — useful for keyboard navigation and
140
+ * for "kill subtree" walks that fire one RPC per descendant.
141
+ */
142
+ export function flattenTree(tree) {
143
+ const out = [];
144
+ const walk = (nodes) => {
145
+ for (const node of nodes) {
146
+ out.push(node);
147
+ walk(node.children);
148
+ }
149
+ };
150
+ walk(tree);
151
+ return out;
152
+ }
153
+ /**
154
+ * Collect every descendant's id for a given node (excluding the node itself).
155
+ */
156
+ export function descendantIds(node) {
157
+ const ids = [];
158
+ const walk = (children) => {
159
+ for (const child of children) {
160
+ ids.push(child.item.id);
161
+ walk(child.children);
162
+ }
163
+ };
164
+ walk(node.children);
165
+ return ids;
166
+ }
167
+ export function isRunning(item) {
168
+ return item.status === 'running' || item.status === 'queued';
169
+ }
170
+ const SPARK_RAMP = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
171
+ /**
172
+ * 8-step unicode bar sparkline from a positive-integer array. Zeroes render
173
+ * as spaces so a sparse tree doesn't read as equal activity at every depth.
174
+ */
175
+ export function sparkline(values) {
176
+ if (!values.length) {
177
+ return '';
178
+ }
179
+ const max = Math.max(...values);
180
+ if (max <= 0) {
181
+ return ' '.repeat(values.length);
182
+ }
183
+ return values
184
+ .map(v => {
185
+ if (v <= 0) {
186
+ return ' ';
187
+ }
188
+ const idx = Math.min(SPARK_RAMP.length - 1, Math.max(0, Math.ceil((v / max) * (SPARK_RAMP.length - 1))));
189
+ return SPARK_RAMP[idx];
190
+ })
191
+ .join('');
192
+ }
193
+ /**
194
+ * Format totals into a compact one-line summary: `d2 · 7 agents · 124 tools · 2m 14s`
195
+ */
196
+ export function formatSummary(totals) {
197
+ const pieces = [`d${Math.max(0, totals.maxDepthFromHere)}`];
198
+ pieces.push(`${totals.descendantCount} agent${totals.descendantCount === 1 ? '' : 's'}`);
199
+ if (totals.totalTools > 0) {
200
+ pieces.push(`${totals.totalTools} tool${totals.totalTools === 1 ? '' : 's'}`);
201
+ }
202
+ if (totals.totalDuration > 0) {
203
+ pieces.push(fmtDuration(totals.totalDuration));
204
+ }
205
+ const tokens = totals.inputTokens + totals.outputTokens;
206
+ if (tokens > 0) {
207
+ pieces.push(`${fmtTokens(tokens)} tok`);
208
+ }
209
+ if (totals.costUsd > 0) {
210
+ pieces.push(fmtCost(totals.costUsd));
211
+ }
212
+ if (totals.activeCount > 0) {
213
+ pieces.push(`⚡${totals.activeCount}`);
214
+ }
215
+ return pieces.join(' · ');
216
+ }
217
+ /** Compact dollar amount: `$0.02`, `$1.34`, `$12.4` — never > 5 chars beyond the `$`. */
218
+ export function fmtCost(usd) {
219
+ if (!Number.isFinite(usd) || usd <= 0) {
220
+ return '';
221
+ }
222
+ if (usd < 0.01) {
223
+ return '<$0.01';
224
+ }
225
+ if (usd < 10) {
226
+ return `$${usd.toFixed(2)}`;
227
+ }
228
+ return `$${usd.toFixed(1)}`;
229
+ }
230
+ /** Compact token count: `12k`, `1.2k`, `542`. */
231
+ export function fmtTokens(n) {
232
+ if (!Number.isFinite(n) || n <= 0) {
233
+ return '0';
234
+ }
235
+ if (n < 1000) {
236
+ return String(Math.round(n));
237
+ }
238
+ if (n < 10_000) {
239
+ return `${(n / 1000).toFixed(1)}k`;
240
+ }
241
+ return `${Math.round(n / 1000)}k`;
242
+ }
243
+ /**
244
+ * `Ns` / `Nm` / `Nm Ss` formatter for seconds. Shared with the agents
245
+ * overlay so the timeline + list + summary all speak the same dialect.
246
+ */
247
+ export function fmtDuration(seconds) {
248
+ if (seconds < 60) {
249
+ return `${Math.max(0, Math.round(seconds))}s`;
250
+ }
251
+ const m = Math.floor(seconds / 60);
252
+ const s = Math.round(seconds - m * 60);
253
+ return s === 0 ? `${m}m` : `${m}m ${s}s`;
254
+ }
255
+ /**
256
+ * A subagent is top-level if it has no `parentId`, or its parent isn't in
257
+ * the same snapshot (orphaned by a pruned mid-flight root). Same rule
258
+ * `buildSubagentTree` uses — keep call sites consistent across the live
259
+ * view, disk label, and diff pane.
260
+ */
261
+ export function topLevelSubagents(items) {
262
+ const ids = new Set(items.map(s => s.id));
263
+ return items.filter(s => !s.parentId || !ids.has(s.parentId));
264
+ }
265
+ /**
266
+ * Normalize a node's hotness into a palette index 0..N-1 where N = buckets.
267
+ * Higher hotness = "hotter" colour. Normalized against the tree's peak hotness
268
+ * so a uniformly slow tree still shows gradient across its busiest branches.
269
+ */
270
+ export function hotnessBucket(hotness, peakHotness, buckets) {
271
+ if (!Number.isFinite(hotness) || hotness <= 0 || peakHotness <= 0 || buckets <= 1) {
272
+ return 0;
273
+ }
274
+ const ratio = Math.min(1, hotness / peakHotness);
275
+ return Math.min(buckets - 1, Math.max(0, Math.round(ratio * (buckets - 1))));
276
+ }
277
+ export function peakHotness(tree) {
278
+ let peak = 0;
279
+ const walk = (nodes) => {
280
+ for (const node of nodes) {
281
+ peak = Math.max(peak, node.aggregate.hotness);
282
+ walk(node.children);
283
+ }
284
+ };
285
+ walk(tree);
286
+ return peak;
287
+ }
@@ -0,0 +1,89 @@
1
+ const KW = (s) => new Set(s.split(/\s+/).filter(Boolean));
2
+ const TS = KW(`
3
+ abstract as async await break case catch class const continue debugger default delete do else enum export extends
4
+ false finally for from function get if implements import in instanceof interface is let new null of package private
5
+ protected public readonly return set static super switch this throw true try type typeof undefined var void while
6
+ with yield
7
+ `);
8
+ const PY = KW(`
9
+ False None True and as assert async await break class continue def del elif else except finally for from global if
10
+ import in is lambda nonlocal not or pass raise return try while with yield
11
+ `);
12
+ const SH = KW(`
13
+ if then else elif fi for in do done while until case esac function return break continue local export readonly
14
+ declare typeset
15
+ `);
16
+ const GO = KW(`
17
+ break case chan const continue default defer else fallthrough for func go goto if import interface map package range
18
+ return select struct switch type var nil true false
19
+ `);
20
+ const RUST = KW(`
21
+ as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut
22
+ pub ref return self Self static struct super trait true type unsafe use where while yield
23
+ `);
24
+ const SQL = KW(`
25
+ select from where and or not in is null as by group order limit offset insert into values update set delete create
26
+ table drop alter add column primary key foreign references join left right inner outer on
27
+ `);
28
+ const LANGS = {
29
+ go: { comment: '//', keywords: GO },
30
+ json: { comment: null, keywords: KW('true false null') },
31
+ py: { comment: '#', keywords: PY },
32
+ rust: { comment: '//', keywords: RUST },
33
+ sh: { comment: '#', keywords: SH },
34
+ sql: { comment: '--', keywords: SQL },
35
+ ts: { comment: '//', keywords: TS },
36
+ yaml: { comment: '#', keywords: KW('true false null yes no on off') }
37
+ };
38
+ const ALIAS = {
39
+ bash: 'sh',
40
+ javascript: 'ts',
41
+ js: 'ts',
42
+ jsx: 'ts',
43
+ python: 'py',
44
+ rs: 'rust',
45
+ shell: 'sh',
46
+ tsx: 'ts',
47
+ typescript: 'ts',
48
+ yml: 'yaml',
49
+ zsh: 'sh'
50
+ };
51
+ const resolve = (lang) => LANGS[ALIAS[lang] ?? lang] ?? null;
52
+ export const isHighlightable = (lang) => resolve(lang) !== null;
53
+ const TOKEN_RE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|\b\d+(?:\.\d+)?\b|[A-Za-z_$][\w$]*/g;
54
+ export function highlightLine(line, lang, t) {
55
+ const spec = resolve(lang);
56
+ if (!spec) {
57
+ return [['', line]];
58
+ }
59
+ if (spec.comment && line.trimStart().startsWith(spec.comment)) {
60
+ return [[t.color.muted, line]];
61
+ }
62
+ const tokens = [];
63
+ let last = 0;
64
+ for (const m of line.matchAll(TOKEN_RE)) {
65
+ const start = m.index ?? 0;
66
+ if (start > last) {
67
+ tokens.push(['', line.slice(last, start)]);
68
+ }
69
+ const tok = m[0];
70
+ const ch = tok[0];
71
+ if (ch === '"' || ch === "'" || ch === '`') {
72
+ tokens.push([t.color.accent, tok]);
73
+ }
74
+ else if (ch >= '0' && ch <= '9') {
75
+ tokens.push([t.color.text, tok]);
76
+ }
77
+ else if (spec.keywords.has(tok)) {
78
+ tokens.push([t.color.border, tok]);
79
+ }
80
+ else {
81
+ tokens.push(['', tok]);
82
+ }
83
+ last = start + tok.length;
84
+ }
85
+ if (last < line.length) {
86
+ tokens.push(['', line.slice(last)]);
87
+ }
88
+ return tokens;
89
+ }
@@ -0,0 +1,46 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: MIT
3
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
4
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
5
+ import { writeSync } from 'node:fs';
6
+ export const TERMINAL_MODE_RESET = '\x1b[0\'z' + // DEC locator reporting
7
+ '\x1b[0\'{' + // selectable locator events
8
+ '\x1b[?2029l' + // passive mouse
9
+ '\x1b[?1016l' + // SGR-pixels mouse
10
+ '\x1b[?1015l' + // urxvt decimal mouse
11
+ '\x1b[?1006l' + // SGR mouse
12
+ '\x1b[?1005l' + // UTF-8 extended mouse
13
+ '\x1b[?1003l' + // any-motion mouse
14
+ '\x1b[?1002l' + // button-motion mouse
15
+ '\x1b[?1001l' + // highlight mouse
16
+ '\x1b[?1000l' + // click mouse
17
+ '\x1b[?9l' + // X10 mouse
18
+ '\x1b[?1004l' + // focus events
19
+ '\x1b[?2004l' + // bracketed paste
20
+ '\x1b[?1049l' + // alternate screen
21
+ '\x1b[<u' + // kitty keyboard
22
+ '\x1b[>4m' + // modifyOtherKeys
23
+ '\x1b[0m' + // attributes
24
+ '\x1b[?25h'; // cursor visible
25
+ export function resetTerminalModes(stream = process.stdout) {
26
+ if (!stream.isTTY) {
27
+ return false;
28
+ }
29
+ const fd = typeof stream.fd === 'number' ? stream.fd : stream === process.stdout ? 1 : undefined;
30
+ if (fd !== undefined) {
31
+ try {
32
+ writeSync(fd, TERMINAL_MODE_RESET);
33
+ return true;
34
+ }
35
+ catch {
36
+ // Fall through to stream.write for mocked or unusual TTY streams.
37
+ }
38
+ }
39
+ try {
40
+ stream.write(TERMINAL_MODE_RESET);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
@@ -0,0 +1,48 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: MIT
3
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
4
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
5
+ import { detectVSCodeLikeTerminal, isRemoteShellSession, shouldPromptForTerminalSetup } from './terminalSetup.js';
6
+ export function detectMacTerminalContext(env = process.env) {
7
+ const termProgram = env['TERM_PROGRAM'] ?? '';
8
+ return {
9
+ isAppleTerminal: termProgram === 'Apple_Terminal' || !!env['TERM_SESSION_ID'],
10
+ isRemote: isRemoteShellSession(env),
11
+ isTmux: !!env['TMUX'],
12
+ vscodeLike: detectVSCodeLikeTerminal(env)
13
+ };
14
+ }
15
+ export async function terminalParityHints(env = process.env, options) {
16
+ const ctx = detectMacTerminalContext(env);
17
+ const hints = [];
18
+ if (ctx.vscodeLike &&
19
+ (await shouldPromptForTerminalSetup({ env, fileOps: options?.fileOps, homeDir: options?.homeDir }))) {
20
+ hints.push({
21
+ key: 'ide-setup',
22
+ tone: 'info',
23
+ message: `Detected ${ctx.vscodeLike} terminal · run /terminal-setup for best Cmd+Enter / undo parity`
24
+ });
25
+ }
26
+ if (ctx.isAppleTerminal) {
27
+ hints.push({
28
+ key: 'apple-terminal',
29
+ tone: 'warn',
30
+ message: 'Apple Terminal detected · use /paste for image-only clipboard fallback, and try Ctrl+A / Ctrl+E / Ctrl+U if Cmd+←/→/⌫ gets rewritten'
31
+ });
32
+ }
33
+ if (ctx.isTmux) {
34
+ hints.push({
35
+ key: 'tmux',
36
+ tone: 'warn',
37
+ message: 'tmux detected · clipboard copy/paste uses passthrough when available; allow-passthrough improves OSC52 reliability'
38
+ });
39
+ }
40
+ if (ctx.isRemote) {
41
+ hints.push({
42
+ key: 'remote',
43
+ tone: 'warn',
44
+ message: 'SSH session detected · text clipboard can bridge via OSC52, but image clipboard and local screenshot paths still depend on the machine running Hermes'
45
+ });
46
+ }
47
+ return hints;
48
+ }