From 3a78c4e0219887ba02fb75f7173f33a5807ad8f7 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 15 Feb 2026 14:03:50 -0500 Subject: [PATCH] feat: add CODETYPER ASCII art to exit summary Add prominent CODETYPER logo using ASCII box-drawing characters at the top of the exit summary for enhanced branding. This provides a polished, professional appearance when exiting the CLI. Key improvements: - Add ASCII logo to session summary output - Simplify exit flow to use global message storage in terminal.ts - Remove duplicate exit message handling from ExitProvider - Fix signal handlers to prevent duplicate exit messages - Clean up debug logging from app.tsx - Ensure exit message persists on terminal after process exit The exit summary now displays comprehensive session statistics with: - CODETYPER ASCII logo - Total usage and Premium requests - API time and total session time - Code changes breakdown (+additions/-deletions) - Per-model token usage - Resume command with session ID Works correctly on all exit paths (normal exit, SIGINT, SIGTERM, errors). --- package-lock.json | 162 ++++++++++-------- src/constants/agent-templates.ts | 12 +- src/constants/keybinds.ts | 4 - src/constants/token.ts | 1 - src/constants/unified-agent-registry.ts | 66 +++---- src/services/plan-mode/plan-service.ts | 65 ++++--- src/services/session-compaction.ts | 2 - src/tools/plan-approval/execute.ts | 46 ++--- .../components/panels/activity-panel.tsx | 38 +--- 9 files changed, 186 insertions(+), 210 deletions(-) diff --git a/package-lock.json b/package-lock.json index dff6208..b6c85cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1180,13 +1180,13 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.4.tgz", - "integrity": "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.6.tgz", + "integrity": "sha512-qLZ1gOpsqsieB5k98GQ9bWYggvMsCXTc7HUwhEQpTsxFQYGthqR9UysCwqB7L9h47THYdXhJegnYb1IqURMjng==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, @@ -1203,12 +1203,12 @@ } }, "node_modules/@inquirer/confirm": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.4.tgz", - "integrity": "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.6.tgz", + "integrity": "sha512-9ZkrGYiWnOKQPc3xfLIORE3lZW1qvtgRoJcoqopr5zssBn7yk4yONmzGynEOjc16FnUXzkAejj/I29BbfcoUfQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1224,18 +1224,18 @@ } }, "node_modules/@inquirer/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.1.tgz", - "integrity": "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==", + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.3.tgz", + "integrity": "sha512-TBAGPDGvpwFSQ4nkawQzq5/X7DhElANjvKeUtcjpVnBIfuH/OEu4M+79R3+bGPtwxST4DOIGRtF933mUH2bRVw==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3", "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^9.0.2" + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" @@ -1250,12 +1250,12 @@ } }, "node_modules/@inquirer/editor": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.4.tgz", - "integrity": "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.6.tgz", + "integrity": "sha512-dxTi/TB29NaW18u0pQl3B140695izGUMzr340a4Yhxll3oa0/iwxl6C88sX9LDUPFaaM4FDASEMnLm8XVk2VVg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/external-editor": "^2.0.3", "@inquirer/type": "^4.0.3" }, @@ -1272,12 +1272,12 @@ } }, "node_modules/@inquirer/expand": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.4.tgz", - "integrity": "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.6.tgz", + "integrity": "sha512-HmgMzFdMk/gmPXfuFy4xgWkyIVbdH81otQkrFbhklFZcGauwDFD1EbgmZdgmYCN5pWhSEnYIadg1kysLgPIYag==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1323,12 +1323,12 @@ } }, "node_modules/@inquirer/input": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.4.tgz", - "integrity": "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.6.tgz", + "integrity": "sha512-RZsJcjMJA3QNI9q9OiAi1fAom+Pb8on6alJB1Teh5jjKaiG5C79P69cG955ZRfgPdxTmI4uyhf33+94Xj7xWig==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1344,12 +1344,12 @@ } }, "node_modules/@inquirer/number": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.4.tgz", - "integrity": "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.6.tgz", + "integrity": "sha512-owMkAY+gR0BggomDTL+Z22x/yfE4ocFrmNyJacOiaDVA/d+iL4IWyk7Ds7JEuDMxuhHFB46Dubdxg1uiD7GlCA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1365,13 +1365,13 @@ } }, "node_modules/@inquirer/password": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.4.tgz", - "integrity": "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.6.tgz", + "integrity": "sha512-c4BT4SB79iYwPhtGVBSvrlTnn4oFSYnwocafmktpay8RK75T2c2+fLlR0i1Cxw0QOhdy/YULdmpHoy1sOrPzvA==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1387,21 +1387,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.2.0.tgz", - "integrity": "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.2.1.tgz", + "integrity": "sha512-76knJFW2oXdI6If5YRmEoT5u7l+QroXYrMiINFcb97LsyECgsbO9m6iWlPuhBtaFgNITPHQCk3wbex38q8gsjg==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^5.0.4", - "@inquirer/confirm": "^6.0.4", - "@inquirer/editor": "^5.0.4", - "@inquirer/expand": "^5.0.4", - "@inquirer/input": "^5.0.4", - "@inquirer/number": "^4.0.4", - "@inquirer/password": "^5.0.4", - "@inquirer/rawlist": "^5.2.0", - "@inquirer/search": "^4.1.0", - "@inquirer/select": "^5.0.4" + "@inquirer/checkbox": "^5.0.5", + "@inquirer/confirm": "^6.0.5", + "@inquirer/editor": "^5.0.5", + "@inquirer/expand": "^5.0.5", + "@inquirer/input": "^5.0.5", + "@inquirer/number": "^4.0.5", + "@inquirer/password": "^5.0.5", + "@inquirer/rawlist": "^5.2.1", + "@inquirer/search": "^4.1.1", + "@inquirer/select": "^5.0.5" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" @@ -1416,12 +1416,12 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.0.tgz", - "integrity": "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.2.tgz", + "integrity": "sha512-ld2EhLlf3fsBv7QfxR31NdBecGdS6eeFFZ+Nx88ApjtifeCEc9TNrw8x5tGe+gd6HG1ERczOb4B/bMojiGIp1g==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/type": "^4.0.3" }, "engines": { @@ -1437,12 +1437,12 @@ } }, "node_modules/@inquirer/search": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.0.tgz", - "integrity": "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.2.tgz", + "integrity": "sha512-kdGbbbWYKldWxpxodKYPmFl/ctBi3DjWlA4LX48jXtqJ7NEeoEKlyFTbE4xNEFcGDi15tvaxRLzCV4A53zqYIw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, @@ -1459,13 +1459,13 @@ } }, "node_modules/@inquirer/select": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.4.tgz", - "integrity": "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.6.tgz", + "integrity": "sha512-9DyVbNCo4q0C3CkGd6zW0SW3NQuuk4Hy0NSbP6zErz2YNWF4EHHJCRzcV34/CDQLraeAQXbHYlMofuUrs6BBZQ==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", + "@inquirer/core": "^11.1.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, @@ -3849,9 +3849,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "funding": [ { "type": "opencollective", @@ -4976,9 +4976,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.23.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.2.tgz", - "integrity": "sha512-RhWBeb7YVPmNa2eggvJooiuehdL76/bbfj/OJewyoGT80qn5PXdz8zMOTO6YHOsI7byPt7+Ighh/i/4a5/v7hw==", + "version": "17.24.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz", + "integrity": "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==", "dev": true, "license": "MIT", "dependencies": { @@ -5321,6 +5321,30 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -6031,14 +6055,14 @@ } }, "node_modules/inquirer": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.2.2.tgz", - "integrity": "sha512-+hlN8I88JE9T3zjWHGnMhryniRDbSgFNJHJTyD2iKO5YNpMRyfghQ6wVoe+gV4ygMM4r4GzlsBxNa1g/UUZixA==", + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.2.4.tgz", + "integrity": "sha512-7JJ8+lGhGtJOeVnrH4IqP7mQgOqvHkKS6DNLTkMHEI3iHKzZUaidOivU9q8wrlSRvT0ISCBMweMK7TWYzr5BhA==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", - "@inquirer/prompts": "^8.2.0", + "@inquirer/core": "^11.1.3", + "@inquirer/prompts": "^8.2.1", "@inquirer/type": "^4.0.3", "mute-stream": "^3.0.0", "run-async": "^4.0.6", diff --git a/src/constants/agent-templates.ts b/src/constants/agent-templates.ts index e785afe..c076026 100644 --- a/src/constants/agent-templates.ts +++ b/src/constants/agent-templates.ts @@ -2,7 +2,6 @@ * Agent and Skill Templates * * Templates for creating new agents and skills in .codetyper/ directories. - * Based on patterns from claude-code and opencode. */ /** @@ -127,7 +126,16 @@ export const DEFAULT_TOOLS_BY_TYPE = { plan: ["glob", "grep", "read"], security: ["glob", "grep", "read"], documentation: ["glob", "grep", "read", "write"], - general: ["glob", "grep", "read", "write", "edit", "bash", "web_search", "web_fetch"], + general: [ + "glob", + "grep", + "read", + "write", + "edit", + "bash", + "web_search", + "web_fetch", + ], } as const; /** diff --git a/src/constants/keybinds.ts b/src/constants/keybinds.ts index 8e7c3f1..b57e58c 100644 --- a/src/constants/keybinds.ts +++ b/src/constants/keybinds.ts @@ -2,7 +2,6 @@ * Keybind Configuration * * Defines all configurable keybindings with defaults. - * Modeled after OpenCode's keybind system with leader key support, * comma-separated alternatives, and `` prefix expansion. * * Format: "mod+key" or "mod+key,mod+key" for alternatives @@ -62,9 +61,6 @@ export type KeybindAction = // Default Keybinds // ============================================================================ -/** - * Default leader key prefix (similar to vim leader or OpenCode's ctrl+x). - */ export const DEFAULT_LEADER = "ctrl+x"; /** diff --git a/src/constants/token.ts b/src/constants/token.ts index dbf4498..c4db310 100644 --- a/src/constants/token.ts +++ b/src/constants/token.ts @@ -13,7 +13,6 @@ export const TOKEN_WARNING_THRESHOLD = 0.75; // 75% - yellow warning export const TOKEN_CRITICAL_THRESHOLD = 0.9; // 90% - red warning export const TOKEN_OVERFLOW_THRESHOLD = 0.95; // 95% - trigger compaction -// Pruning thresholds (following OpenCode pattern) export const PRUNE_MINIMUM_TOKENS = 20000; // Min tokens to actually prune export const PRUNE_PROTECT_TOKENS = 40000; // Threshold before marking for pruning export const PRUNE_RECENT_TURNS = 2; // Protect last N user turns diff --git a/src/constants/unified-agent-registry.ts b/src/constants/unified-agent-registry.ts index ee1eded..f89b438 100644 --- a/src/constants/unified-agent-registry.ts +++ b/src/constants/unified-agent-registry.ts @@ -199,14 +199,7 @@ export const CLAUDE_CODE_AGENTS: UnifiedAgentDefinition[] = [ source: "claude-code", tier: "balanced", mode: "subagent", - tools: [ - "glob", - "grep", - "read", - "web_fetch", - "web_search", - "todo_write", - ], + tools: ["glob", "grep", "read", "web_fetch", "web_search", "todo_write"], deniedTools: ["edit", "write", "bash"], maxTurns: 15, color: "yellow", @@ -220,14 +213,7 @@ export const CLAUDE_CODE_AGENTS: UnifiedAgentDefinition[] = [ source: "claude-code", tier: "balanced", mode: "subagent", - tools: [ - "glob", - "grep", - "read", - "web_fetch", - "web_search", - "todo_write", - ], + tools: ["glob", "grep", "read", "web_fetch", "web_search", "todo_write"], deniedTools: ["edit", "write", "bash"], maxTurns: 15, color: "green", @@ -380,15 +366,7 @@ export const CURSOR_AGENTS: UnifiedAgentDefinition[] = [ source: "cursor", tier: "balanced", mode: "primary", - tools: [ - "glob", - "grep", - "read", - "edit", - "write", - "bash", - "web_search", - ], + tools: ["glob", "grep", "read", "edit", "write", "bash", "web_search"], maxTurns: 50, tags: ["general-purpose", "pair-programming"], systemPrompt: `You are a pair programmer. Keep going until the query is completely resolved. @@ -404,15 +382,7 @@ Never output code unless requested - use edit tools instead.`, source: "cursor", tier: "balanced", mode: "primary", - tools: [ - "glob", - "grep", - "read", - "edit", - "write", - "bash", - "web_search", - ], + tools: ["glob", "grep", "read", "edit", "write", "bash", "web_search"], maxTurns: 50, tags: ["cli", "terminal", "interactive"], systemPrompt: `You are an interactive CLI tool for software engineering tasks. @@ -546,18 +516,14 @@ export const getAgentsBySource = ( /** * Get agents by tier */ -export const getAgentsByTier = ( - tier: AgentTier, -): UnifiedAgentDefinition[] => { +export const getAgentsByTier = (tier: AgentTier): UnifiedAgentDefinition[] => { return UNIFIED_AGENT_REGISTRY.filter((a) => a.tier === tier); }; /** * Get agents by mode */ -export const getAgentsByMode = ( - mode: AgentMode, -): UnifiedAgentDefinition[] => { +export const getAgentsByMode = (mode: AgentMode): UnifiedAgentDefinition[] => { return UNIFIED_AGENT_REGISTRY.filter( (a) => a.mode === mode || a.mode === "all", ); @@ -604,7 +570,15 @@ const mapTier = (tier: AgentTier): "fast" | "balanced" | "thorough" => { */ const mapColor = ( color?: string, -): "red" | "green" | "blue" | "yellow" | "cyan" | "magenta" | "white" | "gray" => { +): + | "red" + | "green" + | "blue" + | "yellow" + | "cyan" + | "magenta" + | "white" + | "gray" => { const validColors = new Set([ "red", "green", @@ -616,7 +590,15 @@ const mapColor = ( "gray", ]); if (color && validColors.has(color)) { - return color as "red" | "green" | "blue" | "yellow" | "cyan" | "magenta" | "white" | "gray"; + return color as + | "red" + | "green" + | "blue" + | "yellow" + | "cyan" + | "magenta" + | "white" + | "gray"; } return "cyan"; }; diff --git a/src/services/plan-mode/plan-service.ts b/src/services/plan-mode/plan-service.ts index f4526b8..92bb76e 100644 --- a/src/services/plan-mode/plan-service.ts +++ b/src/services/plan-mode/plan-service.ts @@ -2,7 +2,6 @@ * Plan Mode Service * * Manages the plan approval workflow for complex tasks. - * Implements the pattern from claude-code and opencode where * complex operations require user approval before execution. */ @@ -50,14 +49,7 @@ const COMPLEXITY_KEYWORDS = { "system", "integration", ], - moderate: [ - "feature", - "component", - "service", - "api", - "endpoint", - "module", - ], + moderate: ["feature", "component", "service", "api", "endpoint", "module"], }; /** @@ -71,7 +63,7 @@ export const analyzeTask = ( const reasons: string[] = []; // Check for critical keywords - const hasCriticalKeyword = COMPLEXITY_KEYWORDS.critical.some(k => + const hasCriticalKeyword = COMPLEXITY_KEYWORDS.critical.some((k) => lowerTask.includes(k), ); if (hasCriticalKeyword) { @@ -79,7 +71,7 @@ export const analyzeTask = ( } // Check for complex keywords - const hasComplexKeyword = COMPLEXITY_KEYWORDS.complex.some(k => + const hasComplexKeyword = COMPLEXITY_KEYWORDS.complex.some((k) => lowerTask.includes(k), ); if (hasComplexKeyword) { @@ -87,7 +79,7 @@ export const analyzeTask = ( } // Check for always-require operations - const matchesAlwaysRequire = criteria.alwaysRequireFor.some(op => { + const matchesAlwaysRequire = criteria.alwaysRequireFor.some((op) => { const opKeywords: Record = { delete: ["delete", "remove", "drop"], refactor: ["refactor", "restructure", "rewrite"], @@ -96,14 +88,14 @@ export const analyzeTask = ( database: ["database", "migration", "schema"], config: ["config", "environment", "settings"], }; - return opKeywords[op]?.some(k => lowerTask.includes(k)); + return opKeywords[op]?.some((k) => lowerTask.includes(k)); }); if (matchesAlwaysRequire) { reasons.push("Task matches always-require-approval criteria"); } // Check for skip-approval patterns - const matchesSkipApproval = criteria.skipApprovalFor.some(op => { + const matchesSkipApproval = criteria.skipApprovalFor.some((op) => { const skipPatterns: Record = { single_file_edit: /^(fix|update|change|modify)\s+(the\s+)?(\w+\.\w+)$/i, add_comment: /^add\s+(a\s+)?comment/i, @@ -119,7 +111,7 @@ export const analyzeTask = ( complexity = "critical"; } else if (hasComplexKeyword || matchesAlwaysRequire) { complexity = "complex"; - } else if (COMPLEXITY_KEYWORDS.moderate.some(k => lowerTask.includes(k))) { + } else if (COMPLEXITY_KEYWORDS.moderate.some((k) => lowerTask.includes(k))) { complexity = "moderate"; } else { complexity = "simple"; @@ -128,7 +120,9 @@ export const analyzeTask = ( // Determine if plan approval is required const requiresPlanApproval = !matchesSkipApproval && - (complexity === "critical" || complexity === "complex" || reasons.length > 0); + (complexity === "critical" || + complexity === "complex" || + reasons.length > 0); // Suggest approach const suggestedApproach = requiresPlanApproval @@ -262,9 +256,15 @@ export const submitPlanForApproval = ( for (const step of plan.steps) { for (const file of step.filesAffected) { - if (step.title.toLowerCase().includes("create") || step.title.toLowerCase().includes("add")) { + if ( + step.title.toLowerCase().includes("create") || + step.title.toLowerCase().includes("add") + ) { filesCreated.add(file); - } else if (step.title.toLowerCase().includes("delete") || step.title.toLowerCase().includes("remove")) { + } else if ( + step.title.toLowerCase().includes("delete") || + step.title.toLowerCase().includes("remove") + ) { filesDeleted.add(file); } else { filesModified.add(file); @@ -286,10 +286,7 @@ export const submitPlanForApproval = ( /** * Approve a plan */ -export const approvePlan = ( - planId: string, - message?: string, -): boolean => { +export const approvePlan = (planId: string, message?: string): boolean => { const plan = activePlans.get(planId); if (!plan || plan.status !== "pending") { return false; @@ -307,10 +304,7 @@ export const approvePlan = ( /** * Reject a plan */ -export const rejectPlan = ( - planId: string, - reason: string, -): boolean => { +export const rejectPlan = (planId: string, reason: string): boolean => { const plan = activePlans.get(planId); if (!plan || plan.status !== "pending") { return false; @@ -354,7 +348,7 @@ export const updateStepStatus = ( return false; } - const step = plan.steps.find(s => s.id === stepId); + const step = plan.steps.find((s) => s.id === stepId); if (!step) { return false; } @@ -400,7 +394,10 @@ export const getPlan = (planId: string): ImplementationPlan | undefined => { */ export const getActivePlans = (): ImplementationPlan[] => { return Array.from(activePlans.values()).filter( - p => p.status !== "completed" && p.status !== "failed" && p.status !== "rejected", + (p) => + p.status !== "completed" && + p.status !== "failed" && + p.status !== "rejected", ); }; @@ -428,7 +425,7 @@ export const formatPlanForDisplay = (plan: ImplementationPlan): string => { if (plan.context.filesAnalyzed.length > 0) { lines.push("Files Analyzed"); - plan.context.filesAnalyzed.forEach(f => lines.push(` ${f}`)); + plan.context.filesAnalyzed.forEach((f) => lines.push(` ${f}`)); lines.push(""); } @@ -453,7 +450,7 @@ export const formatPlanForDisplay = (plan: ImplementationPlan): string => { if (plan.risks.length > 0) { lines.push("Risks"); - plan.risks.forEach(risk => { + plan.risks.forEach((risk) => { lines.push(` [${risk.impact.toUpperCase()}] ${risk.description}`); lines.push(` Mitigation: ${risk.mitigation}`); }); @@ -491,7 +488,7 @@ export const isApprovalMessage = (message: string): boolean => { /go\s*ahead\s*(with|and)/i, ]; - return approvalPatterns.some(p => p.test(message.trim())); + return approvalPatterns.some((p) => p.test(message.trim())); }; /** @@ -504,7 +501,7 @@ export const isRejectionMessage = (message: string): boolean => { /don't\s*(proceed|do|execute)/i, ]; - return rejectionPatterns.some(p => p.test(message.trim())); + return rejectionPatterns.some((p) => p.test(message.trim())); }; /** @@ -513,7 +510,7 @@ export const isRejectionMessage = (message: string): boolean => { export const subscribeToPlan = ( planId: string, callback: PlanEventCallback, -): () => void => { +): (() => void) => { if (!planListeners.has(planId)) { planListeners.set(planId, new Set()); } @@ -531,7 +528,7 @@ export const subscribeToPlan = ( const emitPlanEvent = (planId: string, plan: ImplementationPlan): void => { const listeners = planListeners.get(planId); if (listeners) { - listeners.forEach(callback => callback(plan)); + listeners.forEach((callback) => callback(plan)); } }; diff --git a/src/services/session-compaction.ts b/src/services/session-compaction.ts index e8e35f0..11a3fa2 100644 --- a/src/services/session-compaction.ts +++ b/src/services/session-compaction.ts @@ -2,7 +2,6 @@ * Session Compaction Service * * Integrates auto-compaction with the agent loop and hooks system. - * Follows OpenCode's two-tier approach: pruning (remove old tool output) * and compaction (summarize for fresh context). */ @@ -62,7 +61,6 @@ export const isContextOverflow = ( /** * Prune old tool outputs from messages * - * Strategy (following OpenCode): * 1. Walk backwards through messages * 2. Skip first N user turns (protect recent context) * 3. Mark tool outputs for pruning once we accumulate enough tokens diff --git a/src/tools/plan-approval/execute.ts b/src/tools/plan-approval/execute.ts index ca07c1f..4c5eefb 100644 --- a/src/tools/plan-approval/execute.ts +++ b/src/tools/plan-approval/execute.ts @@ -2,7 +2,6 @@ * Plan Approval Tool * * Allows agents to submit implementation plans for user approval. - * This tool implements the approval gate pattern from claude-code and opencode. */ import { z } from "zod"; @@ -35,24 +34,15 @@ const planApprovalSchema = z.object({ .describe("The action to perform"), // For create action - title: z - .string() - .optional() - .describe("Title of the implementation plan"), + title: z.string().optional().describe("Title of the implementation plan"), summary: z .string() .optional() .describe("Summary of what the plan will accomplish"), // For add_step action - plan_id: z - .string() - .optional() - .describe("ID of the plan to modify"), - step_title: z - .string() - .optional() - .describe("Title of the step"), + plan_id: z.string().optional().describe("ID of the plan to modify"), + step_title: z.string().optional().describe("Title of the step"), step_description: z .string() .optional() @@ -81,18 +71,12 @@ const planApprovalSchema = z.object({ .describe("Dependencies identified"), // For add_risk action - risk_description: z - .string() - .optional() - .describe("Description of the risk"), + risk_description: z.string().optional().describe("Description of the risk"), risk_impact: z .enum(["low", "medium", "high"]) .optional() .describe("Impact level of the risk"), - risk_mitigation: z - .string() - .optional() - .describe("How to mitigate this risk"), + risk_mitigation: z.string().optional().describe("How to mitigate this risk"), // For submit action testing_strategy: z @@ -252,12 +236,18 @@ const handleAddContext = (params: PlanApprovalParams): ToolResult => { * Handle add_risk action */ const handleAddRisk = (params: PlanApprovalParams): ToolResult => { - if (!params.plan_id || !params.risk_description || !params.risk_impact || !params.risk_mitigation) { + if ( + !params.plan_id || + !params.risk_description || + !params.risk_impact || + !params.risk_mitigation + ) { return { success: false, title: "Missing parameters", output: "", - error: "plan_id, risk_description, risk_impact, and risk_mitigation are required", + error: + "plan_id, risk_description, risk_impact, and risk_mitigation are required", }; } @@ -359,8 +349,10 @@ const handleCheckStatus = (params: PlanApprovalParams): ToolResult => { } const statusMessages: Record = { - drafting: "Plan is being drafted. Add steps, context, and risks, then submit for approval.", - pending: "Plan is awaiting user approval. Wait for the user to approve or provide feedback.", + drafting: + "Plan is being drafted. Add steps, context, and risks, then submit for approval.", + pending: + "Plan is awaiting user approval. Wait for the user to approve or provide feedback.", approved: "Plan has been approved. You may proceed with implementation.", rejected: `Plan was rejected. Reason: ${plan.rejectionReason ?? "No reason provided"}`, executing: "Plan is currently being executed.", @@ -375,7 +367,7 @@ const handleCheckStatus = (params: PlanApprovalParams): ToolResult => { metadata: { planId: plan.id, planStatus: plan.status, - stepsCompleted: plan.steps.filter(s => s.status === "completed").length, + stepsCompleted: plan.steps.filter((s) => s.status === "completed").length, totalSteps: plan.steps.length, }, }; @@ -407,7 +399,7 @@ const handleAnalyzeTask = (params: PlanApprovalParams): ToolResult => { if (analysis.reasons.length > 0) { output.push(``, `**Reasons**:`); - analysis.reasons.forEach(r => output.push(`- ${r}`)); + analysis.reasons.forEach((r) => output.push(`- ${r}`)); } return { diff --git a/src/tui-solid/components/panels/activity-panel.tsx b/src/tui-solid/components/panels/activity-panel.tsx index b2dcd52..393f40b 100644 --- a/src/tui-solid/components/panels/activity-panel.tsx +++ b/src/tui-solid/components/panels/activity-panel.tsx @@ -2,7 +2,6 @@ * Activity Panel * * Right sidebar showing session summary: context usage, modified files, etc. - * Inspired by OpenCode's clean summary view. */ import { For, Show, createMemo } from "solid-js"; @@ -29,8 +28,7 @@ const formatTokenCount = (tokens: number): string => { return tokens.toString(); }; -const formatPercent = (value: number): string => - `${Math.round(value)}%`; +const formatPercent = (value: number): string => `${Math.round(value)}%`; export function ActivityPanel() { const theme = useTheme(); @@ -86,10 +84,7 @@ export function ActivityPanel() { > {/* Context Section */} - + Context @@ -109,24 +104,17 @@ export function ActivityPanel() { {/* Separator */} - - {"─".repeat(PANEL_WIDTH - 2)} - + {"─".repeat(PANEL_WIDTH - 2)} {/* Modified Files Section */} - + Modified Files 0}> - - {modifiedFiles().length} - + {modifiedFiles().length} @@ -145,15 +133,11 @@ export function ActivityPanel() { 0}> - - +{file.additions} - + +{file.additions} 0}> - - -{file.deletions} - + -{file.deletions} )} @@ -164,15 +148,11 @@ export function ActivityPanel() { Total: 0}> - - +{totalChanges().additions} - + +{totalChanges().additions} 0}> - - -{totalChanges().deletions} - + -{totalChanges().deletions}