one image attached

This commit is contained in:
Eason Goodale
2025-04-19 19:00:50 -07:00
parent d7d2c3f1e7
commit 2bcc15a839
4 changed files with 42 additions and 22 deletions

View File

@@ -44,6 +44,19 @@ export default function ImagePickerOverlay({
// Keep track of currently highlighted item so <Enter> can act synchronously.
const highlighted = useRef<PickerItem | null>(items[0] ?? null);
// Ensure we only invoke `onPick` / `onCancel` / `onChangeDir` once for the
// lifetime of the overlay. Depending on the environment a single <Enter>
// keypress can bubble through *three* different handlers (raw `data` event,
// `useInput`, plus `SelectInput`\'s `onSelect`). Without this guard the
// parent component would receive duplicate attachments.
const actedRef = useRef(false);
function perform(action: () => void) {
if (actedRef.current) return;
actedRef.current = true;
action();
}
// DEBUG: log all raw data when DEBUG_OVERLAY enabled (useful for tests)
const { stdin: inkStdin } = useStdin();
React.useEffect(() => {
@@ -68,19 +81,21 @@ export default function ImagePickerOverlay({
const item = highlighted.current;
if (!item) return;
if (item.value === "__UP__") {
onChangeDir(path.dirname(cwd));
} else if (item.label.endsWith("/")) {
onChangeDir(item.value);
} else {
onPick(item.value);
}
perform(() => {
if (item.value === "__UP__") {
onChangeDir(path.dirname(cwd));
} else if (item.label.endsWith("/")) {
onChangeDir(item.value);
} else {
onPick(item.value);
}
});
return;
}
// ESC (\u001B) or Backspace (\x7f)
if (str === "\u001b" || str === "\x7f") {
onCancel();
perform(onCancel);
}
}
if (inkStdin) inkStdin.on('data', onData);
@@ -99,9 +114,9 @@ export default function ImagePickerOverlay({
// eslint-disable-next-line no-console
console.log('[overlay] root useInput', JSON.stringify(input), key.return);
}
if (key.escape || key.backspace || input === "\u007f") {
if (key.escape || key.backspace || input === "\u007f") {
if (process.env.DEBUG_OVERLAY) console.log('[overlay] cancel');
onCancel();
perform(onCancel);
} else if (key.return) {
// Act on the currently highlighted item synchronously so tests that
// simulate a bare "\r" keypress without triggering SelectInputs
@@ -117,13 +132,15 @@ export default function ImagePickerOverlay({
console.log('[overlay] return on', item.label, item.value);
}
if (item.value === "__UP__") {
onChangeDir(path.dirname(cwd));
} else if (item.label.endsWith("/")) {
onChangeDir(item.value);
} else {
onPick(item.value);
}
perform(() => {
if (item.value === "__UP__") {
onChangeDir(path.dirname(cwd));
} else if (item.label.endsWith("/")) {
onChangeDir(item.value);
} else {
onPick(item.value);
}
});
}
},
{ isActive: true },

View File

@@ -468,8 +468,10 @@ export default function TerminalChatInput({
// Remove trailing '@' sentinel from draft input
setInput((prev) => (prev.endsWith("@") ? prev.slice(0, -1) : prev));
// Track attachment separately
setAttachedImages((prev) => [...prev, filePath]);
// Track attachment separately, but avoid duplicates
setAttachedImages((prev) =>
prev.includes(filePath) ? prev : [...prev, filePath],
);
if (process.env.DEBUG_TCI) {
// eslint-disable-next-line no-console

View File

@@ -71,7 +71,8 @@ describe("Chat input attachment preview", () => {
await type(stdin, "@", flush);
await type(stdin, "\r", flush); // choose first
expect(lastFrameStripped()).toContain("foo.png");
const frame1 = lastFrameStripped();
expect(frame1.match(/foo\.png/g)?.length ?? 0).toBe(1);
await type(stdin, "\x15", flush); // Ctrl+U

View File

@@ -69,8 +69,8 @@ describe("Backspace deletes attached image", () => {
await type(stdin, "@", flush);
console.log('AFTER @', lastFrameStripped());
await type(stdin, "\r", flush);
console.log('FRAME1', lastFrameStripped());
expect(lastFrameStripped()).toContain("bar.png");
const frame1 = lastFrameStripped();
expect(frame1.match(/bar\.png/g)?.length ?? 0).toBe(1);
await type(stdin, "\x7f", flush);