linted and working

This commit is contained in:
Eason Goodale
2025-04-20 01:26:40 -07:00
parent 1e0a7cc313
commit b1cef74d8c
12 changed files with 121 additions and 65 deletions

View File

@@ -5,4 +5,4 @@
// without the “.tsx” extension when running under ts-node/esm in the test
// environment.
export { default } from "./image-picker-overlay.tsx";
export { default } from "./image-picker-overlay.tsx";

View File

@@ -1,7 +1,6 @@
/* eslint-disable import/order */
import path from "node:path";
import { Box, Text, useInput, useStdin } from "ink";
import SelectInput from "../select-input/select-input.js";
@@ -37,7 +36,7 @@ export default function ImagePickerOverlay({
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log('[overlay] mount, items:', items.map((i) => i.label).join(','));
console.log("[overlay] mount, items:", items.map((i) => i.label).join(","));
}
// Keep track of currently highlighted item so <Enter> can act synchronously.
@@ -64,7 +63,7 @@ export default function ImagePickerOverlay({
function onData(data: Buffer) {
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log('[overlay] stdin data', JSON.stringify(data.toString()));
console.log("[overlay] stdin data", JSON.stringify(data.toString()));
}
// ink-testing-library pipes mocked input through `stdin.emit("data", …)`
@@ -117,48 +116,48 @@ export default function ImagePickerOverlay({
// in the spec).
useInput(
(input, key) => {
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log(
"[overlay] root useInput",
JSON.stringify(input),
key.return,
);
}
if (key.escape || key.backspace || input === "\u007f") {
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log("[overlay] cancel");
}
perform(onCancel);
} else if (key.return) {
// Act on the currently highlighted item synchronously so tests that
// simulate a bare "\r" keypress without triggering SelectInputs
// onSelect callback still work. This mirrors <SelectInput>s own
// behaviour but executing the logic here avoids having to depend on
// that implementation detail.
const item = highlighted.current;
if (!item) {
return;
console.log(
"[overlay] root useInput",
JSON.stringify(input),
key.return,
);
}
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log('[overlay] return on', item.label, item.value);
}
perform(() => {
if (item.value === "__UP__") {
onChangeDir(path.dirname(cwd));
} else if (item.label.endsWith("/")) {
onChangeDir(item.value);
} else {
onPick(item.value);
if (key.escape || key.backspace || input === "\u007f") {
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log("[overlay] cancel");
}
});
}
perform(onCancel);
} else if (key.return) {
// Act on the currently highlighted item synchronously so tests that
// simulate a bare "\r" keypress without triggering SelectInputs
// onSelect callback still work. This mirrors <SelectInput>s own
// behaviour but executing the logic here avoids having to depend on
// that implementation detail.
const item = highlighted.current;
if (!item) {
return;
}
if (process.env["DEBUG_OVERLAY"]) {
// eslint-disable-next-line no-console
console.log("[overlay] return on", item.label, 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

@@ -84,7 +84,11 @@ export default function TerminalChatInput({
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('[TCI] render stage', { input, pickerCwd, attachedCount: attachedImages.length });
console.log("[TCI] render stage", {
input,
pickerCwd,
attachedCount: attachedImages.length,
});
}
// Open picker when user finished typing '@'
React.useEffect(() => {
@@ -115,7 +119,7 @@ export default function TerminalChatInput({
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('[TCI] raw stdin', JSON.stringify(str));
console.log("[TCI] raw stdin", JSON.stringify(str));
}
if (str === "@" && pickerCwd == null) {
@@ -162,7 +166,7 @@ export default function TerminalChatInput({
(_input, _key) => {
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('[TCI] useInput raw', JSON.stringify(_input), _key);
console.log("[TCI] useInput raw", JSON.stringify(_input), _key);
}
// When image picker overlay is open delegate all keystrokes to it.
@@ -172,7 +176,7 @@ export default function TerminalChatInput({
if (!confirmationPrompt && !loading) {
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('useInput received', JSON.stringify(_input));
console.log("useInput received", JSON.stringify(_input));
}
// Open image picker when user types '@' and picker not already open.
@@ -227,7 +231,10 @@ export default function TerminalChatInput({
}
// Backspace on empty draft removes last attached image
if ((_key.backspace || _input === "\u007f") && attachedImages.length > 0) {
if (
(_key.backspace || _input === "\u007f") &&
attachedImages.length > 0
) {
if (input.length === 0) {
setAttachedImages((prev) => prev.slice(0, -1));
}
@@ -450,7 +457,11 @@ export default function TerminalChatInput({
text:
missingImages.length === 1
? `Warning: image "${missingImages[0]}" not found and was not attached.`
: `Warning: ${missingImages.length} images were not found and were skipped: ${missingImages.join(", ")}`,
: `Warning: ${
missingImages.length
} images were not found and were skipped: ${missingImages.join(
", ",
)}`,
},
],
},
@@ -520,7 +531,12 @@ export default function TerminalChatInput({
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('[TCI] attached image added', filePath, 'total', attachedImages.length + 1);
console.log(
"[TCI] attached image added",
filePath,
"total",
attachedImages.length + 1,
);
}
setPickerCwd(null);
}}
@@ -535,7 +551,7 @@ export default function TerminalChatInput({
}
if (process.env["DEBUG_TCI"]) {
// eslint-disable-next-line no-console
console.log('[TCI] render AttachmentPreview', attachedImages);
console.log("[TCI] render AttachmentPreview", attachedImages);
}
return (
<Box flexDirection="column" paddingX={1} marginBottom={1}>

View File

@@ -119,7 +119,12 @@ function TerminalChatResponseMessage({
: c.type === "input_text"
? c.text
: c.type === "input_image"
? imageFilenameByDataUrl.get(c.image_url as string) || "<Image>"
? (() => {
const label = imageFilenameByDataUrl.get(
c.image_url as string,
);
return label ? `<Image path="${label}">` : "<Image>";
})()
: c.type === "input_file"
? c.filename
: "", // unknown content type

View File

@@ -9,6 +9,8 @@ export interface TerminalInlineImageProps {
}
// During tests or when terminal does not support images, fallback to alt.
export default function TerminalInlineImage({ alt = "[image]" }: TerminalInlineImageProps): React.ReactElement {
export default function TerminalInlineImage({
alt = "[image]",
}: TerminalInlineImageProps): React.ReactElement {
return <Text>{alt}</Text>;
}

View File

@@ -1,4 +1,4 @@
// Reexport TypeScript implementation so regular `.js` import paths used by
// the existing testsuite continue to resolve correctly at runtime.
export * from "./image-picker-utils.ts";
export * from "./image-picker-utils.ts";

View File

@@ -28,7 +28,10 @@ export function getDirectoryItems(
try {
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
if (entry.isDirectory()) {
dirs.push({ label: entry.name + "/", value: path.join(cwd, entry.name) });
dirs.push({
label: entry.name + "/",
value: path.join(cwd, entry.name),
});
} else if (entry.isFile() && isImage(entry.name)) {
files.push({ label: entry.name, value: path.join(cwd, entry.name) });
}

View File

@@ -68,7 +68,7 @@ describe("Chat input attachment preview", () => {
process.chdir(TMP);
const { stdin, flush, lastFrameStripped, cleanup } = renderTui(
React.createElement(TerminalChatInput, props())
React.createElement(TerminalChatInput, props()),
);
await flush();

View File

@@ -65,7 +65,7 @@ describe("Backspace deletes attached image", () => {
process.chdir(TMP);
const { stdin, flush, lastFrameStripped, cleanup } = renderTui(
React.createElement(TerminalChatInput, props())
React.createElement(TerminalChatInput, props()),
);
await flush();

View File

@@ -47,7 +47,7 @@ describe("Image picker overlay", () => {
onPick: () => {},
onCancel: () => {},
onChangeDir,
})
}),
);
await flush();
@@ -65,7 +65,7 @@ describe("Image picker overlay", () => {
onPick,
onCancel: () => {},
onChangeDir: () => {},
})
}),
);
await flush();
await type(stdin, "\r", flush);

View File

@@ -5,7 +5,10 @@ import { renderTui } from "./ui-test-helpers.js";
import TerminalInlineImage from "../src/components/chat/terminal-inline-image.js";
import TerminalChatResponseItem from "../src/components/chat/terminal-chat-response-item.js";
import { imageFilenameByDataUrl, createInputItem } from "../src/utils/input-utils.js";
import {
imageFilenameByDataUrl,
createInputItem,
} from "../src/utils/input-utils.js";
// ---------------------------------------------------------------------------
// Helpers
@@ -14,11 +17,10 @@ import { imageFilenameByDataUrl, createInputItem } from "../src/utils/input-util
import path from "node:path";
import fs from "node:fs";
describe("TerminalInlineImage fallback", () => {
it("renders alt text in test env", () => {
const { lastFrameStripped } = renderTui(
<TerminalInlineImage src={Buffer.from("abc")} alt="placeholder" />
<TerminalInlineImage src={Buffer.from("abc")} alt="placeholder" />,
);
expect(lastFrameStripped()).toContain("placeholder");
});
@@ -42,9 +44,9 @@ describe("TerminalChatResponseItem image label", () => {
it("shows filename", () => {
const msg = fakeImageMessage("sample.png");
const { lastFrameStripped } = renderTui(
<TerminalChatResponseItem item={msg as any} />
<TerminalChatResponseItem item={msg as any} />,
);
expect(lastFrameStripped()).toContain("sample.png");
expect(lastFrameStripped()).toContain('<Image path="sample.png">');
});
});

View File

@@ -1,4 +1,33 @@
import { defineConfig } from 'vite';
import { defineConfig } from "vite";
// Provide a stub Vite config in the CLI package to avoid resolving a parent-level vite.config.js
export default defineConfig({});
/**
* Vite configuration used by the Codex CLI package. The build process itself
* doesnt rely on Vites bundling features we only ship this file so that
* Vitest can pick it up when executing the unittest suite. The only custom
* logic we currently inject is a *test* configuration block that registers a
* small setup script executed in each worker thread before any test files are
* loaded. That script polyfills `process.chdir()` which is disallowed inside
* Node.js workers as of v22 and would otherwise throw when some tests attempt
* to change the working directory.
*/
export default defineConfig({
test: {
// Execute tests inside worker threads but force Vitest to spawn *only one*
// worker. This keeps the environment isolation that some components
// depend on while avoiding a `tinypool` recursion bug that occasionally
// triggers when multiple workers are used.
pool: "threads",
poolOptions: {
threads: {
minThreads: 1,
maxThreads: 1,
},
},
/**
* Register the setup file. We use a relative path so that Vitest resolves
* it against the project root irrespective of where the CLI is executed.
*/
setupFiles: ["./tests/test-setup.js"],
},
});