diff --git a/.gitignore b/.gitignore index ecf5e96e57..af71278311 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ coverage/ .doc-typecheck-*/ .vscode/ .DS_Store +.idea +mise.toml diff --git a/examples/coding-agent/src/stdio-chat.ts b/examples/coding-agent/src/stdio-chat.ts index c8ee8265ce..237e976878 100644 --- a/examples/coding-agent/src/stdio-chat.ts +++ b/examples/coding-agent/src/stdio-chat.ts @@ -53,7 +53,11 @@ export function apply(ctx: Context) { }) ctx.effect(() => { - const reader = createInterface({ input: process.stdin }) + const reader = createInterface({ + input: process.stdin, + output: process.stdout, + terminal: process.stdin.isTTY && process.stdout.isTTY, + }) // Piped-input exit, once stdin reaches EOF: // - If no line ever submitted work (empty stdin, blank-only lines), exit // immediately — no turn will ever start, so there is nothing to wait diff --git a/examples/coding-agent/tests/stdio-chat.spec.ts b/examples/coding-agent/tests/stdio-chat.spec.ts new file mode 100644 index 0000000000..3cb128701f --- /dev/null +++ b/examples/coding-agent/tests/stdio-chat.spec.ts @@ -0,0 +1,33 @@ +import { EventEmitter } from 'node:events' +import type { Context } from 'cordis' +import { describe, expect, test, vi } from 'vitest' + +const createInterface = vi.hoisted(() => vi.fn(() => { + const reader = new EventEmitter() as EventEmitter & { close(): void } + reader.close = vi.fn() + return reader +})) + +vi.mock('node:readline', () => ({ createInterface })) + +function fakeContext(): Context { + return { + agents: { get: vi.fn() }, + on: vi.fn(() => vi.fn()), + effect: vi.fn((callback: () => () => void) => callback()), + } as unknown as Context +} + +describe('coding-agent stdio chat', () => { + test('creates a terminal readline interface so TTY editing keys work', async () => { + const { apply } = await import('../src/stdio-chat.ts') + + apply(fakeContext()) + + expect(createInterface).toHaveBeenCalledWith({ + input: process.stdin, + output: process.stdout, + terminal: process.stdin.isTTY && process.stdout.isTTY, + }) + }) +}) diff --git a/examples/echo-agent/src/stdio-chat.ts b/examples/echo-agent/src/stdio-chat.ts index 4160d43c80..e2242441d4 100644 --- a/examples/echo-agent/src/stdio-chat.ts +++ b/examples/echo-agent/src/stdio-chat.ts @@ -35,7 +35,11 @@ export function apply(ctx: Context) { }) ctx.effect(() => { - const reader = createInterface({ input: process.stdin }) + const reader = createInterface({ + input: process.stdin, + output: process.stdout, + terminal: process.stdin.isTTY && process.stdout.isTTY, + }) reader.on('line', (line) => { const text = line.trim() if (!text) return diff --git a/examples/echo-agent/tests/stdio-chat.spec.ts b/examples/echo-agent/tests/stdio-chat.spec.ts new file mode 100644 index 0000000000..4437e55c53 --- /dev/null +++ b/examples/echo-agent/tests/stdio-chat.spec.ts @@ -0,0 +1,33 @@ +import { EventEmitter } from 'node:events' +import type { Context } from 'cordis' +import { describe, expect, test, vi } from 'vitest' + +const createInterface = vi.hoisted(() => vi.fn(() => { + const reader = new EventEmitter() as EventEmitter & { close(): void } + reader.close = vi.fn() + return reader +})) + +vi.mock('node:readline', () => ({ createInterface })) + +function fakeContext(): Context { + return { + agents: { get: vi.fn() }, + on: vi.fn(() => vi.fn()), + effect: vi.fn((callback: () => () => void) => callback()), + } as unknown as Context +} + +describe('echo-agent stdio chat', () => { + test('creates a terminal readline interface so TTY editing keys work', async () => { + const { apply } = await import('../src/stdio-chat.ts') + + apply(fakeContext()) + + expect(createInterface).toHaveBeenCalledWith({ + input: process.stdin, + output: process.stdout, + terminal: process.stdin.isTTY && process.stdout.isTTY, + }) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index e8eb5e204e..0878477fb4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ // instead applies the one root map to every importer. plugins: [tsconfigPaths({ projects: ['./tsconfig.test.json'] })], test: { - include: ['packages/*/tests/**/*.spec.ts'], + include: ['packages/*/tests/**/*.spec.ts', 'examples/*/tests/**/*.spec.ts'], coverage: { provider: 'v8', // Coverage measures OUR runtime source. Types-only files carry no