diff --git a/docs/porting-from-pi-mono.md b/docs/porting-from-pi-mono.md index f4fad5451..567f3440c 100644 --- a/docs/porting-from-pi-mono.md +++ b/docs/porting-from-pi-mono.md @@ -64,7 +64,7 @@ We run on Bun. Replace Node APIs only when Bun provides a better alternative. **DO NOT replace (these work fine in Bun):** -- `os.homedir()` — do NOT replace with `process.env.HOME`, `Bun.env.HOME`, or literal `"~"` +- `os.homedir()` — do NOT replace with `Bun.env.HOME`, `Bun.env.HOME`, or literal `"~"` - `os.tmpdir()` — do NOT replace with `Bun.env.TMPDIR || "/tmp"` or hardcoded paths - `fs.mkdtempSync()` — do NOT replace with manual path construction - `path.join()`, `path.resolve()`, etc. — these are fine @@ -82,7 +82,7 @@ We run on Bun. Replace Node APIs only when Bun provides a better alternative. ```typescript // BROKEN: env vars may be undefined, "~" is not expanded -const home = process.env.HOME || Bun.env.HOME || "~"; +const home = Bun.env.HOME || Bun.env.HOME || "~"; const tmp = Bun.env.TMPDIR || "/tmp"; ``` diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4a98f57e7..699827f40 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -3,9 +3,10 @@ ## [Unreleased] ## [11.0.0] - 2026-02-05 + ### Changed -- Replaced direct `process.env` access with `getEnv()` utility from `@oh-my-pi/pi-utils` for consistent environment variable handling across all providers +- Replaced direct `Bun.env` access with `getEnv()` utility from `@oh-my-pi/pi-utils` for consistent environment variable handling across all providers - Updated environment variable names from `OMP_*` prefix to `PI_*` prefix for consistency (e.g., `OMP_CODING_AGENT_DIR` → `PI_CODING_AGENT_DIR`) ### Removed @@ -13,6 +14,7 @@ - Removed automatic environment variable migration from `PI_*` to `OMP_*` prefixes via `migrate-env.ts` module ## [10.5.0] - 2026-02-04 + ### Changed - Updated @anthropic-ai/sdk to ^0.72.1 @@ -28,18 +30,20 @@ - Removed undici dependency ## [9.4.0] - 2026-01-31 + ### Added -- Added `getEnv()` function to retrieve environment variables from process.env, cwd/.env, or ~/.env +- Added `getEnv()` function to retrieve environment variables from Bun.env, cwd/.env, or ~/.env - Added support for reading .env files from home directory and current working directory - Added support for `exa` and `perplexity` as known providers in `getEnvApiKey()` ### Changed -- Changed `getEnvApiKey()` to check process.env, cwd/.env, and ~/.env files in order of precedence +- Changed `getEnvApiKey()` to check Bun.env, cwd/.env, and ~/.env files in order of precedence - Refactored provider API key resolution to use a declarative service provider map ## [9.2.2] - 2026-01-31 + ### Added - Added OpenCode Zen provider with API key authentication for accessing multiple AI models @@ -86,31 +90,40 @@ ## [8.6.0] - 2026-01-27 ### Changed + - Replaced JSON5 dependency with Bun.JSON5 parsing ### Fixed + - Filtered empty user text blocks for OpenAI-compatible completions and normalized Kimi reasoning_content for OpenRouter tool-call messages ## [8.4.0] - 2026-01-25 ### Added + - Added Azure OpenAI Responses provider with deployment mapping and resource-based base URL support ### Changed + - Added OpenRouter routing preferences for OpenAI-compatible completions ### Fixed + - Defaulted Google tool call arguments to empty objects when providers omit args - Guarded Responses/Codex streaming deltas against missing content parts and handled arguments.done events ## [8.2.1] - 2026-01-24 ### Fixed + - Fixed handling of streaming function call arguments in OpenAI responses to properly parse arguments when sent via `response.function_call_arguments.done` events + ## [8.2.0] - 2026-01-24 ### Changed + - Migrated node module imports from named to namespace imports across all packages for consistency with project guidelines + ## [8.0.0] - 2026-01-23 ### Fixed @@ -119,6 +132,7 @@ - Fixed 400 errors when reading multiple images via GitHub Copilot's Claude models. Claude requires tool_use -> tool_result adjacency with no user messages interleaved. Images from consecutive tool results are now batched into a single user message ## [7.0.0] - 2026-01-21 + ### Added - Added usage tracking system with normalized schema for provider quota/limit endpoints @@ -152,12 +166,14 @@ - Fixed Gemini CLI usage windows to consistently label quota windows for display suppression ## [6.9.69] - 2026-01-21 + ### Added - Added duration and time-to-first-token (ttft) metrics to all AI provider responses - Added performance tracking for streaming responses across all providers ## [6.9.0] - 2026-01-21 + ### Removed - Removed openai-codex provider exports from main package index @@ -165,6 +181,7 @@ - Removed vitest configuration file ## [6.8.4] - 2026-01-21 + ### Changed - Updated prompt caching strategy to follow Anthropic's recommended hierarchy @@ -173,6 +190,7 @@ - Increased OAuth callback timeout from 120 seconds to 120,000 milliseconds ## [6.8.3] - 2026-01-21 + ### Added - Added `headers` option to all providers for custom request headers @@ -197,6 +215,7 @@ - Enhanced AWS credential detection to support ECS task roles and IRSA web identity tokens ## [6.8.2] - 2026-01-21 + ### Fixed - Improved error handling for aborted requests in Google Gemini CLI provider @@ -238,6 +257,7 @@ - Fixed token refresh for all OAuth providers ## [6.7.670] - 2026-01-19 + ### Changed - Updated Claude Code compatibility headers and version @@ -247,16 +267,19 @@ - Updated PKCE verifier generation for better security ## [5.7.67] - 2026-01-18 + ### Fixed - Added error handling for unknown OAuth providers ## [5.6.77] - 2026-01-18 + ### Fixed - Prevented duplicate tool results for errored or aborted messages when results already exist ## [5.6.7] - 2026-01-18 + ### Added - Added automatic retry logic for OpenAI Codex responses with configurable delay and max retries @@ -265,8 +288,8 @@ ### Changed -- Updated environment variable prefix from PI_ to OMP_ for better consistency -- Added automatic migration for legacy PI_ environment variables to OMP_ equivalents +- Updated environment variable prefix from PI* to OMP* for better consistency +- Added automatic migration for legacy PI* environment variables to OMP* equivalents - Adjusted Bedrock Claude thinking budgets to reserve output tokens when maxTokens is too low ### Fixed @@ -281,6 +304,7 @@ - Fixed orphaned tool call handling to skip synthetic results for errored assistant messages ## [5.5.0] - 2026-01-18 + ### Changed - Updated User-Agent header from 'opencode' to 'pi' for OpenAI Codex requests @@ -288,6 +312,7 @@ - Removed bridge text override from Codex system prompt builder ## [5.3.0] - 2026-01-15 + ### Changed - Replaced detailed Codex system instructions with simplified pi assistant instructions @@ -307,6 +332,7 @@ - Fixed numbered list items showing "1." for all items when code blocks break list continuity (via `start` property) ## [5.0.0] - 2026-01-12 + ### Added - Added support for `xhigh` thinking level in `thinkingBudgets` configuration @@ -318,6 +344,7 @@ - Changed `supportsXhigh()` to return true for all Anthropic models ## [4.6.0] - 2026-01-12 + ### Fixed - Fixed incorrect classification of thought signatures in Google Gemini responses—thought signatures are now correctly treated as metadata rather than thinking content indicators @@ -325,22 +352,26 @@ - Fixed Google schema sanitization stripping property names that match schema keywords (e.g., "pattern", "format") from tool definitions ## [4.4.9] - 2026-01-12 + ### Fixed - Fixed Google provider schema sanitization to strip additional unsupported JSON Schema fields (patternProperties, additionalProperties, min/max constraints, pattern, format) ## [4.4.8] - 2026-01-12 + ### Fixed - Fixed Google provider schema sanitization to properly collapse `anyOf`/`oneOf` with const values into enum arrays - Fixed const-to-enum conversion to infer type from the const value when type is not specified ## [4.4.6] - 2026-01-11 + ### Fixed - Fixed tool parameter schema sanitization to only apply Google-specific transformations for Gemini models, preserving original schemas for other model types ## [4.4.5] - 2026-01-11 + ### Changed - Exported `sanitizeSchemaForGoogle` utility function for external use @@ -375,13 +406,14 @@ - Changed Cursor debug logging to use structured JSONL format with automatic MCP argument decoding - Changed MCP tool argument decoding to use protobuf Value schema for improved type handling -- Changed tool advertisement to filter Cursor native tools (bash, read, write, delete, ls, grep, lsp) instead of only exposing mcp_ prefixed tools +- Changed tool advertisement to filter Cursor native tools (bash, read, write, delete, ls, grep, lsp) instead of only exposing mcp\_ prefixed tools ### Fixed - Fixed Cursor conversation history serialization so subagents retain task context and can call complete ## [4.2.1] - 2026-01-11 + ### Changed - Updated `reasoningSummary` option to accept only `"auto"`, `"concise"`, `"detailed"`, or `null` (removed `"off"` and `"on"` values) @@ -393,6 +425,7 @@ - Fixed Cloud Code Assist tool schema conversion to avoid unsupported `const` fields ## [4.0.0] - 2026-01-10 + ### Added - Added `betas` option in `AnthropicOptions` for passing custom Anthropic beta feature flags @@ -434,6 +467,7 @@ - Crash when pasting text with trailing whitespace exceeding terminal width ## [3.37.1] - 2026-01-10 + ### Added - Added automatic type coercion for tool arguments when LLMs return JSON-encoded strings instead of native types (numbers, booleans, arrays, objects) @@ -444,11 +478,13 @@ - Changed validation error messages to include both original and normalized arguments when coercion was attempted ## [3.37.0] - 2026-01-10 + ### Changed - Enabled type coercion in JSON schema validation to automatically convert compatible types ## [3.35.0] - 2026-01-09 + ### Added - Enhanced error messages to include retry-after timing information from API rate limit headers @@ -729,4 +765,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_ ## [0.9.4] - 2025-11-26 -Initial release with multi-provider LLM support. \ No newline at end of file +Initial release with multi-provider LLM support. diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 624f97bf6..3b9985cfc 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -105,7 +105,7 @@ const serviceProviderMap: Record = { * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY. * * Will not return API keys for providers that require OAuth tokens. - * Checks process.env, then cwd/.env, then ~/.env. + * Checks Bun.env, then cwd/.env, then ~/.env. */ export function getEnvApiKey(provider: string): string | undefined { const resolver = serviceProviderMap[provider]; diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 1b7b32373..de4c5ad6f 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -100,7 +100,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic (API Key)", () => { it("claude-3-5-haiku - should detect overflow via isContextOverflow", async () => { const model = getModel("anthropic", "claude-3-5-haiku-20241022"); - const result = await testContextOverflow(model, process.env.ANTHROPIC_API_KEY!); + const result = await testContextOverflow(model, Bun.env.ANTHROPIC_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -167,7 +167,7 @@ describe("Context overflow error handling", () => { it("gpt-4o-mini - should detect overflow via isContextOverflow", async () => { const model = { ...getModel("openai", "gpt-4o-mini") }; model.api = "openai-completions" as any; - const result = await testContextOverflow(model, process.env.OPENAI_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENAI_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -179,7 +179,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses", () => { it("gpt-4o - should detect overflow via isContextOverflow", async () => { const model = getModel("openai", "gpt-4o"); - const result = await testContextOverflow(model, process.env.OPENAI_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENAI_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -196,7 +196,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("GEMINI_API_KEY"))("Google", () => { it("gemini-2.0-flash - should detect overflow via isContextOverflow", async () => { const model = getModel("google", "gemini-2.0-flash"); - const result = await testContextOverflow(model, process.env.GEMINI_API_KEY!); + const result = await testContextOverflow(model, Bun.env.GEMINI_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -292,7 +292,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI", () => { it("grok-3-fast - should detect overflow via isContextOverflow", async () => { const model = getModel("xai", "grok-3-fast"); - const result = await testContextOverflow(model, process.env.XAI_API_KEY!); + const result = await testContextOverflow(model, Bun.env.XAI_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -309,7 +309,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("GROQ_API_KEY"))("Groq", () => { it("llama-3.3-70b-versatile - should detect overflow via isContextOverflow", async () => { const model = getModel("groq", "llama-3.3-70b-versatile"); - const result = await testContextOverflow(model, process.env.GROQ_API_KEY!); + const result = await testContextOverflow(model, Bun.env.GROQ_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -326,7 +326,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("CEREBRAS_API_KEY"))("Cerebras", () => { it("qwen-3-235b - should detect overflow via isContextOverflow", async () => { const model = getModel("cerebras", "qwen-3-235b-a22b-instruct-2507"); - const result = await testContextOverflow(model, process.env.CEREBRAS_API_KEY!); + const result = await testContextOverflow(model, Bun.env.CEREBRAS_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -345,7 +345,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("ZAI_API_KEY"))("z.ai", () => { it("glm-4.5-flash - should detect overflow via isContextOverflow (silent overflow or rate limit)", async () => { const model = getModel("zai", "glm-4.5-flash"); - const result = await testContextOverflow(model, process.env.ZAI_API_KEY!); + const result = await testContextOverflow(model, Bun.env.ZAI_API_KEY!); logResult(result); // z.ai behavior is inconsistent: @@ -371,7 +371,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!e2eApiKey("MISTRAL_API_KEY"))("Mistral", () => { it("devstral-medium-latest - should detect overflow via isContextOverflow", async () => { const model = getModel("mistral", "devstral-medium-latest"); - const result = await testContextOverflow(model, process.env.MISTRAL_API_KEY!); + const result = await testContextOverflow(model, Bun.env.MISTRAL_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -388,7 +388,7 @@ describe("Context overflow error handling", () => { // Anthropic backend it("anthropic/claude-sonnet-4 via OpenRouter - should detect overflow via isContextOverflow", async () => { const model = getModel("openrouter", "anthropic/claude-sonnet-4"); - const result = await testContextOverflow(model, process.env.OPENROUTER_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENROUTER_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -399,7 +399,7 @@ describe("Context overflow error handling", () => { // DeepSeek backend it("deepseek/deepseek-v3.2 via OpenRouter - should detect overflow via isContextOverflow", async () => { const model = getModel("openrouter", "deepseek/deepseek-v3.2"); - const result = await testContextOverflow(model, process.env.OPENROUTER_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENROUTER_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -410,7 +410,7 @@ describe("Context overflow error handling", () => { // Mistral backend it("mistralai/mistral-large-2512 via OpenRouter - should detect overflow via isContextOverflow", async () => { const model = getModel("openrouter", "mistralai/mistral-large-2512"); - const result = await testContextOverflow(model, process.env.OPENROUTER_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENROUTER_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -421,7 +421,7 @@ describe("Context overflow error handling", () => { // Google backend it("google/gemini-2.5-flash via OpenRouter - should detect overflow via isContextOverflow", async () => { const model = getModel("openrouter", "google/gemini-2.5-flash"); - const result = await testContextOverflow(model, process.env.OPENROUTER_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENROUTER_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -432,7 +432,7 @@ describe("Context overflow error handling", () => { // Meta/Llama backend it("meta-llama/llama-4-maverick via OpenRouter - should detect overflow via isContextOverflow", async () => { const model = getModel("openrouter", "meta-llama/llama-4-maverick"); - const result = await testContextOverflow(model, process.env.OPENROUTER_API_KEY!); + const result = await testContextOverflow(model, Bun.env.OPENROUTER_API_KEY!); logResult(result); expect(result.stopReason).toBe("error"); @@ -447,7 +447,7 @@ describe("Context overflow error handling", () => { // Check if ollama is installed and local LLM tests are enabled let ollamaInstalled = false; - if (!process.env.PI_NO_LOCAL_LLM) { + if (!Bun.env.PI_NO_LOCAL_LLM) { try { execSync("which ollama", { stdio: "ignore" }); ollamaInstalled = true; @@ -541,7 +541,7 @@ describe("Context overflow error handling", () => { // ============================================================================= let lmStudioRunning = false; - if (!process.env.PI_NO_LOCAL_LLM) { + if (!Bun.env.PI_NO_LOCAL_LLM) { try { execSync("curl -s --max-time 1 http://localhost:1234/v1/models > /dev/null", { stdio: "ignore" }); lmStudioRunning = true; diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index cfa81da87..c7a76c4bc 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -17,7 +17,7 @@ import { isEnoent } from "@oh-my-pi/pi-utils"; * E2E tests require explicit opt-in via E2E=1 environment variable. * This prevents accidental API calls when keys happen to be in the environment. */ -export const E2E_ENABLED = process.env.E2E === "1" || process.env.E2E === "true"; +export const E2E_ENABLED = Bun.env.E2E === "1" || Bun.env.E2E === "true"; /** * Get an API key from environment, but only if E2E tests are enabled. @@ -25,7 +25,7 @@ export const E2E_ENABLED = process.env.E2E === "1" || process.env.E2E === "true" */ export function e2eApiKey(envVar: string): string | undefined { if (!E2E_ENABLED) return undefined; - return process.env[envVar]; + return Bun.env[envVar]; } const AUTH_PATH = path.join(os.homedir(), ".pi", "agent", "auth.json"); diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index f8dd88f53..8bc4eeda9 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -4,14 +4,14 @@ import type { Context, Model } from "@oh-my-pi/pi-ai/types"; import { TempDir } from "@oh-my-pi/pi-utils"; const originalFetch = global.fetch; -const originalAgentDir = process.env.PI_CODING_AGENT_DIR; +const originalAgentDir = Bun.env.PI_CODING_AGENT_DIR; afterEach(() => { global.fetch = originalFetch; if (originalAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; + delete Bun.env.PI_CODING_AGENT_DIR; } else { - process.env.PI_CODING_AGENT_DIR = originalAgentDir; + Bun.env.PI_CODING_AGENT_DIR = originalAgentDir; } vi.restoreAllMocks(); }); @@ -19,7 +19,7 @@ afterEach(() => { describe("openai-codex streaming", () => { it("streams SSE responses into AssistantMessageEventStream", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.PI_CODING_AGENT_DIR = tempDir.path(); + Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -130,7 +130,7 @@ describe("openai-codex streaming", () => { it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.PI_CODING_AGENT_DIR = tempDir.path(); + Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -231,7 +231,7 @@ describe("openai-codex streaming", () => { it("does not set conversation_id/session_id headers when sessionId is not provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.PI_CODING_AGENT_DIR = tempDir.path(); + Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index b7cca2bf0..239a7e9ea 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -392,8 +392,8 @@ describe("Generate E2E Tests", () => { }); describe("Google Vertex Provider (gemini-3-flash-preview)", () => { - const vertexProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; - const vertexLocation = process.env.GOOGLE_CLOUD_LOCATION; + const vertexProject = Bun.env.GOOGLE_CLOUD_PROJECT || Bun.env.GCLOUD_PROJECT; + const vertexLocation = Bun.env.GOOGLE_CLOUD_LOCATION; const isVertexConfigured = Boolean(vertexProject && vertexLocation); const vertexOptions = { project: vertexProject, location: vertexLocation } as const; const llm = getModel("google-vertex", "gemini-3-flash-preview"); @@ -1350,7 +1350,7 @@ describe("Generate E2E Tests", () => { // Check if ollama is installed and local LLM tests are enabled let ollamaInstalled = false; - if (!process.env.PI_NO_LOCAL_LLM) { + if (!Bun.env.PI_NO_LOCAL_LLM) { try { execSync("which ollama", { stdio: "ignore" }); ollamaInstalled = true; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index cf4627dfb..5882d359b 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -106,7 +106,7 @@ describe("totalTokens field", () => { const llm = getModel("anthropic", "claude-3-5-haiku-20241022"); console.log(`\nAnthropic / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.ANTHROPIC_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.ANTHROPIC_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -224,7 +224,7 @@ describe("totalTokens field", () => { const llm = getModel("xai", "grok-3-fast"); console.log(`\nxAI / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.XAI_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.XAI_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -247,7 +247,7 @@ describe("totalTokens field", () => { const llm = getModel("groq", "openai/gpt-oss-120b"); console.log(`\nGroq / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.GROQ_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.GROQ_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -270,7 +270,7 @@ describe("totalTokens field", () => { const llm = getModel("cerebras", "gpt-oss-120b"); console.log(`\nCerebras / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.CEREBRAS_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.CEREBRAS_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -293,7 +293,7 @@ describe("totalTokens field", () => { const llm = getModel("zai", "glm-4.5-flash"); console.log(`\nz.ai / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.ZAI_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.ZAI_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -316,7 +316,7 @@ describe("totalTokens field", () => { const llm = getModel("mistral", "devstral-medium-latest"); console.log(`\nMistral / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.MISTRAL_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.MISTRAL_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -339,7 +339,7 @@ describe("totalTokens field", () => { const llm = getModel("openrouter", "anthropic/claude-sonnet-4"); console.log(`\nOpenRouter / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.OPENROUTER_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.OPENROUTER_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -356,7 +356,7 @@ describe("totalTokens field", () => { const llm = getModel("openrouter", "deepseek/deepseek-chat"); console.log(`\nOpenRouter / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.OPENROUTER_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.OPENROUTER_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -373,7 +373,7 @@ describe("totalTokens field", () => { const llm = getModel("openrouter", "mistralai/mistral-small-3.1-24b-instruct"); console.log(`\nOpenRouter / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.OPENROUTER_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.OPENROUTER_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -390,7 +390,7 @@ describe("totalTokens field", () => { const llm = getModel("openrouter", "google/gemini-2.0-flash-001"); console.log(`\nOpenRouter / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.OPENROUTER_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.OPENROUTER_API_KEY }); logUsage("First request", first); logUsage("Second request", second); @@ -407,7 +407,7 @@ describe("totalTokens field", () => { const llm = getModel("openrouter", "meta-llama/llama-4-maverick"); console.log(`\nOpenRouter / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.OPENROUTER_API_KEY }); + const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.OPENROUTER_API_KEY }); logUsage("First request", first); logUsage("Second request", second); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 60334b2ef..f5a75706c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,26 @@ # Changelog ## [Unreleased] +### Added + +- Added comprehensive environment variables reference documentation at `docs/environment-variables.md` covering API keys, configuration, debugging, and testing variables +- Added theme system with 44 customizable color tokens, two built-in themes (dark/light), and auto-detection based on terminal background +- Added `/theme` command to interactively select and switch between themes +- Added support for custom themes in `~/.pi/agent/themes/*.json` with live editing - changes apply immediately when files are saved +- Added `userMessageText` theme token for customizing user message text color +- Added `toolTitle` and `toolOutput` theme tokens for separate coloring of tool execution box titles and output + +### Changed + +- Changed all environment variable access from `process.env` to `Bun.env` throughout the codebase for Bun runtime compatibility +- Updated documentation to reference `Bun.env` instead of `process.env` in examples and comments + +### Fixed + +- Fixed `Text` component to properly implement `invalidate()` method, ensuring theme changes apply correctly to all UI elements +- Fixed `TruncatedText` component to properly pad all lines to exactly match the specified width, preventing rendering artifacts +- Fixed `TruncatedText` component to stop at the first newline and only display the first line +- Fixed invalid or malformed themes to fall back gracefully to dark theme instead of crashing the application ## [11.0.2] - 2026-02-05 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index ad1047066..75950b4f8 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -368,7 +368,7 @@ When disabled, neither case triggers automatic compaction (use `/compact` manual **Environment Variables (`env`):** - Automatically sets environment variables when the application starts -- Only sets variables that aren't already present in `process.env` +- Only sets variables that aren't already present in `Bun.env` - Supports any environment variable, not just API keys - Order of precedence: existing env vars > settings.json env vars > auth.json env vars diff --git a/packages/coding-agent/docs/environment-variables.md b/packages/coding-agent/docs/environment-variables.md new file mode 100644 index 000000000..4bea134ed --- /dev/null +++ b/packages/coding-agent/docs/environment-variables.md @@ -0,0 +1,246 @@ +# Environment Variables Reference + +This document lists all environment variables used by the coding agent. + +## API Keys + +### Multi-Provider Support + +| Variable | Description | Default | +|----------|-------------|---------| +| `OPENAI_API_KEY` | OpenAI API key for GPT models | - | +| `ANTHROPIC_API_KEY` | Anthropic API key for Claude models | - | +| `ANTHROPIC_OAUTH_TOKEN` | Anthropic OAuth token (takes precedence over `ANTHROPIC_API_KEY`) | - | +| `GOOGLE_API_KEY` | Google Gemini API key | - | +| `COPILOT_GITHUB_TOKEN` | GitHub Copilot personal access token | `$GH_TOKEN` or `$GITHUB_TOKEN` | +| `GH_TOKEN` | GitHub CLI token (fallback for Copilot) | - | +| `GITHUB_TOKEN` | GitHub token (fallback for Copilot and API access) | - | +| `EXA_API_KEY` | Exa search API key | - | + +### Provider-Specific Configuration + +#### AWS Bedrock + +| Variable | Description | Default | +|----------|-------------|---------| +| `AWS_REGION` | AWS region for Bedrock | `$AWS_DEFAULT_REGION` or `us-east-1` | +| `AWS_DEFAULT_REGION` | AWS default region | - | +| `AWS_PROFILE` | AWS CLI profile name | - | +| `AWS_ACCESS_KEY_ID` | AWS access key ID | - | +| `AWS_SECRET_ACCESS_KEY` | AWS secret access key | - | +| `AWS_BEARER_TOKEN_BEDROCK` | AWS bearer token for Bedrock | - | +| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` | ECS container credentials URI | - | +| `AWS_CONTAINER_CREDENTIALS_FULL_URI` | ECS container credentials full URI | - | +| `AWS_WEB_IDENTITY_TOKEN_FILE` | Web identity token file path | - | +| `AWS_ROLE_ARN` | IAM role ARN for web identity | - | + +#### Azure OpenAI + +| Variable | Description | Default | +|----------|-------------|---------| +| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key (required) | - | +| `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version | `2024-10-01-preview` | +| `AZURE_OPENAI_BASE_URL` | Azure OpenAI base URL | Constructed from resource name | +| `AZURE_OPENAI_RESOURCE_NAME` | Azure OpenAI resource name | - | +| `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` | JSON map of model IDs to deployment names | `{}` | + +#### Google Cloud (Vertex AI) + +| Variable | Description | Default | +|----------|-------------|---------| +| `GOOGLE_CLOUD_PROJECT` | Google Cloud project ID (required for Vertex AI) | `$GCLOUD_PROJECT` | +| `GCLOUD_PROJECT` | Google Cloud project ID (alternative) | - | +| `GOOGLE_CLOUD_PROJECT_ID` | Google Cloud project ID (used during OAuth discovery) | `$GOOGLE_CLOUD_PROJECT` | +| `GOOGLE_CLOUD_LOCATION` | Google Cloud location (required for Vertex AI) | - | +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to Google service account JSON | - | + +#### Anthropic Search & Custom Base URLs + +| Variable | Description | Default | +|----------|-------------|---------| +| `ANTHROPIC_BASE_URL` | Custom Anthropic API base URL | - | +| `ANTHROPIC_SEARCH_API_KEY` | API key for Anthropic search (separate from main API) | - | +| `ANTHROPIC_SEARCH_BASE_URL` | Custom base URL for Anthropic search | - | +| `ANTHROPIC_SEARCH_MODEL` | Model to use for web search | `claude-sonnet-4` | + +#### Kimi + +| Variable | Description | Default | +|----------|-------------|---------| +| `KIMI_CODE_BASE_URL` | Kimi Code API base URL | `https://kimi.moonshot.cn` | +| `KIMI_CODE_OAUTH_HOST` | Kimi Code OAuth host | `$KIMI_OAUTH_HOST` or `https://kimi.moonshot.cn` | +| `KIMI_OAUTH_HOST` | Kimi OAuth host (fallback) | - | + +## Model Configuration + +### Model Role Overrides + +Override model roles via environment variables (ephemeral, not persisted): + +| Variable | Description | CLI Flag | +|----------|-------------|----------| +| `PI_SMOL_MODEL` | Fast model for lightweight tasks | `--smol` | +| `PI_SLOW_MODEL` | Reasoning model for thorough analysis | `--slow` | +| `PI_PLAN_MODEL` | Model for architectural planning | `--plan` | + +## Agent Configuration + +### Core Settings + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_CODING_AGENT_DIR` | Directory for agent data (sessions, auth, cache) | `~/.omp/agent` | +| `PI_SUBPROCESS_CMD` | Custom command for spawning subagents | Auto-detected | +| `PI_NO_TITLE` | Disable automatic session title generation | `false` | +| `NULL_PROMPT` | Use empty system prompt (testing) | `false` | +| `PI_BLOCKED_AGENT` | Override agent type in task tool | - | + +### Caching + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_CACHE_RETENTION` | Prompt cache retention (`long` = 24h for OpenAI) | - | + +## Python Configuration + +### Python Kernel + +| Variable | Description | Default | +|----------|-------------|---------| +| `VIRTUAL_ENV` | Python virtual environment path | Auto-detected (`.venv` or `venv`) | +| `PI_PY` | Python tool mode (`per-session`, `per-call`, `off`) | `per-session` | +| `PI_PYTHON_SKIP_CHECK` | Skip Python availability check (testing) | `false` | + +### External Python Gateway + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_PYTHON_GATEWAY_URL` | External Python gateway URL | - | +| `PI_PYTHON_GATEWAY_TOKEN` | Authentication token for gateway | - | + +### Debugging + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_PYTHON_IPC_TRACE` | Trace Python IPC messages (`1` = enabled) | `false` | + +## Task & Subagent Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_TASK_MAX_OUTPUT_BYTES` | Maximum output bytes per subagent | `500000` | +| `PI_TASK_MAX_OUTPUT_LINES` | Maximum output lines per subagent | `5000` | + +## TUI & Terminal Configuration + +### Terminal Capabilities + +| Variable | Description | Auto-Detected | +|----------|-------------|---------------| +| `COLORTERM` | Terminal color support (`truecolor`, `24bit`) | Yes | +| `COLORFGBG` | Terminal foreground/background colors | Yes | +| `TERM` | Terminal type | Yes | +| `TERM_PROGRAM` | Terminal program name | Yes | +| `TERM_PROGRAM_VERSION` | Terminal program version | Yes | +| `TERMINAL_EMULATOR` | Terminal emulator name | Yes | +| `WT_SESSION` | Windows Terminal session ID | Yes | + +### TUI Behavior + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_NOTIFICATIONS` | Desktop notifications (`off`, `0`, `false` = disabled) | Enabled | +| `PI_TUI_WRITE_LOG` | Log all TUI write operations to file | - | +| `PI_HARDWARE_CURSOR` | Show hardware cursor (`1` = enabled) | `false` | + +## Bash & Shell Configuration + +### Shell Detection + +| Variable | Description | Auto-Detected | +|----------|-------------|---------------| +| `SHELL` | User's default shell | Yes (Unix) | +| `ComSpec` | Command processor | Yes (Windows) | + +### Editor + +| Variable | Description | Fallback | +|----------|-------------|----------| +| `VISUAL` | Visual editor for external editing (Ctrl+G) | `$EDITOR` | +| `EDITOR` | Default text editor | - | + +### Bash Tool Behavior + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_BASH_NO_CI` | Don't set `CI=true` in bash environment | `$CLAUDE_BASH_NO_CI` | +| `CLAUDE_BASH_NO_CI` | Legacy name for `PI_BASH_NO_CI` | - | +| `PI_BASH_NO_LOGIN` | Don't use login shell for bash | `$CLAUDE_BASH_NO_LOGIN` | +| `CLAUDE_BASH_NO_LOGIN` | Legacy name for `PI_BASH_NO_LOGIN` | - | +| `PI_SHELL_PREFIX` | Prefix for bash commands | `$CLAUDE_CODE_SHELL_PREFIX` | +| `CLAUDE_CODE_SHELL_PREFIX` | Legacy name for `PI_SHELL_PREFIX` | - | + +## Desktop Environment Detection + +These are auto-detected for system prompt context: + +| Variable | Purpose | +|----------|---------| +| `KDE_FULL_SESSION` | Detect KDE desktop | +| `XDG_CURRENT_DESKTOP` | Current desktop environment | +| `DESKTOP_SESSION` | Desktop session name | +| `XDG_SESSION_DESKTOP` | XDG desktop session | +| `GDMSESSION` | GDM session type | +| `WINDOWMANAGER` | Window manager name | +| `XDG_CONFIG_HOME` | User config directory | +| `APPDATA` | Windows app data directory | +| `HOME` | User home directory | + +## LSP Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_DISABLE_LSPMUX` | Disable lspmux integration (`1` = disabled) | `false` | + +## Debugging & Development + +### General Debugging + +| Variable | Description | Default | +|----------|-------------|---------| +| `DEBUG` | Enable debug logging | `false` | +| `PI_DEV` | Development mode (verbose native addon loading) | `false` | +| `PI_TIMING` | Log tool factory and operation timings (`1` = enabled) | `false` | + +### Startup Debugging + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_DEBUG_STARTUP` | Print startup stage timings to stderr | `false` | + +### Provider-Specific Debugging + +| Variable | Description | Default | +|----------|-------------|---------| +| `DEBUG_CURSOR` | Cursor provider debug logging (`1` = basic, `2` or `verbose` = detailed) | `false` | +| `DEBUG_CURSOR_LOG` | Path to write Cursor debug log file | - | +| `PI_CODEX_DEBUG` | OpenAI Codex debug logging (`1` or `true` = enabled) | `false` | + +## Commit Message Generation + +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_COMMIT_TEST_FALLBACK` | Force fallback commit generation (testing) | `false` | +| `PI_COMMIT_NO_FALLBACK` | Disable fallback commit generation | `false` | +| `PI_COMMIT_MAP_REDUCE` | Enable/disable map-reduce for large diffs (`false` = disabled) | Enabled | + +## Testing & CI + +These are auto-detected but documented for completeness: + +| Variable | Description | +|----------|-------------| +| `BUN_ENV` | Bun environment (`test` skips certain checks) | +| `NODE_ENV` | Node environment (`test` skips certain checks) | +| `E2E` | Enable end-to-end tests (`1` or `true`) | +| `PI_NO_LOCAL_LLM` | Skip local LLM tests (Ollama, LM Studio) | diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 38b10e3e2..dc0d42f1c 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -87,7 +87,7 @@ interface AgentSession { prompt(text: string, options?: PromptOptions): Promise; sendUserMessage( content: string | (TextContent | ImageContent)[], - options?: { deliverAs?: "steer" | "followUp" }, + options?: { deliverAs?: "steer" | "followUp" } ): Promise; steer(text: string): void; followUp(text: string): void; @@ -106,7 +106,7 @@ interface AgentSession { setThinkingLevel(level: ThinkingLevel): void; cycleModel(direction?: "forward" | "backward"): Promise; cycleRoleModels( - direction?: "forward" | "backward", + direction?: "forward" | "backward" ): Promise<{ model: Model; thinkingLevel: ThinkingLevel; role: ModelRole } | undefined>; cycleThinkingLevel(): ThinkingLevel | undefined; @@ -141,7 +141,7 @@ interface AgentSession { // Compaction compact( customInstructions?: string, - options?: { onComplete?: (result: CompactionResult) => void; onError?: (error: Error) => void }, + options?: { onComplete?: (result: CompactionResult) => void; onError?: (error: Error) => void } ): Promise; abortCompaction(): void; @@ -491,11 +491,7 @@ const { session } = await createAgentSession({ Extensions intercept agent events and can register custom tools/commands. Hooks remain for legacy compatibility. ```typescript -import { - createAgentSession, - discoverExtensions, - type ExtensionFactory, -} from "@oh-my-pi/pi-coding-agent"; +import { createAgentSession, discoverExtensions, type ExtensionFactory } from "@oh-my-pi/pi-coding-agent"; // Inline extension const loggingExtension: ExtensionFactory = (api) => { @@ -878,8 +874,8 @@ import systemPrompt from "./SYSTEM.md" with { type: "text" }; const authStorage = await discoverAuthStorage(); // Runtime API key override (not persisted) -if (process.env.MY_KEY) { - authStorage.setRuntimeApiKey("anthropic", process.env.MY_KEY); +if (Bun.env.MY_KEY) { + authStorage.setRuntimeApiKey("anthropic", Bun.env.MY_KEY); } // Model registry diff --git a/packages/coding-agent/examples/sdk/README.md b/packages/coding-agent/examples/sdk/README.md index 4f7bd1107..a289bdd7e 100644 --- a/packages/coding-agent/examples/sdk/README.md +++ b/packages/coding-agent/examples/sdk/README.md @@ -78,7 +78,7 @@ const { session } = await createAgentSession({ // Full control const customAuth = new AuthStorage("/my/app/auth.json"); -customAuth.setRuntimeApiKey("anthropic", process.env.MY_KEY!); +customAuth.setRuntimeApiKey("anthropic", Bun.env.MY_KEY!); const customRegistry = new ModelRegistry(customAuth); const { session } = await createAgentSession({ diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index cbbecd888..aa18e3fa3 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -150,7 +150,7 @@ function emptyCustomModelsResult(error?: string): CustomModelsResult { * Checks environment variable first, then treats as literal. */ function resolveApiKeyConfig(keyConfig: string): string | undefined { - const envValue = process.env[keyConfig]; + const envValue = Bun.env[keyConfig]; if (envValue) return envValue; return keyConfig; } diff --git a/packages/coding-agent/src/debug/report-bundle.ts b/packages/coding-agent/src/debug/report-bundle.ts index 62f618ac3..7b1ff3658 100644 --- a/packages/coding-agent/src/debug/report-bundle.ts +++ b/packages/coding-agent/src/debug/report-bundle.ts @@ -86,7 +86,7 @@ export async function createReportBundle(options: ReportBundleOptions): Promise< files.push("system.json"); // Sanitized environment - data["env.json"] = JSON.stringify(sanitizeEnv(process.env as Record), null, 2); + data["env.json"] = JSON.stringify(sanitizeEnv(Bun.env as Record), null, 2); files.push("env.json"); // Settings/config diff --git a/packages/coding-agent/src/debug/system-info.ts b/packages/coding-agent/src/debug/system-info.ts index 792c6732c..15b713fdf 100644 --- a/packages/coding-agent/src/debug/system-info.ts +++ b/packages/coding-agent/src/debug/system-info.ts @@ -28,8 +28,8 @@ export async function collectSystemInfo(): Promise { const cpuModel = cpus[0]?.model ?? "Unknown CPU"; // Try to get shell from environment - const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown"; - const terminal = process.env.TERM_PROGRAM ?? process.env.TERM ?? undefined; + const shell = Bun.env.SHELL ?? Bun.env.ComSpec ?? "unknown"; + const terminal = Bun.env.TERM_PROGRAM ?? Bun.env.TERM ?? undefined; return { os: `${os.type()} ${os.release()} (${os.platform()})`, diff --git a/packages/coding-agent/src/discovery/codex.ts b/packages/coding-agent/src/discovery/codex.ts index 39f844811..ddf71c58a 100644 --- a/packages/coding-agent/src/discovery/codex.ts +++ b/packages/coding-agent/src/discovery/codex.ts @@ -160,7 +160,7 @@ function extractMCPServersFromToml(toml: Record): Record = { ...config.env }; if (config.env_vars) { for (const varName of config.env_vars) { - const value = process.env[varName]; + const value = Bun.env[varName]; if (value !== undefined) { env[varName] = value; } @@ -174,14 +174,14 @@ function extractMCPServersFromToml(toml: Record): Record = { ...config.http_headers }; if (config.env_http_headers) { for (const [headerName, envVarName] of Object.entries(config.env_http_headers)) { - const value = process.env[envVarName]; + const value = Bun.env[envVarName]; if (value !== undefined) { headers[headerName] = value; } } } if (config.bearer_token_env_var) { - const token = process.env[config.bearer_token_env_var]; + const token = Bun.env[config.bearer_token_env_var]; if (token) { headers.Authorization = `Bearer ${token}`; } diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index fce187384..f5b7b6d3d 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -294,7 +294,7 @@ export async function loadSkillsFromDir( */ export function expandEnvVars(value: string, extraEnv?: Record): string { return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => { - const envValue = extraEnv?.[varName] ?? process.env[varName]; + const envValue = extraEnv?.[varName] ?? Bun.env[varName]; if (envValue !== undefined) return envValue; if (defaultValue !== undefined) return defaultValue; return `\${${varName}}`; diff --git a/packages/coding-agent/src/exa/mcp-client.ts b/packages/coding-agent/src/exa/mcp-client.ts index caf62afbd..d80b9887d 100644 --- a/packages/coding-agent/src/exa/mcp-client.ts +++ b/packages/coding-agent/src/exa/mcp-client.ts @@ -18,9 +18,9 @@ import type { MCPToolWrapperConfig, } from "./types"; -/** Find EXA_API_KEY from process.env or .env files */ +/** Find EXA_API_KEY from Bun.env or .env files */ export async function findApiKey(): Promise { - // Check process.env first + // Check Bun.env first if ($env.EXA_API_KEY) { return $env.EXA_API_KEY; } diff --git a/packages/coding-agent/src/extensibility/plugins/doctor.ts b/packages/coding-agent/src/extensibility/plugins/doctor.ts index d7e14d493..913ec4052 100644 --- a/packages/coding-agent/src/extensibility/plugins/doctor.ts +++ b/packages/coding-agent/src/extensibility/plugins/doctor.ts @@ -29,7 +29,7 @@ export async function runDoctorChecks(): Promise { ]; for (const key of apiKeys) { - const hasKey = !!process.env[key.name]; + const hasKey = !!Bun.env[key.name]; checks.push({ name: key.name, status: hasKey ? "ok" : "warning", diff --git a/packages/coding-agent/src/ipy/executor.ts b/packages/coding-agent/src/ipy/executor.ts index 06f2cc4e7..eba0c3c22 100644 --- a/packages/coding-agent/src/ipy/executor.ts +++ b/packages/coding-agent/src/ipy/executor.ts @@ -225,7 +225,7 @@ export async function warmPythonEnvironment( useSharedGateway?: boolean, sessionFile?: string, ): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> { - const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test"; + const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test"; let cacheState: PreludeCacheState | null = null; try { debugStartup("warmPython:ensureKernel:start"); diff --git a/packages/coding-agent/src/ipy/kernel.ts b/packages/coding-agent/src/ipy/kernel.ts index 260754077..e9b27140b 100644 --- a/packages/coding-agent/src/ipy/kernel.ts +++ b/packages/coding-agent/src/ipy/kernel.ts @@ -108,7 +108,7 @@ export interface PythonKernelAvailability { } export async function checkPythonKernelAvailability(cwd: string): Promise { - if (process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test" || $env.PI_PYTHON_SKIP_CHECK === "1") { + if (Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test" || $env.PI_PYTHON_SKIP_CHECK === "1") { return { ok: true }; } diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts index 60f79df5f..c6e5657f2 100644 --- a/packages/coding-agent/src/lsp/client.ts +++ b/packages/coding-agent/src/lsp/client.ts @@ -412,7 +412,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT const proc = ptree.spawn([command, ...args], { cwd, stdin: "pipe", - env: env ? { ...process.env, ...env } : undefined, + env: env ? { ...Bun.env, ...env } : undefined, }); const client: LspClient = { diff --git a/packages/coding-agent/src/lsp/lspmux.ts b/packages/coding-agent/src/lsp/lspmux.ts index c18167f2d..3c707ec25 100644 --- a/packages/coding-agent/src/lsp/lspmux.ts +++ b/packages/coding-agent/src/lsp/lspmux.ts @@ -66,11 +66,11 @@ function getConfigPath(): string { const home = os.homedir(); switch (os.platform()) { case "win32": - return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml"); + return path.join(Bun.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml"); case "darwin": return path.join(home, "Library", "Application Support", "lspmux", "config.toml"); default: - return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml"); + return path.join(Bun.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml"); } } diff --git a/packages/coding-agent/src/mcp/transports/stdio.ts b/packages/coding-agent/src/mcp/transports/stdio.ts index 50166f9f7..4bf2321cc 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.ts @@ -47,7 +47,7 @@ export class StdioTransport implements MCPTransport { const args = this.config.args ?? []; const env = { - ...process.env, + ...Bun.env, ...this.config.env, }; diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index eca9a1621..d0d140f2a 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -114,7 +114,7 @@ export class RpcClient { this.process = ptree.spawn(["bun", cliPath, ...args], { cwd: this.options.cwd, - env: { ...process.env, ...this.options.env }, + env: { ...Bun.env, ...this.options.env }, stdin: "pipe", }); diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 7cb90937f..961e31fa7 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -1053,15 +1053,15 @@ type ColorMode = "truecolor" | "256color"; // ============================================================================ function detectColorMode(): ColorMode { - const colorterm = process.env.COLORTERM; + const colorterm = Bun.env.COLORTERM; if (colorterm === "truecolor" || colorterm === "24bit") { return "truecolor"; } // Windows Terminal supports truecolor - if (process.env.WT_SESSION) { + if (Bun.env.WT_SESSION) { return "truecolor"; } - const term = process.env.TERM || ""; + const term = Bun.env.TERM || ""; // Only fall back to 256color for truly limited terminals if (term === "dumb" || term === "" || term === "linux") { return "256color"; @@ -1706,7 +1706,7 @@ export async function getThemeByName(name: string): Promise { } function detectTerminalBackground(): "dark" | "light" { - const colorfgbg = process.env.COLORFGBG || ""; + const colorfgbg = Bun.env.COLORFGBG || ""; if (colorfgbg) { const parts = colorfgbg.split(";"); if (parts.length >= 2) { diff --git a/packages/coding-agent/src/patch/index.ts b/packages/coding-agent/src/patch/index.ts index b670cd58c..f5b824ab8 100644 --- a/packages/coding-agent/src/patch/index.ts +++ b/packages/coding-agent/src/patch/index.ts @@ -218,7 +218,7 @@ export class EditTool implements AgentTool { PI_EDIT_FUZZY: editFuzzy = "auto", PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto", PI_EDIT_VARIANT: envEditVariant = "auto", - } = process.env; + } = Bun.env; this.envEditVariant = envEditVariant; if (envEditVariant !== "replace" && envEditVariant !== "patch" && envEditVariant !== "auto") { diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index b6d1a2749..4d7ac5a96 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -17,7 +17,7 @@ * // Full control * const session = await createAgentSession({ * model: myModel, - * getApiKey: async () => process.env.MY_KEY, + * getApiKey: async () => Bun.env.MY_KEY, * toolNames: ["read", "bash", "edit", "write"], // Filter tools * extensions: [], * skills: [], @@ -566,7 +566,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory { * // Full control * const { session } = await createAgentSession({ * model: myModel, - * getApiKey: async () => process.env.MY_KEY, + * getApiKey: async () => Bun.env.MY_KEY, * systemPrompt: 'You are helpful.', * tools: codingTools({ cwd: process.cwd() }), * skills: [], @@ -814,7 +814,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one if (mcpResult.exaApiKeys.length > 0 && !$env.EXA_API_KEY) { - process.env.EXA_API_KEY = mcpResult.exaApiKeys[0]; + Bun.env.EXA_API_KEY = mcpResult.exaApiKeys[0]; } // Log MCP errors diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 7286aa6e2..da596b30c 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -193,15 +193,15 @@ async function getGpuModel(): Promise { } function getTerminalName(): string { - const termProgram = process.env.TERM_PROGRAM; - const termProgramVersion = process.env.TERM_PROGRAM_VERSION; + const termProgram = Bun.env.TERM_PROGRAM; + const termProgramVersion = Bun.env.TERM_PROGRAM_VERSION; if (termProgram) { return termProgramVersion ? `${termProgram} ${termProgramVersion}` : termProgram; } - if (process.env.WT_SESSION) return "Windows Terminal"; + if (Bun.env.WT_SESSION) return "Windows Terminal"; - const term = firstNonEmpty(process.env.TERM, process.env.COLORTERM, process.env.TERMINAL_EMULATOR); + const term = firstNonEmpty(Bun.env.TERM, Bun.env.COLORTERM, Bun.env.TERMINAL_EMULATOR); return term ?? "unknown"; } @@ -216,12 +216,12 @@ function normalizeDesktopValue(value: string): string { } function getDesktopEnvironment(): string { - if (process.env.KDE_FULL_SESSION === "true") return "KDE"; + if (Bun.env.KDE_FULL_SESSION === "true") return "KDE"; const raw = firstNonEmpty( - process.env.XDG_CURRENT_DESKTOP, - process.env.DESKTOP_SESSION, - process.env.XDG_SESSION_DESKTOP, - process.env.GDMSESSION, + Bun.env.XDG_CURRENT_DESKTOP, + Bun.env.DESKTOP_SESSION, + Bun.env.XDG_SESSION_DESKTOP, + Bun.env.GDMSESSION, ); return raw ? normalizeDesktopValue(raw) : "unknown"; } @@ -252,10 +252,10 @@ function matchKnownWindowManager(value: string): string | null { } function getWindowManager(): string { - const explicit = firstNonEmpty(process.env.WINDOWMANAGER); + const explicit = firstNonEmpty(Bun.env.WINDOWMANAGER); if (explicit) return explicit; - const desktop = firstNonEmpty(process.env.XDG_CURRENT_DESKTOP, process.env.DESKTOP_SESSION); + const desktop = firstNonEmpty(Bun.env.XDG_CURRENT_DESKTOP, Bun.env.DESKTOP_SESSION); if (desktop) { const matched = matchKnownWindowManager(desktop); if (matched) return matched; diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 67080ee66..46bb99953 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -239,7 +239,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P !skipPythonPreflight && pythonMode !== "bash-only" && (requestedTools === undefined || requestedTools.includes("python")); - const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test"; + const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test"; const skipPythonWarm = isTestEnv || $env.PI_PYTHON_SKIP_CHECK === "1"; if (shouldCheckPython) { const availability = await checkPythonKernelAvailability(session.cwd); diff --git a/packages/coding-agent/test/bash-executor.test.ts b/packages/coding-agent/test/bash-executor.test.ts index ad386aa88..871cd01f2 100644 --- a/packages/coding-agent/test/bash-executor.test.ts +++ b/packages/coding-agent/test/bash-executor.test.ts @@ -161,7 +161,7 @@ describe("executeBash", () => { if (process.platform === "win32") { return; } - const bashPath = process.env.SHELL?.includes("bash") ? process.env.SHELL : "/bin/bash"; + const bashPath = Bun.env.SHELL?.includes("bash") ? Bun.env.SHELL : "/bin/bash"; if (!fs.existsSync(bashPath)) { return; } @@ -171,8 +171,8 @@ describe("executeBash", () => { shell: bashPath, args: ["-l", "-c"], env: { - PATH: process.env.PATH ?? "", - HOME: process.env.HOME ?? tempDir, + PATH: Bun.env.PATH ?? "", + HOME: Bun.env.HOME ?? tempDir, }, prefix: undefined, }); diff --git a/packages/coding-agent/test/core/python-executor-per-call.test.ts b/packages/coding-agent/test/core/python-executor-per-call.test.ts index 4dbeef5c6..9ab0843c1 100644 --- a/packages/coding-agent/test/core/python-executor-per-call.test.ts +++ b/packages/coding-agent/test/core/python-executor-per-call.test.ts @@ -11,7 +11,7 @@ interface KernelStub { describe("executePython (per-call)", () => { it("shuts down kernel on timed-out cancellation", async () => { - process.env.PI_PYTHON_SKIP_CHECK = "1"; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; using tempDir = TempDir.createSync("@omp-python-executor-per-call-"); let shutdownCalls = 0; diff --git a/packages/coding-agent/test/core/python-executor.lifecycle.test.ts b/packages/coding-agent/test/core/python-executor.lifecycle.test.ts index a1ad81e67..82f45b7fb 100644 --- a/packages/coding-agent/test/core/python-executor.lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-executor.lifecycle.test.ts @@ -6,7 +6,7 @@ import { PythonKernel, } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; -process.env.PI_PYTHON_SKIP_CHECK = "1"; +Bun.env.PI_PYTHON_SKIP_CHECK = "1"; class FakeKernel { private result: KernelExecuteResult; diff --git a/packages/coding-agent/test/core/python-executor.test.ts b/packages/coding-agent/test/core/python-executor.test.ts index a9a422495..235ccda0e 100644 --- a/packages/coding-agent/test/core/python-executor.test.ts +++ b/packages/coding-agent/test/core/python-executor.test.ts @@ -160,8 +160,8 @@ afterEach(async () => { describe("warmPythonEnvironment", () => { it("caches prelude docs on warmup", async () => { - const previousSkip = process.env.PI_PYTHON_SKIP_CHECK; - process.env.PI_PYTHON_SKIP_CHECK = "1"; + const previousSkip = Bun.env.PI_PYTHON_SKIP_CHECK; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; using tempDir = TempDir.createSync("@python-executor-"); const docs: PreludeHelper[] = [ { @@ -188,9 +188,9 @@ describe("warmPythonEnvironment", () => { startSpy.mockRestore(); if (previousSkip === undefined) { - delete process.env.PI_PYTHON_SKIP_CHECK; + delete Bun.env.PI_PYTHON_SKIP_CHECK; } else { - process.env.PI_PYTHON_SKIP_CHECK = previousSkip; + Bun.env.PI_PYTHON_SKIP_CHECK = previousSkip; } }); }); diff --git a/packages/coding-agent/test/core/python-kernel-session.test.ts b/packages/coding-agent/test/core/python-kernel-session.test.ts index e9b0944fd..710458a54 100644 --- a/packages/coding-agent/test/core/python-kernel-session.test.ts +++ b/packages/coding-agent/test/core/python-kernel-session.test.ts @@ -40,7 +40,7 @@ describe("executePython kernel reuse", () => { let kernels: FakeKernel[] = []; beforeEach(() => { - process.env.PI_PYTHON_SKIP_CHECK = "1"; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; startCalls = 0; kernels = []; PythonKernel.start = (async () => { diff --git a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts index c5aa44d23..f56d5162e 100644 --- a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts @@ -80,9 +80,9 @@ describe("PythonKernel gateway lifecycle", () => { const originalSleep = Bun.sleep; const originalWhich = Bun.which; const originalExecute = PythonKernel.prototype.execute; - const originalGatewayUrl = process.env.PI_PYTHON_GATEWAY_URL; - const originalGatewayToken = process.env.PI_PYTHON_GATEWAY_TOKEN; - const originalBunEnv = process.env.BUN_ENV; + const originalGatewayUrl = Bun.env.PI_PYTHON_GATEWAY_URL; + const originalGatewayToken = Bun.env.PI_PYTHON_GATEWAY_TOKEN; + const originalBunEnv = Bun.env.BUN_ENV; let tempDir: TempDir; let env: MockEnvironment; @@ -91,9 +91,9 @@ describe("PythonKernel gateway lifecycle", () => { tempDir = TempDir.createSync("@omp-python-kernel-"); env = { fetchCalls: [], spawnCalls: [] }; - process.env.BUN_ENV = "test"; - delete process.env.PI_PYTHON_GATEWAY_URL; - delete process.env.PI_PYTHON_GATEWAY_TOKEN; + Bun.env.BUN_ENV = "test"; + delete Bun.env.PI_PYTHON_GATEWAY_URL; + delete Bun.env.PI_PYTHON_GATEWAY_TOKEN; FakeWebSocket.instances = []; globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; @@ -125,19 +125,19 @@ describe("PythonKernel gateway lifecycle", () => { } if (originalBunEnv === undefined) { - delete process.env.BUN_ENV; + delete Bun.env.BUN_ENV; } else { - process.env.BUN_ENV = originalBunEnv; + Bun.env.BUN_ENV = originalBunEnv; } if (originalGatewayUrl === undefined) { - delete process.env.PI_PYTHON_GATEWAY_URL; + delete Bun.env.PI_PYTHON_GATEWAY_URL; } else { - process.env.PI_PYTHON_GATEWAY_URL = originalGatewayUrl; + Bun.env.PI_PYTHON_GATEWAY_URL = originalGatewayUrl; } if (originalGatewayToken === undefined) { - delete process.env.PI_PYTHON_GATEWAY_TOKEN; + delete Bun.env.PI_PYTHON_GATEWAY_TOKEN; } else { - process.env.PI_PYTHON_GATEWAY_TOKEN = originalGatewayToken; + Bun.env.PI_PYTHON_GATEWAY_TOKEN = originalGatewayToken; } globalThis.fetch = originalFetch; diff --git a/packages/coding-agent/test/core/python-kernel.test.ts b/packages/coding-agent/test/core/python-kernel.test.ts index a2262f793..33b83f825 100644 --- a/packages/coding-agent/test/core/python-kernel.test.ts +++ b/packages/coding-agent/test/core/python-kernel.test.ts @@ -145,24 +145,24 @@ class FakeWebSocket { } describe("PythonKernel (external gateway)", () => { - const originalEnv = { ...process.env }; + const originalEnv = { ...Bun.env }; const originalFetch = globalThis.fetch; const originalWebSocket = globalThis.WebSocket; beforeEach(() => { - process.env.PI_PYTHON_GATEWAY_URL = "http://gateway.test"; - process.env.PI_PYTHON_SKIP_CHECK = "1"; + Bun.env.PI_PYTHON_GATEWAY_URL = "http://gateway.test"; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; }); afterEach(() => { - for (const key of Object.keys(process.env)) { + for (const key of Object.keys(Bun.env)) { if (!(key in originalEnv)) { - delete process.env[key]; + delete Bun.env[key]; } } for (const [key, value] of Object.entries(originalEnv)) { - process.env[key] = value; + Bun.env[key] = value; } globalThis.fetch = originalFetch; globalThis.WebSocket = originalWebSocket; diff --git a/packages/coding-agent/test/core/python-prelude.test.ts b/packages/coding-agent/test/core/python-prelude.test.ts index 43b5f2214..486beb689 100644 --- a/packages/coding-agent/test/core/python-prelude.test.ts +++ b/packages/coding-agent/test/core/python-prelude.test.ts @@ -6,7 +6,7 @@ import { resetPreludeDocsCache, warmPythonEnvironment } from "@oh-my-pi/pi-codin import { getPythonToolDescription, PythonTool } from "@oh-my-pi/pi-coding-agent/tools/python"; const resolvePythonPath = (): string | null => { - const venvPath = process.env.VIRTUAL_ENV; + const venvPath = Bun.env.VIRTUAL_ENV; const candidates = [venvPath, path.join(process.cwd(), ".venv"), path.join(process.cwd(), "venv")].filter( Boolean, ) as string[]; diff --git a/packages/coding-agent/test/fixtures/large-session.jsonl b/packages/coding-agent/test/fixtures/large-session.jsonl index 275a2fc42..fef18adb5 100644 --- a/packages/coding-agent/test/fixtures/large-session.jsonl +++ b/packages/coding-agent/test/fixtures/large-session.jsonl @@ -5,7 +5,7 @@ {"type":"message","timestamp":"2025-11-20T23:33:50.804Z","message":{"role":"user","content":[{"type":"text","text":"read packages/coding-agent/docs/theme.md in full, then theme.ts, and then oauth-selector or any of the other selectors. we still need to port over user-message-selector.ts based on the patterns you find in the other files"}],"timestamp":1763681630791}} {"type":"message","timestamp":"2025-11-20T23:33:54.572Z","message":{"role":"assistant","content":[{"type":"text","text":"I'll read the theme documentation, theme.ts, and the selector files to understand the patterns, then help port over user-message-selector.ts."},{"type":"toolCall","id":"toolu_017qEkVzzPb7b7o4FkgJLF23","name":"read","arguments":{"path":"packages/coding-agent/docs/theme.md"}},{"type":"toolCall","id":"toolu_01FnVNKzWWm2s2SFJmJttiWh","name":"read","arguments":{"path":"packages/coding-agent/src/theme/theme.ts"}},{"type":"toolCall","id":"toolu_016aKHTkjrTJcMds3wsEou2R","name":"bash","arguments":{"command":"find packages/coding-agent/src -name \"*selector.ts\" -type f"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":191,"cacheRead":0,"cacheWrite":1684,"cost":{"input":0.000009,"output":0.002865,"cacheRead":0,"cacheWrite":0.006315,"total":0.009189}},"stopReason":"toolUse","timestamp":1763681630793}} {"type":"message","timestamp":"2025-11-20T23:33:54.575Z","message":{"role":"toolResult","toolCallId":"toolu_017qEkVzzPb7b7o4FkgJLF23","toolName":"read","content":[{"type":"text","text":"# Pi Coding Agent Themes\n\nThemes allow you to customize the colors used throughout the coding agent TUI.\n\n## Color Tokens\n\nEvery theme must define all color tokens. There are no optional colors.\n\n### Core UI (10 colors)\n\n| Token | Purpose | Examples |\n|-------|---------|----------|\n| `accent` | Primary accent color | Logo, selected items, cursor (›) |\n| `border` | Normal borders | Selector borders, horizontal lines |\n| `borderAccent` | Highlighted borders | Changelog borders, special panels |\n| `borderMuted` | Subtle borders | Editor borders, secondary separators |\n| `success` | Success states | Success messages, diff additions |\n| `error` | Error states | Error messages, diff deletions |\n| `warning` | Warning states | Warning messages |\n| `muted` | Secondary/dimmed text | Metadata, descriptions, output |\n| `dim` | Very dimmed text | Less important info, placeholders |\n| `text` | Default text color | Main content (usually `\"\"`) |\n\n### Backgrounds & Content Text (6 colors)\n\n| Token | Purpose |\n|-------|---------|\n| `userMessageBg` | User message background |\n| `userMessageText` | User message text color |\n| `toolPendingBg` | Tool execution box (pending state) |\n| `toolSuccessBg` | Tool execution box (success state) |\n| `toolErrorBg` | Tool execution box (error state) |\n| `toolText` | Tool execution box text color (all states) |\n\n### Markdown (9 colors)\n\n| Token | Purpose |\n|-------|---------|\n| `mdHeading` | Heading text (`#`, `##`, etc) |\n| `mdLink` | Link text and URLs |\n| `mdCode` | Inline code (backticks) |\n| `mdCodeBlock` | Code block content |\n| `mdCodeBlockBorder` | Code block fences (```) |\n| `mdQuote` | Blockquote text |\n| `mdQuoteBorder` | Blockquote border (`│`) |\n| `mdHr` | Horizontal rule (`---`) |\n| `mdListBullet` | List bullets/numbers |\n\n### Tool Diffs (3 colors)\n\n| Token | Purpose |\n|-------|---------|\n| `toolDiffAdded` | Added lines in tool diffs |\n| `toolDiffRemoved` | Removed lines in tool diffs |\n| `toolDiffContext` | Context lines in tool diffs |\n\nNote: Diff colors are specific to tool execution boxes and must work with tool background colors.\n\n### Syntax Highlighting (9 colors)\n\nFuture-proofing for syntax highlighting support:\n\n| Token | Purpose |\n|-------|---------|\n| `syntaxComment` | Comments |\n| `syntaxKeyword` | Keywords (`if`, `function`, etc) |\n| `syntaxFunction` | Function names |\n| `syntaxVariable` | Variable names |\n| `syntaxString` | String literals |\n| `syntaxNumber` | Number literals |\n| `syntaxType` | Type names |\n| `syntaxOperator` | Operators (`+`, `-`, etc) |\n| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |\n\n**Total: 37 color tokens** (all required)\n\n## Theme Format\n\nThemes are defined in JSON files with the following structure:\n\n```json\n{\n \"$schema\": \"https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json\",\n \"name\": \"my-theme\",\n \"vars\": {\n \"blue\": \"#0066cc\",\n \"gray\": 242,\n \"brightCyan\": 51\n },\n \"colors\": {\n \"accent\": \"blue\",\n \"muted\": \"gray\",\n \"text\": \"\",\n ...\n }\n}\n```\n\n### Color Values\n\nFour formats are supported:\n\n1. **Hex colors**: `\"#ff0000\"` (6-digit hex RGB)\n2. **256-color palette**: `39` (number 0-255, xterm 256-color palette)\n3. **Color references**: `\"blue\"` (must be defined in `vars`)\n4. **Terminal default**: `\"\"` (empty string, uses terminal's default color)\n\n### The `vars` Section\n\nThe optional `vars` section allows you to define reusable colors:\n\n```json\n{\n \"vars\": {\n \"nord0\": \"#2E3440\",\n \"nord1\": \"#3B4252\",\n \"nord8\": \"#88C0D0\",\n \"brightBlue\": 39\n },\n \"colors\": {\n \"accent\": \"nord8\",\n \"muted\": \"nord1\",\n \"mdLink\": \"brightBlue\"\n }\n}\n```\n\nBenefits:\n- Reuse colors across multiple tokens\n- Easier to maintain theme consistency\n- Can reference standard color palettes\n\nVariables can be hex colors (`\"#ff0000\"`), 256-color indices (`42`), or references to other variables.\n\n### Terminal Default (empty string)\n\nUse `\"\"` (empty string) to inherit the terminal's default foreground/background color:\n\n```json\n{\n \"colors\": {\n \"text\": \"\" // Uses terminal's default text color\n }\n}\n```\n\nThis is useful for:\n- Main text color (adapts to user's terminal theme)\n- Creating themes that blend with terminal appearance\n\n## Built-in Themes\n\nPi comes with two built-in themes:\n\n### `dark` (default)\n\nOptimized for dark terminal backgrounds with bright, saturated colors.\n\n### `light`\n\nOptimized for light terminal backgrounds with darker, muted colors.\n\n## Selecting a Theme\n\nThemes are configured in the settings (accessible via `/settings`):\n\n```json\n{\n \"theme\": \"dark\"\n}\n```\n\nOr use the `/theme` command interactively.\n\nOn first run, Pi detects your terminal's background and sets a sensible default (`dark` or `light`).\n\n## Custom Themes\n\n### Theme Locations\n\nCustom themes are loaded from `~/.pi/agent/themes/*.json`.\n\n### Creating a Custom Theme\n\n1. **Create theme directory:**\n ```bash\n mkdir -p ~/.pi/agent/themes\n ```\n\n2. **Create theme file:**\n ```bash\n vim ~/.pi/agent/themes/my-theme.json\n ```\n\n3. **Define all colors:**\n ```json\n {\n \"$schema\": \"https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json\",\n \"name\": \"my-theme\",\n \"vars\": {\n \"primary\": \"#00aaff\",\n \"secondary\": 242,\n \"brightGreen\": 46\n },\n \"colors\": {\n \"accent\": \"primary\",\n \"border\": \"primary\",\n \"borderAccent\": \"#00ffff\",\n \"borderMuted\": \"secondary\",\n \"success\": \"brightGreen\",\n \"error\": \"#ff0000\",\n \"warning\": \"#ffff00\",\n \"muted\": \"secondary\",\n \"text\": \"\",\n \n \"userMessageBg\": \"#2d2d30\",\n \"userMessageText\": \"\",\n \"toolPendingBg\": \"#1e1e2e\",\n \"toolSuccessBg\": \"#1e2e1e\",\n \"toolErrorBg\": \"#2e1e1e\",\n \"toolText\": \"\",\n \n \"mdHeading\": \"#ffaa00\",\n \"mdLink\": \"primary\",\n \"mdCode\": \"#00ffff\",\n \"mdCodeBlock\": \"#00ff00\",\n \"mdCodeBlockBorder\": \"secondary\",\n \"mdQuote\": \"secondary\",\n \"mdQuoteBorder\": \"secondary\",\n \"mdHr\": \"secondary\",\n \"mdListBullet\": \"#00ffff\",\n \n \"toolDiffAdded\": \"#00ff00\",\n \"toolDiffRemoved\": \"#ff0000\",\n \"toolDiffContext\": \"secondary\",\n \n \"syntaxComment\": \"secondary\",\n \"syntaxKeyword\": \"primary\",\n \"syntaxFunction\": \"#00aaff\",\n \"syntaxVariable\": \"#ffaa00\",\n \"syntaxString\": \"#00ff00\",\n \"syntaxNumber\": \"#ff00ff\",\n \"syntaxType\": \"#00aaff\",\n \"syntaxOperator\": \"primary\",\n \"syntaxPunctuation\": \"secondary\"\n }\n }\n ```\n\n4. **Select your theme:**\n - Use `/settings` command and set `\"theme\": \"my-theme\"`\n - Or use `/theme` command interactively\n\n## Tips\n\n### Light vs Dark Themes\n\n**For dark terminals:**\n- Use bright, saturated colors\n- Higher contrast\n- Example: `#00ffff` (bright cyan)\n\n**For light terminals:**\n- Use darker, muted colors\n- Lower contrast to avoid eye strain\n- Example: `#008888` (dark cyan)\n\n### Color Harmony\n\n- Start with a base palette (e.g., Nord, Gruvbox, Tokyo Night)\n- Define your palette in `defs`\n- Reference colors consistently\n\n### Testing\n\nTest your theme with:\n- Different message types (user, assistant, errors)\n- Tool executions (success and error states)\n- Markdown content (headings, code, lists, etc)\n- Long text that wraps\n\n## Color Format Reference\n\n### Hex Colors\n\nStandard 6-digit hex format:\n- `\"#ff0000\"` - Red\n- `\"#00ff00\"` - Green\n- `\"#0000ff\"` - Blue\n- `\"#808080\"` - Gray\n- `\"#ffffff\"` - White\n- `\"#000000\"` - Black\n\nRGB values: `#RRGGBB` where each component is `00-ff` (0-255)\n\n### 256-Color Palette\n\nUse numeric indices (0-255) to reference the xterm 256-color palette:\n\n**Colors 0-15:** Basic ANSI colors (terminal-dependent, may be themed)\n- `0` - Black\n- `1` - Red\n- `2` - Green\n- `3` - Yellow\n- `4` - Blue\n- `5` - Magenta\n- `6` - Cyan\n- `7` - White\n- `8-15` - Bright variants\n\n**Colors 16-231:** 6×6×6 RGB cube (standardized)\n- Formula: `16 + 36×R + 6×G + B` where R, G, B are 0-5\n- Example: `39` = bright cyan, `196` = bright red\n\n**Colors 232-255:** Grayscale ramp (standardized)\n- `232` - Darkest gray\n- `255` - Near white\n\nExample usage:\n```json\n{\n \"vars\": {\n \"gray\": 242,\n \"brightCyan\": 51,\n \"darkBlue\": 18\n },\n \"colors\": {\n \"muted\": \"gray\",\n \"accent\": \"brightCyan\"\n }\n}\n```\n\n**Benefits:**\n- Works everywhere (`TERM=xterm-256color`)\n- No truecolor detection needed\n- Standardized RGB cube (16-231) looks the same on all terminals\n\n### Terminal Compatibility\n\nPi uses 24-bit RGB colors (`\\x1b[38;2;R;G;Bm`). Most modern terminals support this:\n\n- ✅ iTerm2, Alacritty, Kitty, WezTerm\n- ✅ Windows Terminal\n- ✅ VS Code integrated terminal\n- ✅ Modern GNOME Terminal, Konsole\n\nFor older terminals with only 256-color support, Pi automatically falls back to the nearest 256-color approximation.\n\nTo check if your terminal supports truecolor:\n```bash\necho $COLORTERM # Should output \"truecolor\" or \"24bit\"\n```\n\n## Example Themes\n\nSee the built-in themes for complete examples:\n- [Dark theme](../src/themes/dark.json)\n- [Light theme](../src/themes/light.json)\n\n## Schema Validation\n\nThemes are validated on load using [TypeBox](https://github.com/sinclairzx81/typebox) + [Ajv](https://ajv.js.org/).\n\nInvalid themes will show an error with details about what's wrong:\n```\nError loading theme 'my-theme':\n - colors.accent: must be string or number\n - colors.mdHeading: required property missing\n```\n\nFor editor support, the JSON schema is available at:\n```\nhttps://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json\n```\n\nAdd to your theme file for auto-completion and validation:\n```json\n{\n \"$schema\": \"https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json\",\n ...\n}\n```\n\n## Implementation\n\n### Theme Class\n\nThemes are loaded and converted to a `Theme` class that provides type-safe color methods:\n\n```typescript\nclass Theme {\n // Apply foreground color\n fg(color: ThemeColor, text: string): string\n \n // Apply background color\n bg(color: ThemeBg, text: string): string\n \n // Text attributes (preserve current colors)\n bold(text: string): string\n dim(text: string): string\n italic(text: string): string\n}\n```\n\n### Global Theme Instance\n\nThe active theme is available as a global singleton in `coding-agent`:\n\n```typescript\n// theme.ts\nexport let theme: Theme;\n\nexport function setTheme(name: string) {\n theme = loadTheme(name);\n}\n\n// Usage throughout coding-agent\nimport { theme } from './theme.js';\n\ntheme.fg('accent', 'Selected')\ntheme.bg('userMessageBg', content)\n```\n\n### TUI Component Theming\n\nTUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@oh-my-pi/pi-tui` package and don't have direct access to the theme. Instead, they define interfaces for the colors they need:\n\n```typescript\n// In @oh-my-pi/pi-tui\nexport interface MarkdownTheme {\n heading: (text: string) => string;\n link: (text: string) => string;\n code: (text: string) => string;\n codeBlock: (text: string) => string;\n codeBlockBorder: (text: string) => string;\n quote: (text: string) => string;\n quoteBorder: (text: string) => string;\n hr: (text: string) => string;\n listBullet: (text: string) => string;\n}\n\nexport class Markdown {\n constructor(\n text: string,\n paddingX: number,\n paddingY: number,\n defaultTextStyle?: DefaultTextStyle,\n theme?: MarkdownTheme // Optional theme functions\n )\n \n // Usage in component\n renderHeading(text: string) {\n return this.theme.heading(text); // Applies color\n }\n}\n```\n\nThe `coding-agent` provides themed functions when creating components:\n\n```typescript\n// In coding-agent\nimport { theme } from './theme.js';\nimport { Markdown } from '@oh-my-pi/pi-tui';\n\n// Helper to create markdown theme functions\nfunction getMarkdownTheme(): MarkdownTheme {\n return {\n heading: (text) => theme.fg('mdHeading', text),\n link: (text) => theme.fg('mdLink', text),\n code: (text) => theme.fg('mdCode', text),\n codeBlock: (text) => theme.fg('mdCodeBlock', text),\n codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text),\n quote: (text) => theme.fg('mdQuote', text),\n quoteBorder: (text) => theme.fg('mdQuoteBorder', text),\n hr: (text) => theme.fg('mdHr', text),\n listBullet: (text) => theme.fg('mdListBullet', text),\n };\n}\n\n// Create markdown with theme\nconst md = new Markdown(\n text,\n 1, 1,\n { bgColor: theme.bg('userMessageBg') },\n getMarkdownTheme()\n);\n```\n\nThis approach:\n- Keeps TUI components theme-agnostic (reusable in other projects)\n- Maintains type safety via interfaces\n- Allows components to have sensible defaults if no theme provided\n- Centralizes theme access in `coding-agent`\n\n**Example usage:**\n```typescript\nconst theme = loadTheme('dark');\n\n// Apply foreground colors\ntheme.fg('accent', 'Selected')\ntheme.fg('success', '✓ Done')\ntheme.fg('error', 'Failed')\n\n// Apply background colors\ntheme.bg('userMessageBg', content)\ntheme.bg('toolSuccessBg', output)\n\n// Combine styles\ntheme.bold(theme.fg('accent', 'Title'))\ntheme.dim(theme.fg('muted', 'metadata'))\n\n// Nested foreground + background\nconst userMsg = theme.bg('userMessageBg',\n theme.fg('userMessageText', 'Hello')\n)\n```\n\n**Color resolution:**\n\n1. **Detect terminal capabilities:**\n - Check `$COLORTERM` env var (`truecolor` or `24bit` → truecolor support)\n - Check `$TERM` env var (`*-256color` → 256-color support)\n - Fallback to 256-color mode if detection fails\n\n2. **Load JSON theme file**\n\n3. **Resolve `vars` references recursively:**\n ```json\n {\n \"vars\": {\n \"primary\": \"#0066cc\",\n \"accent\": \"primary\"\n },\n \"colors\": {\n \"accent\": \"accent\" // → \"primary\" → \"#0066cc\"\n }\n }\n ```\n\n4. **Convert colors to ANSI codes based on terminal capability:**\n \n **Truecolor mode (24-bit):**\n - Hex (`\"#ff0000\"`) → `\\x1b[38;2;255;0;0m`\n - 256-color (`42`) → `\\x1b[38;5;42m` (keep as-is)\n - Empty string (`\"\"`) → `\\x1b[39m`\n \n **256-color mode:**\n - Hex (`\"#ff0000\"`) → convert to nearest RGB cube color → `\\x1b[38;5;196m`\n - 256-color (`42`) → `\\x1b[38;5;42m` (keep as-is)\n - Empty string (`\"\"`) → `\\x1b[39m`\n \n **Hex to 256-color conversion:**\n ```typescript\n // Convert RGB to 6x6x6 cube (colors 16-231)\n r_index = Math.round(r / 255 * 5)\n g_index = Math.round(g / 255 * 5)\n b_index = Math.round(b / 255 * 5)\n color_index = 16 + 36 * r_index + 6 * g_index + b_index\n ```\n\n5. **Cache as `Theme` instance**\n\nThis ensures themes work correctly regardless of terminal capabilities, with graceful degradation from truecolor to 256-color.\n"}],"isError":false,"timestamp":1763681634574}} -{"type":"message","timestamp":"2025-11-20T23:33:54.580Z","message":{"role":"toolResult","toolCallId":"toolu_01FnVNKzWWm2s2SFJmJttiWh","toolName":"read","content":[{"type":"text","text":"import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { EditorTheme, MarkdownTheme, SelectListTheme } from \"@oh-my-pi/pi-tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { TypeCompiler } from \"@sinclair/typebox/compiler\";\nimport chalk from \"chalk\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// ============================================================================\n// Types & Schema\n// ============================================================================\n\nconst ColorValueSchema = Type.Union([\n\tType.String(), // hex \"#ff0000\", var ref \"primary\", or empty \"\"\n\tType.Integer({ minimum: 0, maximum: 255 }), // 256-color index\n]);\n\ntype ColorValue = Static;\n\nconst ThemeJsonSchema = Type.Object({\n\t$schema: Type.Optional(Type.String()),\n\tname: Type.String(),\n\tvars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),\n\tcolors: Type.Object({\n\t\t// Core UI (10 colors)\n\t\taccent: ColorValueSchema,\n\t\tborder: ColorValueSchema,\n\t\tborderAccent: ColorValueSchema,\n\t\tborderMuted: ColorValueSchema,\n\t\tsuccess: ColorValueSchema,\n\t\terror: ColorValueSchema,\n\t\twarning: ColorValueSchema,\n\t\tmuted: ColorValueSchema,\n\t\tdim: ColorValueSchema,\n\t\ttext: ColorValueSchema,\n\t\t// Backgrounds & Content Text (6 colors)\n\t\tuserMessageBg: ColorValueSchema,\n\t\tuserMessageText: ColorValueSchema,\n\t\ttoolPendingBg: ColorValueSchema,\n\t\ttoolSuccessBg: ColorValueSchema,\n\t\ttoolErrorBg: ColorValueSchema,\n\t\ttoolText: ColorValueSchema,\n\t\t// Markdown (9 colors)\n\t\tmdHeading: ColorValueSchema,\n\t\tmdLink: ColorValueSchema,\n\t\tmdCode: ColorValueSchema,\n\t\tmdCodeBlock: ColorValueSchema,\n\t\tmdCodeBlockBorder: ColorValueSchema,\n\t\tmdQuote: ColorValueSchema,\n\t\tmdQuoteBorder: ColorValueSchema,\n\t\tmdHr: ColorValueSchema,\n\t\tmdListBullet: ColorValueSchema,\n\t\t// Tool Diffs (3 colors)\n\t\ttoolDiffAdded: ColorValueSchema,\n\t\ttoolDiffRemoved: ColorValueSchema,\n\t\ttoolDiffContext: ColorValueSchema,\n\t\t// Syntax Highlighting (9 colors)\n\t\tsyntaxComment: ColorValueSchema,\n\t\tsyntaxKeyword: ColorValueSchema,\n\t\tsyntaxFunction: ColorValueSchema,\n\t\tsyntaxVariable: ColorValueSchema,\n\t\tsyntaxString: ColorValueSchema,\n\t\tsyntaxNumber: ColorValueSchema,\n\t\tsyntaxType: ColorValueSchema,\n\t\tsyntaxOperator: ColorValueSchema,\n\t\tsyntaxPunctuation: ColorValueSchema,\n\t}),\n});\n\ntype ThemeJson = Static;\n\nconst validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema);\n\nexport type ThemeColor =\n\t| \"accent\"\n\t| \"border\"\n\t| \"borderAccent\"\n\t| \"borderMuted\"\n\t| \"success\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"muted\"\n\t| \"dim\"\n\t| \"text\"\n\t| \"userMessageText\"\n\t| \"toolText\"\n\t| \"mdHeading\"\n\t| \"mdLink\"\n\t| \"mdCode\"\n\t| \"mdCodeBlock\"\n\t| \"mdCodeBlockBorder\"\n\t| \"mdQuote\"\n\t| \"mdQuoteBorder\"\n\t| \"mdHr\"\n\t| \"mdListBullet\"\n\t| \"toolDiffAdded\"\n\t| \"toolDiffRemoved\"\n\t| \"toolDiffContext\"\n\t| \"syntaxComment\"\n\t| \"syntaxKeyword\"\n\t| \"syntaxFunction\"\n\t| \"syntaxVariable\"\n\t| \"syntaxString\"\n\t| \"syntaxNumber\"\n\t| \"syntaxType\"\n\t| \"syntaxOperator\"\n\t| \"syntaxPunctuation\";\n\nexport type ThemeBg = \"userMessageBg\" | \"toolPendingBg\" | \"toolSuccessBg\" | \"toolErrorBg\";\n\ntype ColorMode = \"truecolor\" | \"256color\";\n\n// ============================================================================\n// Color Utilities\n// ============================================================================\n\nfunction detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } {\n\tconst cleaned = hex.replace(\"#\", \"\");\n\tif (cleaned.length !== 6) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\tconst r = parseInt(cleaned.substring(0, 2), 16);\n\tconst g = parseInt(cleaned.substring(2, 4), 16);\n\tconst b = parseInt(cleaned.substring(4, 6), 16);\n\tif (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\treturn { r, g, b };\n}\n\nfunction rgbTo256(r: number, g: number, b: number): number {\n\tconst rIndex = Math.round((r / 255) * 5);\n\tconst gIndex = Math.round((g / 255) * 5);\n\tconst bIndex = Math.round((b / 255) * 5);\n\treturn 16 + 36 * rIndex + 6 * gIndex + bIndex;\n}\n\nfunction hexTo256(hex: string): number {\n\tconst { r, g, b } = hexToRgb(hex);\n\treturn rgbTo256(r, g, b);\n}\n\nfunction fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[49m\";\n\tif (typeof color === \"number\") return `\\x1b[48;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[48;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[48;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction resolveVarRefs(\n\tvalue: ColorValue,\n\tvars: Record,\n\tvisited = new Set(),\n): string | number {\n\tif (typeof value === \"number\" || value === \"\" || value.startsWith(\"#\")) {\n\t\treturn value;\n\t}\n\tif (visited.has(value)) {\n\t\tthrow new Error(`Circular variable reference detected: ${value}`);\n\t}\n\tif (!(value in vars)) {\n\t\tthrow new Error(`Variable reference not found: ${value}`);\n\t}\n\tvisited.add(value);\n\treturn resolveVarRefs(vars[value], vars, visited);\n}\n\nfunction resolveThemeColors>(\n\tcolors: T,\n\tvars: Record = {},\n): Record {\n\tconst resolved: Record = {};\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tresolved[key] = resolveVarRefs(value, vars);\n\t}\n\treturn resolved as Record;\n}\n\n// ============================================================================\n// Theme Class\n// ============================================================================\n\nexport class Theme {\n\tprivate fgColors: Map;\n\tprivate bgColors: Map;\n\tprivate mode: ColorMode;\n\n\tconstructor(\n\t\tfgColors: Record,\n\t\tbgColors: Record,\n\t\tmode: ColorMode,\n\t) {\n\t\tthis.mode = mode;\n\t\tthis.fgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {\n\t\t\tthis.fgColors.set(key, fgAnsi(value, mode));\n\t\t}\n\t\tthis.bgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {\n\t\t\tthis.bgColors.set(key, bgAnsi(value, mode));\n\t\t}\n\t}\n\n\tfg(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[39m`; // Reset only foreground color\n\t}\n\n\tbg(color: ThemeBg, text: string): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[49m`; // Reset only background color\n\t}\n\n\tbold(text: string): string {\n\t\treturn chalk.bold(text);\n\t}\n\n\titalic(text: string): string {\n\t\treturn chalk.italic(text);\n\t}\n\n\tunderline(text: string): string {\n\t\treturn chalk.underline(text);\n\t}\n\n\tgetFgAnsi(color: ThemeColor): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetBgAnsi(color: ThemeBg): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetColorMode(): ColorMode {\n\t\treturn this.mode;\n\t}\n}\n\n// ============================================================================\n// Theme Loading\n// ============================================================================\n\nlet BUILTIN_THEMES: Record | undefined;\n\nfunction getBuiltinThemes(): Record {\n\tif (!BUILTIN_THEMES) {\n\t\tconst darkPath = path.join(__dirname, \"dark.json\");\n\t\tconst lightPath = path.join(__dirname, \"light.json\");\n\t\tBUILTIN_THEMES = {\n\t\t\tdark: JSON.parse(fs.readFileSync(darkPath, \"utf-8\")) as ThemeJson,\n\t\t\tlight: JSON.parse(fs.readFileSync(lightPath, \"utf-8\")) as ThemeJson,\n\t\t};\n\t}\n\treturn BUILTIN_THEMES;\n}\n\nfunction getThemesDir(): string {\n\treturn path.join(os.homedir(), \".pi\", \"agent\", \"themes\");\n}\n\nexport function getAvailableThemes(): string[] {\n\tconst themes = new Set(Object.keys(getBuiltinThemes()));\n\tconst themesDir = getThemesDir();\n\tif (fs.existsSync(themesDir)) {\n\t\tconst files = fs.readdirSync(themesDir);\n\t\tfor (const file of files) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tthemes.add(file.slice(0, -5));\n\t\t\t}\n\t\t}\n\t}\n\treturn Array.from(themes).sort();\n}\n\nfunction loadThemeJson(name: string): ThemeJson {\n\tconst builtinThemes = getBuiltinThemes();\n\tif (name in builtinThemes) {\n\t\treturn builtinThemes[name];\n\t}\n\tconst themesDir = getThemesDir();\n\tconst themePath = path.join(themesDir, `${name}.json`);\n\tif (!fs.existsSync(themePath)) {\n\t\tthrow new Error(`Theme not found: ${name}`);\n\t}\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to parse theme ${name}: ${error}`);\n\t}\n\tif (!validateThemeJson.Check(json)) {\n\t\tconst errors = Array.from(validateThemeJson.Errors(json));\n\t\tconst errorMessages = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n\t\tthrow new Error(`Invalid theme ${name}:\\n${errorMessages}`);\n\t}\n\treturn json as ThemeJson;\n}\n\nfunction createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {\n\tconst colorMode = mode ?? detectColorMode();\n\tconst resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst fgColors: Record = {} as Record;\n\tconst bgColors: Record = {} as Record;\n\tconst bgColorKeys: Set = new Set([\"userMessageBg\", \"toolPendingBg\", \"toolSuccessBg\", \"toolErrorBg\"]);\n\tfor (const [key, value] of Object.entries(resolvedColors)) {\n\t\tif (bgColorKeys.has(key)) {\n\t\t\tbgColors[key as ThemeBg] = value;\n\t\t} else {\n\t\t\tfgColors[key as ThemeColor] = value;\n\t\t}\n\t}\n\treturn new Theme(fgColors, bgColors, colorMode);\n}\n\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n\tconst themeJson = loadThemeJson(name);\n\treturn createTheme(themeJson, mode);\n}\n\nfunction detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n}\n\n// ============================================================================\n// Global Theme Instance\n// ============================================================================\n\nexport let theme: Theme;\n\nexport function initTheme(themeName?: string): void {\n\tconst name = themeName ?? getDefaultTheme();\n\ttheme = loadTheme(name);\n}\n\nexport function setTheme(name: string): void {\n\ttheme = loadTheme(name);\n}\n\n// ============================================================================\n// TUI Helpers\n// ============================================================================\n\nexport function getMarkdownTheme(): MarkdownTheme {\n\treturn {\n\t\theading: (text: string) => theme.fg(\"mdHeading\", text),\n\t\tlink: (text: string) => theme.fg(\"mdLink\", text),\n\t\tcode: (text: string) => theme.fg(\"mdCode\", text),\n\t\tcodeBlock: (text: string) => theme.fg(\"mdCodeBlock\", text),\n\t\tcodeBlockBorder: (text: string) => theme.fg(\"mdCodeBlockBorder\", text),\n\t\tquote: (text: string) => theme.fg(\"mdQuote\", text),\n\t\tquoteBorder: (text: string) => theme.fg(\"mdQuoteBorder\", text),\n\t\thr: (text: string) => theme.fg(\"mdHr\", text),\n\t\tlistBullet: (text: string) => theme.fg(\"mdListBullet\", text),\n\t};\n}\n\nexport function getSelectListTheme(): SelectListTheme {\n\treturn {\n\t\tselectedPrefix: (text: string) => theme.fg(\"accent\", text),\n\t\tselectedText: (text: string) => theme.fg(\"accent\", text),\n\t\tdescription: (text: string) => theme.fg(\"muted\", text),\n\t\tscrollInfo: (text: string) => theme.fg(\"muted\", text),\n\t\tnoMatch: (text: string) => theme.fg(\"muted\", text),\n\t};\n}\n\nexport function getEditorTheme(): EditorTheme {\n\treturn {\n\t\tborderColor: (text: string) => theme.fg(\"borderMuted\", text),\n\t\tselectList: getSelectListTheme(),\n\t};\n}\n"}],"isError":false,"timestamp":1763681634577}} +{"type":"message","timestamp":"2025-11-20T23:33:54.580Z","message":{"role":"toolResult","toolCallId":"toolu_01FnVNKzWWm2s2SFJmJttiWh","toolName":"read","content":[{"type":"text","text":"import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { EditorTheme, MarkdownTheme, SelectListTheme } from \"@oh-my-pi/pi-tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { TypeCompiler } from \"@sinclair/typebox/compiler\";\nimport chalk from \"chalk\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// ============================================================================\n// Types & Schema\n// ============================================================================\n\nconst ColorValueSchema = Type.Union([\n\tType.String(), // hex \"#ff0000\", var ref \"primary\", or empty \"\"\n\tType.Integer({ minimum: 0, maximum: 255 }), // 256-color index\n]);\n\ntype ColorValue = Static;\n\nconst ThemeJsonSchema = Type.Object({\n\t$schema: Type.Optional(Type.String()),\n\tname: Type.String(),\n\tvars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),\n\tcolors: Type.Object({\n\t\t// Core UI (10 colors)\n\t\taccent: ColorValueSchema,\n\t\tborder: ColorValueSchema,\n\t\tborderAccent: ColorValueSchema,\n\t\tborderMuted: ColorValueSchema,\n\t\tsuccess: ColorValueSchema,\n\t\terror: ColorValueSchema,\n\t\twarning: ColorValueSchema,\n\t\tmuted: ColorValueSchema,\n\t\tdim: ColorValueSchema,\n\t\ttext: ColorValueSchema,\n\t\t// Backgrounds & Content Text (6 colors)\n\t\tuserMessageBg: ColorValueSchema,\n\t\tuserMessageText: ColorValueSchema,\n\t\ttoolPendingBg: ColorValueSchema,\n\t\ttoolSuccessBg: ColorValueSchema,\n\t\ttoolErrorBg: ColorValueSchema,\n\t\ttoolText: ColorValueSchema,\n\t\t// Markdown (9 colors)\n\t\tmdHeading: ColorValueSchema,\n\t\tmdLink: ColorValueSchema,\n\t\tmdCode: ColorValueSchema,\n\t\tmdCodeBlock: ColorValueSchema,\n\t\tmdCodeBlockBorder: ColorValueSchema,\n\t\tmdQuote: ColorValueSchema,\n\t\tmdQuoteBorder: ColorValueSchema,\n\t\tmdHr: ColorValueSchema,\n\t\tmdListBullet: ColorValueSchema,\n\t\t// Tool Diffs (3 colors)\n\t\ttoolDiffAdded: ColorValueSchema,\n\t\ttoolDiffRemoved: ColorValueSchema,\n\t\ttoolDiffContext: ColorValueSchema,\n\t\t// Syntax Highlighting (9 colors)\n\t\tsyntaxComment: ColorValueSchema,\n\t\tsyntaxKeyword: ColorValueSchema,\n\t\tsyntaxFunction: ColorValueSchema,\n\t\tsyntaxVariable: ColorValueSchema,\n\t\tsyntaxString: ColorValueSchema,\n\t\tsyntaxNumber: ColorValueSchema,\n\t\tsyntaxType: ColorValueSchema,\n\t\tsyntaxOperator: ColorValueSchema,\n\t\tsyntaxPunctuation: ColorValueSchema,\n\t}),\n});\n\ntype ThemeJson = Static;\n\nconst validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema);\n\nexport type ThemeColor =\n\t| \"accent\"\n\t| \"border\"\n\t| \"borderAccent\"\n\t| \"borderMuted\"\n\t| \"success\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"muted\"\n\t| \"dim\"\n\t| \"text\"\n\t| \"userMessageText\"\n\t| \"toolText\"\n\t| \"mdHeading\"\n\t| \"mdLink\"\n\t| \"mdCode\"\n\t| \"mdCodeBlock\"\n\t| \"mdCodeBlockBorder\"\n\t| \"mdQuote\"\n\t| \"mdQuoteBorder\"\n\t| \"mdHr\"\n\t| \"mdListBullet\"\n\t| \"toolDiffAdded\"\n\t| \"toolDiffRemoved\"\n\t| \"toolDiffContext\"\n\t| \"syntaxComment\"\n\t| \"syntaxKeyword\"\n\t| \"syntaxFunction\"\n\t| \"syntaxVariable\"\n\t| \"syntaxString\"\n\t| \"syntaxNumber\"\n\t| \"syntaxType\"\n\t| \"syntaxOperator\"\n\t| \"syntaxPunctuation\";\n\nexport type ThemeBg = \"userMessageBg\" | \"toolPendingBg\" | \"toolSuccessBg\" | \"toolErrorBg\";\n\ntype ColorMode = \"truecolor\" | \"256color\";\n\n// ============================================================================\n// Color Utilities\n// ============================================================================\n\nfunction detectColorMode(): ColorMode {\n\tconst colorterm = Bun.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = Bun.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } {\n\tconst cleaned = hex.replace(\"#\", \"\");\n\tif (cleaned.length !== 6) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\tconst r = parseInt(cleaned.substring(0, 2), 16);\n\tconst g = parseInt(cleaned.substring(2, 4), 16);\n\tconst b = parseInt(cleaned.substring(4, 6), 16);\n\tif (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\treturn { r, g, b };\n}\n\nfunction rgbTo256(r: number, g: number, b: number): number {\n\tconst rIndex = Math.round((r / 255) * 5);\n\tconst gIndex = Math.round((g / 255) * 5);\n\tconst bIndex = Math.round((b / 255) * 5);\n\treturn 16 + 36 * rIndex + 6 * gIndex + bIndex;\n}\n\nfunction hexTo256(hex: string): number {\n\tconst { r, g, b } = hexToRgb(hex);\n\treturn rgbTo256(r, g, b);\n}\n\nfunction fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[49m\";\n\tif (typeof color === \"number\") return `\\x1b[48;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[48;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[48;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction resolveVarRefs(\n\tvalue: ColorValue,\n\tvars: Record,\n\tvisited = new Set(),\n): string | number {\n\tif (typeof value === \"number\" || value === \"\" || value.startsWith(\"#\")) {\n\t\treturn value;\n\t}\n\tif (visited.has(value)) {\n\t\tthrow new Error(`Circular variable reference detected: ${value}`);\n\t}\n\tif (!(value in vars)) {\n\t\tthrow new Error(`Variable reference not found: ${value}`);\n\t}\n\tvisited.add(value);\n\treturn resolveVarRefs(vars[value], vars, visited);\n}\n\nfunction resolveThemeColors>(\n\tcolors: T,\n\tvars: Record = {},\n): Record {\n\tconst resolved: Record = {};\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tresolved[key] = resolveVarRefs(value, vars);\n\t}\n\treturn resolved as Record;\n}\n\n// ============================================================================\n// Theme Class\n// ============================================================================\n\nexport class Theme {\n\tprivate fgColors: Map;\n\tprivate bgColors: Map;\n\tprivate mode: ColorMode;\n\n\tconstructor(\n\t\tfgColors: Record,\n\t\tbgColors: Record,\n\t\tmode: ColorMode,\n\t) {\n\t\tthis.mode = mode;\n\t\tthis.fgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {\n\t\t\tthis.fgColors.set(key, fgAnsi(value, mode));\n\t\t}\n\t\tthis.bgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {\n\t\t\tthis.bgColors.set(key, bgAnsi(value, mode));\n\t\t}\n\t}\n\n\tfg(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[39m`; // Reset only foreground color\n\t}\n\n\tbg(color: ThemeBg, text: string): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[49m`; // Reset only background color\n\t}\n\n\tbold(text: string): string {\n\t\treturn chalk.bold(text);\n\t}\n\n\titalic(text: string): string {\n\t\treturn chalk.italic(text);\n\t}\n\n\tunderline(text: string): string {\n\t\treturn chalk.underline(text);\n\t}\n\n\tgetFgAnsi(color: ThemeColor): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetBgAnsi(color: ThemeBg): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetColorMode(): ColorMode {\n\t\treturn this.mode;\n\t}\n}\n\n// ============================================================================\n// Theme Loading\n// ============================================================================\n\nlet BUILTIN_THEMES: Record | undefined;\n\nfunction getBuiltinThemes(): Record {\n\tif (!BUILTIN_THEMES) {\n\t\tconst darkPath = path.join(__dirname, \"dark.json\");\n\t\tconst lightPath = path.join(__dirname, \"light.json\");\n\t\tBUILTIN_THEMES = {\n\t\t\tdark: JSON.parse(fs.readFileSync(darkPath, \"utf-8\")) as ThemeJson,\n\t\t\tlight: JSON.parse(fs.readFileSync(lightPath, \"utf-8\")) as ThemeJson,\n\t\t};\n\t}\n\treturn BUILTIN_THEMES;\n}\n\nfunction getThemesDir(): string {\n\treturn path.join(os.homedir(), \".pi\", \"agent\", \"themes\");\n}\n\nexport function getAvailableThemes(): string[] {\n\tconst themes = new Set(Object.keys(getBuiltinThemes()));\n\tconst themesDir = getThemesDir();\n\tif (fs.existsSync(themesDir)) {\n\t\tconst files = fs.readdirSync(themesDir);\n\t\tfor (const file of files) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tthemes.add(file.slice(0, -5));\n\t\t\t}\n\t\t}\n\t}\n\treturn Array.from(themes).sort();\n}\n\nfunction loadThemeJson(name: string): ThemeJson {\n\tconst builtinThemes = getBuiltinThemes();\n\tif (name in builtinThemes) {\n\t\treturn builtinThemes[name];\n\t}\n\tconst themesDir = getThemesDir();\n\tconst themePath = path.join(themesDir, `${name}.json`);\n\tif (!fs.existsSync(themePath)) {\n\t\tthrow new Error(`Theme not found: ${name}`);\n\t}\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to parse theme ${name}: ${error}`);\n\t}\n\tif (!validateThemeJson.Check(json)) {\n\t\tconst errors = Array.from(validateThemeJson.Errors(json));\n\t\tconst errorMessages = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n\t\tthrow new Error(`Invalid theme ${name}:\\n${errorMessages}`);\n\t}\n\treturn json as ThemeJson;\n}\n\nfunction createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {\n\tconst colorMode = mode ?? detectColorMode();\n\tconst resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst fgColors: Record = {} as Record;\n\tconst bgColors: Record = {} as Record;\n\tconst bgColorKeys: Set = new Set([\"userMessageBg\", \"toolPendingBg\", \"toolSuccessBg\", \"toolErrorBg\"]);\n\tfor (const [key, value] of Object.entries(resolvedColors)) {\n\t\tif (bgColorKeys.has(key)) {\n\t\t\tbgColors[key as ThemeBg] = value;\n\t\t} else {\n\t\t\tfgColors[key as ThemeColor] = value;\n\t\t}\n\t}\n\treturn new Theme(fgColors, bgColors, colorMode);\n}\n\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n\tconst themeJson = loadThemeJson(name);\n\treturn createTheme(themeJson, mode);\n}\n\nfunction detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = Bun.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n}\n\n// ============================================================================\n// Global Theme Instance\n// ============================================================================\n\nexport let theme: Theme;\n\nexport function initTheme(themeName?: string): void {\n\tconst name = themeName ?? getDefaultTheme();\n\ttheme = loadTheme(name);\n}\n\nexport function setTheme(name: string): void {\n\ttheme = loadTheme(name);\n}\n\n// ============================================================================\n// TUI Helpers\n// ============================================================================\n\nexport function getMarkdownTheme(): MarkdownTheme {\n\treturn {\n\t\theading: (text: string) => theme.fg(\"mdHeading\", text),\n\t\tlink: (text: string) => theme.fg(\"mdLink\", text),\n\t\tcode: (text: string) => theme.fg(\"mdCode\", text),\n\t\tcodeBlock: (text: string) => theme.fg(\"mdCodeBlock\", text),\n\t\tcodeBlockBorder: (text: string) => theme.fg(\"mdCodeBlockBorder\", text),\n\t\tquote: (text: string) => theme.fg(\"mdQuote\", text),\n\t\tquoteBorder: (text: string) => theme.fg(\"mdQuoteBorder\", text),\n\t\thr: (text: string) => theme.fg(\"mdHr\", text),\n\t\tlistBullet: (text: string) => theme.fg(\"mdListBullet\", text),\n\t};\n}\n\nexport function getSelectListTheme(): SelectListTheme {\n\treturn {\n\t\tselectedPrefix: (text: string) => theme.fg(\"accent\", text),\n\t\tselectedText: (text: string) => theme.fg(\"accent\", text),\n\t\tdescription: (text: string) => theme.fg(\"muted\", text),\n\t\tscrollInfo: (text: string) => theme.fg(\"muted\", text),\n\t\tnoMatch: (text: string) => theme.fg(\"muted\", text),\n\t};\n}\n\nexport function getEditorTheme(): EditorTheme {\n\treturn {\n\t\tborderColor: (text: string) => theme.fg(\"borderMuted\", text),\n\t\tselectList: getSelectListTheme(),\n\t};\n}\n"}],"isError":false,"timestamp":1763681634577}} {"type":"message","timestamp":"2025-11-20T23:33:54.592Z","message":{"role":"toolResult","toolCallId":"toolu_016aKHTkjrTJcMds3wsEou2R","toolName":"bash","content":[{"type":"text","text":"packages/coding-agent/src/tui/thinking-selector.ts\npackages/coding-agent/src/tui/theme-selector.ts\npackages/coding-agent/src/tui/oauth-selector.ts\npackages/coding-agent/src/tui/user-message-selector.ts\npackages/coding-agent/src/tui/queue-mode-selector.ts\npackages/coding-agent/src/tui/model-selector.ts\npackages/coding-agent/src/tui/session-selector.ts\n"}],"isError":false,"timestamp":1763681634589}} {"type":"message","timestamp":"2025-11-20T23:33:59.553Z","message":{"role":"assistant","content":[{"type":"text","text":"Now let me read a few of the selector files to understand the pattern:"},{"type":"toolCall","id":"toolu_01G6wBLUY7g5eyk4TJEcTUiT","name":"read","arguments":{"path":"packages/coding-agent/src/tui/oauth-selector.ts"}},{"type":"toolCall","id":"toolu_012U6SQ4AGg5JrNK6s715ATw","name":"read","arguments":{"path":"packages/coding-agent/src/tui/theme-selector.ts"}},{"type":"toolCall","id":"toolu_01TSY14a4g85DGSpjo9ao8Eu","name":"read","arguments":{"path":"packages/coding-agent/src/tui/model-selector.ts"}},{"type":"toolCall","id":"toolu_01Fo2ghr79kbj5bRxyVyFoQQ","name":"read","arguments":{"path":"packages/coding-agent/src/tui/user-message-selector.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":7,"output":231,"cacheRead":1684,"cacheWrite":9518,"cost":{"input":0.000021000000000000002,"output":0.003465,"cacheRead":0.0005052,"cacheWrite":0.0356925,"total":0.0396837}},"stopReason":"toolUse","timestamp":1763681634589}} {"type":"message","timestamp":"2025-11-20T23:33:59.555Z","message":{"role":"toolResult","toolCallId":"toolu_01G6wBLUY7g5eyk4TJEcTUiT","toolName":"read","content":[{"type":"text","text":"import { Container, Spacer, Text } from \"@oh-my-pi/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../oauth/index.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new Text(theme.bold(title), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t\tthis.allProviders = this.allProviders.filter((p) => p.available);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"}],"isError":false,"timestamp":1763681639554}} @@ -354,20 +354,20 @@ {"type":"message","timestamp":"2025-11-21T00:24:44.598Z","message":{"role":"assistant","content":[],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"aborted","timestamp":1763684683385,"errorMessage":"Request was aborted."}} {"type":"message","timestamp":"2025-11-21T00:28:14.441Z","message":{"role":"user","content":[{"type":"text","text":"'/var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/TemporaryItems/NSIRD_screencaptureui_tby1cQ/Screenshot 2025-11-21 at 01.27.21.png'\n\nany idea why the colors differ in the vs code terminal and ghostty?\n\nghosty:\n➜ pi-mono git:(main) ✗ echo $TERM\nxterm-ghostty\n➜ pi-mono git:(main) ✗ echo $COLORTERM\ntruecolor\n➜ pi-mono git:(main) ✗\n\nvs code terminal:\n➜ pi-mono git:(main) ✗ echo $TERM \nxterm-256color\n➜ pi-mono git:(main) ✗ echo $COLORTERM\ntruecolor"}],"timestamp":1763684894428}} {"type":"message","timestamp":"2025-11-21T00:28:21.318Z","message":{"role":"assistant","content":[{"type":"text","text":"The issue is that Ghostty has `TERM=xterm-ghostty` but VS Code has `TERM=xterm-256color`. Let me check the color mode detection:"},{"type":"toolCall","id":"toolu_0181ij547LjNq6RabKnvjErx","name":"bash","arguments":{"command":"grep -A 10 \"function detectColorMode\" packages/coding-agent/src/theme/theme.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":116,"cacheRead":0,"cacheWrite":94464,"cost":{"input":0.000009,"output":0.00174,"cacheRead":0,"cacheWrite":0.35424,"total":0.355989}},"stopReason":"toolUse","timestamp":1763684894430}} -{"type":"message","timestamp":"2025-11-21T00:28:21.332Z","message":{"role":"toolResult","toolCallId":"toolu_0181ij547LjNq6RabKnvjErx","toolName":"bash","content":[{"type":"text","text":"function detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}\n"}],"isError":false,"timestamp":1763684901327}} +{"type":"message","timestamp":"2025-11-21T00:28:21.332Z","message":{"role":"toolResult","toolCallId":"toolu_0181ij547LjNq6RabKnvjErx","toolName":"bash","content":[{"type":"text","text":"function detectColorMode(): ColorMode {\n\tconst colorterm = Bun.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = Bun.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}\n"}],"isError":false,"timestamp":1763684901327}} {"type":"message","timestamp":"2025-11-21T00:28:28.713Z","message":{"role":"assistant","content":[{"type":"text","text":"I see the issue! The detection logic checks `COLORTERM` first (which both terminals have as `truecolor`), so **both** should be using truecolor mode. But wait, VS Code's TERM is `xterm-256color` while Ghostty is `xterm-ghostty`. \n\nBoth should detect as truecolor because they both have `COLORTERM=truecolor`. So the color difference must be coming from something else.\n\nLet me check if the colors are actually different in the theme files or if it's terminal-specific rendering:"},{"type":"toolCall","id":"toolu_018EJernkPPKvVaKJYYCqKw6","name":"bash","arguments":{"command":"grep -A 3 \"thinkingMedium\\|thinkingHigh\" packages/coding-agent/src/theme/dark.json"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":208,"cacheRead":94464,"cacheWrite":224,"cost":{"input":0.000018,"output":0.00312,"cacheRead":0.0283392,"cacheWrite":0.00084,"total":0.0323172}},"stopReason":"toolUse","timestamp":1763684901327}} {"type":"message","timestamp":"2025-11-21T00:28:28.733Z","message":{"role":"toolResult","toolCallId":"toolu_018EJernkPPKvVaKJYYCqKw6","toolName":"bash","content":[{"type":"text","text":"\t\t\"thinkingMedium\": \"cyan\",\n\t\t\"thinkingHigh\": \"#ff00ff\"\n\t}\n}\n"}],"isError":false,"timestamp":1763684908727}} {"type":"message","timestamp":"2025-11-21T00:28:32.576Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01ReZpmNBeAzg4auPbZgdmU7","name":"bash","arguments":{"command":"grep \"cyan\" packages/coding-agent/src/theme/dark.json"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":67,"cacheRead":94688,"cacheWrite":252,"cost":{"input":0.000018,"output":0.001005,"cacheRead":0.0284064,"cacheWrite":0.000945,"total":0.0303744}},"stopReason":"toolUse","timestamp":1763684908727}} {"type":"message","timestamp":"2025-11-21T00:28:32.596Z","message":{"role":"toolResult","toolCallId":"toolu_01ReZpmNBeAzg4auPbZgdmU7","toolName":"bash","content":[{"type":"text","text":"\t\t\"cyan\": \"#00d7ff\",\n\t\t\"accent\": \"cyan\",\n\t\t\"borderAccent\": \"cyan\",\n\t\t\"mdHeading\": \"cyan\",\n\t\t\"mdCode\": \"cyan\",\n\t\t\"mdListBullet\": \"cyan\",\n\t\t\"syntaxKeyword\": \"cyan\",\n\t\t\"syntaxType\": \"cyan\",\n\t\t\"thinkingMedium\": \"cyan\",\n"}],"isError":false,"timestamp":1763684912589}} {"type":"message","timestamp":"2025-11-21T00:28:39.324Z","message":{"role":"assistant","content":[{"type":"text","text":"So `thinkingMedium` is `cyan` which is `#00d7ff` (bright cyan).\n\nThe issue is likely that:\n1. **Ghostty** is rendering `#00d7ff` as actual truecolor\n2. **VS Code** might be converting it to the nearest 256-color or doing some color correction\n\nHowever, both terminals report `COLORTERM=truecolor`, so our code should be sending the exact same ANSI codes to both. Let me verify what ANSI codes we're actually generating for truecolor:"},{"type":"toolCall","id":"toolu_01Ro5VTvL91e678oyyVgzu5e","name":"bash","arguments":{"command":"grep -A 15 \"function fgAnsi\" packages/coding-agent/src/theme/theme.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":203,"cacheRead":94940,"cacheWrite":173,"cost":{"input":0.000018,"output":0.003045,"cacheRead":0.028482,"cacheWrite":0.00064875,"total":0.03219375000000001}},"stopReason":"toolUse","timestamp":1763684912589}} {"type":"message","timestamp":"2025-11-21T00:28:39.343Z","message":{"role":"toolResult","toolCallId":"toolu_01Ro5VTvL91e678oyyVgzu5e","toolName":"bash","content":[{"type":"text","text":"function fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n"}],"isError":false,"timestamp":1763684919337}} -{"type":"message","timestamp":"2025-11-21T00:28:53.247Z","message":{"role":"assistant","content":[{"type":"text","text":"For `#00d7ff` in truecolor mode, we'd generate:\n- RGB: `r=0, g=215, b=255`\n- ANSI: `\\x1b[38;2;0;215;255m`\n\nBoth terminals should receive this exact sequence. The visual difference you're seeing is likely due to:\n\n1. **Color calibration** - VS Code terminal might have color adjustment/dimming settings\n2. **Theme interference** - VS Code might apply its own theme colors on top\n3. **Font rendering** - Different anti-aliasing/rendering can make colors appear different\n\nActually, looking at your screenshot more carefully - the cyan in Ghostty looks **much brighter/more saturated** than in VS Code. This suggests VS Code might be:\n- Applying opacity/alpha to the terminal\n- Using a \"minimum contrast\" feature that dims bright colors\n- Converting truecolor to 256-color despite reporting truecolor support\n\nLet me check VS Code terminal settings. Do you have any of these enabled in VS Code?\n- \"Terminal > Integrated: Minimum Contrast Ratio\"\n- \"Terminal > Integrated: GPU Acceleration\"\n- Any workspace color theme that might affect terminal colors\n\nOr we could add debug logging to verify which color mode is actually being detected:"},{"type":"toolCall","id":"toolu_01EpNbhE5W9znd6xf4XRPLrS","name":"edit","arguments":{"path":"packages/coding-agent/src/theme/theme.ts","oldText":"function detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}","newText":"function detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\t// console.error(`[theme] Detected truecolor mode (COLORTERM=${colorterm}, TERM=${process.env.TERM})`);\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\t// console.error(`[theme] Detected 256color mode (TERM=${term})`);\n\t\treturn \"256color\";\n\t}\n\t// console.error(`[theme] Defaulting to 256color mode (TERM=${term})`);\n\treturn \"256color\";\n}"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":667,"cacheRead":95113,"cacheWrite":418,"cost":{"input":0.000018,"output":0.010005,"cacheRead":0.028533899999999997,"cacheWrite":0.0015675,"total":0.0401244}},"stopReason":"toolUse","timestamp":1763684919338}} -{"type":"message","timestamp":"2025-11-21T00:28:53.258Z","message":{"role":"toolResult","toolCallId":"toolu_01EpNbhE5W9znd6xf4XRPLrS","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/src/theme/theme.ts. Changed 283 characters to 531 characters."}],"details":{"diff":" ...\n 1 \n 2 function detectColorMode(): ColorMode {\n 3 \tconst colorterm = process.env.COLORTERM;\n 4 \tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n+134 \t\t// console.error(`[theme] Detected truecolor mode (COLORTERM=${colorterm}, TERM=${process.env.TERM})`);\n 134 \t\treturn \"truecolor\";\n 135 \t}\n 136 \tconst term = process.env.TERM || \"\";\n 137 \tif (term.includes(\"256color\")) {\n+139 \t\t// console.error(`[theme] Detected 256color mode (TERM=${term})`);\n 138 \t\treturn \"256color\";\n 139 \t}\n+142 \t// console.error(`[theme] Defaulting to 256color mode (TERM=${term})`);\n 140 \treturn \"256color\";\n 141 }\n 142 \n 143 function hexToRgb(hex: string): { r: number; g: number; b: number } {\n ..."},"isError":false,"timestamp":1763684933253}} +{"type":"message","timestamp":"2025-11-21T00:28:53.247Z","message":{"role":"assistant","content":[{"type":"text","text":"For `#00d7ff` in truecolor mode, we'd generate:\n- RGB: `r=0, g=215, b=255`\n- ANSI: `\\x1b[38;2;0;215;255m`\n\nBoth terminals should receive this exact sequence. The visual difference you're seeing is likely due to:\n\n1. **Color calibration** - VS Code terminal might have color adjustment/dimming settings\n2. **Theme interference** - VS Code might apply its own theme colors on top\n3. **Font rendering** - Different anti-aliasing/rendering can make colors appear different\n\nActually, looking at your screenshot more carefully - the cyan in Ghostty looks **much brighter/more saturated** than in VS Code. This suggests VS Code might be:\n- Applying opacity/alpha to the terminal\n- Using a \"minimum contrast\" feature that dims bright colors\n- Converting truecolor to 256-color despite reporting truecolor support\n\nLet me check VS Code terminal settings. Do you have any of these enabled in VS Code?\n- \"Terminal > Integrated: Minimum Contrast Ratio\"\n- \"Terminal > Integrated: GPU Acceleration\"\n- Any workspace color theme that might affect terminal colors\n\nOr we could add debug logging to verify which color mode is actually being detected:"},{"type":"toolCall","id":"toolu_01EpNbhE5W9znd6xf4XRPLrS","name":"edit","arguments":{"path":"packages/coding-agent/src/theme/theme.ts","oldText":"function detectColorMode(): ColorMode {\n\tconst colorterm = Bun.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = Bun.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\treturn \"256color\";\n\t}\n\treturn \"256color\";\n}","newText":"function detectColorMode(): ColorMode {\n\tconst colorterm = Bun.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\t// console.error(`[theme] Detected truecolor mode (COLORTERM=${colorterm}, TERM=${Bun.env.TERM})`);\n\t\treturn \"truecolor\";\n\t}\n\tconst term = Bun.env.TERM || \"\";\n\tif (term.includes(\"256color\")) {\n\t\t// console.error(`[theme] Detected 256color mode (TERM=${term})`);\n\t\treturn \"256color\";\n\t}\n\t// console.error(`[theme] Defaulting to 256color mode (TERM=${term})`);\n\treturn \"256color\";\n}"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":667,"cacheRead":95113,"cacheWrite":418,"cost":{"input":0.000018,"output":0.010005,"cacheRead":0.028533899999999997,"cacheWrite":0.0015675,"total":0.0401244}},"stopReason":"toolUse","timestamp":1763684919338}} +{"type":"message","timestamp":"2025-11-21T00:28:53.258Z","message":{"role":"toolResult","toolCallId":"toolu_01EpNbhE5W9znd6xf4XRPLrS","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/src/theme/theme.ts. Changed 283 characters to 531 characters."}],"details":{"diff":" ...\n 1 \n 2 function detectColorMode(): ColorMode {\n 3 \tconst colorterm = Bun.env.COLORTERM;\n 4 \tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n+134 \t\t// console.error(`[theme] Detected truecolor mode (COLORTERM=${colorterm}, TERM=${Bun.env.TERM})`);\n 134 \t\treturn \"truecolor\";\n 135 \t}\n 136 \tconst term = Bun.env.TERM || \"\";\n 137 \tif (term.includes(\"256color\")) {\n+139 \t\t// console.error(`[theme] Detected 256color mode (TERM=${term})`);\n 138 \t\treturn \"256color\";\n 139 \t}\n+142 \t// console.error(`[theme] Defaulting to 256color mode (TERM=${term})`);\n 140 \treturn \"256color\";\n 141 }\n 142 \n 143 function hexToRgb(hex: string): { r: number; g: number; b: number } {\n ..."},"isError":false,"timestamp":1763684933253}} {"type":"message","timestamp":"2025-11-21T00:29:00.613Z","message":{"role":"assistant","content":[{"type":"text","text":"The commented-out debug lines are there if you want to enable them. But my guess is that both terminals are getting the same ANSI codes, and the visual difference is purely VS Code's terminal rendering settings being more conservative/dimmed compared to Ghostty's more vibrant rendering.\n\nYou could try:\n1. In VS Code: Check Settings → \"terminal.integrated.minimumContrastRatio\" (set to 1 to disable contrast adjustment)\n2. In VS Code: Check if GPU acceleration is enabled/disabled and try toggling it\n3. Compare by running `printf \"\\x1b[38;2;0;215;255mTEST\\x1b[0m\\n\"` in both terminals to see if the raw color rendering differs"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":5,"output":169,"cacheRead":95531,"cacheWrite":708,"cost":{"input":0.000015,"output":0.002535,"cacheRead":0.0286593,"cacheWrite":0.0026550000000000002,"total":0.0338643}},"stopReason":"stop","timestamp":1763684933253}} {"type":"message","timestamp":"2025-11-21T00:30:33.225Z","message":{"role":"user","content":[{"type":"text","text":"vs code\n➜ pi-mono git:(main) ✗ npx tsx packages/coding-agent/src/cli.ts \n[theme] Detected truecolor mode (COLORTERM=truecolor, TERM=xterm-256color)\n\nghostty\n➜ pi-mono git:(main) ✗ npx tsx packages/coding-agent/src/cli.ts\n[theme] Detected truecolor mode (COLORTERM=truecolor, TERM=xterm-ghostty)\n\nwtf"}],"timestamp":1763685033213}} {"type":"message","timestamp":"2025-11-21T00:30:38.503Z","message":{"role":"assistant","content":[{"type":"text","text":"OK, so both are detecting truecolor correctly. The difference must be in how the terminals are rendering the same RGB values.\n\nLet me check if maybe the theme detection for light vs dark is different:"},{"type":"toolCall","id":"toolu_01P4UAdqizMtNUEWh2rCFjAC","name":"bash","arguments":{"command":"grep -A 15 \"function detectTerminalBackground\" packages/coding-agent/src/theme/theme.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":2,"output":119,"cacheRead":96239,"cacheWrite":298,"cost":{"input":0.000006,"output":0.0017850000000000001,"cacheRead":0.0288717,"cacheWrite":0.0011175,"total":0.0317802}},"stopReason":"toolUse","timestamp":1763685033215}} -{"type":"message","timestamp":"2025-11-21T00:30:38.524Z","message":{"role":"toolResult","toolCallId":"toolu_01P4UAdqizMtNUEWh2rCFjAC","toolName":"bash","content":[{"type":"text","text":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n"}],"isError":false,"timestamp":1763685038517}} -{"type":"message","timestamp":"2025-11-21T00:30:47.977Z","message":{"role":"assistant","content":[{"type":"text","text":"Can you check what `$COLORFGBG` is set to in both terminals?\n\n```bash\necho $COLORFGBG\n```\n\nIf VS Code has a light background color (or COLORFGBG is set differently), it might be loading the `light` theme instead of `dark`, which would explain why the cyan looks different (the light theme has `darkCyan: #008899` instead of `cyan: #00d7ff`).\n\nAlso, let me add more debug output:"},{"type":"toolCall","id":"toolu_01EGafkE5B44WNAzEot5DNf3","name":"edit","arguments":{"path":"packages/coding-agent/src/theme/theme.ts","oldText":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}","newText":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n\t\t\t\tconsole.error(`[theme] Detected ${result} background (COLORFGBG=${colorfgbg}, bg=${bg})`);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tconsole.error(`[theme] No COLORFGBG, defaulting to dark (COLORFGBG=${colorfgbg})`);\n\treturn \"dark\";\n}"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":5,"output":554,"cacheRead":96537,"cacheWrite":279,"cost":{"input":0.000015,"output":0.00831,"cacheRead":0.0289611,"cacheWrite":0.00104625,"total":0.03833235}},"stopReason":"toolUse","timestamp":1763685038517}} +{"type":"message","timestamp":"2025-11-21T00:30:38.524Z","message":{"role":"toolResult","toolCallId":"toolu_01P4UAdqizMtNUEWh2rCFjAC","toolName":"bash","content":[{"type":"text","text":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = Bun.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n"}],"isError":false,"timestamp":1763685038517}} +{"type":"message","timestamp":"2025-11-21T00:30:47.977Z","message":{"role":"assistant","content":[{"type":"text","text":"Can you check what `$COLORFGBG` is set to in both terminals?\n\n```bash\necho $COLORFGBG\n```\n\nIf VS Code has a light background color (or COLORFGBG is set differently), it might be loading the `light` theme instead of `dark`, which would explain why the cyan looks different (the light theme has `darkCyan: #008899` instead of `cyan: #00d7ff`).\n\nAlso, let me add more debug output:"},{"type":"toolCall","id":"toolu_01EGafkE5B44WNAzEot5DNf3","name":"edit","arguments":{"path":"packages/coding-agent/src/theme/theme.ts","oldText":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = Bun.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}","newText":"function detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = Bun.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n\t\t\t\tconsole.error(`[theme] Detected ${result} background (COLORFGBG=${colorfgbg}, bg=${bg})`);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tconsole.error(`[theme] No COLORFGBG, defaulting to dark (COLORFGBG=${colorfgbg})`);\n\treturn \"dark\";\n}"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":5,"output":554,"cacheRead":96537,"cacheWrite":279,"cost":{"input":0.000015,"output":0.00831,"cacheRead":0.0289611,"cacheWrite":0.00104625,"total":0.03833235}},"stopReason":"toolUse","timestamp":1763685038517}} {"type":"message","timestamp":"2025-11-21T00:30:47.988Z","message":{"role":"toolResult","toolCallId":"toolu_01EGafkE5B44WNAzEot5DNf3","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/src/theme/theme.ts. Changed 320 characters to 527 characters."}],"details":{"diff":" ...\n 1 \t\tconst parts = colorfgbg.split(\";\");\n 2 \t\tif (parts.length >= 2) {\n 3 \t\t\tconst bg = parseInt(parts[1], 10);\n 4 \t\t\tif (!Number.isNaN(bg)) {\n-404 \t\t\t\treturn bg < 8 ? \"dark\" : \"light\";\n+404 \t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n+405 \t\t\t\tconsole.error(`[theme] Detected ${result} background (COLORFGBG=${colorfgbg}, bg=${bg})`);\n+406 \t\t\t\treturn result;\n 405 \t\t\t}\n 406 \t\t}\n 407 \t}\n+410 \tconsole.error(`[theme] No COLORFGBG, defaulting to dark (COLORFGBG=${colorfgbg})`);\n 408 \treturn \"dark\";\n 409 }\n 410 \n 411 function getDefaultTheme(): string {\n ..."},"isError":false,"timestamp":1763685047982}} {"type":"message","timestamp":"2025-11-21T00:30:52.082Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01VGQ8A9dgHxViGc6V2K1ME2","name":"bash","arguments":{"command":"cd packages/coding-agent && npm run build 2>&1 | tail -3","timeout":30}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":4,"output":86,"cacheRead":96816,"cacheWrite":595,"cost":{"input":0.000012,"output":0.0012900000000000001,"cacheRead":0.0290448,"cacheWrite":0.00223125,"total":0.03257805}},"stopReason":"toolUse","timestamp":1763685047982}} {"type":"message","timestamp":"2025-11-21T00:30:53.088Z","message":{"role":"toolResult","toolCallId":"toolu_01VGQ8A9dgHxViGc6V2K1ME2","toolName":"bash","content":[{"type":"text","text":"\nWaiting for the debugger to disconnect...\nWaiting for the debugger to disconnect...\n"}],"isError":false,"timestamp":1763685053072}} @@ -377,8 +377,8 @@ {"type":"message","timestamp":"2025-11-21T00:32:43.119Z","message":{"role":"toolResult","toolCallId":"toolu_01JLwtFx2PxDgFqnzLp8gcE5","toolName":"write","content":[{"type":"text","text":"Successfully wrote 3124 bytes to packages/coding-agent/test/test-theme-colors.ts"}],"isError":false,"timestamp":1763685163113}} {"type":"message","timestamp":"2025-11-21T00:32:47.510Z","message":{"role":"assistant","content":[{"type":"text","text":"Wait, I need to export `loadTheme` first:"},{"type":"toolCall","id":"toolu_01KSjKYUGJAFxicjbrd78Z9j","name":"bash","arguments":{"command":"grep \"^function loadTheme\\|^export function loadTheme\" packages/coding-agent/src/theme/theme.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":4,"output":93,"cacheRead":97661,"cacheWrite":1148,"cost":{"input":0.000012,"output":0.001395,"cacheRead":0.0292983,"cacheWrite":0.004305,"total":0.035010299999999994}},"stopReason":"toolUse","timestamp":1763685163113}} {"type":"message","timestamp":"2025-11-21T00:32:47.530Z","message":{"role":"toolResult","toolCallId":"toolu_01KSjKYUGJAFxicjbrd78Z9j","toolName":"bash","content":[{"type":"text","text":"function loadThemeJson(name: string): ThemeJson {\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n"}],"isError":false,"timestamp":1763685167524}} -{"type":"message","timestamp":"2025-11-21T00:32:53.633Z","message":{"role":"assistant","content":[{"type":"text","text":"It's not exported. Let me fix the test to use the exported API:"},{"type":"toolCall","id":"toolu_01ApQgR2He9obNghvEcZCe23","name":"edit","arguments":{"path":"packages/coding-agent/test/test-theme-colors.ts","oldText":"import { loadTheme } from \"../src/theme/theme.js\";\n\n// Load dark theme explicitly\nconst theme = loadTheme(\"dark\", \"truecolor\");","newText":"import { initTheme, theme } from \"../src/theme/theme.js\";\n\n// Initialize with dark theme explicitly\nprocess.env.COLORTERM = \"truecolor\";\ninitTheme(\"dark\");"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":4,"output":199,"cacheRead":98809,"cacheWrite":137,"cost":{"input":0.000012,"output":0.0029850000000000002,"cacheRead":0.029642699999999998,"cacheWrite":0.00051375,"total":0.03315345}},"stopReason":"toolUse","timestamp":1763685167524}} -{"type":"message","timestamp":"2025-11-21T00:32:53.644Z","message":{"role":"toolResult","toolCallId":"toolu_01ApQgR2He9obNghvEcZCe23","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/test/test-theme-colors.ts. Changed 127 characters to 155 characters."}],"details":{"diff":"- 1 import { loadTheme } from \"../src/theme/theme.js\";\n+ 1 import { initTheme, theme } from \"../src/theme/theme.js\";\n 2 \n- 3 // Load dark theme explicitly\n- 4 const theme = loadTheme(\"dark\", \"truecolor\");\n+ 3 // Initialize with dark theme explicitly\n+ 4 process.env.COLORTERM = \"truecolor\";\n+ 5 initTheme(\"dark\");\n 5 \n 6 console.log(\"\\n=== Foreground Colors ===\\n\");\n 7 \n 8 // Core UI colors\n ..."},"isError":false,"timestamp":1763685173636}} +{"type":"message","timestamp":"2025-11-21T00:32:53.633Z","message":{"role":"assistant","content":[{"type":"text","text":"It's not exported. Let me fix the test to use the exported API:"},{"type":"toolCall","id":"toolu_01ApQgR2He9obNghvEcZCe23","name":"edit","arguments":{"path":"packages/coding-agent/test/test-theme-colors.ts","oldText":"import { loadTheme } from \"../src/theme/theme.js\";\n\n// Load dark theme explicitly\nconst theme = loadTheme(\"dark\", \"truecolor\");","newText":"import { initTheme, theme } from \"../src/theme/theme.js\";\n\n// Initialize with dark theme explicitly\nBun.env.COLORTERM = \"truecolor\";\ninitTheme(\"dark\");"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":4,"output":199,"cacheRead":98809,"cacheWrite":137,"cost":{"input":0.000012,"output":0.0029850000000000002,"cacheRead":0.029642699999999998,"cacheWrite":0.00051375,"total":0.03315345}},"stopReason":"toolUse","timestamp":1763685167524}} +{"type":"message","timestamp":"2025-11-21T00:32:53.644Z","message":{"role":"toolResult","toolCallId":"toolu_01ApQgR2He9obNghvEcZCe23","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/test/test-theme-colors.ts. Changed 127 characters to 155 characters."}],"details":{"diff":"- 1 import { loadTheme } from \"../src/theme/theme.js\";\n+ 1 import { initTheme, theme } from \"../src/theme/theme.js\";\n 2 \n- 3 // Load dark theme explicitly\n- 4 const theme = loadTheme(\"dark\", \"truecolor\");\n+ 3 // Initialize with dark theme explicitly\n+ 4 Bun.env.COLORTERM = \"truecolor\";\n+ 5 initTheme(\"dark\");\n 5 \n 6 console.log(\"\\n=== Foreground Colors ===\\n\");\n 7 \n 8 // Core UI colors\n ..."},"isError":false,"timestamp":1763685173636}} {"type":"message","timestamp":"2025-11-21T00:33:00.810Z","message":{"role":"assistant","content":[{"type":"text","text":"Now run it in both terminals:\n\n```bash\nnpx tsx packages/coding-agent/test/test-theme-colors.ts\n```\n\nThis will show you:\n1. All the themed colors side by side\n2. The raw ANSI codes being generated\n3. A direct comparison between a hardcoded `#00d7ff` and what the theme produces\n\nCompare the output in both terminals, especially:\n- The \"Raw ANSI Codes\" section - make sure they're identical\n- The \"Hex Color Test\" section - if the direct test looks the same in both terminals but the theme one doesn't, then there's an issue with our theme code\n- If both look different between terminals, it's purely a terminal rendering difference"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":165,"cacheRead":98946,"cacheWrite":242,"cost":{"input":0.000009,"output":0.002475,"cacheRead":0.0296838,"cacheWrite":0.0009075,"total":0.033075299999999995}},"stopReason":"stop","timestamp":1763685173637}} {"type":"message","timestamp":"2025-11-21T00:37:12.385Z","message":{"role":"user","content":[{"type":"text","text":"'/var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/TemporaryItems/NSIRD_screencaptureui_t8nr4u/Screenshot 2025-11-21 at 01.36.58.png'\n\nsome differences, muted and dim are the same in vs code terminal, wtf?"}],"timestamp":1763685432376}} {"type":"message","timestamp":"2025-11-21T00:37:17.904Z","message":{"role":"assistant","content":[{"type":"text","text":"Looking at the screenshot, I can see:\n\n1. **VS Code (left)**: `muted` and `dim` render the same (gray)\n2. **Ghostty (right)**: `muted` and `dim` are different shades\n\nLet me check what those values are in the theme:"},{"type":"toolCall","id":"toolu_01VU7LkK8gWm3dkk9r2zsMEp","name":"bash","arguments":{"command":"grep -E \"\\\"muted\\\"|\\\"dim\\\"|\\\"gray\\\"|\\\"dimGray\\\"\" packages/coding-agent/src/theme/dark.json"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":0,"output":155,"cacheRead":99188,"cacheWrite":263,"cost":{"input":0,"output":0.0023250000000000002,"cacheRead":0.0297564,"cacheWrite":0.00098625,"total":0.03306765}},"stopReason":"toolUse","timestamp":1763685432377}} @@ -399,8 +399,8 @@ {"type":"message","timestamp":"2025-11-21T00:38:11.003Z","message":{"role":"toolResult","toolCallId":"toolu_01N2Kwdb8oZwrrF1JAuULitj","toolName":"bash","content":[{"type":"text","text":"\nWaiting for the debugger to disconnect...\nWaiting for the debugger to disconnect...\n"}],"isError":false,"timestamp":1763685491000}} {"type":"message","timestamp":"2025-11-21T00:38:17.996Z","message":{"role":"assistant","content":[{"type":"text","text":"Perfect! Now all colors use hex RGB values. The conversions I made:\n\n**Dark theme:**\n- `gray: 242` → `#6c6c6c` (medium gray)\n- `dimGray: 238` → `#444444` (darker gray)\n- `darkGray: 236` → `#303030` (very dark gray)\n\n**Light theme:**\n- `mediumGray: 242` → `#6c6c6c` (medium gray)\n- `dimGray: 246` → `#949494` (lighter gray)\n- `lightGray: 250` → `#bcbcbc` (very light gray)\n\nNow run the test again in both terminals:\n```bash\nnpx tsx packages/coding-agent/test/test-theme-colors.ts\n```\n\nBoth should now render identically since we're using pure RGB values instead of relying on the terminal's 256-color palette interpretation!"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":1,"output":223,"cacheRead":102747,"cacheWrite":127,"cost":{"input":0.000003,"output":0.003345,"cacheRead":0.0308241,"cacheWrite":0.00047625,"total":0.034648349999999994}},"stopReason":"stop","timestamp":1763685491000}} {"type":"message","timestamp":"2025-11-21T00:38:43.940Z","message":{"role":"user","content":[{"type":"text","text":"fucking hell, the vs code shit still looks the same for all gray levels"}],"timestamp":1763685523931}} -{"type":"message","timestamp":"2025-11-21T00:38:56.810Z","message":{"role":"assistant","content":[{"type":"text","text":"That's... really weird. VS Code should render different RGB values differently. Let me add more detailed debugging to see what's actually happening:"},{"type":"toolCall","id":"toolu_011S4Fa9UNuMxjfiZKBKNQeq","name":"edit","arguments":{"path":"packages/coding-agent/test/test-theme-colors.ts","oldText":"console.log(\"\\n=== Raw ANSI Codes ===\\n\");\nconsole.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\nconsole.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\n\nconsole.log(\"\\n=== Hex Color Test ===\\n\");\nconsole.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\nconsole.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n\nconsole.log(\"\\n\");","newText":"console.log(\"\\n=== Raw ANSI Codes ===\\n\");\nconsole.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\nconsole.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\nconsole.log(\"muted ANSI:\", JSON.stringify(theme.getFgAnsi(\"muted\")));\nconsole.log(\"dim ANSI:\", JSON.stringify(theme.getFgAnsi(\"dim\")));\n\nconsole.log(\"\\n=== Direct RGB Test ===\\n\");\nconsole.log(\"Gray #6c6c6c: \\x1b[38;2;108;108;108mSample text\\x1b[0m\");\nconsole.log(\"Gray #444444: \\x1b[38;2;68;68;68mSample text\\x1b[0m\");\nconsole.log(\"Gray #303030: \\x1b[38;2;48;48;48mSample text\\x1b[0m\");\n\nconsole.log(\"\\n=== Hex Color Test ===\\n\");\nconsole.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\nconsole.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n\nconsole.log(\"\\n=== Environment ===\\n\");\nconsole.log(\"TERM:\", process.env.TERM);\nconsole.log(\"COLORTERM:\", process.env.COLORTERM);\nconsole.log(\"Color mode:\", theme.getColorMode());\n\nconsole.log(\"\\n\");"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":0,"output":704,"cacheRead":102874,"cacheWrite":242,"cost":{"input":0,"output":0.01056,"cacheRead":0.0308622,"cacheWrite":0.0009075,"total":0.0423297}},"stopReason":"toolUse","timestamp":1763685523932}} -{"type":"message","timestamp":"2025-11-21T00:38:56.822Z","message":{"role":"toolResult","toolCallId":"toolu_011S4Fa9UNuMxjfiZKBKNQeq","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/test/test-theme-colors.ts. Changed 426 characters to 995 characters."}],"details":{"diff":" ...\n 1 \n 2 console.log(\"\\n=== Raw ANSI Codes ===\\n\");\n 3 console.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\n 4 console.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\n+57 console.log(\"muted ANSI:\", JSON.stringify(theme.getFgAnsi(\"muted\")));\n+58 console.log(\"dim ANSI:\", JSON.stringify(theme.getFgAnsi(\"dim\")));\n 57 \n+60 console.log(\"\\n=== Direct RGB Test ===\\n\");\n+61 console.log(\"Gray #6c6c6c: \\x1b[38;2;108;108;108mSample text\\x1b[0m\");\n+62 console.log(\"Gray #444444: \\x1b[38;2;68;68;68mSample text\\x1b[0m\");\n+63 console.log(\"Gray #303030: \\x1b[38;2;48;48;48mSample text\\x1b[0m\");\n+64 \n 58 console.log(\"\\n=== Hex Color Test ===\\n\");\n 59 console.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\n 60 console.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n 61 \n+69 console.log(\"\\n=== Environment ===\\n\");\n+70 console.log(\"TERM:\", process.env.TERM);\n+71 console.log(\"COLORTERM:\", process.env.COLORTERM);\n+72 console.log(\"Color mode:\", theme.getColorMode());\n+73 \n 62 console.log(\"\\n\");"},"isError":false,"timestamp":1763685536814}} +{"type":"message","timestamp":"2025-11-21T00:38:56.810Z","message":{"role":"assistant","content":[{"type":"text","text":"That's... really weird. VS Code should render different RGB values differently. Let me add more detailed debugging to see what's actually happening:"},{"type":"toolCall","id":"toolu_011S4Fa9UNuMxjfiZKBKNQeq","name":"edit","arguments":{"path":"packages/coding-agent/test/test-theme-colors.ts","oldText":"console.log(\"\\n=== Raw ANSI Codes ===\\n\");\nconsole.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\nconsole.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\n\nconsole.log(\"\\n=== Hex Color Test ===\\n\");\nconsole.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\nconsole.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n\nconsole.log(\"\\n\");","newText":"console.log(\"\\n=== Raw ANSI Codes ===\\n\");\nconsole.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\nconsole.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\nconsole.log(\"muted ANSI:\", JSON.stringify(theme.getFgAnsi(\"muted\")));\nconsole.log(\"dim ANSI:\", JSON.stringify(theme.getFgAnsi(\"dim\")));\n\nconsole.log(\"\\n=== Direct RGB Test ===\\n\");\nconsole.log(\"Gray #6c6c6c: \\x1b[38;2;108;108;108mSample text\\x1b[0m\");\nconsole.log(\"Gray #444444: \\x1b[38;2;68;68;68mSample text\\x1b[0m\");\nconsole.log(\"Gray #303030: \\x1b[38;2;48;48;48mSample text\\x1b[0m\");\n\nconsole.log(\"\\n=== Hex Color Test ===\\n\");\nconsole.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\nconsole.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n\nconsole.log(\"\\n=== Environment ===\\n\");\nconsole.log(\"TERM:\", Bun.env.TERM);\nconsole.log(\"COLORTERM:\", Bun.env.COLORTERM);\nconsole.log(\"Color mode:\", theme.getColorMode());\n\nconsole.log(\"\\n\");"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":0,"output":704,"cacheRead":102874,"cacheWrite":242,"cost":{"input":0,"output":0.01056,"cacheRead":0.0308622,"cacheWrite":0.0009075,"total":0.0423297}},"stopReason":"toolUse","timestamp":1763685523932}} +{"type":"message","timestamp":"2025-11-21T00:38:56.822Z","message":{"role":"toolResult","toolCallId":"toolu_011S4Fa9UNuMxjfiZKBKNQeq","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/test/test-theme-colors.ts. Changed 426 characters to 995 characters."}],"details":{"diff":" ...\n 1 \n 2 console.log(\"\\n=== Raw ANSI Codes ===\\n\");\n 3 console.log(\"thinkingMedium ANSI:\", JSON.stringify(theme.getFgAnsi(\"thinkingMedium\")));\n 4 console.log(\"accent ANSI:\", JSON.stringify(theme.getFgAnsi(\"accent\")));\n+57 console.log(\"muted ANSI:\", JSON.stringify(theme.getFgAnsi(\"muted\")));\n+58 console.log(\"dim ANSI:\", JSON.stringify(theme.getFgAnsi(\"dim\")));\n 57 \n+60 console.log(\"\\n=== Direct RGB Test ===\\n\");\n+61 console.log(\"Gray #6c6c6c: \\x1b[38;2;108;108;108mSample text\\x1b[0m\");\n+62 console.log(\"Gray #444444: \\x1b[38;2;68;68;68mSample text\\x1b[0m\");\n+63 console.log(\"Gray #303030: \\x1b[38;2;48;48;48mSample text\\x1b[0m\");\n+64 \n 58 console.log(\"\\n=== Hex Color Test ===\\n\");\n 59 console.log(\"Direct #00d7ff test: \\x1b[38;2;0;215;255mBRIGHT CYAN\\x1b[0m\");\n 60 console.log(\"Theme cyan (should match above):\", theme.fg(\"accent\", \"BRIGHT CYAN\"));\n 61 \n+69 console.log(\"\\n=== Environment ===\\n\");\n+70 console.log(\"TERM:\", Bun.env.TERM);\n+71 console.log(\"COLORTERM:\", Bun.env.COLORTERM);\n+72 console.log(\"Color mode:\", theme.getColorMode());\n+73 \n 62 console.log(\"\\n\");"},"isError":false,"timestamp":1763685536814}} {"type":"message","timestamp":"2025-11-21T00:39:00.354Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_0168nZqbo23T17PjAN7PRGc8","name":"bash","arguments":{"command":"cd packages/coding-agent && npm run build 2>&1 | tail -3","timeout":30}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":2,"output":86,"cacheRead":103116,"cacheWrite":744,"cost":{"input":0.000006,"output":0.0012900000000000001,"cacheRead":0.0309348,"cacheWrite":0.00279,"total":0.0350208}},"stopReason":"toolUse","timestamp":1763685536814}} {"type":"message","timestamp":"2025-11-21T00:39:01.462Z","message":{"role":"toolResult","toolCallId":"toolu_0168nZqbo23T17PjAN7PRGc8","toolName":"bash","content":[{"type":"text","text":"\nWaiting for the debugger to disconnect...\nWaiting for the debugger to disconnect...\n"}],"isError":false,"timestamp":1763685541456}} {"type":"message","timestamp":"2025-11-21T00:39:09.109Z","message":{"role":"assistant","content":[{"type":"text","text":"Run this again in VS Code. If the \"Direct RGB Test\" section shows all three grays as the same, then **VS Code terminal itself is broken** and is not properly rendering different RGB values in the gray range.\n\nIf that's the case, it's a VS Code bug/limitation, not our code. But I want to verify this is what's happening.\n\nAlso check VS Code settings:\n1. Open Settings (Cmd+,)\n2. Search for \"terminal.integrated.minimumContrastRatio\"\n3. Set it to `1` to disable contrast adjustment\n4. Search for \"terminal.integrated.gpuAcceleration\" \n5. Try toggling it\n\nVS Code's terminal has known issues with color fidelity, especially with subtle gray differences."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":2,"output":172,"cacheRead":103860,"cacheWrite":119,"cost":{"input":0.000006,"output":0.0025800000000000003,"cacheRead":0.031157999999999998,"cacheWrite":0.00044625000000000003,"total":0.03419025}},"stopReason":"stop","timestamp":1763685541456}} @@ -522,7 +522,7 @@ {"type":"thinking_level_change","timestamp":"2025-11-21T01:02:48.923Z","thinkingLevel":"off"} {"type":"message","timestamp":"2025-11-21T01:03:06.866Z","message":{"role":"user","content":[{"type":"text","text":"what's footer using? should be dimmer, don't modify the theme, look into the footer sources"}],"timestamp":1763686986852}} {"type":"message","timestamp":"2025-11-21T01:03:12.543Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01W2HjH6QvLstYkCrDH8r7Zi","name":"read","arguments":{"path":"packages/coding-agent/src/tui/footer.ts"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":64,"cacheRead":0,"cacheWrite":112082,"cost":{"input":0.000009,"output":0.00096,"cacheRead":0,"cacheWrite":0.4203075,"total":0.4212765}},"stopReason":"toolUse","timestamp":1763686986853}} -{"type":"message","timestamp":"2025-11-21T01:03:12.551Z","message":{"role":"toolResult","toolCallId":"toolu_01W2HjH6QvLstYkCrDH8r7Zi","toolName":"read","content":[{"type":"text","text":"import type { AgentState } from \"@oh-my-pi/pi-agent\";\nimport type { AssistantMessage } from \"@oh-my-pi/pi-ai\";\nimport { type Component, visibleWidth } from \"@oh-my-pi/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : \"0.0\";\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\t\tif (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);\n\t\tstatsParts.push(`${contextPercent}%`);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"muted\", pwd), theme.fg(\"muted\", statsLine)];\n\t}\n}\n"}],"isError":false,"timestamp":1763686992545}} +{"type":"message","timestamp":"2025-11-21T01:03:12.551Z","message":{"role":"toolResult","toolCallId":"toolu_01W2HjH6QvLstYkCrDH8r7Zi","toolName":"read","content":[{"type":"text","text":"import type { AgentState } from \"@oh-my-pi/pi-agent\";\nimport type { AssistantMessage } from \"@oh-my-pi/pi-ai\";\nimport { type Component, visibleWidth } from \"@oh-my-pi/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : \"0.0\";\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = Bun.env.HOME || Bun.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\t\tif (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);\n\t\tstatsParts.push(`${contextPercent}%`);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"muted\", pwd), theme.fg(\"muted\", statsLine)];\n\t}\n}\n"}],"isError":false,"timestamp":1763686992545}} {"type":"message","timestamp":"2025-11-21T01:03:18.372Z","message":{"role":"assistant","content":[{"type":"text","text":"The footer is using `theme.fg(\"muted\", ...)` for both the pwd and stats lines. It should use `\"dim\"` instead to be dimmer:"},{"type":"toolCall","id":"toolu_01MrTFnuD1GTKFfTkZBGVZb9","name":"edit","arguments":{"path":"packages/coding-agent/src/tui/footer.ts","oldText":"\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"muted\", pwd), theme.fg(\"muted\", statsLine)];","newText":"\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":6,"output":198,"cacheRead":112082,"cacheWrite":1651,"cost":{"input":0.000018,"output":0.00297,"cacheRead":0.0336246,"cacheWrite":0.006191250000000001,"total":0.04280385}},"stopReason":"toolUse","timestamp":1763686992545}} {"type":"message","timestamp":"2025-11-21T01:03:18.387Z","message":{"role":"toolResult","toolCallId":"toolu_01MrTFnuD1GTKFfTkZBGVZb9","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/src/tui/footer.ts. Changed 101 characters to 97 characters."}],"details":{"diff":" ...\n 1 \t\t\t}\n 2 \t\t}\n 3 \n 4 \t\t// Return two lines: pwd and stats\n-133 \t\treturn [theme.fg(\"muted\", pwd), theme.fg(\"muted\", statsLine)];\n+133 \t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];\n 134 \t}\n 135 }"},"isError":false,"timestamp":1763686998379}} {"type":"message","timestamp":"2025-11-21T01:03:22.771Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01VaJi8YXcmTzPuZmqyTwTUN","name":"bash","arguments":{"command":"cd packages/coding-agent && npm run build 2>&1 | tail -3","timeout":30}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":5,"output":86,"cacheRead":113733,"cacheWrite":240,"cost":{"input":0.000015,"output":0.0012900000000000001,"cacheRead":0.0341199,"cacheWrite":0.0009,"total":0.0363249}},"stopReason":"toolUse","timestamp":1763686998379}} @@ -961,7 +961,7 @@ {"type":"message","timestamp":"2025-11-21T02:05:19.499Z","message":{"role":"assistant","content":[{"type":"text","text":"Need to update that:"},{"type":"toolCall","id":"toolu_01NE1xXaEorPTd5yr8RKk9zL","name":"edit","arguments":{"path":"packages/coding-agent/README.md","oldText":"See [Theme Documentation](docs/theme.md) for:\n- Complete list of 43 color tokens\n- Theme format and examples\n- Color value formats (hex, RGB, terminal default)","newText":"See [Theme Documentation](docs/theme.md) for:\n- Complete list of 44 color tokens\n- Theme format and examples\n- Color value formats (hex, RGB, terminal default)"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":4,"output":181,"cacheRead":163869,"cacheWrite":94,"cost":{"input":0.000012,"output":0.002715,"cacheRead":0.049160699999999995,"cacheWrite":0.0003525,"total":0.052240199999999994}},"stopReason":"toolUse","timestamp":1763690713355}} {"type":"message","timestamp":"2025-11-21T02:05:19.507Z","message":{"role":"toolResult","toolCallId":"toolu_01NE1xXaEorPTd5yr8RKk9zL","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/README.md. Changed 159 characters to 159 characters."}],"details":{"diff":" ...\n 1 3. Edit `~/.pi/agent/themes/my-theme.json` - changes apply immediately on save\n 2 4. Iterate until satisfied (no need to re-select the theme)\n 3 \n 4 See [Theme Documentation](docs/theme.md) for:\n-327 - Complete list of 43 color tokens\n+327 - Complete list of 44 color tokens\n 328 - Theme format and examples\n 329 - Color value formats (hex, RGB, terminal default)\n 330 \n 331 Example custom theme:\n ..."},"isError":false,"timestamp":1763690719502}} {"type":"message","timestamp":"2025-11-21T02:05:23.829Z","message":{"role":"assistant","content":[{"type":"text","text":"Now check the CHANGELOG:"},{"type":"toolCall","id":"toolu_011S6ShGBcKzNpraY7qbokMS","name":"bash","arguments":{"command":"head -50 packages/coding-agent/CHANGELOG.md"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":71,"cacheRead":163963,"cacheWrite":218,"cost":{"input":0.000009,"output":0.001065,"cacheRead":0.0491889,"cacheWrite":0.0008175,"total":0.0510804}},"stopReason":"toolUse","timestamp":1763690719502}} -{"type":"message","timestamp":"2025-11-21T02:05:23.842Z","message":{"role":"toolResult","toolCallId":"toolu_011S6ShGBcKzNpraY7qbokMS","toolName":"bash","content":[{"type":"text","text":"# Changelog\n\n## [Unreleased]\n\n## [0.7.29] - 2025-11-20\n\n### Improved\n\n- **Read Tool Display**: When the `read` tool is called with offset/limit parameters, the tool execution now displays the line range in a compact format (e.g., `read src/main.ts:100-200` for offset=100, limit=100).\n\n## [0.7.28] - 2025-11-20\n\n### Added\n\n- **Message Queuing**: You can now send multiple messages while the agent is processing without waiting for the previous response to complete. Messages submitted during streaming are queued and processed based on your queue mode setting. Queued messages are shown in a pending area below the chat. Press Escape to abort and restore all queued messages to the editor. Use `/queue` to select between \"one-at-a-time\" (process queued messages sequentially, recommended) or \"all\" (process all queued messages at once). The queue mode setting is saved and persists across sessions. ([#15](https://github.com/badlogic/pi-mono/issues/15))\n\n## [0.7.27] - 2025-11-20\n\n### Fixed\n\n- **Slash Command Submission**: Fixed issue where slash commands required two Enter presses to execute. Now pressing Enter on a slash command autocomplete suggestion immediately submits the command, while Tab still applies the completion for adding arguments. ([#30](https://github.com/badlogic/pi-mono/issues/30))\n- **Slash Command Autocomplete**: Fixed issue where typing a typo then correcting it would not show autocomplete suggestions. Autocomplete now re-triggers when typing or backspacing in a slash command context. ([#29](https://github.com/badlogic/pi-mono/issues/29))\n\n## [0.7.26] - 2025-11-20\n\n### Added\n\n- **Tool Output Expansion**: Press `Ctrl+O` to toggle between collapsed and expanded tool output display. Expands all tool call outputs (bash, read, write, etc.) to show full content instead of truncated previews. ([#31](https://github.com/badlogic/pi-mono/issues/31))\n- **Custom Headers**: Added support for custom HTTP headers in `models.json` configuration. Headers can be specified at both provider and model level, with model-level headers overriding provider-level ones. This enables bypassing Cloudflare bot detection and other proxy requirements. ([#39](https://github.com/badlogic/pi-mono/issues/39))\n\n### Fixed\n\n- **Chutes AI Provider**: Fixed 400 errors when using Chutes AI provider. Added compatibility fixes for `store` field exclusion, `max_tokens` parameter usage, and system prompt role handling. ([#42](https://github.com/badlogic/pi-mono/pull/42) by [@butelo](https://github.com/butelo))\n- **Mistral/Chutes Syntax Error**: Fixed syntax error in merged PR that used `iif` instead of `if`.\n- **Anthropic OAuth Bug**: Fixed bug where `process.env.ANTHROPIC_API_KEY = undefined` set the env var to string \"undefined\" instead of deleting it. Now uses `delete` operator.\n\n## [0.7.25] - 2025-11-20\n\n### Added\n\n- **Model Cycling**: Press `Ctrl+P` to quickly cycle through models. Use `--models` CLI argument to scope to specific models (e.g., `--models claude-sonnet,gpt-4o`). Supports pattern matching and smart version selection (prefers aliases over dated versions). ([#37](https://github.com/badlogic/pi-mono/pull/37) by [@fightbulc](https://github.com/fightbulc))\n\n## [0.7.24] - 2025-11-20\n\n### Added\n\n- **Thinking Level Cycling**: Press `Shift+Tab` to cycle through thinking levels (off → minimal → low → medium → high) for reasoning-capable models. Editor border color changes to indicate current level (gray → blue → cyan → magenta). ([#36](https://github.com/badlogic/pi-mono/pull/36) by [@fightbulc](https://github.com/fightbulc))\n\n## [0.7.23] - 2025-11-20\n\n"}],"isError":false,"timestamp":1763690723836}} +{"type":"message","timestamp":"2025-11-21T02:05:23.842Z","message":{"role":"toolResult","toolCallId":"toolu_011S6ShGBcKzNpraY7qbokMS","toolName":"bash","content":[{"type":"text","text":"# Changelog\n\n## [Unreleased]\n\n## [0.7.29] - 2025-11-20\n\n### Improved\n\n- **Read Tool Display**: When the `read` tool is called with offset/limit parameters, the tool execution now displays the line range in a compact format (e.g., `read src/main.ts:100-200` for offset=100, limit=100).\n\n## [0.7.28] - 2025-11-20\n\n### Added\n\n- **Message Queuing**: You can now send multiple messages while the agent is processing without waiting for the previous response to complete. Messages submitted during streaming are queued and processed based on your queue mode setting. Queued messages are shown in a pending area below the chat. Press Escape to abort and restore all queued messages to the editor. Use `/queue` to select between \"one-at-a-time\" (process queued messages sequentially, recommended) or \"all\" (process all queued messages at once). The queue mode setting is saved and persists across sessions. ([#15](https://github.com/badlogic/pi-mono/issues/15))\n\n## [0.7.27] - 2025-11-20\n\n### Fixed\n\n- **Slash Command Submission**: Fixed issue where slash commands required two Enter presses to execute. Now pressing Enter on a slash command autocomplete suggestion immediately submits the command, while Tab still applies the completion for adding arguments. ([#30](https://github.com/badlogic/pi-mono/issues/30))\n- **Slash Command Autocomplete**: Fixed issue where typing a typo then correcting it would not show autocomplete suggestions. Autocomplete now re-triggers when typing or backspacing in a slash command context. ([#29](https://github.com/badlogic/pi-mono/issues/29))\n\n## [0.7.26] - 2025-11-20\n\n### Added\n\n- **Tool Output Expansion**: Press `Ctrl+O` to toggle between collapsed and expanded tool output display. Expands all tool call outputs (bash, read, write, etc.) to show full content instead of truncated previews. ([#31](https://github.com/badlogic/pi-mono/issues/31))\n- **Custom Headers**: Added support for custom HTTP headers in `models.json` configuration. Headers can be specified at both provider and model level, with model-level headers overriding provider-level ones. This enables bypassing Cloudflare bot detection and other proxy requirements. ([#39](https://github.com/badlogic/pi-mono/issues/39))\n\n### Fixed\n\n- **Chutes AI Provider**: Fixed 400 errors when using Chutes AI provider. Added compatibility fixes for `store` field exclusion, `max_tokens` parameter usage, and system prompt role handling. ([#42](https://github.com/badlogic/pi-mono/pull/42) by [@butelo](https://github.com/butelo))\n- **Mistral/Chutes Syntax Error**: Fixed syntax error in merged PR that used `iif` instead of `if`.\n- **Anthropic OAuth Bug**: Fixed bug where `Bun.env.ANTHROPIC_API_KEY = undefined` set the env var to string \"undefined\" instead of deleting it. Now uses `delete` operator.\n\n## [0.7.25] - 2025-11-20\n\n### Added\n\n- **Model Cycling**: Press `Ctrl+P` to quickly cycle through models. Use `--models` CLI argument to scope to specific models (e.g., `--models claude-sonnet,gpt-4o`). Supports pattern matching and smart version selection (prefers aliases over dated versions). ([#37](https://github.com/badlogic/pi-mono/pull/37) by [@fightbulc](https://github.com/fightbulc))\n\n## [0.7.24] - 2025-11-20\n\n### Added\n\n- **Thinking Level Cycling**: Press `Shift+Tab` to cycle through thinking levels (off → minimal → low → medium → high) for reasoning-capable models. Editor border color changes to indicate current level (gray → blue → cyan → magenta). ([#36](https://github.com/badlogic/pi-mono/pull/36) by [@fightbulc](https://github.com/fightbulc))\n\n## [0.7.23] - 2025-11-20\n\n"}],"isError":false,"timestamp":1763690723836}} {"type":"message","timestamp":"2025-11-21T02:05:38.966Z","message":{"role":"assistant","content":[{"type":"text","text":"The CHANGELOG needs a new entry for the theme system. Let me add it:"},{"type":"toolCall","id":"toolu_01H9J3hCR5Vrgsvnui3XazRW","name":"edit","arguments":{"path":"packages/coding-agent/CHANGELOG.md","oldText":"# Changelog\n\n## [Unreleased]\n\n## [0.7.29] - 2025-11-20","newText":"# Changelog\n\n## [Unreleased]\n\n### Added\n\n- **Theme System**: Full theming support with 44 customizable color tokens. Two built-in themes (`dark`, `light`) with auto-detection based on terminal background. Use `/theme` command to select themes interactively. Custom themes in `~/.pi/agent/themes/*.json` support live editing - changes apply immediately when the file is saved. Themes use RGB hex values for consistent rendering across terminals. VS Code users: set `terminal.integrated.minimumContrastRatio` to `1` for proper color rendering. See [Theme Documentation](docs/theme.md) for details.\n- **User Message Text Color**: User messages now respect the `userMessageText` theme token for text color (in addition to the existing `userMessageBg` background).\n- **Tool Title & Output Colors**: Tool execution boxes now use separate colors for titles (`toolTitle`) and output (`toolOutput`), replacing the unused `toolText` token.\n\n### Fixed\n\n- **Component Invalidation**: `Text` component now properly implements `invalidate()` method, ensuring theme changes apply correctly to all UI elements.\n- **TruncatedText Width Compliance**: Fixed `TruncatedText` component to properly pad all lines to exactly match the specified width, preventing rendering artifacts.\n- **TruncatedText Newline Handling**: `TruncatedText` now stops at the first newline and only displays the first line.\n- **Invalid Theme Handling**: Invalid or malformed themes no longer crash the application. On startup, invalid themes fall back to the dark theme silently. During theme selection, errors are displayed in the UI with a fallback to dark theme.\n\n## [0.7.29] - 2025-11-20"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":3,"output":534,"cacheRead":164181,"cacheWrite":1105,"cost":{"input":0.000009,"output":0.00801,"cacheRead":0.0492543,"cacheWrite":0.00414375,"total":0.06141705}},"stopReason":"toolUse","timestamp":1763690723836}} {"type":"message","timestamp":"2025-11-21T02:05:38.977Z","message":{"role":"toolResult","toolCallId":"toolu_01H9J3hCR5Vrgsvnui3XazRW","toolName":"edit","content":[{"type":"text","text":"Successfully replaced text in packages/coding-agent/CHANGELOG.md. Changed 54 characters to 1647 characters."}],"details":{"diff":" 1 # Changelog\n 2 \n 3 ## [Unreleased]\n 4 \n+ 5 ### Added\n+ 6 \n+ 7 - **Theme System**: Full theming support with 44 customizable color tokens. Two built-in themes (`dark`, `light`) with auto-detection based on terminal background. Use `/theme` command to select themes interactively. Custom themes in `~/.pi/agent/themes/*.json` support live editing - changes apply immediately when the file is saved. Themes use RGB hex values for consistent rendering across terminals. VS Code users: set `terminal.integrated.minimumContrastRatio` to `1` for proper color rendering. See [Theme Documentation](docs/theme.md) for details.\n+ 8 - **User Message Text Color**: User messages now respect the `userMessageText` theme token for text color (in addition to the existing `userMessageBg` background).\n+ 9 - **Tool Title & Output Colors**: Tool execution boxes now use separate colors for titles (`toolTitle`) and output (`toolOutput`), replacing the unused `toolText` token.\n+ 10 \n+ 11 ### Fixed\n+ 12 \n+ 13 - **Component Invalidation**: `Text` component now properly implements `invalidate()` method, ensuring theme changes apply correctly to all UI elements.\n+ 14 - **TruncatedText Width Compliance**: Fixed `TruncatedText` component to properly pad all lines to exactly match the specified width, preventing rendering artifacts.\n+ 15 - **TruncatedText Newline Handling**: `TruncatedText` now stops at the first newline and only displays the first line.\n+ 16 - **Invalid Theme Handling**: Invalid or malformed themes no longer crash the application. On startup, invalid themes fall back to the dark theme silently. During theme selection, errors are displayed in the UI with a fallback to dark theme.\n+ 17 \n 5 ## [0.7.29] - 2025-11-20\n 6 \n 7 ### Improved\n 8 \n ..."},"isError":false,"timestamp":1763690738971}} {"type":"message","timestamp":"2025-11-21T02:05:44.426Z","message":{"role":"assistant","content":[{"type":"text","text":"Perfect! Both README and CHANGELOG are now up to date with all the theme system changes and the new token count of 44."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":2,"output":30,"cacheRead":165286,"cacheWrite":573,"cost":{"input":0.000006,"output":0.00045,"cacheRead":0.0495858,"cacheWrite":0.00214875,"total":0.052190549999999995}},"stopReason":"stop","timestamp":1763690738971}} diff --git a/packages/coding-agent/test/streaming-render-debug.ts b/packages/coding-agent/test/streaming-render-debug.ts index c2fbe0777..0d0772e03 100644 --- a/packages/coding-agent/test/streaming-render-debug.ts +++ b/packages/coding-agent/test/streaming-render-debug.ts @@ -11,7 +11,7 @@ import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui"; import { sleep } from "bun"; // Initialize dark theme with full color support -process.env.COLORTERM = "truecolor"; +Bun.env.COLORTERM = "truecolor"; initTheme("dark"); async function main() { diff --git a/packages/coding-agent/test/test-theme-colors.ts b/packages/coding-agent/test/test-theme-colors.ts index b86ae2e76..f8eaeffc0 100644 --- a/packages/coding-agent/test/test-theme-colors.ts +++ b/packages/coding-agent/test/test-theme-colors.ts @@ -1,7 +1,7 @@ import { initTheme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; // Initialize with dark theme explicitly -process.env.COLORTERM = "truecolor"; +Bun.env.COLORTERM = "truecolor"; initTheme("dark"); console.log("\n=== Foreground Colors ===\n"); @@ -68,8 +68,8 @@ console.log("Direct #00d7ff test: \x1b[38;2;0;215;255mBRIGHT CYAN\x1b[0m"); console.log("Theme cyan (should match above):", theme.fg("accent", "BRIGHT CYAN")); console.log("\n=== Environment ===\n"); -console.log("TERM:", process.env.TERM); -console.log("COLORTERM:", process.env.COLORTERM); +console.log("TERM:", Bun.env.TERM); +console.log("COLORTERM:", Bun.env.COLORTERM); console.log("Color mode:", theme.getColorMode()); console.log("\n"); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index e8d9cf4c3..fa88b93f3 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -46,8 +46,8 @@ describe("Coding Agent Tools", () => { beforeEach(() => { // Force replace mode for edit tool tests using old_text/new_text - originalEditVariant = process.env.PI_EDIT_VARIANT; - process.env.PI_EDIT_VARIANT = "replace"; + originalEditVariant = Bun.env.PI_EDIT_VARIANT; + Bun.env.PI_EDIT_VARIANT = "replace"; // Create a unique temporary directory for each test testDir = path.join(os.tmpdir(), `coding-agent-test-${Snowflake.next()}`); @@ -69,9 +69,9 @@ describe("Coding Agent Tools", () => { // Restore original edit variant if (originalEditVariant === undefined) { - delete process.env.PI_EDIT_VARIANT; + delete Bun.env.PI_EDIT_VARIANT; } else { - process.env.PI_EDIT_VARIANT = originalEditVariant; + Bun.env.PI_EDIT_VARIANT = originalEditVariant; } }); @@ -419,7 +419,7 @@ function b() { }); it("should persist environment variables between commands", async () => { - if (process.platform === "win32" || process.env.PI_SHELL_PERSIST !== "1") { + if (process.platform === "win32" || Bun.env.PI_SHELL_PERSIST !== "1") { return; } @@ -575,8 +575,8 @@ describe("edit tool CRLF handling", () => { beforeEach(() => { // Force replace mode for edit tool tests using old_text/new_text - originalEditVariant = process.env.PI_EDIT_VARIANT; - process.env.PI_EDIT_VARIANT = "replace"; + originalEditVariant = Bun.env.PI_EDIT_VARIANT; + Bun.env.PI_EDIT_VARIANT = "replace"; testDir = path.join(os.tmpdir(), `coding-agent-crlf-test-${Snowflake.next()}`); fs.mkdirSync(testDir, { recursive: true }); @@ -588,9 +588,9 @@ describe("edit tool CRLF handling", () => { // Restore original edit variant if (originalEditVariant === undefined) { - delete process.env.PI_EDIT_VARIANT; + delete Bun.env.PI_EDIT_VARIANT; } else { - process.env.PI_EDIT_VARIANT = originalEditVariant; + Bun.env.PI_EDIT_VARIANT = originalEditVariant; } }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index 9c61f5a6a..cba7f7395 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { type SettingPath, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { createTools, HIDDEN_TOOLS, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -process.env.PI_PYTHON_SKIP_CHECK = "1"; +Bun.env.PI_PYTHON_SKIP_CHECK = "1"; function createTestSession(overrides: Partial = {}): ToolSession { return { diff --git a/packages/coding-agent/test/tools/python-tool-mode.test.ts b/packages/coding-agent/test/tools/python-tool-mode.test.ts index 75b467c1b..f931ff510 100644 --- a/packages/coding-agent/test/tools/python-tool-mode.test.ts +++ b/packages/coding-agent/test/tools/python-tool-mode.test.ts @@ -19,8 +19,8 @@ function createSession(overrides: Partial = {}): ToolSession { describe("createTools python fallback", () => { it("falls back to bash when python is requested but disabled", async () => { - const previous = process.env.PI_PYTHON_SKIP_CHECK; - process.env.PI_PYTHON_SKIP_CHECK = "1"; + const previous = Bun.env.PI_PYTHON_SKIP_CHECK; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; const session = createSession(); const tools = await createTools(session, ["python"]); const names = tools.map(tool => tool.name).sort(); @@ -28,9 +28,9 @@ describe("createTools python fallback", () => { expect(names).toEqual(["bash", "exit_plan_mode"]); if (previous === undefined) { - delete process.env.PI_PYTHON_SKIP_CHECK; + delete Bun.env.PI_PYTHON_SKIP_CHECK; } else { - process.env.PI_PYTHON_SKIP_CHECK = previous; + Bun.env.PI_PYTHON_SKIP_CHECK = previous; } }); }); diff --git a/packages/coding-agent/test/tools/python.test.ts b/packages/coding-agent/test/tools/python.test.ts index c8b1db84b..8475e1a4d 100644 --- a/packages/coding-agent/test/tools/python.test.ts +++ b/packages/coding-agent/test/tools/python.test.ts @@ -9,16 +9,16 @@ let previousSkipCheck: string | undefined; let tempDir: TempDir; beforeAll(() => { tempDir = TempDir.createSync("@omp-python-test-"); - previousSkipCheck = process.env.PI_PYTHON_SKIP_CHECK; - process.env.PI_PYTHON_SKIP_CHECK = "1"; + previousSkipCheck = Bun.env.PI_PYTHON_SKIP_CHECK; + Bun.env.PI_PYTHON_SKIP_CHECK = "1"; }); afterAll(() => { if (previousSkipCheck === undefined) { - delete process.env.PI_PYTHON_SKIP_CHECK; + delete Bun.env.PI_PYTHON_SKIP_CHECK; return; } - process.env.PI_PYTHON_SKIP_CHECK = previousSkipCheck; + Bun.env.PI_PYTHON_SKIP_CHECK = previousSkipCheck; tempDir.removeSync(); }); diff --git a/packages/coding-agent/test/tools/web-scrapers/academic.test.ts b/packages/coding-agent/test/tools/web-scrapers/academic.test.ts index 43658791a..c6fead011 100644 --- a/packages/coding-agent/test/tools/web-scrapers/academic.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/academic.test.ts @@ -4,7 +4,7 @@ import { handleIacr } from "@oh-my-pi/pi-coding-agent/web/scrapers/iacr"; import { handlePubMed } from "@oh-my-pi/pi-coding-agent/web/scrapers/pubmed"; import { handleSemanticScholar } from "@oh-my-pi/pi-coding-agent/web/scrapers/semantic-scholar"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleSemanticScholar", () => { it("returns null for non-S2 URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/business.test.ts b/packages/coding-agent/test/tools/web-scrapers/business.test.ts index d6886e7ae..99a953fd6 100644 --- a/packages/coding-agent/test/tools/web-scrapers/business.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/business.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { handleOpenCorporates } from "@oh-my-pi/pi-coding-agent/web/scrapers/opencorporates"; import { handleSecEdgar } from "@oh-my-pi/pi-coding-agent/web/scrapers/sec-edgar"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleSecEdgar", () => { it("returns null for non-matching URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/dev-platforms.test.ts b/packages/coding-agent/test/tools/web-scrapers/dev-platforms.test.ts index e273c41f5..47dd300e1 100644 --- a/packages/coding-agent/test/tools/web-scrapers/dev-platforms.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/dev-platforms.test.ts @@ -4,7 +4,7 @@ import { handleGitLab } from "@oh-my-pi/pi-coding-agent/web/scrapers/gitlab"; import { handleHackerNews } from "@oh-my-pi/pi-coding-agent/web/scrapers/hackernews"; import { handleLobsters } from "@oh-my-pi/pi-coding-agent/web/scrapers/lobsters"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; // ============================================================================= // HackerNews Tests diff --git a/packages/coding-agent/test/tools/web-scrapers/documentation.test.ts b/packages/coding-agent/test/tools/web-scrapers/documentation.test.ts index 2ae9f3182..be89cc16a 100644 --- a/packages/coding-agent/test/tools/web-scrapers/documentation.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/documentation.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { handleMDN } from "@oh-my-pi/pi-coding-agent/web/scrapers/mdn"; import { handleReadTheDocs } from "@oh-my-pi/pi-coding-agent/web/scrapers/readthedocs"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleMDN", () => { it("returns null for non-MDN URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/finance-media.test.ts b/packages/coding-agent/test/tools/web-scrapers/finance-media.test.ts index 3c15bc8b3..b0583af30 100644 --- a/packages/coding-agent/test/tools/web-scrapers/finance-media.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/finance-media.test.ts @@ -3,7 +3,7 @@ import { handleArtifactHub } from "@oh-my-pi/pi-coding-agent/web/scrapers/artifa import { handleCoinGecko } from "@oh-my-pi/pi-coding-agent/web/scrapers/coingecko"; import { handleDiscogs } from "@oh-my-pi/pi-coding-agent/web/scrapers/discogs"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleCoinGecko", () => { it("returns null for non-CoinGecko URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts b/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts index 8a1f27151..78a08cb04 100644 --- a/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { handleGitHub } from "@oh-my-pi/pi-coding-agent/web/scrapers/github"; import { handleGitHubGist } from "@oh-my-pi/pi-coding-agent/web/scrapers/github-gist"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; // ============================================================================= // GitHub Tests diff --git a/packages/coding-agent/test/tools/web-scrapers/media.test.ts b/packages/coding-agent/test/tools/web-scrapers/media.test.ts index 1ba594dd4..51647336e 100644 --- a/packages/coding-agent/test/tools/web-scrapers/media.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/media.test.ts @@ -3,7 +3,7 @@ import { handleHuggingFace } from "@oh-my-pi/pi-coding-agent/web/scrapers/huggin import { handleSpotify } from "@oh-my-pi/pi-coding-agent/web/scrapers/spotify"; import { handleVimeo } from "@oh-my-pi/pi-coding-agent/web/scrapers/vimeo"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleVimeo", () => { it("returns null for non-Vimeo URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/package-managers-2.test.ts b/packages/coding-agent/test/tools/web-scrapers/package-managers-2.test.ts index a875995e6..1e032195f 100644 --- a/packages/coding-agent/test/tools/web-scrapers/package-managers-2.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/package-managers-2.test.ts @@ -6,7 +6,7 @@ import { handleMetaCPAN } from "@oh-my-pi/pi-coding-agent/web/scrapers/metacpan" import { handleRepology } from "@oh-my-pi/pi-coding-agent/web/scrapers/repology"; import { handleTerraform } from "@oh-my-pi/pi-coding-agent/web/scrapers/terraform"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleMetaCPAN", () => { it("returns null for non-MetaCPAN URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts b/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts index d7072a303..596129dec 100644 --- a/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts @@ -6,7 +6,7 @@ import { handleNuGet } from "@oh-my-pi/pi-coding-agent/web/scrapers/nuget"; import { handlePackagist } from "@oh-my-pi/pi-coding-agent/web/scrapers/packagist"; import { handleRubyGems } from "@oh-my-pi/pi-coding-agent/web/scrapers/rubygems"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleBrew", () => { it("returns null for non-Homebrew URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/package-registries.test.ts b/packages/coding-agent/test/tools/web-scrapers/package-registries.test.ts index 090346b74..ac0f7251c 100644 --- a/packages/coding-agent/test/tools/web-scrapers/package-registries.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/package-registries.test.ts @@ -6,7 +6,7 @@ import { handleNpm } from "@oh-my-pi/pi-coding-agent/web/scrapers/npm"; import { handlePubDev } from "@oh-my-pi/pi-coding-agent/web/scrapers/pub-dev"; import { handlePyPI } from "@oh-my-pi/pi-coding-agent/web/scrapers/pypi"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handlePyPI", () => { it("returns null for non-PyPI URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/research.test.ts b/packages/coding-agent/test/tools/web-scrapers/research.test.ts index e9f75a4a8..ae7e5b90e 100644 --- a/packages/coding-agent/test/tools/web-scrapers/research.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/research.test.ts @@ -3,7 +3,7 @@ import { handleBiorxiv } from "@oh-my-pi/pi-coding-agent/web/scrapers/biorxiv"; import { handleOpenLibrary } from "@oh-my-pi/pi-coding-agent/web/scrapers/openlibrary"; import { handleWikidata } from "@oh-my-pi/pi-coding-agent/web/scrapers/wikidata"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleWikidata", () => { it("returns null for non-matching URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/security.test.ts b/packages/coding-agent/test/tools/web-scrapers/security.test.ts index 59eba8bd9..ad7998d3c 100644 --- a/packages/coding-agent/test/tools/web-scrapers/security.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/security.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { handleNvd } from "@oh-my-pi/pi-coding-agent/web/scrapers/nvd"; import { handleOsv } from "@oh-my-pi/pi-coding-agent/web/scrapers/osv"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleNvd", () => { it("returns null for non-NVD URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts b/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts index 35607814a..b6a91de53 100644 --- a/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { handleBluesky } from "@oh-my-pi/pi-coding-agent/web/scrapers/bluesky"; import { handleMastodon } from "@oh-my-pi/pi-coding-agent/web/scrapers/mastodon"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleMastodon", () => { it("returns null for non-Mastodon URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/social.test.ts b/packages/coding-agent/test/tools/web-scrapers/social.test.ts index 1bc33266b..d5204dca1 100644 --- a/packages/coding-agent/test/tools/web-scrapers/social.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/social.test.ts @@ -3,7 +3,7 @@ import { handleReddit } from "@oh-my-pi/pi-coding-agent/web/scrapers/reddit"; import { handleStackOverflow } from "@oh-my-pi/pi-coding-agent/web/scrapers/stackoverflow"; import { handleTwitter } from "@oh-my-pi/pi-coding-agent/web/scrapers/twitter"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleTwitter", () => { it("returns null for non-Twitter URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts b/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts index 0fda920c6..5da21c3f4 100644 --- a/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { handleStackOverflow } from "@oh-my-pi/pi-coding-agent/web/scrapers/stackoverflow"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleStackOverflow", () => { it("returns null for non-SE URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/standards.test.ts b/packages/coding-agent/test/tools/web-scrapers/standards.test.ts index 0cb90bcd5..e716e83e7 100644 --- a/packages/coding-agent/test/tools/web-scrapers/standards.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/standards.test.ts @@ -3,7 +3,7 @@ import { handleCheatSh } from "@oh-my-pi/pi-coding-agent/web/scrapers/cheatsh"; import { handleRfc } from "@oh-my-pi/pi-coding-agent/web/scrapers/rfc"; import { handleTldr } from "@oh-my-pi/pi-coding-agent/web/scrapers/tldr"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleRfc", () => { it("returns null for non-RFC URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/wikipedia.test.ts b/packages/coding-agent/test/tools/web-scrapers/wikipedia.test.ts index b29bd1de1..cdbaa3fb4 100644 --- a/packages/coding-agent/test/tools/web-scrapers/wikipedia.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/wikipedia.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { handleWikipedia } from "@oh-my-pi/pi-coding-agent/web/scrapers/wikipedia"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleWikipedia", () => { it("returns null for non-Wikipedia URLs", async () => { diff --git a/packages/coding-agent/test/tools/web-scrapers/youtube.test.ts b/packages/coding-agent/test/tools/web-scrapers/youtube.test.ts index e570b6a4a..3431b88ca 100644 --- a/packages/coding-agent/test/tools/web-scrapers/youtube.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/youtube.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { handleYouTube } from "@oh-my-pi/pi-coding-agent/web/scrapers/youtube"; -const SKIP = !process.env.WEB_FETCH_INTEGRATION; +const SKIP = !Bun.env.WEB_FETCH_INTEGRATION; describe.skipIf(SKIP)("handleYouTube", () => { it("returns null for non-YouTube URLs", async () => { diff --git a/packages/natives/scripts/build-native.ts b/packages/natives/scripts/build-native.ts index 68d22ada2..e6d6b9b65 100644 --- a/packages/natives/scripts/build-native.ts +++ b/packages/natives/scripts/build-native.ts @@ -7,9 +7,9 @@ const rustDir = path.join(repoRoot, "crates/pi-natives"); const nativeDir = path.join(import.meta.dir, "../native"); const isDev = process.argv.includes("--dev"); -const crossTarget = process.env.CROSS_TARGET; -const targetPlatform = process.env.TARGET_PLATFORM || process.platform; -const targetArch = process.env.TARGET_ARCH || process.arch; +const crossTarget = Bun.env.CROSS_TARGET; +const targetPlatform = Bun.env.TARGET_PLATFORM || process.platform; +const targetArch = Bun.env.TARGET_ARCH || process.arch; const isCrossCompile = Boolean(crossTarget) || targetPlatform !== process.platform || @@ -79,7 +79,7 @@ if (buildResult.exitCode !== 0) { const profile = isDev ? "debug" : "release"; const targetRoots = [ - process.env.CARGO_TARGET_DIR ? path.resolve(process.env.CARGO_TARGET_DIR) : undefined, + Bun.env.CARGO_TARGET_DIR ? path.resolve(Bun.env.CARGO_TARGET_DIR) : undefined, path.join(repoRoot, "target"), path.join(rustDir, "target"), ].filter((v): v is string => Boolean(v)); diff --git a/packages/natives/scripts/embed-native.ts b/packages/natives/scripts/embed-native.ts index 2e4b69775..a16c1bad1 100644 --- a/packages/natives/scripts/embed-native.ts +++ b/packages/natives/scripts/embed-native.ts @@ -12,8 +12,8 @@ if (reset) { process.exit(0); } -const targetPlatform = process.env.TARGET_PLATFORM || process.platform; -const targetArch = process.env.TARGET_ARCH || process.arch; +const targetPlatform = Bun.env.TARGET_PLATFORM || process.platform; +const targetArch = Bun.env.TARGET_ARCH || process.arch; const platformTag = `${targetPlatform}-${targetArch}`; const addonFilename = `pi_natives.${platformTag}.node`; diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index 7957a8ba8..76d965835 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -40,7 +40,7 @@ const versionedDir = path.join(os.homedir(), ".omp", "natives", packageVersion); const versionedAddonPath = path.join(versionedDir, addonFilename); const legacyUserDataDir = process.platform === "win32" - ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp") + ? path.join(Bun.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp") : path.join(os.homedir(), ".local", "bin"); const downloadUrl = `https://github.com/can1357/oh-my-pi/releases/latest/download/${addonFilename}`; const isCompiledBinary = diff --git a/packages/tui/src/terminal-capabilities.ts b/packages/tui/src/terminal-capabilities.ts index 224b4224e..da5f764c4 100644 --- a/packages/tui/src/terminal-capabilities.ts +++ b/packages/tui/src/terminal-capabilities.ts @@ -75,7 +75,7 @@ export const TERMINAL_ID: TerminalId = (() => { TERM_PROGRAM, TERM, COLORTERM, - } = process.env; + } = Bun.env; if (KITTY_WINDOW_ID) return "kitty"; if (GHOSTTY_RESOURCES_DIR) return "ghostty"; diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index afcb90ef9..15bfc38d5 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -50,17 +50,17 @@ const projectEnv = parseEnvFile(path.join(process.cwd(), ".env")); for (const file of [projectEnv, homeEnv]) { for (const [key, value] of Object.entries(file)) { - if (!process.env[key]) { - process.env[key] = value; + if (!Bun.env[key]) { + Bun.env[key] = value; } } } /** - * Intentional re-export of process.env. + * Intentional re-export of Bun.env. * * All users should import this env module (import { $env } from "@oh-my-pi/pi-utils") * before using environment variables. This ensures that .env files have been loaded and * overrides (project, home) have been applied, so $env always reflects the correct values. */ -export const $env: Record = process.env as Record; +export const $env: Record = Bun.env as Record; diff --git a/packages/utils/src/procmgr.ts b/packages/utils/src/procmgr.ts index edfd5a6e4..3a088cb2c 100644 --- a/packages/utils/src/procmgr.ts +++ b/packages/utils/src/procmgr.ts @@ -34,7 +34,7 @@ function isExecutable(path: string): boolean { function buildSpawnEnv(shell: string): Record { const noCI = $env.PI_BASH_NO_CI || $env.CLAUDE_BASH_NO_CI; return { - ...process.env, + ...Bun.env, SHELL: shell, GIT_EDITOR: "true", GPG_TTY: "not a tty", @@ -135,11 +135,11 @@ export function getShellConfig(customShellPath?: string): ShellConfig { if (process.platform === "win32") { // 2. Try Git Bash in known locations const paths: string[] = []; - const programFiles = process.env.ProgramFiles; + const programFiles = Bun.env.ProgramFiles; if (programFiles) { paths.push(`${programFiles}\\Git\\bin\\bash.exe`); } - const programFilesX86 = process.env["ProgramFiles(x86)"]; + const programFilesX86 = Bun.env["ProgramFiles(x86)"]; if (programFilesX86) { paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`); } @@ -168,7 +168,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig { } // Unix: prefer user's shell from $SHELL if it's bash/zsh and executable - const userShell = process.env.SHELL; + const userShell = Bun.env.SHELL; const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh")); if (isValidShell && isExecutable(userShell)) { cachedShellConfig = buildConfig(userShell); diff --git a/scripts/repro-stuck.ts b/scripts/repro-stuck.ts index 0237469de..a2ec3c7aa 100644 --- a/scripts/repro-stuck.ts +++ b/scripts/repro-stuck.ts @@ -77,7 +77,7 @@ async function spawnBatch(count: number, basePort: number, timeout: number): Pro stdout: "pipe", stderr: "pipe", stdin: "pipe", - env: { ...process.env, NO_COLOR: "1", PI_DEBUG_STARTUP: "1" }, + env: { ...Bun.env, NO_COLOR: "1", PI_DEBUG_STARTUP: "1" }, }); instances.push({ proc, port, stdout: "", stderr: "", status: "pending" }); }