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).
This commit is contained in:
2026-02-15 14:03:50 -05:00
parent 87d53f7035
commit 3a78c4e021
9 changed files with 186 additions and 210 deletions

162
package-lock.json generated
View File

@@ -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",

View File

@@ -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;
/**

View File

@@ -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 `<leader>` 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";
/**

View File

@@ -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

View File

@@ -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";
};

View File

@@ -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<string, string[]> = {
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<string, RegExp> = {
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));
}
};

View File

@@ -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

View File

@@ -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<string, string> = {
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 {

View File

@@ -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 */}
<box flexDirection="column" marginBottom={1}>
<text
fg={theme.colors.text}
attributes={TextAttributes.BOLD}
>
<text fg={theme.colors.text} attributes={TextAttributes.BOLD}>
Context
</text>
<box flexDirection="row" marginTop={1}>
@@ -109,24 +104,17 @@ export function ActivityPanel() {
{/* Separator */}
<box marginBottom={1}>
<text fg={theme.colors.border}>
{"─".repeat(PANEL_WIDTH - 2)}
</text>
<text fg={theme.colors.border}>{"─".repeat(PANEL_WIDTH - 2)}</text>
</box>
{/* Modified Files Section */}
<box flexDirection="column">
<box flexDirection="row" justifyContent="space-between">
<text
fg={theme.colors.text}
attributes={TextAttributes.BOLD}
>
<text fg={theme.colors.text} attributes={TextAttributes.BOLD}>
Modified Files
</text>
<Show when={modifiedFiles().length > 0}>
<text fg={theme.colors.textDim}>
{modifiedFiles().length}
</text>
<text fg={theme.colors.textDim}>{modifiedFiles().length}</text>
</Show>
</box>
@@ -145,15 +133,11 @@ export function ActivityPanel() {
</text>
<text fg={theme.colors.textDim}> </text>
<Show when={file.additions > 0}>
<text fg={theme.colors.success}>
+{file.additions}
</text>
<text fg={theme.colors.success}>+{file.additions}</text>
</Show>
<Show when={file.deletions > 0}>
<text fg={theme.colors.textDim}> </text>
<text fg={theme.colors.error}>
-{file.deletions}
</text>
<text fg={theme.colors.error}>-{file.deletions}</text>
</Show>
</box>
)}
@@ -164,15 +148,11 @@ export function ActivityPanel() {
<box marginTop={1}>
<text fg={theme.colors.textDim}>Total: </text>
<Show when={totalChanges().additions > 0}>
<text fg={theme.colors.success}>
+{totalChanges().additions}
</text>
<text fg={theme.colors.success}>+{totalChanges().additions}</text>
</Show>
<Show when={totalChanges().deletions > 0}>
<text fg={theme.colors.textDim}> </text>
<text fg={theme.colors.error}>
-{totalChanges().deletions}
</text>
<text fg={theme.colors.error}>-{totalChanges().deletions}</text>
</Show>
</box>
</Show>