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,175 @@
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 { writeFileSync } from 'node:fs';
6
+ import { evictInkCaches } from '../vendor/cvc-ink/index.js';
7
+ import { useCallback } from 'react';
8
+ import { buildSetupRequiredSections, SETUP_REQUIRED_TITLE } from '../content/setup.js';
9
+ import { introMsg, toTranscriptMessages } from '../domain/messages.js';
10
+ import { ZERO } from '../domain/usage.js';
11
+ import { asRpcResult } from '../lib/rpc.js';
12
+ import { patchOverlayState } from './overlayStore.js';
13
+ import { turnController } from './turnController.js';
14
+ import { patchTurnState } from './turnStore.js';
15
+ import { getUiState, patchUiState } from './uiStore.js';
16
+ const usageFrom = (info) => (info?.usage ? { ...ZERO, ...info.usage } : ZERO);
17
+ export const writeActiveSessionFile = (sessionId, file = process.env.CVC_TUI_ACTIVE_SESSION_FILE) => {
18
+ if (!file || !sessionId) {
19
+ return;
20
+ }
21
+ try {
22
+ writeFileSync(file, JSON.stringify({ session_id: sessionId }), { mode: 0o600 });
23
+ }
24
+ catch {
25
+ // Best-effort shell epilogue hint only; never break live session changes.
26
+ }
27
+ };
28
+ const trimTail = (items) => {
29
+ const q = [...items];
30
+ while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') {
31
+ q.pop();
32
+ }
33
+ if (q.at(-1)?.role === 'user') {
34
+ q.pop();
35
+ }
36
+ return q;
37
+ };
38
+ export function useSessionLifecycle(opts) {
39
+ const { colsRef, composerActions, gw, panel, rpc, scrollRef, setHistoryItems, setLastUserMsg, setSessionStartedAt, setStickyPrompt, setVoiceProcessing, setVoiceRecording, sys } = opts;
40
+ const closeSession = useCallback((targetSid) => targetSid ? rpc('session.close', { session_id: targetSid }) : Promise.resolve(null), [rpc]);
41
+ const resetSession = useCallback(() => {
42
+ turnController.fullReset();
43
+ setVoiceRecording(false);
44
+ setVoiceProcessing(false);
45
+ patchUiState({ bgTasks: new Set(), info: null, sid: null, usage: ZERO });
46
+ setHistoryItems([]);
47
+ setLastUserMsg('');
48
+ setStickyPrompt('');
49
+ composerActions.setPasteSnips([]);
50
+ // Half-prune: new session has new keys, but keep a warm pool in case
51
+ // the user resumes back to the prior session.
52
+ evictInkCaches('half');
53
+ }, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording]);
54
+ const resetVisibleHistory = useCallback((info = null) => {
55
+ turnController.idle();
56
+ turnController.clearReasoning();
57
+ turnController.turnTools = [];
58
+ turnController.persistedToolLabels.clear();
59
+ setHistoryItems(info ? [introMsg(info)] : []);
60
+ setStickyPrompt('');
61
+ setLastUserMsg('');
62
+ composerActions.setPasteSnips([]);
63
+ patchTurnState({ activity: [] });
64
+ patchUiState({ info, usage: usageFrom(info) });
65
+ }, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt]);
66
+ const newSession = useCallback(async (msg, title) => {
67
+ const setup = await rpc('setup.status', {});
68
+ if (setup?.provider_configured === false) {
69
+ panel(SETUP_REQUIRED_TITLE, buildSetupRequiredSections());
70
+ patchUiState({ status: 'setup required' });
71
+ return;
72
+ }
73
+ await closeSession(getUiState().sid);
74
+ const r = await rpc('session.create', { cols: colsRef.current });
75
+ if (!r) {
76
+ return patchUiState({ status: 'ready' });
77
+ }
78
+ const info = r.info ?? null;
79
+ const requestedTitle = title?.trim() ?? '';
80
+ resetSession();
81
+ setSessionStartedAt(Date.now());
82
+ writeActiveSessionFile(r.session_id);
83
+ patchUiState({
84
+ info,
85
+ sid: r.session_id,
86
+ status: info?.version ? 'ready' : 'starting agent…',
87
+ usage: usageFrom(info)
88
+ });
89
+ if (info) {
90
+ setHistoryItems([introMsg(info)]);
91
+ }
92
+ if (info?.credential_warning) {
93
+ sys(`warning: ${info.credential_warning}`);
94
+ }
95
+ if (info?.config_warning) {
96
+ sys(`warning: ${info.config_warning}`);
97
+ }
98
+ if (msg) {
99
+ sys(msg);
100
+ }
101
+ if (requestedTitle) {
102
+ rpc('session.title', {
103
+ session_id: r.session_id,
104
+ title: requestedTitle
105
+ })
106
+ .then(result => {
107
+ if (!result || getUiState().sid !== r.session_id) {
108
+ return;
109
+ }
110
+ const nextTitle = (result.title ?? requestedTitle).trim();
111
+ const suffix = result.pending ? ' (queued while session initializes)' : '';
112
+ sys(`session title set: ${nextTitle}${suffix}`);
113
+ })
114
+ .catch((err) => {
115
+ if (getUiState().sid !== r.session_id) {
116
+ return;
117
+ }
118
+ const message = err instanceof Error ? err.message : String(err);
119
+ sys(`warning: failed to set session title: ${message}`);
120
+ });
121
+ }
122
+ }, [closeSession, colsRef, panel, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]);
123
+ const resumeById = useCallback((id) => {
124
+ patchOverlayState({ picker: false });
125
+ patchUiState({ status: 'resuming…' });
126
+ rpc('setup.status', {}).then(setup => {
127
+ if (setup?.provider_configured === false) {
128
+ panel(SETUP_REQUIRED_TITLE, buildSetupRequiredSections());
129
+ patchUiState({ status: 'setup required' });
130
+ return;
131
+ }
132
+ closeSession(getUiState().sid === id ? null : getUiState().sid).then(() => gw
133
+ .request('session.resume', { cols: colsRef.current, session_id: id })
134
+ .then(raw => {
135
+ const r = asRpcResult(raw);
136
+ if (!r) {
137
+ sys('error: invalid response: session.resume');
138
+ return patchUiState({ status: 'ready' });
139
+ }
140
+ resetSession();
141
+ setSessionStartedAt(Date.now());
142
+ const resumed = toTranscriptMessages(r.messages);
143
+ setHistoryItems(r.info ? [introMsg(r.info), ...resumed] : resumed);
144
+ writeActiveSessionFile(r.resumed ?? r.session_id);
145
+ patchUiState({
146
+ info: r.info ?? null,
147
+ sid: r.session_id,
148
+ status: 'ready',
149
+ usage: usageFrom(r.info ?? null)
150
+ });
151
+ setTimeout(() => scrollRef.current?.scrollToBottom(), 0);
152
+ })
153
+ .catch((e) => {
154
+ sys(`error: ${e.message}`);
155
+ patchUiState({ status: 'ready' });
156
+ }));
157
+ });
158
+ }, [closeSession, colsRef, gw, panel, resetSession, rpc, scrollRef, setHistoryItems, setSessionStartedAt, sys]);
159
+ const guardBusySessionSwitch = useCallback((what = 'switch sessions') => {
160
+ if (!getUiState().busy) {
161
+ return false;
162
+ }
163
+ sys(`interrupt the current turn before trying to ${what}`);
164
+ return true;
165
+ }, [sys]);
166
+ return {
167
+ closeSession,
168
+ guardBusySessionSwitch,
169
+ newSession,
170
+ resetSession,
171
+ resetVisibleHistory,
172
+ resumeById,
173
+ trimLastExchange: trimTail
174
+ };
175
+ }
@@ -0,0 +1,287 @@
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 { useCallback, useEffect, useRef } from 'react';
6
+ import { TYPING_IDLE_MS } from '../config/timing.js';
7
+ import { attachedImageNotice } from '../domain/messages.js';
8
+ import { looksLikeSlashCommand } from '../domain/slash.js';
9
+ import { asRpcResult } from '../lib/rpc.js';
10
+ import { hasInterpolation, INTERPOLATION_RE } from '../protocol/interpolation.js';
11
+ import { PASTE_SNIPPET_RE } from '../protocol/paste.js';
12
+ import { turnController } from './turnController.js';
13
+ import { getUiState, patchUiState } from './uiStore.js';
14
+ const DOUBLE_ENTER_MS = 450;
15
+ const SESSION_BUSY_RE = /session busy|waiting for model response/i;
16
+ const isSessionBusyError = (e) => e instanceof Error && SESSION_BUSY_RE.test(e.message);
17
+ const expandSnips = (snips) => {
18
+ const byLabel = new Map();
19
+ for (const { label, text } of snips) {
20
+ const hit = byLabel.get(label);
21
+ hit ? hit.push(text) : byLabel.set(label, [text]);
22
+ }
23
+ return (value) => value.replace(PASTE_SNIPPET_RE, tok => byLabel.get(tok)?.shift() ?? tok);
24
+ };
25
+ const spliceMatches = (text, matches, results) => matches.reduceRight((acc, m, i) => acc.slice(0, m.index) + results[i] + acc.slice(m.index + m[0].length), text);
26
+ export function useSubmission(opts) {
27
+ const { appendMessage, composerActions, composerRefs, composerState, gw, maybeGoodVibes, setLastUserMsg, slashRef, submitRef, sys } = opts;
28
+ const lastEmptyAt = useRef(0);
29
+ const typingIdleTimer = useRef(null);
30
+ useEffect(() => {
31
+ if (typingIdleTimer.current) {
32
+ clearTimeout(typingIdleTimer.current);
33
+ typingIdleTimer.current = null;
34
+ }
35
+ if (!composerState.input && !composerState.inputBuf.length) {
36
+ turnController.relaxStreaming();
37
+ return;
38
+ }
39
+ if (getUiState().busy) {
40
+ turnController.boostStreamingForTyping();
41
+ }
42
+ typingIdleTimer.current = setTimeout(() => {
43
+ typingIdleTimer.current = null;
44
+ turnController.relaxStreaming();
45
+ }, TYPING_IDLE_MS);
46
+ return () => {
47
+ if (typingIdleTimer.current) {
48
+ clearTimeout(typingIdleTimer.current);
49
+ typingIdleTimer.current = null;
50
+ }
51
+ };
52
+ }, [composerState.input, composerState.inputBuf]);
53
+ const send = useCallback((text, showUserMessage = true) => {
54
+ const expand = expandSnips(composerState.pasteSnips);
55
+ const startSubmit = (displayText, submitText, showUserMessage = true) => {
56
+ const sid = getUiState().sid;
57
+ if (!sid) {
58
+ return sys('session not ready yet');
59
+ }
60
+ turnController.clearStatusTimer();
61
+ maybeGoodVibes(submitText);
62
+ setLastUserMsg(text);
63
+ if (showUserMessage) {
64
+ appendMessage({ role: 'user', text: displayText });
65
+ }
66
+ patchUiState({ busy: true, status: 'running…' });
67
+ turnController.bufRef = '';
68
+ turnController.interrupted = false;
69
+ gw.request('prompt.submit', { session_id: sid, text: submitText }).catch((e) => {
70
+ if (isSessionBusyError(e)) {
71
+ composerActions.enqueue(submitText);
72
+ patchUiState({ busy: true, status: 'queued for next turn' });
73
+ return sys(`queued: "${submitText.slice(0, 50)}${submitText.length > 50 ? '…' : ''}"`);
74
+ }
75
+ sys(`error: ${e.message}`);
76
+ patchUiState({ busy: false, status: 'ready' });
77
+ });
78
+ };
79
+ const sid = getUiState().sid;
80
+ if (!sid) {
81
+ return sys('session not ready yet');
82
+ }
83
+ // Always ask the backend whether this looks like a file drop.
84
+ // The backend's _detect_file_drop handles paths with spaces, quotes,
85
+ // Windows drive letters, and escaped characters correctly.
86
+ gw.request('input.detect_drop', { session_id: sid, text })
87
+ .then(r => {
88
+ if (!r?.matched) {
89
+ return startSubmit(text, expand(text), showUserMessage);
90
+ }
91
+ if (r.is_image) {
92
+ turnController.pushActivity(attachedImageNotice(r));
93
+ }
94
+ else {
95
+ turnController.pushActivity(`detected file: ${r.name}`);
96
+ }
97
+ startSubmit(r.text || text, expand(r.text || text), showUserMessage);
98
+ })
99
+ .catch(() => startSubmit(text, expand(text), showUserMessage));
100
+ }, [appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]);
101
+ const shellExec = useCallback((cmd) => {
102
+ appendMessage({ role: 'user', text: `!${cmd}` });
103
+ patchUiState({ busy: true, status: 'running…' });
104
+ gw.request('shell.exec', { command: cmd })
105
+ .then(raw => {
106
+ const r = asRpcResult(raw);
107
+ if (!r) {
108
+ return sys('error: invalid response: shell.exec');
109
+ }
110
+ const out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
111
+ if (out) {
112
+ sys(out);
113
+ }
114
+ if (r.code !== 0 || !out) {
115
+ sys(`exit ${r.code}`);
116
+ }
117
+ })
118
+ .catch((e) => sys(`error: ${e.message}`))
119
+ .finally(() => patchUiState({ busy: false, status: 'ready' }));
120
+ }, [appendMessage, gw, sys]);
121
+ const interpolate = useCallback((text, then) => {
122
+ patchUiState({ status: 'interpolating…' });
123
+ const matches = [...text.matchAll(new RegExp(INTERPOLATION_RE.source, 'g'))];
124
+ Promise.all(matches.map(m => gw
125
+ .request('shell.exec', { command: m[1] })
126
+ .then(raw => {
127
+ const r = asRpcResult(raw);
128
+ return [r?.stdout, r?.stderr].filter(Boolean).join('\n').trim();
129
+ })
130
+ .catch(() => '(error)'))).then(results => then(spliceMatches(text, matches, results)));
131
+ }, [gw]);
132
+ const sendQueued = useCallback((text) => {
133
+ if (text.startsWith('!')) {
134
+ return shellExec(text.slice(1).trim());
135
+ }
136
+ if (hasInterpolation(text)) {
137
+ patchUiState({ busy: true });
138
+ return interpolate(text, send);
139
+ }
140
+ send(text);
141
+ }, [interpolate, send, shellExec]);
142
+ // Honors `display.busy_input_mode` from config.yaml (CLI parity):
143
+ // - 'queue' (legacy): append to queueRef; drains on busy → false
144
+ // - 'steer' : inject into the current turn via session.steer; falls
145
+ // back to queue when steer is rejected (no agent / no
146
+ // tool window).
147
+ // - 'interrupt' (default): cancel the in-flight turn, then send the
148
+ // new text as a fresh prompt so it actually moves.
149
+ //
150
+ // `opts.fallbackToFront` controls whether a steer fallback re-inserts
151
+ // at the front of the queue (used by the queue-edit path to preserve
152
+ // a picked item's position); the mainline submit path always appends.
153
+ const handleBusyInput = useCallback((full, opts = {}) => {
154
+ const live = getUiState();
155
+ const mode = live.busyInputMode;
156
+ const fallback = (note) => {
157
+ if (opts.fallbackToFront) {
158
+ composerRefs.queueRef.current.unshift(full);
159
+ composerActions.syncQueue();
160
+ }
161
+ else {
162
+ composerActions.enqueue(full);
163
+ }
164
+ sys(note);
165
+ };
166
+ if (mode === 'queue') {
167
+ return composerActions.enqueue(full);
168
+ }
169
+ if (mode === 'steer' && live.sid) {
170
+ gw.request('session.steer', { session_id: live.sid, text: full })
171
+ .then(raw => {
172
+ const r = asRpcResult(raw);
173
+ if (r?.status !== 'queued') {
174
+ fallback('steer rejected — message queued for next turn');
175
+ }
176
+ })
177
+ .catch(() => fallback('steer failed — message queued for next turn'));
178
+ return;
179
+ }
180
+ // 'interrupt' (default): tear down the current turn, then send.
181
+ // `interruptTurn` fires `session.interrupt` without awaiting; if
182
+ // the gateway is still mid-response when `prompt.submit` lands,
183
+ // `send()`'s catch path re-queues with a "queued: ..." sys note
184
+ // (`isSessionBusyError`) — so a lost race degrades to queue
185
+ // semantics, not a dropped message.
186
+ if (live.sid) {
187
+ turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys });
188
+ }
189
+ if (hasInterpolation(full)) {
190
+ patchUiState({ busy: true });
191
+ return interpolate(full, send);
192
+ }
193
+ send(full);
194
+ }, [appendMessage, composerActions, composerRefs, gw, interpolate, send, sys]);
195
+ const dispatchSubmission = useCallback((full) => {
196
+ if (!full.trim()) {
197
+ return;
198
+ }
199
+ if (looksLikeSlashCommand(full)) {
200
+ appendMessage({ kind: 'slash', role: 'system', text: full });
201
+ composerActions.pushHistory(full);
202
+ slashRef.current(full);
203
+ composerActions.clearIn();
204
+ return;
205
+ }
206
+ if (full.startsWith('!')) {
207
+ composerActions.clearIn();
208
+ return shellExec(full.slice(1).trim());
209
+ }
210
+ const live = getUiState();
211
+ if (!live.sid) {
212
+ composerActions.pushHistory(full);
213
+ composerActions.enqueue(full);
214
+ composerActions.clearIn();
215
+ return;
216
+ }
217
+ const editIdx = composerRefs.queueEditRef.current;
218
+ composerActions.clearIn();
219
+ if (editIdx !== null) {
220
+ composerActions.replaceQueue(editIdx, full);
221
+ const picked = composerRefs.queueRef.current.splice(editIdx, 1)[0];
222
+ composerActions.syncQueue();
223
+ composerActions.setQueueEdit(null);
224
+ if (!picked || !live.sid) {
225
+ return;
226
+ }
227
+ if (getUiState().busy) {
228
+ // 'interrupt' / 'steer' should reach the live turn instead of
229
+ // silently going back to the queue. handleBusyInput resolves
230
+ // mode-specific behavior (interrupt-and-send, steer, or queue).
231
+ if (getUiState().busyInputMode === 'queue') {
232
+ composerRefs.queueRef.current.unshift(picked);
233
+ return composerActions.syncQueue();
234
+ }
235
+ return handleBusyInput(picked, { fallbackToFront: true });
236
+ }
237
+ return sendQueued(picked);
238
+ }
239
+ composerActions.pushHistory(full);
240
+ if (getUiState().busy) {
241
+ return handleBusyInput(full);
242
+ }
243
+ if (hasInterpolation(full)) {
244
+ patchUiState({ busy: true });
245
+ return interpolate(full, send);
246
+ }
247
+ send(full);
248
+ }, [appendMessage, composerActions, composerRefs, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]);
249
+ const submit = useCallback((value) => {
250
+ if (composerState.completions.length) {
251
+ const row = composerState.completions[composerState.compIdx];
252
+ if (row?.text) {
253
+ const text = value.startsWith('/') && row.text.startsWith('/') ? row.text.slice(1) : row.text;
254
+ const next = value.slice(0, composerState.compReplace) + text;
255
+ if (next !== value) {
256
+ return composerActions.setInput(next);
257
+ }
258
+ }
259
+ }
260
+ if (!value.trim() && !composerState.inputBuf.length) {
261
+ const live = getUiState();
262
+ const now = Date.now();
263
+ const doubleTap = now - lastEmptyAt.current < DOUBLE_ENTER_MS;
264
+ lastEmptyAt.current = now;
265
+ if (doubleTap && live.busy && live.sid) {
266
+ return turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys });
267
+ }
268
+ if (doubleTap && live.sid && composerRefs.queueRef.current.length) {
269
+ const next = composerActions.dequeue();
270
+ composerActions.syncQueue();
271
+ if (next) {
272
+ composerActions.setQueueEdit(null);
273
+ dispatchSubmission(next);
274
+ }
275
+ }
276
+ return;
277
+ }
278
+ lastEmptyAt.current = 0;
279
+ if (value.endsWith('\\')) {
280
+ composerActions.setInputBuf(prev => [...prev, value.slice(0, -1)]);
281
+ return composerActions.setInput('');
282
+ }
283
+ dispatchSubmission([...composerState.inputBuf, value].join('\n'));
284
+ }, [appendMessage, composerActions, composerRefs, composerState, dispatchSubmission, gw, sys]);
285
+ submitRef.current = submit;
286
+ return { dispatchSubmission, send, sendQueued, submit };
287
+ }
package/dist/app.js ADDED
@@ -0,0 +1,15 @@
1
+ import { 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 { useStore } from '@nanostores/react';
7
+ import { GatewayProvider } from './app/gatewayContext.js';
8
+ import { $uiState } from './app/uiStore.js';
9
+ import { useMainApp } from './app/useMainApp.js';
10
+ import { AppLayout } from './components/appLayout.js';
11
+ export function App({ gw }) {
12
+ const { appActions, appComposer, appProgress, appStatus, appTranscript, gateway } = useMainApp(gw);
13
+ const { mouseTracking } = useStore($uiState);
14
+ return (_jsx(GatewayProvider, { value: gateway, children: _jsx(AppLayout, { actions: appActions, composer: appComposer, mouseTracking: mouseTracking, progress: appProgress, status: appStatus, transcript: appTranscript }) }));
15
+ }
package/dist/banner.js ADDED
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
3
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
4
+ //
5
+ // CVC ASCII banner. Rendered once at startup by <Branding/>.
6
+ // Colour applied at render-time via Ink <Text color="..."> wrappers.
7
+ import { readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, resolve } from 'node:path';
10
+ function readPkgVersion() {
11
+ try {
12
+ // src/banner.ts → ../package.json (src) or dist/banner.js → ../package.json (dist)
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const candidates = [resolve(here, '../package.json'), resolve(here, '../../package.json')];
15
+ for (const p of candidates) {
16
+ try {
17
+ const raw = readFileSync(p, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed.name === 'cvc-tui' && typeof parsed.version === 'string') {
20
+ return parsed.version;
21
+ }
22
+ }
23
+ catch {
24
+ /* try next */
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ /* fall through */
30
+ }
31
+ return '0.0.0';
32
+ }
33
+ export const CVC_BANNER = String.raw `
34
+ ██████╗██╗ ██╗ ██████╗
35
+ ██╔════╝██║ ██║██╔════╝
36
+ ██║ ██║ ██║██║
37
+ ██║ ╚██╗ ██╔╝██║
38
+ ╚██████╗ ╚████╔╝ ╚██████╗
39
+ ╚═════╝ ╚═══╝ ╚═════╝
40
+ `.trim();
41
+ export const CVC_TAGLINE = 'Cognitive Version Control — git for your AI\u2019s mind';
42
+ export const CVC_BOOT_TIP = 'Type /help for commands · Ctrl+C to exit';
43
+ export const BANNER_COLOR = '#e63946';
44
+ export const TAGLINE_COLOR = '#4a9eff';
45
+ /**
46
+ * Version of cvc-tui as published in package.json.
47
+ * Wired here so the banner is never out-of-date with the package version
48
+ * (this fixes the v0.0.1 fallback bug).
49
+ */
50
+ export const CVC_VERSION = readPkgVersion();
51
+ /**
52
+ * Returns the banner as a plain (uncoloured) string suitable for tests
53
+ * and for non-TTY fallbacks. The TTY renderer colours it via Ink.
54
+ */
55
+ export function renderBannerPlain({ version = CVC_VERSION, model = 'unknown' } = {}) {
56
+ return [CVC_BANNER, '', `${CVC_TAGLINE} v${version} · model: ${model}`, CVC_BOOT_TIP].join('\n');
57
+ }
58
+ export const LOGO_WIDTH = 28;
59
+ export const CADUCEUS_WIDTH = 28;
60
+ const _bannerLines = (color) => CVC_BANNER.split('\n').map(t => [color, t]);
61
+ export const logo = (_c, _customLogo) => _bannerLines(BANNER_COLOR);
62
+ export const caduceus = (_c, _customHero) => _bannerLines(BANNER_COLOR);
63
+ export const artWidth = (lines) => lines.reduce((m, [, t]) => Math.max(m, t.length), 0);