diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index ddeb2b2d..98d6a150 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -310,6 +310,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { stdin, setRawMode, isValidPath, + shellModeActive, }); const handleExit = useCallback( diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index 7f180dae..89930c18 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -439,6 +439,60 @@ describe('useTextBuffer', () => { }); }); + describe('Shell Mode Behavior', () => { + it('should not prepend @ to valid file paths when shellModeActive is true', () => { + const { result } = renderHook(() => + useTextBuffer({ + viewport, + isValidPath: () => true, + shellModeActive: true, + }), + ); + const filePath = '/path/to/a/valid/file.txt'; + act(() => result.current.insert(filePath)); + expect(getBufferState(result).text).toBe(filePath); // No @ prefix + }); + + it('should not prepend @ to quoted paths when shellModeActive is true', () => { + const { result } = renderHook(() => + useTextBuffer({ + viewport, + isValidPath: () => true, + shellModeActive: true, + }), + ); + const quotedFilePath = "'/path/to/a/valid/file.txt'"; + act(() => result.current.insert(quotedFilePath)); + expect(getBufferState(result).text).toBe(quotedFilePath); // No @ prefix, keeps quotes + }); + + it('should behave normally with invalid paths when shellModeActive is true', () => { + const { result } = renderHook(() => + useTextBuffer({ + viewport, + isValidPath: () => false, + shellModeActive: true, + }), + ); + const notAPath = 'this is just some text'; + act(() => result.current.insert(notAPath)); + expect(getBufferState(result).text).toBe(notAPath); + }); + + it('should behave normally with short text when shellModeActive is true', () => { + const { result } = renderHook(() => + useTextBuffer({ + viewport, + isValidPath: () => true, + shellModeActive: true, + }), + ); + const shortText = 'ls'; + act(() => result.current.insert(shortText)); + expect(getBufferState(result).text).toBe(shortText); // No @ prefix for short text + }); + }); + describe('Cursor Movement', () => { it('move: left/right should work within and across visual lines (due to wrapping)', () => { // Text: "long line1next line2" (20 chars) @@ -722,6 +776,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: 'h', }), ); @@ -731,6 +786,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: 'i', }), ); @@ -747,6 +803,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: '\r', }), ); @@ -768,6 +825,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: '\x7f', }), ); @@ -863,6 +921,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: '\x1b[D', }), ); // cursor [0,1] @@ -873,6 +932,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: false, + paste: false, sequence: '\x1b[C', }), ); // cursor [0,2] @@ -887,10 +947,11 @@ describe('useTextBuffer', () => { // Simulate pasting by calling handleInput with a string longer than 1 char act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: textWithAnsi, }), ); @@ -907,6 +968,7 @@ describe('useTextBuffer', () => { ctrl: false, meta: false, shift: true, + paste: false, sequence: '\r', }), ); // Simulates Shift+Enter in VSCode terminal @@ -1096,10 +1158,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const textWithAnsi = '\x1B[31mHello\x1B[0m'; act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: textWithAnsi, }), ); @@ -1113,10 +1176,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: textWithControlChars, }), ); @@ -1130,10 +1194,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const textWithMixed = '\u001B[4mH\u001B[0mello'; act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: textWithMixed, }), ); @@ -1147,10 +1212,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const validText = 'Hello World\nThis is a test.'; act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: validText, }), ); @@ -1164,10 +1230,11 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const pastedText = '\u001B[4mPasted\u001B[4m Text'; act(() => result.current.handleInput({ - name: undefined, + name: '', ctrl: false, meta: false, shift: false, + paste: false, sequence: pastedText, }), ); diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index 0283e059..4b7c3e79 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -73,6 +73,7 @@ interface UseTextBufferProps { setRawMode?: (mode: boolean) => void; // For external editor onChange?: (text: string) => void; // Callback for when text changes isValidPath: (path: string) => boolean; + shellModeActive?: boolean; // Whether the text buffer is in shell mode } interface UndoHistoryEntry { @@ -960,6 +961,7 @@ export function useTextBuffer({ setRawMode, onChange, isValidPath, + shellModeActive = false, }: UseTextBufferProps): TextBuffer { const initialState = useMemo((): TextBufferState => { const lines = initialText.split('\n'); @@ -1028,7 +1030,7 @@ export function useTextBuffer({ } const minLengthToInferAsDragDrop = 3; - if (ch.length >= minLengthToInferAsDragDrop) { + if (ch.length >= minLengthToInferAsDragDrop && !shellModeActive) { let potentialPath = ch; if ( potentialPath.length > 2 && @@ -1060,7 +1062,7 @@ export function useTextBuffer({ dispatch({ type: 'insert', payload: currentText }); } }, - [isValidPath], + [isValidPath, shellModeActive], ); const newline = useCallback((): void => {