cvc-tui 0.4.0 → 0.4.2

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 (133) hide show
  1. package/dist/entry.js +71148 -61
  2. package/package.json +2 -2
  3. package/dist/app/completion.js +0 -102
  4. package/dist/app/createGatewayEventHandler.js +0 -508
  5. package/dist/app/createSlashHandler.js +0 -101
  6. package/dist/app/delegationStore.js +0 -51
  7. package/dist/app/gatewayContext.js +0 -17
  8. package/dist/app/historyStore.js +0 -123
  9. package/dist/app/inputBuffer.js +0 -120
  10. package/dist/app/inputSelectionStore.js +0 -8
  11. package/dist/app/inputStore.js +0 -28
  12. package/dist/app/interfaces.js +0 -6
  13. package/dist/app/overlayStore.js +0 -40
  14. package/dist/app/promptStore.js +0 -44
  15. package/dist/app/queueStore.js +0 -25
  16. package/dist/app/scroll.js +0 -44
  17. package/dist/app/setupHandoff.js +0 -28
  18. package/dist/app/slash/commands/core.js +0 -479
  19. package/dist/app/slash/commands/debug.js +0 -44
  20. package/dist/app/slash/commands/ops.js +0 -498
  21. package/dist/app/slash/commands/session.js +0 -431
  22. package/dist/app/slash/commands/setup.js +0 -20
  23. package/dist/app/slash/commands/toggles.js +0 -40
  24. package/dist/app/slash/registry.js +0 -18
  25. package/dist/app/slash/types.js +0 -1
  26. package/dist/app/spawnHistoryStore.js +0 -105
  27. package/dist/app/turnController.js +0 -650
  28. package/dist/app/turnStore.js +0 -48
  29. package/dist/app/uiStore.js +0 -36
  30. package/dist/app/useComposerState.js +0 -265
  31. package/dist/app/useConfigSync.js +0 -144
  32. package/dist/app/useInputHandlers.js +0 -403
  33. package/dist/app/useLongRunToolCharms.js +0 -50
  34. package/dist/app/useMainApp.js +0 -629
  35. package/dist/app/useSessionLifecycle.js +0 -175
  36. package/dist/app/useSubmission.js +0 -287
  37. package/dist/app.js +0 -15
  38. package/dist/banner.js +0 -57
  39. package/dist/components/agentsOverlay.js +0 -474
  40. package/dist/components/appChrome.js +0 -252
  41. package/dist/components/appLayout.js +0 -121
  42. package/dist/components/appOverlays.js +0 -65
  43. package/dist/components/branding.js +0 -97
  44. package/dist/components/fpsOverlay.js +0 -22
  45. package/dist/components/helpHint.js +0 -21
  46. package/dist/components/markdown.js +0 -501
  47. package/dist/components/maskedPrompt.js +0 -12
  48. package/dist/components/messageLine.js +0 -82
  49. package/dist/components/modelPicker.js +0 -254
  50. package/dist/components/overlayControls.js +0 -30
  51. package/dist/components/overlays/confirmPrompt.js +0 -25
  52. package/dist/components/overlays/helpOverlay.js +0 -76
  53. package/dist/components/overlays/historySearch.js +0 -49
  54. package/dist/components/overlays/modelPicker.js +0 -60
  55. package/dist/components/overlays/overlayUtils.js +0 -19
  56. package/dist/components/overlays/secretPrompt.js +0 -36
  57. package/dist/components/overlays/sessionPicker.js +0 -93
  58. package/dist/components/overlays/skillsHub.js +0 -71
  59. package/dist/components/prompts.js +0 -95
  60. package/dist/components/queuedMessages.js +0 -24
  61. package/dist/components/sessionPicker.js +0 -130
  62. package/dist/components/skillsHub.js +0 -165
  63. package/dist/components/streamingAssistant.js +0 -35
  64. package/dist/components/streamingMarkdown.js +0 -144
  65. package/dist/components/textInput.js +0 -794
  66. package/dist/components/themed.js +0 -12
  67. package/dist/components/thinking.js +0 -496
  68. package/dist/components/todoPanel.js +0 -40
  69. package/dist/components/transcript.js +0 -22
  70. package/dist/config/env.js +0 -18
  71. package/dist/config/limits.js +0 -22
  72. package/dist/config/timing.js +0 -18
  73. package/dist/content/charms.js +0 -5
  74. package/dist/content/faces.js +0 -21
  75. package/dist/content/fortunes.js +0 -29
  76. package/dist/content/hotkeys.js +0 -38
  77. package/dist/content/placeholders.js +0 -15
  78. package/dist/content/setup.js +0 -14
  79. package/dist/content/verbs.js +0 -41
  80. package/dist/domain/details.js +0 -53
  81. package/dist/domain/messages.js +0 -63
  82. package/dist/domain/paths.js +0 -16
  83. package/dist/domain/providers.js +0 -11
  84. package/dist/domain/roles.js +0 -6
  85. package/dist/domain/slash.js +0 -11
  86. package/dist/domain/usage.js +0 -1
  87. package/dist/domain/viewport.js +0 -33
  88. package/dist/gateway/client.js +0 -312
  89. package/dist/gatewayClient.js +0 -574
  90. package/dist/gatewayTypes.js +0 -1
  91. package/dist/hooks/useCompletion.js +0 -86
  92. package/dist/hooks/useGitBranch.js +0 -58
  93. package/dist/hooks/useInputHistory.js +0 -12
  94. package/dist/hooks/useQueue.js +0 -57
  95. package/dist/hooks/useVirtualHistory.js +0 -401
  96. package/dist/lib/circularBuffer.js +0 -43
  97. package/dist/lib/clipboard.js +0 -126
  98. package/dist/lib/editor.js +0 -41
  99. package/dist/lib/editor.test.js +0 -58
  100. package/dist/lib/emoji.js +0 -49
  101. package/dist/lib/externalCli.js +0 -11
  102. package/dist/lib/forceTruecolor.js +0 -26
  103. package/dist/lib/fpsStore.js +0 -36
  104. package/dist/lib/gracefulExit.js +0 -29
  105. package/dist/lib/history.js +0 -69
  106. package/dist/lib/inputMetrics.js +0 -143
  107. package/dist/lib/liveProgress.js +0 -51
  108. package/dist/lib/liveProgress.test.js +0 -89
  109. package/dist/lib/mathUnicode.js +0 -685
  110. package/dist/lib/memory.js +0 -123
  111. package/dist/lib/memoryMonitor.js +0 -76
  112. package/dist/lib/messages.js +0 -3
  113. package/dist/lib/messages.test.js +0 -25
  114. package/dist/lib/osc52.js +0 -53
  115. package/dist/lib/perfPane.js +0 -94
  116. package/dist/lib/platform.js +0 -312
  117. package/dist/lib/precisionWheel.js +0 -25
  118. package/dist/lib/reasoning.js +0 -39
  119. package/dist/lib/rpc.js +0 -26
  120. package/dist/lib/subagentTree.js +0 -287
  121. package/dist/lib/syntax.js +0 -89
  122. package/dist/lib/terminalModes.js +0 -46
  123. package/dist/lib/terminalParity.js +0 -48
  124. package/dist/lib/terminalSetup.js +0 -321
  125. package/dist/lib/text.js +0 -203
  126. package/dist/lib/text.test.js +0 -18
  127. package/dist/lib/todo.js +0 -2
  128. package/dist/lib/todo.test.js +0 -22
  129. package/dist/lib/viewportStore.js +0 -82
  130. package/dist/lib/virtualHeights.js +0 -61
  131. package/dist/lib/wheelAccel.js +0 -143
  132. package/dist/theme.js +0 -398
  133. package/dist/types.js +0 -1
@@ -1,574 +0,0 @@
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 { spawn } from 'node:child_process';
6
- import { EventEmitter } from 'node:events';
7
- import { existsSync } from 'node:fs';
8
- import { delimiter, resolve } from 'node:path';
9
- import { createInterface } from 'node:readline';
10
- import { CircularBuffer } from './lib/circularBuffer.js';
11
- const MAX_GATEWAY_LOG_LINES = 200;
12
- const MAX_LOG_LINE_BYTES = 4096;
13
- const MAX_BUFFERED_EVENTS = 2000;
14
- const MAX_LOG_PREVIEW = 240;
15
- const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.CVC_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000);
16
- const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.CVC_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000);
17
- const WS_CONNECTING = 0;
18
- const WS_OPEN = 1;
19
- const WS_CLOSING = 2;
20
- const WS_CLOSED = 3;
21
- const truncateLine = (line) => line.length > MAX_LOG_LINE_BYTES ? `${line.slice(0, MAX_LOG_LINE_BYTES)}… [truncated ${line.length} bytes]` : line;
22
- const resolveGatewayAttachUrl = () => {
23
- const raw = process.env.CVC_TUI_GATEWAY_URL?.trim() || process.env.CVC_GATEWAY_URL?.trim();
24
- if (raw)
25
- return raw;
26
- // Default CVC FastAPI gateway port
27
- const port = process.env.CVC_GATEWAY_PORT?.trim() || '13421';
28
- return `http://127.0.0.1:${port}`;
29
- };
30
- const resolveSidecarUrl = () => {
31
- const raw = process.env.CVC_TUI_SIDECAR_URL?.trim();
32
- return raw ? raw : null;
33
- };
34
- const resolvePython = (root) => {
35
- const configured = process.env.HERMES_PYTHON?.trim() || process.env.PYTHON?.trim();
36
- if (configured) {
37
- return configured;
38
- }
39
- const venv = process.env.VIRTUAL_ENV?.trim();
40
- const hit = [
41
- venv && resolve(venv, 'bin/python'),
42
- venv && resolve(venv, 'Scripts/python.exe'),
43
- resolve(root, '.venv/bin/python'),
44
- resolve(root, '.venv/bin/python3'),
45
- resolve(root, 'venv/bin/python'),
46
- resolve(root, 'venv/bin/python3')
47
- ].find(p => p && existsSync(p));
48
- return hit || (process.platform === 'win32' ? 'python' : 'python3');
49
- };
50
- const asGatewayEvent = (value) => value && typeof value === 'object' && !Array.isArray(value) && typeof value.type === 'string'
51
- ? value
52
- : null;
53
- // Hoisted decoder: attach mode can drive high-frequency binary frames
54
- // (tool deltas, reasoning streams) and constructing a fresh TextDecoder
55
- // per message creates avoidable GC pressure. One module-level instance
56
- // is fine because UTF-8 is stateless and we always pass entire frames.
57
- const _wireDecoder = new TextDecoder();
58
- const asWireText = (raw) => {
59
- if (typeof raw === 'string') {
60
- return raw;
61
- }
62
- if (raw instanceof ArrayBuffer) {
63
- return _wireDecoder.decode(raw);
64
- }
65
- if (ArrayBuffer.isView(raw)) {
66
- return _wireDecoder.decode(raw);
67
- }
68
- return null;
69
- };
70
- // Matches `<scheme>://user:pass@host…` style user-info segments in
71
- // otherwise-malformed URLs that the WHATWG `URL` parser can't accept.
72
- // Used by the `redactUrl` fallback so embedded credentials are
73
- // scrubbed from log lines even when the URL is unparseable.
74
- const _USERINFO_FALLBACK_RE = /^([a-z][a-z0-9+.\-]*:\/\/)[^/?#@]*@/i;
75
- // Connection URLs (gateway, sidecar) often carry bearer tokens in the query
76
- // string. We surface them in user-facing log lines and the
77
- // `gateway.start_timeout` payload, so always strip the query string and any
78
- // embedded user-info before logging.
79
- const redactUrl = (raw) => {
80
- if (!raw) {
81
- return raw;
82
- }
83
- try {
84
- const url = new URL(raw);
85
- const userInfo = url.username || url.password ? '***@' : '';
86
- const query = url.search ? '?***' : '';
87
- return `${url.protocol}//${userInfo}${url.host}${url.pathname}${query}`;
88
- }
89
- catch {
90
- // WHATWG URL rejected the input. Best-effort: strip an embedded
91
- // `user:pass@` segment AND the query string so a malformed token
92
- // bearer can never escape into the log tail.
93
- const noUserInfo = raw.replace(_USERINFO_FALLBACK_RE, '$1***@');
94
- const queryIdx = noUserInfo.indexOf('?');
95
- return queryIdx >= 0 ? `${noUserInfo.slice(0, queryIdx)}?***` : noUserInfo;
96
- }
97
- };
98
- export class GatewayClient extends EventEmitter {
99
- proc = null;
100
- ws = null;
101
- wsConnectPromise = null;
102
- sidecarWs = null;
103
- attachUrl = null;
104
- sidecarUrl = null;
105
- reqId = 0;
106
- logs = new CircularBuffer(MAX_GATEWAY_LOG_LINES);
107
- pending = new Map();
108
- bufferedEvents = new CircularBuffer(MAX_BUFFERED_EVENTS);
109
- pendingExit;
110
- ready = false;
111
- readyTimer = null;
112
- subscribed = false;
113
- stdoutRl = null;
114
- stderrRl = null;
115
- constructor() {
116
- super();
117
- // useInput / createGatewayEventHandler can legitimately attach many
118
- // listeners. Default 10-cap triggers spurious warnings.
119
- this.setMaxListeners(0);
120
- }
121
- publish(ev) {
122
- if (ev.type === 'gateway.ready') {
123
- this.ready = true;
124
- if (this.readyTimer) {
125
- clearTimeout(this.readyTimer);
126
- this.readyTimer = null;
127
- }
128
- }
129
- if (this.subscribed) {
130
- return void this.emit('event', ev);
131
- }
132
- this.bufferedEvents.push(ev);
133
- }
134
- clearReadyTimer() {
135
- if (this.readyTimer) {
136
- clearTimeout(this.readyTimer);
137
- this.readyTimer = null;
138
- }
139
- }
140
- closeSidecarSocket() {
141
- try {
142
- this.sidecarWs?.close();
143
- }
144
- catch {
145
- // best effort
146
- }
147
- finally {
148
- this.sidecarWs = null;
149
- }
150
- }
151
- closeGatewaySocket() {
152
- // Null the active reference BEFORE invoking close(): real WebSocket
153
- // implementations dispatch the 'close' event after a microtask hop,
154
- // so by the time the handler runs `this.ws` should already be null
155
- // and the identity guard will correctly classify the close as
156
- // belonging to a discarded socket. (Test fakes emit synchronously,
157
- // so doing the swap up front is also what makes the identity guard
158
- // match real timing in tests.)
159
- const ws = this.ws;
160
- this.ws = null;
161
- this.wsConnectPromise = null;
162
- try {
163
- ws?.close();
164
- }
165
- catch {
166
- // best effort
167
- }
168
- }
169
- resetStartupState() {
170
- // Reject any in-flight RPCs left over from the previous transport
171
- // before we swap. Otherwise the old transport's stale exit/close
172
- // handlers (now identity-gated to ignore unrelated transports)
173
- // never fire `rejectPending`, leaving callers hanging on promises
174
- // attached to a discarded child / socket.
175
- this.rejectPending(new Error('gateway restarting'));
176
- this.ready = false;
177
- this.bufferedEvents.clear();
178
- this.pendingExit = undefined;
179
- this.stdoutRl?.close();
180
- this.stderrRl?.close();
181
- this.stdoutRl = null;
182
- this.stderrRl = null;
183
- this.clearReadyTimer();
184
- }
185
- startReadyTimer(python, cwd) {
186
- this.readyTimer = setTimeout(() => {
187
- if (this.ready) {
188
- return;
189
- }
190
- // Append the most recent gateway stderr/log lines to the timeout
191
- // event so users can tell apart "wrong python", "missing dep",
192
- // and "config parse failure" from one glance instead of having
193
- // to dig through `/logs`. Capped to keep the activity feed
194
- // readable on slow boots.
195
- const stderrTail = this.getLogTail(20);
196
- this.pushLog(`[startup] timed out waiting for gateway.ready (python=${python}, cwd=${cwd})`);
197
- this.publish({
198
- type: 'gateway.start_timeout',
199
- payload: { cwd, python, stderr_tail: stderrTail }
200
- });
201
- }, STARTUP_TIMEOUT_MS);
202
- }
203
- handleTransportExit(code, reason) {
204
- this.clearReadyTimer();
205
- this.closeSidecarSocket();
206
- this.rejectPending(new Error(reason || `gateway exited${code === null ? '' : ` (${code})`}`));
207
- if (this.subscribed) {
208
- this.emit('exit', code);
209
- }
210
- else {
211
- this.pendingExit = code;
212
- }
213
- }
214
- connectSidecarMirror() {
215
- this.closeSidecarSocket();
216
- if (!this.sidecarUrl) {
217
- return;
218
- }
219
- if (typeof WebSocket === 'undefined') {
220
- this.pushLog(`[sidecar] WebSocket unavailable; skipping mirror to ${redactUrl(this.sidecarUrl)}`);
221
- return;
222
- }
223
- try {
224
- const ws = new WebSocket(this.sidecarUrl);
225
- this.sidecarWs = ws;
226
- ws.addEventListener('close', () => {
227
- if (this.sidecarWs === ws) {
228
- this.sidecarWs = null;
229
- }
230
- });
231
- ws.addEventListener('error', () => {
232
- this.pushLog('[sidecar] mirror connection error');
233
- });
234
- }
235
- catch (err) {
236
- this.pushLog(`[sidecar] failed to connect ${redactUrl(this.sidecarUrl)} (constructor error)`);
237
- this.sidecarWs = null;
238
- }
239
- }
240
- mirrorEventToSidecar(rawFrame) {
241
- const ws = this.sidecarWs;
242
- if (!ws || ws.readyState !== WS_OPEN) {
243
- return;
244
- }
245
- try {
246
- ws.send(rawFrame);
247
- }
248
- catch {
249
- // best effort
250
- }
251
- }
252
- handleWebSocketFrame(raw) {
253
- const text = asWireText(raw);
254
- if (!text) {
255
- return;
256
- }
257
- try {
258
- const frame = JSON.parse(text);
259
- if (frame.method === 'event') {
260
- this.mirrorEventToSidecar(text);
261
- }
262
- this.dispatch(frame);
263
- }
264
- catch {
265
- const preview = text.trim().slice(0, MAX_LOG_PREVIEW) || '(empty frame)';
266
- this.pushLog(`[protocol] malformed websocket frame: ${preview}`);
267
- this.publish({ type: 'gateway.protocol_error', payload: { preview } });
268
- }
269
- }
270
- startSpawnedGateway(root) {
271
- const python = resolvePython(root);
272
- const cwd = process.env.HERMES_CWD || root;
273
- const env = { ...process.env };
274
- const pyPath = env.PYTHONPATH?.trim();
275
- env.PYTHONPATH = pyPath ? `${root}${delimiter}${pyPath}` : root;
276
- this.startReadyTimer(python, cwd);
277
- this.proc = spawn(python, ['-m', 'tui_gateway.entry'], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
278
- this.stdoutRl = createInterface({ input: this.proc.stdout });
279
- this.stdoutRl.on('line', raw => {
280
- try {
281
- this.dispatch(JSON.parse(raw));
282
- }
283
- catch {
284
- const preview = raw.trim().slice(0, MAX_LOG_PREVIEW) || '(empty line)';
285
- this.pushLog(`[protocol] malformed stdout: ${preview}`);
286
- this.publish({ type: 'gateway.protocol_error', payload: { preview } });
287
- }
288
- });
289
- this.stderrRl = createInterface({ input: this.proc.stderr });
290
- this.stderrRl.on('line', raw => {
291
- const line = truncateLine(raw.trim());
292
- if (!line) {
293
- return;
294
- }
295
- this.pushLog(line);
296
- this.publish({ type: 'gateway.stderr', payload: { line } });
297
- });
298
- const ownedProc = this.proc;
299
- this.proc.on('error', err => {
300
- // Skip stale errors on an already-replaced child.
301
- if (this.proc !== ownedProc) {
302
- return;
303
- }
304
- const line = `[spawn] ${err.message}`;
305
- this.pushLog(line);
306
- this.publish({ type: 'gateway.stderr', payload: { line } });
307
- // Detach the reference up front so the late `exit` event for
308
- // this same child is identity-skipped (we don't want to emit
309
- // 'exit' twice). Then run the full teardown — clears the
310
- // startup timer so we don't fire a misleading
311
- // `gateway.start_timeout`, rejects pending RPCs, and emits or
312
- // queues a single `exit`.
313
- this.proc = null;
314
- this.handleTransportExit(1, `gateway error: ${err.message}`);
315
- });
316
- this.proc.on('exit', code => {
317
- // start() can replace `this.proc` while an old child is still
318
- // tearing down. Skip stale exits so we don't clear the new
319
- // startup timer or reject newly-issued pending requests.
320
- if (this.proc !== ownedProc) {
321
- return;
322
- }
323
- this.handleTransportExit(code);
324
- });
325
- }
326
- startAttachedGateway(attachUrl) {
327
- const safeAttachUrl = redactUrl(attachUrl);
328
- this.startReadyTimer('websocket', safeAttachUrl);
329
- if (typeof WebSocket === 'undefined') {
330
- const line = `[startup] WebSocket API unavailable; cannot attach to ${safeAttachUrl}`;
331
- this.pushLog(line);
332
- this.publish({ type: 'gateway.stderr', payload: { line } });
333
- this.handleTransportExit(1, 'gateway websocket unavailable');
334
- return;
335
- }
336
- try {
337
- const ws = new WebSocket(attachUrl);
338
- let settled = false;
339
- this.ws = ws;
340
- const connectPromise = new Promise((resolve, reject) => {
341
- ws.addEventListener('open', () => {
342
- if (!settled) {
343
- settled = true;
344
- resolve();
345
- }
346
- this.connectSidecarMirror();
347
- }, { once: true });
348
- ws.addEventListener('error', () => {
349
- if (!settled) {
350
- this.pushLog('[startup] gateway websocket connect error');
351
- settled = true;
352
- reject(new Error('gateway websocket connection failed'));
353
- }
354
- }, { once: true });
355
- ws.addEventListener('close', ev => {
356
- if (!settled) {
357
- settled = true;
358
- reject(new Error(`gateway websocket closed (${ev.code}) during connect`));
359
- }
360
- }, { once: true });
361
- });
362
- // The connect promise is only awaited by RPCs that arrive while
363
- // the socket is still connecting. If no request races the open
364
- // (or a teardown drops the reference before anyone observes it),
365
- // a connect-error / early-close rejection would surface as an
366
- // unhandled promise rejection in Node. Attach a no-op handler to
367
- // ensure the rejection is always observed.
368
- connectPromise.catch(() => { });
369
- this.wsConnectPromise = connectPromise;
370
- ws.addEventListener('message', ev => this.handleWebSocketFrame(ev.data));
371
- ws.addEventListener('close', ev => {
372
- // Skip close events from sockets that have already been
373
- // replaced — start() / closeGatewaySocket() can swap `this.ws`
374
- // before an in-flight close lands, and we must not clear the
375
- // new ready timer or reject the new pending requests on behalf
376
- // of a stale socket.
377
- if (this.ws !== ws) {
378
- return;
379
- }
380
- this.ws = null;
381
- this.wsConnectPromise = null;
382
- this.handleTransportExit(ev.code, `gateway websocket closed${ev.code ? ` (${ev.code})` : ''}`);
383
- });
384
- ws.addEventListener('error', () => {
385
- const line = '[gateway] websocket transport error';
386
- this.pushLog(line);
387
- this.publish({ type: 'gateway.stderr', payload: { line } });
388
- });
389
- }
390
- catch (err) {
391
- this.pushLog(`[startup] failed to connect websocket gateway ${safeAttachUrl} (constructor error)`);
392
- this.handleTransportExit(1, 'gateway websocket startup failed');
393
- }
394
- }
395
- start() {
396
- const root = process.env.HERMES_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../');
397
- const attachUrl = resolveGatewayAttachUrl();
398
- const sidecarUrl = resolveSidecarUrl();
399
- this.attachUrl = attachUrl;
400
- this.sidecarUrl = sidecarUrl;
401
- this.resetStartupState();
402
- if (this.proc && !this.proc.killed && this.proc.exitCode === null) {
403
- this.proc.kill();
404
- }
405
- this.proc = null;
406
- this.closeGatewaySocket();
407
- this.closeSidecarSocket();
408
- if (attachUrl) {
409
- this.startAttachedGateway(attachUrl);
410
- return;
411
- }
412
- this.startSpawnedGateway(root);
413
- }
414
- dispatch(msg) {
415
- const id = msg.id;
416
- const p = id ? this.pending.get(id) : undefined;
417
- if (p) {
418
- this.settle(p, msg.error ? this.toError(msg.error) : null, msg.result);
419
- return;
420
- }
421
- if (msg.method === 'event') {
422
- const ev = asGatewayEvent(msg.params);
423
- if (ev) {
424
- this.publish(ev);
425
- }
426
- }
427
- }
428
- toError(raw) {
429
- const err = raw;
430
- return new Error(typeof err?.message === 'string' ? err.message : 'request failed');
431
- }
432
- settle(p, err, result) {
433
- clearTimeout(p.timeout);
434
- this.pending.delete(p.id);
435
- if (err) {
436
- p.reject(err);
437
- }
438
- else {
439
- p.resolve(result);
440
- }
441
- }
442
- pushLog(line) {
443
- this.logs.push(truncateLine(line));
444
- }
445
- rejectPending(err) {
446
- for (const p of this.pending.values()) {
447
- clearTimeout(p.timeout);
448
- p.reject(err);
449
- }
450
- this.pending.clear();
451
- }
452
- // Arrow class-field — stable identity, so `setTimeout(this.onTimeout, …, id)`
453
- // doesn't allocate a bound function per request.
454
- onTimeout = (id) => {
455
- const p = this.pending.get(id);
456
- if (p) {
457
- this.pending.delete(id);
458
- p.reject(new Error(`timeout: ${p.method}`));
459
- }
460
- };
461
- drain() {
462
- this.subscribed = true;
463
- for (const ev of this.bufferedEvents.drain()) {
464
- this.emit('event', ev);
465
- }
466
- if (this.pendingExit !== undefined) {
467
- const code = this.pendingExit;
468
- this.pendingExit = undefined;
469
- this.emit('exit', code);
470
- }
471
- }
472
- getLogTail(limit = 20) {
473
- return this.logs.tail(Math.max(1, limit)).join('\n');
474
- }
475
- async ensureAttachedWebSocket(method) {
476
- if (!this.attachUrl) {
477
- throw new Error('gateway not running');
478
- }
479
- if (!this.ws || this.ws.readyState === WS_CLOSED || this.ws.readyState === WS_CLOSING) {
480
- this.start();
481
- }
482
- if (this.ws?.readyState === WS_CONNECTING) {
483
- try {
484
- await this.wsConnectPromise;
485
- }
486
- catch (err) {
487
- throw err instanceof Error ? err : new Error(String(err));
488
- }
489
- }
490
- if (!this.ws || this.ws.readyState !== WS_OPEN) {
491
- throw new Error(`gateway not connected: ${method}`);
492
- }
493
- return this.ws;
494
- }
495
- requestOverWebSocket(method, params = {}) {
496
- return this.ensureAttachedWebSocket(method).then(ws => new Promise((resolve, reject) => {
497
- const id = `r${++this.reqId}`;
498
- const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id);
499
- timeout.unref?.();
500
- this.pending.set(id, {
501
- id,
502
- method,
503
- reject,
504
- resolve: v => resolve(v),
505
- timeout
506
- });
507
- try {
508
- ws.send(JSON.stringify({ id, jsonrpc: '2.0', method, params }));
509
- }
510
- catch (e) {
511
- const pending = this.pending.get(id);
512
- if (pending) {
513
- clearTimeout(pending.timeout);
514
- this.pending.delete(id);
515
- }
516
- reject(e instanceof Error ? e : new Error(String(e)));
517
- }
518
- }));
519
- }
520
- request(method, params = {}) {
521
- const attachUrl = resolveGatewayAttachUrl();
522
- if (attachUrl) {
523
- if (this.attachUrl !== attachUrl) {
524
- // The env var rotated at runtime — restart the transport so
525
- // switching from spawned-gateway mode to attach mode also
526
- // tears down the old Python child. Merely closing `this.ws`
527
- // would leave a previously spawned gateway process alive.
528
- this.rejectPending(new Error('gateway attach url changed'));
529
- this.start();
530
- }
531
- return this.requestOverWebSocket(method, params);
532
- }
533
- if (!this.proc?.stdin || this.proc.killed || this.proc.exitCode !== null) {
534
- this.start();
535
- }
536
- if (!this.proc?.stdin) {
537
- return Promise.reject(new Error('gateway not running'));
538
- }
539
- const id = `r${++this.reqId}`;
540
- return new Promise((resolve, reject) => {
541
- const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id);
542
- timeout.unref?.();
543
- this.pending.set(id, {
544
- id,
545
- method,
546
- reject,
547
- resolve: v => resolve(v),
548
- timeout
549
- });
550
- try {
551
- this.proc.stdin.write(JSON.stringify({ id, jsonrpc: '2.0', method, params }) + '\n');
552
- }
553
- catch (e) {
554
- const pending = this.pending.get(id);
555
- if (pending) {
556
- clearTimeout(pending.timeout);
557
- this.pending.delete(id);
558
- }
559
- reject(e instanceof Error ? e : new Error(String(e)));
560
- }
561
- });
562
- }
563
- kill() {
564
- this.proc?.kill();
565
- this.closeGatewaySocket();
566
- this.closeSidecarSocket();
567
- this.clearReadyTimer();
568
- // The ws 'close' handler is identity-gated on `this.ws === ws`
569
- // and we just nulled `this.ws`, so it will short-circuit and
570
- // skip handleTransportExit. Reject pending RPCs explicitly so
571
- // attach-mode promises do not hang after an intentional kill.
572
- this.rejectPending(new Error('gateway closed'));
573
- }
574
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,86 +0,0 @@
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 { useEffect, useRef, useState } from 'react';
6
- import { looksLikeSlashCommand } from '../domain/slash.js';
7
- import { asRpcResult } from '../lib/rpc.js';
8
- const TAB_PATH_RE = /((?:["']?(?:[A-Za-z]:[\\/]|\.{1,2}\/|~\/|\/|@|[^"'`\s]+\/))[^\s]*)$/;
9
- export function completionRequestForInput(input) {
10
- const isSlashCommand = looksLikeSlashCommand(input);
11
- const pathWord = isSlashCommand ? null : (input.match(TAB_PATH_RE)?.[1] ?? null);
12
- if (!isSlashCommand && !pathWord) {
13
- return null;
14
- }
15
- // `/model` uses the two-step ModelPicker (real curated IDs).
16
- // Slash completion here only showed short aliases + vendor/family meta.
17
- if (isSlashCommand && /^\/model(?:\s|$)/.test(input)) {
18
- return null;
19
- }
20
- if (isSlashCommand) {
21
- return { method: 'complete.slash', params: { text: input }, replaceFrom: 1 };
22
- }
23
- return {
24
- method: 'complete.path',
25
- params: { word: pathWord },
26
- replaceFrom: input.length - pathWord.length
27
- };
28
- }
29
- export function useCompletion(input, blocked, gw) {
30
- const [completions, setCompletions] = useState([]);
31
- const [compIdx, setCompIdx] = useState(0);
32
- const [compReplace, setCompReplace] = useState(0);
33
- const ref = useRef('');
34
- useEffect(() => {
35
- const clear = () => {
36
- setCompletions(prev => (prev.length ? [] : prev));
37
- setCompIdx(prev => (prev ? 0 : prev));
38
- setCompReplace(prev => (prev ? 0 : prev));
39
- };
40
- if (blocked) {
41
- ref.current = '';
42
- clear();
43
- return;
44
- }
45
- if (input === ref.current) {
46
- return;
47
- }
48
- ref.current = input;
49
- const request = completionRequestForInput(input);
50
- if (!request) {
51
- clear();
52
- return;
53
- }
54
- const t = setTimeout(() => {
55
- if (ref.current !== input) {
56
- return;
57
- }
58
- gw.request(request.method, request.params)
59
- .then(raw => {
60
- if (ref.current !== input) {
61
- return;
62
- }
63
- const r = asRpcResult(raw);
64
- setCompletions(r?.items ?? []);
65
- setCompIdx(0);
66
- setCompReplace(request.method === 'complete.slash' ? (r?.replace_from ?? 1) : request.replaceFrom);
67
- })
68
- .catch((e) => {
69
- if (ref.current !== input) {
70
- return;
71
- }
72
- setCompletions([
73
- {
74
- text: '',
75
- display: 'completion unavailable',
76
- meta: e instanceof Error && e.message ? e.message : 'unavailable'
77
- }
78
- ]);
79
- setCompIdx(0);
80
- setCompReplace(request.replaceFrom);
81
- });
82
- }, 60);
83
- return () => clearTimeout(t);
84
- }, [blocked, gw, input]);
85
- return { completions, compIdx, setCompIdx, compReplace };
86
- }