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.
- package/README.md +2 -2
- package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
- package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
- package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
- package/dist/components/MultilineInput/BlockMarker.js +83 -0
- package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
- package/dist/components/MultilineInput/BlockRegistry.js +236 -0
- package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
- package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
- package/dist/components/MultilineInput/ImageTypes.js +1 -2
- package/dist/components/MultilineInput/ImageValidator.js +1 -1
- package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
- package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
- package/dist/components/MultilineInput/TextBuffer.js +91 -161
- package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
- package/dist/components/MultilineInput/TextRenderer.js +7 -7
- package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
- package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
- package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
- package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
- package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
- package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
- package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
- package/dist/components/MultilineInput/index.d.ts +6 -6
- package/dist/components/MultilineInput/index.js +56 -13
- package/dist/components/MultilineInput/types.d.ts +0 -20
- package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
- package/dist/components/MultilineInput/useTextInput.js +79 -76
- package/package.json +1 -1
- package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
- package/dist/components/MultilineInput/Placeholder.js +0 -200
- package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
- package/dist/examples/examples/basic.js +0 -9
- package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
- package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
- package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
- package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
- package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
- package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
- package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
- package/dist/examples/src/components/MultilineInput/index.js +0 -82
- package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
- package/dist/examples/src/components/MultilineInput/types.js +0 -1
- package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
- package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
- package/dist/examples/src/hello.test.d.ts +0 -1
- package/dist/examples/src/hello.test.js +0 -13
- package/dist/examples/src/index.d.ts +0 -2
- package/dist/examples/src/index.js +0 -2
- /package/dist/components/MultilineInput/{__tests__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
- /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
- /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 {
|
|
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 =
|
|
8
|
-
const sentinel2 =
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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,
|
|
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 };
|
|
62
|
-
const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor,
|
|
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 };
|
|
68
|
-
const { container } = render(_jsx(TextRenderer, { buffer: buffer, cursor: cursor,
|
|
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 {
|
|
5
|
+
import { createBlockMarker } from '../BlockMarker.js';
|
|
6
6
|
describe('MultilineInputCore with images', () => {
|
|
7
|
-
const sentinel1 =
|
|
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 {
|
|
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'
|
|
16
|
+
const imgRef = makeImageRef('img1');
|
|
17
17
|
act(() => {
|
|
18
18
|
result.current.insertImage(imgRef);
|
|
19
19
|
});
|
|
20
|
-
|
|
21
|
-
expect(result.current.
|
|
22
|
-
|
|
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'
|
|
51
|
+
const imgRef = makeImageRef('img1');
|
|
40
52
|
act(() => { result.current.insertImage(imgRef); });
|
|
41
|
-
const
|
|
42
|
-
|
|
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'
|
|
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'
|
|
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'
|
|
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('
|
|
101
|
+
it('setText clears block entries', () => {
|
|
94
102
|
const { result } = renderHook(() => useTextInput());
|
|
95
|
-
const img1 = makeImageRef('img1'
|
|
96
|
-
const img2 = makeImageRef('img2', 2);
|
|
103
|
+
const img1 = makeImageRef('img1');
|
|
97
104
|
act(() => { result.current.insertImage(img1); });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
24
|
-
* Default: (
|
|
23
|
+
* Receives the display number (1-based) and should return the display string.
|
|
24
|
+
* Default: (n) => `[Paste text #${n}]`
|
|
25
25
|
*/
|
|
26
|
-
formatPastePlaceholder?: (
|
|
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
|
|
55
|
-
* Default: (
|
|
54
|
+
* Receives the display number (1-based) and should return the display string.
|
|
55
|
+
* Default: (n) => `[Paste text #${n}]`
|
|
56
56
|
*/
|
|
57
|
-
formatPastePlaceholder?: (
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
27
|
+
blockState: BlockState;
|
|
35
28
|
insertImage: (imageRef: ImageRef) => void;
|
|
36
29
|
images: ImageRef[];
|
|
37
30
|
getImages: () => ImageRef[];
|