@blockrun/franklin 3.5.1 → 3.6.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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mouse event support for Ink terminal UI.
3
+ * Enables SGR extended mouse tracking (DECSET 1000+1006) and parses events from stdin.
4
+ * Lightweight — only handles clicks, not drag/hover/selection.
5
+ */
6
+ import { EventEmitter } from 'node:events';
7
+ // ─── Terminal escape sequences ────────────────────────────────────────────
8
+ const ENABLE_MOUSE = '\x1b[?1000h' + // Normal mouse tracking (clicks + wheel)
9
+ '\x1b[?1006h'; // SGR extended format (readable coordinates)
10
+ const DISABLE_MOUSE = '\x1b[?1006l' +
11
+ '\x1b[?1000l';
12
+ // SGR mouse event format: ESC [ < button ; col ; row M (press) or m (release)
13
+ const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
14
+ // ─── Mouse Manager ───────────────────────────────────────────────────────
15
+ class MouseManager extends EventEmitter {
16
+ enabled = false;
17
+ stdinListener = null;
18
+ /**
19
+ * Enable mouse tracking. Call once on app startup.
20
+ * Returns cleanup function to call on unmount.
21
+ */
22
+ enable() {
23
+ if (this.enabled)
24
+ return () => { };
25
+ this.enabled = true;
26
+ // Write enable sequences
27
+ process.stdout.write(ENABLE_MOUSE);
28
+ // Listen on stdin for mouse sequences
29
+ // We use 'data' event at a higher priority than Ink's handler.
30
+ // Mouse sequences that we parse are still passed to Ink (we can't consume them),
31
+ // but Ink will ignore unrecognized escape sequences.
32
+ this.stdinListener = (data) => {
33
+ const str = data.toString('utf-8');
34
+ let match;
35
+ SGR_MOUSE_RE.lastIndex = 0;
36
+ while ((match = SGR_MOUSE_RE.exec(str)) !== null) {
37
+ const btnCode = parseInt(match[1], 10);
38
+ const col = parseInt(match[2], 10) - 1; // 1-indexed → 0-indexed
39
+ const row = parseInt(match[3], 10) - 1;
40
+ const isPress = match[4] === 'M';
41
+ // Decode button
42
+ const baseBtn = btnCode & 0x03;
43
+ const isWheel = (btnCode & 0x40) !== 0;
44
+ let button;
45
+ if (isWheel) {
46
+ button = baseBtn === 0 ? 'wheel-up' : 'wheel-down';
47
+ }
48
+ else {
49
+ button = baseBtn === 0 ? 'left' : baseBtn === 1 ? 'middle' : 'right';
50
+ }
51
+ const event = {
52
+ button,
53
+ action: isPress ? 'press' : 'release',
54
+ col,
55
+ row,
56
+ };
57
+ this.emit('mouse', event);
58
+ // Emit convenience events
59
+ if (button === 'left' && isPress) {
60
+ this.emit('click', event);
61
+ }
62
+ }
63
+ };
64
+ process.stdin.on('data', this.stdinListener);
65
+ return () => this.disable();
66
+ }
67
+ /**
68
+ * Disable mouse tracking and clean up.
69
+ */
70
+ disable() {
71
+ if (!this.enabled)
72
+ return;
73
+ this.enabled = false;
74
+ if (this.stdinListener) {
75
+ process.stdin.removeListener('data', this.stdinListener);
76
+ this.stdinListener = null;
77
+ }
78
+ // Best-effort: disable mouse tracking
79
+ try {
80
+ process.stdout.write(DISABLE_MOUSE);
81
+ }
82
+ catch {
83
+ // Ignore write errors during cleanup (stdout may be closed)
84
+ }
85
+ }
86
+ isEnabled() { return this.enabled; }
87
+ }
88
+ /** Singleton mouse manager. */
89
+ export const mouse = new MouseManager();
@@ -74,12 +74,29 @@ class MarkdownRenderer {
74
74
  }
75
75
  else {
76
76
  this.inCodeBlock = true;
77
- this.codeBlockLang = line.slice(3).trim();
78
- return chalk.dim('```' + this.codeBlockLang);
77
+ const lang = line.slice(3).trim().split(/\s/)[0].toLowerCase();
78
+ this.codeBlockLang = lang;
79
+ const LANG_LABELS = {
80
+ ts: 'TypeScript', typescript: 'TypeScript', js: 'JavaScript', javascript: 'JavaScript',
81
+ py: 'Python', python: 'Python', rs: 'Rust', rust: 'Rust', go: 'Go',
82
+ sh: 'Shell', bash: 'Shell', zsh: 'Shell', json: 'JSON', yaml: 'YAML',
83
+ sql: 'SQL', html: 'HTML', css: 'CSS', diff: 'Diff',
84
+ tsx: 'TSX', jsx: 'JSX',
85
+ };
86
+ const label = LANG_LABELS[lang] || (lang ? lang.toUpperCase() : '');
87
+ return chalk.dim('```') + (label ? chalk.dim.italic(` ${label}`) : '');
79
88
  }
80
89
  }
81
- // Inside code block — render dim
90
+ // Inside code block — diff highlighting + cyan
82
91
  if (this.inCodeBlock) {
92
+ if (this.codeBlockLang === 'diff') {
93
+ if (line.startsWith('+'))
94
+ return chalk.green(line);
95
+ if (line.startsWith('-'))
96
+ return chalk.red(line);
97
+ if (line.startsWith('@@'))
98
+ return chalk.cyan(line);
99
+ }
83
100
  return chalk.cyan(line);
84
101
  }
85
102
  // Headers
@@ -232,38 +249,38 @@ export class TerminalUI {
232
249
  const capName = cap?.name || 'unknown';
233
250
  const elapsed = cap ? Date.now() - cap.startTime : 0;
234
251
  this.activeCapabilities.delete(event.id);
235
- const timeStr = elapsed > 100 ? chalk.dim(` ${elapsed}ms`) : '';
252
+ const elapsedFmt = elapsed >= 1000
253
+ ? `${(elapsed / 1000).toFixed(1)}s`
254
+ : `${elapsed}ms`;
255
+ const timeStr = elapsed > 100 ? chalk.dim(` ${elapsedFmt}`) : '';
236
256
  if (event.result.isError) {
237
- console.error(chalk.red(` ✗ ${capName}`) +
238
- timeStr +
239
- chalk.red(`: ${truncateOutput(event.result.output, 200)}`));
257
+ console.error(chalk.red(` ✗ `) + chalk.bold(capName) +
258
+ timeStr);
259
+ // Show error preview lines
260
+ const errLines = event.result.output.split('\n').filter(Boolean).slice(0, 3);
261
+ for (const line of errLines) {
262
+ console.error(chalk.red(` ⎿ ${line.slice(0, 120)}`));
263
+ }
240
264
  }
241
265
  else {
242
- // Show diff-like output for Edit tool
243
266
  const output = event.result.output;
244
- if (capName === 'Edit' && output.includes('replacement')) {
245
- console.error(chalk.green(` ${capName}`) + timeStr + chalk.dim(` — ${output}`));
246
- }
247
- else if (capName === 'Write') {
248
- console.error(chalk.green(` ✓ ${capName}`) + timeStr + chalk.dim(` — ${output}`));
249
- }
250
- else if (capName === 'Bash') {
251
- // Show command output preview
252
- const preview = truncateOutput(output, 120);
253
- console.error(chalk.green(` ✓ ${capName}`) + timeStr);
254
- if (preview && preview !== '(no output)') {
255
- const lines = output.split('\n').slice(0, 5);
256
- for (const line of lines) {
257
- console.error(chalk.dim(` │ ${line.slice(0, 100)}`));
258
- }
259
- if (output.split('\n').length > 5) {
260
- console.error(chalk.dim(` │ ... (${output.split('\n').length - 5} more lines)`));
261
- }
267
+ const icon = chalk.green('');
268
+ console.error(` ${icon} ${chalk.bold(capName)}${timeStr}`);
269
+ if (capName === 'Bash') {
270
+ // Show last 5 lines of command output
271
+ const outLines = output.split('\n').filter(Boolean);
272
+ const show = outLines.slice(-5);
273
+ for (const line of show) {
274
+ console.error(chalk.dim(` ⎿ ${line.slice(0, 120)}`));
275
+ }
276
+ if (outLines.length > 5) {
277
+ console.error(chalk.dim(` ⎿ ... ${outLines.length - 5} more lines`));
262
278
  }
263
279
  }
264
- else {
280
+ else if (output.trim()) {
281
+ // Other tools: show first line as preview
265
282
  const preview = truncateOutput(output, 120);
266
- console.error(chalk.green(` ${capName}`) + timeStr + chalk.dim(` — ${preview}`));
283
+ console.error(chalk.dim(` ${preview}`));
267
284
  }
268
285
  }
269
286
  break;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vim-style text input for Franklin's Ink UI.
3
+ * Supports normal/insert mode, motions, operators, counts.
4
+ *
5
+ * Normal mode: h/l/w/b/e/0/$ for movement, i/a/A/I to enter insert, x/dd/dw/D for delete
6
+ * Insert mode: standard text entry, Esc to return to normal mode
7
+ */
8
+ export type VimMode = 'insert' | 'normal';
9
+ interface VimInputProps {
10
+ value: string;
11
+ onChange: (value: string) => void;
12
+ onSubmit: (value: string) => void;
13
+ placeholder?: string;
14
+ focus?: boolean;
15
+ showMode?: boolean;
16
+ onModeChange?: (mode: VimMode) => void;
17
+ }
18
+ export default function VimInput({ value, onChange, onSubmit, placeholder, focus, showMode, onModeChange, }: VimInputProps): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,439 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Vim-style text input for Franklin's Ink UI.
4
+ * Supports normal/insert mode, motions, operators, counts.
5
+ *
6
+ * Normal mode: h/l/w/b/e/0/$ for movement, i/a/A/I to enter insert, x/dd/dw/D for delete
7
+ * Insert mode: standard text entry, Esc to return to normal mode
8
+ */
9
+ import { useState, useCallback, useRef } from 'react';
10
+ import { Box, Text, useInput } from 'ink';
11
+ /**
12
+ * Find the start of the next word (Vim 'w' motion).
13
+ */
14
+ function nextWord(text, pos) {
15
+ let i = pos;
16
+ // Skip current word chars
17
+ while (i < text.length && /\w/.test(text[i]))
18
+ i++;
19
+ // Skip non-word non-space
20
+ while (i < text.length && /[^\w\s]/.test(text[i]))
21
+ i++;
22
+ // Skip whitespace
23
+ while (i < text.length && /\s/.test(text[i]))
24
+ i++;
25
+ return Math.min(i, text.length);
26
+ }
27
+ /**
28
+ * Find the start of the previous word (Vim 'b' motion).
29
+ */
30
+ function prevWord(text, pos) {
31
+ let i = pos - 1;
32
+ // Skip whitespace backwards
33
+ while (i > 0 && /\s/.test(text[i]))
34
+ i--;
35
+ // Skip non-word non-space backwards
36
+ if (i > 0 && /[^\w\s]/.test(text[i])) {
37
+ while (i > 0 && /[^\w\s]/.test(text[i - 1]))
38
+ i--;
39
+ return i;
40
+ }
41
+ // Skip word chars backwards
42
+ while (i > 0 && /\w/.test(text[i - 1]))
43
+ i--;
44
+ return Math.max(0, i);
45
+ }
46
+ /**
47
+ * Find the end of the current word (Vim 'e' motion).
48
+ */
49
+ function endWord(text, pos) {
50
+ let i = pos + 1;
51
+ // Skip whitespace
52
+ while (i < text.length && /\s/.test(text[i]))
53
+ i++;
54
+ // Move to end of word
55
+ while (i < text.length - 1 && /\w/.test(text[i + 1]))
56
+ i++;
57
+ return Math.min(i, text.length - 1);
58
+ }
59
+ export default function VimInput({ value, onChange, onSubmit, placeholder = '', focus = true, showMode = true, onModeChange, }) {
60
+ const [mode, setMode] = useState('insert');
61
+ const [cursor, setCursor] = useState(value.length);
62
+ const [cmdBuf, setCmdBuf] = useState(''); // accumulated command buffer (for counts + operators)
63
+ const [yankBuf, setYankBuf] = useState(''); // internal clipboard
64
+ const [undoStack, setUndoStack] = useState([]); // simple undo
65
+ const lastValueRef = useRef(value);
66
+ // Keep cursor in bounds when value changes externally
67
+ const clampedCursor = Math.min(cursor, mode === 'normal' ? Math.max(0, value.length - 1) : value.length);
68
+ const switchMode = useCallback((newMode) => {
69
+ setMode(newMode);
70
+ setCmdBuf('');
71
+ onModeChange?.(newMode);
72
+ }, [onModeChange]);
73
+ const saveUndo = useCallback(() => {
74
+ setUndoStack(prev => [...prev.slice(-20), value]);
75
+ }, [value]);
76
+ const updateValue = useCallback((newVal, newCursor) => {
77
+ onChange(newVal);
78
+ setCursor(Math.max(0, Math.min(newCursor, mode === 'normal' ? Math.max(0, newVal.length - 1) : newVal.length)));
79
+ lastValueRef.current = newVal;
80
+ }, [onChange, mode]);
81
+ useInput((input, key) => {
82
+ if (!focus)
83
+ return;
84
+ // Submit on Enter in any mode
85
+ if (key.return) {
86
+ if (mode === 'normal')
87
+ switchMode('insert');
88
+ onSubmit(value);
89
+ return;
90
+ }
91
+ // ── INSERT MODE ──
92
+ if (mode === 'insert') {
93
+ // Escape → normal mode
94
+ if (key.escape) {
95
+ const newCursor = Math.max(0, clampedCursor - 1);
96
+ setCursor(newCursor);
97
+ switchMode('normal');
98
+ return;
99
+ }
100
+ // Backspace
101
+ if (key.backspace) {
102
+ if (clampedCursor > 0) {
103
+ saveUndo();
104
+ updateValue(value.slice(0, clampedCursor - 1) + value.slice(clampedCursor), clampedCursor - 1);
105
+ }
106
+ return;
107
+ }
108
+ // Delete
109
+ if (key.delete) {
110
+ if (clampedCursor < value.length) {
111
+ saveUndo();
112
+ updateValue(value.slice(0, clampedCursor) + value.slice(clampedCursor + 1), clampedCursor);
113
+ }
114
+ return;
115
+ }
116
+ // Arrow keys in insert mode
117
+ if (key.leftArrow) {
118
+ setCursor(Math.max(0, clampedCursor - 1));
119
+ return;
120
+ }
121
+ if (key.rightArrow) {
122
+ setCursor(Math.min(value.length, clampedCursor + 1));
123
+ return;
124
+ }
125
+ if (key.upArrow || key.downArrow)
126
+ return; // let parent handle history
127
+ // Ctrl+A: beginning of line
128
+ if (key.ctrl && input === 'a') {
129
+ setCursor(0);
130
+ return;
131
+ }
132
+ // Ctrl+E: end of line
133
+ if (key.ctrl && input === 'e') {
134
+ setCursor(value.length);
135
+ return;
136
+ }
137
+ // Ctrl+W: delete word backward
138
+ if (key.ctrl && input === 'w') {
139
+ const wp = prevWord(value, clampedCursor);
140
+ saveUndo();
141
+ updateValue(value.slice(0, wp) + value.slice(clampedCursor), wp);
142
+ return;
143
+ }
144
+ // Ctrl+U: delete to beginning
145
+ if (key.ctrl && input === 'u') {
146
+ saveUndo();
147
+ setYankBuf(value.slice(0, clampedCursor));
148
+ updateValue(value.slice(clampedCursor), 0);
149
+ return;
150
+ }
151
+ // Ctrl+K: delete to end
152
+ if (key.ctrl && input === 'k') {
153
+ saveUndo();
154
+ setYankBuf(value.slice(clampedCursor));
155
+ updateValue(value.slice(0, clampedCursor), clampedCursor);
156
+ return;
157
+ }
158
+ // Skip control chars and tab
159
+ if (key.ctrl || key.meta || key.tab)
160
+ return;
161
+ // Regular character input
162
+ if (input) {
163
+ saveUndo();
164
+ updateValue(value.slice(0, clampedCursor) + input + value.slice(clampedCursor), clampedCursor + input.length);
165
+ }
166
+ return;
167
+ }
168
+ // ── NORMAL MODE ──
169
+ if (key.escape) {
170
+ setCmdBuf('');
171
+ return;
172
+ }
173
+ // Arrow keys work in normal mode too
174
+ if (key.leftArrow) {
175
+ setCursor(Math.max(0, clampedCursor - 1));
176
+ return;
177
+ }
178
+ if (key.rightArrow) {
179
+ setCursor(Math.min(Math.max(0, value.length - 1), clampedCursor + 1));
180
+ return;
181
+ }
182
+ if (key.upArrow || key.downArrow)
183
+ return; // let parent handle
184
+ // Backspace in normal mode = left
185
+ if (key.backspace) {
186
+ setCursor(Math.max(0, clampedCursor - 1));
187
+ return;
188
+ }
189
+ // Build command buffer
190
+ const fullCmd = cmdBuf + input;
191
+ // Parse count prefix
192
+ const countMatch = fullCmd.match(/^(\d+)(.*)/);
193
+ const count = countMatch ? parseInt(countMatch[1]) : 1;
194
+ const cmd = countMatch ? countMatch[2] : fullCmd;
195
+ // ── Mode switches ──
196
+ if (cmd === 'i') {
197
+ switchMode('insert');
198
+ return;
199
+ }
200
+ if (cmd === 'a') {
201
+ setCursor(Math.min(value.length, clampedCursor + 1));
202
+ switchMode('insert');
203
+ return;
204
+ }
205
+ if (cmd === 'I') {
206
+ setCursor(0);
207
+ switchMode('insert');
208
+ return;
209
+ }
210
+ if (cmd === 'A') {
211
+ setCursor(value.length);
212
+ switchMode('insert');
213
+ return;
214
+ }
215
+ if (cmd === 's') { // substitute: delete char and enter insert
216
+ saveUndo();
217
+ updateValue(value.slice(0, clampedCursor) + value.slice(clampedCursor + 1), clampedCursor);
218
+ switchMode('insert');
219
+ return;
220
+ }
221
+ if (cmd === 'S' || cmd === 'cc') { // substitute line
222
+ saveUndo();
223
+ setYankBuf(value);
224
+ updateValue('', 0);
225
+ switchMode('insert');
226
+ return;
227
+ }
228
+ // ── Navigation ──
229
+ if (cmd === 'h') {
230
+ setCursor(Math.max(0, clampedCursor - count));
231
+ setCmdBuf('');
232
+ return;
233
+ }
234
+ if (cmd === 'l') {
235
+ setCursor(Math.min(Math.max(0, value.length - 1), clampedCursor + count));
236
+ setCmdBuf('');
237
+ return;
238
+ }
239
+ if (cmd === 'w') {
240
+ let pos = clampedCursor;
241
+ for (let n = 0; n < count; n++)
242
+ pos = nextWord(value, pos);
243
+ setCursor(Math.min(pos, Math.max(0, value.length - 1)));
244
+ setCmdBuf('');
245
+ return;
246
+ }
247
+ if (cmd === 'b') {
248
+ let pos = clampedCursor;
249
+ for (let n = 0; n < count; n++)
250
+ pos = prevWord(value, pos);
251
+ setCursor(pos);
252
+ setCmdBuf('');
253
+ return;
254
+ }
255
+ if (cmd === 'e') {
256
+ let pos = clampedCursor;
257
+ for (let n = 0; n < count; n++)
258
+ pos = endWord(value, pos);
259
+ setCursor(Math.min(pos, Math.max(0, value.length - 1)));
260
+ setCmdBuf('');
261
+ return;
262
+ }
263
+ if (cmd === '0') {
264
+ // Only if not building a count (e.g., "10w" — "0" is part of count)
265
+ if (!countMatch || countMatch[2] === '0') {
266
+ setCursor(0);
267
+ setCmdBuf('');
268
+ return;
269
+ }
270
+ }
271
+ if (cmd === '$') {
272
+ setCursor(Math.max(0, value.length - 1));
273
+ setCmdBuf('');
274
+ return;
275
+ }
276
+ if (cmd === '^') {
277
+ const firstNonSpace = value.search(/\S/);
278
+ setCursor(firstNonSpace >= 0 ? firstNonSpace : 0);
279
+ setCmdBuf('');
280
+ return;
281
+ }
282
+ // ── Editing ──
283
+ if (cmd === 'x') {
284
+ if (value.length > 0) {
285
+ saveUndo();
286
+ const deleted = value.slice(clampedCursor, clampedCursor + count);
287
+ setYankBuf(deleted);
288
+ const newVal = value.slice(0, clampedCursor) + value.slice(clampedCursor + count);
289
+ updateValue(newVal, Math.min(clampedCursor, Math.max(0, newVal.length - 1)));
290
+ }
291
+ setCmdBuf('');
292
+ return;
293
+ }
294
+ if (cmd === 'X') {
295
+ if (clampedCursor > 0) {
296
+ saveUndo();
297
+ const start = Math.max(0, clampedCursor - count);
298
+ setYankBuf(value.slice(start, clampedCursor));
299
+ updateValue(value.slice(0, start) + value.slice(clampedCursor), start);
300
+ }
301
+ setCmdBuf('');
302
+ return;
303
+ }
304
+ if (cmd === 'dd') {
305
+ saveUndo();
306
+ setYankBuf(value);
307
+ updateValue('', 0);
308
+ setCmdBuf('');
309
+ return;
310
+ }
311
+ if (cmd === 'D') {
312
+ saveUndo();
313
+ setYankBuf(value.slice(clampedCursor));
314
+ updateValue(value.slice(0, clampedCursor), Math.max(0, clampedCursor - 1));
315
+ setCmdBuf('');
316
+ return;
317
+ }
318
+ if (cmd === 'C') { // change to end of line
319
+ saveUndo();
320
+ setYankBuf(value.slice(clampedCursor));
321
+ updateValue(value.slice(0, clampedCursor), clampedCursor);
322
+ switchMode('insert');
323
+ return;
324
+ }
325
+ if (cmd === 'dw') {
326
+ saveUndo();
327
+ let pos = clampedCursor;
328
+ for (let n = 0; n < count; n++)
329
+ pos = nextWord(value, pos);
330
+ setYankBuf(value.slice(clampedCursor, pos));
331
+ updateValue(value.slice(0, clampedCursor) + value.slice(pos), clampedCursor);
332
+ setCmdBuf('');
333
+ return;
334
+ }
335
+ if (cmd === 'db') {
336
+ saveUndo();
337
+ let pos = clampedCursor;
338
+ for (let n = 0; n < count; n++)
339
+ pos = prevWord(value, pos);
340
+ setYankBuf(value.slice(pos, clampedCursor));
341
+ updateValue(value.slice(0, pos) + value.slice(clampedCursor), pos);
342
+ setCmdBuf('');
343
+ return;
344
+ }
345
+ if (cmd === 'cw') { // change word
346
+ saveUndo();
347
+ let pos = clampedCursor;
348
+ for (let n = 0; n < count; n++)
349
+ pos = nextWord(value, pos);
350
+ setYankBuf(value.slice(clampedCursor, pos));
351
+ updateValue(value.slice(0, clampedCursor) + value.slice(pos), clampedCursor);
352
+ switchMode('insert');
353
+ return;
354
+ }
355
+ if (cmd === 'cb') { // change back
356
+ saveUndo();
357
+ let pos = clampedCursor;
358
+ for (let n = 0; n < count; n++)
359
+ pos = prevWord(value, pos);
360
+ setYankBuf(value.slice(pos, clampedCursor));
361
+ updateValue(value.slice(0, pos) + value.slice(clampedCursor), pos);
362
+ switchMode('insert');
363
+ return;
364
+ }
365
+ // ── Yank & Paste ──
366
+ if (cmd === 'yy') {
367
+ setYankBuf(value);
368
+ setCmdBuf('');
369
+ return;
370
+ }
371
+ if (cmd === 'yw') {
372
+ const pos = nextWord(value, clampedCursor);
373
+ setYankBuf(value.slice(clampedCursor, pos));
374
+ setCmdBuf('');
375
+ return;
376
+ }
377
+ if (cmd === 'p') {
378
+ if (yankBuf) {
379
+ saveUndo();
380
+ const insertAt = Math.min(clampedCursor + 1, value.length);
381
+ updateValue(value.slice(0, insertAt) + yankBuf + value.slice(insertAt), insertAt + yankBuf.length - 1);
382
+ }
383
+ setCmdBuf('');
384
+ return;
385
+ }
386
+ if (cmd === 'P') {
387
+ if (yankBuf) {
388
+ saveUndo();
389
+ updateValue(value.slice(0, clampedCursor) + yankBuf + value.slice(clampedCursor), clampedCursor + yankBuf.length - 1);
390
+ }
391
+ setCmdBuf('');
392
+ return;
393
+ }
394
+ // ── Undo ──
395
+ if (cmd === 'u') {
396
+ if (undoStack.length > 0) {
397
+ const prev = undoStack[undoStack.length - 1];
398
+ setUndoStack(s => s.slice(0, -1));
399
+ onChange(prev);
400
+ setCursor(Math.min(clampedCursor, Math.max(0, prev.length - 1)));
401
+ lastValueRef.current = prev;
402
+ }
403
+ setCmdBuf('');
404
+ return;
405
+ }
406
+ // ── Accumulate partial commands ──
407
+ // Valid prefixes for multi-key commands
408
+ if (/^\d+$/.test(fullCmd)) {
409
+ setCmdBuf(fullCmd);
410
+ return;
411
+ } // count accumulating
412
+ if (fullCmd === 'd' || fullCmd === 'c' || fullCmd === 'y') {
413
+ setCmdBuf(fullCmd);
414
+ return;
415
+ } // operator pending
416
+ if (/^\d+[dcy]$/.test(fullCmd)) {
417
+ setCmdBuf(fullCmd);
418
+ return;
419
+ } // count + operator
420
+ // Unknown command — reset
421
+ setCmdBuf('');
422
+ }, { isActive: focus });
423
+ // ── Render ──
424
+ const displayValue = value || (mode === 'insert' ? placeholder : '');
425
+ const isEmpty = !value;
426
+ // Build the displayed text with cursor
427
+ let rendered;
428
+ if (isEmpty && mode === 'insert') {
429
+ rendered = (_jsx(Text, { dimColor: true, children: placeholder }));
430
+ }
431
+ else {
432
+ // Split text around cursor for highlighting
433
+ const before = displayValue.slice(0, clampedCursor);
434
+ const atCursor = displayValue[clampedCursor] || ' ';
435
+ const after = displayValue.slice(clampedCursor + 1);
436
+ rendered = (_jsxs(Text, { children: [before, _jsx(Text, { inverse: focus, bold: mode === 'normal', children: atCursor }), after] }));
437
+ }
438
+ return (_jsxs(Box, { children: [showMode && mode === 'normal' && (_jsx(Text, { color: "yellow", bold: true, children: "[N] " })), rendered, cmdBuf && mode === 'normal' && (_jsxs(Text, { dimColor: true, children: [" ", cmdBuf] }))] }));
439
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.5.1",
3
+ "version": "3.6.2",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {