diff --git a/crates/goose-acp/src/bin/server.rs b/crates/goose-acp/src/bin/server.rs index c3c6a9aed5..f59f634a2a 100644 --- a/crates/goose-acp/src/bin/server.rs +++ b/crates/goose-acp/src/bin/server.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Parser; +use goose::builtin_extension::register_builtin_extensions; use goose::config::paths::Paths; use goose_acp::server_factory::{AcpServer, AcpServerFactoryConfig}; use std::net::SocketAddr; @@ -29,6 +30,8 @@ async fn main() -> Result<()> { .with(tracing_subscriber::fmt::layer().with_target(true)) .init(); + register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone()); + let cli = Cli::parse(); let builtins = if cli.builtins.is_empty() { diff --git a/ui/acp/generate-schema.ts b/ui/acp/generate-schema.ts index ee63176c2d..909542f842 100644 --- a/ui/acp/generate-schema.ts +++ b/ui/acp/generate-schema.ts @@ -28,7 +28,6 @@ main().catch((err) => { async function main() { const schemaSrc = await fs.readFile(SCHEMA_PATH, "utf8"); const jsonSchema = JSON.parse( - // Convert JSON Schema $defs refs to OpenAPI component refs schemaSrc.replaceAll("#/$defs/", "#/components/schemas/"), ); @@ -63,7 +62,6 @@ async function main() { async function postProcessTypes() { const tsPath = resolve(OUTPUT_DIR, "types.gen.ts"); let src = await fs.readFile(tsPath, "utf8"); - // Remove the ClientOptions type block injected by @hey-api (not part of our schema) src = src.replace(/\nexport type ClientOptions =[\s\S]*?^};\n/m, "\n"); await fs.writeFile(tsPath, src); } @@ -72,17 +70,14 @@ async function postProcessIndex(meta: { methods: unknown[] }) { const indexPath = resolve(OUTPUT_DIR, "index.ts"); let src = await fs.readFile(indexPath, "utf8"); - // Strip ClientOptions from re-exports src = src.replace(/,?\s*ClientOptions\s*,?/g, (match) => { if (match.startsWith(",") && match.endsWith(",")) return ","; if (match.startsWith(",")) return ""; return ""; }); - // Fix bare relative imports to use .js extensions (required by nodenext consumers) src = fixRelativeImports(src); - // Append method constants const methodConstants = await prettier.format( ` export const GOOSE_EXT_METHODS = ${JSON.stringify(meta.methods, null, 2)} as const; @@ -94,7 +89,6 @@ export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; await fs.writeFile(indexPath, `${src}\n${methodConstants}`); - // Also fix imports in zod.gen.ts (it may import from types.gen) for (const file of ["zod.gen.ts", "types.gen.ts"]) { const filePath = resolve(OUTPUT_DIR, file); try { @@ -127,9 +121,6 @@ interface MethodMeta { responseType: string | null; } -/** - * Convert a method path like "session/list" or "working_dir/update" to camelCase "sessionList", "workingDirUpdate". - */ function methodToCamelCase(method: string): string { return method .split(/[/_]/) @@ -139,10 +130,6 @@ function methodToCamelCase(method: string): string { .join(""); } -/** - * Generate a typed GooseClient class that wraps ClientSideConnection.extMethod() - * with proper TypeScript types and Zod runtime validation. - */ async function generateClient(meta: { methods: MethodMeta[] }) { const typeImports = new Set(); const zodImports = new Set(); @@ -153,7 +140,6 @@ async function generateClient(meta: { methods: MethodMeta[] }) { const fnName = methodToCamelCase(m.method); const fullMethod = `_goose/${m.method}`; - // Build param type and arg let paramType = ""; let paramArg = ""; let callParams = "{}"; @@ -164,7 +150,6 @@ async function generateClient(meta: { methods: MethodMeta[] }) { callParams = "params"; } - // Build return type and validation let returnType: string; let bodyLines: string[]; @@ -183,7 +168,6 @@ async function generateClient(meta: { methods: MethodMeta[] }) { `await this.conn.extMethod("${fullMethod}", ${callParams});`, ]; } else { - // Both request and response are untyped (serde_json::Value) returnType = "Record"; bodyLines = [ `return await this.conn.extMethod("${fullMethod}", ${callParams ? callParams : "{}"});`, @@ -212,11 +196,7 @@ export interface ExtMethodProvider { ${typeImportLine} ${zodImportLine} -/** - * Typed client for Goose custom extension methods. - * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. - */ -export class GooseClient { +export class GooseExtClient { constructor(private conn: ExtMethodProvider) {} ${methodDefs.join("\n")} } diff --git a/ui/acp/src/generated/client.gen.ts b/ui/acp/src/generated/client.gen.ts index db0ae5eb21..8de00f5a1e 100644 --- a/ui/acp/src/generated/client.gen.ts +++ b/ui/acp/src/generated/client.gen.ts @@ -39,7 +39,7 @@ import { * Typed client for Goose custom extension methods. * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. */ -export class GooseClient { +export class GooseExtClient { constructor(private conn: ExtMethodProvider) {} async extensionsAdd(params: AddExtensionRequest): Promise { @@ -98,32 +98,4 @@ export class GooseClient { const raw = await this.conn.extMethod("_goose/config/extensions", {}); return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse; } - - async toolCall(): Promise> { - return await this.conn.extMethod("_goose/tool/call", {}); - } - - async providerUpdate(): Promise> { - return await this.conn.extMethod("_goose/provider/update", {}); - } - - async containerSet(): Promise> { - return await this.conn.extMethod("_goose/container/set", {}); - } - - async appsList(): Promise> { - return await this.conn.extMethod("_goose/apps/list", {}); - } - - async appsExport(): Promise> { - return await this.conn.extMethod("_goose/apps/export", {}); - } - - async appsImport(): Promise> { - return await this.conn.extMethod("_goose/apps/import", {}); - } - - async configProviders(): Promise> { - return await this.conn.extMethod("_goose/config/providers", {}); - } } diff --git a/ui/acp/src/generated/index.ts b/ui/acp/src/generated/index.ts index b44b568427..2f6a85b441 100644 --- a/ui/acp/src/generated/index.ts +++ b/ui/acp/src/generated/index.ts @@ -58,41 +58,6 @@ export const GOOSE_EXT_METHODS = [ requestType: null, responseType: "GetExtensionsResponse", }, - { - method: "tool/call", - requestType: null, - responseType: null, - }, - { - method: "provider/update", - requestType: null, - responseType: null, - }, - { - method: "container/set", - requestType: null, - responseType: null, - }, - { - method: "apps/list", - requestType: null, - responseType: null, - }, - { - method: "apps/export", - requestType: null, - responseType: null, - }, - { - method: "apps/import", - requestType: null, - responseType: null, - }, - { - method: "config/providers", - requestType: null, - responseType: null, - }, ] as const; export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; diff --git a/ui/acp/src/goose-client.ts b/ui/acp/src/goose-client.ts new file mode 100644 index 0000000000..f64a190329 --- /dev/null +++ b/ui/acp/src/goose-client.ts @@ -0,0 +1,118 @@ +import { + ClientSideConnection, + type Client, + type Stream, + type InitializeRequest, + type InitializeResponse, + type NewSessionRequest, + type NewSessionResponse, + type LoadSessionRequest, + type LoadSessionResponse, + type PromptRequest, + type PromptResponse, + type CancelNotification, + type AuthenticateRequest, + type AuthenticateResponse, + type SetSessionModeRequest, + type SetSessionModeResponse, + type SetSessionConfigOptionRequest, + type SetSessionConfigOptionResponse, + type ForkSessionRequest, + type ForkSessionResponse, + type ListSessionsRequest, + type ListSessionsResponse, + type ResumeSessionRequest, + type ResumeSessionResponse, + type SetSessionModelRequest, + type SetSessionModelResponse, +} from "@agentclientprotocol/sdk"; +import { GooseExtClient } from "./generated/client.gen.js"; + +export class GooseClient { + private conn: ClientSideConnection; + private ext: GooseExtClient; + + constructor(toClient: () => Client, stream: Stream) { + this.conn = new ClientSideConnection(toClient, stream); + this.ext = new GooseExtClient(this.conn); + } + + get signal(): AbortSignal { + return this.conn.signal; + } + + get closed(): Promise { + return this.conn.closed; + } + + initialize(params: InitializeRequest): Promise { + return this.conn.initialize(params); + } + + newSession(params: NewSessionRequest): Promise { + return this.conn.newSession(params); + } + + loadSession(params: LoadSessionRequest): Promise { + return this.conn.loadSession(params); + } + + prompt(params: PromptRequest): Promise { + return this.conn.prompt(params); + } + + cancel(params: CancelNotification): Promise { + return this.conn.cancel(params); + } + + authenticate(params: AuthenticateRequest): Promise { + return this.conn.authenticate(params); + } + + setSessionMode( + params: SetSessionModeRequest, + ): Promise { + return this.conn.setSessionMode(params); + } + + setSessionConfigOption( + params: SetSessionConfigOptionRequest, + ): Promise { + return this.conn.setSessionConfigOption(params); + } + + unstable_forkSession( + params: ForkSessionRequest, + ): Promise { + return this.conn.unstable_forkSession(params); + } + + unstable_listSessions( + params: ListSessionsRequest, + ): Promise { + return this.conn.unstable_listSessions(params); + } + + unstable_resumeSession( + params: ResumeSessionRequest, + ): Promise { + return this.conn.unstable_resumeSession(params); + } + + unstable_setSessionModel( + params: SetSessionModelRequest, + ): Promise { + return this.conn.unstable_setSessionModel(params); + } + + extMethod( + method: string, + params: Record, + ): Promise> { + return this.conn.extMethod(method, params); + } + + get goose(): GooseExtClient { + return this.ext; + } +} diff --git a/ui/acp/src/index.ts b/ui/acp/src/index.ts index 04ae78458e..eb8a86ec5a 100644 --- a/ui/acp/src/index.ts +++ b/ui/acp/src/index.ts @@ -1,3 +1,9 @@ -export * from "./generated/index.js"; +export * from "./generated/types.gen.js"; export * from "./generated/zod.gen.js"; -export { GooseClient } from "./generated/client.gen.js"; +export { GooseClient } from "./goose-client.js"; + +export { + ClientSideConnection, + type Client, + type Stream, +} from "@agentclientprotocol/sdk"; diff --git a/ui/text/.gitignore b/ui/text/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/ui/text/.gitignore @@ -0,0 +1 @@ +dist diff --git a/ui/text/.npmrc b/ui/text/.npmrc new file mode 100644 index 0000000000..214c29d139 --- /dev/null +++ b/ui/text/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/ui/text/README.md b/ui/text/README.md new file mode 100644 index 0000000000..09555d5ac2 --- /dev/null +++ b/ui/text/README.md @@ -0,0 +1,16 @@ +# goose ACP TUI + +Early stage and part of goose's broader move to ACP + +https://github.com/block/goose/issues/6642 +https://github.com/block/goose/discussions/7309 + +## Running + +1. Run the server `cargo run -p goose-acp --bin goose-acp-server` +2. Run the tui + ``` + cd ui/text + npm i + npm run start + ``` diff --git a/ui/text/package-lock.json b/ui/text/package-lock.json new file mode 100644 index 0000000000..bf6499854a --- /dev/null +++ b/ui/text/package-lock.json @@ -0,0 +1,1244 @@ +{ + "name": "goose-text", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "goose-text", + "version": "0.1.0", + "dependencies": { + "@agentclientprotocol/sdk": "^0.14.1", + "@goose/acp": "file:../acp", + "ink": "^5.1.0", + "ink-text-input": "^6.0.0", + "meow": "^13.2.0", + "react": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^25.2.3", + "@types/react": "^18.3.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "../acp": { + "name": "goose-acp-types", + "version": "0.1.0", + "dependencies": { + "zod": "^3.25.76" + }, + "devDependencies": { + "@agentclientprotocol/sdk": "^0.14.1", + "@hey-api/openapi-ts": "^0.92.3", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "~5.9.3" + }, + "peerDependencies": { + "@agentclientprotocol/sdk": "*" + } + }, + "node_modules/@agentclientprotocol/sdk": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.14.1.tgz", + "integrity": "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==", + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@goose/acp": { + "resolved": "../acp", + "link": true + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/ui/text/package.json b/ui/text/package.json new file mode 100644 index 0000000000..ded27fb2af --- /dev/null +++ b/ui/text/package.json @@ -0,0 +1,25 @@ +{ + "name": "goose-text", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "tsx src/cli.tsx", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.14.1", + "@goose/acp": "file:../acp", + "ink": "^5.1.0", + "ink-text-input": "^6.0.0", + "meow": "^13.2.0", + "react": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^25.2.3", + "@types/react": "^18.3.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/ui/text/src/app.tsx b/ui/text/src/app.tsx new file mode 100644 index 0000000000..135ccae9c5 --- /dev/null +++ b/ui/text/src/app.tsx @@ -0,0 +1,764 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { Box, Text, useApp, useInput, useStdout } from "ink"; +import TextInput from "ink-text-input"; +import type { + SessionNotification, + RequestPermissionRequest, + RequestPermissionResponse, +} from "@agentclientprotocol/sdk"; +import { GooseClient } from "@goose/acp"; +import { createHttpStream } from "./transport.js"; + +interface PendingPermission { + toolTitle: string; + options: Array<{ optionId: string; name: string; kind: string }>; + resolve: (response: RequestPermissionResponse) => void; +} + +const CRANBERRY_BRIGHT = "#C0354A"; +const HARBOR_NAVY = "#1B2A4A"; +const DEEP_SLATE = "#3A4F6F"; +const SLATE = "#6B7F99"; +const LIGHT_SLATE = "#8FA4BD"; +const AUTUMN_GOLD = "#C4883A"; +const OCEAN_TEAL = "#3A7D7B"; +const CEDAR_BROWN = "#6B5344"; +const FOG_WHITE = "#E8E4DF"; +const PARCHMENT = "#D4CFC8"; + +const GOOSE_FRAMES = [ + [ + " ,_", + " (o >", + " //\\", + " \\\\ \\", + " \\\\_/", + " | |", + " ^ ^", + ], + [ + " ,_", + " (o >", + " //\\", + " \\\\ \\", + " \\\\_/", + " / |", + " ^ ^", + ], + [ + " ,_", + " (o >", + " //\\", + " \\\\ \\", + " \\\\_/", + " | |", + " ^ ^", + ], + [ + " ,_", + " (o >", + " //\\", + " \\\\ \\", + " \\\\_/", + " | \\", + " ^ ^", + ], +]; + +const TITLE_TEXT = "goose"; + +const GREETING_MESSAGES = [ + "What would you like to work on?", + "Ready to build something amazing?", + "What would you like to explore?", + "What's on your mind?", + "What shall we create today?", + "What project needs attention?", + "What would you like to tackle?", + "What needs to be done?", + "What's the plan for today?", + "Ready to create something great?", + "What can be built today?", + "What's the next challenge?", + "What progress can be made?", + "What would you like to accomplish?", + "What task awaits?", + "What's the mission today?", + "What can be achieved?", + "What project is ready to begin?", +]; + +const INITIAL_GREETING = + GREETING_MESSAGES[Math.floor(Math.random() * GREETING_MESSAGES.length)]!; + +const SPINNER_FRAMES = ["◐", "◓", "◑", "◒"]; + +const PERMISSION_LABELS: Record = { + allow_once: "Allow once", + allow_always: "Always allow", + reject_once: "Reject once", + reject_always: "Always reject", +}; + +const PERMISSION_KEYS: Record = { + allow_once: "y", + allow_always: "a", + reject_once: "n", + reject_always: "N", +}; + +interface TextMessage { + kind: "text"; + role: "user" | "agent"; + text: string; +} + +interface ToolCallMessage { + kind: "tool_call"; + title: string; +} + +type Message = TextMessage | ToolCallMessage; + +function HRule({ width, color }: { width: number; color?: string }) { + return ( + + + {"─".repeat(Math.max(width, 1))} + + + ); +} + +function HeaderBar({ + width, + status, + loading, + spinIdx, + hasPendingPermission, +}: { + width: number; + status: string; + loading: boolean; + spinIdx: number; + hasPendingPermission: boolean; +}) { + const statusColor = + status === "ready" + ? OCEAN_TEAL + : status.startsWith("error") || status.startsWith("failed") + ? CRANBERRY_BRIGHT + : SLATE; + + const leftContent = ` ${TITLE_TEXT} `; + const spinner = + loading && !hasPendingPermission + ? ` ${SPINNER_FRAMES[spinIdx % SPINNER_FRAMES.length]} ` + : ""; + + return ( + + + + + {leftContent} + + + {status} + {spinner && {spinner}} + + + + ctrl+c to exit{" "} + + + + + + ); +} + +function ToolCallBlock({ title, width }: { title: string; width: number }) { + return ( + + + + {title} + + + ); +} + +function UserMessage({ text, width }: { text: string; width: number }) { + return ( + + + + {"❯ "} + + + {text} + + + + ); +} + +function AgentMessage({ text, width }: { text: string; width: number }) { + return ( + + {text} + + ); +} + +function PermissionPrompt({ + toolTitle, + options, + selectedIdx, + width, +}: { + toolTitle: string; + options: Array<{ optionId: string; name: string; kind: string }>; + selectedIdx: number; + width: number; +}) { + return ( + + + 🔒 Permission required + + {toolTitle} + + {options.map((opt, i) => { + const key = PERMISSION_KEYS[opt.kind] ?? String(i + 1); + const label = PERMISSION_LABELS[opt.kind] ?? opt.name; + const selected = i === selectedIdx; + return ( + + + {selected ? " ▸ " : " "} + + + [{key}] {label} + + + ); + })} + + + + ↑↓ select · enter confirm · esc cancel + + + + ); +} + +function SplashScreen({ + animFrame, + width, + height, + status, + loading, + spinIdx, + showInput, + input, + onInputChange, + onInputSubmit, +}: { + animFrame: number; + width: number; + height: number; + status: string; + loading: boolean; + spinIdx: number; + showInput: boolean; + input: string; + onInputChange: (v: string) => void; + onInputSubmit: (v: string) => void; +}) { + const frame = GOOSE_FRAMES[animFrame % GOOSE_FRAMES.length]!; + const statusColor = + status === "ready" + ? OCEAN_TEAL + : status.startsWith("error") || status.startsWith("failed") + ? CRANBERRY_BRIGHT + : SLATE; + + const inputWidth = Math.min(60, width - 8); + + return ( + + + {frame.map((line, i) => ( + + {line} + + ))} + + + + {TITLE_TEXT} + + + + your on-machine AI agent + + + {showInput ? ( + <> + + + + + + {"❯ "} + + + + + + + + ) : ( + <> + + + + + {loading && ( + + {SPINNER_FRAMES[spinIdx % SPINNER_FRAMES.length]}{" "} + + )} + {status} + + + )} + + ); +} + +function InputBar({ + width, + input, + onChange, + onSubmit, +}: { + width: number; + input: string; + onChange: (v: string) => void; + onSubmit: (v: string) => void; +}) { + return ( + + + + + {"❯ "} + + + + + ); +} + +function LoadingIndicator({ + status, + spinIdx, +}: { + status: string; + spinIdx: number; +}) { + return ( + + + {SPINNER_FRAMES[spinIdx % SPINNER_FRAMES.length]}{" "} + + + {status} + + + ); +} + +export default function App({ + serverUrl, + initialPrompt, +}: { + serverUrl: string; + initialPrompt?: string; +}) { + const { exit } = useApp(); + const { stdout } = useStdout(); + const termWidth = stdout?.columns ?? 80; + const termHeight = stdout?.rows ?? 24; + + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(true); + const [status, setStatus] = useState("connecting..."); + const [spinIdx, setSpinIdx] = useState(0); + const [gooseFrame, setGooseFrame] = useState(0); + const [bannerVisible, setBannerVisible] = useState(true); + const [pendingPermission, setPendingPermission] = + useState(null); + const [permissionIdx, setPermissionIdx] = useState(0); + const clientRef = useRef(null); + const sessionIdRef = useRef(null); + const streamBuf = useRef(""); + const sentInitialPrompt = useRef(false); + + useEffect(() => { + const t = setInterval(() => { + setSpinIdx((i) => (i + 1) % SPINNER_FRAMES.length); + setGooseFrame((f) => f + 1); + }, 300); + return () => clearInterval(t); + }, []); + + useEffect(() => { + if (messages.length > 0) { + setBannerVisible(false); + } + }, [messages]); + + const appendAgent = useCallback((text: string) => { + setMessages((prev) => { + const last = prev[prev.length - 1]; + if (last && last.kind === "text" && last.role === "agent") { + return [ + ...prev.slice(0, -1), + { + kind: "text" as const, + role: "agent" as const, + text: last.text + text, + }, + ]; + } + return [...prev, { kind: "text" as const, role: "agent" as const, text }]; + }); + }, []); + + const appendToolCall = useCallback((title: string) => { + setMessages((prev) => [...prev, { kind: "tool_call" as const, title }]); + }, []); + + const resolvePermission = useCallback( + (option: { optionId: string } | "cancelled") => { + if (!pendingPermission) return; + const { resolve } = pendingPermission; + if (option === "cancelled") { + resolve({ outcome: { outcome: "cancelled" } }); + } else { + resolve({ + outcome: { outcome: "selected", optionId: option.optionId }, + }); + } + setPendingPermission(null); + setPermissionIdx(0); + }, + [pendingPermission], + ); + + const sendPrompt = useCallback( + async (text: string) => { + const client = clientRef.current; + const sid = sessionIdRef.current; + if (!client || !sid) return; + + setMessages((prev) => [ + ...prev, + { kind: "text" as const, role: "user" as const, text }, + ]); + setLoading(true); + setStatus("thinking..."); + streamBuf.current = ""; + + try { + const result = await client.prompt({ + sessionId: sid, + prompt: [{ type: "text", text }], + }); + + if (streamBuf.current) { + appendAgent(""); + } + + setStatus( + result.stopReason === "end_turn" + ? "ready" + : `stopped: ${result.stopReason}`, + ); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setStatus(`error: ${errMsg}`); + } finally { + setLoading(false); + } + }, + [appendAgent], + ); + + useEffect(() => { + let cancelled = false; + + (async () => { + try { + setStatus("initializing..."); + const stream = createHttpStream(serverUrl); + + const client = new GooseClient( + () => ({ + sessionUpdate: async (params: SessionNotification) => { + const update = params.update; + + if (update.sessionUpdate === "agent_message_chunk") { + if (update.content.type === "text") { + streamBuf.current += update.content.text; + appendAgent(update.content.text); + } + } else if (update.sessionUpdate === "tool_call") { + appendToolCall(update.title || "tool"); + } + }, + requestPermission: async ( + params: RequestPermissionRequest, + ): Promise => { + return new Promise((resolve) => { + const toolTitle = params.toolCall.title ?? "unknown tool"; + const options = params.options.map((opt) => ({ + optionId: opt.optionId, + name: opt.name, + kind: opt.kind, + })); + setPendingPermission({ toolTitle, options, resolve }); + setPermissionIdx(0); + }); + }, + }), + stream, + ); + + if (cancelled) return; + clientRef.current = client; + + setStatus("handshaking..."); + await client.initialize({ + protocolVersion: 0, + clientInfo: { name: "goose-text", version: "0.1.0" }, + clientCapabilities: {}, + }); + + if (cancelled) return; + + setStatus("creating session..."); + const session = await client.newSession({ + cwd: process.cwd(), + mcpServers: [], + }); + + if (cancelled) return; + sessionIdRef.current = session.sessionId; + setLoading(false); + setStatus("ready"); + + if (initialPrompt && !sentInitialPrompt.current) { + sentInitialPrompt.current = true; + await sendPrompt(initialPrompt); + if (initialPrompt) { + setTimeout(() => exit(), 100); + } + } + } catch (e: unknown) { + if (cancelled) return; + const errMsg = e instanceof Error ? e.message : String(e); + setStatus(`failed: ${errMsg}`); + setLoading(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [serverUrl, initialPrompt, sendPrompt, appendAgent, appendToolCall, exit]); + + const handleSubmit = useCallback( + (value: string) => { + const trimmed = value.trim(); + if (!trimmed || loading) return; + setInput(""); + sendPrompt(trimmed); + }, + [loading, sendPrompt], + ); + + useInput((ch, key) => { + if (key.escape || (ch === "c" && key.ctrl)) { + if (pendingPermission) { + resolvePermission("cancelled"); + return; + } + exit(); + } + + if (pendingPermission) { + const opts = pendingPermission.options; + + if (key.upArrow) { + setPermissionIdx((i) => (i - 1 + opts.length) % opts.length); + return; + } + if (key.downArrow) { + setPermissionIdx((i) => (i + 1) % opts.length); + return; + } + if (key.return) { + const selected = opts[permissionIdx]; + if (selected) { + resolvePermission({ optionId: selected.optionId }); + } + return; + } + + const keyMap: Record = { + y: "allow_once", + a: "allow_always", + n: "reject_once", + N: "reject_always", + }; + const targetKind = keyMap[ch]; + if (targetKind) { + const match = opts.find((o) => o.kind === targetKind); + if (match) { + resolvePermission({ optionId: match.optionId }); + return; + } + } + } + }); + + const PAD_X = 2; + const PAD_BOTTOM = 1; + const innerWidth = Math.max(termWidth - PAD_X * 2, 20); + const headerHeight = 2; + const inputBarHeight = initialPrompt ? 0 : 2; + const bodyHeight = Math.max(termHeight - headerHeight - inputBarHeight - PAD_BOTTOM, 3); + + if (bannerVisible) { + return ( + + + + ); + } + + return ( + + + + + {messages.map((msg, i) => { + if (msg.kind === "tool_call") { + return ; + } + if (msg.role === "user") { + return ( + + {i > 0 && } + + + + ); + } + return ; + })} + + {pendingPermission && ( + + )} + + {loading && !pendingPermission && messages.length > 0 && ( + + )} + + + {!loading && !pendingPermission && !initialPrompt && ( + + )} + + ); +} diff --git a/ui/text/src/cli.tsx b/ui/text/src/cli.tsx new file mode 100644 index 0000000000..568890a1b9 --- /dev/null +++ b/ui/text/src/cli.tsx @@ -0,0 +1,25 @@ +#!/usr/bin/env node +import React from "react"; +import { render } from "ink"; +import meow from "meow"; +import App from "./app.js"; + +const cli = meow( + ` + Usage + $ goose-text + + Options + --server, -s Server URL (default: http://127.0.0.1:3284) + --text, -t Send a single prompt and exit +`, + { + importMeta: import.meta, + flags: { + server: { type: "string", shortFlag: "s", default: "http://127.0.0.1:3284" }, + text: { type: "string", shortFlag: "t" }, + }, + } +); + +render(); diff --git a/ui/text/src/transport.ts b/ui/text/src/transport.ts new file mode 100644 index 0000000000..193b5a1375 --- /dev/null +++ b/ui/text/src/transport.ts @@ -0,0 +1,120 @@ +import type { AnyMessage, Stream } from "@agentclientprotocol/sdk"; + +const ACP_SESSION_HEADER = "Acp-Session-Id"; + +export function createHttpStream(serverUrl: string): Stream { + let sessionId: string | null = null; + const incoming: AnyMessage[] = []; + const waiters: Array<() => void> = []; + const sseAbort = new AbortController(); + + function pushMessage(msg: AnyMessage) { + incoming.push(msg); + const w = waiters.shift(); + if (w) w(); + } + + function waitForMessage(): Promise { + if (incoming.length > 0) return Promise.resolve(); + return new Promise((r) => waiters.push(r)); + } + + async function consumeSSE(response: Response) { + if (!response.body) return; + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + const parts = buffer.split("\n\n"); + buffer = parts.pop() || ""; + + for (const part of parts) { + for (const line of part.split("\n")) { + if (line.startsWith("data: ")) { + try { + const msg = JSON.parse(line.slice(6)) as AnyMessage; + pushMessage(msg); + } catch { + } + } + } + } + } + } catch (e: unknown) { + if (e instanceof DOMException && e.name === "AbortError") return; + } + } + + // POST initialize (no session header) opens a long-lived SSE stream that receives + // ALL subsequent responses and notifications. Later POSTs with the session header + // are fire-and-forget for requests (responses arrive on the first stream) or + // return 202 immediately for notifications/responses. + let isFirstRequest = true; + + const readable = new ReadableStream({ + async pull(controller) { + await waitForMessage(); + while (incoming.length > 0) { + controller.enqueue(incoming.shift()!); + } + }, + }); + + const writable = new WritableStream({ + async write(msg) { + const isRequest = + "method" in msg && "id" in msg && msg.id !== undefined && msg.id !== null; + + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }; + if (sessionId) { + headers[ACP_SESSION_HEADER] = sessionId; + } + + if (isFirstRequest && isRequest) { + isFirstRequest = false; + + const response = await fetch(`${serverUrl}/acp`, { + method: "POST", + headers, + body: JSON.stringify(msg), + signal: sseAbort.signal, + }); + + const sid = response.headers.get(ACP_SESSION_HEADER); + if (sid) sessionId = sid; + + consumeSSE(response); + } else if (isRequest) { + const abort = new AbortController(); + fetch(`${serverUrl}/acp`, { + method: "POST", + headers, + body: JSON.stringify(msg), + signal: abort.signal, + }).catch(() => {}); + setTimeout(() => abort.abort(), 200); + } else { + await fetch(`${serverUrl}/acp`, { + method: "POST", + headers, + body: JSON.stringify(msg), + }); + } + }, + + close() { + sseAbort.abort(); + }, + }); + + return { readable, writable }; +} diff --git a/ui/text/tsconfig.json b/ui/text/tsconfig.json new file mode 100644 index 0000000000..c3ee214ac3 --- /dev/null +++ b/ui/text/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src"] +}