ink-prompt 0.2.2 → 0.2.4

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 (61) hide show
  1. package/README.md +2 -2
  2. package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
  3. package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
  4. package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
  5. package/dist/components/MultilineInput/BlockMarker.js +83 -0
  6. package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
  7. package/dist/components/MultilineInput/BlockRegistry.js +236 -0
  8. package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
  9. package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
  10. package/dist/components/MultilineInput/ImageTypes.js +1 -2
  11. package/dist/components/MultilineInput/ImageValidator.js +1 -1
  12. package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
  13. package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
  14. package/dist/components/MultilineInput/TextBuffer.js +91 -161
  15. package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
  16. package/dist/components/MultilineInput/TextRenderer.js +7 -7
  17. package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
  18. package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
  19. package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
  20. package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
  21. package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
  22. package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
  23. package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
  24. package/dist/components/MultilineInput/index.d.ts +6 -6
  25. package/dist/components/MultilineInput/index.js +56 -13
  26. package/dist/components/MultilineInput/types.d.ts +0 -20
  27. package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
  28. package/dist/components/MultilineInput/useTextInput.js +79 -76
  29. package/package.json +1 -1
  30. package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
  31. package/dist/components/MultilineInput/Placeholder.js +0 -200
  32. package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
  33. package/dist/examples/examples/basic.js +0 -9
  34. package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
  35. package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
  36. package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
  37. package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
  38. package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
  39. package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
  40. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
  41. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
  42. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
  43. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
  44. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
  45. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
  46. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
  47. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
  48. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
  49. package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
  50. package/dist/examples/src/components/MultilineInput/index.js +0 -82
  51. package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
  52. package/dist/examples/src/components/MultilineInput/types.js +0 -1
  53. package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
  54. package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
  55. package/dist/examples/src/hello.test.d.ts +0 -1
  56. package/dist/examples/src/hello.test.js +0 -13
  57. package/dist/examples/src/index.d.ts +0 -2
  58. package/dist/examples/src/index.js +0 -2
  59. /package/dist/components/MultilineInput/{__tests__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
  60. /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
  61. /package/dist/{examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
@@ -2,14 +2,28 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { describe, it, expect } from 'vitest';
3
3
  import { render } from '@testing-library/react';
4
4
  import { TextRenderer, wrapLines } from '../TextRenderer.js';
5
- import { createSentinel } from '../ImageSentinel.js';
5
+ import { createBlockMarker } from '../BlockMarker.js';
6
+ function makeBlockState(entries) {
7
+ const map = new Map();
8
+ for (const e of entries) {
9
+ map.set(e.id, {
10
+ kind: 'image',
11
+ id: e.id,
12
+ displayNumber: e.displayNumber,
13
+ data: '',
14
+ mimeType: 'image/png',
15
+ byteSize: 100,
16
+ });
17
+ }
18
+ return { entries: map, nextPasteNumber: 1, nextImageNumber: 3 };
19
+ }
6
20
  describe('TextRenderer with images', () => {
7
- const sentinel1 = createSentinel('img1', 1);
8
- const sentinel2 = createSentinel('img2', 2);
9
- const images = {
10
- img1: { id: 'img1', data: '', mimeType: 'image/png', byteSize: 100, displayNumber: 1 },
11
- img2: { id: 'img2', data: '', mimeType: 'image/png', byteSize: 100, displayNumber: 2 },
12
- };
21
+ const sentinel1 = createBlockMarker('i', 'img1', 1);
22
+ const sentinel2 = createBlockMarker('i', 'img2', 2);
23
+ const blockState = makeBlockState([
24
+ { id: 'img1', displayNumber: 1 },
25
+ { id: 'img2', displayNumber: 2 },
26
+ ]);
13
27
  describe('wrapLines', () => {
14
28
  it('renders normal text unchanged when no sentinels', () => {
15
29
  const buffer = { lines: ['hello'] };
@@ -46,26 +60,26 @@ describe('TextRenderer with images', () => {
46
60
  it('renders sentinel as dimmed placeholder text', () => {
47
61
  const buffer = { lines: [sentinel1] };
48
62
  const cursor = { line: 0, column: sentinel1.length };
49
- const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, images: images, showCursor: false }));
63
+ const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, blockState: blockState, showCursor: false }));
50
64
  expect(container.textContent).toContain('[Pasted Image #1]');
51
65
  });
52
66
  it('renders multiple sentinels', () => {
53
67
  const buffer = { lines: [`${sentinel1} ${sentinel2}`] };
54
68
  const cursor = { line: 0, column: 0 };
55
- const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, images: images, showCursor: false }));
69
+ const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, blockState: blockState, showCursor: false }));
56
70
  expect(container.textContent).toContain('[Pasted Image #1]');
57
71
  expect(container.textContent).toContain('[Pasted Image #2]');
58
72
  });
59
73
  it('renders cursor before sentinel', () => {
60
74
  const buffer = { lines: [sentinel1] };
61
- const cursor = { line: 0, column: 0 }; // before sentinel
62
- const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, images: images, showCursor: true }));
75
+ const cursor = { line: 0, column: 0 };
76
+ const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, blockState: blockState, showCursor: true }));
63
77
  expect(container.textContent).toContain('[Pasted Image #1]');
64
78
  });
65
79
  it('renders cursor after sentinel', () => {
66
80
  const buffer = { lines: [sentinel1] };
67
- const cursor = { line: 0, column: sentinel1.length }; // after sentinel
68
- const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, images: images, showCursor: true }));
81
+ const cursor = { line: 0, column: sentinel1.length };
82
+ const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor, blockState: blockState, showCursor: true }));
69
83
  expect(container.textContent).toContain('[Pasted Image #1]');
70
84
  });
71
85
  });
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { describe, it, expect } from 'vitest';
3
3
  import { render } from '@testing-library/react';
4
4
  import { MultilineInputCore } from '../index.js';
5
- import { createSentinel } from '../ImageSentinel.js';
5
+ import { createBlockMarker } from '../BlockMarker.js';
6
6
  describe('MultilineInputCore with images', () => {
7
- const sentinel1 = createSentinel('img1', 1);
7
+ const sentinel1 = createBlockMarker('i', 'img1', 1);
8
8
  const img1 = {
9
9
  id: 'img1',
10
10
  data: 'base64data',
@@ -26,8 +26,6 @@ describe('MultilineInputCore with images', () => {
26
26
  });
27
27
  describe('submit with images', () => {
28
28
  it('submit passes images when provided via textInput', () => {
29
- // We test that the component renders with images correctly
30
- // The actual submit behavior with images is tested in useTextInput_images tests
31
29
  const { container } = render(_jsx(MultilineInputCore, { value: sentinel1, images: [img1], showCursor: false }));
32
30
  expect(container.textContent).toContain('[Pasted Image #1]');
33
31
  });
@@ -1,25 +1,26 @@
1
1
  import { renderHook, act } from '@testing-library/react';
2
2
  import { describe, it, expect } from 'vitest';
3
3
  import { useTextInput } from '../useTextInput.js';
4
- import { createSentinel } from '../ImageSentinel.js';
4
+ import { createBlockMarker } from '../BlockMarker.js';
5
5
  describe('useTextInput with images', () => {
6
6
  const makeImageRef = (id, displayNumber) => ({
7
7
  id,
8
8
  data: 'base64data',
9
9
  mimeType: 'image/png',
10
10
  byteSize: 100,
11
- displayNumber,
11
+ displayNumber: displayNumber || 1,
12
12
  });
13
13
  describe('insertImage', () => {
14
14
  it('adds sentinel to text and image to map', () => {
15
15
  const { result } = renderHook(() => useTextInput());
16
- const imgRef = makeImageRef('img1', 1);
16
+ const imgRef = makeImageRef('img1');
17
17
  act(() => {
18
18
  result.current.insertImage(imgRef);
19
19
  });
20
- const sentinel = createSentinel('img1', 1);
21
- expect(result.current.value).toBe(sentinel);
22
- expect(result.current.getImages()).toEqual([imgRef]);
20
+ expect(result.current.getImages()).toHaveLength(1);
21
+ expect(result.current.getImages()[0].id).toBe('img1');
22
+ const line = result.current.buffer.lines[0];
23
+ expect(line).toContain(createBlockMarker('i', 'img1', 1));
23
24
  });
24
25
  it('assigns correct displayNumber', () => {
25
26
  const { result } = renderHook(() => useTextInput());
@@ -32,15 +33,25 @@ describe('useTextInput with images', () => {
32
33
  expect(images[0].displayNumber).toBe(1);
33
34
  expect(images[1].displayNumber).toBe(2);
34
35
  });
36
+ it('continues numbering after controlled images are synced', () => {
37
+ const { result } = renderHook(() => useTextInput());
38
+ act(() => {
39
+ result.current.setImages([makeImageRef('img5', 5)]);
40
+ });
41
+ act(() => {
42
+ result.current.insertImage(makeImageRef('img6', 6));
43
+ });
44
+ expect(result.current.getImages().map((img) => img.displayNumber)).toEqual([5, 6]);
45
+ expect(result.current.buffer.lines[0]).toContain(createBlockMarker('i', 'img6', 6));
46
+ });
35
47
  });
36
48
  describe('deleteChar removes image and sentinel', () => {
37
49
  it('removes sentinel and image from map on backspace', () => {
38
50
  const { result } = renderHook(() => useTextInput());
39
- const imgRef = makeImageRef('img1', 1);
51
+ const imgRef = makeImageRef('img1');
40
52
  act(() => { result.current.insertImage(imgRef); });
41
- const sentinel = createSentinel('img1', 1);
42
- // Cursor is at end of sentinel
43
- expect(result.current.cursor.column).toBe(sentinel.length);
53
+ const markerLen = createBlockMarker('i', 'img1', 1).length;
54
+ expect(result.current.cursor.column).toBe(markerLen);
44
55
  act(() => {
45
56
  result.current.delete();
46
57
  });
@@ -51,17 +62,14 @@ describe('useTextInput with images', () => {
51
62
  describe('deleteForward removes image and sentinel', () => {
52
63
  it('removes sentinel and image from map on delete key', () => {
53
64
  const { result } = renderHook(() => useTextInput());
54
- const imgRef = makeImageRef('img1', 1);
65
+ const imgRef = makeImageRef('img1');
55
66
  act(() => { result.current.insertImage(imgRef); });
56
- // Move cursor to before the sentinel
57
67
  act(() => { result.current.moveCursor('left'); });
58
68
  act(() => { result.current.moveCursor('left'); });
59
- // Cursor should now be before the sentinel
60
69
  expect(result.current.cursor.column).toBe(0);
61
70
  act(() => {
62
71
  result.current.deleteForward();
63
72
  });
64
- // Should have deleted the whole sentinel
65
73
  expect(result.current.value).toBe('');
66
74
  expect(result.current.getImages()).toEqual([]);
67
75
  });
@@ -69,7 +77,7 @@ describe('useTextInput with images', () => {
69
77
  describe('undo/redo with images', () => {
70
78
  it('undo restores deleted image', () => {
71
79
  const { result } = renderHook(() => useTextInput({ undoDebounceMs: 0 }));
72
- const imgRef = makeImageRef('img1', 1);
80
+ const imgRef = makeImageRef('img1');
73
81
  act(() => { result.current.insertImage(imgRef); });
74
82
  expect(result.current.getImages()).toHaveLength(1);
75
83
  act(() => { result.current.delete(); });
@@ -80,7 +88,7 @@ describe('useTextInput with images', () => {
80
88
  });
81
89
  it('redo re-applies image insert', () => {
82
90
  const { result } = renderHook(() => useTextInput({ undoDebounceMs: 0 }));
83
- const imgRef = makeImageRef('img1', 1);
91
+ const imgRef = makeImageRef('img1');
84
92
  act(() => { result.current.insertImage(imgRef); });
85
93
  act(() => { result.current.undo(); });
86
94
  expect(result.current.getImages()).toHaveLength(0);
@@ -90,21 +98,14 @@ describe('useTextInput with images', () => {
90
98
  });
91
99
  });
92
100
  describe('setText cleans up orphaned sentinels', () => {
93
- it('removes images whose sentinels are no longer in text', () => {
101
+ it('setText clears block entries', () => {
94
102
  const { result } = renderHook(() => useTextInput());
95
- const img1 = makeImageRef('img1', 1);
96
- const img2 = makeImageRef('img2', 2);
103
+ const img1 = makeImageRef('img1');
97
104
  act(() => { result.current.insertImage(img1); });
98
- act(() => { result.current.insertImage(img2); });
99
- expect(result.current.getImages()).toHaveLength(2);
100
- // setText to a value that only contains img1's sentinel
101
- const sentinel1 = createSentinel('img1', 1);
102
- act(() => {
103
- result.current.setText(sentinel1 + ' extra');
104
- });
105
- const images = result.current.getImages();
106
- expect(images).toHaveLength(1);
107
- expect(images[0].id).toBe('img1');
105
+ expect(result.current.getImages()).toHaveLength(1);
106
+ act(() => { result.current.setText('new text'); });
107
+ expect(result.current.getImages()).toHaveLength(0);
108
+ expect(result.current.value).toBe('new text');
108
109
  });
109
110
  });
110
111
  });
@@ -20,10 +20,10 @@ export interface MultilineInputProps {
20
20
  pasteThreshold?: number;
21
21
  /**
22
22
  * Custom formatter for the placeholder display text.
23
- * Receives the placeholder ID and should return the display string.
24
- * Default: (id) => `[Paste text #${id}]`
23
+ * Receives the display number (1-based) and should return the display string.
24
+ * Default: (n) => `[Paste text #${n}]`
25
25
  */
26
- formatPastePlaceholder?: (id: number) => string;
26
+ formatPastePlaceholder?: (displayNumber: number) => string;
27
27
  images?: ImageRef[];
28
28
  onImagesChange?: (images: ImageRef[]) => void;
29
29
  onPasteError?: (reason: PasteErrorReason) => void;
@@ -51,10 +51,10 @@ export interface MultilineInputCoreProps {
51
51
  pasteThreshold?: number;
52
52
  /**
53
53
  * Custom formatter for the placeholder display text.
54
- * Receives the placeholder ID and should return the display string.
55
- * Default: (id) => `[Paste text #${id}]`
54
+ * Receives the display number (1-based) and should return the display string.
55
+ * Default: (n) => `[Paste text #${n}]`
56
56
  */
57
- formatPastePlaceholder?: (id: number) => string;
57
+ formatPastePlaceholder?: (displayNumber: number) => string;
58
58
  images?: ImageRef[];
59
59
  onImagesChange?: (images: ImageRef[]) => void;
60
60
  }
@@ -7,15 +7,6 @@ import { handleKey } from './KeyHandler.js';
7
7
  import { TextRenderer } from './TextRenderer.js';
8
8
  import { useClipboardPaste } from './useClipboardPaste.js';
9
9
  import { log } from '../../utils/logger.js';
10
- function imagesToRecord(images) {
11
- if (!images || images.length === 0)
12
- return {};
13
- const record = {};
14
- for (const img of images) {
15
- record[img.id] = img;
16
- }
17
- return record;
18
- }
19
10
  export const MultilineInputCore = ({ value, onChange, placeholder, showCursor = true, width = 80, onCursorChange, cursorOverride, undoDebounceMs, pasteThreshold, formatPastePlaceholder, images, onImagesChange, }) => {
20
11
  const textInput = useTextInput({ initialValue: value ?? '', undoDebounceMs, pasteThreshold, formatPastePlaceholder });
21
12
  const isSyncingFromProps = useRef(false);
@@ -67,24 +58,70 @@ export const MultilineInputCore = ({ value, onChange, placeholder, showCursor =
67
58
  if (showPlaceholder) {
68
59
  return _jsx("div", { style: { opacity: 0.5 }, children: placeholder });
69
60
  }
70
- return (_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: width, showCursor: showCursor, placeholderState: textInput.placeholderState, images: imagesToRecord(textInput.images) }));
61
+ return (_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: width, showCursor: showCursor, blockState: textInput.blockState }));
71
62
  };
72
63
  export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCursor = true, width, isActive = true, onCursorChange, cursorOverride, onBoundaryArrow, undoDebounceMs, pasteThreshold, formatPastePlaceholder, images, onImagesChange, onPasteError, enableImagePaste = false, maxImageSizeBytes, maxImageCount, acceptedMimeTypes, }) => {
73
64
  const terminalWidth = useTerminalWidth(width);
74
65
  const { stdin } = useStdin();
75
66
  const lastRawInput = useRef('');
67
+ const pasteActive = useRef(false);
68
+ const pasteBuffer = useRef('');
69
+ const suppressNextInput = useRef(false);
70
+ const textInput = useTextInput({ initialValue: value ?? '', width: terminalWidth, undoDebounceMs, pasteThreshold, formatPastePlaceholder });
71
+ const textInputRef = useRef(textInput);
72
+ useEffect(() => {
73
+ textInputRef.current = textInput;
74
+ }, [textInput]);
76
75
  useEffect(() => {
77
76
  if (!stdin || !isActive)
78
77
  return;
78
+ const PASTE_START = '\x1b[200~';
79
+ const PASTE_END = '\x1b[201~';
80
+ // Enable bracketed paste mode so terminal-mediated pastes (e.g. Cmd+V on
81
+ // macOS) are wrapped in \x1b[200~ ... \x1b[201~ markers we can detect.
82
+ process.stdout.write('\x1b[?2004h');
79
83
  const handleData = (data) => {
80
- lastRawInput.current = data.toString();
84
+ const str = data.toString();
85
+ lastRawInput.current = str;
86
+ const hasStart = str.includes(PASTE_START);
87
+ const hasEnd = str.includes(PASTE_END);
88
+ if (!pasteActive.current && !hasStart)
89
+ return;
90
+ let remaining = str;
91
+ if (!pasteActive.current && hasStart) {
92
+ pasteActive.current = true;
93
+ pasteBuffer.current = '';
94
+ remaining = remaining.slice(remaining.indexOf(PASTE_START) + PASTE_START.length);
95
+ }
96
+ // Suppress the useInput dispatch for any chunk that participates in a paste.
97
+ suppressNextInput.current = true;
98
+ if (pasteActive.current) {
99
+ const endIdx = remaining.indexOf(PASTE_END);
100
+ if (endIdx === -1) {
101
+ pasteBuffer.current += remaining;
102
+ }
103
+ else {
104
+ pasteBuffer.current += remaining.slice(0, endIdx);
105
+ const pasted = pasteBuffer.current;
106
+ pasteActive.current = false;
107
+ pasteBuffer.current = '';
108
+ // Defer to next tick so the insert isn't tangled with React's
109
+ // current render/dispatch cycle for this stdin chunk.
110
+ queueMicrotask(() => {
111
+ textInputRef.current?.insert(pasted);
112
+ });
113
+ }
114
+ }
81
115
  };
82
116
  stdin.on('data', handleData);
83
117
  return () => {
118
+ process.stdout.write('\x1b[?2004l');
84
119
  stdin.off('data', handleData);
120
+ pasteActive.current = false;
121
+ pasteBuffer.current = '';
122
+ suppressNextInput.current = false;
85
123
  };
86
124
  }, [stdin, isActive]);
87
- const textInput = useTextInput({ initialValue: value ?? '', width: terminalWidth, undoDebounceMs, pasteThreshold, formatPastePlaceholder });
88
125
  const { isPasting, paste: clipboardPaste } = useClipboardPaste({
89
126
  enableImagePaste,
90
127
  maxImageSizeBytes,
@@ -168,6 +205,12 @@ export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCur
168
205
  paste: handlePaste,
169
206
  };
170
207
  useInput((input, key) => {
208
+ if (suppressNextInput.current) {
209
+ // This stdin chunk is part of a bracketed paste — already handled by the
210
+ // raw 'data' listener. Don't dispatch as keystrokes.
211
+ suppressNextInput.current = false;
212
+ return;
213
+ }
171
214
  log(`[USEINPUT] input="${input.replace(/[\x00-\x1F\x7F-￿]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}" key=${JSON.stringify(key)} rawLen=${lastRawInput.current?.length || 0}`);
172
215
  handleKey(key, input, textInput.buffer, actions, textInput.cursor, lastRawInput.current, terminalWidth);
173
216
  }, { isActive });
@@ -176,5 +219,5 @@ export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCur
176
219
  if (showPlaceholder && !isPasting) {
177
220
  return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: placeholder }) }));
178
221
  }
179
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: terminalWidth, showCursor: showCursor, placeholderState: textInput.placeholderState, images: imagesToRecord(textInput.images) }), isPasting && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Reading clipboard..." }) }))] }));
222
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: terminalWidth, showCursor: showCursor, blockState: textInput.blockState }), isPasting && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Reading clipboard..." }) }))] }));
180
223
  };
@@ -33,26 +33,6 @@ export interface WrapResult {
33
33
  /** Column in that visual row where cursor appears */
34
34
  cursorVisualCol: number;
35
35
  }
36
- /**
37
- * Information about a paste placeholder: its identifier, original text, and display label.
38
- */
39
- export interface PlaceholderInfo {
40
- /** Unique placeholder identifier */
41
- id: number;
42
- /** The original pasted text (may contain newlines) */
43
- originalText: string;
44
- /** The display label shown in place of the original text (e.g., "[Paste text #1]") */
45
- displayText: string;
46
- }
47
- /**
48
- * Registry state for paste placeholders.
49
- */
50
- export interface PlaceholderState {
51
- /** Map of placeholder ID to info */
52
- placeholders: Map<number, PlaceholderInfo>;
53
- /** Next auto-incrementing ID */
54
- nextId: number;
55
- }
56
36
  /**
57
37
  * Keyboard key state (mirrors Ink's Key interface)
58
38
  * Defined locally to avoid ESM/CJS import issues with Ink
@@ -1,20 +1,13 @@
1
- import type { Buffer, Cursor, Direction, PlaceholderState } from './types.js';
1
+ import type { Buffer, Cursor, Direction } from './types.js';
2
2
  import type { ImageRef } from './ImageTypes.js';
3
+ import type { BlockState } from './BlockTypes.js';
3
4
  export interface UseTextInputProps {
4
5
  initialValue?: string;
5
6
  width?: number;
6
7
  historyLimit?: number;
7
8
  undoDebounceMs?: number;
8
- /**
9
- * When set, pasted text exceeding this character count is replaced
10
- * with a placeholder for cleaner display.
11
- */
12
9
  pasteThreshold?: number;
13
- /**
14
- * Custom formatter for placeholder display text.
15
- * Default: (id) => `[Paste text #${id}]`
16
- */
17
- formatPastePlaceholder?: (id: number) => string;
10
+ formatPastePlaceholder?: (displayNumber: number) => string;
18
11
  }
19
12
  export interface UseTextInputResult {
20
13
  value: string;
@@ -31,7 +24,7 @@ export interface UseTextInputResult {
31
24
  setText: (text: string) => void;
32
25
  cursorOffset: number;
33
26
  setCursorOffset: (offset: number) => void;
34
- placeholderState: PlaceholderState;
27
+ blockState: BlockState;
35
28
  insertImage: (imageRef: ImageRef) => void;
36
29
  images: ImageRef[];
37
30
  getImages: () => ImageRef[];