From 67b90205ed5c784d22468a88a623c70b904b5456 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Thu, 16 Apr 2026 11:36:53 -0400 Subject: [PATCH] chore: set goose binaries as executable in package.json (#8589) --- .../goose-binary-darwin-arm64/package.json | 3 + .../goose-binary-darwin-x64/package.json | 3 + .../goose-binary-linux-arm64/package.json | 3 + .../goose-binary-linux-x64/package.json | 3 + .../goose-binary-win32-x64/package.json | 3 + ui/pnpm-lock.yaml | 32 +- ui/scripts/publish.sh | 5 +- ui/sdk/package.json | 17 + ui/sdk/src/resolve-binary.ts | 43 ++ ui/text/.gitignore | 1 - ui/text/package.json | 15 +- ui/text/scripts/dev-binary.mjs | 103 ---- ui/text/scripts/dev-start.mjs | 36 ++ ui/text/scripts/postinstall.mjs | 63 --- ui/text/src/tui.tsx | 512 +++++++++++------- 15 files changed, 452 insertions(+), 390 deletions(-) create mode 100644 ui/sdk/src/resolve-binary.ts delete mode 100755 ui/text/scripts/dev-binary.mjs create mode 100644 ui/text/scripts/dev-start.mjs delete mode 100644 ui/text/scripts/postinstall.mjs diff --git a/ui/goose-binary/goose-binary-darwin-arm64/package.json b/ui/goose-binary/goose-binary-darwin-arm64/package.json index 6aad3c04..c5b92ece 100644 --- a/ui/goose-binary/goose-binary-darwin-arm64/package.json +++ b/ui/goose-binary/goose-binary-darwin-arm64/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/aaif-goose/goose.git" }, + "bin": { + "goose": "bin/goose" + }, "keywords": [ "goose", "ai", diff --git a/ui/goose-binary/goose-binary-darwin-x64/package.json b/ui/goose-binary/goose-binary-darwin-x64/package.json index 2ed89a60..8cae773e 100644 --- a/ui/goose-binary/goose-binary-darwin-x64/package.json +++ b/ui/goose-binary/goose-binary-darwin-x64/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/aaif-goose/goose.git" }, + "bin": { + "goose": "bin/goose" + }, "keywords": [ "goose", "ai", diff --git a/ui/goose-binary/goose-binary-linux-arm64/package.json b/ui/goose-binary/goose-binary-linux-arm64/package.json index 9caf0124..e5cb9f28 100644 --- a/ui/goose-binary/goose-binary-linux-arm64/package.json +++ b/ui/goose-binary/goose-binary-linux-arm64/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/aaif-goose/goose.git" }, + "bin": { + "goose": "bin/goose" + }, "keywords": [ "goose", "ai", diff --git a/ui/goose-binary/goose-binary-linux-x64/package.json b/ui/goose-binary/goose-binary-linux-x64/package.json index 917e3fda..7aebcbd2 100644 --- a/ui/goose-binary/goose-binary-linux-x64/package.json +++ b/ui/goose-binary/goose-binary-linux-x64/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/aaif-goose/goose.git" }, + "bin": { + "goose": "bin/goose" + }, "keywords": [ "goose", "ai", diff --git a/ui/goose-binary/goose-binary-win32-x64/package.json b/ui/goose-binary/goose-binary-win32-x64/package.json index 1f498c2c..99d94d7a 100644 --- a/ui/goose-binary/goose-binary-win32-x64/package.json +++ b/ui/goose-binary/goose-binary-win32-x64/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/aaif-goose/goose.git" }, + "bin": { + "goose": "bin/goose.exe" + }, "keywords": [ "goose", "ai", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 4882d39e..3def31fa 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -667,6 +667,22 @@ importers: typescript: specifier: ~5.9.3 version: 5.9.3 + optionalDependencies: + '@aaif/goose-binary-darwin-arm64': + specifier: workspace:* + version: link:../goose-binary/goose-binary-darwin-arm64 + '@aaif/goose-binary-darwin-x64': + specifier: workspace:* + version: link:../goose-binary/goose-binary-darwin-x64 + '@aaif/goose-binary-linux-arm64': + specifier: workspace:* + version: link:../goose-binary/goose-binary-linux-arm64 + '@aaif/goose-binary-linux-x64': + specifier: workspace:* + version: link:../goose-binary/goose-binary-linux-x64 + '@aaif/goose-binary-win32-x64': + specifier: workspace:* + version: link:../goose-binary/goose-binary-win32-x64 text: dependencies: @@ -716,22 +732,6 @@ importers: typescript: specifier: ^5.7.0 version: 5.9.3 - optionalDependencies: - '@aaif/goose-binary-darwin-arm64': - specifier: workspace:* - version: link:../goose-binary/goose-binary-darwin-arm64 - '@aaif/goose-binary-darwin-x64': - specifier: workspace:* - version: link:../goose-binary/goose-binary-darwin-x64 - '@aaif/goose-binary-linux-arm64': - specifier: workspace:* - version: link:../goose-binary/goose-binary-linux-arm64 - '@aaif/goose-binary-linux-x64': - specifier: workspace:* - version: link:../goose-binary/goose-binary-linux-x64 - '@aaif/goose-binary-win32-x64': - specifier: workspace:* - version: link:../goose-binary/goose-binary-win32-x64 packages: diff --git a/ui/scripts/publish.sh b/ui/scripts/publish.sh index f50aee86..533e2698 100755 --- a/ui/scripts/publish.sh +++ b/ui/scripts/publish.sh @@ -79,7 +79,6 @@ apt-get install -y -qq --no-install-recommends \ echo "==> Compiling goose (this takes a while)..." cargo build --release --bin goose cp /build/target/release/goose /output/goose -chmod +x /output/goose echo "==> Done" ' @@ -123,8 +122,7 @@ WORKDIR /build COPY . . RUN mkdir -p /output && \ cargo build --release --bin goose && \ - cp target/release/goose /output/goose && \ - chmod +x /output/goose + cp target/release/goose /output/goose DEOF # Build in Docker and extract the binary @@ -141,7 +139,6 @@ DEOF docker cp "${cid}:/output/goose" "${pkg_dir}/goose" docker rm "${cid}" >/dev/null docker rmi "${iid}" >/dev/null 2>&1 || true - chmod +x "${pkg_dir}/goose" rm -rf "${ctx}" diff --git a/ui/sdk/package.json b/ui/sdk/package.json index 78bfed5b..d13dd63b 100644 --- a/ui/sdk/package.json +++ b/ui/sdk/package.json @@ -17,6 +17,16 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./node": { + "types": "./dist/resolve-binary.d.ts", + "default": "./dist/resolve-binary.js" + } + }, "files": [ "dist" ], @@ -35,6 +45,13 @@ "peerDependencies": { "@agentclientprotocol/sdk": "*" }, + "optionalDependencies": { + "@aaif/goose-binary-darwin-arm64": "workspace:*", + "@aaif/goose-binary-darwin-x64": "workspace:*", + "@aaif/goose-binary-linux-arm64": "workspace:*", + "@aaif/goose-binary-linux-x64": "workspace:*", + "@aaif/goose-binary-win32-x64": "workspace:*" + }, "devDependencies": { "@agentclientprotocol/sdk": "^0.14.1", "@hey-api/openapi-ts": "^0.92.3", diff --git a/ui/sdk/src/resolve-binary.ts b/ui/sdk/src/resolve-binary.ts new file mode 100644 index 00000000..59c4a67e --- /dev/null +++ b/ui/sdk/src/resolve-binary.ts @@ -0,0 +1,43 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; + +const PLATFORMS: Record = { + "darwin-arm64": "@aaif/goose-binary-darwin-arm64", + "darwin-x64": "@aaif/goose-binary-darwin-x64", + "linux-arm64": "@aaif/goose-binary-linux-arm64", + "linux-x64": "@aaif/goose-binary-linux-x64", + "win32-x64": "@aaif/goose-binary-win32-x64", +}; + +/** + * Resolves the path to the goose binary. + * + * Resolution order: + * 1. `GOOSE_BINARY` environment variable (explicit override) + * 2. Platform-specific `@aaif/goose-binary-*` optional dependency + * + * @throws if no binary can be found + */ +export function resolveGooseBinary(): string { + const envBinary = process.env.GOOSE_BINARY; + if (envBinary) return envBinary; + + const key = `${process.platform}-${process.arch}`; + const pkg = PLATFORMS[key]; + if (!pkg) { + throw new Error( + `No goose binary available for ${key}. Set GOOSE_BINARY to the path of a goose binary.`, + ); + } + + try { + const require = createRequire(import.meta.url); + const pkgDir = dirname(require.resolve(`${pkg}/package.json`)); + const binName = process.platform === "win32" ? "goose.exe" : "goose"; + return join(pkgDir, "bin", binName); + } catch { + throw new Error( + `goose binary package ${pkg} is not installed. Set GOOSE_BINARY or install the native package.`, + ); + } +} diff --git a/ui/text/.gitignore b/ui/text/.gitignore index 5712d1d0..de4d1f00 100644 --- a/ui/text/.gitignore +++ b/ui/text/.gitignore @@ -1,3 +1,2 @@ dist node_modules -server-binary.json diff --git a/ui/text/package.json b/ui/text/package.json index a851c283..6051330f 100644 --- a/ui/text/package.json +++ b/ui/text/package.json @@ -19,15 +19,11 @@ "goose": "dist/tui.js" }, "files": [ - "dist", - "scripts/postinstall.mjs", - "server-binary.json" + "dist" ], "scripts": { "build": "tsc", - "dev:binary": "node scripts/dev-binary.mjs", - "start": "npm run dev:binary && tsx src/tui.tsx", - "postinstall": "node scripts/postinstall.mjs", + "start": "node scripts/dev-start.mjs", "lint": "tsc --noEmit" }, "dependencies": { @@ -41,13 +37,6 @@ "meow": "^13.2.0", "react": "^19.2.4" }, - "optionalDependencies": { - "@aaif/goose-binary-darwin-arm64": "workspace:*", - "@aaif/goose-binary-darwin-x64": "workspace:*", - "@aaif/goose-binary-linux-arm64": "workspace:*", - "@aaif/goose-binary-linux-x64": "workspace:*", - "@aaif/goose-binary-win32-x64": "workspace:*" - }, "overrides": { "react": "^19.2.4" }, diff --git a/ui/text/scripts/dev-binary.mjs b/ui/text/scripts/dev-binary.mjs deleted file mode 100755 index 9fc52dd2..00000000 --- a/ui/text/scripts/dev-binary.mjs +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env node - -// For development: ensures the Rust binary is built from source and -// points server-binary.json to the local target/release/goose binary. -// Rebuilds if source files are newer than the binary. - -import { writeFileSync, existsSync, statSync } from "node:fs"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const projectRoot = join(__dirname, "..", "..", ".."); -const binaryName = process.platform === "win32" ? "goose.exe" : "goose"; -const binaryPath = join(projectRoot, "target", "release", binaryName); - -// Verify we're in a development environment with Cargo.toml -const cargoToml = join(projectRoot, "Cargo.toml"); -if (!existsSync(cargoToml)) { - console.error("Error: Not in a Rust workspace (Cargo.toml not found)"); - console.error("This script is for development only. In production, use the prebuilt binaries."); - process.exit(1); -} - -function needsRebuild() { - if (!existsSync(binaryPath)) { - console.log("Binary not found, needs build"); - return true; - } - - const binaryMtime = statSync(binaryPath).mtimeMs; - - // Check if any Rust source files are newer than the binary - const cargoLock = join(projectRoot, "Cargo.lock"); - - if (existsSync(cargoToml) && statSync(cargoToml).mtimeMs > binaryMtime) { - console.log("Cargo.toml changed, needs rebuild"); - return true; - } - - if (existsSync(cargoLock) && statSync(cargoLock).mtimeMs > binaryMtime) { - console.log("Cargo.lock changed, needs rebuild"); - return true; - } - - // Check if goose-acp crate sources are newer than the binary - const acpDir = join(projectRoot, "crates", "goose-acp"); - if (existsSync(acpDir)) { - const result = spawnSync( - "find", - [acpDir, "-type", "f", "(", "-name", "*.rs", "-o", "-name", "Cargo.toml", ")", "-newer", binaryPath], - { encoding: "utf-8" }, - ); - const changed = (result.stdout ?? "").trim(); - if (changed) { - const first = changed.split("\n")[0]; - console.log(`goose-acp changed (e.g. ${first}), needs rebuild`); - return true; - } - } - - return false; -} - -function buildBinary() { - console.log("Building goose-cli from source..."); - const result = spawnSync( - "cargo", - ["build", "--release", "-p", "goose-cli"], - { - cwd: projectRoot, - stdio: "inherit", - } - ); - - if (result.error) { - console.error(`Failed to build: ${result.error.message}`); - process.exit(1); - } - - if (result.status !== 0) { - console.error(`Build failed with exit code ${result.status}`); - process.exit(1); - } - - console.log(`Built goose binary at ${binaryPath}`); -} - -// Main logic -if (needsRebuild()) { - buildBinary(); -} else { - console.log("Binary is up to date, skipping build"); -} - -// Write the server-binary.json to point to the local build -const outDir = join(__dirname, ".."); -writeFileSync( - join(outDir, "server-binary.json"), - JSON.stringify({ binaryPath }, null, 2) + "\n", -); - -console.log(`Using local goose binary at ${binaryPath}`); diff --git a/ui/text/scripts/dev-start.mjs b/ui/text/scripts/dev-start.mjs new file mode 100644 index 00000000..8f836628 --- /dev/null +++ b/ui/text/scripts/dev-start.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +// Development entrypoint: ensures a goose binary is available, then launches +// the TUI via tsx. Skips the cargo build if GOOSE_BINARY is already set. + +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(__dirname, "..", "..", ".."); + +if (!process.env.GOOSE_BINARY) { + const binName = process.platform === "win32" ? "goose.exe" : "goose"; + const binaryPath = join(repoRoot, "target", "debug", binName); + + console.log("Building goose (debug)…"); + execFileSync("cargo", ["build", "-p", "goose-cli"], { + cwd: repoRoot, + stdio: "inherit", + }); + + if (!existsSync(binaryPath)) { + console.error(`Build succeeded but binary not found at ${binaryPath}`); + process.exit(1); + } + + process.env.GOOSE_BINARY = binaryPath; +} + +execFileSync("tsx", [join(__dirname, "..", "src", "tui.tsx"), ...process.argv.slice(2)], { + cwd: process.cwd(), + stdio: "inherit", + env: process.env, +}); diff --git a/ui/text/scripts/postinstall.mjs b/ui/text/scripts/postinstall.mjs deleted file mode 100644 index afefbf41..00000000 --- a/ui/text/scripts/postinstall.mjs +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node - -// Resolves the path to the goose binary from the platform-specific -// optional dependency. Writes the result to a JSON file that the CLI reads at -// startup so it can spawn the server automatically. - -import { writeFileSync, mkdirSync, chmodSync } from "node:fs"; -import { createRequire } from "node:module"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const require = createRequire(import.meta.url); - -const PLATFORMS = { - "darwin-arm64": "@aaif/goose-binary-darwin-arm64", - "darwin-x64": "@aaif/goose-binary-darwin-x64", - "linux-arm64": "@aaif/goose-binary-linux-arm64", - "linux-x64": "@aaif/goose-binary-linux-x64", - "win32-x64": "@aaif/goose-binary-win32-x64", -}; - -const key = `${process.platform}-${process.arch}`; -const pkg = PLATFORMS[key]; - -if (!pkg) { - console.warn( - `@aaif/goose: no prebuilt goose binary for ${key}. ` + - `You will need to provide a server URL manually with --server.`, - ); - process.exit(0); -} - -let binaryPath; -try { - // Resolve the package directory, then point at the binary inside it - const pkgDir = dirname(require.resolve(`${pkg}/package.json`)); - const binName = process.platform === "win32" ? "goose.exe" : "goose"; - binaryPath = join(pkgDir, "bin", binName); -} catch { - // The optional dependency wasn't installed (e.g. wrong platform). That's fine. - console.warn( - `@aaif/goose: optional dependency ${pkg} not installed. ` + - `You will need to provide a server URL manually with --server.`, - ); - process.exit(0); -} - -// Ensure the binary is executable (npm may strip permissions during packaging) -if (process.platform !== "win32") { - try { - chmodSync(binaryPath, 0o755); - } catch {} -} - -const outDir = join(__dirname, ".."); -mkdirSync(outDir, { recursive: true }); -writeFileSync( - join(outDir, "server-binary.json"), - JSON.stringify({ binaryPath }, null, 2) + "\n", -); - -console.log(`@aaif/goose: found native goose binary at ${binaryPath}`); diff --git a/ui/text/src/tui.tsx b/ui/text/src/tui.tsx index 4054d083..fde633ed 100644 --- a/ui/text/src/tui.tsx +++ b/ui/text/src/tui.tsx @@ -5,9 +5,6 @@ import { MultilineInput } from "ink-multiline-input"; import meow from "meow"; import { spawn } from "node:child_process"; import { Readable, Writable } from "node:stream"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; import type { SessionNotification, RequestPermissionRequest, @@ -19,6 +16,7 @@ import type { } from "@agentclientprotocol/sdk"; import { ndJsonStream } from "@agentclientprotocol/sdk"; import { GooseClient } from "@aaif/goose-sdk"; +import { resolveGooseBinary } from "@aaif/goose-sdk/node"; import Onboarding from "./onboarding.js"; import ConfigureScreen, { ConfigureIntent } from "./configure.js"; import ExtensionsManager from "./extensions.js"; @@ -35,7 +33,15 @@ import { import { Header } from "./components/Header.js"; import { Rule } from "./components/Rule.js"; import { isErrorStatus, formatError } from "./utils.js"; -import { CRANBERRY, TEAL, GOLD, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_DIM, RULE_COLOR } from "./colors.js"; +import { + CRANBERRY, + TEAL, + GOLD, + TEXT_PRIMARY, + TEXT_SECONDARY, + TEXT_DIM, + RULE_COLOR, +} from "./colors.js"; import { Spinner, SPINNER_FRAMES } from "./components/Spinner.js"; import { PASTE_THRESHOLD, @@ -137,7 +143,9 @@ const InputBar = React.memo(function InputBar({ flexShrink={0} > - {"❯ "} + + {"❯ "} + {isPasteMode ? ( @@ -145,10 +153,16 @@ const InputBar = React.memo(function InputBar({ {(() => { const text = pastedFull; const availableWidth = Math.max(contentWidth - 20, 10); - const flat = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim(); + const flat = text + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim(); if (flat.length <= availableWidth) return flat; const suffix = ` (${flat.length.toLocaleString()} chars)`; - const previewLen = Math.max(availableWidth - suffix.length - 1, 5); + const previewLen = Math.max( + availableWidth - suffix.length - 1, + 5, + ); return flat.slice(0, previewLen) + "…" + suffix; })()} @@ -170,10 +184,13 @@ const InputBar = React.memo(function InputBar({ newline: (key) => key.return && key.ctrl, }} useCustomInput={(handler, isActive) => { - useInput((ch, key) => { - if (key.shift && (key.upArrow || key.downArrow)) return; - handler(ch, key); - }, { isActive }); + useInput( + (ch, key) => { + if (key.shift && (key.upArrow || key.downArrow)) return; + handler(ch, key); + }, + { isActive }, + ); }} /> {scrollHint && shift+↑↓ history} @@ -227,36 +244,64 @@ function buildContentLines({ const safeWidth = Math.max(width, 20); const turnId = String(turnIndex); - lines.push(...renderUserPrompt(turn.userText, safeWidth, turnId, (text: string, availableWidth: number) => { - const flat = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim(); - const safeWidth = Math.max(availableWidth, 10); - const maxPreview = Math.max(safeWidth - 30, Math.min(SENT_PREVIEW_LEN, safeWidth - 10)); - if (flat.length <= maxPreview + 10) { - return ( - - {flat} - - ); - } - const preview = flat.slice(0, maxPreview) + "…"; - const remaining = flat.length - maxPreview; - return ( - - {preview} - ({remaining.toLocaleString()} more chars) - - ); - })); + lines.push( + ...renderUserPrompt( + turn.userText, + safeWidth, + turnId, + (text: string, availableWidth: number) => { + const flat = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim(); + const safeWidth = Math.max(availableWidth, 10); + const maxPreview = Math.max( + safeWidth - 30, + Math.min(SENT_PREVIEW_LEN, safeWidth - 10), + ); + if (flat.length <= maxPreview + 10) { + return ( + + + {flat} + + + ); + } + const preview = flat.slice(0, maxPreview) + "…"; + const remaining = flat.length - maxPreview; + return ( + + + {preview} + + + {" "} + ({remaining.toLocaleString()} more chars) + + + ); + }, + ), + ); // Process response items - const hasToolCalls = turn.responseItems.some((it) => it.itemType === "tool_call"); + const hasToolCalls = turn.responseItems.some( + (it) => it.itemType === "tool_call", + ); let tcIdx = 0; for (let i = 0; i < turn.responseItems.length; i++) { const item = turn.responseItems[i]!; if (item.itemType === "tool_call") { - lines.push(...renderToolCallItem(item, i, safeWidth, toolCallsExpanded, tcIdx === 0, hasToolCalls)); + lines.push( + ...renderToolCallItem( + item, + i, + safeWidth, + toolCallsExpanded, + tcIdx === 0, + hasToolCalls, + ), + ); tcIdx++; } else if (item.itemType === "error") { lines.push(...renderErrorItem(item, i, safeWidth)); @@ -280,7 +325,12 @@ function buildContentLines({ const hRule = "─".repeat(Math.max(dialogWidth - 2, 0)); const permissionLines: React.ReactElement[] = []; - permissionLines.push(emptyLine(`pm-gap-${perm.toolTitle.slice(0, 10).replace(/[^a-zA-Z0-9]/g, '')}`, fullWidth)); + permissionLines.push( + emptyLine( + `pm-gap-${perm.toolTitle.slice(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`, + fullWidth, + ), + ); permissionLines.push( @@ -292,15 +342,27 @@ function buildContentLines({ permissionLines.push( - {content} + + {content} + , ); }; - row("pm-title", 🔒 Permission required); + row( + "pm-title", + + 🔒 Permission required + , + ); row("pm-g1", ); - row("pm-tool", {perm.toolTitle}); + row( + "pm-tool", + + {perm.toolTitle} + , + ); row("pm-g2", ); for (let i = 0; i < perm.options.length; i++) { @@ -308,18 +370,22 @@ function buildContentLines({ const k = PERMISSION_KEYS[opt.kind] ?? String(i + 1); const label = PERMISSION_LABELS[opt.kind] ?? opt.name; const active = i === selectedIdx; - row(`pm-o${i}`, ( + row( + `pm-o${i}`, <> {active ? "▸ " : " "} [{k}] {label} - - )); + , + ); } row("pm-g3", ); - row("pm-help", ↑↓ select · enter confirm · esc cancel); + row( + "pm-help", + ↑↓ select · enter confirm · esc cancel, + ); permissionLines.push( @@ -367,9 +433,11 @@ const Viewport = React.memo(function Viewport({ const above = startIdx; elements.push( - {above > 0 - ? ▲ {above} more (↑) - : } + {above > 0 ? ( + ▲ {above} more (↑) + ) : ( + + )} , ); } @@ -383,9 +451,11 @@ const Viewport = React.memo(function Viewport({ const below = total - endIdx; elements.push( - {below > 0 - ? ▼ {below} more (↓) - : } + {below > 0 ? ( + ▼ {below} more (↓) + ) : ( + + )} , ); } @@ -394,7 +464,11 @@ const Viewport = React.memo(function Viewport({ const constrainedHeight = Math.max(height, 1); return ( - + {elements} ); @@ -438,11 +512,15 @@ const SplashScreen = React.memo(function SplashScreen({ {topPad > 0 && } {frame.map((line, i) => ( - {line} + + {line} + ))} - goose + + goose + your on-machine AI agent @@ -484,7 +562,9 @@ function App({ const [scrollOffset, setScrollOffset] = useState(0); const [pastedFull, setPastedFull] = useState(null); const [needsOnboarding, setNeedsOnboarding] = useState(false); - type Overlay = { screen: "configure"; intent: ConfigureIntent } | { screen: "extensions" }; + type Overlay = + | { screen: "configure"; intent: ConfigureIntent } + | { screen: "extensions" }; const [overlay, setOverlay] = useState(null); const clientRef = useRef(null); @@ -527,13 +607,22 @@ function App({ if (lastItem.content.type === "text") { newItems[newItems.length - 1] = { ...lastItem, - content: { ...lastItem.content, text: lastItem.content.text + text }, + content: { + ...lastItem.content, + text: lastItem.content.text + text, + }, }; } else { - newItems.push({ itemType: "content_chunk", content: { type: "text", text } }); + newItems.push({ + itemType: "content_chunk", + content: { type: "text", text }, + }); } } else { - newItems.push({ itemType: "content_chunk", content: { type: "text", text } }); + newItems.push({ + itemType: "content_chunk", + content: { type: "text", text }, + }); } return [...prev.slice(0, -1), { ...last, responseItems: newItems }]; @@ -605,7 +694,9 @@ function App({ if (option === "cancelled") { resolve({ outcome: { outcome: "cancelled" } }); } else { - resolve({ outcome: { outcome: "selected", optionId: option.optionId } }); + resolve({ + outcome: { outcome: "selected", optionId: option.optionId }, + }); } setPendingPermission(null); setPermissionIdx(0); @@ -665,29 +756,32 @@ function App({ [executePrompt, processQueue], ); - const createSession = useCallback(async (client: GooseClient) => { - setStatus("creating session…"); - setLoading(true); - try { - const session = await client.newSession({ - cwd: process.cwd(), - mcpServers: [], - }); - sessionIdRef.current = session.sessionId; - setLoading(false); - setStatus("ready"); + const createSession = useCallback( + async (client: GooseClient) => { + setStatus("creating session…"); + setLoading(true); + try { + const session = await client.newSession({ + cwd: process.cwd(), + mcpServers: [], + }); + sessionIdRef.current = session.sessionId; + setLoading(false); + setStatus("ready"); - if (initialPrompt && !sentInitialPrompt.current) { - sentInitialPrompt.current = true; - await sendPrompt(initialPrompt); - setTimeout(() => exit(), 100); + if (initialPrompt && !sentInitialPrompt.current) { + sentInitialPrompt.current = true; + await sendPrompt(initialPrompt); + setTimeout(() => exit(), 100); + } + } catch (e: unknown) { + const errorMsg = formatError(e); + setStatus(`failed: ${errorMsg}`); + setLoading(false); } - } catch (e: unknown) { - const errorMsg = formatError(e); - setStatus(`failed: ${errorMsg}`); - setLoading(false); - } - }, [initialPrompt, sendPrompt, exit]); + }, + [initialPrompt, sendPrompt, exit], + ); const handleOnboardingComplete = useCallback(() => { setNeedsOnboarding(false); @@ -751,8 +845,11 @@ function App({ setStatus("checking provider…"); let hasProvider = false; try { - const resp = await client.goose.GooseConfigRead({ key: "GOOSE_PROVIDER" }); - hasProvider = resp.value != null && resp.value !== "" && resp.value !== "null"; + const resp = await client.goose.GooseConfigRead({ + key: "GOOSE_PROVIDER", + }); + hasProvider = + resp.value != null && resp.value !== "" && resp.value !== "null"; } catch { hasProvider = false; } @@ -774,10 +871,17 @@ function App({ } })(); - return () => { cancelled = true; }; + return () => { + cancelled = true; + }; }, [ - serverConnection, initialPrompt, createSession, - appendAgent, handleToolCall, handleToolCallUpdate, exit, + serverConnection, + initialPrompt, + createSession, + appendAgent, + handleToolCall, + handleToolCallUpdate, + exit, ]); const handleSubmit = useCallback( @@ -800,86 +904,118 @@ function App({ [loading, sendPrompt], ); - useInput((ch, key) => { - if (key.escape || (ch === "c" && key.ctrl)) { - if (pendingPermission) { resolvePermission("cancelled"); return; } - if (key.escape && pastedFull !== null) return; - exit(); - } + useInput( + (ch, key) => { + if (key.escape || (ch === "c" && key.ctrl)) { + if (pendingPermission) { + resolvePermission("cancelled"); + return; + } + if (key.escape && pastedFull !== null) return; + exit(); + } - if (!loading && !pendingPermission && sessionIdRef.current) { - if (key.ctrl && (ch === "p" || ch === "P")) { setOverlay({ screen: "configure", intent: "provider" }); return; } - if (key.ctrl && (ch === "m" || ch === "M")) { setOverlay({ screen: "configure", intent: "model" }); return; } - if (key.ctrl && (ch === "e" || ch === "E")) { setOverlay({ screen: "extensions" }); return; } - if (ch === "g" && key.ctrl) { setOverlay({ screen: "configure", intent: "provider" }); return; } - } + if (!loading && !pendingPermission && sessionIdRef.current) { + if (key.ctrl && (ch === "p" || ch === "P")) { + setOverlay({ screen: "configure", intent: "provider" }); + return; + } + if (key.ctrl && (ch === "m" || ch === "M")) { + setOverlay({ screen: "configure", intent: "model" }); + return; + } + if (key.ctrl && (ch === "e" || ch === "E")) { + setOverlay({ screen: "extensions" }); + return; + } + if (ch === "g" && key.ctrl) { + setOverlay({ screen: "configure", intent: "provider" }); + return; + } + } - 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 sel = opts[permissionIdx]; - if (sel) resolvePermission({ optionId: sel.optionId }); + 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 sel = opts[permissionIdx]; + if (sel) resolvePermission({ optionId: sel.optionId }); + return; + } + const keyMap: Record = { + y: "allow_once", + a: "allow_always", + n: "reject_once", + N: "reject_always", + }; + const kind = keyMap[ch]; + if (kind) { + const m = opts.find((o) => o.kind === kind); + if (m) resolvePermission({ optionId: m.optionId }); + } return; } - const keyMap: Record = { - y: "allow_once", a: "allow_always", n: "reject_once", N: "reject_always", - }; - const kind = keyMap[ch]; - if (kind) { - const m = opts.find((o) => o.kind === kind); - if (m) resolvePermission({ optionId: m.optionId }); + + const viewingHistory = + viewTurnIdx !== -1 && viewTurnIdx < turns.length - 1; + const multilineOwnsArrows = + !pendingPermission && + !initialPrompt && + !viewingHistory && + pastedFull === null; + + if (key.tab) { + const idx = viewTurnIdx === -1 ? turns.length - 1 : viewTurnIdx; + const t = turns[idx]; + if (t && t.responseItems.some((it) => it.itemType === "tool_call")) { + setToolCallsExpanded((prev) => !prev); + } + return; } - return; - } - const viewingHistory = viewTurnIdx !== -1 && viewTurnIdx < turns.length - 1; - const multilineOwnsArrows = - !pendingPermission && !initialPrompt && !viewingHistory && pastedFull === null; - - if (key.tab) { - const idx = viewTurnIdx === -1 ? turns.length - 1 : viewTurnIdx; - const t = turns[idx]; - if (t && t.responseItems.some((it) => it.itemType === "tool_call")) { - setToolCallsExpanded((prev) => !prev); + if (key.upArrow && !key.shift) { + if (!multilineOwnsArrows) setScrollOffset((prev) => prev + 3); + return; + } + if (key.downArrow && !key.shift) { + if (!multilineOwnsArrows) + setScrollOffset((prev) => Math.max(prev - 3, 0)); + return; } - return; - } - if (key.upArrow && !key.shift) { - if (!multilineOwnsArrows) setScrollOffset((prev) => prev + 3); - return; - } - if (key.downArrow && !key.shift) { - if (!multilineOwnsArrows) setScrollOffset((prev) => Math.max(prev - 3, 0)); - return; - } - - if (key.upArrow && key.shift) { - setTurns((cur) => { - if (cur.length <= 1) return cur; - setViewTurnIdx((prev) => { - const eff = prev === -1 ? cur.length - 1 : prev; - return Math.max(eff - 1, 0); + if (key.upArrow && key.shift) { + setTurns((cur) => { + if (cur.length <= 1) return cur; + setViewTurnIdx((prev) => { + const eff = prev === -1 ? cur.length - 1 : prev; + return Math.max(eff - 1, 0); + }); + return cur; }); - return cur; - }); - return; - } - if (key.downArrow && key.shift) { - setTurns((cur) => { - if (cur.length <= 1) return cur; - setViewTurnIdx((prev) => { - if (prev === -1) return -1; - const next = prev + 1; - return next >= cur.length ? -1 : next; + return; + } + if (key.downArrow && key.shift) { + setTurns((cur) => { + if (cur.length <= 1) return cur; + setViewTurnIdx((prev) => { + if (prev === -1) return -1; + const next = prev + 1; + return next >= cur.length ? -1 : next; + }); + return cur; }); - return cur; - }); - return; - } - }, { isActive: !needsOnboarding && !overlay }); + return; + } + }, + { isActive: !needsOnboarding && !overlay }, + ); const PAD_X = 2; const PAD_Y = 1; @@ -891,7 +1027,8 @@ function App({ const currentTurn = turns[effectiveTurnIdx]; const isViewingHistory = viewTurnIdx !== -1 && viewTurnIdx < turns.length - 1; const isLatest = !isViewingHistory; - const showInputBar = !pendingPermission && !initialPrompt && !isViewingHistory; + const showInputBar = + !pendingPermission && !initialPrompt && !isViewingHistory; const headerH = 2; const isPasteMode = pastedFull !== null; @@ -924,11 +1061,7 @@ function App({ if (needsOnboarding && clientRef.current) { return ( - + + { setOverlay(null); setStatus("ready"); }} + onComplete={() => { + setOverlay(null); + setStatus("ready"); + }} onCancel={() => setOverlay(null)} initialIntent={intent} /> @@ -957,7 +1097,11 @@ function App({ ); } else if (overlay.screen === "extensions") { return ( - + | null = null; async function runTextMode(serverConnection: Stream | string, prompt: string) { @@ -1077,7 +1219,9 @@ async function runTextMode(serverConnection: Stream | string, prompt: string) { params: RequestPermissionRequest, ): Promise => { // Auto-reject in text mode - const rejectOption = params.options.find(o => o.kind === "reject_once"); + const rejectOption = params.options.find( + (o) => o.kind === "reject_once", + ); if (rejectOption) { return { outcome: { outcome: "selected", optionId: rejectOption.optionId }, @@ -1119,29 +1263,7 @@ async function main() { if (cli.flags.server) { serverConnection = cli.flags.server; } else { - const binary = (() => { - const __dirname = dirname(fileURLToPath(import.meta.url)); - const candidates = [ - join(__dirname, "..", "server-binary.json"), - join(__dirname, "server-binary.json"), - ]; - for (const candidate of candidates) { - try { - const data = JSON.parse(readFileSync(candidate, "utf-8")); - return data.binaryPath ?? null; - } catch { - // not found here, try next - } - } - return null; - })(); - if (!binary) { - console.error( - "No goose binary found. Use --server or install the native package.", - ); - process.exit(1); - } - + const binary = resolveGooseBinary(); serverProcess = spawn(binary, ["acp"], { stdio: ["pipe", "pipe", "ignore"], detached: false, @@ -1152,8 +1274,12 @@ async function main() { process.exit(1); }); - const output = Writable.toWeb(serverProcess.stdin!) as WritableStream; - const input = Readable.toWeb(serverProcess.stdout!) as ReadableStream; + const output = Writable.toWeb( + serverProcess.stdin!, + ) as WritableStream; + const input = Readable.toWeb( + serverProcess.stdout!, + ) as ReadableStream; serverConnection = ndJsonStream(output, input); } @@ -1180,8 +1306,14 @@ function cleanup() { } process.on("exit", cleanup); -process.on("SIGINT", () => { cleanup(); process.exit(0); }); -process.on("SIGTERM", () => { cleanup(); process.exit(0); }); +process.on("SIGINT", () => { + cleanup(); + process.exit(0); +}); +process.on("SIGTERM", () => { + cleanup(); + process.exit(0); +}); main().catch((err) => { console.error(err);