@iaforged/context-code 1.2.12 → 1.3.5
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.
|
@@ -6,6 +6,7 @@ import { createOrchestrationMessage, createOrchestrationRun, createOrchestration
|
|
|
6
6
|
import { activityManager } from '../../utils/activityManager.js';
|
|
7
7
|
import { manuallyExtractSessionMemory } from '../../services/sessionMemory/sessionMemory.js';
|
|
8
8
|
import { createUserMessage, createAssistantMessage } from '../../utils/messages.js';
|
|
9
|
+
import { logForDebugging } from '../../utils/debug.js';
|
|
9
10
|
function parseArgs(args) {
|
|
10
11
|
const trimmed = args.trim();
|
|
11
12
|
if (!trimmed) {
|
|
@@ -5,6 +5,8 @@ import { getTerminalFocusState, subscribeTerminalFocus, } from '../ink/terminal-
|
|
|
5
5
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
|
6
6
|
import { generateAwaySummary } from '../services/awaySummary.js';
|
|
7
7
|
import { createAwaySummaryMessage } from '../utils/messages.js';
|
|
8
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
9
|
+
import { getGlobalConfig } from '../utils/config.js';
|
|
8
10
|
function hasSummarySinceLastUserTurn(messages) {
|
|
9
11
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10
12
|
const m = messages[i];
|
|
@@ -17,7 +17,7 @@ export function useDoublePress(setPending, onDoublePress, onFirstPress) {
|
|
|
17
17
|
clearTimeoutSafe();
|
|
18
18
|
};
|
|
19
19
|
}, [clearTimeoutSafe]);
|
|
20
|
-
return useCallback(() => {
|
|
20
|
+
return useCallback((...args) => {
|
|
21
21
|
const now = Date.now();
|
|
22
22
|
const timeSinceLastPress = now - lastPressRef.current;
|
|
23
23
|
const isDoublePress = timeSinceLastPress <= DOUBLE_PRESS_TIMEOUT_MS &&
|
|
@@ -25,19 +25,19 @@ export function useDoublePress(setPending, onDoublePress, onFirstPress) {
|
|
|
25
25
|
if (isDoublePress) {
|
|
26
26
|
// Double press detected
|
|
27
27
|
clearTimeoutSafe();
|
|
28
|
-
setPending(false);
|
|
29
|
-
onDoublePress();
|
|
28
|
+
setPending(false, ...args);
|
|
29
|
+
onDoublePress(...args);
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
32
|
// First press
|
|
33
|
-
onFirstPress?.();
|
|
34
|
-
setPending(true);
|
|
33
|
+
onFirstPress?.(...args);
|
|
34
|
+
setPending(true, ...args);
|
|
35
35
|
// Clear any existing timeout and set new one
|
|
36
36
|
clearTimeoutSafe();
|
|
37
|
-
timeoutRef.current = setTimeout((setPending, timeoutRef) => {
|
|
38
|
-
setPending(false);
|
|
37
|
+
timeoutRef.current = setTimeout((setPending, timeoutRef, ...args) => {
|
|
38
|
+
setPending(false, ...args);
|
|
39
39
|
timeoutRef.current = undefined;
|
|
40
|
-
}, DOUBLE_PRESS_TIMEOUT_MS, setPending, timeoutRef);
|
|
40
|
+
}, DOUBLE_PRESS_TIMEOUT_MS, setPending, timeoutRef, ...args);
|
|
41
41
|
}
|
|
42
42
|
lastPressRef.current = now;
|
|
43
43
|
}, [setPending, onDoublePress, onFirstPress, clearTimeoutSafe]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
1
2
|
import { isInputModeCharacter } from '../components/PromptInput/inputModes.js';
|
|
2
3
|
import { useNotifications } from '../context/notifications.js';
|
|
3
4
|
import stripAnsi from 'strip-ansi';
|
|
@@ -8,7 +9,7 @@ import { env } from '../utils/env.js';
|
|
|
8
9
|
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js';
|
|
9
10
|
import { isModifierPressed, prewarmModifiers } from '../utils/modifiers.js';
|
|
10
11
|
import { useDoublePress } from './useDoublePress.js';
|
|
11
|
-
const NOOP_HANDLER = () => { };
|
|
12
|
+
const NOOP_HANDLER = (_c, _i) => { };
|
|
12
13
|
function insertPlainText(cursor, input) {
|
|
13
14
|
const text = stripAnsi(input)
|
|
14
15
|
// eslint-disable-next-line custom-rules/no-lookbehind-regex -- .replace(re, str) on 1-2 char keystrokes: no-match returns same string (Object.is), regex never runs
|
|
@@ -21,8 +22,8 @@ function insertPlainText(cursor, input) {
|
|
|
21
22
|
}
|
|
22
23
|
function mapInput(input_map) {
|
|
23
24
|
const map = new Map(input_map);
|
|
24
|
-
return function (input) {
|
|
25
|
-
return (map.get(input) ?? NOOP_HANDLER)(input);
|
|
25
|
+
return function (cursor, input) {
|
|
26
|
+
return (map.get(input) ?? NOOP_HANDLER)(cursor, input);
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
export function useTextInput({ value: originalValue, onChange, onSubmit, onExit, onExitMessage, onHistoryUp, onHistoryDown, onHistoryReset, onClearInput, mask = '', multiline = false, cursorChar, invert, columns, onImagePaste: _onImagePaste, disableCursorMovementForUpDownKeys = false, disableEscapeDoublePress = false, maxVisibleLines, externalOffset, onOffsetChange, inputFilter, inlineGhostText, dim, }) {
|
|
@@ -32,12 +33,25 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
32
33
|
}
|
|
33
34
|
const offset = externalOffset;
|
|
34
35
|
const setOffset = onOffsetChange;
|
|
36
|
+
// Maintain synchronous internal state to prevent character loss during rapid typing.
|
|
37
|
+
// React re-renders can be slower than keystrokes in a complex TUI.
|
|
38
|
+
const lastProcessedValueRef = useRef(originalValue);
|
|
39
|
+
const lastProcessedOffsetRef = useRef(externalOffset);
|
|
40
|
+
// Sync internal state with props when they change externally (history, undo, etc.)
|
|
41
|
+
const [lastPropValue, setLastPropValue] = useState(originalValue);
|
|
42
|
+
const [lastPropOffset, setLastPropOffset] = useState(externalOffset);
|
|
43
|
+
if (originalValue !== lastPropValue || externalOffset !== lastPropOffset) {
|
|
44
|
+
lastProcessedValueRef.current = originalValue;
|
|
45
|
+
lastProcessedOffsetRef.current = externalOffset;
|
|
46
|
+
setLastPropValue(originalValue);
|
|
47
|
+
setLastPropOffset(externalOffset);
|
|
48
|
+
}
|
|
35
49
|
const cursor = Cursor.fromText(originalValue, columns, offset);
|
|
36
50
|
const { addNotification, removeNotification } = useNotifications();
|
|
37
|
-
const handleCtrlC = useDoublePress(show => {
|
|
51
|
+
const handleCtrlC = useDoublePress((show, _cursor) => {
|
|
38
52
|
onExitMessage?.(show, 'Ctrl-C');
|
|
39
|
-
}, () => onExit?.(), () => {
|
|
40
|
-
if (
|
|
53
|
+
}, (_cursor) => onExit?.(), (cursor) => {
|
|
54
|
+
if (cursor.text) {
|
|
41
55
|
onChange('');
|
|
42
56
|
setOffset(0);
|
|
43
57
|
onHistoryReset?.();
|
|
@@ -47,8 +61,8 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
47
61
|
// It's a text-level double-press escape for clearing input, not an action-level keybinding.
|
|
48
62
|
// Double-press Esc clears the input and saves to history - this is text editing behavior,
|
|
49
63
|
// not dialog dismissal, and needs the double-press safety mechanism.
|
|
50
|
-
const handleEscape = useDoublePress((show) => {
|
|
51
|
-
if (!
|
|
64
|
+
const handleEscape = useDoublePress((show, cursor) => {
|
|
65
|
+
if (!cursor.text || !show) {
|
|
52
66
|
return;
|
|
53
67
|
}
|
|
54
68
|
addNotification({
|
|
@@ -57,57 +71,57 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
57
71
|
priority: 'immediate',
|
|
58
72
|
timeoutMs: 1000,
|
|
59
73
|
});
|
|
60
|
-
}, () => {
|
|
74
|
+
}, (_show, cursor) => {
|
|
61
75
|
// Remove the "Esc again to clear" notification immediately
|
|
62
76
|
removeNotification('escape-again-to-clear');
|
|
63
77
|
onClearInput?.();
|
|
64
|
-
if (
|
|
78
|
+
if (cursor.text) {
|
|
65
79
|
// Track double-escape usage for feature discovery
|
|
66
80
|
// Save to history before clearing
|
|
67
|
-
if (
|
|
68
|
-
addToHistory(
|
|
81
|
+
if (cursor.text.trim() !== '') {
|
|
82
|
+
addToHistory(cursor.text);
|
|
69
83
|
}
|
|
70
84
|
onChange('');
|
|
71
85
|
setOffset(0);
|
|
72
86
|
onHistoryReset?.();
|
|
73
87
|
}
|
|
74
88
|
});
|
|
75
|
-
const handleEmptyCtrlD = useDoublePress(show => {
|
|
76
|
-
if (
|
|
89
|
+
const handleEmptyCtrlD = useDoublePress((show, cursor) => {
|
|
90
|
+
if (cursor.text !== '') {
|
|
77
91
|
return;
|
|
78
92
|
}
|
|
79
93
|
onExitMessage?.(show, 'Ctrl-D');
|
|
80
|
-
}, () => {
|
|
81
|
-
if (
|
|
94
|
+
}, (_show, cursor) => {
|
|
95
|
+
if (cursor.text !== '') {
|
|
82
96
|
return;
|
|
83
97
|
}
|
|
84
98
|
onExit?.();
|
|
85
99
|
});
|
|
86
|
-
function handleCtrlD() {
|
|
100
|
+
function handleCtrlD(cursor) {
|
|
87
101
|
if (cursor.text === '') {
|
|
88
102
|
// When input is empty, handle double-press
|
|
89
|
-
handleEmptyCtrlD();
|
|
103
|
+
handleEmptyCtrlD(cursor);
|
|
90
104
|
return cursor;
|
|
91
105
|
}
|
|
92
106
|
// When input is not empty, delete forward like iPython
|
|
93
107
|
return cursor.del();
|
|
94
108
|
}
|
|
95
|
-
function killToLineEnd() {
|
|
109
|
+
function killToLineEnd(cursor) {
|
|
96
110
|
const { cursor: newCursor, killed } = cursor.deleteToLineEnd();
|
|
97
111
|
pushToKillRing(killed, 'append');
|
|
98
112
|
return newCursor;
|
|
99
113
|
}
|
|
100
|
-
function killToLineStart() {
|
|
114
|
+
function killToLineStart(cursor) {
|
|
101
115
|
const { cursor: newCursor, killed } = cursor.deleteToLineStart();
|
|
102
116
|
pushToKillRing(killed, 'prepend');
|
|
103
117
|
return newCursor;
|
|
104
118
|
}
|
|
105
|
-
function killWordBefore() {
|
|
119
|
+
function killWordBefore(cursor) {
|
|
106
120
|
const { cursor: newCursor, killed } = cursor.deleteWordBefore();
|
|
107
121
|
pushToKillRing(killed, 'prepend');
|
|
108
122
|
return newCursor;
|
|
109
123
|
}
|
|
110
|
-
function yank() {
|
|
124
|
+
function yank(cursor) {
|
|
111
125
|
const text = getLastKill();
|
|
112
126
|
if (text.length > 0) {
|
|
113
127
|
const startOffset = cursor.offset;
|
|
@@ -117,7 +131,7 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
117
131
|
}
|
|
118
132
|
return cursor;
|
|
119
133
|
}
|
|
120
|
-
function handleYankPop() {
|
|
134
|
+
function handleYankPop(cursor) {
|
|
121
135
|
const popResult = yankPop();
|
|
122
136
|
if (!popResult) {
|
|
123
137
|
return cursor;
|
|
@@ -132,27 +146,27 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
132
146
|
return Cursor.fromText(newText, columns, newOffset);
|
|
133
147
|
}
|
|
134
148
|
const handleCtrl = mapInput([
|
|
135
|
-
['a',
|
|
136
|
-
['b',
|
|
137
|
-
['c', handleCtrlC],
|
|
138
|
-
['d', handleCtrlD],
|
|
139
|
-
['e',
|
|
140
|
-
['f',
|
|
141
|
-
['h',
|
|
142
|
-
['k', killToLineEnd],
|
|
143
|
-
['n', () => downOrHistoryDown()],
|
|
144
|
-
['p', () => upOrHistoryUp()],
|
|
145
|
-
['u', killToLineStart],
|
|
146
|
-
['w', killWordBefore],
|
|
147
|
-
['y', yank],
|
|
149
|
+
['a', cursor => cursor.startOfLine()],
|
|
150
|
+
['b', cursor => cursor.left()],
|
|
151
|
+
['c', (cursor, _input) => handleCtrlC(cursor)],
|
|
152
|
+
['d', (cursor, _input) => handleCtrlD(cursor)],
|
|
153
|
+
['e', cursor => cursor.endOfLine()],
|
|
154
|
+
['f', cursor => cursor.right()],
|
|
155
|
+
['h', cursor => cursor.deleteTokenBefore() ?? cursor.backspace()],
|
|
156
|
+
['k', (cursor, _input) => killToLineEnd(cursor)],
|
|
157
|
+
['n', (cursor, _input) => downOrHistoryDown(cursor)],
|
|
158
|
+
['p', (cursor, _input) => upOrHistoryUp(cursor)],
|
|
159
|
+
['u', (cursor, _input) => killToLineStart(cursor)],
|
|
160
|
+
['w', (cursor, _input) => killWordBefore(cursor)],
|
|
161
|
+
['y', (cursor, _input) => yank(cursor)],
|
|
148
162
|
]);
|
|
149
163
|
const handleMeta = mapInput([
|
|
150
|
-
['b',
|
|
151
|
-
['f',
|
|
152
|
-
['d',
|
|
153
|
-
['y', handleYankPop],
|
|
164
|
+
['b', cursor => cursor.prevWord()],
|
|
165
|
+
['f', cursor => cursor.nextWord()],
|
|
166
|
+
['d', cursor => cursor.deleteWordAfter()],
|
|
167
|
+
['y', (cursor, _input) => handleYankPop(cursor)],
|
|
154
168
|
]);
|
|
155
|
-
function handleEnter(key) {
|
|
169
|
+
function handleEnter(cursor, key) {
|
|
156
170
|
if (multiline &&
|
|
157
171
|
cursor.offset > 0 &&
|
|
158
172
|
cursor.text[cursor.offset - 1] === '\\') {
|
|
@@ -169,9 +183,9 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
169
183
|
if (env.terminal === 'Apple_Terminal' && isModifierPressed('shift')) {
|
|
170
184
|
return cursor.insert('\n');
|
|
171
185
|
}
|
|
172
|
-
onSubmit?.(
|
|
186
|
+
onSubmit?.(cursor.text);
|
|
173
187
|
}
|
|
174
|
-
function upOrHistoryUp() {
|
|
188
|
+
function upOrHistoryUp(cursor) {
|
|
175
189
|
if (disableCursorMovementForUpDownKeys) {
|
|
176
190
|
onHistoryUp?.();
|
|
177
191
|
return cursor;
|
|
@@ -193,7 +207,7 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
193
207
|
onHistoryUp?.();
|
|
194
208
|
return cursor;
|
|
195
209
|
}
|
|
196
|
-
function downOrHistoryDown() {
|
|
210
|
+
function downOrHistoryDown(cursor) {
|
|
197
211
|
if (disableCursorMovementForUpDownKeys) {
|
|
198
212
|
onHistoryDown?.();
|
|
199
213
|
return cursor;
|
|
@@ -218,7 +232,7 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
218
232
|
function mapKey(key) {
|
|
219
233
|
switch (true) {
|
|
220
234
|
case key.escape:
|
|
221
|
-
return () => {
|
|
235
|
+
return (cursor) => {
|
|
222
236
|
// Skip when a keybinding context (e.g. Autocomplete) owns escape.
|
|
223
237
|
// useKeybindings can't shield us via stopImmediatePropagation —
|
|
224
238
|
// BaseTextInput's useInput registers first (child effects fire
|
|
@@ -226,38 +240,40 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
226
240
|
// time the keybinding's handler stops propagation.
|
|
227
241
|
if (disableEscapeDoublePress)
|
|
228
242
|
return cursor;
|
|
229
|
-
handleEscape();
|
|
243
|
+
handleEscape(cursor);
|
|
230
244
|
// Return the current cursor unchanged - handleEscape manages state internally
|
|
231
245
|
return cursor;
|
|
232
246
|
};
|
|
233
247
|
case key.leftArrow && (key.ctrl || key.meta || key.fn):
|
|
234
|
-
return () => cursor.prevWord();
|
|
248
|
+
return (cursor) => cursor.prevWord();
|
|
235
249
|
case key.rightArrow && (key.ctrl || key.meta || key.fn):
|
|
236
|
-
return () => cursor.nextWord();
|
|
250
|
+
return (cursor) => cursor.nextWord();
|
|
237
251
|
case key.backspace:
|
|
238
252
|
return key.meta || key.ctrl
|
|
239
|
-
? killWordBefore
|
|
240
|
-
: () => cursor.deleteTokenBefore() ?? cursor.backspace();
|
|
253
|
+
? (cursor) => killWordBefore(cursor)
|
|
254
|
+
: (cursor) => cursor.deleteTokenBefore() ?? cursor.backspace();
|
|
241
255
|
case key.delete:
|
|
242
|
-
return key.meta
|
|
256
|
+
return key.meta
|
|
257
|
+
? (cursor) => killToLineEnd(cursor)
|
|
258
|
+
: (cursor) => cursor.del();
|
|
243
259
|
case key.ctrl:
|
|
244
260
|
return handleCtrl;
|
|
245
261
|
case key.home:
|
|
246
|
-
return () => cursor.startOfLine();
|
|
262
|
+
return (cursor) => cursor.startOfLine();
|
|
247
263
|
case key.end:
|
|
248
|
-
return () => cursor.endOfLine();
|
|
264
|
+
return (cursor) => cursor.endOfLine();
|
|
249
265
|
case key.pageDown:
|
|
250
266
|
// In fullscreen mode, PgUp/PgDn scroll the message viewport instead
|
|
251
267
|
// of moving the cursor — no-op here, ScrollKeybindingHandler handles it.
|
|
252
268
|
if (isFullscreenEnvEnabled()) {
|
|
253
269
|
return NOOP_HANDLER;
|
|
254
270
|
}
|
|
255
|
-
return () => cursor.endOfLine();
|
|
271
|
+
return (cursor) => cursor.endOfLine();
|
|
256
272
|
case key.pageUp:
|
|
257
273
|
if (isFullscreenEnvEnabled()) {
|
|
258
274
|
return NOOP_HANDLER;
|
|
259
275
|
}
|
|
260
|
-
return () => cursor.startOfLine();
|
|
276
|
+
return (cursor) => cursor.startOfLine();
|
|
261
277
|
case key.wheelUp:
|
|
262
278
|
case key.wheelDown:
|
|
263
279
|
// Mouse wheel events only exist when fullscreen mouse tracking is on.
|
|
@@ -266,21 +282,21 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
266
282
|
return NOOP_HANDLER;
|
|
267
283
|
case key.return:
|
|
268
284
|
// Must come before key.meta so Option+Return inserts newline
|
|
269
|
-
return () => handleEnter(key);
|
|
285
|
+
return (cursor) => handleEnter(cursor, key);
|
|
270
286
|
case key.meta:
|
|
271
287
|
return handleMeta;
|
|
272
288
|
case key.tab:
|
|
273
|
-
return () => cursor;
|
|
289
|
+
return (cursor) => cursor;
|
|
274
290
|
case key.upArrow && !key.shift:
|
|
275
|
-
return upOrHistoryUp;
|
|
291
|
+
return (cursor) => upOrHistoryUp(cursor);
|
|
276
292
|
case key.downArrow && !key.shift:
|
|
277
|
-
return downOrHistoryDown;
|
|
293
|
+
return (cursor) => downOrHistoryDown(cursor);
|
|
278
294
|
case key.leftArrow:
|
|
279
|
-
return () => cursor.left();
|
|
295
|
+
return (cursor) => cursor.left();
|
|
280
296
|
case key.rightArrow:
|
|
281
|
-
return () => cursor.right();
|
|
297
|
+
return (cursor) => cursor.right();
|
|
282
298
|
default: {
|
|
283
|
-
return function (input) {
|
|
299
|
+
return function (cursor, input) {
|
|
284
300
|
switch (true) {
|
|
285
301
|
// Home key
|
|
286
302
|
case input === '\x1b[H' || input === '\x1b[1~':
|
|
@@ -327,30 +343,34 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
327
343
|
if (filteredInput === '' && input !== '') {
|
|
328
344
|
return;
|
|
329
345
|
}
|
|
346
|
+
// Get the current synchronous state
|
|
347
|
+
const currentCursor = Cursor.fromText(lastProcessedValueRef.current, columns, lastProcessedOffsetRef.current);
|
|
330
348
|
// In SSH/tmux or slow terminals, raw DEL characters can be coalesced with
|
|
331
349
|
// printable input. Process the chunk sequentially so DEL only removes the
|
|
332
350
|
// preceding character and the rest of the packet still gets inserted.
|
|
333
351
|
if (!key.backspace && !key.delete && filteredInput.includes('\x7f')) {
|
|
334
|
-
let
|
|
352
|
+
let activeCursor = currentCursor;
|
|
335
353
|
let segmentStart = 0;
|
|
336
354
|
for (let i = 0; i < filteredInput.length; i++) {
|
|
337
355
|
if (filteredInput[i] !== '\x7f') {
|
|
338
356
|
continue;
|
|
339
357
|
}
|
|
340
358
|
if (segmentStart < i) {
|
|
341
|
-
|
|
359
|
+
activeCursor = insertPlainText(activeCursor, filteredInput.slice(segmentStart, i));
|
|
342
360
|
}
|
|
343
|
-
|
|
361
|
+
activeCursor = activeCursor.backspace();
|
|
344
362
|
segmentStart = i + 1;
|
|
345
363
|
}
|
|
346
364
|
if (segmentStart < filteredInput.length) {
|
|
347
|
-
|
|
365
|
+
activeCursor = insertPlainText(activeCursor, filteredInput.slice(segmentStart));
|
|
348
366
|
}
|
|
349
|
-
if (!
|
|
350
|
-
if (
|
|
351
|
-
|
|
367
|
+
if (!currentCursor.equals(activeCursor)) {
|
|
368
|
+
if (currentCursor.text !== activeCursor.text) {
|
|
369
|
+
lastProcessedValueRef.current = activeCursor.text;
|
|
370
|
+
onChange(activeCursor.text);
|
|
352
371
|
}
|
|
353
|
-
|
|
372
|
+
lastProcessedOffsetRef.current = activeCursor.offset;
|
|
373
|
+
setOffset(activeCursor.offset);
|
|
354
374
|
}
|
|
355
375
|
resetKillAccumulation();
|
|
356
376
|
resetYankState();
|
|
@@ -364,12 +384,14 @@ export function useTextInput({ value: originalValue, onChange, onSubmit, onExit,
|
|
|
364
384
|
if (!isYankKey(key, filteredInput)) {
|
|
365
385
|
resetYankState();
|
|
366
386
|
}
|
|
367
|
-
const nextCursor = mapKey(key)(filteredInput);
|
|
387
|
+
const nextCursor = mapKey(key)(currentCursor, filteredInput);
|
|
368
388
|
if (nextCursor) {
|
|
369
|
-
if (!
|
|
370
|
-
if (
|
|
389
|
+
if (!currentCursor.equals(nextCursor)) {
|
|
390
|
+
if (currentCursor.text !== nextCursor.text) {
|
|
391
|
+
lastProcessedValueRef.current = nextCursor.text;
|
|
371
392
|
onChange(nextCursor.text);
|
|
372
393
|
}
|
|
394
|
+
lastProcessedOffsetRef.current = nextCursor.offset;
|
|
373
395
|
setOffset(nextCursor.offset);
|
|
374
396
|
}
|
|
375
397
|
// SSH-coalesced Enter: on slow links, "o" + Enter can arrive as one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iaforged/context-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Context Code es un asistente de desarrollo para la terminal. Puede revisar tu proyecto, editar archivos, ejecutar comandos y apoyarte en tareas reales de programacion.",
|
|
5
5
|
"author": "Context AI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { createRequire } from 'module'
|
|
3
|
-
|
|
4
|
-
const require = createRequire(import.meta.url)
|
|
5
|
-
|
|
6
|
-
type AudioCaptureNapi = {
|
|
7
|
-
startRecording(
|
|
8
|
-
onData: (data: Buffer) => void,
|
|
9
|
-
onEnd: () => void,
|
|
10
|
-
): boolean
|
|
11
|
-
stopRecording(): void
|
|
12
|
-
isRecording(): boolean
|
|
13
|
-
startPlayback(sampleRate: number, channels: number): boolean
|
|
14
|
-
writePlaybackData(data: Buffer): void
|
|
15
|
-
stopPlayback(): void
|
|
16
|
-
isPlaying(): boolean
|
|
17
|
-
// TCC microphone authorization status (macOS only):
|
|
18
|
-
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
|
|
19
|
-
// Linux: always returns 3 (authorized) — no system-level microphone permission API.
|
|
20
|
-
// Windows: returns 3 (authorized) if registry key absent or allowed,
|
|
21
|
-
// 2 (denied) if microphone access is explicitly denied.
|
|
22
|
-
microphoneAuthorizationStatus?(): number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let cachedModule: AudioCaptureNapi | null = null
|
|
26
|
-
let loadAttempted = false
|
|
27
|
-
|
|
28
|
-
function loadModule(): AudioCaptureNapi | null {
|
|
29
|
-
if (loadAttempted) {
|
|
30
|
-
return cachedModule
|
|
31
|
-
}
|
|
32
|
-
loadAttempted = true
|
|
33
|
-
|
|
34
|
-
// Supported platforms: macOS (darwin), Linux, Windows (win32)
|
|
35
|
-
const platform = process.platform
|
|
36
|
-
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
|
|
41
|
-
// defined at build time in build-with-plugins.ts for native builds only — the
|
|
42
|
-
// define resolves it to the static literal "../../audio-capture.node" so bun
|
|
43
|
-
// compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
|
|
44
|
-
// direct require(env var) — bun cannot analyze require(variable) from a loop.
|
|
45
|
-
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
|
|
46
|
-
try {
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
-
cachedModule = require(
|
|
49
|
-
process.env.AUDIO_CAPTURE_NODE_PATH,
|
|
50
|
-
) as AudioCaptureNapi
|
|
51
|
-
return cachedModule
|
|
52
|
-
} catch {
|
|
53
|
-
// fall through to runtime fallbacks below
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
|
|
58
|
-
// fine here — in bundled output (node --target build) require() resolves at
|
|
59
|
-
// runtime relative to cli.js at the package root; in dev it resolves
|
|
60
|
-
// relative to this file (vendor/audio-capture-src/index.ts).
|
|
61
|
-
const platformDir = `${process.arch}-${platform}`
|
|
62
|
-
const fallbacks = [
|
|
63
|
-
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
|
|
64
|
-
`../audio-capture/${platformDir}/audio-capture.node`,
|
|
65
|
-
]
|
|
66
|
-
for (const p of fallbacks) {
|
|
67
|
-
try {
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
-
cachedModule = require(p) as AudioCaptureNapi
|
|
70
|
-
return cachedModule
|
|
71
|
-
} catch {
|
|
72
|
-
// try next
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function isNativeAudioAvailable(): boolean {
|
|
79
|
-
return loadModule() !== null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function startNativeRecording(
|
|
83
|
-
onData: (data: Buffer) => void,
|
|
84
|
-
onEnd: () => void,
|
|
85
|
-
): boolean {
|
|
86
|
-
const mod = loadModule()
|
|
87
|
-
if (!mod) {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
return mod.startRecording(onData, onEnd)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function stopNativeRecording(): void {
|
|
94
|
-
const mod = loadModule()
|
|
95
|
-
if (!mod) {
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
mod.stopRecording()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function isNativeRecordingActive(): boolean {
|
|
102
|
-
const mod = loadModule()
|
|
103
|
-
if (!mod) {
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
return mod.isRecording()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function startNativePlayback(
|
|
110
|
-
sampleRate: number,
|
|
111
|
-
channels: number,
|
|
112
|
-
): boolean {
|
|
113
|
-
const mod = loadModule()
|
|
114
|
-
if (!mod) {
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
117
|
-
return mod.startPlayback(sampleRate, channels)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function writeNativePlaybackData(data: Buffer): void {
|
|
121
|
-
const mod = loadModule()
|
|
122
|
-
if (!mod) {
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
mod.writePlaybackData(data)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function stopNativePlayback(): void {
|
|
129
|
-
const mod = loadModule()
|
|
130
|
-
if (!mod) {
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
mod.stopPlayback()
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function isNativePlaying(): boolean {
|
|
137
|
-
const mod = loadModule()
|
|
138
|
-
if (!mod) {
|
|
139
|
-
return false
|
|
140
|
-
}
|
|
141
|
-
return mod.isPlaying()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Returns the microphone authorization status.
|
|
145
|
-
// On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
|
|
146
|
-
// On Linux, always returns 3 (authorized) — no system-level mic permission API.
|
|
147
|
-
// On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
|
|
148
|
-
// Returns 0 (notDetermined) if the native module is unavailable.
|
|
149
|
-
export function microphoneAuthorizationStatus(): number {
|
|
150
|
-
const mod = loadModule()
|
|
151
|
-
if (!mod || !mod.microphoneAuthorizationStatus) {
|
|
152
|
-
return 0
|
|
153
|
-
}
|
|
154
|
-
return mod.microphoneAuthorizationStatus()
|
|
155
|
-
}
|