@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41

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 (68) hide show
  1. package/README.md +49 -4
  2. package/dist/bin/ai.js +119 -111
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +11 -6
  5. package/dist/src/api/auth.js +6 -4
  6. package/dist/src/api/browser-login.js +152 -37
  7. package/dist/src/api/chat.js +286 -44
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +127 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/background-jobs.js +4 -2
  13. package/dist/src/browser/bridge.js +232 -0
  14. package/dist/src/browser/framing.js +42 -0
  15. package/dist/src/browser/native-host.js +209 -0
  16. package/dist/src/browser/protocol.js +46 -0
  17. package/dist/src/browser/session-bridge.js +104 -0
  18. package/dist/src/cli-args.js +19 -5
  19. package/dist/src/client-environment.js +2 -0
  20. package/dist/src/core/clipboard.js +7 -13
  21. package/dist/src/core/image-limits.js +56 -0
  22. package/dist/src/core/image-path-extractor.js +70 -3
  23. package/dist/src/core/session-image-store.js +246 -0
  24. package/dist/src/edit-journal.js +33 -2
  25. package/dist/src/executor.js +25 -3
  26. package/dist/src/help-text.js +58 -17
  27. package/dist/src/patcher.js +43 -4
  28. package/dist/src/permissions.js +288 -0
  29. package/dist/src/session-safety.js +0 -12
  30. package/dist/src/session-store.js +121 -20
  31. package/dist/src/session.js +19 -4
  32. package/dist/src/signin.js +58 -0
  33. package/dist/src/tool-executor.js +12 -46
  34. package/dist/src/tools/browser.js +628 -0
  35. package/dist/src/tools/delete-file.js +20 -4
  36. package/dist/src/tools/index.js +32 -10
  37. package/dist/src/tools/patch-file.js +12 -26
  38. package/dist/src/tools/read-image-file.js +85 -0
  39. package/dist/src/tools/replace-document-text.js +28 -18
  40. package/dist/src/tools/restore-checkpoint.js +0 -1
  41. package/dist/src/tools/run-command.js +14 -71
  42. package/dist/src/tools/run-node-script.js +12 -81
  43. package/dist/src/tools/save-generated-image.js +120 -0
  44. package/dist/src/tools/str-replace.js +12 -26
  45. package/dist/src/tools/undo-edit.js +1 -6
  46. package/dist/src/tools/write-file.js +67 -11
  47. package/dist/src/ui/prompt-history-store.js +1 -1
  48. package/dist/src/ui/repl.js +802 -195
  49. package/dist/src/ui/tui/bridge.js +10 -0
  50. package/dist/src/ui/tui/build-frame.js +517 -165
  51. package/dist/src/ui/tui/composer-layout.js +76 -0
  52. package/dist/src/ui/tui/markdown-render.js +81 -73
  53. package/dist/src/ui/tui/shell-input.js +227 -67
  54. package/dist/src/ui/tui/terminal-theme.js +28 -0
  55. package/dist/src/ui/tui/terminal-title.js +3 -0
  56. package/dist/src/ui/tui/terminal-writes.js +48 -0
  57. package/dist/src/ui/tui/text.js +158 -4
  58. package/dist/src/ui/tui/user-input.js +568 -0
  59. package/dist/src/utils.js +9 -0
  60. package/package.json +20 -8
  61. package/dist/src/markdown-renderer.js +0 -112
  62. package/dist/src/project-index.js +0 -221
  63. package/dist/src/tools/code-intel.js +0 -472
  64. package/dist/src/tools/find-symbol.js +0 -70
  65. package/dist/src/tools/hover-symbol.js +0 -95
  66. package/dist/src/tools/list-symbols.js +0 -55
  67. package/dist/src/tools/search-code.js +0 -37
  68. package/dist/src/tools/signature-help.js +0 -118
@@ -1,6 +1,11 @@
1
+ import { moveComposerCursorVertically } from './composer-layout.js';
1
2
  import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
2
- import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, resolveApprovalChoiceFromInput, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
3
+ import { MAX_IMAGES_PER_MESSAGE, MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE, approximateBase64DecodedBytes, totalAttachmentBytes, } from '../../core/image-limits.js';
4
+ import { tryCacheAttachmentBytes } from '../../core/session-image-store.js';
5
+ import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
3
6
  import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
7
+ import { handleUserInputPromptEvent, } from './user-input.js';
8
+ const APPROVAL_PREVIEW_PAGE_ROWS = 3;
4
9
  function isClipboardImagePasteKey(key) {
5
10
  if (process.platform === 'win32') {
6
11
  return ((key.ctrl || key.meta) &&
@@ -9,6 +14,38 @@ function isClipboardImagePasteKey(key) {
9
14
  }
10
15
  return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
11
16
  }
17
+ export const APPROVAL_INPUT_GUARD_MS = 500;
18
+ function approvalIsGuarded(state) {
19
+ const openedAt = state.approvalOpenedAt;
20
+ if (typeof openedAt !== 'number')
21
+ return false;
22
+ return Date.now() - openedAt < APPROVAL_INPUT_GUARD_MS;
23
+ }
24
+ function applyUserInputPromptEvent(store, handlers, event) {
25
+ const current = store.getState();
26
+ if (!current.userInputPrompt)
27
+ return false;
28
+ const outcome = handleUserInputPromptEvent(current.userInputPrompt, event, handlers.getUserInputViewport?.());
29
+ store.update((state) => ({
30
+ ...state,
31
+ userInputPrompt: outcome.state,
32
+ }));
33
+ if (outcome.result) {
34
+ void handlers.onResolveUserInput?.(outcome.result);
35
+ }
36
+ return true;
37
+ }
38
+ function composerIsEmpty(state) {
39
+ return (!state.input &&
40
+ state.cursor === 0 &&
41
+ state.pastedChunks.length === 0 &&
42
+ state.promptHistoryCursor === null);
43
+ }
44
+ function composerHasDiscardableDraft(state) {
45
+ if (!composerIsEmpty(state))
46
+ return true;
47
+ return !state.busy && state.imageAttachments.length > 0;
48
+ }
12
49
  function shouldShowCommandPalette(state) {
13
50
  if (state.busy ||
14
51
  state.exiting ||
@@ -74,6 +111,41 @@ function pasteTextFromClipboard(store, handlers) {
74
111
  }
75
112
  insertPastedText(store, handlers, text);
76
113
  }
114
+ function scrollTranscript(store, handlers, delta) {
115
+ if (delta === 0)
116
+ return;
117
+ store.update((current) => {
118
+ const limit = handlers.getTranscriptScrollLimit?.() ??
119
+ current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
120
+ const next = current.transcriptScrollOffset + delta;
121
+ return {
122
+ ...current,
123
+ transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
124
+ };
125
+ });
126
+ }
127
+ function scrollTranscriptTo(store, handlers, offset) {
128
+ store.update((current) => {
129
+ const limit = handlers.getTranscriptScrollLimit?.() ??
130
+ current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
131
+ return {
132
+ ...current,
133
+ transcriptScrollOffset: Math.max(0, Math.min(Math.trunc(offset), limit)),
134
+ };
135
+ });
136
+ }
137
+ function scrollApprovalPreview(store, handlers, delta) {
138
+ if (delta === 0)
139
+ return;
140
+ store.update((current) => {
141
+ const limit = handlers.getApprovalScrollLimit?.() ?? 0;
142
+ const next = (current.approvalScrollOffset ?? 0) + delta;
143
+ return {
144
+ ...current,
145
+ approvalScrollOffset: Math.max(0, Math.min(next, limit)),
146
+ };
147
+ });
148
+ }
77
149
  function filterResumeSessionsLocal(sessions, filter, serverModels) {
78
150
  const q = filter.trim().toLowerCase();
79
151
  if (!q)
@@ -88,6 +160,12 @@ function filterResumeSessionsLocal(sessions, filter, serverModels) {
88
160
  }
89
161
  export function handleShellKeyEvent(store, handlers, event) {
90
162
  if (event.kind === 'paste') {
163
+ if (applyUserInputPromptEvent(store, handlers, {
164
+ kind: 'paste',
165
+ text: event.text,
166
+ })) {
167
+ return;
168
+ }
91
169
  insertPastedText(store, handlers, event.text);
92
170
  return;
93
171
  }
@@ -110,33 +188,67 @@ export function handleShellKeyEvent(store, handlers, event) {
110
188
  return;
111
189
  }
112
190
  if (event.kind === 'contextMenu') {
191
+ if (store.getState().userInputPrompt) {
192
+ const text = (handlers.readClipboardText ?? readClipboardText)();
193
+ if (text) {
194
+ applyUserInputPromptEvent(store, handlers, { kind: 'paste', text });
195
+ }
196
+ return;
197
+ }
113
198
  pasteTextFromClipboard(store, handlers);
114
199
  return;
115
200
  }
116
201
  if (event.kind === 'transcriptScroll') {
117
- const delta = Math.trunc(event.deltaLines);
118
- if (delta !== 0) {
119
- store.update((current) => {
120
- const limit = handlers.getTranscriptScrollLimit?.() ??
121
- current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
122
- const next = current.transcriptScrollOffset + delta;
123
- return {
124
- ...current,
125
- transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
126
- };
127
- });
128
- }
202
+ scrollTranscript(store, handlers, Math.trunc(event.deltaLines));
203
+ return;
204
+ }
205
+ if (event.kind === 'transcriptScrollTo') {
206
+ scrollTranscriptTo(store, handlers, Number(event.offset ?? 0));
129
207
  return;
130
208
  }
131
209
  if (event.kind !== 'key')
132
210
  return;
133
211
  const key = event;
134
212
  const state = store.getState();
213
+ const commandPaletteActive = shouldShowCommandPalette(state);
214
+ const commandSuggestions = commandPaletteActive
215
+ ? getSlashCommandSuggestions(state.input)
216
+ : [];
217
+ const prepareForComposerInputChange = (current, nextInput) => {
218
+ if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
219
+ handlers.onLiveFrameShapeChange();
220
+ }
221
+ };
135
222
  if (key.ctrl && key.input === 'c') {
223
+ if (state.userInputPrompt) {
224
+ handlers.onCtrlC?.();
225
+ return;
226
+ }
136
227
  if (state.sudoPrompt) {
137
228
  handlers.onSudoPasswordInput({ kind: 'cancel' });
138
229
  return;
139
230
  }
231
+ if (state.queuedMessage) {
232
+ handlers.onLiveFrameShapeChange();
233
+ store.update((current) => ({ ...current, queuedMessage: null }));
234
+ return;
235
+ }
236
+ if (composerHasDiscardableDraft(state)) {
237
+ store.update((current) => {
238
+ prepareForComposerInputChange(current, '');
239
+ return {
240
+ ...current,
241
+ commandCursor: 0,
242
+ cursor: 0,
243
+ imageAttachments: current.busy ? current.imageAttachments : [],
244
+ input: '',
245
+ pastedChunks: [],
246
+ promptHistoryCursor: null,
247
+ promptHistoryDraft: '',
248
+ };
249
+ });
250
+ return;
251
+ }
140
252
  if (handlers.onCtrlC) {
141
253
  handlers.onCtrlC();
142
254
  return;
@@ -144,15 +256,10 @@ export function handleShellKeyEvent(store, handlers, event) {
144
256
  handlers.onRequestExit();
145
257
  return;
146
258
  }
147
- const commandPaletteActive = shouldShowCommandPalette(state);
148
- const commandSuggestions = commandPaletteActive
149
- ? getSlashCommandSuggestions(state.input)
150
- : [];
151
- const prepareForComposerInputChange = (current, nextInput) => {
152
- if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
153
- handlers.onLiveFrameShapeChange();
154
- }
155
- };
259
+ if (state.userInputPrompt &&
260
+ applyUserInputPromptEvent(store, handlers, key)) {
261
+ return;
262
+ }
156
263
  if (state.sudoPrompt) {
157
264
  if (key.escape) {
158
265
  handlers.onSudoPasswordInput({ kind: 'cancel' });
@@ -176,24 +283,32 @@ export function handleShellKeyEvent(store, handlers, event) {
176
283
  return;
177
284
  }
178
285
  if (state.approvalPrompt) {
179
- const directChoice = resolveApprovalChoiceFromInput(key.input);
180
- if (directChoice) {
181
- void handlers.onResolveApproval(directChoice);
182
- return;
183
- }
184
- if (key.escape) {
185
- void handlers.onResolveApproval('n');
286
+ if (key.pageUp || key.pageDown) {
287
+ const delta = key.pageUp ? -APPROVAL_PREVIEW_PAGE_ROWS : APPROVAL_PREVIEW_PAGE_ROWS;
288
+ if (key.shift) {
289
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
290
+ }
291
+ else {
292
+ scrollApprovalPreview(store, handlers, delta);
293
+ }
186
294
  return;
187
295
  }
188
296
  if (key.upArrow || key.downArrow) {
189
297
  store.update((current) => ({
190
298
  ...current,
191
- approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1),
299
+ approvalCursor: getNextApprovalCursor(current.approvalCursor, key.upArrow ? -1 : 1, current.approvalPrompt?.options?.length ?? 0),
192
300
  }));
193
301
  return;
194
302
  }
303
+ if (approvalIsGuarded(state)) {
304
+ return;
305
+ }
306
+ if (key.escape) {
307
+ void handlers.onResolveApproval(-1);
308
+ return;
309
+ }
195
310
  if (key.returnKey) {
196
- void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
311
+ void handlers.onResolveApproval(state.approvalCursor);
197
312
  }
198
313
  return;
199
314
  }
@@ -201,24 +316,18 @@ export function handleShellKeyEvent(store, handlers, event) {
201
316
  return;
202
317
  }
203
318
  if (key.pageUp || key.pageDown) {
204
- store.update((current) => {
205
- const transcriptLines = handlers.getTranscriptScrollLimit?.() ??
206
- current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
207
- const next = current.transcriptScrollOffset + (key.pageUp ? 8 : -8);
208
- return {
209
- ...current,
210
- transcriptScrollOffset: Math.max(0, Math.min(next, transcriptLines)),
211
- };
212
- });
319
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
213
320
  return;
214
321
  }
215
322
  if (state.jobsPickerOpen) {
216
- if (key.escape) {
323
+ if (key.escape || (key.ctrl && key.input === 'b' && !key.meta && !key.shift)) {
324
+ handlers.onLiveFrameShapeChange();
217
325
  store.update((current) => ({
218
326
  ...current,
219
327
  jobsPickerExpandedId: null,
220
328
  jobsPickerOpen: false,
221
- status: 'Ready',
329
+ jobsPickerReturnStatus: null,
330
+ status: current.jobsPickerReturnStatus ?? 'Ready',
222
331
  }));
223
332
  return;
224
333
  }
@@ -306,6 +415,12 @@ export function handleShellKeyEvent(store, handlers, event) {
306
415
  }
307
416
  return;
308
417
  }
418
+ if (key.ctrl && key.input === 'b' && !key.meta && !key.shift) {
419
+ if ((state.backgroundJobs ?? []).length > 0) {
420
+ handlers.onOpenJobsPicker();
421
+ }
422
+ return;
423
+ }
309
424
  if (key.escape) {
310
425
  if (state.busy) {
311
426
  if (state.queuedMessage) {
@@ -317,10 +432,7 @@ export function handleShellKeyEvent(store, handlers, event) {
317
432
  return;
318
433
  }
319
434
  store.update((current) => {
320
- if (!current.input &&
321
- current.cursor === 0 &&
322
- current.pastedChunks.length === 0 &&
323
- current.promptHistoryCursor === null) {
435
+ if (composerIsEmpty(current)) {
324
436
  return current;
325
437
  }
326
438
  prepareForComposerInputChange(current, '');
@@ -340,6 +452,15 @@ export function handleShellKeyEvent(store, handlers, event) {
340
452
  handlers.onCycleAgentMode();
341
453
  return;
342
454
  }
455
+ if (state.busy && state.queuedMessage) {
456
+ if (key.returnKey) {
457
+ void handlers.onFireQueuedMessage?.();
458
+ return;
459
+ }
460
+ if (!key.upArrow && !isClipboardImagePasteKey(key)) {
461
+ return;
462
+ }
463
+ }
343
464
  if (commandPaletteActive && (key.upArrow || key.downArrow)) {
344
465
  store.update((current) => ({
345
466
  ...current,
@@ -348,7 +469,7 @@ export function handleShellKeyEvent(store, handlers, event) {
348
469
  return;
349
470
  }
350
471
  if (key.upArrow) {
351
- if (state.busy && state.input.trim() === '' && state.queuedMessage) {
472
+ if (state.busy && state.queuedMessage) {
352
473
  handlers.onLiveFrameShapeChange();
353
474
  store.update((current) => {
354
475
  const queued = current.queuedMessage;
@@ -367,9 +488,13 @@ export function handleShellKeyEvent(store, handlers, event) {
367
488
  });
368
489
  return;
369
490
  }
370
- if (state.busy)
491
+ const nextCursor = moveComposerCursorVertically(state.input, state.cursor, handlers.getComposerInputWidth?.() ?? Number.POSITIVE_INFINITY, -1);
492
+ if (nextCursor === null && state.busy)
371
493
  return;
372
494
  store.update((current) => {
495
+ if (nextCursor !== null) {
496
+ return { ...current, commandCursor: 0, cursor: nextCursor };
497
+ }
373
498
  const next = navigatePromptHistory(current, 'previous');
374
499
  prepareForComposerInputChange(current, next.input);
375
500
  return next;
@@ -377,9 +502,13 @@ export function handleShellKeyEvent(store, handlers, event) {
377
502
  return;
378
503
  }
379
504
  if (key.downArrow) {
380
- if (state.busy)
505
+ const nextCursor = moveComposerCursorVertically(state.input, state.cursor, handlers.getComposerInputWidth?.() ?? Number.POSITIVE_INFINITY, 1);
506
+ if (nextCursor === null && state.busy)
381
507
  return;
382
508
  store.update((current) => {
509
+ if (nextCursor !== null) {
510
+ return { ...current, commandCursor: 0, cursor: nextCursor };
511
+ }
383
512
  const next = navigatePromptHistory(current, 'next');
384
513
  prepareForComposerInputChange(current, next.input);
385
514
  return next;
@@ -467,12 +596,16 @@ export function handleShellKeyEvent(store, handlers, event) {
467
596
  }
468
597
  if (isClipboardImagePasteKey(key)) {
469
598
  const current = store.getState();
470
- if (current.busy)
471
- return;
472
- const liveAttachments = current.imageAttachments.filter((attachment) => current.input.includes(`[Image #${attachment.index}]`));
473
- if (liveAttachments.length >= 2) {
599
+ const target = current.queuedMessage
600
+ ? {
601
+ body: current.queuedMessage.body,
602
+ attachments: current.queuedMessage.imageAttachments,
603
+ }
604
+ : { body: current.input, attachments: current.imageAttachments };
605
+ const liveAttachments = target.attachments.filter((attachment) => target.body.includes(`[Image #${attachment.index}]`));
606
+ if (liveAttachments.length >= MAX_IMAGES_PER_MESSAGE) {
474
607
  store.appendEntry({
475
- body: 'Maximum of 2 images per message. Send the current message first.',
608
+ body: `Maximum of ${MAX_IMAGES_PER_MESSAGE} images per message. Send the current message first.`,
476
609
  kind: 'error',
477
610
  title: 'Image',
478
611
  });
@@ -480,22 +613,49 @@ export function handleShellKeyEvent(store, handlers, event) {
480
613
  }
481
614
  try {
482
615
  const clipResult = readClipboardImage();
616
+ if (totalAttachmentBytes(liveAttachments) +
617
+ approximateBase64DecodedBytes(clipResult.base64Data) >
618
+ MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE) {
619
+ store.appendEntry({
620
+ body: `This image would put the message over the ${Math.round(MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE / 1024 / 1024)}MB combined image limit. Send the current message first.`,
621
+ kind: 'error',
622
+ title: 'Image',
623
+ });
624
+ return;
625
+ }
483
626
  const idx = liveAttachments.length === 0
484
627
  ? 1
485
628
  : Math.max(...liveAttachments.map((attachment) => attachment.index)) + 1;
486
- store.update((shellState) => ({
487
- ...shellState,
488
- imageAttachments: [
489
- ...liveAttachments,
490
- {
491
- index: idx,
492
- mimeType: clipResult.mimeType,
493
- base64Data: clipResult.base64Data,
494
- source: 'clipboard',
495
- },
496
- ],
497
- ...insertAtCursor(shellState, `[Image #${idx}] `),
498
- }));
629
+ const cached = tryCacheAttachmentBytes({
630
+ base64Data: clipResult.base64Data,
631
+ mimeType: clipResult.mimeType,
632
+ });
633
+ const attachment = {
634
+ index: cached?.index ?? idx,
635
+ mimeType: clipResult.mimeType,
636
+ base64Data: clipResult.base64Data,
637
+ source: 'clipboard',
638
+ ...(cached ? { cachePath: cached.cachePath } : {}),
639
+ };
640
+ const marker = attachment.index;
641
+ store.update((shellState) => {
642
+ const nextAttachments = [...liveAttachments, attachment];
643
+ if (shellState.queuedMessage) {
644
+ return {
645
+ ...shellState,
646
+ queuedMessage: {
647
+ ...shellState.queuedMessage,
648
+ body: `${shellState.queuedMessage.body} [Image #${marker}]`.trim(),
649
+ imageAttachments: nextAttachments,
650
+ },
651
+ };
652
+ }
653
+ return {
654
+ ...shellState,
655
+ imageAttachments: nextAttachments,
656
+ ...insertAtCursor(shellState, `[Image #${marker}] `),
657
+ };
658
+ });
499
659
  }
500
660
  catch (error) {
501
661
  if (error?.code === 'NO_IMAGE') {
@@ -0,0 +1,28 @@
1
+ export function isDarkTerminalBackground(env = process.env) {
2
+ const raw = String(env.COLORFGBG ?? '').trim();
3
+ if (!raw)
4
+ return false;
5
+ const parts = raw
6
+ .split(/[;:]/)
7
+ .map((part) => part.trim())
8
+ .filter(Boolean);
9
+ const bgToken = parts[parts.length - 1];
10
+ if (!bgToken)
11
+ return false;
12
+ const bg = Number(bgToken);
13
+ if (!Number.isFinite(bg))
14
+ return false;
15
+ return bg >= 0 && bg < 7;
16
+ }
17
+ export function mutedStyle(env = process.env) {
18
+ if (isDarkTerminalBackground(env)) {
19
+ return { color: 'ansi256(247)' };
20
+ }
21
+ return { color: 'gray' };
22
+ }
23
+ export function mutedColor(env = process.env) {
24
+ if (isDarkTerminalBackground(env)) {
25
+ return 'ansi256(247)';
26
+ }
27
+ return 'gray';
28
+ }
@@ -1,3 +1,4 @@
1
+ import { isTuiMode } from '../../runtime-mode.js';
1
2
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
2
3
  const TITLE_BRAND = 'TheGitAI';
3
4
  const TITLE_MARK_PREFIX = '❯_';
@@ -19,6 +20,8 @@ export function formatTerminalTitle(state, spinnerFrame = 0) {
19
20
  return `${TITLE_MARK_PREFIX}● ${TITLE_BRAND}`;
20
21
  }
21
22
  export function writeTerminalTitle(title, stream = process.stdout) {
23
+ if (isTuiMode())
24
+ return;
22
25
  if (!('isTTY' in stream) || !stream.isTTY)
23
26
  return;
24
27
  stream.write(`\x1b]0;${title}\x07`);
@@ -0,0 +1,48 @@
1
+ const MAX_CAPTURED_CHARS = 1_000_000;
2
+ let restore = null;
3
+ let captured = [];
4
+ let capturedChars = 0;
5
+ function chunkToString(chunk, encoding) {
6
+ if (typeof chunk === 'string')
7
+ return chunk;
8
+ if (chunk instanceof Uint8Array) {
9
+ return Buffer.from(chunk).toString(typeof encoding === 'string' ? encoding : 'utf8');
10
+ }
11
+ return String(chunk ?? '');
12
+ }
13
+ export function captureTerminalWrites() {
14
+ if (restore)
15
+ return;
16
+ const streams = [process.stdout, process.stderr];
17
+ const originals = streams.map((stream) => stream.write.bind(stream));
18
+ for (const stream of streams) {
19
+ stream.write = (chunk, encoding, callback) => {
20
+ if (capturedChars < MAX_CAPTURED_CHARS) {
21
+ const text = chunkToString(chunk, encoding);
22
+ captured.push(text);
23
+ capturedChars += text.length;
24
+ }
25
+ const done = typeof encoding === 'function' ? encoding : callback;
26
+ if (typeof done === 'function')
27
+ done();
28
+ return true;
29
+ };
30
+ }
31
+ restore = () => {
32
+ streams.forEach((stream, index) => {
33
+ stream.write = originals[index];
34
+ });
35
+ };
36
+ }
37
+ export function releaseTerminalWrites() {
38
+ if (!restore)
39
+ return;
40
+ restore();
41
+ restore = null;
42
+ if (captured.length > 0) {
43
+ const text = captured.join('');
44
+ captured = [];
45
+ capturedChars = 0;
46
+ process.stderr.write(text);
47
+ }
48
+ }