From 0c6b0549e792d063bc0889ed9bb6e4b534f10ef8 Mon Sep 17 00:00:00 2001
From: rock-solid-sites
Date: Wed, 13 May 2026 23:57:29 +0200
Subject: [PATCH 001/129] docs: translate configuration.md and mcp.md to
English
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
These docs ship in Chinese only. This adds English translations
for the same content. Translation was done in DeepSeek web chat
(free, off-API) by a non-Chinese-speaking contributor — please
verify accuracy before merging.
---
docs/configuration.md | 201 +++++++++++++++++++++---------------------
docs/mcp.md | 136 ++++++++++++++--------------
2 files changed, 168 insertions(+), 169 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index f8e52c3..369f8e4 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,71 +1,71 @@
-# Deep Code 配置
+# Deep Code Configuration
-## 配置层级
+## Configuration Hierarchy
-配置按以下优先级顺序应用(数字较小的会被数字较大的覆盖):
+Configuration is applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones):
-| 层级 | 配置来源 | 说明 |
-| ---- | ------------ | ------------------------------------------- |
-| 1 | 默认值 | 应用程序内硬编码的默认值 |
-| 2 | 用户设置文件 | 当前用户的全局设置 |
-| 3 | 项目设置文件 | 项目特定的设置 |
-| 4 | 环境变量 | 系统范围或会话特定的变量 |
+| Layer | Configuration Source | Description |
+| ----- | -------------------- | ---------------------------------------------- |
+| 1 | Defaults | Hardcoded defaults within the application |
+| 2 | User settings file | Global settings for the current user |
+| 3 | Project settings file| Project-specific settings |
+| 4 | Environment variables| System-wide or session-specific variables |
-## 设置文件
+## Settings File
-Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两个层级的存放位置:
+Deep Code uses the `settings.json` file for persistent configuration, supporting two storage locations:
-| 文件类型 | 位置 | 作用范围 |
-| ------------ | ---------------------------------- | ---------------------------------------------------- |
-| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 |
-| 项目设置文件 | `项目根目录/.deepcode/settings.json` | 仅在该特定项目中运行 Deep Code 时生效。项目设置会覆盖用户设置。 |
+| File Type | Location | Scope |
+| ------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
+| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. |
+| Project settings file | `/.deepcode/settings.json` | Takes effect only when running Deep Code in that specific project. Project settings override user settings. |
-### `settings.json` 中的可用设置
+### Available Settings in `settings.json`
-以下是 `settings.json` 支持的全部顶层字段,以及 `env` 内部支持的子字段:
+The following are all the top-level fields supported in `settings.json`, along with the sub-fields inside `env`:
-| 字段 | 类型 | 说明 |
-| -------------------- | --------- | ------------------------------------------------------------------- |
-| `env` | object | 环境变量分组(见下方子字段表) |
-| `model` | string | 模型名称。优先级高于 `env.MODEL` |
-| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) |
-| `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) |
-| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) |
-| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) |
-| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 |
-| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) |
+| Field | Type | Description |
+| ------------------ | ------- | --------------------------------------------------------------------------- |
+| `env` | object | Group of environment variables (see sub-field table below) |
+| `model` | string | Model name. Takes precedence over `env.MODEL` |
+| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)|
+| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) |
+| `debugLogEnabled` | boolean | Enable debug log output (default `false`) |
+| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) |
+| `webSearchTool` | string | Full path to a custom web search script |
+| `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) |
-#### `env` 子字段
+#### `env` Sub-fields
-| 字段 | 类型 | 说明 |
-| ---------- | ------ | ------------------------------------------------------------------ |
-| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` |
-| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` |
-| `API_KEY` | string | API 密钥 |
-| `THINKING_ENABLED` | string | 是否启用思考模式 |
-| `REASONING_EFFORT` | string | 推理强度 |
-| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 |
-| `<其他任意KEY>` | string | 自定义环境变量 |
+| Field | Type | Description |
+| ----------------- | ------ | ---------------------------------------------------------------- |
+| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` |
+| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` |
+| `API_KEY` | string | API key |
+| `THINKING_ENABLED`| string | Enable thinking mode |
+| `REASONING_EFFORT`| string | Reasoning intensity |
+| `DEBUG_LOG_ENABLED`| string| Enable debug log output |
+| `` | string | Custom environment variable |
-#### `thinkingEnabled` — 思考模式
+#### `thinkingEnabled` — Thinking Mode
-是否启用 DeepSeek 思考模式。设置为 `true` 启用、`false` 禁用。
+Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to disable.
-- 对于 `deepseek-v4-pro` 和 `deepseek-v4-flash`,思考模式**默认启用**。
-- 对于其他模型,思考模式**默认关闭**。
+- For `deepseek-v4-pro` and `deepseek-v4-flash`, thinking mode is **enabled by default**.
+- For other models, thinking mode is **disabled by default**.
-#### `reasoningEffort` — 推理强度
+#### `reasoningEffort` — Reasoning Intensity
-当思考模式启用时,控制模型思考的深度:
+When thinking mode is enabled, controls the depth of the model’s reasoning:
-| 值 | 说明 |
-| ------ | --------------------------------- |
-| `max` | 最大推理深度(默认值) |
-| `high` | 较高推理深度,token消耗相对较小 |
+| Value | Description |
+| ------ | --------------------------------------------------------- |
+| `max` | Maximum reasoning depth (default) |
+| `high` | Higher reasoning depth with relatively lower token usage |
-#### `notify` — 任务完成通知
+#### `notify` — Task Completion Notification
-设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。
+Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message).
```json
{
@@ -73,9 +73,9 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
}
```
-#### `webSearchTool` — 自定义联网搜索
+#### `webSearchTool` — Custom Web Search
-Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径:
+Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script:
```json
{
@@ -83,16 +83,16 @@ Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索
}
```
-脚本接收一个搜索查询参数,输出 JSON 格式的结果供 AI 使用。
+The script receives a search query as an argument and outputs results in JSON format for the AI.
-#### `mcpServers` — MCP 服务器
+#### `mcpServers` — MCP Servers
-MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。
+Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object.
```json
{
"mcpServers": {
- "<服务名>": {
+ "": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
@@ -103,71 +103,70 @@ MCP(Model Context Protocol)服务器配置。值是键值对,键为服务
}
```
-| McpServerConfig 字段 | 类型 | 必填 | 说明 |
-| -------------------- | -------- | ---- | -------------------------------------------------------------------- |
-| `command` | string | 是 | 可执行文件路径或命令(如 `npx`、`node`、`python`) |
-| `args` | string[] | 否 | 传递给命令的参数列表 |
-| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量 |
+| McpServerConfig field | Type | Required | Description |
+| --------------------- | -------- | -------- | ------------------------------------------------------------------------ |
+| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) |
+| `args` | string[] | No | List of arguments passed to the command |
+| `env` | object | No | Environment variables passed to the MCP server process |
-> 当 `command` 为 `npx` 时,Deep Code 会自动在参数前补充 `-y`。
+> When `command` is `npx`, Deep Code automatically prepends `-y` to the arguments.
-详细 MCP 使用说明请参考 [mcp.md](mcp.md)。
+For detailed MCP usage instructions, refer to [mcp.md](mcp.md).
+#### `debugLogEnabled` — Debug Log
-#### `debugLogEnabled` — 调试日志
+Set to `true` to enable detailed debug logging (default `false`), useful for troubleshooting API calls and tool execution.
-设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。
+## Environment Variable Priority
-## 环境变量优先级
+Environment variables are a common way to configure applications, especially for sensitive information (such as api-key) or settings that may change between environments.
-环境变量是配置应用程序的常用方式,尤其适用于敏感信息(如 api-key)或可能在不同环境之间更改的设置。
+### Priority Principle
-### 优先级原则
+Environment variable priority follows the logic of “the more specific and localized the configuration, the higher the priority”, and the override rule of “env files protect existing environment by default, system variables override env files”. (The `env` object in settings.json can be thought of as a type of env file.)
-环境变量优先级遵循“越具体、越局部的配置,优先级越高”和“env文件默认保护现有环境,系统变量高于env文件”的覆盖逻辑。(settings.json的env对象可以认为是一种env文件)
+Priority levels (from lowest to highest):
+1. `env` defined at the top level of `settings.json` – this is a general configuration for the entire tool and all its subprocesses (global variables). Can be overridden by outer environment variables, but the environment variable KEY has the `DEEPCODE_` prefix removed.
+2. `env` defined inside `mcpServers` in `settings.json` – this is the most specific configuration for a particular MCP service (local variables). Can be overridden by outer environment variables, but the KEY has the `MCP_` prefix removed.
+3. Shell/system environment variables – operating system level.
-优先级层级 (由低到高)
-1. settings.json 外层的 env:这是针对整个工具及其所有子进程的通用配置(全局变量)。可被外层环境变量覆盖,但环境变量KEY会移除`DEEPCODE_`前缀。
-2. settings.json mcpServers 内定义的 env:这是针对特定 MCP 服务的最具体配置(局部变量)。可被外层环境变量覆盖,但环境变量KEY会移除`MCP_`前缀。
-3. Shell 环境系统变量:操作系统层面的环境变量。
+### Scenarios
-### 场景
+#### 1. Setting the model’s api_key and base_url
-#### 一、设置模型的api_key, base_url
+Applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones) – using api_key as an example:
-按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以api_key为例):
+1. Hardcoded default: `""`
+2. User-level settings.json: `{"env": {"API_KEY": "abc123"}}`
+3. Project-level settings.json: `{"env": {"API_KEY": "abc123"}}`
+4. System environment variable: `DEEPCODE_API_KEY=abc123 deepcode`
-1. 硬编码默认值: `""`
-2. 用户级settings.json: `{"env": {"API_KEY": "abc123"}}`
-3. 项目级settings.json: `{"env": {"API_KEY": "abc123"}}`
-4. 系统环境变量: `DEEPCODE_API_KEY=abc123 deepcode`
+#### 2. Setting model, thinkingEnabled, and reasoningEffort
-#### 二、设置模型的model, thinkingEnabled, reasoningEffort
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using thinkingEnabled as an example:
-按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以thinkingEnabled为例):
+1. Hardcoded default: `true`
+2. User-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+3. User-level settings.json: `{"thinkingEnabled": true}`
+4. Project-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+5. Project-level settings.json: `{"thinkingEnabled": true}`
+6. System environment variable: `DEEPCODE_THINKING_ENABLED=true deepcode`
-1. 硬编码默认值: `true`
-2. 用户级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
-3. 用户级settings.json: `{"thinkingEnabled": true}`
-4. 项目级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
-5. 项目级settings.json: `{"thinkingEnabled": true}`
-6. 系统环境变量: `DEEPCODE_THINKING_ENABLED=true deepcode`
+#### 3. Setting environment variables for external scripts like notify and webSearchTool
-#### 三、设置启动notify, webSearchTool等外挂脚本的环境变量
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using notify as an example:
-按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以notify为例):
+1. Hardcoded default: `os.environ.get('WEBHOOK', '...') # notify script code`
+2. User-level settings.json: `{"env": {"WEBHOOK": "..."}}`
+3. Project-level settings.json: `{"env": {"WEBHOOK": "true"}}`
+4. System environment variable: `DEEPCODE_WEBHOOK=... deepcode`
-1. 硬编码默认值:`os.environ.get('WEBHOOK', '...') # notify脚本代码`
-2. 用户级settings.json: `{"env": {"WEBHOOK": "..."}}`
-3. 项目级settings.json: `{"env": {"WEBHOOK": "true"}}`
-4. 系统环境变量: `DEEPCODE_WEBHOOK=... deepcode`
+#### 4. Setting environment variables for an MCP Service
-#### 四、设置MCP Service的环境变量
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using a GitHub MCP server as an example:
-按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以github MCP server为例):
-
-1. 用户级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
-2. 用户级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
-3. 项目级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
-4. 项目级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
-5. 系统环境变量: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
+1. User-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+2. User-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
\ No newline at end of file
diff --git a/docs/mcp.md b/docs/mcp.md
index fe6711d..11adda5 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -1,22 +1,22 @@
-# Deep Code CLI MCP 配置指南
+# Deep Code CLI MCP Configuration Guide
-Deep Code CLI 支持 MCP(Model Context Protocol),让 AI 助手能够连接外部工具和服务,如 GitHub、浏览器、数据库等。
+Deep Code CLI supports MCP (Model Context Protocol), enabling AI assistants to connect with external tools and services such as GitHub, browsers, databases, and more.
-## 概述
+## Overview
-配置 MCP 后,Deep Code 可以:
+Once MCP is configured, Deep Code can:
-- 操作 GitHub 仓库(查看 Issues、创建 PR、搜索代码等)
-- 操控浏览器(截图、点击、填表单等)
-- 访问文件系统
-- 连接数据库和 API
-- ...以及任何兼容 MCP 协议的外部服务
+- Operate on GitHub repositories (view issues, create PRs, search code, etc.)
+- Control browsers (screenshots, clicks, form filling, etc.)
+- Access the file system
+- Connect to databases and APIs
+- ...and any external service compatible with the MCP protocol
-MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,例如 `mcp__github__search_code`。
+MCP tools are named in Deep Code using the format `mcp____`, for example `mcp__github__search_code`.
-## 配置 MCP 服务器
+## Configuring MCP Servers
-编辑 `~/.deepcode/settings.json`,添加 `mcpServers` 字段:
+Edit `~/.deepcode/settings.json` and add the `mcpServers` field:
```json
{
@@ -28,30 +28,30 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
"thinkingEnabled": true,
"reasoningEffort": "max",
"mcpServers": {
- "<服务名称>": {
- "command": "<可执行文件>",
- "args": ["<参数1>", "<参数2>"],
+ "": {
+ "command": "",
+ "args": ["", ""],
"env": {
- "<环境变量>": "<值>"
+ "": ""
}
}
}
}
```
-### 配置项说明
+### Configuration Fields
-| 字段 | 类型 | 必填 | 说明 |
-| --------- | -------- | ---- | ---------------------------------------------------------------------------------------------------------------------- |
-| `command` | string | 是 | MCP 服务器的可执行文件路径或命令(如 `npx`、`node`、`python`)。当命令是 `npx` 时,Deep Code 会自动在参数前补充 `-y`。 |
-| `args` | string[] | 否 | 传递给命令的参数列表 |
-| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量(如 API Key) |
+| Field | Type | Required | Description |
+| --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `command` | string | Yes | Path or command of the MCP server executable (e.g., `npx`, `node`, `python`). When the command is `npx`, Deep Code automatically prepends `-y` to the arguments. |
+| `args` | string[] | No | List of arguments to pass to the command |
+| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process |
-## 常用 MCP 示例
+## Common MCP Examples
### GitHub MCP
-让 Deep Code 直接操作 GitHub 仓库(搜索代码、管理 Issue/PR、读写文件等):
+Allows Deep Code to directly operate on GitHub repositories (search code, manage issues/PRs, read/write files, etc.):
```json
{
@@ -67,11 +67,11 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
}
```
-> GitHub Personal Access Token 可在 [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 生成。
+> Generate a GitHub Personal Access Token at [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens).
-### 浏览器控制(Playwright)
+### Browser Control (Playwright)
-让 Deep Code 操控浏览器进行截图、页面操作等:
+Lets Deep Code control a browser for screenshots, page interactions, etc.:
```json
{
@@ -84,9 +84,9 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
}
```
-### 文件系统
+### File System
-让 Deep Code 在指定目录中读写文件:
+Enables Deep Code to read and write files within a specified directory:
```json
{
@@ -99,7 +99,7 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
}
```
-### 自定义 Python MCP
+### Custom Python MCP
```json
{
@@ -115,9 +115,9 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
}
```
-## 完整配置示例
+## Full Configuration Example
-以下是一个配置了 GitHub 和 Playwright 两个 MCP 服务器的完整 `~/.deepcode/settings.json`:
+Below is a complete `~/.deepcode/settings.json` with both GitHub and Playwright MCP servers configured:
```json
{
@@ -144,62 +144,62 @@ MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,
}
```
-## 使用 MCP
+## Using MCP
-配置完成后,启动 `deepcode`,使用 `/mcp` 命令管理 MCP 连接:
+After configuration, start `deepcode` and use the `/mcp` command to manage MCP connections:
-- `/mcp` — 查看已配置的 MCP 服务器状态
-- `/mcp add` — 添加新的 MCP 服务器
-- `/mcp remove` — 移除 MCP 服务器
-- `/mcp list` — 列出所有已连接的 MCP 服务器及其工具
+- `/mcp` — View the status of configured MCP servers
+- `/mcp add` — Add a new MCP server
+- `/mcp remove` — Remove an MCP server
+- `/mcp list` — List all connected MCP servers and their tools
-在对话中直接使用 MCP 工具名称即可调用,例如:
+Simply use the MCP tool name in your conversation to invoke it, for example:
```
-帮我搜索 GitHub 上 deepcode-cli 仓库的 issues
+Help me search for issues in the deepcode-cli repository on GitHub
```
-AI 会自动调用 `mcp__github__search_issues` 工具完成操作。
+The AI will automatically invoke the `mcp__github__search_issues` tool to complete the action.
-## 工具命名规则
+## Tool Naming Convention
-MCP 工具名称由三部分组成:`mcp__<服务名>__<工具名>`
+An MCP tool name consists of three parts: `mcp____`
-| 服务名 | 工具名 | 完整调用名 |
-| ---------- | ----------------------- | ------------------------------------------ |
-| github | search_code | `mcp__github__search_code` |
-| github | create_pull_request | `mcp__github__create_pull_request` |
-| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
-| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
+| Service | Tool Name | Full Invocation Name |
+| ---------- | ----------------------- | ------------------------------------------- |
+| github | search_code | `mcp__github__search_code` |
+| github | create_pull_request | `mcp__github__create_pull_request` |
+| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
+| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
-你可以通过 `/mcp list` 查看每个服务器提供的具体工具列表。
+You can view the list of tools provided by each server using `/mcp list`.
-## 故障排查
+## Troubleshooting
-### 启动失败
+### Startup Failure
-如果 MCP 服务器无法启动,检查:
+If an MCP server fails to start, check:
-1. `command` 是否已安装(如 `npx` 需要 Node.js)
-2. `env` 中的环境变量是否正确(如 `GITHUB_PERSONAL_ACCESS_TOKEN`)
-3. 运行 `deepcode` 的终端是否有网络访问权限
+1. Whether `command` is installed (e.g., `npx` requires Node.js)
+2. Whether environment variables in `env` are correct (e.g., `GITHUB_PERSONAL_ACCESS_TOKEN`)
+3. Whether the terminal running `deepcode` has network access
-### 工具不显示
+### Tools Not Showing Up
-1. 确认 `settings.json` 中的 `mcpServers` 字段格式正确
-2. 启动 deepcode 后使用 `/mcp` 查看服务器状态
-3. 如果服务器状态显示错误,根据错误信息排查
+1. Verify that the `mcpServers` field in `settings.json` is correctly formatted
+2. After starting deepcode, use `/mcp` to check server status
+3. If the server status shows an error, debug based on the error message
-### Windows 用户
+### Windows Users
-在 Windows 上,Deep Code CLI 会自动为 `.cmd` 命令添加 shell 支持。如果你的 MCP 命令是批处理脚本,确保文件名以 `.cmd` 结尾。
+On Windows, Deep Code CLI automatically adds shell support for `.cmd` commands. If your MCP command is a batch script, ensure the filename ends with `.cmd`.
-## 编写你自己的 MCP 服务器
+## Writing Your Own MCP Server
-MCP 服务器遵循 [Model Context Protocol](https://modelcontextprotocol.io/) 规范,使用 JSON-RPC 2.0 通信。你可以用任何语言编写 MCP 服务器,只要实现以下协议即可:
+MCP servers follow the [Model Context Protocol](https://modelcontextprotocol.io/) specification and communicate using JSON‑RPC 2.0. You can write an MCP server in any language as long as it implements the following methods:
-1. `initialize` — 握手和协议协商
-2. `tools/list` — 返回可用工具列表
-3. `tools/call` — 执行工具调用
+1. `initialize` — Handshake and protocol negotiation
+2. `tools/list` — Return the list of available tools
+3. `tools/call` — Execute a tool call
-更多参考:[MCP 官方文档](https://modelcontextprotocol.io/)
+For more information, see the [official MCP documentation](https://modelcontextprotocol.io/).
\ No newline at end of file
From fb38560830a55c217122ebff5ee42836523d7c3b Mon Sep 17 00:00:00 2001
From: hcyang
Date: Thu, 14 May 2026 14:23:04 +0800
Subject: [PATCH 002/129] =?UTF-8?q?feat(mcp):=20=E9=9B=86=E6=88=90?=
=?UTF-8?q?=E5=B9=B6=E5=B1=95=E7=A4=BAMCP=E6=9C=8D=E5=8A=A1=E5=99=A8?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E8=B5=84=E6=BA=90=E5=88=97=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增McpStatusList组件,实现MCP服务器状态、工具、提示和资源的可视化展示
- 修改App组件,支持切换视图显示MCP状态列表
- MCP客户端增加对prompts和resources的分页列表及读取功能
- MCP管理器扩展,支持发现和管理prompts及resources,及工具列表变化的事件通知
- 优化MCP客户端,支持JSON-RPC通知的处理,完善请求超时控制
- 在session中添加MCP工具列表变化监听,实时更新工具定义数据
- 提供键盘操作支持,实现MCP状态列表的上下翻页和快速导航
- 美化MCP状态列表界面,显示服务器状态及能力的详细信息
---
src/mcp/mcp-client.ts | 133 ++++++++++++++++++++++--
src/mcp/mcp-manager.ts | 185 ++++++++++++++++++++++++++++++++-
src/session.ts | 3 +
src/ui/App.tsx | 39 ++-----
src/ui/McpStatusList.tsx | 219 +++++++++++++++++++++++++++++++++++++++
5 files changed, 537 insertions(+), 42 deletions(-)
create mode 100644 src/ui/McpStatusList.tsx
diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts
index 0fad322..a086c1d 100644
--- a/src/mcp/mcp-client.ts
+++ b/src/mcp/mcp-client.ts
@@ -17,6 +17,12 @@ type JsonRpcResponse = {
error?: { code: number; message: string; data?: unknown };
};
+type JsonRpcNotification = {
+ jsonrpc: "2.0";
+ method: string;
+ params?: Record;
+};
+
export type McpToolDefinition = {
name: string;
description?: string;
@@ -24,6 +30,7 @@ export type McpToolDefinition = {
type: "object";
properties: Record;
required?: string[];
+ additionalProperties?: boolean;
};
};
@@ -37,6 +44,58 @@ type CallToolResult = {
isError?: boolean;
};
+export type McpPromptArgument = {
+ name: string;
+ description?: string;
+ required?: boolean;
+};
+
+export type McpPromptDefinition = {
+ name: string;
+ description?: string;
+ arguments?: McpPromptArgument[];
+};
+
+type ListPromptsResult = {
+ prompts: McpPromptDefinition[];
+ nextCursor?: string;
+};
+
+export type McpPromptMessage = {
+ role: "user" | "assistant";
+ content: { type: string; text?: string };
+};
+
+type GetPromptResult = {
+ description?: string;
+ messages: McpPromptMessage[];
+};
+
+export type McpResourceDefinition = {
+ uri: string;
+ name: string;
+ description?: string;
+ mimeType?: string;
+};
+
+type ListResourcesResult = {
+ resources: McpResourceDefinition[];
+ nextCursor?: string;
+};
+
+export type McpResourceContent = {
+ uri: string;
+ mimeType?: string;
+ text?: string;
+ blob?: string;
+};
+
+type ReadResourceResult = {
+ contents: McpResourceContent[];
+};
+
+export type McpNotificationHandler = (method: string, params?: Record) => void;
+
export class McpClient {
private process: ChildProcess | null = null;
private reader: Interface | null = null;
@@ -46,13 +105,17 @@ export class McpClient {
{ resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }
>();
private stderrBuffer = "";
+ private notificationHandler: McpNotificationHandler | null = null;
constructor(
private readonly serverName: string,
private readonly command: string,
private readonly args: string[] = [],
- private readonly env?: Record
- ) {}
+ private readonly env?: Record,
+ onNotification?: McpNotificationHandler
+ ) {
+ this.notificationHandler = onNotification ?? null;
+ }
async connect(timeoutMs: number): Promise {
return new Promise((resolve, reject) => {
@@ -109,7 +172,7 @@ export class McpClient {
this.sendRequest(
"initialize",
{
- protocolVersion: "2024-11-05",
+ protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "deepcode-cli", version: "0.1.0" },
},
@@ -141,8 +204,50 @@ export class McpClient {
throw this.withStderr(`MCP server "${this.serverName}" returned too many tools/list pages`);
}
- async callTool(name: string, args: Record): Promise {
- return (await this.sendRequest("tools/call", { name, arguments: args })) as CallToolResult;
+ async callTool(name: string, args: Record, timeoutMs = 60_000): Promise {
+ return (await this.sendRequest("tools/call", { name, arguments: args }, timeoutMs)) as CallToolResult;
+ }
+
+ async listPrompts(timeoutMs: number): Promise {
+ const prompts: McpPromptDefinition[] = [];
+ let cursor: string | undefined;
+
+ for (let page = 0; page < 100; page++) {
+ const params = cursor ? { cursor } : {};
+ const result = (await this.sendRequest("prompts/list", params, timeoutMs)) as ListPromptsResult;
+ prompts.push(...(result.prompts ?? []));
+ cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
+ if (!cursor) {
+ return prompts;
+ }
+ }
+
+ throw this.withStderr(`MCP server "${this.serverName}" returned too many prompts/list pages`);
+ }
+
+ async getPrompt(name: string, args: Record, timeoutMs = 30_000): Promise {
+ return (await this.sendRequest("prompts/get", { name, arguments: args }, timeoutMs)) as GetPromptResult;
+ }
+
+ async listResources(timeoutMs: number): Promise {
+ const resources: McpResourceDefinition[] = [];
+ let cursor: string | undefined;
+
+ for (let page = 0; page < 100; page++) {
+ const params = cursor ? { cursor } : {};
+ const result = (await this.sendRequest("resources/list", params, timeoutMs)) as ListResourcesResult;
+ resources.push(...(result.resources ?? []));
+ cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
+ if (!cursor) {
+ return resources;
+ }
+ }
+
+ throw this.withStderr(`MCP server "${this.serverName}" returned too many resources/list pages`);
+ }
+
+ async readResource(uri: string, timeoutMs = 30_000): Promise {
+ return (await this.sendRequest("resources/read", { uri }, timeoutMs)) as ReadResourceResult;
}
disconnect(): void {
@@ -195,7 +300,23 @@ export class McpClient {
private handleLine(line: string): void {
try {
- const message = JSON.parse(line) as JsonRpcResponse;
+ const parsed: unknown = JSON.parse(line);
+
+ // Handle notifications (no id field — server-initiated)
+ if (parsed && typeof parsed === "object" && !("id" in parsed)) {
+ const notification = parsed as JsonRpcNotification;
+ if (this.notificationHandler && typeof notification.method === "string") {
+ try {
+ this.notificationHandler(notification.method, notification.params);
+ } catch {
+ // Swallow handler errors to avoid crashing the reader loop
+ }
+ }
+ return;
+ }
+
+ // Handle responses to our requests
+ const message = parsed as JsonRpcResponse;
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
const pending = this.pendingRequests.get(message.id)!;
this.pendingRequests.delete(message.id);
diff --git a/src/mcp/mcp-manager.ts b/src/mcp/mcp-manager.ts
index 030d0d3..8121dbc 100644
--- a/src/mcp/mcp-manager.ts
+++ b/src/mcp/mcp-manager.ts
@@ -1,7 +1,8 @@
-import { McpClient, type McpToolDefinition } from "./mcp-client";
+import { McpClient, type McpToolDefinition, type McpPromptDefinition, type McpResourceDefinition } from "./mcp-client";
import type { McpServerConfig } from "../settings";
const MCP_STARTUP_TIMEOUT_MS = 30_000;
+const MCP_CALL_TOOL_TIMEOUT_MS = 60_000;
type McpToolEntry = {
serverName: string;
@@ -18,15 +19,32 @@ export type McpServerStatus = {
error?: string;
toolCount: number;
tools: string[];
+ promptCount: number;
+ prompts: string[];
+ resourceCount: number;
+ resources: string[];
};
export class McpManager {
private clients: McpClient[] = [];
private tools: McpToolEntry[] = [];
+ private prompts: Array<{
+ serverName: string;
+ namespacedName: string;
+ definition: McpPromptDefinition;
+ client: McpClient;
+ }> = [];
+ private resources: Array<{
+ serverName: string;
+ namespacedName: string;
+ definition: McpResourceDefinition;
+ client: McpClient;
+ }> = [];
private initialized = false;
private disposed = false;
private configuredServerNames: string[] = [];
private serverStatuses: McpServerStatus[] = [];
+ private onToolsListChanged: (() => void) | null = null;
prepare(servers?: Record): void {
if (!servers || Object.keys(servers).length === 0) return;
@@ -48,6 +66,10 @@ export class McpManager {
connected: false,
toolCount: 0,
tools: [],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
});
}
}
@@ -65,7 +87,13 @@ export class McpManager {
if (this.disposed) break;
let client: McpClient | null = null;
try {
- client = new McpClient(name, config.command, config.args ?? [], config.env);
+ client = new McpClient(name, config.command, config.args ?? [], config.env, (method) => {
+ if (method === "notifications/tools/list_changed") {
+ this.refreshServerTools(name, client!).catch(() => {
+ // swallow refresh errors
+ });
+ }
+ });
await client.connect(MCP_STARTUP_TIMEOUT_MS);
if (this.disposed) {
client.disconnect();
@@ -73,6 +101,7 @@ export class McpManager {
}
this.clients.push(client);
+ // Discover tools
const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS);
if (this.disposed) break;
const toolNamespacedNames: string[] = [];
@@ -87,12 +116,57 @@ export class McpManager {
});
toolNamespacedNames.push(namespacedName);
}
+
+ // Discover prompts
+ let serverPrompts: McpPromptDefinition[] = [];
+ try {
+ serverPrompts = await client.listPrompts(MCP_STARTUP_TIMEOUT_MS);
+ } catch {
+ // Server may not support prompts — safe to ignore
+ }
+ if (this.disposed) break;
+ const promptNamespacedNames: string[] = [];
+ for (const prompt of serverPrompts) {
+ const namespacedName = `mcp__${name}__${prompt.name}`;
+ this.prompts.push({
+ serverName: name,
+ namespacedName,
+ definition: prompt,
+ client,
+ });
+ promptNamespacedNames.push(namespacedName);
+ }
+
+ // Discover resources
+ let serverResources: McpResourceDefinition[] = [];
+ try {
+ serverResources = await client.listResources(MCP_STARTUP_TIMEOUT_MS);
+ } catch {
+ // Server may not support resources — safe to ignore
+ }
+ if (this.disposed) break;
+ const resourceNamespacedNames: string[] = [];
+ for (const resource of serverResources) {
+ const namespacedName = `mcp__${name}__${resource.name}`;
+ this.resources.push({
+ serverName: name,
+ namespacedName,
+ definition: resource,
+ client,
+ });
+ resourceNamespacedNames.push(namespacedName);
+ }
+
this.setStatus({
name,
status: "ready",
connected: true,
toolCount: serverTools.length,
tools: toolNamespacedNames,
+ promptCount: serverPrompts.length,
+ prompts: promptNamespacedNames,
+ resourceCount: serverResources.length,
+ resources: resourceNamespacedNames,
});
} catch (err) {
if (this.disposed) break;
@@ -106,6 +180,10 @@ export class McpManager {
error: message,
toolCount: 0,
tools: [],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
});
}
}
@@ -122,6 +200,10 @@ export class McpManager {
connected: false,
toolCount: 0,
tools: [],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
});
}
}
@@ -150,7 +232,9 @@ export class McpManager {
type: "object" as const,
properties: t.definition.inputSchema.properties,
required: t.definition.inputSchema.required,
- additionalProperties: false,
+ ...(t.definition.inputSchema.additionalProperties !== undefined
+ ? { additionalProperties: t.definition.inputSchema.additionalProperties }
+ : {}),
},
},
}));
@@ -162,7 +246,8 @@ export class McpManager {
async executeMcpTool(
name: string,
- args: Record
+ args: Record,
+ timeoutMs = MCP_CALL_TOOL_TIMEOUT_MS
): Promise<{ ok: boolean; name: string; output?: string; error?: string }> {
const tool = this.tools.find((t) => t.namespacedName === name);
if (!tool) {
@@ -170,7 +255,7 @@ export class McpManager {
}
try {
- const result = await tool.client.callTool(tool.originalName, args);
+ const result = await tool.client.callTool(tool.originalName, args, timeoutMs);
const text = result.content
.filter((c) => c.type === "text" && c.text)
.map((c) => c.text)
@@ -189,6 +274,64 @@ export class McpManager {
}
}
+ async getMcpPrompt(
+ name: string,
+ args: Record
+ ): Promise<{ ok: boolean; name: string; output?: string; error?: string }> {
+ const prompt = this.prompts.find((p) => p.namespacedName === name);
+ if (!prompt) {
+ return { ok: false, name, error: `Unknown MCP prompt: ${name}` };
+ }
+
+ try {
+ const result = await prompt.client.getPrompt(prompt.definition.name, args);
+ const text = result.messages
+ .filter((m) => m.content.type === "text" && m.content.text)
+ .map((m) => `[${m.role}] ${m.content.text}`)
+ .join("\n");
+ return {
+ ok: true,
+ name,
+ output: text || JSON.stringify(result),
+ };
+ } catch (err) {
+ return {
+ ok: false,
+ name,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+ }
+
+ async readMcpResource(
+ name: string,
+ uri: string
+ ): Promise<{ ok: boolean; name: string; output?: string; error?: string }> {
+ const resource = this.resources.find((r) => r.namespacedName === name);
+ if (!resource) {
+ return { ok: false, name, error: `Unknown MCP resource: ${name}` };
+ }
+
+ try {
+ const result = await resource.client.readResource(uri);
+ const text = result.contents
+ .filter((c) => c.text)
+ .map((c) => c.text)
+ .join("\n");
+ return {
+ ok: true,
+ name,
+ output: text || JSON.stringify(result.contents),
+ };
+ } catch (err) {
+ return {
+ ok: false,
+ name,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+ }
+
disconnect(): void {
this.disposed = true;
for (const client of this.clients) {
@@ -196,11 +339,43 @@ export class McpManager {
}
this.clients = [];
this.tools = [];
+ this.prompts = [];
+ this.resources = [];
this.serverStatuses = [];
this.configuredServerNames = [];
this.initialized = false;
}
+ private async refreshServerTools(serverName: string, client: McpClient): Promise {
+ const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS);
+ // Remove old tool entries for this server
+ this.tools = this.tools.filter((t) => t.serverName !== serverName);
+ const toolNamespacedNames: string[] = [];
+ for (const tool of serverTools) {
+ const namespacedName = `mcp__${serverName}__${tool.name}`;
+ this.tools.push({
+ serverName,
+ originalName: tool.name,
+ namespacedName,
+ definition: tool,
+ client,
+ });
+ toolNamespacedNames.push(namespacedName);
+ }
+ // Update status
+ const existing = this.serverStatuses.find((s) => s.name === serverName);
+ if (existing) {
+ existing.toolCount = serverTools.length;
+ existing.tools = toolNamespacedNames;
+ }
+ // Notify listener
+ this.onToolsListChanged?.();
+ }
+
+ setOnToolsListChanged(handler: () => void): void {
+ this.onToolsListChanged = handler;
+ }
+
private setStatus(status: McpServerStatus): void {
if (this.disposed) return;
const index = this.serverStatuses.findIndex((s) => s.name === status.name);
diff --git a/src/session.ts b/src/session.ts
index a174f9b..161ba98 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -203,6 +203,9 @@ export class SessionManager {
}
async initMcpServers(servers?: Record): Promise {
+ this.mcpManager.setOnToolsListChanged(() => {
+ this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
+ });
await this.mcpManager.initialize(servers);
this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
}
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 757b40c..ffe89e4 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -29,6 +29,7 @@ import { buildLoadingText } from "./loadingText";
import { findExpandedThinkingId } from "./thinkingState";
import { WelcomeScreen } from "./WelcomeScreen";
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
+import { McpStatusList } from "./McpStatusList";
import {
findPendingAskUserQuestion,
formatAskUserQuestionAnswers,
@@ -39,7 +40,7 @@ import { buildExitSummaryText } from "./exitSummary";
const DEFAULT_MODEL = "deepseek-v4-pro";
const DEFAULT_BASE_URL = "https://api.deepseek.com";
-type View = "chat" | "session-list";
+type View = "chat" | "session-list" | "mcp-status";
type AppProps = {
projectRoot: string;
@@ -67,6 +68,7 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
const [welcomeNonce, setWelcomeNonce] = useState(0);
const [resolvedSettings, setResolvedSettings] = useState(() => resolveCurrentSettings(projectRoot));
const [nowTick, setNowTick] = useState(0);
+ const [mcpStatuses, setMcpStatuses] = useState>([]);
const messagesRef = useRef([]);
messagesRef.current = messages;
@@ -189,36 +191,9 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
return;
}
if (submission.command === "mcp") {
- process.stdout.write("\n");
- process.stdout.write(chalk.bold.cyan("MCP Server Status\n"));
- process.stdout.write(chalk.dim("─────────────────\n"));
- const statuses = sessionManager.getMcpStatus();
- if (statuses.length === 0) {
- process.stdout.write(chalk.dim(" No MCP servers configured.\n"));
- } else {
- for (const s of statuses) {
- if (s.status === "starting") {
- process.stdout.write(`${chalk.yellow("●")} ${chalk.bold(s.name)} - Starting...`);
- } else if (s.status === "failed") {
- process.stdout.write(`${chalk.red("✖")} ${chalk.bold(s.name)} - Failed (${s.error ?? "unknown error"})`);
- } else {
- process.stdout.write(`${chalk.green("✔")} ${chalk.bold(s.name)} - Ready (${s.toolCount} tools)`);
- }
- process.stdout.write("\n");
- if (s.status === "ready" && s.tools.length > 0) {
- for (const tool of s.tools) {
- process.stdout.write(chalk.dim(` - ${tool}\n`));
- }
- }
- }
- }
- process.stdout.write(chalk.dim("─────────────────\n"));
- process.stdout.write(
- chalk.dim(` Total: ${statuses.filter((s) => s.status === "ready").length} ready, `) +
- chalk.dim(`${statuses.filter((s) => s.status === "starting").length} starting, `) +
- chalk.dim(`${statuses.filter((s) => s.status === "failed").length} failed\n`)
- );
- process.stdout.write("\n");
+ setShowWelcome(false);
+ setMcpStatuses(sessionManager.getMcpStatus());
+ setView("mcp-status");
return;
}
@@ -471,6 +446,8 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
onSelect={(id) => void handleSelectSession(id)}
onCancel={() => setView("chat")}
/>
+ ) : view === "mcp-status" ? (
+ setView("chat")} />
) : shouldShowQuestionPrompt && pendingQuestion && !busy ? (
void;
+};
+
+type FlatItem =
+ | { kind: "server"; status: McpServerStatus; serverIndex: number }
+ | { kind: "tool"; name: string; serverName: string }
+ | { kind: "prompt"; name: string; serverName: string }
+ | { kind: "resource"; name: string; serverName: string };
+
+function buildFlatItems(statuses: McpServerStatus[]): FlatItem[] {
+ const items: FlatItem[] = [];
+ for (let i = 0; i < statuses.length; i++) {
+ const status = statuses[i];
+ items.push({ kind: "server", status, serverIndex: i });
+ if (status.status === "ready") {
+ for (const tool of status.tools) {
+ items.push({ kind: "tool", name: tool, serverName: status.name });
+ }
+ for (const prompt of status.prompts) {
+ items.push({ kind: "prompt", name: prompt, serverName: status.name });
+ }
+ for (const resource of status.resources) {
+ items.push({ kind: "resource", name: resource, serverName: status.name });
+ }
+ }
+ }
+ return items;
+}
+
+export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement {
+ const [index, setIndex] = useState(0);
+ const { columns, rows } = useWindowSize();
+
+ const flatItems = useMemo(() => buildFlatItems(statuses), [statuses]);
+
+ const maxVisible = useMemo(() => {
+ const reservedLines = 8;
+ const linesPerItem = 2;
+ const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines);
+ return Math.max(1, Math.floor(availableLines / linesPerItem));
+ }, [rows]);
+
+ const safeIndex = useMemo(() => {
+ if (flatItems.length === 0) return 0;
+ return Math.max(0, Math.min(index, flatItems.length - 1));
+ }, [index, flatItems.length]);
+
+ const scrollOffset = useMemo(() => {
+ if (safeIndex < maxVisible) return 0;
+ return safeIndex - maxVisible + 1;
+ }, [safeIndex, maxVisible]);
+
+ const visibleItems = useMemo(() => {
+ return flatItems.slice(scrollOffset, scrollOffset + maxVisible);
+ }, [flatItems, scrollOffset, maxVisible]);
+
+ useInput((input, key) => {
+ if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
+ onCancel();
+ return;
+ }
+ if (flatItems.length === 0) {
+ return;
+ }
+ if (key.upArrow) {
+ setIndex((i) => Math.max(0, i - 1));
+ return;
+ }
+ if (key.downArrow) {
+ setIndex((i) => Math.min(flatItems.length - 1, i + 1));
+ return;
+ }
+ if (key.pageUp) {
+ setIndex((i) => Math.max(0, i - maxVisible));
+ return;
+ }
+ if (key.pageDown) {
+ setIndex((i) => Math.min(flatItems.length - 1, i + maxVisible));
+ return;
+ }
+ if (key.home) {
+ setIndex(0);
+ return;
+ }
+ if (key.end) {
+ setIndex(flatItems.length - 1);
+ }
+ });
+
+ const readyCount = statuses.filter((s) => s.status === "ready").length;
+ const startingCount = statuses.filter((s) => s.status === "starting").length;
+ const failedCount = statuses.filter((s) => s.status === "failed").length;
+
+ if (statuses.length === 0) {
+ return (
+
+ Manage MCP servers
+ 0 servers
+ No MCP servers configured.
+ Add MCP servers to your settings to get started.
+ Esc to close
+
+ );
+ }
+
+ return (
+
+
+ {/* Header row */}
+
+
+ MCP Server Status
+
+
+ {" "}
+ ({readyCount} ready, {startingCount} starting, {failedCount} failed)
+
+
+ {/* Items list */}
+
+ {visibleItems.map((item, i) => {
+ const actualIndex = scrollOffset + i;
+ const isSelected = actualIndex === safeIndex;
+
+ if (item.kind === "server") {
+ return ;
+ }
+ return (
+
+ );
+ })}
+ {scrollOffset > 0 || scrollOffset + maxVisible < flatItems.length ? (
+
+ {scrollOffset > 0 ? … {scrollOffset} items above. : null}
+ {scrollOffset + maxVisible < flatItems.length ? (
+ … {flatItems.length - scrollOffset - maxVisible} items below.
+ ) : null}
+
+ ) : null}
+
+ {/* Footer */}
+
+ ↑/↓ navigate · PgUp/PgDn page · Esc cancel
+
+
+
+ );
+}
+
+function ServerRow({ status, selected }: { status: McpServerStatus; selected: boolean }): React.ReactElement {
+ const icon = status.status === "ready" ? "✔" : status.status === "failed" ? "✖" : "●";
+ const color = status.status === "ready" ? "green" : status.status === "failed" ? "red" : "yellow";
+ const detail =
+ status.status === "ready"
+ ? `Ready (${status.toolCount} tools, ${status.promptCount} prompts, ${status.resourceCount} resources)`
+ : status.status === "failed"
+ ? `Failed (${status.error ?? "unknown error"})`
+ : "Starting...";
+
+ return (
+
+ {selected ? "› " : " "}
+
+ {icon}
+ {status.name}
+ — {detail}
+
+
+ );
+}
+
+function CapabilityRow({
+ kind,
+ name,
+ selected,
+}: {
+ kind: "tool" | "prompt" | "resource";
+ name: string;
+ selected: boolean;
+}): React.ReactElement {
+ const prefix = kind === "tool" ? "🔧" : kind === "prompt" ? "📝" : "📦";
+ return (
+
+ {selected ? "› " : " "}
+
+ {prefix} {name}
+
+
+ );
+}
From 91e52406293ff1e8683e15ec00eac98bed175f7d Mon Sep 17 00:00:00 2001
From: hcyang
Date: Thu, 14 May 2026 15:40:35 +0800
Subject: [PATCH 003/129] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E5=99=A8=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在FlatItem类型中添加错误信息项
- 失败状态的服务器添加对应错误消息
- 失败错误信息单独用ErrorRow组件渲染
- 更新无服务器时的提示样式,增强视觉层次感
- 为工具项添加左侧缩进优化排版布局
---
src/ui/McpStatusList.tsx | 43 ++++++++++++++++++++++++++++++++++------
1 file changed, 37 insertions(+), 6 deletions(-)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index b04d7af..d3e9048 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -9,6 +9,7 @@ type Props = {
type FlatItem =
| { kind: "server"; status: McpServerStatus; serverIndex: number }
+ | { kind: "error"; error: string; serverName: string }
| { kind: "tool"; name: string; serverName: string }
| { kind: "prompt"; name: string; serverName: string }
| { kind: "resource"; name: string; serverName: string };
@@ -18,6 +19,10 @@ function buildFlatItems(statuses: McpServerStatus[]): FlatItem[] {
for (let i = 0; i < statuses.length; i++) {
const status = statuses[i];
items.push({ kind: "server", status, serverIndex: i });
+ // 为失败的服务添加错误消息
+ if (status.status === "failed" && status.error) {
+ items.push({ kind: "error", error: status.error, serverName: status.name });
+ }
if (status.status === "ready") {
for (const tool of status.tools) {
items.push({ kind: "tool", name: tool, serverName: status.name });
@@ -99,11 +104,17 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement
if (statuses.length === 0) {
return (
-
- Manage MCP servers
- 0 servers
- No MCP servers configured.
- Add MCP servers to your settings to get started.
+
+
+
+ Manage MCP servers
+
+ 0 servers
+
+
+ No MCP servers configured.
+ Add MCP servers to your settings to get started.
+
Esc to close
);
@@ -149,6 +160,9 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement
if (item.kind === "server") {
return ;
}
+ if (item.kind === "error") {
+ return ;
+ }
return (
+
{selected ? "› " : " "}
{prefix} {name}
@@ -217,3 +231,20 @@ function CapabilityRow({
);
}
+
+function ErrorRow({ error }: { error: string }): React.ReactElement {
+ // 将错误消息按行分割,每行单独显示
+ const lines = error.split("\n").filter((line) => line.trim().length > 0);
+
+ return (
+
+ {lines.map((line, index) => (
+
+
+ {line}
+
+
+ ))}
+
+ );
+}
From 40025e495a6c19aa34939be78a4ace696a966ed7 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Thu, 14 May 2026 17:59:52 +0800
Subject: [PATCH 004/129] =?UTF-8?q?feat(mcp):=20=E5=AE=9E=E6=97=B6?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=92=8C=E5=B1=95=E7=A4=BA=20MCP=20=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 MCP 状态变更回调,支持 UI 实时刷新显示
- MCP 管理器中添加状态变更事件处理机制
- 调整 UI 组件以支持状态计数的高亮显示
- 移除 MCP 初始化失败的控制台错误输出,避免信息泄露
- 优化退出命令行提示颜色为灰色,提升可读性
---
src/mcp/mcp-manager.ts | 14 +++++++++++---
src/session.ts | 7 +++++++
src/ui/App.tsx | 6 +++++-
src/ui/McpStatusList.tsx | 21 +++++++++++++++------
4 files changed, 38 insertions(+), 10 deletions(-)
diff --git a/src/mcp/mcp-manager.ts b/src/mcp/mcp-manager.ts
index 8121dbc..5a9f553 100644
--- a/src/mcp/mcp-manager.ts
+++ b/src/mcp/mcp-manager.ts
@@ -45,6 +45,7 @@ export class McpManager {
private configuredServerNames: string[] = [];
private serverStatuses: McpServerStatus[] = [];
private onToolsListChanged: (() => void) | null = null;
+ private onStatusChanged: (() => void) | null = null;
prepare(servers?: Record): void {
if (!servers || Object.keys(servers).length === 0) return;
@@ -172,7 +173,8 @@ export class McpManager {
if (this.disposed) break;
client?.disconnect();
const message = err instanceof Error ? err.message : String(err);
- process.stderr.write(`[deepcode] MCP server "${name}" failed to initialize: ${message}\n`);
+ // 不在控制台输出错误日志,避免暴露敏感信息
+ // process.stderr.write(`[deepcode] MCP server "${name}" failed to initialize: ${message}\n`);
this.setStatus({
name,
status: "failed",
@@ -376,13 +378,19 @@ export class McpManager {
this.onToolsListChanged = handler;
}
+ setOnStatusChanged(handler: () => void): void {
+ this.onStatusChanged = handler;
+ }
+
private setStatus(status: McpServerStatus): void {
if (this.disposed) return;
const index = this.serverStatuses.findIndex((s) => s.name === status.name);
if (index === -1) {
this.serverStatuses.push(status);
- return;
+ } else {
+ this.serverStatuses[index] = status;
}
- this.serverStatuses[index] = status;
+ // 触发状态变更回调
+ this.onStatusChanged?.();
}
}
diff --git a/src/session.ts b/src/session.ts
index 161ba98..3f264b4 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -166,6 +166,7 @@ type SessionManagerOptions = {
onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void;
onSessionEntryUpdated?: (entry: SessionEntry) => void;
onLlmStreamProgress?: (progress: LlmStreamProgress) => void;
+ onMcpStatusChanged?: () => void;
};
export type LlmStreamProgress = {
@@ -184,6 +185,7 @@ export class SessionManager {
private readonly onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void;
private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void;
private readonly onLlmStreamProgress?: (progress: LlmStreamProgress) => void;
+ private readonly onMcpStatusChanged?: () => void;
private activeSessionId: string | null = null;
private activePromptController: AbortController | null = null;
private readonly sessionControllers = new Map();
@@ -198,6 +200,7 @@ export class SessionManager {
this.onAssistantMessage = options.onAssistantMessage;
this.onSessionEntryUpdated = options.onSessionEntryUpdated;
this.onLlmStreamProgress = options.onLlmStreamProgress;
+ this.onMcpStatusChanged = options.onMcpStatusChanged;
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager);
this.mcpManager.prepare(this.getResolvedSettings().mcpServers);
}
@@ -206,6 +209,10 @@ export class SessionManager {
this.mcpManager.setOnToolsListChanged(() => {
this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
});
+ // 设置状态变更回调,通知 UI 更新
+ this.mcpManager.setOnStatusChanged(() => {
+ this.onMcpStatusChanged?.();
+ });
await this.mcpManager.initialize(servers);
this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
}
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index ffe89e4..709df67 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -94,6 +94,10 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
}
setStreamProgress(progress);
},
+ onMcpStatusChanged: () => {
+ // 当 MCP 状态变更时,如果当前正在查看 MCP 状态页面,则更新显示
+ setMcpStatuses(sessionManager.getMcpStatus());
+ },
});
}, [projectRoot]);
@@ -156,7 +160,7 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
const resolved = resolveCurrentSettings(projectRoot);
const summary = buildExitSummaryText({ session, messages: allMessages, model: resolved.model });
process.stdout.write("\n");
- process.stdout.write(chalk.green("> /exit "));
+ process.stdout.write(chalk.rgb(128, 128, 128)("> /exit "));
process.stdout.write("\n\n");
process.stdout.write(summary);
process.stdout.write("\n\n");
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index d3e9048..455c586 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -131,14 +131,23 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement
>
{/* Header row */}
-
-
- MCP Server Status
-
+
- {" "}
- ({readyCount} ready, {startingCount} starting, {failedCount} failed)
+ Manage MCP servers
+
+ (
+
+ {readyCount} ready,
+
+
+ {startingCount} starting,
+
+
+ {failedCount} failed
+
+ )
+
{/* Items list */}
Date: Thu, 14 May 2026 23:26:21 +0800
Subject: [PATCH 005/129] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96=20MC?=
=?UTF-8?q?P=20=E7=8A=B6=E6=80=81=E5=88=97=E8=A1=A8=E8=A7=86=E5=9B=BE?=
=?UTF-8?q?=E5=8F=8A=E7=95=8C=E9=9D=A2=E7=BB=86=E8=8A=82=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将 MCP 状态列表拆分为服务器列表和服务器详情两种视图
- 支持通过方向键分页和滚动,确保选中项始终可见
- 服务器详情视图新增工具、提示和资源的详细列表展示及导航支持
- 为启动状态服务器添加动态加载动画,提升交互体验
- 错误信息使用带红色边框的样式包裹,更加醒目
- 统一界面中选中项前缀符号由 "› " 改为 "> "
- 修正 DropdownMenu 和 SlashCommandMenu 的注释与前缀符号
- 调整 SessionList 中选中标识符,保持样式一致
- 优化行宽计算逻辑,提升标签列自适应能力
- 改善布局间距和边框样式,增强视觉层次感
---
src/ui/AskUserQuestionPrompt.tsx | 2 +-
src/ui/DropdownMenu.tsx | 4 +-
src/ui/McpStatusList.tsx | 482 ++++++++++++++++++++++++-------
src/ui/PromptInput.tsx | 6 +-
src/ui/SessionList.tsx | 2 +-
src/ui/SlashCommandMenu.tsx | 6 +-
6 files changed, 380 insertions(+), 122 deletions(-)
diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/AskUserQuestionPrompt.tsx
index 952f9cf..7c76ae3 100644
--- a/src/ui/AskUserQuestionPrompt.tsx
+++ b/src/ui/AskUserQuestionPrompt.tsx
@@ -184,7 +184,7 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props):
return (
- {isCursor ? "› " : " "}
+ {isCursor ? "> " : " "}
{marker} {option.label}
{option.isOther ? (
diff --git a/src/ui/DropdownMenu.tsx b/src/ui/DropdownMenu.tsx
index 3a963a5..d651720 100644
--- a/src/ui/DropdownMenu.tsx
+++ b/src/ui/DropdownMenu.tsx
@@ -82,7 +82,7 @@ const DropdownMenu = React.memo(function DropdownMenu({
// 计算每个 item 实际需要的最大宽度
const maxContentWidth = Math.max(
...visibleItems.map((item) => {
- let width = 2; // prefix "› " or " "
+ let width = 2; // prefix "> " or " "
if (item.selected !== undefined) {
width += 2; // "● " or "○ "
}
@@ -152,7 +152,7 @@ const DropdownMenu = React.memo(function DropdownMenu({
- {isActive ? "› " : " "}
+ {isActive ? "> " : " "}
{item.selected !== undefined ? (item.selected ? "●" : "○") : null} {item.label}
{item.statusIndicator ? (
{item.statusIndicator.symbol}
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index 455c586..e448b5c 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useMemo } from "react";
+import React, { useState, useMemo, useCallback, useEffect } from "react";
import { Box, Text, useInput, useWindowSize } from "ink";
import type { McpServerStatus } from "../mcp/mcp-manager";
@@ -7,94 +7,160 @@ type Props = {
onCancel: () => void;
};
-type FlatItem =
- | { kind: "server"; status: McpServerStatus; serverIndex: number }
- | { kind: "error"; error: string; serverName: string }
- | { kind: "tool"; name: string; serverName: string }
- | { kind: "prompt"; name: string; serverName: string }
- | { kind: "resource"; name: string; serverName: string };
-
-function buildFlatItems(statuses: McpServerStatus[]): FlatItem[] {
- const items: FlatItem[] = [];
- for (let i = 0; i < statuses.length; i++) {
- const status = statuses[i];
- items.push({ kind: "server", status, serverIndex: i });
- // 为失败的服务添加错误消息
- if (status.status === "failed" && status.error) {
- items.push({ kind: "error", error: status.error, serverName: status.name });
- }
- if (status.status === "ready") {
- for (const tool of status.tools) {
- items.push({ kind: "tool", name: tool, serverName: status.name });
- }
- for (const prompt of status.prompts) {
- items.push({ kind: "prompt", name: prompt, serverName: status.name });
- }
- for (const resource of status.resources) {
- items.push({ kind: "resource", name: resource, serverName: status.name });
- }
+export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement {
+ const { columns, rows } = useWindowSize();
+
+ // 视图模式:server-list(服务器列表) 或 server-detail(服务器详情)
+ const [viewMode, setViewMode] = useState<"server-list" | "server-detail">("server-list");
+ // 选中的服务器索引
+ const [selectedServerIndex, setSelectedServerIndex] = useState(0);
+
+ // 返回服务器列表
+ const goBack = useCallback(() => {
+ setViewMode("server-list");
+ }, []);
+
+ // 进入服务器详情
+ const enterDetail = useCallback(() => {
+ const server = statuses[selectedServerIndex];
+ if (server && server.status === "ready") {
+ setViewMode("server-detail");
}
+ }, [statuses, selectedServerIndex]);
+
+ if (statuses.length === 0) {
+ return (
+
+
+
+ Manage MCP servers
+
+ 0 servers
+
+
+ No MCP servers configured.
+ Add MCP servers to your settings to get started.
+
+ Esc to close
+
+ );
}
- return items;
-}
-export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement {
- const [index, setIndex] = useState(0);
- const { columns, rows } = useWindowSize();
+ if (viewMode === "server-detail") {
+ return (
+
+ );
+ }
- const flatItems = useMemo(() => buildFlatItems(statuses), [statuses]);
+ return (
+
+ );
+}
+
+// ==================== 服务器列表视图 ====================
+function ServerListView({
+ statuses,
+ selectedIndex,
+ onSelect,
+ onEnter,
+ onCancel,
+ rows,
+ columns,
+}: {
+ statuses: McpServerStatus[];
+ selectedIndex: number;
+ onSelect: (index: number) => void;
+ onEnter: () => void;
+ onCancel: () => void;
+ rows: number;
+ columns: number;
+}): React.ReactElement {
+ const [scrollOffset, setScrollOffset] = useState(0);
+ const serverCount = statuses.length;
const maxVisible = useMemo(() => {
- const reservedLines = 8;
- const linesPerItem = 2;
+ const reservedLines = 8; // header + footer + borders
const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines);
- return Math.max(1, Math.floor(availableLines / linesPerItem));
+ // 每个服务器占用 1 行(标题)+ 1 行(错误信息或统计)+ 1 行(间隔)
+ return Math.max(1, Math.floor(availableLines / 3));
}, [rows]);
+ // 计算标签列宽度:找到最长的服务器名称,加上前缀和图标
+ const labelColumnWidth = useMemo(() => {
+ if (serverCount === 0) return 0;
+ const longestName = Math.max(...statuses.map((s) => s.name.length));
+ const contentWidth = longestName + 5; // +2 for prefix "> " or " ", +3 for icon "✓ "
+ const maxAllowed = Math.max(15, Math.floor((columns - 6) * 0.4)); // 容器40%宽度,至少15列
+ return Math.min(contentWidth, maxAllowed);
+ }, [statuses, serverCount, columns]);
+
const safeIndex = useMemo(() => {
- if (flatItems.length === 0) return 0;
- return Math.max(0, Math.min(index, flatItems.length - 1));
- }, [index, flatItems.length]);
+ if (serverCount === 0) return 0;
+ return Math.max(0, Math.min(selectedIndex, serverCount - 1));
+ }, [selectedIndex, serverCount]);
- const scrollOffset = useMemo(() => {
- if (safeIndex < maxVisible) return 0;
- return safeIndex - maxVisible + 1;
- }, [safeIndex, maxVisible]);
+ // 自动滚动确保选中项可见
+ React.useEffect(() => {
+ if (safeIndex < scrollOffset) {
+ setScrollOffset(safeIndex);
+ } else if (safeIndex >= scrollOffset + maxVisible) {
+ setScrollOffset(safeIndex - maxVisible + 1);
+ }
+ }, [safeIndex, scrollOffset, maxVisible]);
- const visibleItems = useMemo(() => {
- return flatItems.slice(scrollOffset, scrollOffset + maxVisible);
- }, [flatItems, scrollOffset, maxVisible]);
+ const visibleServers = useMemo(() => {
+ return statuses.slice(scrollOffset, scrollOffset + maxVisible);
+ }, [statuses, scrollOffset, maxVisible]);
useInput((input, key) => {
if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
onCancel();
return;
}
- if (flatItems.length === 0) {
+ if (serverCount === 0) {
return;
}
if (key.upArrow) {
- setIndex((i) => Math.max(0, i - 1));
+ onSelect(Math.max(0, selectedIndex - 1));
return;
}
if (key.downArrow) {
- setIndex((i) => Math.min(flatItems.length - 1, i + 1));
+ onSelect(Math.min(serverCount - 1, selectedIndex + 1));
return;
}
if (key.pageUp) {
- setIndex((i) => Math.max(0, i - maxVisible));
+ onSelect(Math.max(0, selectedIndex - maxVisible));
return;
}
if (key.pageDown) {
- setIndex((i) => Math.min(flatItems.length - 1, i + maxVisible));
+ onSelect(Math.min(serverCount - 1, selectedIndex + maxVisible));
return;
}
if (key.home) {
- setIndex(0);
+ onSelect(0);
return;
}
if (key.end) {
- setIndex(flatItems.length - 1);
+ onSelect(serverCount - 1);
+ }
+ // Enter 键进入详情
+ if (key.return) {
+ onEnter();
+ return;
}
});
@@ -102,24 +168,6 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement
const startingCount = statuses.filter((s) => s.status === "starting").length;
const failedCount = statuses.filter((s) => s.status === "failed").length;
- if (statuses.length === 0) {
- return (
-
-
-
- Manage MCP servers
-
- 0 servers
-
-
- No MCP servers configured.
- Add MCP servers to your settings to get started.
-
- Esc to close
-
- );
- }
-
return (
- {visibleItems.map((item, i) => {
+ {visibleServers.map((status, i) => {
const actualIndex = scrollOffset + i;
const isSelected = actualIndex === safeIndex;
- if (item.kind === "server") {
- return ;
- }
- if (item.kind === "error") {
- return ;
- }
return (
-
);
})}
- {scrollOffset > 0 || scrollOffset + maxVisible < flatItems.length ? (
+ {scrollOffset > 0 || scrollOffset + maxVisible < serverCount ? (
- {scrollOffset > 0 ? … {scrollOffset} items above. : null}
- {scrollOffset + maxVisible < flatItems.length ? (
- … {flatItems.length - scrollOffset - maxVisible} items below.
+ {scrollOffset > 0 ? … {scrollOffset} servers above. : null}
+ {scrollOffset + maxVisible < serverCount ? (
+ … {serverCount - scrollOffset - maxVisible} servers below.
) : null}
) : null}
{/* Footer */}
-
- ↑/↓ navigate · PgUp/PgDn page · Esc cancel
+
+ ↑/↓ navigate · Enter view details · Esc cancel
);
}
-function ServerRow({ status, selected }: { status: McpServerStatus; selected: boolean }): React.ReactElement {
- const icon = status.status === "ready" ? "✔" : status.status === "failed" ? "✖" : "●";
+function ServerRow({
+ status,
+ selected,
+ labelColumnWidth,
+}: {
+ status: McpServerStatus;
+ selected: boolean;
+ labelColumnWidth: number;
+}): React.ReactElement {
+ const icon = status.status === "ready" ? "✓" : status.status === "failed" ? "✗" : "●";
const color = status.status === "ready" ? "green" : status.status === "failed" ? "red" : "yellow";
+
+ // 加载动画:循环显示 (空) → . → .. → ... → (空) → ...
+ const [dots, setDots] = React.useState(0);
+ React.useEffect(() => {
+ if (status.status !== "starting") return;
+ const interval = setInterval(() => {
+ setDots((d) => (d + 1) % 4); // 0 → 1 → 2 → 3 → 0 ...
+ }, 500);
+ return () => clearInterval(interval);
+ }, [status.status]);
+
const detail =
status.status === "ready"
? `Ready (${status.toolCount} tools, ${status.promptCount} prompts, ${status.resourceCount} resources)`
: status.status === "failed"
- ? `Failed (${status.error ?? "unknown error"})`
- : "Starting...";
+ ? `Failed`
+ : "Starting" + (dots > 0 ? ".".repeat(dots) : " "); // 动态显示 (空) / . / .. / ...
return (
-
- {selected ? "› " : " "}
-
- {icon}
- {status.name}
- — {detail}
-
+
+ {/* Server row */}
+
+
+
+ {selected ? "> " : " "}
+ {icon}
+ {status.name}
+
+
+
+ {detail}
+
+
+
+ {/* Error message for failed servers */}
+ {status.status === "failed" && status.error ? : null}
);
}
-function CapabilityRow({
- kind,
- name,
- selected,
+// ==================== 服务器详情视图 ====================
+function ServerDetailView({
+ server,
+ onBack,
+ onCancel,
+ rows,
+ columns,
}: {
- kind: "tool" | "prompt" | "resource";
- name: string;
- selected: boolean;
+ server: McpServerStatus;
+ onBack: () => void;
+ onCancel: () => void;
+ rows: number;
+ columns: number;
}): React.ReactElement {
- const prefix = kind === "tool" ? "🔧" : kind === "prompt" ? "📝" : "📦";
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ // 合并所有 items(tools, prompts, resources)
+ const allItems = useMemo(() => {
+ const items: { type: string; name: string }[] = [];
+ server.tools.forEach((tool) => items.push({ type: "tool", name: tool }));
+ server.prompts.forEach((prompt) => items.push({ type: "prompt", name: prompt }));
+ server.resources.forEach((resource) => items.push({ type: "resource", name: resource }));
+ return items;
+ }, [server]);
+
+ const totalItems = allItems.length;
+
+ const maxVisible = useMemo(() => {
+ const reservedLines = 10; // header + title + stats + footer + borders
+ const availableLines = Math.max(0, Math.min(rows, 28) - reservedLines);
+ return Math.max(1, availableLines);
+ }, [rows]);
+
+ // 使用 ref 跟踪 visibleStart,避免循环依赖
+ const visibleStartRef = React.useRef(0);
+
+ // 计算可见窗口起始位置:当 activeIndex 超出可见区域时才滚动(类似终端光标行为)
+ const visibleStart = useMemo(() => {
+ if (totalItems === 0) return 0;
+
+ const currentStart = visibleStartRef.current;
+ let newStart = currentStart;
+
+ // 如果 activeIndex 在当前可见窗口之前,滚动到 activeIndex
+ if (activeIndex < currentStart) {
+ newStart = activeIndex;
+ }
+ // 如果 activeIndex 在当前可见窗口之后,滚动到 activeIndex
+ else if (activeIndex >= currentStart + maxVisible) {
+ newStart = activeIndex - maxVisible + 1;
+ }
+
+ console.log("maxVisible:", maxVisible);
+
+ // 限制在合法范围内
+ newStart = Math.max(0, Math.min(newStart, Math.max(0, totalItems - maxVisible)));
+
+ // 更新 ref
+ visibleStartRef.current = newStart;
+
+ return newStart;
+ }, [activeIndex, maxVisible, totalItems]);
+
+ const visibleItems = allItems.slice(visibleStart, visibleStart + maxVisible);
+
+ useInput((input, key) => {
+ if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
+ onCancel();
+ return;
+ }
+ if (key.escape) {
+ onBack();
+ return;
+ }
+ // Space 或 Enter 键返回一级菜单
+ if (input === " " || key.return) {
+ onBack();
+ return;
+ }
+ if (key.upArrow) {
+ setActiveIndex((prev) => Math.max(0, prev - 1));
+ return;
+ }
+ if (key.downArrow) {
+ setActiveIndex((prev) => Math.min(totalItems - 1, prev + 1));
+ return;
+ }
+ if (key.pageUp) {
+ setActiveIndex((prev) => Math.max(0, prev - maxVisible));
+ return;
+ }
+ if (key.pageDown) {
+ setActiveIndex((prev) => Math.min(totalItems - 1, prev + maxVisible));
+ return;
+ }
+ if (key.home) {
+ setActiveIndex(0);
+ return;
+ }
+ if (key.end) {
+ setActiveIndex(totalItems - 1);
+ }
+ });
+
+ const icon = "✓";
+ const color = "green";
+
+ return (
+
+
+ {/* Header row */}
+
+ {icon}
+
+ {server.name}
+
+ — Details
+
+ {activeIndex + 1}/{totalItems}
+
+
+ {/* Server info */}
+
+
+ {server.toolCount} tools, {server.promptCount} prompts, {server.resourceCount} resources
+
+
+ {/* Items list */}
+
+ {visibleStart > 0 ? (
+
+ ▲
+
+ ) : (
+
+ )}
+
+ {visibleItems.length === 0 ? (
+
+ No items available
+
+ ) : (
+ visibleItems.map((item, idx) => {
+ const actualIndex = visibleStart + idx;
+ const isSelected = actualIndex === activeIndex;
+ return ;
+ })
+ )}
+
+ {visibleStart > 0 || visibleStart + maxVisible < totalItems ? (
+
+ {totalItems - visibleStart - maxVisible > 0 ? ▼ : }
+ {visibleStart > 0 ? … {visibleStart} items above. : null}
+ {totalItems - visibleStart - maxVisible > 0 ? (
+ … {totalItems - visibleStart - maxVisible} items below.
+ ) : null}
+
+ ) : null}
+
+ {/* Footer */}
+
+ ↑/↓ scroll · Space/Enter back · Esc close
+
+
+
+ );
+}
+
+function ItemRow({ item, selected }: { item: { type: string; name: string }; selected: boolean }): React.ReactElement {
+ const icon = item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦";
+
return (
-
- {selected ? "› " : " "}
-
- {prefix} {name}
+
+ {icon}
+
+ {item.name}
);
@@ -246,7 +496,15 @@ function ErrorRow({ error }: { error: string }): React.ReactElement {
const lines = error.split("\n").filter((line) => line.trim().length > 0);
return (
-
+
{lines.map((line, index) => (
diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx
index 6a11431..6c0f8ea 100644
--- a/src/ui/PromptInput.tsx
+++ b/src/ui/PromptInput.tsx
@@ -722,7 +722,7 @@ export const PromptInput = React.memo(function PromptInput({
({
key: skill.path || skill.name,
@@ -742,8 +742,8 @@ export const PromptInput = React.memo(function PromptInput({
title={modelDropdownStep === "model" ? "Select Model" : "Select Thinking Mode"}
helpText={
modelDropdownStep === "model"
- ? "space/enter select model · esc to cancel"
- : "space/enter apply · esc to cancel"
+ ? "Space/Enter select model · Esc to cancel"
+ : "Space/Enter apply · Esc to cancel"
}
items={modelDropdownItems.map((item) => ({
key: item.label,
diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx
index 67f2e10..fdbd1fe 100644
--- a/src/ui/SessionList.tsx
+++ b/src/ui/SessionList.tsx
@@ -126,7 +126,7 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
return (
- {actualIndex === safeIndex ? "› " : " "}
+ {actualIndex === safeIndex ? "> " : " "}
diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx
index 9b79293..436be83 100644
--- a/src/ui/SlashCommandMenu.tsx
+++ b/src/ui/SlashCommandMenu.tsx
@@ -16,13 +16,13 @@ const SlashCommandMenu = React.memo(function SlashCommandMenu({
maxVisible = 6,
width,
}: SlashCommandMenuProps): React.ReactElement | null {
- // 计算标签列最佳宽度:包含前缀"› "或" "(2字符),不超过容器一半(扣除gap)
+ // 计算标签列最佳宽度:包含前缀"> "或" "(2字符),不超过容器一半(扣除gap)
const labelColumnWidth = React.useMemo(() => {
if (items.length === 0) {
return 0;
}
const longestLabel = Math.max(...items.map((s) => s.label.length));
- const contentWidth = longestLabel + 2; // +2 for prefix "› " or " "
+ const contentWidth = longestLabel + 2; // +2 for prefix "> " or " "
const maxAllowed = Math.max(10, (width - 2) >> 1); // 容器50%宽度(减去gap),至少保留10列
return Math.min(contentWidth, maxAllowed);
}, [items, width]);
@@ -51,7 +51,7 @@ const SlashCommandMenu = React.memo(function SlashCommandMenu({
- {actualIndex === activeIndex ? "› " : " "}
+ {actualIndex === activeIndex ? "> " : " "}
{formatSlashCommandLabel(item)}
From b858c8755978480321f79dd8068376c7c45408d4 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Thu, 14 May 2026 23:58:18 +0800
Subject: [PATCH 006/129] chore: update McpStatusList
---
src/ui/McpStatusList.tsx | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index e448b5c..e7912d9 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -321,7 +321,7 @@ function ServerDetailView({
const maxVisible = useMemo(() => {
const reservedLines = 10; // header + title + stats + footer + borders
- const availableLines = Math.max(0, Math.min(rows, 28) - reservedLines);
+ const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines);
return Math.max(1, availableLines);
}, [rows]);
@@ -344,8 +344,6 @@ function ServerDetailView({
newStart = activeIndex - maxVisible + 1;
}
- console.log("maxVisible:", maxVisible);
-
// 限制在合法范围内
newStart = Math.max(0, Math.min(newStart, Math.max(0, totalItems - maxVisible)));
@@ -412,7 +410,7 @@ function ServerDetailView({
{/* Header row */}
{icon}
-
+
{server.name}
— Details
@@ -422,7 +420,7 @@ function ServerDetailView({
{/* Server info */}
-
+
{server.toolCount} tools, {server.promptCount} prompts, {server.resourceCount} resources
@@ -455,7 +453,7 @@ function ServerDetailView({
visibleItems.map((item, idx) => {
const actualIndex = visibleStart + idx;
const isSelected = actualIndex === activeIndex;
- return ;
+ return ;
})
)}
@@ -482,7 +480,7 @@ function ItemRow({ item, selected }: { item: { type: string; name: string }; sel
const icon = item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦";
return (
-
+
{icon}
{item.name}
From f3554dfedc321c67fe676ce593f98c38e816576b Mon Sep 17 00:00:00 2001
From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com>
Date: Fri, 15 May 2026 10:30:20 +0800
Subject: [PATCH 007/129] fix: add Kitty keyboard protocol Shift+Enter
sequences for broader terminal compatibility
SHIFT_RETURN_SEQUENCES now covers both xterm modifyOtherKeys (Shift=2)
and Kitty keyboard protocol (Shift=1) encodings. Also clear input when
key.return is true to prevent escape sequence artifacts from leaking
into the text buffer.
---
src/tests/promptInputKeys.test.ts | 2 +-
src/ui/prompt/useTerminalInput.ts | 12 ++++++++++--
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts
index 69d2075..372dfc7 100644
--- a/src/tests/promptInputKeys.test.ts
+++ b/src/tests/promptInputKeys.test.ts
@@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => {
test("parseTerminalInput recognizes shifted return sequences", () => {
const { input, key } = parseTerminalInput("\u001B\r");
- assert.equal(input, "\r");
+ assert.equal(input, "");
assert.equal(key.return, true);
assert.equal(key.shift, true);
assert.equal(key.meta, false);
diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts
index 8013ff6..ea368b5 100644
--- a/src/ui/prompt/useTerminalInput.ts
+++ b/src/ui/prompt/useTerminalInput.ts
@@ -26,7 +26,15 @@ const BACKSPACE_BYTES = new Set(["\u007F", "\b"]);
const FORWARD_DELETE_SEQUENCES = new Set(["\u001B[3~", "\u001B[P"]);
const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]);
const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]);
-const SHIFT_RETURN_SEQUENCES = new Set(["\u001B\r", "\u001B[13;2u", "\u001B[13;2~", "\u001B[27;2;13~"]);
+const SHIFT_RETURN_SEQUENCES = new Set([
+ "\u001B\r",
+ "\u001B[13;2u", // xterm modifyOtherKeys: keycode=13 (Enter), modifier=2 (Shift)
+ "\u001B[13;1u", // Kitty keyboard protocol: keycode=13, modifier=1 (Shift)
+ "\u001B[13;2~",
+ "\u001B[13;1~", // tmux / alternate terminals may use ~ terminator
+ "\u001B[27;2;13~",
+ "\u001B[27;1;13~", // extended format, Kitty encoding
+]);
const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]);
const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]);
const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]);
@@ -162,7 +170,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
key.shift = true;
}
- if (key.tab || key.backspace || key.delete) {
+ if (key.tab || key.backspace || key.delete || key.return) {
input = "";
}
From efde004299e5892b132412845d5a3f08e13242d2 Mon Sep 17 00:00:00 2001
From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com>
Date: Fri, 15 May 2026 11:25:43 +0800
Subject: [PATCH 008/129] fix: use dynamic modifier parsing for Shift+Enter
recognition
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace exact string matching in SHIFT_RETURN_SEQUENCES with CSI
parameter parsing that checks modifier bits. Windows Terminal sends
ESC[13;130u (modifier=128+2) where 128 is a terminal-specific flag —
the old code only matched modifier=2 exactly.
Also enable Kitty progressive enhancement (ESC[>1u) alongside xterm
modifyOtherKeys, since Windows Terminal requires the Kitty protocol
to report modified keys.
---
src/tests/promptInputKeys.test.ts | 4 +--
src/ui/prompt/cursor.ts | 6 ++--
src/ui/prompt/useTerminalInput.ts | 52 +++++++++++++++++++++++++++----
3 files changed, 52 insertions(+), 10 deletions(-)
diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts
index 372dfc7..8952a3d 100644
--- a/src/tests/promptInputKeys.test.ts
+++ b/src/tests/promptInputKeys.test.ts
@@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => {
});
test("terminal extended key helpers request and restore modifyOtherKeys mode", () => {
- assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m");
- assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m");
+ assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u");
+ assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[ {
diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts
index 2668470..5eff1de 100644
--- a/src/ui/prompt/cursor.ts
+++ b/src/ui/prompt/cursor.ts
@@ -40,12 +40,14 @@ function disableTerminalFocusReporting(): string {
return "\u001B[?1004l";
}
+// xterm modifyOtherKeys + Kitty progressive enhancement.
+// Both are needed: some terminals (incl. Windows Terminal) only respond to Kitty.
export function enableTerminalExtendedKeys(): string {
- return "\u001B[>4;1m";
+ return "\u001B[>4;1m\u001B[>1u";
}
export function disableTerminalExtendedKeys(): string {
- return "\u001B[>4;0m";
+ return "\u001B[>4;0m\u001B[1u) sends plain Enter as ESC[13u
+// or ESC[13;NUMBERu with extra flags; xterm sends ESC[13;2u for Shift.
+const CSI_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
+const CSI_EXTENDED_RETURN_RE = /^\u001B\[27;(\d+);13~$/;
+
+function isReturn(raw: string): boolean {
+ if (raw === "\r") return true;
+ if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
+ if (META_RETURN_SEQUENCES.has(raw)) return true;
+ return CSI_RETURN_RE.test(raw) || CSI_EXTENDED_RETURN_RE.test(raw);
+}
const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]);
const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]);
const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]);
@@ -121,10 +161,10 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
end: END_SEQUENCES.has(raw),
pageDown: raw === "\u001B[6~",
pageUp: raw === "\u001B[5~",
- return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw),
+ return: isReturn(raw),
escape: raw === "\u001B",
ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw),
- shift: SHIFT_RETURN_SEQUENCES.has(raw),
+ shift: isShiftReturn(raw),
tab: raw === "\t" || raw === "\u001B[Z",
backspace: BACKSPACE_BYTES.has(raw),
delete: FORWARD_DELETE_SEQUENCES.has(raw),
From 35b686d55fa1a6fcd91f3845d49b17d5975fcd61 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 13:34:27 +0800
Subject: [PATCH 009/129] =?UTF-8?q?test(session):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BB=A5=E5=8C=85=E6=8B=AC=E6=8F=90=E7=A4=BA?=
=?UTF-8?q?=E5=92=8C=E8=B5=84=E6=BA=90=E8=AE=A1=E6=95=B0=E4=BF=A1=E6=81=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 getMcpStatus 的断言中添加 promptCount、prompts、resourceCount 和 resources 字段
- 确保各状态对象包含完整的提示和资源数组信息
- 维护现有工具和连接状态的正确性
- 扩展测试覆盖,支持提示和资源相关的状态字段验证
---
src/tests/session.test.ts | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index 2ac6c29..60147ea 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -410,7 +410,17 @@ rl.on("line", (line) => {
const initPromise = manager.initMcpServers({ smoke: { command: process.execPath, args: [serverPath] } });
assert.deepEqual(manager.getMcpStatus(), [
- { name: "smoke", status: "starting", connected: false, toolCount: 0, tools: [] },
+ {
+ name: "smoke",
+ status: "starting",
+ connected: false,
+ toolCount: 0,
+ tools: [],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
+ },
]);
await initPromise;
@@ -422,6 +432,10 @@ rl.on("line", (line) => {
connected: true,
toolCount: 2,
tools: ["mcp__smoke__echo", "mcp__smoke__count"],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
},
]);
const mcpManager = (manager as any).mcpManager;
@@ -457,7 +471,17 @@ test("SessionManager reports configured MCP servers as starting before initializ
});
assert.deepEqual(manager.getMcpStatus(), [
- { name: "playwright", status: "starting", connected: false, toolCount: 0, tools: [] },
+ {
+ name: "playwright",
+ status: "starting",
+ connected: false,
+ toolCount: 0,
+ tools: [],
+ promptCount: 0,
+ prompts: [],
+ resourceCount: 0,
+ resources: [],
+ },
]);
});
From 5a996281dce9cb2ef6371374669aea304bdbec7c Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 13:37:13 +0800
Subject: [PATCH 010/129] =?UTF-8?q?refactor(ui):=20=E7=A7=BB=E9=99=A4McpSt?=
=?UTF-8?q?atusList=E4=B8=AD=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84React?=
=?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=92=8C=E6=96=87=E6=9C=AC=E6=98=BE=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 删除了未使用的useEffect导入,简化依赖引入
- 移除了界面中显示的当前索引及总数文本,减少冗余信息显示
- 优化了McpStatusList组件代码的可读性和整洁度
---
src/ui/McpStatusList.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index e7912d9..1f49bd6 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useMemo, useCallback, useEffect } from "react";
+import React, { useState, useMemo, useCallback } from "react";
import { Box, Text, useInput, useWindowSize } from "ink";
import type { McpServerStatus } from "../mcp/mcp-manager";
@@ -414,9 +414,6 @@ function ServerDetailView({
{server.name}
— Details
-
- {activeIndex + 1}/{totalItems}
-
{/* Server info */}
From bee349bb8ca700eefe96615cba4b51352bdc907b Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 14:09:28 +0800
Subject: [PATCH 011/129] =?UTF-8?q?fix(mcp-client):=20=E5=A2=9E=E5=BC=BA?=
=?UTF-8?q?=E5=8D=8F=E8=AE=AE=E7=89=88=E6=9C=AC=E6=A0=A1=E9=AA=8C=E5=92=8C?=
=?UTF-8?q?JSON-RPC=E6=89=B9=E5=A4=84=E7=90=86=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在初始化时验证服务器返回的MCP协议版本,拒绝不支持的版本
- 支持处理JSON-RPC批量消息,逐条分发处理
- 抽离单条消息处理逻辑,统一处理通知和响应
- 防止通知处理器异常导致读取循环崩溃
fix(ui): 优化键盘交互提示与布局间距
- 移除Esc键作为取消操作,避免与Ctrl+C冲突
- 更新底部提示,明确Ctrl+C为关闭操作,Esc为返回操作
- 调整MessageView内容左边距,使间距更合理
---
src/mcp/mcp-client.ts | 70 ++++++++++++++++++++++++++++------------
src/ui/McpStatusList.tsx | 4 +--
src/ui/MessageView.tsx | 2 +-
3 files changed, 53 insertions(+), 23 deletions(-)
diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts
index a086c1d..f89e11d 100644
--- a/src/mcp/mcp-client.ts
+++ b/src/mcp/mcp-client.ts
@@ -178,7 +178,19 @@ export class McpClient {
},
timeoutMs
)
- .then(() => {
+ .then((result) => {
+ // Validate protocol version from server response (per MCP spec §4.2.1.2)
+ const initResult = result as { protocolVersion?: string } | undefined;
+ const serverVersion = initResult?.protocolVersion;
+ if (serverVersion && serverVersion !== "2025-03-26" && serverVersion !== "2024-11-05") {
+ reject(
+ new Error(
+ `Unsupported MCP protocol version "${serverVersion}" from server "${this.serverName}". ` +
+ `Client supports 2025-03-26 and 2024-11-05.`
+ )
+ );
+ return;
+ }
// Send initialized notification
this.sendNotification("notifications/initialized");
resolve();
@@ -302,36 +314,54 @@ export class McpClient {
try {
const parsed: unknown = JSON.parse(line);
- // Handle notifications (no id field — server-initiated)
- if (parsed && typeof parsed === "object" && !("id" in parsed)) {
- const notification = parsed as JsonRpcNotification;
- if (this.notificationHandler && typeof notification.method === "string") {
- try {
- this.notificationHandler(notification.method, notification.params);
- } catch {
- // Swallow handler errors to avoid crashing the reader loop
+ // Handle JSON-RPC batch (array of requests/notifications/responses)
+ // Per MCP 2025-03-26 §4.1.1.3: implementations MUST support receiving batches.
+ if (Array.isArray(parsed)) {
+ for (const item of parsed) {
+ if (item && typeof item === "object") {
+ this.handleSingleMessage(item);
}
}
return;
}
- // Handle responses to our requests
- const message = parsed as JsonRpcResponse;
- if (message.id !== undefined && this.pendingRequests.has(message.id)) {
- const pending = this.pendingRequests.get(message.id)!;
- this.pendingRequests.delete(message.id);
- clearTimeout(pending.timer);
- if (message.error) {
- pending.reject(this.withStderr(`MCP error: ${message.error.message}`));
- } else {
- pending.resolve(message.result);
- }
+ // Handle single message
+ if (parsed && typeof parsed === "object") {
+ this.handleSingleMessage(parsed);
}
} catch {
// Ignore unparseable lines
}
}
+ private handleSingleMessage(msg: object): void {
+ // Handle notifications (no id field — server-initiated)
+ if (!("id" in msg)) {
+ const notification = msg as unknown as JsonRpcNotification;
+ if (this.notificationHandler && typeof notification.method === "string") {
+ try {
+ this.notificationHandler(notification.method, notification.params);
+ } catch {
+ // Swallow handler errors to avoid crashing the reader loop
+ }
+ }
+ return;
+ }
+
+ // Handle responses to our requests
+ const message = msg as unknown as JsonRpcResponse;
+ if (message.id !== undefined && this.pendingRequests.has(message.id)) {
+ const pending = this.pendingRequests.get(message.id)!;
+ this.pendingRequests.delete(message.id);
+ clearTimeout(pending.timer);
+ if (message.error) {
+ pending.reject(this.withStderr(`MCP error: ${message.error.message}`));
+ } else {
+ pending.resolve(message.result);
+ }
+ }
+ }
+
private withNpxYesArg(command: string, args: string[]): string[] {
const executable = path
.basename(command)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index 1f49bd6..0f5f906 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -356,7 +356,7 @@ function ServerDetailView({
const visibleItems = allItems.slice(visibleStart, visibleStart + maxVisible);
useInput((input, key) => {
- if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
+ if (key.ctrl && (input === "c" || input === "C")) {
onCancel();
return;
}
@@ -466,7 +466,7 @@ function ServerDetailView({
{/* Footer */}
- ↑/↓ scroll · Space/Enter back · Esc close
+ ↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close
diff --git a/src/ui/MessageView.tsx b/src/ui/MessageView.tsx
index acdc645..c8793fc 100644
--- a/src/ui/MessageView.tsx
+++ b/src/ui/MessageView.tsx
@@ -47,7 +47,7 @@ export function MessageView({ message, collapsed, width = 80 }: Props): React.Re
return (
-
+
{content ? {renderMarkdown(content)} : null}
From c81937434575bee3eb734fcf28d9bec01bad4667 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 15:10:15 +0800
Subject: [PATCH 012/129] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=97=A0?=
=?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=97=B6=E6=8C=89Esc=E9=94=AE?=
=?UTF-8?q?=E9=80=80=E5=87=BA=E7=9A=84=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在McpStatusList组件中监听键盘输入事件
- 当服务器列表为空时,按Esc键或Ctrl+C触发退出操作
- 确保用户可通过快捷键安全退出无服务器状态界面
---
src/ui/McpStatusList.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index 0f5f906..bdb1854 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -28,6 +28,13 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement
}
}, [statuses, selectedServerIndex]);
+ // 当没有服务器时,监听 Esc 键退出
+ useInput((input, key) => {
+ if (statuses.length === 0 && (key.escape || (key.ctrl && (input === "c" || input === "C")))) {
+ onCancel();
+ }
+ });
+
if (statuses.length === 0) {
return (
From a094524e8fd4e939b93e71d56743b0fd1a50806d Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 16:37:15 +0800
Subject: [PATCH 013/129] fix(ui): update string
---
src/ui/McpStatusList.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ui/McpStatusList.tsx b/src/ui/McpStatusList.tsx
index bdb1854..a09039d 100644
--- a/src/ui/McpStatusList.tsx
+++ b/src/ui/McpStatusList.tsx
@@ -241,7 +241,7 @@ function ServerListView({
{/* Footer */}
- ↑/↓ navigate · Enter view details · Esc cancel
+ ↑/↓ navigate · Enter view details · Esc close
From 9a2188407e71a66a46b1937a6798c094ae2a486f Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 16:42:27 +0800
Subject: [PATCH 014/129] fix(ui): update string
---
src/ui/App.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index eea8091..44d5a70 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -160,7 +160,7 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
const resolved = resolveCurrentSettings(projectRoot);
const summary = buildExitSummaryText({ session, messages: allMessages, model: resolved.model });
process.stdout.write("\n");
- process.stdout.write(chalk.rgb(128, 128, 128)("> /exit "));
+ process.stdout.write(chalk.rgb(34, 154, 195)("> /exit "));
process.stdout.write("\n\n");
process.stdout.write(summary);
process.stdout.write("\n\n");
From d480486a650ace02305e45bb12bb0355a47e56be Mon Sep 17 00:00:00 2001
From: hcyang
Date: Fri, 15 May 2026 17:10:13 +0800
Subject: [PATCH 015/129] =?UTF-8?q?style(DropdownMenu):=20=E8=B0=83?=
=?UTF-8?q?=E6=95=B4=E5=86=85=E8=BE=B9=E8=B7=9D=E4=BC=98=E5=8C=96=E4=B8=8B?=
=?UTF-8?q?=E6=8B=89=E8=8F=9C=E5=8D=95=E5=B8=83=E5=B1=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为标题和底部帮助文本添加水平内边距
- 为可见条目列表及每个条目增加水平内边距
- 用包含容器包装可见条目,优化布局结构
- 保持原有选中指示和状态指示符渲染逻辑不变
---
src/ui/DropdownMenu.tsx | 48 ++++++++++++++++++++++-------------------
1 file changed, 26 insertions(+), 22 deletions(-)
diff --git a/src/ui/DropdownMenu.tsx b/src/ui/DropdownMenu.tsx
index 3a963a5..a3083aa 100644
--- a/src/ui/DropdownMenu.tsx
+++ b/src/ui/DropdownMenu.tsx
@@ -123,6 +123,7 @@ const DropdownMenu = React.memo(function DropdownMenu({
borderRight={false}
borderTop={false}
borderLeft={false}
+ paddingX={1}
>
{title}
@@ -138,31 +139,33 @@ const DropdownMenu = React.memo(function DropdownMenu({
) : null}
{/* Visible items */}
- {visibleItems.map((item, idx) => {
- const actualIndex = visibleStart + idx;
- const isActive = actualIndex === activeIndex;
+
+ {visibleItems.map((item, idx) => {
+ const actualIndex = visibleStart + idx;
+ const isActive = actualIndex === activeIndex;
- // Use custom renderer if provided
- if (renderItem) {
- return {renderItem(item, isActive)};
- }
+ // Use custom renderer if provided
+ if (renderItem) {
+ return {renderItem(item, isActive)};
+ }
- // Default rendering with selection indicator and optional features
- return (
-
-
-
- {isActive ? "› " : " "}
- {item.selected !== undefined ? (item.selected ? "●" : "○") : null} {item.label}
- {item.statusIndicator ? (
- {item.statusIndicator.symbol}
- ) : null}
-
+ // Default rendering with selection indicator and optional features
+ return (
+
+
+
+ {isActive ? "› " : " "}
+ {item.selected !== undefined ? (item.selected ? "●" : "○") : null} {item.label}
+ {item.statusIndicator ? (
+ {item.statusIndicator.symbol}
+ ) : null}
+
+
+ {item.description ? {`${item.description}`} : null}
- {item.description ? {`${item.description}`} : null}
-
- );
- })}
+ );
+ })}
+
{/* Scroll indicator - bottom */}
{visibleStart + visibleItems.length < items.length ? (
@@ -180,6 +183,7 @@ const DropdownMenu = React.memo(function DropdownMenu({
borderRight={false}
borderTop={true}
borderLeft={false}
+ paddingX={1}
>
{helpText}
From 309a887c652589c9467316d3412ccff15a4aec3f Mon Sep 17 00:00:00 2001
From: rock-solid-sites
Date: Fri, 15 May 2026 14:53:10 +0200
Subject: [PATCH 016/129] docs: restructure translations as _en.md siblings per
maintainer request; sync mcp.md with upstream/main
---
docs/configuration.md | 201 ++++++++++++++++++++-------------------
docs/configuration_en.md | 172 +++++++++++++++++++++++++++++++++
docs/mcp.md | 133 +++++++++++++-------------
docs/mcp_en.md | 200 ++++++++++++++++++++++++++++++++++++++
4 files changed, 537 insertions(+), 169 deletions(-)
create mode 100644 docs/configuration_en.md
create mode 100644 docs/mcp_en.md
diff --git a/docs/configuration.md b/docs/configuration.md
index 369f8e4..f8e52c3 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,71 +1,71 @@
-# Deep Code Configuration
+# Deep Code 配置
-## Configuration Hierarchy
+## 配置层级
-Configuration is applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones):
+配置按以下优先级顺序应用(数字较小的会被数字较大的覆盖):
-| Layer | Configuration Source | Description |
-| ----- | -------------------- | ---------------------------------------------- |
-| 1 | Defaults | Hardcoded defaults within the application |
-| 2 | User settings file | Global settings for the current user |
-| 3 | Project settings file| Project-specific settings |
-| 4 | Environment variables| System-wide or session-specific variables |
+| 层级 | 配置来源 | 说明 |
+| ---- | ------------ | ------------------------------------------- |
+| 1 | 默认值 | 应用程序内硬编码的默认值 |
+| 2 | 用户设置文件 | 当前用户的全局设置 |
+| 3 | 项目设置文件 | 项目特定的设置 |
+| 4 | 环境变量 | 系统范围或会话特定的变量 |
-## Settings File
+## 设置文件
-Deep Code uses the `settings.json` file for persistent configuration, supporting two storage locations:
+Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两个层级的存放位置:
-| File Type | Location | Scope |
-| ------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
-| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. |
-| Project settings file | `/.deepcode/settings.json` | Takes effect only when running Deep Code in that specific project. Project settings override user settings. |
+| 文件类型 | 位置 | 作用范围 |
+| ------------ | ---------------------------------- | ---------------------------------------------------- |
+| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 |
+| 项目设置文件 | `项目根目录/.deepcode/settings.json` | 仅在该特定项目中运行 Deep Code 时生效。项目设置会覆盖用户设置。 |
-### Available Settings in `settings.json`
+### `settings.json` 中的可用设置
-The following are all the top-level fields supported in `settings.json`, along with the sub-fields inside `env`:
+以下是 `settings.json` 支持的全部顶层字段,以及 `env` 内部支持的子字段:
-| Field | Type | Description |
-| ------------------ | ------- | --------------------------------------------------------------------------- |
-| `env` | object | Group of environment variables (see sub-field table below) |
-| `model` | string | Model name. Takes precedence over `env.MODEL` |
-| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)|
-| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) |
-| `debugLogEnabled` | boolean | Enable debug log output (default `false`) |
-| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) |
-| `webSearchTool` | string | Full path to a custom web search script |
-| `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) |
+| 字段 | 类型 | 说明 |
+| -------------------- | --------- | ------------------------------------------------------------------- |
+| `env` | object | 环境变量分组(见下方子字段表) |
+| `model` | string | 模型名称。优先级高于 `env.MODEL` |
+| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) |
+| `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) |
+| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) |
+| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) |
+| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 |
+| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) |
-#### `env` Sub-fields
+#### `env` 子字段
-| Field | Type | Description |
-| ----------------- | ------ | ---------------------------------------------------------------- |
-| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` |
-| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` |
-| `API_KEY` | string | API key |
-| `THINKING_ENABLED`| string | Enable thinking mode |
-| `REASONING_EFFORT`| string | Reasoning intensity |
-| `DEBUG_LOG_ENABLED`| string| Enable debug log output |
-| `` | string | Custom environment variable |
+| 字段 | 类型 | 说明 |
+| ---------- | ------ | ------------------------------------------------------------------ |
+| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` |
+| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` |
+| `API_KEY` | string | API 密钥 |
+| `THINKING_ENABLED` | string | 是否启用思考模式 |
+| `REASONING_EFFORT` | string | 推理强度 |
+| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 |
+| `<其他任意KEY>` | string | 自定义环境变量 |
-#### `thinkingEnabled` — Thinking Mode
+#### `thinkingEnabled` — 思考模式
-Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to disable.
+是否启用 DeepSeek 思考模式。设置为 `true` 启用、`false` 禁用。
-- For `deepseek-v4-pro` and `deepseek-v4-flash`, thinking mode is **enabled by default**.
-- For other models, thinking mode is **disabled by default**.
+- 对于 `deepseek-v4-pro` 和 `deepseek-v4-flash`,思考模式**默认启用**。
+- 对于其他模型,思考模式**默认关闭**。
-#### `reasoningEffort` — Reasoning Intensity
+#### `reasoningEffort` — 推理强度
-When thinking mode is enabled, controls the depth of the model’s reasoning:
+当思考模式启用时,控制模型思考的深度:
-| Value | Description |
-| ------ | --------------------------------------------------------- |
-| `max` | Maximum reasoning depth (default) |
-| `high` | Higher reasoning depth with relatively lower token usage |
+| 值 | 说明 |
+| ------ | --------------------------------- |
+| `max` | 最大推理深度(默认值) |
+| `high` | 较高推理深度,token消耗相对较小 |
-#### `notify` — Task Completion Notification
+#### `notify` — 任务完成通知
-Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message).
+设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。
```json
{
@@ -73,9 +73,9 @@ Set a full path to a shell script. When the AI assistant finishes a round of tas
}
```
-#### `webSearchTool` — Custom Web Search
+#### `webSearchTool` — 自定义联网搜索
-Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script:
+Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径:
```json
{
@@ -83,16 +83,16 @@ Deep Code has a built-in, free-to-use Web Search tool. If you need custom search
}
```
-The script receives a search query as an argument and outputs results in JSON format for the AI.
+脚本接收一个搜索查询参数,输出 JSON 格式的结果供 AI 使用。
-#### `mcpServers` — MCP Servers
+#### `mcpServers` — MCP 服务器
-Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object.
+MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。
```json
{
"mcpServers": {
- "": {
+ "<服务名>": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
@@ -103,70 +103,71 @@ Configuration for MCP (Model Context Protocol) servers. The value is a key-value
}
```
-| McpServerConfig field | Type | Required | Description |
-| --------------------- | -------- | -------- | ------------------------------------------------------------------------ |
-| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) |
-| `args` | string[] | No | List of arguments passed to the command |
-| `env` | object | No | Environment variables passed to the MCP server process |
+| McpServerConfig 字段 | 类型 | 必填 | 说明 |
+| -------------------- | -------- | ---- | -------------------------------------------------------------------- |
+| `command` | string | 是 | 可执行文件路径或命令(如 `npx`、`node`、`python`) |
+| `args` | string[] | 否 | 传递给命令的参数列表 |
+| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量 |
-> When `command` is `npx`, Deep Code automatically prepends `-y` to the arguments.
+> 当 `command` 为 `npx` 时,Deep Code 会自动在参数前补充 `-y`。
-For detailed MCP usage instructions, refer to [mcp.md](mcp.md).
+详细 MCP 使用说明请参考 [mcp.md](mcp.md)。
-#### `debugLogEnabled` — Debug Log
-Set to `true` to enable detailed debug logging (default `false`), useful for troubleshooting API calls and tool execution.
+#### `debugLogEnabled` — 调试日志
-## Environment Variable Priority
+设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。
-Environment variables are a common way to configure applications, especially for sensitive information (such as api-key) or settings that may change between environments.
+## 环境变量优先级
-### Priority Principle
+环境变量是配置应用程序的常用方式,尤其适用于敏感信息(如 api-key)或可能在不同环境之间更改的设置。
-Environment variable priority follows the logic of “the more specific and localized the configuration, the higher the priority”, and the override rule of “env files protect existing environment by default, system variables override env files”. (The `env` object in settings.json can be thought of as a type of env file.)
+### 优先级原则
-Priority levels (from lowest to highest):
-1. `env` defined at the top level of `settings.json` – this is a general configuration for the entire tool and all its subprocesses (global variables). Can be overridden by outer environment variables, but the environment variable KEY has the `DEEPCODE_` prefix removed.
-2. `env` defined inside `mcpServers` in `settings.json` – this is the most specific configuration for a particular MCP service (local variables). Can be overridden by outer environment variables, but the KEY has the `MCP_` prefix removed.
-3. Shell/system environment variables – operating system level.
+环境变量优先级遵循“越具体、越局部的配置,优先级越高”和“env文件默认保护现有环境,系统变量高于env文件”的覆盖逻辑。(settings.json的env对象可以认为是一种env文件)
-### Scenarios
+优先级层级 (由低到高)
+1. settings.json 外层的 env:这是针对整个工具及其所有子进程的通用配置(全局变量)。可被外层环境变量覆盖,但环境变量KEY会移除`DEEPCODE_`前缀。
+2. settings.json mcpServers 内定义的 env:这是针对特定 MCP 服务的最具体配置(局部变量)。可被外层环境变量覆盖,但环境变量KEY会移除`MCP_`前缀。
+3. Shell 环境系统变量:操作系统层面的环境变量。
-#### 1. Setting the model’s api_key and base_url
+### 场景
-Applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones) – using api_key as an example:
+#### 一、设置模型的api_key, base_url
-1. Hardcoded default: `""`
-2. User-level settings.json: `{"env": {"API_KEY": "abc123"}}`
-3. Project-level settings.json: `{"env": {"API_KEY": "abc123"}}`
-4. System environment variable: `DEEPCODE_API_KEY=abc123 deepcode`
+按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以api_key为例):
-#### 2. Setting model, thinkingEnabled, and reasoningEffort
+1. 硬编码默认值: `""`
+2. 用户级settings.json: `{"env": {"API_KEY": "abc123"}}`
+3. 项目级settings.json: `{"env": {"API_KEY": "abc123"}}`
+4. 系统环境变量: `DEEPCODE_API_KEY=abc123 deepcode`
-Applied in the following priority order (lower-numbered overridden by higher-numbered) – using thinkingEnabled as an example:
+#### 二、设置模型的model, thinkingEnabled, reasoningEffort
-1. Hardcoded default: `true`
-2. User-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
-3. User-level settings.json: `{"thinkingEnabled": true}`
-4. Project-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
-5. Project-level settings.json: `{"thinkingEnabled": true}`
-6. System environment variable: `DEEPCODE_THINKING_ENABLED=true deepcode`
+按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以thinkingEnabled为例):
-#### 3. Setting environment variables for external scripts like notify and webSearchTool
+1. 硬编码默认值: `true`
+2. 用户级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+3. 用户级settings.json: `{"thinkingEnabled": true}`
+4. 项目级settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+5. 项目级settings.json: `{"thinkingEnabled": true}`
+6. 系统环境变量: `DEEPCODE_THINKING_ENABLED=true deepcode`
-Applied in the following priority order (lower-numbered overridden by higher-numbered) – using notify as an example:
+#### 三、设置启动notify, webSearchTool等外挂脚本的环境变量
-1. Hardcoded default: `os.environ.get('WEBHOOK', '...') # notify script code`
-2. User-level settings.json: `{"env": {"WEBHOOK": "..."}}`
-3. Project-level settings.json: `{"env": {"WEBHOOK": "true"}}`
-4. System environment variable: `DEEPCODE_WEBHOOK=... deepcode`
+按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以notify为例):
-#### 4. Setting environment variables for an MCP Service
+1. 硬编码默认值:`os.environ.get('WEBHOOK', '...') # notify脚本代码`
+2. 用户级settings.json: `{"env": {"WEBHOOK": "..."}}`
+3. 项目级settings.json: `{"env": {"WEBHOOK": "true"}}`
+4. 系统环境变量: `DEEPCODE_WEBHOOK=... deepcode`
-Applied in the following priority order (lower-numbered overridden by higher-numbered) – using a GitHub MCP server as an example:
+#### 四、设置MCP Service的环境变量
-1. User-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
-2. User-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
-3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
-4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
-5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
\ No newline at end of file
+按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以github MCP server为例):
+
+1. 用户级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+2. 用户级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+3. 项目级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+4. 项目级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+5. 系统环境变量: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
diff --git a/docs/configuration_en.md b/docs/configuration_en.md
new file mode 100644
index 0000000..369f8e4
--- /dev/null
+++ b/docs/configuration_en.md
@@ -0,0 +1,172 @@
+# Deep Code Configuration
+
+## Configuration Hierarchy
+
+Configuration is applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones):
+
+| Layer | Configuration Source | Description |
+| ----- | -------------------- | ---------------------------------------------- |
+| 1 | Defaults | Hardcoded defaults within the application |
+| 2 | User settings file | Global settings for the current user |
+| 3 | Project settings file| Project-specific settings |
+| 4 | Environment variables| System-wide or session-specific variables |
+
+## Settings File
+
+Deep Code uses the `settings.json` file for persistent configuration, supporting two storage locations:
+
+| File Type | Location | Scope |
+| ------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
+| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. |
+| Project settings file | `/.deepcode/settings.json` | Takes effect only when running Deep Code in that specific project. Project settings override user settings. |
+
+### Available Settings in `settings.json`
+
+The following are all the top-level fields supported in `settings.json`, along with the sub-fields inside `env`:
+
+| Field | Type | Description |
+| ------------------ | ------- | --------------------------------------------------------------------------- |
+| `env` | object | Group of environment variables (see sub-field table below) |
+| `model` | string | Model name. Takes precedence over `env.MODEL` |
+| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)|
+| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) |
+| `debugLogEnabled` | boolean | Enable debug log output (default `false`) |
+| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) |
+| `webSearchTool` | string | Full path to a custom web search script |
+| `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) |
+
+#### `env` Sub-fields
+
+| Field | Type | Description |
+| ----------------- | ------ | ---------------------------------------------------------------- |
+| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` |
+| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` |
+| `API_KEY` | string | API key |
+| `THINKING_ENABLED`| string | Enable thinking mode |
+| `REASONING_EFFORT`| string | Reasoning intensity |
+| `DEBUG_LOG_ENABLED`| string| Enable debug log output |
+| `` | string | Custom environment variable |
+
+#### `thinkingEnabled` — Thinking Mode
+
+Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to disable.
+
+- For `deepseek-v4-pro` and `deepseek-v4-flash`, thinking mode is **enabled by default**.
+- For other models, thinking mode is **disabled by default**.
+
+#### `reasoningEffort` — Reasoning Intensity
+
+When thinking mode is enabled, controls the depth of the model’s reasoning:
+
+| Value | Description |
+| ------ | --------------------------------------------------------- |
+| `max` | Maximum reasoning depth (default) |
+| `high` | Higher reasoning depth with relatively lower token usage |
+
+#### `notify` — Task Completion Notification
+
+Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message).
+
+```json
+{
+ "notify": "/path/to/slack-notify.sh"
+}
+```
+
+#### `webSearchTool` — Custom Web Search
+
+Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script:
+
+```json
+{
+ "webSearchTool": "/path/to/my-search-script.sh"
+}
+```
+
+The script receives a search query as an argument and outputs results in JSON format for the AI.
+
+#### `mcpServers` — MCP Servers
+
+Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object.
+
+```json
+{
+ "mcpServers": {
+ "": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-github"],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
+ }
+ }
+ }
+}
+```
+
+| McpServerConfig field | Type | Required | Description |
+| --------------------- | -------- | -------- | ------------------------------------------------------------------------ |
+| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) |
+| `args` | string[] | No | List of arguments passed to the command |
+| `env` | object | No | Environment variables passed to the MCP server process |
+
+> When `command` is `npx`, Deep Code automatically prepends `-y` to the arguments.
+
+For detailed MCP usage instructions, refer to [mcp.md](mcp.md).
+
+#### `debugLogEnabled` — Debug Log
+
+Set to `true` to enable detailed debug logging (default `false`), useful for troubleshooting API calls and tool execution.
+
+## Environment Variable Priority
+
+Environment variables are a common way to configure applications, especially for sensitive information (such as api-key) or settings that may change between environments.
+
+### Priority Principle
+
+Environment variable priority follows the logic of “the more specific and localized the configuration, the higher the priority”, and the override rule of “env files protect existing environment by default, system variables override env files”. (The `env` object in settings.json can be thought of as a type of env file.)
+
+Priority levels (from lowest to highest):
+1. `env` defined at the top level of `settings.json` – this is a general configuration for the entire tool and all its subprocesses (global variables). Can be overridden by outer environment variables, but the environment variable KEY has the `DEEPCODE_` prefix removed.
+2. `env` defined inside `mcpServers` in `settings.json` – this is the most specific configuration for a particular MCP service (local variables). Can be overridden by outer environment variables, but the KEY has the `MCP_` prefix removed.
+3. Shell/system environment variables – operating system level.
+
+### Scenarios
+
+#### 1. Setting the model’s api_key and base_url
+
+Applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones) – using api_key as an example:
+
+1. Hardcoded default: `""`
+2. User-level settings.json: `{"env": {"API_KEY": "abc123"}}`
+3. Project-level settings.json: `{"env": {"API_KEY": "abc123"}}`
+4. System environment variable: `DEEPCODE_API_KEY=abc123 deepcode`
+
+#### 2. Setting model, thinkingEnabled, and reasoningEffort
+
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using thinkingEnabled as an example:
+
+1. Hardcoded default: `true`
+2. User-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+3. User-level settings.json: `{"thinkingEnabled": true}`
+4. Project-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}`
+5. Project-level settings.json: `{"thinkingEnabled": true}`
+6. System environment variable: `DEEPCODE_THINKING_ENABLED=true deepcode`
+
+#### 3. Setting environment variables for external scripts like notify and webSearchTool
+
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using notify as an example:
+
+1. Hardcoded default: `os.environ.get('WEBHOOK', '...') # notify script code`
+2. User-level settings.json: `{"env": {"WEBHOOK": "..."}}`
+3. Project-level settings.json: `{"env": {"WEBHOOK": "true"}}`
+4. System environment variable: `DEEPCODE_WEBHOOK=... deepcode`
+
+#### 4. Setting environment variables for an MCP Service
+
+Applied in the following priority order (lower-numbered overridden by higher-numbered) – using a GitHub MCP server as an example:
+
+1. User-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+2. User-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}`
+4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}`
+5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode`
\ No newline at end of file
diff --git a/docs/mcp.md b/docs/mcp.md
index 11adda5..73034a3 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -1,22 +1,22 @@
-# Deep Code CLI MCP Configuration Guide
+# Deep Code CLI MCP 配置指南
-Deep Code CLI supports MCP (Model Context Protocol), enabling AI assistants to connect with external tools and services such as GitHub, browsers, databases, and more.
+Deep Code CLI 支持 MCP(Model Context Protocol),让 AI 助手能够连接外部工具和服务,如 GitHub、浏览器、数据库等。
-## Overview
+## 概述
-Once MCP is configured, Deep Code can:
+配置 MCP 后,Deep Code 可以:
-- Operate on GitHub repositories (view issues, create PRs, search code, etc.)
-- Control browsers (screenshots, clicks, form filling, etc.)
-- Access the file system
-- Connect to databases and APIs
-- ...and any external service compatible with the MCP protocol
+- 操作 GitHub 仓库(查看 Issues、创建 PR、搜索代码等)
+- 操控浏览器(截图、点击、填表单等)
+- 访问文件系统
+- 连接数据库和 API
+- ...以及任何兼容 MCP 协议的外部服务
-MCP tools are named in Deep Code using the format `mcp____`, for example `mcp__github__search_code`.
+MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,例如 `mcp__github__search_code`。
-## Configuring MCP Servers
+## 配置 MCP 服务器
-Edit `~/.deepcode/settings.json` and add the `mcpServers` field:
+编辑 `~/.deepcode/settings.json`,添加 `mcpServers` 字段:
```json
{
@@ -28,30 +28,30 @@ Edit `~/.deepcode/settings.json` and add the `mcpServers` field:
"thinkingEnabled": true,
"reasoningEffort": "max",
"mcpServers": {
- "": {
- "command": "",
- "args": ["", ""],
+ "<服务名称>": {
+ "command": "<可执行文件>",
+ "args": ["<参数1>", "<参数2>"],
"env": {
- "": ""
+ "<环境变量>": "<值>"
}
}
}
}
```
-### Configuration Fields
+### 配置项说明
-| Field | Type | Required | Description |
-| --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `command` | string | Yes | Path or command of the MCP server executable (e.g., `npx`, `node`, `python`). When the command is `npx`, Deep Code automatically prepends `-y` to the arguments. |
-| `args` | string[] | No | List of arguments to pass to the command |
-| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process |
+| 字段 | 类型 | 必填 | 说明 |
+| --------- | -------- | ---- | ---------------------------------------------------------------------------------------------------------------------- |
+| `command` | string | 是 | MCP 服务器的可执行文件路径或命令(如 `npx`、`node`、`python`)。当命令是 `npx` 时,Deep Code 会自动在参数前补充 `-y`。 |
+| `args` | string[] | 否 | 传递给命令的参数列表 |
+| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量(如 API Key) |
-## Common MCP Examples
+## 常用 MCP 示例
### GitHub MCP
-Allows Deep Code to directly operate on GitHub repositories (search code, manage issues/PRs, read/write files, etc.):
+让 Deep Code 直接操作 GitHub 仓库(搜索代码、管理 Issue/PR、读写文件等):
```json
{
@@ -67,11 +67,11 @@ Allows Deep Code to directly operate on GitHub repositories (search code, manage
}
```
-> Generate a GitHub Personal Access Token at [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens).
+> GitHub Personal Access Token 可在 [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 生成。
-### Browser Control (Playwright)
+### 浏览器控制(Playwright)
-Lets Deep Code control a browser for screenshots, page interactions, etc.:
+让 Deep Code 操控浏览器进行截图、页面操作等:
```json
{
@@ -84,9 +84,9 @@ Lets Deep Code control a browser for screenshots, page interactions, etc.:
}
```
-### File System
+### 文件系统
-Enables Deep Code to read and write files within a specified directory:
+让 Deep Code 在指定目录中读写文件:
```json
{
@@ -99,7 +99,7 @@ Enables Deep Code to read and write files within a specified directory:
}
```
-### Custom Python MCP
+### 自定义 Python MCP
```json
{
@@ -115,9 +115,9 @@ Enables Deep Code to read and write files within a specified directory:
}
```
-## Full Configuration Example
+## 完整配置示例
-Below is a complete `~/.deepcode/settings.json` with both GitHub and Playwright MCP servers configured:
+以下是一个配置了 GitHub 和 Playwright 两个 MCP 服务器的完整 `~/.deepcode/settings.json`:
```json
{
@@ -144,62 +144,57 @@ Below is a complete `~/.deepcode/settings.json` with both GitHub and Playwright
}
```
-## Using MCP
+## 使用 MCP
-After configuration, start `deepcode` and use the `/mcp` command to manage MCP connections:
+配置完成后,启动 `deepcode`,在聊天中输入 `/mcp` 即可查看所有已配置的 MCP 服务器状态以及每个服务器提供的工具列表。
-- `/mcp` — View the status of configured MCP servers
-- `/mcp add` — Add a new MCP server
-- `/mcp remove` — Remove an MCP server
-- `/mcp list` — List all connected MCP servers and their tools
-
-Simply use the MCP tool name in your conversation to invoke it, for example:
+在对话中直接使用 MCP 工具名称即可调用,例如:
```
-Help me search for issues in the deepcode-cli repository on GitHub
+帮我搜索 GitHub 上 deepcode-cli 仓库的 issues
```
-The AI will automatically invoke the `mcp__github__search_issues` tool to complete the action.
+AI 会自动调用 `mcp__github__search_issues` 工具完成操作。
-## Tool Naming Convention
+## 工具命名规则
-An MCP tool name consists of three parts: `mcp____`
+MCP 工具名称由三部分组成:`mcp__<服务名>__<工具名>`
-| Service | Tool Name | Full Invocation Name |
-| ---------- | ----------------------- | ------------------------------------------- |
-| github | search_code | `mcp__github__search_code` |
-| github | create_pull_request | `mcp__github__create_pull_request` |
-| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
-| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
+| 服务名 | 工具名 | 完整调用名 |
+| ---------- | ----------------------- | ------------------------------------------ |
+| github | search_code | `mcp__github__search_code` |
+| github | create_pull_request | `mcp__github__create_pull_request` |
+| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
+| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
-You can view the list of tools provided by each server using `/mcp list`.
+你可以通过 `/mcp` 查看每个服务器提供的具体工具列表。
-## Troubleshooting
+## 故障排查
-### Startup Failure
+### 启动失败
-If an MCP server fails to start, check:
+如果 MCP 服务器无法启动,检查:
-1. Whether `command` is installed (e.g., `npx` requires Node.js)
-2. Whether environment variables in `env` are correct (e.g., `GITHUB_PERSONAL_ACCESS_TOKEN`)
-3. Whether the terminal running `deepcode` has network access
+1. `command` 是否已安装(如 `npx` 需要 Node.js)
+2. `env` 中的环境变量是否正确(如 `GITHUB_PERSONAL_ACCESS_TOKEN`)
+3. 运行 `deepcode` 的终端是否有网络访问权限
-### Tools Not Showing Up
+### 工具不显示
-1. Verify that the `mcpServers` field in `settings.json` is correctly formatted
-2. After starting deepcode, use `/mcp` to check server status
-3. If the server status shows an error, debug based on the error message
+1. 确认 `settings.json` 中的 `mcpServers` 字段格式正确
+2. 启动 deepcode 后使用 `/mcp` 查看服务器状态
+3. 如果服务器状态显示错误,根据错误信息排查
-### Windows Users
+### Windows 用户
-On Windows, Deep Code CLI automatically adds shell support for `.cmd` commands. If your MCP command is a batch script, ensure the filename ends with `.cmd`.
+在 Windows 上,Deep Code CLI 会自动为 `.cmd` 命令添加 shell 支持。如果你的 MCP 命令是批处理脚本,确保文件名以 `.cmd` 结尾。
-## Writing Your Own MCP Server
+## 编写你自己的 MCP 服务器
-MCP servers follow the [Model Context Protocol](https://modelcontextprotocol.io/) specification and communicate using JSON‑RPC 2.0. You can write an MCP server in any language as long as it implements the following methods:
+MCP 服务器遵循 [Model Context Protocol](https://modelcontextprotocol.io/) 规范,使用 JSON-RPC 2.0 通信。你可以用任何语言编写 MCP 服务器,只要实现以下协议即可:
-1. `initialize` — Handshake and protocol negotiation
-2. `tools/list` — Return the list of available tools
-3. `tools/call` — Execute a tool call
+1. `initialize` — 握手和协议协商
+2. `tools/list` — 返回可用工具列表
+3. `tools/call` — 执行工具调用
-For more information, see the [official MCP documentation](https://modelcontextprotocol.io/).
\ No newline at end of file
+更多参考:[MCP 官方文档](https://modelcontextprotocol.io/)
diff --git a/docs/mcp_en.md b/docs/mcp_en.md
new file mode 100644
index 0000000..03c4b30
--- /dev/null
+++ b/docs/mcp_en.md
@@ -0,0 +1,200 @@
+# Deep Code CLI MCP Configuration Guide
+
+Deep Code CLI supports MCP (Model Context Protocol), enabling AI assistants to connect with external tools and services such as GitHub, browsers, databases, and more.
+
+## Overview
+
+Once MCP is configured, Deep Code can:
+
+- Operate on GitHub repositories (view issues, create PRs, search code, etc.)
+- Control browsers (screenshots, clicks, form filling, etc.)
+- Access the file system
+- Connect to databases and APIs
+- ...and any external service compatible with the MCP protocol
+
+MCP tools are named in Deep Code using the format `mcp____`, for example `mcp__github__search_code`.
+
+## Configuring MCP Servers
+
+Edit `~/.deepcode/settings.json` and add the `mcpServers` field:
+
+```json
+{
+ "env": {
+ "MODEL": "deepseek-v4-pro",
+ "BASE_URL": "https://api.deepseek.com",
+ "API_KEY": "sk-..."
+ },
+ "thinkingEnabled": true,
+ "reasoningEffort": "max",
+ "mcpServers": {
+ "": {
+ "command": "",
+ "args": ["", ""],
+ "env": {
+ "": ""
+ }
+ }
+ }
+}
+```
+
+### Configuration Fields
+
+| Field | Type | Required | Description |
+| --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `command` | string | Yes | Path or command of the MCP server executable (e.g., `npx`, `node`, `python`). When the command is `npx`, Deep Code automatically prepends `-y` to the arguments. |
+| `args` | string[] | No | List of arguments to pass to the command |
+| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process |
+
+## Common MCP Examples
+
+### GitHub MCP
+
+Allows Deep Code to directly operate on GitHub repositories (search code, manage issues/PRs, read/write files, etc.):
+
+```json
+{
+ "mcpServers": {
+ "github": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-github"],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
+ }
+ }
+ }
+}
+```
+
+> Generate a GitHub Personal Access Token at [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens).
+
+### Browser Control (Playwright)
+
+Lets Deep Code control a browser for screenshots, page interactions, etc.:
+
+```json
+{
+ "mcpServers": {
+ "playwright": {
+ "command": "npx",
+ "args": ["@playwright/mcp@latest"]
+ }
+ }
+}
+```
+
+### File System
+
+Enables Deep Code to read and write files within a specified directory:
+
+```json
+{
+ "mcpServers": {
+ "filesystem": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
+ }
+ }
+}
+```
+
+### Custom Python MCP
+
+```json
+{
+ "mcpServers": {
+ "my-tool": {
+ "command": "python",
+ "args": ["-m", "my_mcp_server"],
+ "env": {
+ "API_KEY": "xxx"
+ }
+ }
+ }
+}
+```
+
+## Full Configuration Example
+
+Below is a complete `~/.deepcode/settings.json` with both GitHub and Playwright MCP servers configured:
+
+```json
+{
+ "env": {
+ "MODEL": "deepseek-v4-pro",
+ "BASE_URL": "https://api.deepseek.com",
+ "API_KEY": "sk-xxxxxxxxxxxx"
+ },
+ "thinkingEnabled": true,
+ "reasoningEffort": "max",
+ "mcpServers": {
+ "github": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-github"],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
+ }
+ },
+ "playwright": {
+ "command": "npx",
+ "args": ["@playwright/mcp@latest"]
+ }
+ }
+}
+```
+
+## Using MCP
+
+After configuration, start `deepcode` and type `/mcp` in the chat to view the status of all configured MCP servers and the list of tools each server provides.
+
+Simply use the MCP tool name in your conversation to invoke it, for example:
+
+```
+Help me search for issues in the deepcode-cli repository on GitHub
+```
+
+The AI will automatically invoke the `mcp__github__search_issues` tool to complete the action.
+
+## Tool Naming Convention
+
+An MCP tool name consists of three parts: `mcp____`
+
+| Service | Tool Name | Full Invocation Name |
+| ---------- | ----------------------- | ------------------------------------------- |
+| github | search_code | `mcp__github__search_code` |
+| github | create_pull_request | `mcp__github__create_pull_request` |
+| playwright | browser_navigate | `mcp__playwright__browser_navigate` |
+| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` |
+
+You can view the list of tools provided by each server using `/mcp`.
+
+## Troubleshooting
+
+### Startup Failure
+
+If an MCP server fails to start, check:
+
+1. Whether `command` is installed (e.g., `npx` requires Node.js)
+2. Whether environment variables in `env` are correct (e.g., `GITHUB_PERSONAL_ACCESS_TOKEN`)
+3. Whether the terminal running `deepcode` has network access
+
+### Tools Not Showing Up
+
+1. Verify that the `mcpServers` field in `settings.json` is correctly formatted
+2. After starting deepcode, use `/mcp` to check server status
+3. If the server status shows an error, debug based on the error message
+
+### Windows Users
+
+On Windows, Deep Code CLI automatically adds shell support for `.cmd` commands. If your MCP command is a batch script, ensure the filename ends with `.cmd`.
+
+## Writing Your Own MCP Server
+
+MCP servers follow the [Model Context Protocol](https://modelcontextprotocol.io/) specification and communicate using JSON‑RPC 2.0. You can write an MCP server in any language as long as it implements the following methods:
+
+1. `initialize` — Handshake and protocol negotiation
+2. `tools/list` — Return the list of available tools
+3. `tools/call` — Execute a tool call
+
+For more information, see the [official MCP documentation](https://modelcontextprotocol.io/).
\ No newline at end of file
From dbe859e588300c7b4309d78cc60508f0abc5e18c Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Fri, 15 May 2026 22:50:00 +0800
Subject: [PATCH 017/129] refactor: update usage types to ModelUsage in session
---
src/session.ts | 29 ++++++++++++++++++++---------
src/tests/exitSummary.test.ts | 4 ++--
2 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/src/session.ts b/src/session.ts
index 894ff80..9295ff2 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -65,11 +65,11 @@ function addUsageValue(current: unknown, next: unknown): unknown {
return next;
}
-function accumulateUsage(current: unknown | null, next: unknown | null | undefined): unknown | null {
+function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null {
if (next == null) {
return current ?? null;
}
- return addUsageValue(current, next);
+ return addUsageValue(current, next) as ModelUsage;
}
function getExtensionRoot(): string {
@@ -81,7 +81,7 @@ function getExtensionRoot(): string {
return path.resolve(path.dirname(currentFilePath), "..");
}
-function getTotalTokens(usage: unknown | null | undefined): number {
+function getTotalTokens(usage: ModelUsage | null | undefined): number {
if (!isUsageRecord(usage)) {
return 0;
}
@@ -91,6 +91,17 @@ function getTotalTokens(usage: unknown | null | undefined): number {
export type SessionStatus = "failed" | "pending" | "processing" | "waiting_for_user" | "completed" | "interrupted";
+export type ModelUsage = {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ completion_tokens_details?: Record;
+ prompt_tokens_details?: Record;
+ prompt_cache_hit_tokens?: number;
+ prompt_cache_miss_tokens?: number;
+ total_reqs?: number;
+};
+
export type SessionEntry = {
id: string;
summary: string | null;
@@ -100,7 +111,7 @@ export type SessionEntry = {
toolCalls: unknown[] | null;
status: SessionStatus;
failReason: string | null;
- usage: unknown | null;
+ usage: ModelUsage | null;
activeTokens: number;
createTime: string;
updateTime: string;
@@ -286,7 +297,7 @@ export class SessionManager {
debug?: ChatCompletionDebugOptions
): Promise<{
choices?: Array<{ message?: Record }>;
- usage?: unknown;
+ usage?: ModelUsage | null;
}> {
const requestId = crypto.randomUUID();
const startedAt = new Date().toISOString();
@@ -355,13 +366,13 @@ export class SessionManager {
request: streamRequest,
response,
});
- return response as { choices?: Array<{ message?: Record }>; usage?: unknown };
+ return response as { choices?: Array<{ message?: Record }>; usage?: ModelUsage | null };
}
let content = "";
let reasoningContent = "";
let refusal: string | null = null;
- let usage: unknown = null;
+ let usage: ModelUsage | null = null;
const responseChunks: unknown[] = [];
const toolCallsByIndex = new Map<
number,
@@ -386,7 +397,7 @@ export class SessionManager {
responseChunks.push(chunk);
}
if ("usage" in chunk && chunk.usage != null) {
- usage = chunk.usage;
+ usage = chunk.usage as ModelUsage;
}
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
@@ -2096,7 +2107,7 @@ ${skillMd}
toolCalls: Array.isArray(value.toolCalls) ? value.toolCalls : null,
status: this.normalizeSessionStatus(value.status),
failReason: typeof value.failReason === "string" ? value.failReason : null,
- usage: value.usage ?? null,
+ usage: (value.usage as ModelUsage) ?? null,
activeTokens: typeof value.activeTokens === "number" ? value.activeTokens : 0,
createTime: typeof value.createTime === "string" ? value.createTime : new Date().toISOString(),
updateTime: typeof value.updateTime === "string" ? value.updateTime : new Date().toISOString(),
diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts
index 9cff34b..0257bf2 100644
--- a/src/tests/exitSummary.test.ts
+++ b/src/tests/exitSummary.test.ts
@@ -1,7 +1,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildExitSummaryText } from "../ui";
-import type { SessionEntry, SessionMessage } from "../session";
+import type { SessionEntry, SessionMessage, ModelUsage } from "../session";
const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, "");
@@ -33,7 +33,7 @@ test("buildExitSummaryText only shows Goodbye and model usage with cached tokens
assert.doesNotMatch(summary, /Reasoning Tokens/);
});
-function buildSession(usage: unknown): SessionEntry {
+function buildSession(usage: ModelUsage | null): SessionEntry {
return {
id: "session-1",
summary: null,
From 3ef5b8e7e293d80095edcd1e8f91d5b2b1b7ad4f Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Fri, 15 May 2026 23:18:27 +0800
Subject: [PATCH 018/129] feat: add usage tracking per model and update exit
summary display
---
src/session.ts | 45 +++++++++++++++++
src/tests/exitSummary.test.ts | 93 +++++++++++++++++++++++++----------
src/tests/session.test.ts | 89 +++++++++++++++++++++++++++++++++
src/ui/App.tsx | 8 +--
src/ui/exitSummary.ts | 60 +++++++++++++---------
5 files changed, 241 insertions(+), 54 deletions(-)
diff --git a/src/session.ts b/src/session.ts
index 9295ff2..97bc3ab 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -72,6 +72,29 @@ function accumulateUsage(current: ModelUsage | null, next: unknown | null | unde
return addUsageValue(current, next) as ModelUsage;
}
+function usageWithRequestCount(usage: ModelUsage): ModelUsage {
+ const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1;
+ return {
+ ...usage,
+ total_reqs: totalReqs,
+ };
+}
+
+function accumulateUsagePerModel(
+ current: Record | null | undefined,
+ model: string,
+ next: ModelUsage | null | undefined
+): Record | null {
+ if (next == null) {
+ return current ?? null;
+ }
+
+ const usagePerModel = { ...(current ?? {}) };
+ const modelName = model.trim() || "unknown";
+ usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!;
+ return usagePerModel;
+}
+
function getExtensionRoot(): string {
if (typeof __dirname !== "undefined") {
return path.resolve(__dirname, "..");
@@ -112,6 +135,7 @@ export type SessionEntry = {
status: SessionStatus;
failReason: string | null;
usage: ModelUsage | null;
+ usagePerModel: Record | null;
activeTokens: number;
createTime: string;
updateTime: string;
@@ -847,6 +871,7 @@ The candidate skills are as follows:\n\n`;
status: "pending",
failReason: null,
usage: null,
+ usagePerModel: null,
activeTokens: 0,
createTime: now,
updateTime: now,
@@ -1087,6 +1112,7 @@ ${skillMd}
assistantRefusal: refusal,
toolCalls,
usage: accumulateUsage(entry.usage, responseUsage),
+ usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage),
activeTokens: getTotalTokens(responseUsage),
status: refusal ? "failed" : waitingForUser ? "waiting_for_user" : toolCalls ? "processing" : "completed",
failReason: refusal ? refusal : entry.failReason,
@@ -1196,6 +1222,7 @@ ${skillMd}
this.updateSessionEntry(sessionId, (entry) => ({
...entry,
usage: accumulateUsage(entry.usage, responseUsage),
+ usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage),
activeTokens: getTotalTokens(responseUsage),
updateTime: now,
}));
@@ -2108,6 +2135,7 @@ ${skillMd}
status: this.normalizeSessionStatus(value.status),
failReason: typeof value.failReason === "string" ? value.failReason : null,
usage: (value.usage as ModelUsage) ?? null,
+ usagePerModel: this.normalizeUsagePerModel(value),
activeTokens: typeof value.activeTokens === "number" ? value.activeTokens : 0,
createTime: typeof value.createTime === "string" ? value.createTime : new Date().toISOString(),
updateTime: typeof value.updateTime === "string" ? value.updateTime : new Date().toISOString(),
@@ -2129,6 +2157,23 @@ ${skillMd}
return "pending";
}
+ private normalizeUsagePerModel(entry: Record): Record | null {
+ if (!Object.prototype.hasOwnProperty.call(entry, "usagePerModel")) {
+ return null;
+ }
+ if (!isUsageRecord(entry.usagePerModel)) {
+ return null;
+ }
+ const usagePerModel: Record = {};
+ for (const [model, usage] of Object.entries(entry.usagePerModel)) {
+ if (!model || !isUsageRecord(usage)) {
+ continue;
+ }
+ usagePerModel[model] = usage as ModelUsage;
+ }
+ return usagePerModel;
+ }
+
private deserializeProcesses(value: unknown): Map | null {
if (!value || typeof value !== "object") {
return null;
diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts
index 0257bf2..5ea4b57 100644
--- a/src/tests/exitSummary.test.ts
+++ b/src/tests/exitSummary.test.ts
@@ -1,22 +1,23 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildExitSummaryText } from "../ui";
-import type { SessionEntry, SessionMessage, ModelUsage } from "../session";
+import type { ModelUsage, SessionEntry } from "../session";
const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, "");
test("buildExitSummaryText only shows Goodbye and model usage with cached tokens", () => {
const summary = stripAnsi(
buildExitSummaryText({
- session: buildSession({
- prompt_tokens: 11_966,
- completion_tokens: 236,
- total_tokens: 12_202,
- prompt_tokens_details: { cached_tokens: 11_776 },
- completion_tokens_details: { reasoning_tokens: 144 },
+ session: buildSession(null, {
+ "mimo-v2.5-pro": {
+ prompt_tokens: 11_966,
+ completion_tokens: 236,
+ total_tokens: 12_202,
+ prompt_tokens_details: { cached_tokens: 11_776 },
+ completion_tokens_details: { reasoning_tokens: 144 },
+ total_reqs: 2,
+ },
}),
- messages: [buildAssistantMessage("assistant-1"), buildAssistantMessage("assistant-2")],
- model: "mimo-v2.5-pro",
})
);
@@ -33,7 +34,63 @@ test("buildExitSummaryText only shows Goodbye and model usage with cached tokens
assert.doesNotMatch(summary, /Reasoning Tokens/);
});
-function buildSession(usage: ModelUsage | null): SessionEntry {
+test("buildExitSummaryText shows all usagePerModel rows sorted by request count", () => {
+ const summary = stripAnsi(
+ buildExitSummaryText({
+ session: buildSession(
+ {
+ prompt_tokens: 999,
+ completion_tokens: 999,
+ total_tokens: 1_998,
+ },
+ {
+ "deepseek-v4-pro": {
+ prompt_tokens: 100,
+ completion_tokens: 10,
+ total_tokens: 110,
+ total_reqs: 1,
+ },
+ "deepseek-v4-flash": {
+ prompt_tokens: 300,
+ completion_tokens: 30,
+ total_tokens: 330,
+ prompt_cache_hit_tokens: 111,
+ total_reqs: 3,
+ },
+ }
+ ),
+ })
+ );
+
+ const flashIndex = summary.indexOf("deepseek-v4-flash");
+ const proIndex = summary.indexOf("deepseek-v4-pro");
+
+ assert.notEqual(flashIndex, -1);
+ assert.notEqual(proIndex, -1);
+ assert.ok(flashIndex < proIndex);
+ assert.match(summary, /deepseek-v4-flash\s+3\s+300\s+30\s+111/);
+ assert.match(summary, /deepseek-v4-pro\s+1\s+100\s+10\s+0/);
+ assert.doesNotMatch(summary, /999/);
+});
+
+test("buildExitSummaryText does not derive usage rows from legacy aggregate usage", () => {
+ const summary = stripAnsi(
+ buildExitSummaryText({
+ session: buildSession({
+ prompt_tokens: 11_966,
+ completion_tokens: 236,
+ total_tokens: 12_202,
+ total_reqs: 2,
+ }),
+ })
+ );
+
+ assert.match(summary, /Goodbye!/);
+ assert.doesNotMatch(summary, /Model Usage/);
+ assert.doesNotMatch(summary, /11,966/);
+});
+
+function buildSession(usage: ModelUsage | null, usagePerModel: Record | null = null): SessionEntry {
return {
id: "session-1",
summary: null,
@@ -44,24 +101,10 @@ function buildSession(usage: ModelUsage | null): SessionEntry {
status: "completed",
failReason: null,
usage,
+ usagePerModel,
activeTokens: 0,
createTime: "2026-01-01T00:00:00.000Z",
updateTime: "2026-01-01T00:00:01.000Z",
processes: null,
};
}
-
-function buildAssistantMessage(id: string): SessionMessage {
- return {
- id,
- sessionId: "session-1",
- role: "assistant",
- content: "",
- contentParams: null,
- messageParams: null,
- compacted: false,
- visible: true,
- createTime: "2026-01-01T00:00:00.000Z",
- updateTime: "2026-01-01T00:00:00.000Z",
- };
-}
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index 2ac6c29..1e5e5ce 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -275,6 +275,20 @@ test("SessionManager normalizes legacy sessions without activeTokens to zero", (
const manager = createSessionManager(workspace, "machine-id-legacy");
assert.equal(manager.getSession("legacy-session")?.activeTokens, 0);
+ assert.equal(manager.getSession("legacy-session")?.usagePerModel, null);
+});
+
+test("SessionManager keeps usagePerModel null until response usage is available", async () => {
+ const workspace = createTempDir("deepcode-null-usage-per-model-workspace-");
+ const home = createTempDir("deepcode-null-usage-per-model-home-");
+ process.env.HOME = home;
+
+ const manager = createMockedClientSessionManager(workspace, [{ choices: [{ message: { content: "no usage" } }] }]);
+
+ const sessionId = await manager.createSession({ text: "" });
+
+ assert.equal(manager.getSession(sessionId)?.usage, null);
+ assert.equal(manager.getSession(sessionId)?.usagePerModel, null);
});
test("SessionManager marks skills loaded from existing session messages", async () => {
@@ -1122,6 +1136,7 @@ test("SessionManager accumulates response usage while active tokens track the la
const session = manager.getSession(sessionId);
const usage = session?.usage as Record;
+ const usagePerModel = session?.usagePerModel?.["test-model"] as Record;
assert.equal(session?.activeTokens, 27);
assert.equal(usage.prompt_tokens, 30);
assert.equal(usage.completion_tokens, 12);
@@ -1130,6 +1145,75 @@ test("SessionManager accumulates response usage while active tokens track the la
assert.equal(usage.completion_tokens_details.reasoning_tokens, 7);
assert.equal(usage.prompt_cache_hit_tokens, 18);
assert.equal(usage.prompt_cache_miss_tokens, 12);
+ assert.equal(usagePerModel.prompt_tokens, 30);
+ assert.equal(usagePerModel.completion_tokens, 12);
+ assert.equal(usagePerModel.total_tokens, 42);
+ assert.equal(usagePerModel.prompt_tokens_details.cached_tokens, 18);
+ assert.equal(usagePerModel.completion_tokens_details.reasoning_tokens, 7);
+ assert.equal(usagePerModel.prompt_cache_hit_tokens, 18);
+ assert.equal(usagePerModel.prompt_cache_miss_tokens, 12);
+ assert.equal(usagePerModel.total_reqs, 2);
+});
+
+test("SessionManager stores usage per model across model changes", async () => {
+ const workspace = createTempDir("deepcode-usage-per-model-workspace-");
+ const home = createTempDir("deepcode-usage-per-model-home-");
+ process.env.HOME = home;
+
+ let currentModel = "deepseek-v4-pro";
+ const responses = [
+ createChatResponse("pro response", {
+ prompt_tokens: 10,
+ completion_tokens: 5,
+ total_tokens: 15,
+ }),
+ createChatResponse("flash response", {
+ prompt_tokens: 20,
+ completion_tokens: 7,
+ total_tokens: 27,
+ prompt_cache_hit_tokens: 6,
+ }),
+ ];
+ const client = {
+ chat: {
+ completions: {
+ create: async () => {
+ const response = responses.shift();
+ assert.ok(response, "expected a queued chat response");
+ return response;
+ },
+ },
+ },
+ };
+ const manager = new SessionManager({
+ projectRoot: workspace,
+ createOpenAIClient: () => ({
+ client: client as any,
+ model: currentModel,
+ baseURL: "https://api.deepseek.com",
+ thinkingEnabled: false,
+ }),
+ getResolvedSettings: () => ({ model: currentModel }),
+ renderMarkdown: (text) => text,
+ onAssistantMessage: () => {},
+ });
+
+ const sessionId = await manager.createSession({ text: "" });
+ currentModel = "deepseek-v4-flash";
+ await manager.replySession(sessionId, { text: "" });
+
+ const session = manager.getSession(sessionId);
+ assert.deepEqual(Object.keys(session?.usagePerModel ?? {}).sort(), ["deepseek-v4-flash", "deepseek-v4-pro"]);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-pro"]?.prompt_tokens, 10);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-pro"]?.completion_tokens, 5);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-pro"]?.total_reqs, 1);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-flash"]?.prompt_tokens, 20);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-flash"]?.completion_tokens, 7);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-flash"]?.prompt_cache_hit_tokens, 6);
+ assert.equal(session?.usagePerModel?.["deepseek-v4-flash"]?.total_reqs, 1);
+ assert.equal(session?.usage?.prompt_tokens, 30);
+ assert.equal(session?.usage?.completion_tokens, 12);
+ assert.equal(session?.usage?.total_tokens, 42);
});
test("SessionManager resets active tokens to latest post-compaction response usage", async () => {
@@ -1163,10 +1247,15 @@ test("SessionManager resets active tokens to latest post-compaction response usa
const session = manager.getSession(sessionId);
const usage = session?.usage as Record;
+ const usagePerModel = session?.usagePerModel?.["test-model"] as Record;
assert.equal(session?.activeTokens, 7);
assert.equal(usage.prompt_tokens, 140_095);
assert.equal(usage.completion_tokens, 35);
assert.equal(usage.total_tokens, 140_130);
+ assert.equal(usagePerModel.prompt_tokens, 140_095);
+ assert.equal(usagePerModel.completion_tokens, 35);
+ assert.equal(usagePerModel.total_tokens, 140_130);
+ assert.equal(usagePerModel.total_reqs, 3);
});
test("SessionManager streams chat completions and counts reasoning progress", async () => {
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 3f32f56..25671ce 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -148,11 +148,7 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
setTimeout(() => {
const activeSessionId = sessionManager.getActiveSessionId();
const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null;
- const allMessages = activeSessionId
- ? sessionManager.listSessionMessages(activeSessionId)
- : messagesRef.current;
- const resolved = resolveCurrentSettings(projectRoot);
- const summary = buildExitSummaryText({ session, messages: allMessages, model: resolved.model });
+ const summary = buildExitSummaryText({ session });
process.stdout.write("\n");
process.stdout.write(chalk.green("> /exit "));
process.stdout.write("\n\n");
@@ -256,7 +252,7 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
setRunningProcesses(null);
}
},
- [exit, onRestart, projectRoot, sessionManager, refreshSkills, refreshSessionsList]
+ [exit, onRestart, sessionManager, refreshSkills, refreshSessionsList]
);
const handleInterrupt = useCallback(() => {
diff --git a/src/ui/exitSummary.ts b/src/ui/exitSummary.ts
index 910cceb..c55d9ce 100644
--- a/src/ui/exitSummary.ts
+++ b/src/ui/exitSummary.ts
@@ -1,11 +1,9 @@
import chalk from "chalk";
import gradientString from "gradient-string";
-import type { SessionEntry, SessionMessage } from "../session";
+import type { ModelUsage, SessionEntry } from "../session";
type ExitSummaryInput = {
session: SessionEntry | null;
- messages: SessionMessage[];
- model?: string;
};
const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
@@ -32,13 +30,15 @@ type UsageFields = {
promptTokens: number;
completionTokens: number;
cachedTokens: number;
+ totalReqs: number;
};
-function extractUsageFields(usage: unknown | null): UsageFields {
+function extractUsageFields(usage: ModelUsage | null): UsageFields {
const empty: UsageFields = {
promptTokens: 0,
completionTokens: 0,
cachedTokens: 0,
+ totalReqs: 0,
};
if (!usage || typeof usage !== "object" || Array.isArray(usage)) {
return empty;
@@ -61,14 +61,13 @@ function extractUsageFields(usage: unknown | null): UsageFields {
cachedTokens = record.prompt_cache_hit_tokens;
}
- return { promptTokens, completionTokens, cachedTokens };
+ const totalReqs = typeof record.total_reqs === "number" ? record.total_reqs : 0;
+
+ return { promptTokens, completionTokens, cachedTokens, totalReqs };
}
export function buildExitSummaryText(input: ExitSummaryInput): string {
- const { session, messages, model } = input;
-
- // Count assistant messages as the request count shown in the usage table.
- const assistantCount = messages.filter((m) => m.role === "assistant").length;
+ const { session } = input;
const innerWidth = 98;
const contentWidth = innerWidth - 4; // "│ " prefix + " │" suffix → 4 chars padding
@@ -81,9 +80,22 @@ export function buildExitSummaryText(input: ExitSummaryInput): string {
const rows: string[] = ["", `${header}`, ""];
- const usage = extractUsageFields(session?.usage ?? null);
- const modelName = model ?? "unknown";
- const hasUsage = usage.promptTokens > 0 || usage.completionTokens > 0;
+ const usageRows = Object.entries(session?.usagePerModel ?? {})
+ .map(([modelName, usage]) => ({
+ modelName,
+ usage: extractUsageFields(usage),
+ }))
+ .filter(
+ (row) =>
+ row.usage.totalReqs > 0 ||
+ row.usage.promptTokens > 0 ||
+ row.usage.completionTokens > 0 ||
+ row.usage.cachedTokens > 0
+ )
+ .sort(
+ (left, right) => right.usage.totalReqs - left.usage.totalReqs || left.modelName.localeCompare(right.modelName)
+ );
+ const hasUsage = usageRows.length > 0;
if (hasUsage) {
const colModel = 34;
@@ -103,17 +115,19 @@ export function buildExitSummaryText(input: ExitSummaryInput): string {
rows.push(chalk.bold(headerRow));
rows.push(divider);
- const reqsStr = String(assistantCount).padStart(colReqs);
- const inputStr = formatNumber(usage.promptTokens).padStart(colInput);
- const outputStr = formatNumber(usage.completionTokens).padStart(colOutput);
- const cachedStr = formatNumber(usage.cachedTokens).padStart(colCached);
- const dataRow =
- padRight(modelName, colModel) +
- padRight(reqsStr, colReqs) +
- padRight(chalk.yellow(inputStr), colInput) +
- padRight(chalk.yellow(outputStr), colOutput) +
- padRight(chalk.yellow(cachedStr), colCached);
- rows.push(dataRow);
+ for (const { modelName, usage } of usageRows) {
+ const reqsStr = formatNumber(usage.totalReqs).padStart(colReqs);
+ const inputStr = formatNumber(usage.promptTokens).padStart(colInput);
+ const outputStr = formatNumber(usage.completionTokens).padStart(colOutput);
+ const cachedStr = formatNumber(usage.cachedTokens).padStart(colCached);
+ const dataRow =
+ padRight(modelName, colModel) +
+ padRight(reqsStr, colReqs) +
+ padRight(chalk.yellow(inputStr), colInput) +
+ padRight(chalk.yellow(outputStr), colOutput) +
+ padRight(chalk.yellow(cachedStr), colCached);
+ rows.push(dataRow);
+ }
rows.push("");
}
From 0aa3f0dfbc2549e0c507a5e2e3f71a94aab145f8 Mon Sep 17 00:00:00 2001
From: Lellansin
Date: Sat, 16 May 2026 09:52:39 +0800
Subject: [PATCH 019/129] feat: add GitHub CI workflow with multi-version
Node.js and cross-platform matrix
---
.github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..11cf1a3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+jobs:
+ build-and-test:
+ name: Node ${{ matrix.node-version }} / ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ node-version:
+ - "18"
+ - "20"
+ - "22"
+ - "24"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: TypeCheck + Lint + Format Check
+ run: npm run check
+
+ - name: Bundle
+ run: npm run bundle
+
+ - name: Test
+ run: npm test
From 9fc53984975c15b726d56fe4e45b40cdb3170aac Mon Sep 17 00:00:00 2001
From: Lellansin
Date: Sat, 16 May 2026 09:55:37 +0800
Subject: [PATCH 020/129] fix(ci): trigger CI on all push events so PRs can
verify before merge
---
.github/workflows/ci.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 11cf1a3..0b214bf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,9 +1,8 @@
name: CI
on:
- pull_request:
- branches: [main]
push:
+ pull_request:
branches: [main]
jobs:
From 21a3add40b6ca57aed401aa0348d5bfd4815db09 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Sat, 16 May 2026 11:12:55 +0800
Subject: [PATCH 021/129] feat: align the project to Node >=22, because the
direct dependency ink@7.0.1 already declares node >=22.
---
.github/workflows/ci.yml | 2 --
package-lock.json | 2 +-
package.json | 2 +-
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0b214bf..de8600c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,8 +18,6 @@ jobs:
- windows-latest
- macos-latest
node-version:
- - "18"
- - "20"
- "22"
- "24"
diff --git a/package-lock.json b/package-lock.json
index dd029a3..17fa96f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,7 +40,7 @@
"typescript-eslint": "^8.59.2"
},
"engines": {
- "node": ">=18.17.0"
+ "node": ">=22"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
diff --git a/package.json b/package.json
index 0564936..5405b26 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"LICENSE"
],
"engines": {
- "node": ">=18.17.0"
+ "node": ">=22"
},
"scripts": {
"typecheck": "tsc -p ./ --noEmit",
From f1e696834d9f3a1eda2c88c9f6a093d13d6affbb Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 11:32:30 +0800
Subject: [PATCH 022/129] fix: add .gitattributes to enforce LF line endings on
Windows
---
.gitattributes | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 .gitattributes
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..18f5c60
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,12 @@
+# Normalize line endings to LF across all platforms
+* text=auto eol=lf
+
+# Binary files should not be normalized
+*.png binary
+*.jpg binary
+*.gif binary
+*.ico binary
+*.woff binary
+*.woff2 binary
+*.eot binary
+*.ttf binary
From f62b1ce2286ac86a7a604e49ac22a5b25b4b5a40 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 11:40:13 +0800
Subject: [PATCH 023/129] fix: resolve Windows CI failures (CRLF, MCP spawn,
test skips)
---
src/mcp/mcp-client.ts | 8 +--
src/tests/clipboard.test.ts | 72 ++++++++++++++------------
src/tests/settings-and-notify.test.ts | 74 ++++++++++++++-------------
src/tests/web-search-handler.test.ts | 68 ++++++++++++------------
src/tests/welcomeScreen.test.ts | 15 ++++--
5 files changed, 129 insertions(+), 108 deletions(-)
diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts
index f89e11d..9636732 100644
--- a/src/mcp/mcp-client.ts
+++ b/src/mcp/mcp-client.ts
@@ -128,10 +128,10 @@ export class McpClient {
const isWindows = os.platform() === "win32";
if (isWindows) {
- // On Windows, .cmd files require shell: true to be spawned.
- // Build a single command string so cmd.exe handles quoting correctly.
- const cmd = [this.command + ".cmd", ...args].join(" ");
- this.process = spawn(cmd, [], {
+ // On Windows, shell: true lets cmd.exe resolve the command via
+ // PATHEXT (npx → npx.cmd, etc.) without blindly appending .cmd,
+ // which would break absolute paths like process.execPath.
+ this.process = spawn(this.command, args, {
stdio: ["pipe", "pipe", "pipe"],
env: childEnv,
shell: true,
diff --git a/src/tests/clipboard.test.ts b/src/tests/clipboard.test.ts
index 022b2f8..dbe9ff9 100644
--- a/src/tests/clipboard.test.ts
+++ b/src/tests/clipboard.test.ts
@@ -36,40 +36,44 @@ test("readClipboardImage returns null when no clipboard helpers are installed",
assert.equal(result, null);
});
-test("readClipboardImage uses osascript fallback on macOS when pngpaste is missing", async () => {
- const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-clipboard-test-bin-"));
- try {
- fs.writeFileSync(path.join(binDir, "pngpaste"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
- fs.writeFileSync(
- path.join(binDir, "osascript"),
- [
- "#!/bin/sh",
- 'for arg in "$@"; do',
- ' case "$arg" in',
- " *'open for access POSIX file " + '"' + "'*)",
- ' path_part=${arg#*POSIX file \\"}',
- ' out_path=${path_part%%\\"*}',
- ' printf fakepng > "$out_path"',
- " exit 0",
- " ;;",
- " esac",
- "done",
- "exit 1",
- "",
- ].join("\n"),
- { mode: 0o755 }
- );
+test(
+ "readClipboardImage uses osascript fallback on macOS when pngpaste is missing",
+ { skip: process.platform === "win32" },
+ async () => {
+ const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-clipboard-test-bin-"));
+ try {
+ fs.writeFileSync(path.join(binDir, "pngpaste"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
+ fs.writeFileSync(
+ path.join(binDir, "osascript"),
+ [
+ "#!/bin/sh",
+ 'for arg in "$@"; do',
+ ' case "$arg" in',
+ " *'open for access POSIX file " + '"' + "'*)",
+ ' path_part=${arg#*POSIX file \\"}',
+ ' out_path=${path_part%%\\"*}',
+ ' printf fakepng > "$out_path"',
+ " exit 0",
+ " ;;",
+ " esac",
+ "done",
+ "exit 1",
+ "",
+ ].join("\n"),
+ { mode: 0o755 }
+ );
- const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href;
- const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule;
+ const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href;
+ const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule;
- process.env.PATH = binDir;
- const result = withPlatform("darwin", () => readClipboardImage());
- assert.equal(result?.mimeType, "image/png");
- assert.equal(result?.dataUrl, `data:image/png;base64,${Buffer.from("fakepng").toString("base64")}`);
- } finally {
- process.env.PATH = ORIGINAL_PATH;
- Object.defineProperty(process, "platform", { value: ORIGINAL_PLATFORM });
- fs.rmSync(binDir, { recursive: true, force: true });
+ process.env.PATH = binDir;
+ const result = withPlatform("darwin", () => readClipboardImage());
+ assert.equal(result?.mimeType, "image/png");
+ assert.equal(result?.dataUrl, `data:image/png;base64,${Buffer.from("fakepng").toString("base64")}`);
+ } finally {
+ process.env.PATH = ORIGINAL_PATH;
+ Object.defineProperty(process, "platform", { value: ORIGINAL_PLATFORM });
+ fs.rmSync(binDir, { recursive: true, force: true });
+ }
}
-});
+);
diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts
index 69b939f..6990288 100644
--- a/src/tests/settings-and-notify.test.ts
+++ b/src/tests/settings-and-notify.test.ts
@@ -364,39 +364,43 @@ test("buildNotifyEnv injects DURATION", () => {
assert.equal(env.DURATION, "2");
});
-test("launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts", () => {
- const calls: Array<{
- command: string;
- args: string[];
- options: { cwd?: string | URL; env?: NodeJS.ProcessEnv };
- }> = [];
-
- const spawnProcess: NotifySpawn = (command, args, options) => {
- calls.push({ command, args, options: { cwd: options.cwd, env: options.env } });
-
- return {
- once(event, listener) {
- if (event === "error" && calls.length === 1) {
- listener({ code: "EACCES" } as NodeJS.ErrnoException);
- }
- return this;
- },
- unref() {
- return undefined;
- },
+test(
+ "launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts",
+ { skip: process.platform === "win32" },
+ () => {
+ const calls: Array<{
+ command: string;
+ args: string[];
+ options: { cwd?: string | URL; env?: NodeJS.ProcessEnv };
+ }> = [];
+
+ const spawnProcess: NotifySpawn = (command, args, options) => {
+ calls.push({ command, args, options: { cwd: options.cwd, env: options.env } });
+
+ return {
+ once(event, listener) {
+ if (event === "error" && calls.length === 1) {
+ listener({ code: "EACCES" } as NodeJS.ErrnoException);
+ }
+ return this;
+ },
+ unref() {
+ return undefined;
+ },
+ };
};
- };
-
- launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" });
-
- assert.equal(calls.length, 2);
- assert.equal(calls[0]?.command, "/tmp/notify.sh");
- assert.deepEqual(calls[0]?.args, []);
- assert.equal(calls[0]?.options.cwd, "/tmp/project");
- assert.equal(calls[0]?.options.env?.DURATION, "2");
- assert.equal(calls[0]?.options.env?.WEBHOOK, "configured");
- assert.equal(calls[1]?.command, "/bin/sh");
- assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]);
- assert.equal(calls[1]?.options.cwd, "/tmp/project");
- assert.equal(calls[1]?.options.env?.DURATION, "2");
-});
+
+ launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" });
+
+ assert.equal(calls.length, 2);
+ assert.equal(calls[0]?.command, "/tmp/notify.sh");
+ assert.deepEqual(calls[0]?.args, []);
+ assert.equal(calls[0]?.options.cwd, "/tmp/project");
+ assert.equal(calls[0]?.options.env?.DURATION, "2");
+ assert.equal(calls[0]?.options.env?.WEBHOOK, "configured");
+ assert.equal(calls[1]?.command, "/bin/sh");
+ assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]);
+ assert.equal(calls[1]?.options.cwd, "/tmp/project");
+ assert.equal(calls[1]?.options.env?.DURATION, "2");
+ }
+);
diff --git a/src/tests/web-search-handler.test.ts b/src/tests/web-search-handler.test.ts
index 576a1cb..417c6c4 100644
--- a/src/tests/web-search-handler.test.ts
+++ b/src/tests/web-search-handler.test.ts
@@ -20,40 +20,44 @@ afterEach(() => {
}
});
-test("WebSearch executes the configured script with the query as one argument", async () => {
- const workspace = createTempWorkspace();
- const scriptPath = path.join(workspace, "web-search.sh");
- fs.writeFileSync(
- scriptPath,
- [
- "#!/bin/sh",
- "printf 'query=%s\\n' \"$1\"",
- "printf 'cwd=%s\\n' \"$PWD\"",
- "printf 'webhook=%s\\n' \"$WEBHOOK\"",
- ].join("\n"),
- "utf8"
- );
- fs.chmodSync(scriptPath, 0o755);
+test(
+ "WebSearch executes the configured script with the query as one argument",
+ { skip: process.platform === "win32" },
+ async () => {
+ const workspace = createTempWorkspace();
+ const scriptPath = path.join(workspace, "web-search.sh");
+ fs.writeFileSync(
+ scriptPath,
+ [
+ "#!/bin/sh",
+ "printf 'query=%s\\n' \"$1\"",
+ "printf 'cwd=%s\\n' \"$PWD\"",
+ "printf 'webhook=%s\\n' \"$WEBHOOK\"",
+ ].join("\n"),
+ "utf8"
+ );
+ fs.chmodSync(scriptPath, 0o755);
- const starts: Array<{ id: string | number; command: string }> = [];
- const exits: Array = [];
- const result = await handleWebSearchTool(
- { query: "latest node release" },
- createContext(workspace, {
- webSearchTool: scriptPath,
- env: { WEBHOOK: "configured" },
- onProcessStart: (id, command) => starts.push({ id, command }),
- onProcessExit: (id) => exits.push(id),
- })
- );
- const realWorkspace = fs.realpathSync(workspace);
+ const starts: Array<{ id: string | number; command: string }> = [];
+ const exits: Array = [];
+ const result = await handleWebSearchTool(
+ { query: "latest node release" },
+ createContext(workspace, {
+ webSearchTool: scriptPath,
+ env: { WEBHOOK: "configured" },
+ onProcessStart: (id, command) => starts.push({ id, command }),
+ onProcessExit: (id) => exits.push(id),
+ })
+ );
+ const realWorkspace = fs.realpathSync(workspace);
- assert.equal(result.ok, true);
- assert.equal(result.output, `query=latest node release\ncwd=${realWorkspace}\nwebhook=configured\n`);
- assert.equal(starts.length, 1);
- assert.match(starts[0].command, /^WebSearch: latest node release$/);
- assert.deepEqual(exits, [starts[0].id]);
-});
+ assert.equal(result.ok, true);
+ assert.equal(result.output, `query=latest node release\ncwd=${realWorkspace}\nwebhook=configured\n`);
+ assert.equal(starts.length, 1);
+ assert.match(starts[0].command, /^WebSearch: latest node release$/);
+ assert.deepEqual(exits, [starts[0].id]);
+ }
+);
test("WebSearch uses the default API when no script is configured", async () => {
const workspace = createTempWorkspace();
diff --git a/src/tests/welcomeScreen.test.ts b/src/tests/welcomeScreen.test.ts
index 1e5bc19..df7e109 100644
--- a/src/tests/welcomeScreen.test.ts
+++ b/src/tests/welcomeScreen.test.ts
@@ -1,17 +1,26 @@
import { test } from "node:test";
import assert from "node:assert/strict";
+import * as os from "os";
+import * as path from "path";
import { buildWelcomeTips, formatHomeRelativePath } from "../ui";
test("formatHomeRelativePath returns tilde for the home directory", () => {
- assert.equal(formatHomeRelativePath("/Users/example", "/Users/example"), "~");
+ const home = path.resolve("/Users/example");
+ assert.equal(formatHomeRelativePath(home, home), "~");
});
test("formatHomeRelativePath shortens paths inside the home directory", () => {
- assert.equal(formatHomeRelativePath("/Users/example/dev/project", "/Users/example"), "~/dev/project");
+ const home = path.resolve("/Users/example");
+ const result = formatHomeRelativePath(path.resolve("/Users/example/dev/project"), home);
+ assert.equal(result, `~${path.sep}dev${path.sep}project`);
});
test("formatHomeRelativePath keeps paths outside the home directory absolute", () => {
- assert.equal(formatHomeRelativePath("/tmp/project", "/Users/example"), "/tmp/project");
+ const home = path.resolve("/Users/example");
+ const other = path.resolve("/tmp/project");
+ // The result should be the absolute path since it's outside home
+ const result = formatHomeRelativePath(other, home);
+ assert.equal(result, other);
});
test("buildWelcomeTips includes built-in slash commands and loaded skills", () => {
From cdce3ca6470e0b5768cba308c8d44453fa30e440 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 11:43:22 +0800
Subject: [PATCH 024/129] fix: cross-platform homedir in tests, skip
Windows-incompatible npx test
---
src/tests/session.test.ts | 84 ++++++++++++++++++++++++---------------
1 file changed, 51 insertions(+), 33 deletions(-)
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index ff684a3..3dad6df 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -7,8 +7,17 @@ import { SessionManager, type SessionMessage } from "../session";
const originalFetch = globalThis.fetch;
const originalHome = process.env.HOME;
+const originalUserProfile = process.env.USERPROFILE;
const tempDirs: string[] = [];
+/** Set homedir in a cross-platform way (HOME on Unix, USERPROFILE on Windows). */
+function setHomeDir(dir: string): void {
+ process.env.HOME = dir;
+ if (process.platform === "win32") {
+ process.env.USERPROFILE = dir;
+ }
+}
+
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalHome === undefined) {
@@ -16,6 +25,11 @@ afterEach(() => {
} else {
process.env.HOME = originalHome;
}
+ if (originalUserProfile === undefined) {
+ delete process.env.USERPROFILE;
+ } else {
+ process.env.USERPROFILE = originalUserProfile;
+ }
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
@@ -249,7 +263,7 @@ test("SessionManager replays normal assistant messages with reasoning content in
test("SessionManager normalizes legacy sessions without activeTokens to zero", () => {
const workspace = createTempDir("deepcode-legacy-active-tokens-workspace-");
const home = createTempDir("deepcode-legacy-active-tokens-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, "");
const projectDir = path.join(home, ".deepcode", "projects", projectCode);
@@ -281,7 +295,7 @@ test("SessionManager normalizes legacy sessions without activeTokens to zero", (
test("SessionManager keeps usagePerModel null until response usage is available", async () => {
const workspace = createTempDir("deepcode-null-usage-per-model-workspace-");
const home = createTempDir("deepcode-null-usage-per-model-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const manager = createMockedClientSessionManager(workspace, [{ choices: [{ message: { content: "no usage" } }] }]);
@@ -294,7 +308,7 @@ test("SessionManager keeps usagePerModel null until response usage is available"
test("SessionManager marks skills loaded from existing session messages", async () => {
const workspace = createTempDir("deepcode-loaded-skills-workspace-");
const home = createTempDir("deepcode-loaded-skills-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const skillDir = path.join(home, ".agents", "skills", "lessweb-starter");
fs.mkdirSync(skillDir, { recursive: true });
@@ -341,7 +355,7 @@ test("SessionManager marks skills loaded from existing session messages", async
test("SessionManager lists project skills from .agents with legacy .deepcode compatibility", async () => {
const workspace = createTempDir("deepcode-project-skills-workspace-");
const home = createTempDir("deepcode-project-skills-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const userSkillDir = path.join(home, ".agents", "skills", "shared");
fs.mkdirSync(userSkillDir, { recursive: true });
@@ -514,13 +528,16 @@ test("SessionManager reports MCP startup stderr on failure", async () => {
assert.match(status?.error ?? "", /mcp startup boom/);
});
-test("SessionManager adds -y when launching MCP servers through npx", async () => {
- const workspace = createTempDir("deepcode-mcp-npx-workspace-");
- const argsPath = path.join(workspace, "args.json");
- const fakeNpxPath = path.join(workspace, "npx");
- fs.writeFileSync(
- fakeNpxPath,
- `#!/usr/bin/env node
+test(
+ "SessionManager adds -y when launching MCP servers through npx",
+ { skip: process.platform === "win32" },
+ async () => {
+ const workspace = createTempDir("deepcode-mcp-npx-workspace-");
+ const argsPath = path.join(workspace, "args.json");
+ const fakeNpxPath = path.join(workspace, "npx");
+ fs.writeFileSync(
+ fakeNpxPath,
+ `#!/usr/bin/env node
const fs = require("fs");
const readline = require("readline");
fs.writeFileSync(process.env.ARGS_PATH, JSON.stringify(process.argv.slice(2)));
@@ -544,23 +561,24 @@ rl.on("line", (line) => {
send({ jsonrpc: "2.0", id: request.id, result: { content: [] } });
});
`,
- "utf8"
- );
- fs.chmodSync(fakeNpxPath, 0o755);
+ "utf8"
+ );
+ fs.chmodSync(fakeNpxPath, 0o755);
- const manager = createSessionManager(workspace, "machine-id-mcp-npx");
- await manager.initMcpServers({
- npxed: { command: fakeNpxPath, args: ["@playwright/mcp@latest"], env: { ARGS_PATH: argsPath } },
- });
+ const manager = createSessionManager(workspace, "machine-id-mcp-npx");
+ await manager.initMcpServers({
+ npxed: { command: fakeNpxPath, args: ["@playwright/mcp@latest"], env: { ARGS_PATH: argsPath } },
+ });
- assert.deepEqual(JSON.parse(fs.readFileSync(argsPath, "utf8")) as string[], ["-y", "@playwright/mcp@latest"]);
- manager.dispose();
-});
+ assert.deepEqual(JSON.parse(fs.readFileSync(argsPath, "utf8")) as string[], ["-y", "@playwright/mcp@latest"]);
+ manager.dispose();
+ }
+);
test("createSession stores /init and sends the active .deepcode project AGENTS path to the LLM", async () => {
const workspace = createTempDir("deepcode-init-deepcode-workspace-");
const home = createTempDir("deepcode-init-deepcode-home-");
- process.env.HOME = home;
+ setHomeDir(home);
globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch;
fs.mkdirSync(path.join(workspace, ".deepcode"), { recursive: true });
@@ -592,7 +610,7 @@ test("createSession stores /init and sends the active .deepcode project AGENTS p
test("replySession stores /init and sends the active root project AGENTS path to the LLM", async () => {
const workspace = createTempDir("deepcode-init-root-workspace-");
const home = createTempDir("deepcode-init-root-home-");
- process.env.HOME = home;
+ setHomeDir(home);
globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch;
fs.writeFileSync(path.join(workspace, "AGENTS.md"), "root project instructions", "utf8");
@@ -619,7 +637,7 @@ test("replySession stores /init and sends the active root project AGENTS path to
test("createSession stores /init and sends generate prompt when no project AGENTS file is effective", async () => {
const workspace = createTempDir("deepcode-init-generate-workspace-");
const home = createTempDir("deepcode-init-generate-home-");
- process.env.HOME = home;
+ setHomeDir(home);
globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch;
fs.mkdirSync(path.join(home, ".deepcode"), { recursive: true });
@@ -645,7 +663,7 @@ test("createSession stores /init and sends generate prompt when no project AGENT
test("createSession reports a new prompt with the machineId token", async () => {
const workspace = createTempDir("deepcode-session-workspace-");
const home = createTempDir("deepcode-session-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const fetchCalls: Array<{ input: string | URL; init?: RequestInit }> = [];
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
@@ -677,7 +695,7 @@ test("createSession reports a new prompt with the machineId token", async () =>
test("replySession reports a new prompt with the machineId token", async () => {
const workspace = createTempDir("deepcode-reply-workspace-");
const home = createTempDir("deepcode-reply-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const fetchCalls: Array<{ input: string | URL; init?: RequestInit }> = [];
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
@@ -708,7 +726,7 @@ test("replySession reports a new prompt with the machineId token", async () => {
test("replySession preserves raw session messages when a previous tool call is pending", async () => {
const workspace = createTempDir("deepcode-pending-tool-workspace-");
const home = createTempDir("deepcode-pending-tool-home-");
- process.env.HOME = home;
+ setHomeDir(home);
globalThis.fetch = (async () =>
({
@@ -1131,7 +1149,7 @@ test("buildOpenAIMessages ignores tool messages that appear before their assista
test("SessionManager accumulates response usage while active tokens track the latest response", async () => {
const workspace = createTempDir("deepcode-usage-workspace-");
const home = createTempDir("deepcode-usage-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const responses = [
createChatResponse("first", {
@@ -1182,7 +1200,7 @@ test("SessionManager accumulates response usage while active tokens track the la
test("SessionManager stores usage per model across model changes", async () => {
const workspace = createTempDir("deepcode-usage-per-model-workspace-");
const home = createTempDir("deepcode-usage-per-model-home-");
- process.env.HOME = home;
+ setHomeDir(home);
let currentModel = "deepseek-v4-pro";
const responses = [
@@ -1243,7 +1261,7 @@ test("SessionManager stores usage per model across model changes", async () => {
test("SessionManager resets active tokens to latest post-compaction response usage", async () => {
const workspace = createTempDir("deepcode-compact-usage-workspace-");
const home = createTempDir("deepcode-compact-usage-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const responses = [
createChatResponse("large", {
@@ -1285,7 +1303,7 @@ test("SessionManager resets active tokens to latest post-compaction response usa
test("SessionManager streams chat completions and counts reasoning progress", async () => {
const workspace = createTempDir("deepcode-stream-workspace-");
const home = createTempDir("deepcode-stream-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const progressEvents: Array<{
phase: string;
@@ -1352,7 +1370,7 @@ test("SessionManager streams chat completions and counts reasoning progress", as
test("SessionManager cancels skill matching before a session is created", async () => {
const workspace = createTempDir("deepcode-skill-abort-workspace-");
const home = createTempDir("deepcode-skill-abort-home-");
- process.env.HOME = home;
+ setHomeDir(home);
const skillDir = path.join(home, ".agents", "skills", "demo");
fs.mkdirSync(skillDir, { recursive: true });
@@ -1384,7 +1402,7 @@ test("SessionManager cancels skill matching before a session is created", async
test("SessionManager treats OpenAI APIUserAbortError as interrupted", async () => {
const workspace = createTempDir("deepcode-api-abort-workspace-");
const home = createTempDir("deepcode-api-abort-home-");
- process.env.HOME = home;
+ setHomeDir(home);
let manager: SessionManager;
const client = {
From a21e907ddcd61fee119272dadd81bdf35d40f13a Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 11:57:36 +0800
Subject: [PATCH 025/129] fix: restore Node 20 in CI matrix
---
.github/workflows/ci.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index de8600c..4dc891f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,6 +18,7 @@ jobs:
- windows-latest
- macos-latest
node-version:
+ - "20"
- "22"
- "24"
From 64bf782fb3ebbbcc7964e7df413cf2bc22bb6559 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Sat, 16 May 2026 11:59:48 +0800
Subject: [PATCH 026/129] feat: update getCurrentDateAndModelPrompt
---
src/prompt.ts | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/prompt.ts b/src/prompt.ts
index b854860..4774725 100644
--- a/src/prompt.ts
+++ b/src/prompt.ts
@@ -281,14 +281,17 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s
return docs.join("\n\n");
}
-function getCurrentDatePrompt(date = new Date()): string {
- return `今天是${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日。随着对话的进行,时间在流逝。`;
+function getCurrentDateAndModelPrompt(model?: string): string {
+ const date = new Date();
+ let prompt = `今天是${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日。随着对话的进行,时间在流逝。`;
+ prompt += model ? `\n当前LLM模型为${model},对话中可通过/model命令切换模型。` : "";
+ return prompt;
}
export function getSystemPrompt(projectRoot: string, options: PromptToolOptions = {}): string {
const toolDocs = readToolDocs(getExtensionRoot(), options);
const basePrompt = toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE;
- return `${basePrompt}\n\n${getCurrentDatePrompt()}\n\n${getRuntimeContext(projectRoot)}`;
+ return `${basePrompt}\n\n${getCurrentDateAndModelPrompt(options.model)}\n\n${getRuntimeContext(projectRoot)}`;
}
export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
From cf56131cbbd5c57b2079730754d3fd658f6eae71 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:00:52 +0800
Subject: [PATCH 027/129] fix: quote test glob pattern for Node 20 Windows
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 5405b26..ae125e0 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
"check": "npm run typecheck && npm run lint && npm run format:check",
"build": "npm run check && npm run bundle && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
- "test": "tsx --test src/tests/*.test.ts",
+ "test": "tsx --test \"src/tests/*.test.ts\"",
"test:single": "tsx --test",
"prepack": "npm run build",
"prepare": "husky"
From 16e6810875d0b4782a6ee937b8e4df39712c1bb1 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:04:42 +0800
Subject: [PATCH 028/129] fix: add cross-platform test runner for Node 20
compatibility
---
package.json | 2 +-
src/tests/run-tests.mjs | 26 ++++++++++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
create mode 100644 src/tests/run-tests.mjs
diff --git a/package.json b/package.json
index ae125e0..aceb496 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
"check": "npm run typecheck && npm run lint && npm run format:check",
"build": "npm run check && npm run bundle && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
- "test": "tsx --test \"src/tests/*.test.ts\"",
+ "test": "node src/tests/run-tests.mjs",
"test:single": "tsx --test",
"prepack": "npm run build",
"prepare": "husky"
diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs
new file mode 100644
index 0000000..653ca66
--- /dev/null
+++ b/src/tests/run-tests.mjs
@@ -0,0 +1,26 @@
+// Cross-platform test runner: finds all *.test.ts files and runs them via tsx.
+// Needed because glob expansion in npm scripts behaves differently across
+// shells and Node versions (particularly Node 20 on Windows).
+/* eslint-disable */
+
+import { spawnSync } from "child_process";
+import { readdirSync } from "fs";
+import { fileURLToPath } from "url";
+import { dirname, join } from "path";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+const testFiles = readdirSync(__dirname)
+ .filter((f) => f.endsWith(".test.ts"))
+ .map((f) => join(__dirname, f))
+ .sort();
+
+// Resolve tsx from the project root
+const tsx = new URL("../../node_modules/.bin/tsx", import.meta.url).pathname;
+
+const result = spawnSync(process.execPath, [tsx, "--test", ...testFiles], {
+ stdio: "inherit",
+ cwd: join(__dirname, "../.."),
+});
+
+process.exit(result.status ?? 1);
From 9dba13688051cf2b8fffe0e9ed8221bd2ecc0464 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:07:06 +0800
Subject: [PATCH 029/129] fix: use npx for cross-platform tsx resolution in
test runner
---
src/tests/run-tests.mjs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs
index 653ca66..3a8465f 100644
--- a/src/tests/run-tests.mjs
+++ b/src/tests/run-tests.mjs
@@ -15,10 +15,7 @@ const testFiles = readdirSync(__dirname)
.map((f) => join(__dirname, f))
.sort();
-// Resolve tsx from the project root
-const tsx = new URL("../../node_modules/.bin/tsx", import.meta.url).pathname;
-
-const result = spawnSync(process.execPath, [tsx, "--test", ...testFiles], {
+const result = spawnSync("npx", ["--no-install", "tsx", "--test", ...testFiles], {
stdio: "inherit",
cwd: join(__dirname, "../.."),
});
From 20add1c8deaab468f1802f9d1b56bfe209b0942f Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:09:31 +0800
Subject: [PATCH 030/129] fix: resolve tsx binary path cross-platform in test
runner
---
src/tests/run-tests.mjs | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs
index 3a8465f..ed1fe16 100644
--- a/src/tests/run-tests.mjs
+++ b/src/tests/run-tests.mjs
@@ -4,20 +4,35 @@
/* eslint-disable */
import { spawnSync } from "child_process";
-import { readdirSync } from "fs";
+import { readdirSync, existsSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
+const projectRoot = join(__dirname, "../..");
const testFiles = readdirSync(__dirname)
.filter((f) => f.endsWith(".test.ts"))
.map((f) => join(__dirname, f))
.sort();
-const result = spawnSync("npx", ["--no-install", "tsx", "--test", ...testFiles], {
+// Cross-platform resolution of the local tsx binary
+function findTsx() {
+ const candidates = [
+ join(projectRoot, "node_modules", ".bin", "tsx"),
+ join(projectRoot, "node_modules", ".bin", "tsx.cmd"),
+ ];
+ for (const c of candidates) {
+ if (existsSync(c)) return c;
+ }
+ return candidates[0];
+}
+
+const tsx = findTsx();
+
+const result = spawnSync(tsx, ["--test", ...testFiles], {
stdio: "inherit",
- cwd: join(__dirname, "../.."),
+ cwd: projectRoot,
});
process.exit(result.status ?? 1);
From abe5190ccc11243e10c20a11e5209ccf04b813d3 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:12:54 +0800
Subject: [PATCH 031/129] fix: use node --import tsx --test with native glob
expansion for cross-platform
---
package.json | 2 +-
src/tests/run-tests.mjs | 38 --------------------------------------
2 files changed, 1 insertion(+), 39 deletions(-)
delete mode 100644 src/tests/run-tests.mjs
diff --git a/package.json b/package.json
index aceb496..1bff1df 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
"check": "npm run typecheck && npm run lint && npm run format:check",
"build": "npm run check && npm run bundle && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
- "test": "node src/tests/run-tests.mjs",
+ "test": "node --import tsx --test src/tests/*.test.ts",
"test:single": "tsx --test",
"prepack": "npm run build",
"prepare": "husky"
diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs
deleted file mode 100644
index ed1fe16..0000000
--- a/src/tests/run-tests.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Cross-platform test runner: finds all *.test.ts files and runs them via tsx.
-// Needed because glob expansion in npm scripts behaves differently across
-// shells and Node versions (particularly Node 20 on Windows).
-/* eslint-disable */
-
-import { spawnSync } from "child_process";
-import { readdirSync, existsSync } from "fs";
-import { fileURLToPath } from "url";
-import { dirname, join } from "path";
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const projectRoot = join(__dirname, "../..");
-
-const testFiles = readdirSync(__dirname)
- .filter((f) => f.endsWith(".test.ts"))
- .map((f) => join(__dirname, f))
- .sort();
-
-// Cross-platform resolution of the local tsx binary
-function findTsx() {
- const candidates = [
- join(projectRoot, "node_modules", ".bin", "tsx"),
- join(projectRoot, "node_modules", ".bin", "tsx.cmd"),
- ];
- for (const c of candidates) {
- if (existsSync(c)) return c;
- }
- return candidates[0];
-}
-
-const tsx = findTsx();
-
-const result = spawnSync(tsx, ["--test", ...testFiles], {
- stdio: "inherit",
- cwd: projectRoot,
-});
-
-process.exit(result.status ?? 1);
From d5da01e848a724534b22763daf6db1e64e7f191c Mon Sep 17 00:00:00 2001
From: lellansin
Date: Sat, 16 May 2026 12:15:53 +0800
Subject: [PATCH 032/129] fix: add glob package, use cross-platform test runner
---
package-lock.json | 95 +++++++++++++++++++++++++++++++++++++++++
package.json | 3 +-
src/tests/run-tests.mjs | 13 ++++++
3 files changed, 110 insertions(+), 1 deletion(-)
create mode 100644 src/tests/run-tests.mjs
diff --git a/package-lock.json b/package-lock.json
index 17fa96f..958eed7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
+ "glob": "^13.0.6",
"husky": "^9.1.7",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
@@ -2249,6 +2250,24 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2262,6 +2281,45 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
@@ -2911,6 +2969,16 @@
"node": "*"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -3060,6 +3128,33 @@
"node": ">=8"
}
},
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "11.3.6",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz",
+ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/package.json b/package.json
index 1bff1df..f1fd660 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
"check": "npm run typecheck && npm run lint && npm run format:check",
"build": "npm run check && npm run bundle && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
- "test": "node --import tsx --test src/tests/*.test.ts",
+ "test": "node src/tests/run-tests.mjs",
"test:single": "tsx --test",
"prepack": "npm run build",
"prepare": "husky"
@@ -58,6 +58,7 @@
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
+ "glob": "^13.0.6",
"husky": "^9.1.7",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs
new file mode 100644
index 0000000..4d09f5b
--- /dev/null
+++ b/src/tests/run-tests.mjs
@@ -0,0 +1,13 @@
+// Cross-platform test runner: finds all *.test.ts files and runs them via tsx.
+// Uses the glob package for reliable cross-platform pattern expansion (Node 20+).
+/* eslint-disable */
+
+import { globSync } from "glob";
+import { spawnSync } from "child_process";
+
+const cwd = new URL("../..", import.meta.url);
+const testFiles = globSync("src/tests/*.test.ts", { cwd });
+
+const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...testFiles], { stdio: "inherit", cwd });
+
+process.exit(result.status ?? 1);
From be6d66f19eaabfc370776336557abf414a89af79 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Sat, 16 May 2026 13:33:07 +0800
Subject: [PATCH 033/129] revert: undo PR #70
---
src/tests/promptInputKeys.test.ts | 6 ++--
src/ui/prompt/cursor.ts | 6 ++--
src/ui/prompt/useTerminalInput.ts | 56 +++----------------------------
3 files changed, 9 insertions(+), 59 deletions(-)
diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts
index 8952a3d..69d2075 100644
--- a/src/tests/promptInputKeys.test.ts
+++ b/src/tests/promptInputKeys.test.ts
@@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => {
test("parseTerminalInput recognizes shifted return sequences", () => {
const { input, key } = parseTerminalInput("\u001B\r");
- assert.equal(input, "");
+ assert.equal(input, "\r");
assert.equal(key.return, true);
assert.equal(key.shift, true);
assert.equal(key.meta, false);
@@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => {
});
test("terminal extended key helpers request and restore modifyOtherKeys mode", () => {
- assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u");
- assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[4;1m");
+ assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m");
});
test("parseTerminalInput recognizes terminal focus events", () => {
diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts
index 5eff1de..2668470 100644
--- a/src/ui/prompt/cursor.ts
+++ b/src/ui/prompt/cursor.ts
@@ -40,14 +40,12 @@ function disableTerminalFocusReporting(): string {
return "\u001B[?1004l";
}
-// xterm modifyOtherKeys + Kitty progressive enhancement.
-// Both are needed: some terminals (incl. Windows Terminal) only respond to Kitty.
export function enableTerminalExtendedKeys(): string {
- return "\u001B[>4;1m\u001B[>1u";
+ return "\u001B[>4;1m";
}
export function disableTerminalExtendedKeys(): string {
- return "\u001B[>4;0m\u001B[4;0m";
}
export function getPromptCursorPlacement(
diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts
index e6e0e28..8013ff6 100644
--- a/src/ui/prompt/useTerminalInput.ts
+++ b/src/ui/prompt/useTerminalInput.ts
@@ -26,55 +26,7 @@ const BACKSPACE_BYTES = new Set(["\u007F", "\b"]);
const FORWARD_DELETE_SEQUENCES = new Set(["\u001B[3~", "\u001B[P"]);
const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]);
const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]);
-// Known exact Shift+Enter sequences (both xterm modifyOtherKeys and Kitty protocol).
-const SHIFT_RETURN_SEQUENCES = new Set([
- "\u001B\r",
- "\u001B[13;2u",
- "\u001B[13;1u",
- "\u001B[13;2~",
- "\u001B[13;1~",
- "\u001B[27;2;13~",
- "\u001B[27;1;13~",
-]);
-
-// CSI u format: ESC [ keycode ; modifier u
-// CSI ~ format: ESC [ keycode ; modifier ~
-// Extended: ESC [ 27 ; modifier ; keycode ~
-const CSI_SHIFT_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
-const CSI_EXTENDED_SHIFT_RETURN_RE = /^\u001B\[27;(\d+);13~$/;
-
-// Check whether a raw sequence represents Shift+Enter by parsing the modifier
-// parameter dynamically. This handles terminals (e.g. Windows Terminal) that
-// set extra flags on the modifier (e.g. 130 = 128 + 2) while the existing
-// SHIFT_RETURN_SEQUENCES Set only covers the canonical values (2 and 1).
-function isShiftReturn(raw: string): boolean {
- if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
-
- let m: RegExpMatchArray | null;
- if ((m = raw.match(CSI_SHIFT_RETURN_RE)) !== null) {
- const mod = parseInt(m[1], 10);
- // xterm: Shift=2 (bit 1); Kitty: Shift=1 (bit 0)
- return (mod & 2) !== 0 || (mod & 1) !== 0;
- }
- if ((m = raw.match(CSI_EXTENDED_SHIFT_RETURN_RE)) !== null) {
- const mod = parseInt(m[1], 10);
- return (mod & 2) !== 0 || (mod & 1) !== 0;
- }
- return false;
-}
-
-// Any CSI sequence with keycode=13 (Enter) — with or without modifiers.
-// Kitty progressive enhancement (ESC[>1u) sends plain Enter as ESC[13u
-// or ESC[13;NUMBERu with extra flags; xterm sends ESC[13;2u for Shift.
-const CSI_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
-const CSI_EXTENDED_RETURN_RE = /^\u001B\[27;(\d+);13~$/;
-
-function isReturn(raw: string): boolean {
- if (raw === "\r") return true;
- if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
- if (META_RETURN_SEQUENCES.has(raw)) return true;
- return CSI_RETURN_RE.test(raw) || CSI_EXTENDED_RETURN_RE.test(raw);
-}
+const SHIFT_RETURN_SEQUENCES = new Set(["\u001B\r", "\u001B[13;2u", "\u001B[13;2~", "\u001B[27;2;13~"]);
const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]);
const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]);
const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]);
@@ -161,10 +113,10 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
end: END_SEQUENCES.has(raw),
pageDown: raw === "\u001B[6~",
pageUp: raw === "\u001B[5~",
- return: isReturn(raw),
+ return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw),
escape: raw === "\u001B",
ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw),
- shift: isShiftReturn(raw),
+ shift: SHIFT_RETURN_SEQUENCES.has(raw),
tab: raw === "\t" || raw === "\u001B[Z",
backspace: BACKSPACE_BYTES.has(raw),
delete: FORWARD_DELETE_SEQUENCES.has(raw),
@@ -210,7 +162,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
key.shift = true;
}
- if (key.tab || key.backspace || key.delete || key.return) {
+ if (key.tab || key.backspace || key.delete) {
input = "";
}
From 5e3540c2d503b0bfb3d6405b8b37832363e74226 Mon Sep 17 00:00:00 2001
From: Lellansin
Date: Sat, 16 May 2026 15:20:08 +0800
Subject: [PATCH 034/129] feat: add Ctrl+O to view live process stdout in
fullscreen overlay
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add onProcessStdout callback chain through executor → bash-handler → session → App
- Stream real-time stdout/stderr from bash commands to UI ref (capped at 1MB)
- Add ProcessStdoutView fullscreen overlay with scroll support
- Bind Ctrl+O in PromptInput to toggle the stdout view
- Footer hint shows 'ctrl+o view output' when a process is running
---
package-lock.json | 814 +++++++----------------------------
src/session.ts | 4 +
src/tools/bash-handler.ts | 4 +
src/tools/executor.ts | 3 +
src/ui/App.tsx | 37 +-
src/ui/ProcessStdoutView.tsx | 109 +++++
src/ui/PromptInput.tsx | 21 +-
7 files changed, 327 insertions(+), 665 deletions(-)
create mode 100644 src/ui/ProcessStdoutView.tsx
diff --git a/package-lock.json b/package-lock.json
index 958eed7..f9caecd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -846,13 +846,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -863,19 +856,6 @@
"node": ">= 4"
}
},
- "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/@eslint/js": {
"version": "9.39.4",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz",
@@ -1060,13 +1040,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.6.0",
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz",
- "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "version": "25.8.0",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.8.0.tgz",
+ "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.19.0"
+ "undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/react": {
@@ -1086,17 +1066,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
- "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
+ "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/type-utils": "8.59.2",
- "@typescript-eslint/utils": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/type-utils": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1109,22 +1089,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.59.2",
+ "@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz",
- "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.3.tgz",
+ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1140,14 +1120,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
- "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
+ "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.59.2",
- "@typescript-eslint/types": "^8.59.2",
+ "@typescript-eslint/tsconfig-utils": "^8.59.3",
+ "@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1162,14 +1142,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
- "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
+ "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2"
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1180,9 +1160,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
- "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
+ "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1197,15 +1177,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
- "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
+ "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2",
- "@typescript-eslint/utils": "8.59.2",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1222,9 +1202,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz",
- "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz",
+ "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1236,16 +1216,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
- "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
+ "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.59.2",
- "@typescript-eslint/tsconfig-utils": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/project-service": "8.59.3",
+ "@typescript-eslint/tsconfig-utils": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1316,16 +1296,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz",
- "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz",
+ "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2"
+ "@typescript-eslint/scope-manager": "8.59.3",
+ "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1340,13 +1320,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
- "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
+ "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1450,13 +1430,11 @@
}
},
"node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
},
"node_modules/auto-bind": {
"version": "5.0.1",
@@ -1725,7 +1703,7 @@
},
"node_modules/ejs": {
"version": "5.0.2",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz",
+ "resolved": "https://registry.npmmirror.com/ejs/-/ejs-5.0.2.tgz",
"integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==",
"license": "Apache-2.0",
"bin": {
@@ -1736,9 +1714,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.353",
- "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
- "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
+ "version": "1.5.356",
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz",
+ "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==",
"dev": true,
"license": "ISC"
},
@@ -1824,12 +1802,16 @@
}
},
"node_modules/escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
@@ -1991,19 +1973,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/eslint/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/eslint/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -2226,9 +2195,9 @@
}
},
"node_modules/get-east-asian-width": {
- "version": "1.5.0",
- "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
- "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2237,22 +2206,9 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/get-tsconfig": {
- "version": "4.14.0",
- "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
- "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
"node_modules/glob": {
"version": "13.0.6",
- "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2283,7 +2239,7 @@
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
- "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
@@ -2293,7 +2249,7 @@
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.6",
- "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
@@ -2306,7 +2262,7 @@
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.5",
- "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2361,6 +2317,28 @@
"node": ">=6.0"
}
},
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -2453,9 +2431,9 @@
}
},
"node_modules/ink": {
- "version": "7.0.1",
- "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.1.tgz",
- "integrity": "sha512-o6LAC268PLawlGVYrXTyaTfke4VtJftEheuwbgkQf7yvSXyWp1nRwBbAyKEkWXFZZsW/la5wrMuNbuBvZK2C1w==",
+ "version": "7.0.3",
+ "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.3.tgz",
+ "integrity": "sha512-5kxHkIj9+RuqCU3zyvP4qvYWNOSHP2TW/SHayHGHOmk87KwfVcZwvJGemi9ch+ci2gXUqerK/Eh2DGEDt5q45g==",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0",
@@ -2598,13 +2576,13 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.14.2",
- "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz",
- "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
+ "argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
@@ -2971,7 +2949,7 @@
},
"node_modules/minipass": {
"version": "7.1.3",
- "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2994,9 +2972,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.38",
- "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz",
- "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "version": "2.0.44",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
"dev": true,
"license": "MIT"
},
@@ -3016,9 +2994,9 @@
}
},
"node_modules/openai": {
- "version": "6.35.0",
- "resolved": "https://registry.npmmirror.com/openai/-/openai-6.35.0.tgz",
- "integrity": "sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==",
+ "version": "6.37.0",
+ "resolved": "https://registry.npmmirror.com/openai/-/openai-6.37.0.tgz",
+ "integrity": "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
@@ -3130,7 +3108,7 @@
},
"node_modules/path-scurry": {
"version": "2.0.2",
- "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -3147,7 +3125,7 @@
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.3.6",
- "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -3212,9 +3190,9 @@
}
},
"node_modules/react": {
- "version": "19.2.5",
- "resolved": "https://registry.npmmirror.com/react/-/react-19.2.5.tgz",
- "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "version": "19.2.6",
+ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3245,16 +3223,6 @@
"node": ">=4"
}
},
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz",
@@ -3370,6 +3338,15 @@
"node": ">=10"
}
},
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz",
@@ -3527,14 +3504,13 @@
}
},
"node_modules/tsx": {
- "version": "4.21.0",
- "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz",
- "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "version": "4.22.0",
+ "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.0.tgz",
+ "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "~0.27.0",
- "get-tsconfig": "^4.7.5"
+ "esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
@@ -3546,490 +3522,6 @@
"fsevents": "~2.3.3"
}
},
- "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
- "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
- "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
- "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
- "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
- "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
- "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
- "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
- "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
- "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
- "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
- "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
- "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
- "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
- "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
- "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
- "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
- "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
- "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
- "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
- "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
- "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
- "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
- "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
- "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/esbuild": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz",
- "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.7",
- "@esbuild/android-arm": "0.27.7",
- "@esbuild/android-arm64": "0.27.7",
- "@esbuild/android-x64": "0.27.7",
- "@esbuild/darwin-arm64": "0.27.7",
- "@esbuild/darwin-x64": "0.27.7",
- "@esbuild/freebsd-arm64": "0.27.7",
- "@esbuild/freebsd-x64": "0.27.7",
- "@esbuild/linux-arm": "0.27.7",
- "@esbuild/linux-arm64": "0.27.7",
- "@esbuild/linux-ia32": "0.27.7",
- "@esbuild/linux-loong64": "0.27.7",
- "@esbuild/linux-mips64el": "0.27.7",
- "@esbuild/linux-ppc64": "0.27.7",
- "@esbuild/linux-riscv64": "0.27.7",
- "@esbuild/linux-s390x": "0.27.7",
- "@esbuild/linux-x64": "0.27.7",
- "@esbuild/netbsd-arm64": "0.27.7",
- "@esbuild/netbsd-x64": "0.27.7",
- "@esbuild/openbsd-arm64": "0.27.7",
- "@esbuild/openbsd-x64": "0.27.7",
- "@esbuild/openharmony-arm64": "0.27.7",
- "@esbuild/sunos-x64": "0.27.7",
- "@esbuild/win32-arm64": "0.27.7",
- "@esbuild/win32-ia32": "0.27.7",
- "@esbuild/win32-x64": "0.27.7"
- }
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
@@ -4073,16 +3565,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.59.2",
- "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz",
- "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==",
+ "version": "8.59.3",
+ "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
+ "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.59.2",
- "@typescript-eslint/parser": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2",
- "@typescript-eslint/utils": "8.59.2"
+ "@typescript-eslint/eslint-plugin": "8.59.3",
+ "@typescript-eslint/parser": "8.59.3",
+ "@typescript-eslint/typescript-estree": "8.59.3",
+ "@typescript-eslint/utils": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4097,9 +3589,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.19.2",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz",
- "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
@@ -4203,9 +3695,9 @@
}
},
"node_modules/ws": {
- "version": "8.20.0",
- "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz",
- "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "version": "8.20.1",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -4231,9 +3723,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.4",
- "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
- "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"dev": true,
"license": "ISC",
"optional": true,
diff --git a/src/session.ts b/src/session.ts
index 8c078f3..6da8835 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -197,6 +197,7 @@ type SessionManagerOptions = {
onSessionEntryUpdated?: (entry: SessionEntry) => void;
onLlmStreamProgress?: (progress: LlmStreamProgress) => void;
onMcpStatusChanged?: () => void;
+ onProcessStdout?: (pid: number, chunk: string) => void;
};
export type LlmStreamProgress = {
@@ -220,6 +221,7 @@ export class SessionManager {
private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void;
private readonly onLlmStreamProgress?: (progress: LlmStreamProgress) => void;
private readonly onMcpStatusChanged?: () => void;
+ private readonly onProcessStdout?: (pid: number, chunk: string) => void;
private activeSessionId: string | null = null;
private activePromptController: AbortController | null = null;
private readonly sessionControllers = new Map();
@@ -235,6 +237,7 @@ export class SessionManager {
this.onSessionEntryUpdated = options.onSessionEntryUpdated;
this.onLlmStreamProgress = options.onLlmStreamProgress;
this.onMcpStatusChanged = options.onMcpStatusChanged;
+ this.onProcessStdout = options.onProcessStdout;
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager);
this.mcpManager.prepare(this.getResolvedSettings().mcpServers);
}
@@ -1699,6 +1702,7 @@ ${skillMd}
const toolExecutions = await this.toolExecutor.executeToolCalls(sessionId, toolCalls, {
onProcessStart: (pid, command) => this.addSessionProcess(sessionId, pid, command),
onProcessExit: (pid) => this.removeSessionProcess(sessionId, pid),
+ onProcessStdout: (pid, chunk) => this.onProcessStdout?.(Number(pid), chunk),
shouldStop: () => this.isInterrupted(sessionId),
});
if (this.isInterrupted(sessionId)) {
diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts
index 95e7e76..071da53 100644
--- a/src/tools/bash-handler.ts
+++ b/src/tools/bash-handler.ts
@@ -124,9 +124,13 @@ async function executeShellCommand(
child.stdout?.on("data", (chunk: string | Buffer) => {
stdout = appendChunk(stdout, chunk);
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
+ context.onProcessStdout?.(pid as number, text);
});
child.stderr?.on("data", (chunk: string | Buffer) => {
stderr = appendChunk(stderr, chunk);
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
+ context.onProcessStdout?.(pid as number, text);
});
child.on("error", (spawnError) => {
diff --git a/src/tools/executor.ts b/src/tools/executor.ts
index bc2d7d8..e6018d9 100644
--- a/src/tools/executor.ts
+++ b/src/tools/executor.ts
@@ -37,11 +37,13 @@ export type ToolExecutionContext = {
createOpenAIClient?: CreateOpenAIClient;
onProcessStart?: (processId: string | number, command: string) => void;
onProcessExit?: (processId: string | number) => void;
+ onProcessStdout?: (processId: string | number, chunk: string) => void;
};
export type ToolExecutionHooks = {
onProcessStart?: (processId: string | number, command: string) => void;
onProcessExit?: (processId: string | number) => void;
+ onProcessStdout?: (processId: string | number, chunk: string) => void;
shouldStop?: () => boolean;
};
@@ -195,6 +197,7 @@ export class ToolExecutor {
createOpenAIClient: this.createOpenAIClient,
onProcessStart: hooks?.onProcessStart,
onProcessExit: hooks?.onProcessExit,
+ onProcessStdout: hooks?.onProcessStdout,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 8c5c375..c864187 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -30,6 +30,7 @@ import { findExpandedThinkingId } from "./thinkingState";
import { WelcomeScreen } from "./WelcomeScreen";
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
import { McpStatusList } from "./McpStatusList";
+import { ProcessStdoutView } from "./ProcessStdoutView";
import {
findPendingAskUserQuestion,
formatAskUserQuestionAnswers,
@@ -69,6 +70,8 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
const [resolvedSettings, setResolvedSettings] = useState(() => resolveCurrentSettings(projectRoot));
const [nowTick, setNowTick] = useState(0);
const [mcpStatuses, setMcpStatuses] = useState>([]);
+ const [showProcessStdout, setShowProcessStdout] = useState(false);
+ const processStdoutRef = useRef
) : null}
- {view === "session-list" ? (
+ {showProcessStdout ? (
+
+ ) : view === "session-list" ? (
void handleSelectSession(id)}
@@ -464,9 +497,11 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R
promptHistory={promptHistory}
busy={busy}
loadingText={loadingText}
+ runningProcesses={runningProcesses}
onSubmit={handleSubmit}
onModelConfigChange={handleModelConfigChange}
onInterrupt={handleInterrupt}
+ onToggleProcessStdout={handleToggleProcessStdout}
placeholder="Type your message..."
/>
)}
diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/ProcessStdoutView.tsx
new file mode 100644
index 0000000..a0676c6
--- /dev/null
+++ b/src/ui/ProcessStdoutView.tsx
@@ -0,0 +1,109 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { Box, Text } from "ink";
+import type { SessionEntry } from "../session";
+import { useTerminalInput } from "./prompt";
+
+type RunningProcesses = SessionEntry["processes"];
+
+type ProcessStdoutViewProps = {
+ processStdoutRef: React.MutableRefObject
- {/* Error message for failed servers */}
- {status.status === "failed" && status.error ? : null}
+ {/* Error message for failed or reconnecting servers */}
+ {(status.status === "failed" || status.status === "reconnecting") && status.error ? (
+
+ ) : null}
);
}
@@ -304,59 +324,54 @@ function ServerDetailView({
server,
onBack,
onCancel,
+ onReconnect,
rows,
columns,
}: {
server: McpServerStatus;
onBack: () => void;
onCancel: () => void;
+ onReconnect: (name: string) => void;
rows: number;
columns: number;
}): React.ReactElement {
- const [activeIndex, setActiveIndex] = useState(0);
+ const [activeIndex, setActiveIndex] = React.useState(0);
+ const hasReconnect = server.status === "failed";
+ const canScroll = server.status === "ready";
- // 合并所有 items(tools, prompts, resources)
+ // 合并所有 items(tools, prompts, resources)+ Reconnect 选项
const allItems = useMemo(() => {
const items: { type: string; name: string }[] = [];
+ if (hasReconnect) {
+ items.push({ type: "action", name: "Reconnect" });
+ }
server.tools.forEach((tool) => items.push({ type: "tool", name: tool }));
server.prompts.forEach((prompt) => items.push({ type: "prompt", name: prompt }));
server.resources.forEach((resource) => items.push({ type: "resource", name: resource }));
return items;
- }, [server]);
+ }, [server, hasReconnect]);
const totalItems = allItems.length;
const maxVisible = useMemo(() => {
- const reservedLines = 10; // header + title + stats + footer + borders
+ const reservedLines = 12; // header + title + stats + error + footer + borders
const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines);
return Math.max(1, availableLines);
}, [rows]);
- // 使用 ref 跟踪 visibleStart,避免循环依赖
const visibleStartRef = React.useRef(0);
- // 计算可见窗口起始位置:当 activeIndex 超出可见区域时才滚动(类似终端光标行为)
const visibleStart = useMemo(() => {
if (totalItems === 0) return 0;
-
const currentStart = visibleStartRef.current;
let newStart = currentStart;
-
- // 如果 activeIndex 在当前可见窗口之前,滚动到 activeIndex
if (activeIndex < currentStart) {
newStart = activeIndex;
- }
- // 如果 activeIndex 在当前可见窗口之后,滚动到 activeIndex
- else if (activeIndex >= currentStart + maxVisible) {
+ } else if (activeIndex >= currentStart + maxVisible) {
newStart = activeIndex - maxVisible + 1;
}
-
- // 限制在合法范围内
newStart = Math.max(0, Math.min(newStart, Math.max(0, totalItems - maxVisible)));
-
- // 更新 ref
visibleStartRef.current = newStart;
-
return newStart;
}, [activeIndex, maxVisible, totalItems]);
@@ -371,11 +386,16 @@ function ServerDetailView({
onBack();
return;
}
- // Space 或 Enter 键返回一级菜单
- if (input === " " || key.return) {
+ if (key.return || input === " ") {
+ if (activeIndex === 0 && hasReconnect) {
+ onReconnect(server.name);
+ onBack();
+ return;
+ }
onBack();
return;
}
+ if (!canScroll && !hasReconnect) return;
if (key.upArrow) {
setActiveIndex((prev) => Math.max(0, prev - 1));
return;
@@ -384,25 +404,33 @@ function ServerDetailView({
setActiveIndex((prev) => Math.min(totalItems - 1, prev + 1));
return;
}
- if (key.pageUp) {
+ if (key.pageUp && canScroll) {
setActiveIndex((prev) => Math.max(0, prev - maxVisible));
return;
}
- if (key.pageDown) {
+ if (key.pageDown && canScroll) {
setActiveIndex((prev) => Math.min(totalItems - 1, prev + maxVisible));
return;
}
- if (key.home) {
+ if (key.home && canScroll) {
setActiveIndex(0);
return;
}
- if (key.end) {
+ if (key.end && canScroll) {
setActiveIndex(totalItems - 1);
}
});
- const icon = "✓";
- const color = "green";
+ const statusIcon =
+ server.status === "ready" ? "✓" : server.status === "failed" ? "✗" : server.status === "reconnecting" ? "↻" : "●";
+ const statusColor =
+ server.status === "ready"
+ ? "green"
+ : server.status === "failed"
+ ? "red"
+ : server.status === "reconnecting"
+ ? "#ff9900"
+ : "yellow";
return (
{/* Header row */}
- {icon}
+ {statusIcon}
{server.name}
- — Details
+ — {server.status === "ready" ? "Details" : "Status"}
{/* Server info */}
- {server.toolCount} tools, {server.promptCount} prompts, {server.resourceCount} resources
+ {server.status === "ready"
+ ? `${server.toolCount} tools, ${server.promptCount} prompts, ${server.resourceCount} resources`
+ : `Status: ${server.status}`}
+ {/* Error for failed/reconnecting */}
+ {server.error && (server.status === "failed" || server.status === "reconnecting") ? (
+
+
+
+ ) : null}
{/* Items list */}
{/* Footer */}
- ↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close
+
+ {hasReconnect
+ ? "Enter to reconnect · Esc back · Ctrl+C close"
+ : canScroll
+ ? "↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close"
+ : "Space/Enter back · Esc back · Ctrl+C close"}
+
@@ -481,13 +523,16 @@ function ServerDetailView({
}
function ItemRow({ item, selected }: { item: { type: string; name: string }; selected: boolean }): React.ReactElement {
- const icon = item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦";
+ const isAction = item.type === "action";
+ const icon = isAction ? "↻" : item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦";
+ const color = isAction && selected ? "#ff9900" : selected ? "#229ac3" : undefined;
return (
+ {selected ? "> " : " "}
{icon}
-
- {item.name}
+
+ {isAction ? `[${item.name}]` : item.name}
);
From 2b509715fec54287cbc872c078e09ebb8985c2ad Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Sun, 17 May 2026 10:31:18 +0800
Subject: [PATCH 039/129] fix: the -p/--prompt option causes duplicated LLM
calls when using /new.
---
src/cli.tsx | 6 ++++--
src/session.ts | 19 +++++++------------
src/tests/session.test.ts | 26 ++++++++++++++++++++++++++
src/ui/App.tsx | 15 ++++++++++++++-
src/ui/PromptInput.tsx | 19 +------------------
5 files changed, 52 insertions(+), 33 deletions(-)
diff --git a/src/cli.tsx b/src/cli.tsx
index 5f5ccb2..435499a 100644
--- a/src/cli.tsx
+++ b/src/cli.tsx
@@ -60,7 +60,7 @@ function extractInitialPrompt(args: string[]): string | undefined {
return undefined;
}
-const initialPrompt = extractInitialPrompt(args);
+let initialPrompt = extractInitialPrompt(args);
const projectRoot = process.cwd();
configureWindowsShell();
@@ -78,11 +78,13 @@ async function main(): Promise {
function startApp(): void {
let restarting = false;
+ const appInitialPrompt = initialPrompt;
+ initialPrompt = undefined;
const inkInstance = render(
restartRef.current?.()}
/>,
{ exitOnCtrlC: false }
diff --git a/src/session.ts b/src/session.ts
index 095cd3a..5d6ee26 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -18,6 +18,7 @@ import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debu
const MAX_SESSION_ENTRIES = 50;
const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new";
+const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000;
const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024;
const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024;
@@ -1305,6 +1306,9 @@ ${skillMd}
return;
}
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), NEW_PROMPT_REPORT_TIMEOUT_MS);
+
void fetch(DEFAULT_NEW_PROMPT_API_URL, {
method: "POST",
headers: {
@@ -1312,19 +1316,10 @@ ${skillMd}
Token: machineId,
},
body: JSON.stringify({}),
+ signal: controller.signal,
})
- .then(async (response) => {
- if (response.ok) {
- return;
- }
-
- const body = await response.text().catch(() => "");
- throw new Error(`New prompt API request failed with status ${response.status}${body ? `: ${body}` : ""}`);
- })
- .catch((error) => {
- const message = error instanceof Error ? error.message : String(error);
- console.warn(`Failed to report new prompt: ${message}`);
- });
+ .catch(() => {})
+ .finally(() => clearTimeout(timeout));
}
interruptActiveSession(): void {
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index 50d016c..e5ab740 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -6,6 +6,7 @@ import * as path from "path";
import { SessionManager, type SessionMessage } from "../session";
const originalFetch = globalThis.fetch;
+const originalConsoleWarn = console.warn;
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;
const tempDirs: string[] = [];
@@ -20,6 +21,7 @@ function setHomeDir(dir: string): void {
afterEach(() => {
globalThis.fetch = originalFetch;
+ console.warn = originalConsoleWarn;
if (originalHome === undefined) {
delete process.env.HOME;
} else {
@@ -688,6 +690,7 @@ test("createSession reports a new prompt with the machineId token", async () =>
assert.equal(fetchCalls.length, 1);
assert.equal(String(fetchCalls[0].input), "https://deepcode.vegamo.cn/api/plugin/new");
assert.equal(fetchCalls[0].init?.method, "POST");
+ assert.ok(fetchCalls[0].init?.signal instanceof AbortSignal);
assert.deepEqual(JSON.parse(String(fetchCalls[0].init?.body)), {});
assert.equal((fetchCalls[0].init?.headers as Record).Token, "machine-id-123");
});
@@ -719,10 +722,33 @@ test("replySession reports a new prompt with the machineId token", async () => {
assert.equal(fetchCalls.length, 1);
assert.equal(String(fetchCalls[0].input), "https://deepcode.vegamo.cn/api/plugin/new");
assert.equal(fetchCalls[0].init?.method, "POST");
+ assert.ok(fetchCalls[0].init?.signal instanceof AbortSignal);
assert.deepEqual(JSON.parse(String(fetchCalls[0].init?.body)), {});
assert.equal((fetchCalls[0].init?.headers as Record).Token, "machine-id-456");
});
+test("reporting a new prompt does not warn when the background request fails", async () => {
+ const workspace = createTempDir("deepcode-report-failure-workspace-");
+ const home = createTempDir("deepcode-report-failure-home-");
+ setHomeDir(home);
+
+ const warnings: unknown[][] = [];
+ console.warn = (...args: unknown[]) => {
+ warnings.push(args);
+ };
+ globalThis.fetch = (async () => {
+ throw new Error("fetch failed");
+ }) as typeof fetch;
+
+ const manager = createSessionManager(workspace, "machine-id-failure");
+ (manager as any).activateSession = async () => {};
+
+ await manager.createSession({ text: "hello world" });
+ await flushPromises();
+
+ assert.deepEqual(warnings, []);
+});
+
test("replySession continues without appending /continue as a user message", async () => {
const workspace = createTempDir("deepcode-continue-workspace-");
const home = createTempDir("deepcode-continue-home-");
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 1b60618..b416647 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -53,6 +53,7 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
const { exit } = useApp();
const { stdout, write } = useStdout();
const { columns } = useWindowSize();
+ const initialPromptSubmittedRef = useRef(false);
const [view, setView] = useState("chat");
const [busy, setBusy] = useState(false);
const [skills, setSkills] = useState([]);
@@ -296,6 +297,19 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
[handlePrompt]
);
+ useEffect(() => {
+ if (initialPromptSubmittedRef.current || !initialPrompt || !initialPrompt.trim()) {
+ return;
+ }
+
+ initialPromptSubmittedRef.current = true;
+ handleSubmit({
+ text: initialPrompt,
+ imageUrls: [],
+ selectedSkills: undefined,
+ });
+ }, [handleSubmit, initialPrompt]);
+
const handleSelectSession = useCallback(
async (sessionId: string) => {
const currentSessionId = sessionManager.getActiveSessionId();
@@ -471,7 +485,6 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
promptHistory={promptHistory}
busy={busy}
loadingText={loadingText}
- initialPrompt={initialPrompt}
onSubmit={handleSubmit}
onModelConfigChange={handleModelConfigChange}
onInterrupt={handleInterrupt}
diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx
index 353a827..b32d926 100644
--- a/src/ui/PromptInput.tsx
+++ b/src/ui/PromptInput.tsx
@@ -60,7 +60,6 @@ type Props = {
loadingText?: string | null;
disabled?: boolean;
placeholder?: string;
- initialPrompt?: string;
onSubmit: (submission: PromptSubmission) => void;
onModelConfigChange: (selection: ModelConfigSelection) => string | Promise;
onInterrupt: () => void;
@@ -110,16 +109,13 @@ export const PromptInput = React.memo(function PromptInput({
loadingText,
disabled,
placeholder,
- initialPrompt,
onSubmit,
onModelConfigChange,
onInterrupt,
}: Props): React.ReactElement {
const { exit } = useApp();
const { stdout } = useStdout();
- const [buffer, setBuffer] = useState(() =>
- initialPrompt ? { text: initialPrompt, cursor: initialPrompt.length } : EMPTY_BUFFER
- );
+ const [buffer, setBuffer] = useState(EMPTY_BUFFER);
const [imageUrls, setImageUrls] = useState([]);
const [selectedSkills, setSelectedSkills] = useState([]);
const [statusMessage, setStatusMessage] = useState(null);
@@ -196,19 +192,6 @@ export const PromptInput = React.memo(function PromptInput({
setDraftBeforeHistory(null);
}, [promptHistoryKey]);
- // Auto-submit initial prompt provided via -p/--prompt CLI flag
- useEffect(() => {
- if (!initialPrompt || !initialPrompt.trim()) return;
-
- onSubmit({
- text: initialPrompt,
- imageUrls: [],
- selectedSkills: undefined,
- });
- setBuffer(EMPTY_BUFFER);
- clearPromptUndoRedoState(undoRedoRef.current);
- }, []); // Only on mount
-
useTerminalInput(
(input, key) => {
if (key.focusIn) {
From 65caf610b535b6a230ba9c58ac0ec5cc6b4ca7f0 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Sun, 17 May 2026 12:57:24 +0800
Subject: [PATCH 040/129] feat: add test for Bash tool output streaming before
command completion
---
package-lock.json | 814 ++++++++++++++++++++++++++------
src/tests/tool-handlers.test.ts | 43 ++
2 files changed, 704 insertions(+), 153 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f9caecd..958eed7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -846,6 +846,13 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -856,6 +863,19 @@
"node": ">= 4"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/@eslint/js": {
"version": "9.39.4",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz",
@@ -1040,13 +1060,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.8.0",
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.8.0.tgz",
- "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
+ "version": "25.6.0",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "undici-types": "~7.19.0"
}
},
"node_modules/@types/react": {
@@ -1066,17 +1086,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
- "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
+ "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/type-utils": "8.59.3",
- "@typescript-eslint/utils": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/scope-manager": "8.59.2",
+ "@typescript-eslint/type-utils": "8.59.2",
+ "@typescript-eslint/utils": "8.59.2",
+ "@typescript-eslint/visitor-keys": "8.59.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1089,22 +1109,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.59.3",
+ "@typescript-eslint/parser": "^8.59.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.3.tgz",
- "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz",
+ "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/scope-manager": "8.59.2",
+ "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/typescript-estree": "8.59.2",
+ "@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3"
},
"engines": {
@@ -1120,14 +1140,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
- "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
+ "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.59.3",
- "@typescript-eslint/types": "^8.59.3",
+ "@typescript-eslint/tsconfig-utils": "^8.59.2",
+ "@typescript-eslint/types": "^8.59.2",
"debug": "^4.4.3"
},
"engines": {
@@ -1142,14 +1162,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
- "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
+ "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3"
+ "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/visitor-keys": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1160,9 +1180,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
- "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
+ "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1177,15 +1197,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
- "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
+ "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/utils": "8.59.3",
+ "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/typescript-estree": "8.59.2",
+ "@typescript-eslint/utils": "8.59.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1202,9 +1222,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz",
- "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz",
+ "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1216,16 +1236,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
- "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
+ "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.59.3",
- "@typescript-eslint/tsconfig-utils": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/project-service": "8.59.2",
+ "@typescript-eslint/tsconfig-utils": "8.59.2",
+ "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1296,16 +1316,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz",
- "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz",
+ "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3"
+ "@typescript-eslint/scope-manager": "8.59.2",
+ "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/typescript-estree": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1320,13 +1340,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
- "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
+ "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/types": "8.59.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1430,11 +1450,13 @@
}
},
"node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
},
"node_modules/auto-bind": {
"version": "5.0.1",
@@ -1703,7 +1725,7 @@
},
"node_modules/ejs": {
"version": "5.0.2",
- "resolved": "https://registry.npmmirror.com/ejs/-/ejs-5.0.2.tgz",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz",
"integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==",
"license": "Apache-2.0",
"bin": {
@@ -1714,9 +1736,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.356",
- "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz",
- "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==",
+ "version": "1.5.353",
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
+ "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
"dev": true,
"license": "ISC"
},
@@ -1802,16 +1824,12 @@
}
},
"node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"license": "MIT",
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=8"
}
},
"node_modules/eslint": {
@@ -1973,6 +1991,19 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/eslint/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -2195,9 +2226,9 @@
}
},
"node_modules/get-east-asian-width": {
- "version": "1.6.0",
- "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
- "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
+ "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2206,9 +2237,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/glob": {
"version": "13.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2239,7 +2283,7 @@
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
@@ -2249,7 +2293,7 @@
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
@@ -2262,7 +2306,7 @@
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2317,28 +2361,6 @@
"node": ">=6.0"
}
},
- "node_modules/gray-matter/node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
- "node_modules/gray-matter/node_modules/js-yaml": {
- "version": "3.14.2",
- "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz",
- "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
- "license": "MIT",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -2431,9 +2453,9 @@
}
},
"node_modules/ink": {
- "version": "7.0.3",
- "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.3.tgz",
- "integrity": "sha512-5kxHkIj9+RuqCU3zyvP4qvYWNOSHP2TW/SHayHGHOmk87KwfVcZwvJGemi9ch+ci2gXUqerK/Eh2DGEDt5q45g==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.1.tgz",
+ "integrity": "sha512-o6LAC268PLawlGVYrXTyaTfke4VtJftEheuwbgkQf7yvSXyWp1nRwBbAyKEkWXFZZsW/la5wrMuNbuBvZK2C1w==",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0",
@@ -2576,13 +2598,13 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
+ "version": "3.14.2",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
- "argparse": "^2.0.1"
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
@@ -2949,7 +2971,7 @@
},
"node_modules/minipass": {
"version": "7.1.3",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -2972,9 +2994,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.44",
- "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.44.tgz",
- "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "version": "2.0.38",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"dev": true,
"license": "MIT"
},
@@ -2994,9 +3016,9 @@
}
},
"node_modules/openai": {
- "version": "6.37.0",
- "resolved": "https://registry.npmmirror.com/openai/-/openai-6.37.0.tgz",
- "integrity": "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==",
+ "version": "6.35.0",
+ "resolved": "https://registry.npmmirror.com/openai/-/openai-6.35.0.tgz",
+ "integrity": "sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
@@ -3108,7 +3130,7 @@
},
"node_modules/path-scurry": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -3125,7 +3147,7 @@
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.3.6",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz",
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
"dev": true,
"license": "BlueOak-1.0.0",
@@ -3190,9 +3212,9 @@
}
},
"node_modules/react": {
- "version": "19.2.6",
- "resolved": "https://registry.npmmirror.com/react/-/react-19.2.6.tgz",
- "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "version": "19.2.5",
+ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3223,6 +3245,16 @@
"node": ">=4"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz",
@@ -3338,15 +3370,6 @@
"node": ">=10"
}
},
- "node_modules/stack-utils/node_modules/escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz",
@@ -3504,13 +3527,14 @@
}
},
"node_modules/tsx": {
- "version": "4.22.0",
- "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.0.tgz",
- "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
+ "version": "4.21.0",
+ "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "~0.28.0"
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
@@ -3522,6 +3546,490 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
@@ -3565,16 +4073,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.59.3",
- "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
- "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
+ "version": "8.59.2",
+ "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz",
+ "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.59.3",
- "@typescript-eslint/parser": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/utils": "8.59.3"
+ "@typescript-eslint/eslint-plugin": "8.59.2",
+ "@typescript-eslint/parser": "8.59.2",
+ "@typescript-eslint/typescript-estree": "8.59.2",
+ "@typescript-eslint/utils": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3589,9 +4097,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "version": "7.19.2",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
@@ -3695,9 +4203,9 @@
}
},
"node_modules/ws": {
- "version": "8.20.1",
- "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz",
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -3723,9 +4231,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.9.0",
- "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "version": "2.8.4",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
+ "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
"dev": true,
"license": "ISC",
"optional": true,
diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts
index 43af7ca..611d012 100644
--- a/src/tests/tool-handlers.test.ts
+++ b/src/tests/tool-handlers.test.ts
@@ -3,7 +3,9 @@ import assert from "node:assert/strict";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
+import { setTimeout as delay } from "node:timers/promises";
import type { ToolExecutionContext } from "../tools/executor";
+import { handleBashTool } from "../tools/bash-handler";
import { handleEditTool } from "../tools/edit-handler";
import { handleReadTool } from "../tools/read-handler";
import { handleWriteTool } from "../tools/write-handler";
@@ -19,6 +21,36 @@ afterEach(() => {
}
});
+test("Bash streams stdout and stderr before command completion", async () => {
+ const workspace = createTempWorkspace();
+ const chunks: string[] = [];
+ let completed = false;
+
+ const resultPromise = handleBashTool(
+ {
+ command: "printf 'first\\n'; sleep 1; printf 'second\\n'; printf 'err\\n' >&2",
+ },
+ createContext("bash-live-output", workspace, {
+ onProcessStdout: (_pid, chunk) => {
+ chunks.push(chunk);
+ },
+ })
+ ).finally(() => {
+ completed = true;
+ });
+
+ await waitFor(() => chunks.join("").includes("first"), 1500);
+
+ assert.equal(completed, false);
+
+ const result = await resultPromise;
+ const streamedOutput = chunks.join("");
+ assert.equal(result.ok, true);
+ assert.match(streamedOutput, /first/);
+ assert.match(streamedOutput, /second/);
+ assert.match(streamedOutput, /err/);
+});
+
test("Read returns snippet metadata and Edit can scope replacements by snippet_id", async () => {
const workspace = createTempWorkspace();
const filePath = path.join(workspace, "sample.txt");
@@ -573,3 +605,14 @@ function createTempWorkspace(): string {
tempDirs.push(dir);
return dir;
}
+
+async function waitFor(predicate: () => boolean, timeoutMs: number): Promise {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ if (predicate()) {
+ return;
+ }
+ await delay(25);
+ }
+ assert.equal(predicate(), true);
+}
From ac223020cdbca70c145f93608066f21b942f54e2 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 09:33:40 +0800
Subject: [PATCH 041/129] feat: add update plan tool
---
docs/SKILL.md | 259 ++++++++++++++++++++++++++++++
docs/SKILL_new.md | 264 +++++++++++++++++++++++++++++++
src/prompt.ts | 24 +++
src/tests/prompt.test.ts | 13 ++
src/tests/tool-handlers.test.ts | 26 +++
src/tools/executor.ts | 2 +
src/tools/update-plan-handler.ts | 23 +++
src/ui/MessageView.tsx | 31 ++++
templates/tools/update-plan.md | 38 +++++
9 files changed, 680 insertions(+)
create mode 100644 docs/SKILL.md
create mode 100644 docs/SKILL_new.md
create mode 100644 src/tools/update-plan-handler.ts
create mode 100644 templates/tools/update-plan.md
diff --git a/docs/SKILL.md b/docs/SKILL.md
new file mode 100644
index 0000000..f6d7149
--- /dev/null
+++ b/docs/SKILL.md
@@ -0,0 +1,259 @@
+---
+name: plan-and-execute
+description: Automatically plan and execute tasks from issue documents. Reads issue requirements, creates a task list at the end of the document, and systematically executes each task while updating progress. Use when working with issue documents, task planning, or when you need to break down and execute complex multi-step requirements.
+---
+
+# Plan and Execute
+
+This Skill helps you automatically plan and execute tasks based on issue documents. It reads your requirements, creates a structured task list directly in the document, and systematically works through each task while keeping the document updated with progress.
+
+## Quick Start
+
+When you need to work through an issue document:
+
+1. The Skill will first ask you for the issue document path
+2. It reads the document to understand requirements
+3. Creates a task list at the end of the document
+4. Executes tasks one by one, updating status in real-time
+
+## Instructions
+
+### Step 1: Get the issue document path
+
+Ask the user for the path to their issue document:
+
+```
+What is the path to your issue document?
+```
+
+The document can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions.
+
+### Step 2: Read and analyze the issue document
+
+Use the Read tool to load the document content and analyze:
+
+- What are the main requirements?
+- What tasks need to be completed?
+- Are there dependencies between tasks?
+- What is the complexity level?
+
+### Step 3: Create the task list
+
+Create a structured task list at the END of the issue document using this format:
+
+```markdown
+## Task List
+
+- [ ] Task 1 description
+- [ ] Task 2 description
+- [ ] Task 3 description
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+Use the Edit tool to append this section to the document. Break down complex requirements into specific, actionable tasks.
+
+### Step 4: Execute tasks systematically
+
+For each task in the list:
+
+1. **Mark as in progress**: Update the task in the document from `[ ]` to `[>]`
+2. **Execute the task**: Use appropriate tools to complete the work
+3. **Mark as completed**: Update the task from `[>]` to `[x]` when finished
+4. **Move to next task**: Only ONE task should be in progress at a time
+
+Important rules:
+- Always update the document BEFORE starting work on a task
+- Always update the document IMMEDIATELY after completing a task
+- Never work on multiple tasks simultaneously
+- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
+
+### Step 5: Handle task breakdown
+
+If during execution you discover a task is more complex than expected:
+
+1. Keep the current task as `[>]`
+2. Add new sub-tasks below it with indentation:
+ ```markdown
+ - [>] Main task
+ - [ ] Sub-task 1
+ - [ ] Sub-task 2
+ ```
+3. Complete sub-tasks first, then mark the main task as complete
+
+### Step 6: Final verification
+
+After all tasks are completed (`[x]`):
+
+1. Review the issue requirements to ensure everything is addressed
+2. Run any final checks (tests, builds, linting)
+3. Add a completion summary at the end of the document
+
+## Task State Symbols
+
+- `[ ]` - **Pending**: Not started yet
+- `[>]` - **In Progress**: Currently working on this
+- `[x]` - **Completed**: Finished successfully
+- `[!]` - **Blocked**: Cannot proceed (optional, for blocked tasks)
+
+## Examples
+
+### Example 1: Simple feature request
+
+**Issue document (before):**
+```markdown
+# Feature: Add dark mode toggle
+
+Users should be able to switch between light and dark themes.
+The toggle should be in the settings page.
+```
+
+**Issue document (after task list added):**
+```markdown
+# Feature: Add dark mode toggle
+
+Users should be able to switch between light and dark themes.
+The toggle should be in the settings page.
+
+## Task List
+
+- [ ] Create dark mode toggle component in Settings page
+- [ ] Add dark mode state management (context/store)
+- [ ] Implement CSS-in-JS styles for dark theme
+- [ ] Update existing components to support theme switching
+- [ ] Run tests and verify functionality
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+**During execution:**
+```markdown
+## Task List
+
+- [x] Create dark mode toggle component in Settings page
+- [>] Add dark mode state management (context/store)
+- [ ] Implement CSS-in-JS styles for dark theme
+- [ ] Update existing components to support theme switching
+- [ ] Run tests and verify functionality
+```
+
+### Example 2: Bug fix with investigation
+
+**Issue document:**
+```markdown
+# Bug: Login form crashes on submit
+
+When users click submit, the app crashes.
+Error message: "Cannot read property 'email' of undefined"
+```
+
+**Task list created:**
+```markdown
+## Task List
+
+- [ ] Reproduce the bug locally
+- [ ] Investigate the error in login form component
+- [ ] Identify root cause of undefined email property
+- [ ] Implement fix
+- [ ] Add validation to prevent similar issues
+- [ ] Test the fix with various inputs
+- [ ] Update error handling
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+## When to Use This Skill
+
+Use this Skill when:
+
+1. **Complex multi-step tasks** - Issue requires 3+ distinct steps
+2. **Feature implementation** - Building new functionality from requirements
+3. **Bug fixing** - Need to investigate, fix, and verify
+4. **Refactoring** - Multiple files or components need changes
+5. **User provides requirements** - Issue document contains specifications
+6. **Need progress tracking** - Want visible progress in the document itself
+
+## When NOT to Use This Skill
+
+Skip this Skill when:
+
+1. **Single simple task** - Just one straightforward action needed
+2. **Trivial changes** - Quick fixes that don't need planning
+3. **Informational requests** - User just wants explanation, not execution
+4. **No document provided** - User hasn't created an issue document
+
+## Best Practices
+
+1. **Be specific with tasks**: "Add login button to navbar" not "Update UI"
+2. **Keep tasks atomic**: Each task should be independently completable
+3. **Update immediately**: Don't batch status updates, do them in real-time
+4. **One task at a time**: Never mark multiple tasks as `[>]`
+5. **Handle blockers**: If stuck, create new tasks to resolve the blocker
+6. **Verify completion**: Only mark `[x]` when task is fully done
+
+## Advanced Usage
+
+### Handling dependencies
+
+When tasks have dependencies, order them properly:
+
+```markdown
+- [ ] Create database schema
+- [ ] Implement API endpoints (depends on schema)
+- [ ] Build frontend forms (depends on API)
+```
+
+### Using sub-tasks
+
+For complex tasks, break them down:
+
+```markdown
+- [>] Implement authentication system
+ - [x] Set up JWT library
+ - [>] Create login endpoint
+ - [ ] Create logout endpoint
+ - [ ] Add token refresh logic
+```
+
+### Adding notes
+
+Add implementation notes or findings:
+
+```markdown
+- [x] Investigate performance issue
+ - Note: Found N+1 query in user loader
+ - Solution: Added dataloader batching
+```
+
+## Requirements
+
+This Skill uses standard Deep Code tools:
+
+- **Read**: To read the issue document
+- **Edit**: To update task status in the document
+- **Bash**: To run tests, builds, or other commands
+- **Write**: To create new files if needed
+
+No additional dependencies required.
+
+## Workflow Summary
+
+1. Ask user for issue document path
+2. Read and analyze the document
+3. Append structured task list to document
+4. For each task:
+ - Update to `[>]` in document
+ - Execute the task
+ - Update to `[x]` in document
+5. Add completion summary when done
+
+This approach keeps all planning and progress tracking in one place - the issue document itself - making it easy for users to see what's been done and what's remaining.
diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md
new file mode 100644
index 0000000..cdcc514
--- /dev/null
+++ b/docs/SKILL_new.md
@@ -0,0 +1,264 @@
+---
+name: plan-and-execute
+description: Automatically plan and execute tasks from issue documents. Reads issue requirements, creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with issue documents, task planning, or when you need to break down and execute complex multi-step requirements.
+---
+
+# Plan and Execute
+
+This Skill helps you automatically plan and execute tasks based on issue documents. It reads your requirements, creates a structured markdown task list with the UpdatePlan tool, and systematically works through each task while keeping progress visible.
+
+## Quick Start
+
+When you need to work through an issue document:
+
+1. The Skill will first ask you for the issue document path
+2. It reads the document to understand requirements
+3. Creates a markdown task list by calling the UpdatePlan tool
+4. Executes tasks one by one, updating the tool plan in real time
+
+## Instructions
+
+### Step 1: Get the issue document path
+
+Ask the user for the path to their issue document:
+
+```
+What is the path to your issue document?
+```
+
+The document can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions.
+
+### Step 2: Read and analyze the issue document
+
+Use the Read tool to load the document content and analyze:
+
+- What are the main requirements?
+- What tasks need to be completed?
+- Are there dependencies between tasks?
+- What is the complexity level?
+
+### Step 3: Create the task list
+
+Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
+
+```json
+{
+ "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description\n\n### Task Status Legend\n- [ ] Pending\n- [>] In Progress\n- [x] Completed"
+}
+```
+
+Use this markdown format for the `plan` content:
+
+```markdown
+## Task List
+
+- [ ] Task 1 description
+- [ ] Task 2 description
+- [ ] Task 3 description
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+Do not append the task list to the issue document. Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
+
+### Step 4: Execute tasks systematically
+
+For each task in the list:
+
+1. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
+2. **Execute the task**: Use appropriate tools to complete the work
+3. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
+4. **Move to next task**: Only ONE task should be in progress at a time
+
+Important rules:
+- Always call UpdatePlan BEFORE starting work on a task
+- Always call UpdatePlan IMMEDIATELY after completing a task
+- Always pass the complete current markdown task list, not a partial diff
+- Never work on multiple tasks simultaneously
+- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
+
+### Step 5: Handle task breakdown
+
+If during execution you discover a task is more complex than expected:
+
+1. Keep the current task as `[>]`
+2. Call UpdatePlan with new sub-tasks below it with indentation:
+ ```markdown
+ - [>] Main task
+ - [ ] Sub-task 1
+ - [ ] Sub-task 2
+ ```
+3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
+
+### Step 6: Final verification
+
+After all tasks are completed (`[x]`):
+
+1. Review the issue requirements to ensure everything is addressed
+2. Run any final checks (tests, builds, linting)
+3. Call UpdatePlan with every task marked `[x]`
+4. Provide a concise completion summary in the final response
+
+## Task State Symbols
+
+- `[ ]` - **Pending**: Not started yet
+- `[>]` - **In Progress**: Currently working on this
+- `[x]` - **Completed**: Finished successfully
+- `[!]` - **Blocked**: Cannot proceed (optional, for blocked tasks)
+
+## Examples
+
+### Example 1: Simple feature request
+
+**Issue document (before):**
+```markdown
+# Feature: Add dark mode toggle
+
+Users should be able to switch between light and dark themes.
+The toggle should be in the settings page.
+```
+
+**UpdatePlan call after analysis:**
+```markdown
+## Task List
+
+- [ ] Create dark mode toggle component in Settings page
+- [ ] Add dark mode state management (context/store)
+- [ ] Implement CSS-in-JS styles for dark theme
+- [ ] Update existing components to support theme switching
+- [ ] Run tests and verify functionality
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+**UpdatePlan call during execution:**
+```markdown
+## Task List
+
+- [x] Create dark mode toggle component in Settings page
+- [>] Add dark mode state management (context/store)
+- [ ] Implement CSS-in-JS styles for dark theme
+- [ ] Update existing components to support theme switching
+- [ ] Run tests and verify functionality
+```
+
+### Example 2: Bug fix with investigation
+
+**Issue document:**
+```markdown
+# Bug: Login form crashes on submit
+
+When users click submit, the app crashes.
+Error message: "Cannot read property 'email' of undefined"
+```
+
+**UpdatePlan call after analysis:**
+```markdown
+## Task List
+
+- [ ] Reproduce the bug locally
+- [ ] Investigate the error in login form component
+- [ ] Identify root cause of undefined email property
+- [ ] Implement fix
+- [ ] Add validation to prevent similar issues
+- [ ] Test the fix with various inputs
+- [ ] Update error handling
+
+### Task Status Legend
+- [ ] Pending
+- [>] In Progress
+- [x] Completed
+```
+
+## When to Use This Skill
+
+Use this Skill when:
+
+1. **Complex multi-step tasks** - Issue requires 3+ distinct steps
+2. **Feature implementation** - Building new functionality from requirements
+3. **Bug fixing** - Need to investigate, fix, and verify
+4. **Refactoring** - Multiple files or components need changes
+5. **User provides requirements** - Issue document contains specifications
+6. **Need progress tracking** - Want visible progress without editing the issue document
+
+## When NOT to Use This Skill
+
+Skip this Skill when:
+
+1. **Single simple task** - Just one straightforward action needed
+2. **Trivial changes** - Quick fixes that don't need planning
+3. **Informational requests** - User just wants explanation, not execution
+4. **No document provided** - User hasn't created an issue document
+
+## Best Practices
+
+1. **Be specific with tasks**: "Add login button to navbar" not "Update UI"
+2. **Keep tasks atomic**: Each task should be independently completable
+3. **Update immediately**: Don't batch status updates, do them in real-time
+4. **One task at a time**: Never mark multiple tasks as `[>]`
+5. **Handle blockers**: If stuck, create new tasks to resolve the blocker
+6. **Verify completion**: Only mark `[x]` when task is fully done
+
+## Advanced Usage
+
+### Handling dependencies
+
+When tasks have dependencies, order them properly:
+
+```markdown
+- [ ] Create database schema
+- [ ] Implement API endpoints (depends on schema)
+- [ ] Build frontend forms (depends on API)
+```
+
+### Using sub-tasks
+
+For complex tasks, break them down:
+
+```markdown
+- [>] Implement authentication system
+ - [x] Set up JWT library
+ - [>] Create login endpoint
+ - [ ] Create logout endpoint
+ - [ ] Add token refresh logic
+```
+
+### Adding notes
+
+Add implementation notes or findings:
+
+```markdown
+- [x] Investigate performance issue
+ - Note: Found N+1 query in user loader
+ - Solution: Added dataloader batching
+```
+
+## Requirements
+
+This Skill uses standard tools:
+
+- **Read**: To read the issue document
+- **UpdatePlan**: To create and update the markdown task list
+- **Bash**: To run tests, builds, or other commands
+- **Write**: To create new files if needed
+
+No additional dependencies required.
+
+## Workflow Summary
+
+1. Ask user for issue document path
+2. Read and analyze the document
+3. Call UpdatePlan with the structured markdown task list
+4. For each task:
+ - Update to `[>]` with UpdatePlan
+ - Execute the task
+ - Update to `[x]` with UpdatePlan
+5. Call UpdatePlan with all tasks completed and summarize the result
+
+This approach keeps planning and progress tracking in the UpdatePlan display, leaving the issue document unchanged unless the actual task requires editing it.
diff --git a/src/prompt.ts b/src/prompt.ts
index 4774725..50aa2a3 100644
--- a/src/prompt.ts
+++ b/src/prompt.ts
@@ -509,6 +509,30 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe
},
},
},
+ {
+ type: "function",
+ function: {
+ name: "UpdatePlan",
+ description:
+ "Update the current task plan. The plan argument must be the complete markdown task list to show as the latest progress state.",
+ parameters: {
+ type: "object",
+ properties: {
+ plan: {
+ type: "string",
+ description:
+ "The complete markdown task list, including task status markers such as [ ], [>], [x], and optional notes.",
+ },
+ explanation: {
+ type: "string",
+ description: "Optional short reason for changing the plan.",
+ },
+ },
+ required: ["plan"],
+ additionalProperties: false,
+ },
+ },
+ },
{
type: "function",
function: {
diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts
index 28c6488..b7c9178 100644
--- a/src/tests/prompt.test.ts
+++ b/src/tests/prompt.test.ts
@@ -12,11 +12,24 @@ test("getTools always includes WebSearch", () => {
assert.equal(names.includes("WebSearch"), true);
});
+test("getTools includes UpdatePlan with string plan schema", () => {
+ const tool = getTools().find((candidate) => candidate.function.name === "UpdatePlan");
+ assert.ok(tool);
+ assert.deepEqual(tool.function.parameters.required, ["plan"]);
+ assert.equal((tool.function.parameters.properties.plan as { type?: unknown }).type, "string");
+});
+
test("getSystemPrompt always includes WebSearch docs", () => {
const prompt = getSystemPrompt("/tmp/project");
assert.equal(prompt.includes("## WebSearch"), true);
});
+test("getSystemPrompt includes UpdatePlan docs", () => {
+ const prompt = getSystemPrompt("/tmp/project");
+ assert.equal(prompt.includes("## UpdatePlan"), true);
+ assert.equal(prompt.includes("The `plan` argument is a markdown string, not an array of step objects."), true);
+});
+
test("getSystemPrompt includes current date guidance", () => {
const now = new Date();
const expected = `今天是${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日。随着对话的进行,时间在流逝。`;
diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts
index 58828a2..0b21edd 100644
--- a/src/tests/tool-handlers.test.ts
+++ b/src/tests/tool-handlers.test.ts
@@ -8,6 +8,7 @@ import type { ToolExecutionContext } from "../tools/executor";
import { handleBashTool } from "../tools/bash-handler";
import { handleEditTool } from "../tools/edit-handler";
import { handleReadTool } from "../tools/read-handler";
+import { handleUpdatePlanTool } from "../tools/update-plan-handler";
import { handleWriteTool } from "../tools/write-handler";
const tempDirs: string[] = [];
@@ -51,6 +52,31 @@ test("Bash streams stdout and stderr before command completion", async () => {
assert.match(streamedOutput, /err/);
});
+test("UpdatePlan accepts a markdown task list string", async () => {
+ const workspace = createTempWorkspace();
+ const plan = ["## Task List", "", "- [>] Inspect current behavior", "- [ ] Implement UpdatePlan"].join("\n");
+
+ const result = await handleUpdatePlanTool({ plan }, createContext("update-plan", workspace));
+
+ assert.equal(result.ok, true);
+ assert.equal(result.name, "UpdatePlan");
+ assert.equal(result.output, "Plan updated.");
+ assert.equal(result.metadata?.plan, plan);
+});
+
+test("UpdatePlan rejects non-string plan payloads", async () => {
+ const workspace = createTempWorkspace();
+
+ const result = await handleUpdatePlanTool(
+ { plan: [{ step: "Inspect current behavior", status: "in_progress" }] },
+ createContext("update-plan-invalid", workspace)
+ );
+
+ assert.equal(result.ok, false);
+ assert.equal(result.name, "UpdatePlan");
+ assert.match(result.error ?? "", /InputValidationError/);
+});
+
test("Read returns snippet metadata and Edit can scope replacements by snippet_id", async () => {
const workspace = createTempWorkspace();
const filePath = path.join(workspace, "sample.txt");
diff --git a/src/tools/executor.ts b/src/tools/executor.ts
index e6018d9..70ceab1 100644
--- a/src/tools/executor.ts
+++ b/src/tools/executor.ts
@@ -4,6 +4,7 @@ import { handleAskUserQuestionTool } from "./ask-user-question-handler";
import { handleBashTool } from "./bash-handler";
import { handleEditTool } from "./edit-handler";
import { handleReadTool } from "./read-handler";
+import { handleUpdatePlanTool } from "./update-plan-handler";
import { handleWebSearchTool } from "./web-search-handler";
import { handleWriteTool } from "./write-handler";
import type { McpManager } from "../mcp/mcp-manager";
@@ -120,6 +121,7 @@ export class ToolExecutor {
this.toolHandlers.set("write", handleWriteTool);
this.toolHandlers.set("edit", handleEditTool);
this.toolHandlers.set("AskUserQuestion", handleAskUserQuestionTool);
+ this.toolHandlers.set("UpdatePlan", handleUpdatePlanTool);
this.toolHandlers.set("WebSearch", handleWebSearchTool);
}
diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts
new file mode 100644
index 0000000..7c7198e
--- /dev/null
+++ b/src/tools/update-plan-handler.ts
@@ -0,0 +1,23 @@
+import { z } from "zod";
+import type { ToolExecutionContext, ToolExecutionResult } from "./executor";
+import { executeValidatedTool } from "../common/runtime";
+
+const updatePlanSchema = z.strictObject({
+ plan: z.string().trim().min(1, "plan must not be empty."),
+ explanation: z.string().trim().optional(),
+});
+
+export async function handleUpdatePlanTool(
+ args: Record,
+ _context: ToolExecutionContext
+): Promise {
+ return executeValidatedTool("UpdatePlan", updatePlanSchema, args, _context, async (input) => ({
+ ok: true,
+ name: "UpdatePlan",
+ output: "Plan updated.",
+ metadata: {
+ plan: input.plan,
+ ...(input.explanation ? { explanation: input.explanation } : {}),
+ },
+ }));
+}
diff --git a/src/ui/MessageView.tsx b/src/ui/MessageView.tsx
index c8793fc..6f388d0 100644
--- a/src/ui/MessageView.tsx
+++ b/src/ui/MessageView.tsx
@@ -72,6 +72,7 @@ export function MessageView({ message, collapsed, width = 80 }: Props): React.Re
if (message.role === "tool") {
const summary = buildToolSummary(message);
const diffLines = getToolDiffPreviewLines(summary);
+ const planLines = getUpdatePlanPreviewLines(summary);
return (
{diffLines.length > 0 ? : null}
+ {planLines.length > 0 ? : null}
);
}
@@ -272,6 +274,20 @@ function getToolDiffPreviewLines(summary: ToolSummary): DiffPreviewLine[] {
return parseDiffPreview(diffPreview);
}
+function getUpdatePlanPreviewLines(summary: ToolSummary): string[] {
+ if (!summary.ok || summary.name !== "UpdatePlan") {
+ return [];
+ }
+ const plan = summary.metadata?.plan;
+ if (typeof plan !== "string" || !plan.trim()) {
+ return [];
+ }
+ return plan
+ .split(/\r?\n/)
+ .map((line) => line.trimEnd())
+ .filter((line) => line.trim().length > 0);
+}
+
export function parseDiffPreview(diffPreview: string): DiffPreviewLine[] {
return diffPreview
.split("\n")
@@ -311,6 +327,21 @@ function DiffPreview({ lines }: { lines: DiffPreviewLine[] }): React.ReactElemen
);
}
+function PlanPreview({ lines }: { lines: string[] }): React.ReactElement {
+ return (
+
+ └ Plan
+
+ {lines.map((line, index) => (
+
+ {line}
+
+ ))}
+
+
+ );
+}
+
function isPlainRecord(value: unknown): value is Record {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
diff --git a/templates/tools/update-plan.md b/templates/tools/update-plan.md
new file mode 100644
index 0000000..c3e08f0
--- /dev/null
+++ b/templates/tools/update-plan.md
@@ -0,0 +1,38 @@
+## UpdatePlan
+
+Updates the current task plan and progress display.
+
+Usage:
+- Use this tool for non-trivial multi-step tasks when a task list helps track execution progress.
+- Pass the complete current task list every time. The latest call replaces the previous visible plan.
+- The `plan` argument is a markdown string, not an array of step objects.
+- Keep exactly one task marked `[>]` while work is in progress.
+- Update the plan before starting a task, immediately after completing a task, and whenever tasks are split, merged, reordered, blocked, or changed.
+- Do not edit issue documents just to maintain task status; use `UpdatePlan` for the task list instead.
+
+Task markers:
+- `[ ]` Pending
+- `[>]` In progress
+- `[x]` Completed
+- `[!]` Blocked
+
+```json
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "plan": {
+ "description": "The complete markdown task list to display as the latest plan state.",
+ "type": "string"
+ },
+ "explanation": {
+ "description": "Optional short reason for changing the plan.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "plan"
+ ],
+ "additionalProperties": false
+}
+```
From 52dafba25903dc70258d7e59dbe86e283a0f091f Mon Sep 17 00:00:00 2001
From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com>
Date: Mon, 18 May 2026 09:50:38 +0800
Subject: [PATCH 042/129] fix: re-apply dynamic modifier parsing for
Shift+Enter after upstream sync
Upstream v0.1.21 reverted PR #70. Re-apply:
- isShiftReturn() / isReturn() dynamic CSI modifier bit parsing
- Kitty progressive enhancement (ESC[>1u) alongside xterm modifyOtherKeys
- Clear input when key.return is true (safety net)
---
src/tests/promptInputKeys.test.ts | 6 ++---
src/ui/prompt/cursor.ts | 4 +--
src/ui/prompt/useTerminalInput.ts | 43 ++++++++++++++++++++++++++++---
3 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts
index 69d2075..8952a3d 100644
--- a/src/tests/promptInputKeys.test.ts
+++ b/src/tests/promptInputKeys.test.ts
@@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => {
test("parseTerminalInput recognizes shifted return sequences", () => {
const { input, key } = parseTerminalInput("\u001B\r");
- assert.equal(input, "\r");
+ assert.equal(input, "");
assert.equal(key.return, true);
assert.equal(key.shift, true);
assert.equal(key.meta, false);
@@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => {
});
test("terminal extended key helpers request and restore modifyOtherKeys mode", () => {
- assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m");
- assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m");
+ assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u");
+ assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[ {
diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts
index 2668470..59b24f2 100644
--- a/src/ui/prompt/cursor.ts
+++ b/src/ui/prompt/cursor.ts
@@ -41,11 +41,11 @@ function disableTerminalFocusReporting(): string {
}
export function enableTerminalExtendedKeys(): string {
- return "\u001B[>4;1m";
+ return "\u001B[>4;1m\u001B[>1u";
}
export function disableTerminalExtendedKeys(): string {
- return "\u001B[>4;0m";
+ return "\u001B[>4;0m\u001B[
Date: Mon, 18 May 2026 10:22:22 +0800
Subject: [PATCH 043/129] fix: refresh mcpToolDefinitions cache after MCP
reconnect
After reconnectMcpServer succeeds, SessionManager's cached
mcpToolDefinitions was stale, causing "Unknown MCP tool" errors
when the model tried to call reconnected tools.
---
src/session.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/session.ts b/src/session.ts
index eddfe5c..0527ba8 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -261,6 +261,7 @@ export class SessionManager {
async reconnectMcpServer(name: string, config?: McpServerConfig): Promise {
await this.mcpManager.reconnect(name, config);
+ this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions();
}
dispose(): void {
From 63ec2a340192f6e53b8003162320e102e521a49e Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 10:40:39 +0800
Subject: [PATCH 044/129] feat: update update-plan prompt draft
---
docs/SKILL.md | 124 +++++++++++++++------------------
docs/SKILL_new.md | 77 +++++++++-----------
templates/tools/update-plan.md | 7 --
3 files changed, 90 insertions(+), 118 deletions(-)
diff --git a/docs/SKILL.md b/docs/SKILL.md
index f6d7149..8f45c3b 100644
--- a/docs/SKILL.md
+++ b/docs/SKILL.md
@@ -1,36 +1,38 @@
---
name: plan-and-execute
-description: Automatically plan and execute tasks from issue documents. Reads issue requirements, creates a task list at the end of the document, and systematically executes each task while updating progress. Use when working with issue documents, task planning, or when you need to break down and execute complex multi-step requirements.
+description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements.
---
# Plan and Execute
-This Skill helps you automatically plan and execute tasks based on issue documents. It reads your requirements, creates a structured task list directly in the document, and systematically works through each task while keeping the document updated with progress.
+This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible.
## Quick Start
-When you need to work through an issue document:
+When you need to work through a multi-step request:
-1. The Skill will first ask you for the issue document path
-2. It reads the document to understand requirements
-3. Creates a task list at the end of the document
-4. Executes tasks one by one, updating status in real-time
+1. Understand the requirements
+2. Read referenced files only when they are needed for context
+3. Create a markdown task list by calling the UpdatePlan tool
+4. Execute tasks one by one, updating the tool plan in real time
## Instructions
-### Step 1: Get the issue document path
+### Step 1: Gather the requirements
-Ask the user for the path to their issue document:
+Identify the requirements from the available context. Do not require the requirements to be moved into a separate document.
+
+If a required referenced file path is missing, ask for it:
```
-What is the path to your issue document?
+What is the path to the referenced file?
```
-The document can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions.
+Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
-### Step 2: Read and analyze the issue document
+### Step 2: Read and analyze the requirements
-Use the Read tool to load the document content and analyze:
+Analyze the requirements and read any referenced files needed for context:
- What are the main requirements?
- What tasks need to be completed?
@@ -39,7 +41,15 @@ Use the Read tool to load the document content and analyze:
### Step 3: Create the task list
-Create a structured task list at the END of the issue document using this format:
+Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
+
+```json
+{
+ "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description"
+}
+```
+
+Use this markdown format for the `plan` content:
```markdown
## Task List
@@ -47,27 +57,23 @@ Create a structured task list at the END of the issue document using this format
- [ ] Task 1 description
- [ ] Task 2 description
- [ ] Task 3 description
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
-Use the Edit tool to append this section to the document. Break down complex requirements into specific, actionable tasks.
+Do not append the task list to a source file. Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
### Step 4: Execute tasks systematically
For each task in the list:
-1. **Mark as in progress**: Update the task in the document from `[ ]` to `[>]`
+1. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
2. **Execute the task**: Use appropriate tools to complete the work
-3. **Mark as completed**: Update the task from `[>]` to `[x]` when finished
+3. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
4. **Move to next task**: Only ONE task should be in progress at a time
Important rules:
-- Always update the document BEFORE starting work on a task
-- Always update the document IMMEDIATELY after completing a task
+- Always call UpdatePlan BEFORE starting work on a task
+- Always call UpdatePlan IMMEDIATELY after completing a task
+- Always pass the complete current markdown task list, not a partial diff
- Never work on multiple tasks simultaneously
- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
@@ -76,34 +82,35 @@ Important rules:
If during execution you discover a task is more complex than expected:
1. Keep the current task as `[>]`
-2. Add new sub-tasks below it with indentation:
+2. Call UpdatePlan with new sub-tasks below it with indentation:
```markdown
- [>] Main task
- [ ] Sub-task 1
- [ ] Sub-task 2
```
-3. Complete sub-tasks first, then mark the main task as complete
+3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
### Step 6: Final verification
After all tasks are completed (`[x]`):
-1. Review the issue requirements to ensure everything is addressed
+1. Review the original requirements to ensure everything is addressed
2. Run any final checks (tests, builds, linting)
-3. Add a completion summary at the end of the document
+3. Call UpdatePlan with every task marked `[x]`
+4. Provide a concise completion summary in the final response
## Task State Symbols
-- `[ ]` - **Pending**: Not started yet
-- `[>]` - **In Progress**: Currently working on this
-- `[x]` - **Completed**: Finished successfully
-- `[!]` - **Blocked**: Cannot proceed (optional, for blocked tasks)
+- `[ ]` - Pending
+- `[>]` - In progress
+- `[x]` - Completed
+- `[!]` - Blocked
## Examples
### Example 1: Simple feature request
-**Issue document (before):**
+**Example requirements:**
```markdown
# Feature: Add dark mode toggle
@@ -111,13 +118,8 @@ Users should be able to switch between light and dark themes.
The toggle should be in the settings page.
```
-**Issue document (after task list added):**
+**UpdatePlan call after analysis:**
```markdown
-# Feature: Add dark mode toggle
-
-Users should be able to switch between light and dark themes.
-The toggle should be in the settings page.
-
## Task List
- [ ] Create dark mode toggle component in Settings page
@@ -125,14 +127,9 @@ The toggle should be in the settings page.
- [ ] Implement CSS-in-JS styles for dark theme
- [ ] Update existing components to support theme switching
- [ ] Run tests and verify functionality
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
-**During execution:**
+**UpdatePlan call during execution:**
```markdown
## Task List
@@ -145,7 +142,7 @@ The toggle should be in the settings page.
### Example 2: Bug fix with investigation
-**Issue document:**
+**Example requirements:**
```markdown
# Bug: Login form crashes on submit
@@ -153,7 +150,7 @@ When users click submit, the app crashes.
Error message: "Cannot read property 'email' of undefined"
```
-**Task list created:**
+**UpdatePlan call after analysis:**
```markdown
## Task List
@@ -164,23 +161,18 @@ Error message: "Cannot read property 'email' of undefined"
- [ ] Add validation to prevent similar issues
- [ ] Test the fix with various inputs
- [ ] Update error handling
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
## When to Use This Skill
Use this Skill when:
-1. **Complex multi-step tasks** - Issue requires 3+ distinct steps
+1. **Complex multi-step tasks** - Request requires 3+ distinct steps
2. **Feature implementation** - Building new functionality from requirements
3. **Bug fixing** - Need to investigate, fix, and verify
4. **Refactoring** - Multiple files or components need changes
-5. **User provides requirements** - Issue document contains specifications
-6. **Need progress tracking** - Want visible progress in the document itself
+5. **Detailed requirements** - Specifications need to be translated into concrete tasks
+6. **Need progress tracking** - Want visible progress without editing source files
## When NOT to Use This Skill
@@ -189,7 +181,7 @@ Skip this Skill when:
1. **Single simple task** - Just one straightforward action needed
2. **Trivial changes** - Quick fixes that don't need planning
3. **Informational requests** - User just wants explanation, not execution
-4. **No document provided** - User hasn't created an issue document
+4. **No execution requested** - User only wants brainstorming or a high-level explanation
## Best Practices
@@ -236,10 +228,10 @@ Add implementation notes or findings:
## Requirements
-This Skill uses standard Deep Code tools:
+This Skill uses standard tools:
-- **Read**: To read the issue document
-- **Edit**: To update task status in the document
+- **Read**: To inspect referenced files when needed
+- **UpdatePlan**: To create and update the markdown task list
- **Bash**: To run tests, builds, or other commands
- **Write**: To create new files if needed
@@ -247,13 +239,13 @@ No additional dependencies required.
## Workflow Summary
-1. Ask user for issue document path
-2. Read and analyze the document
-3. Append structured task list to document
+1. Analyze the requirements
+2. Read referenced files when needed
+3. Call UpdatePlan with the structured markdown task list
4. For each task:
- - Update to `[>]` in document
+ - Update to `[>]` with UpdatePlan
- Execute the task
- - Update to `[x]` in document
-5. Add completion summary when done
+ - Update to `[x]` with UpdatePlan
+5. Call UpdatePlan with all tasks completed and summarize the result
-This approach keeps all planning and progress tracking in one place - the issue document itself - making it easy for users to see what's been done and what's remaining.
+This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md
index cdcc514..6efbee9 100644
--- a/docs/SKILL_new.md
+++ b/docs/SKILL_new.md
@@ -1,36 +1,38 @@
---
name: plan-and-execute
-description: Automatically plan and execute tasks from issue documents. Reads issue requirements, creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with issue documents, task planning, or when you need to break down and execute complex multi-step requirements.
+description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements.
---
# Plan and Execute
-This Skill helps you automatically plan and execute tasks based on issue documents. It reads your requirements, creates a structured markdown task list with the UpdatePlan tool, and systematically works through each task while keeping progress visible.
+This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible.
## Quick Start
-When you need to work through an issue document:
+When you need to work through a multi-step request:
-1. The Skill will first ask you for the issue document path
-2. It reads the document to understand requirements
-3. Creates a markdown task list by calling the UpdatePlan tool
-4. Executes tasks one by one, updating the tool plan in real time
+1. Understand the requirements
+2. Read referenced files when they are needed for context
+3. Create a markdown task list by calling the UpdatePlan tool
+4. Execute tasks one by one, updating the tool plan in real time
## Instructions
-### Step 1: Get the issue document path
+### Step 1: Gather the requirements
-Ask the user for the path to their issue document:
+Identify the requirements from the available context. Do not require the requirements to be moved into a separate document.
+
+If a required referenced file path is missing, ask for it:
```
-What is the path to your issue document?
+What is the path to the referenced file?
```
-The document can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions.
+Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
-### Step 2: Read and analyze the issue document
+### Step 2: Read and analyze the requirements
-Use the Read tool to load the document content and analyze:
+Analyze the requirements and read any referenced files needed for context:
- What are the main requirements?
- What tasks need to be completed?
@@ -43,7 +45,7 @@ Create a structured markdown task list and pass it to the UpdatePlan tool as the
```json
{
- "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description\n\n### Task Status Legend\n- [ ] Pending\n- [>] In Progress\n- [x] Completed"
+ "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description"
}
```
@@ -55,14 +57,9 @@ Use this markdown format for the `plan` content:
- [ ] Task 1 description
- [ ] Task 2 description
- [ ] Task 3 description
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
-Do not append the task list to the issue document. Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
+Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
### Step 4: Execute tasks systematically
@@ -97,23 +94,23 @@ If during execution you discover a task is more complex than expected:
After all tasks are completed (`[x]`):
-1. Review the issue requirements to ensure everything is addressed
+1. Review the original requirements to ensure everything is addressed
2. Run any final checks (tests, builds, linting)
3. Call UpdatePlan with every task marked `[x]`
4. Provide a concise completion summary in the final response
## Task State Symbols
-- `[ ]` - **Pending**: Not started yet
-- `[>]` - **In Progress**: Currently working on this
-- `[x]` - **Completed**: Finished successfully
-- `[!]` - **Blocked**: Cannot proceed (optional, for blocked tasks)
+- `[ ]` - Pending
+- `[>]` - In progress
+- `[x]` - Completed
+- `[!]` - Blocked
## Examples
### Example 1: Simple feature request
-**Issue document (before):**
+**Example requirements:**
```markdown
# Feature: Add dark mode toggle
@@ -130,11 +127,6 @@ The toggle should be in the settings page.
- [ ] Implement CSS-in-JS styles for dark theme
- [ ] Update existing components to support theme switching
- [ ] Run tests and verify functionality
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
**UpdatePlan call during execution:**
@@ -150,7 +142,7 @@ The toggle should be in the settings page.
### Example 2: Bug fix with investigation
-**Issue document:**
+**Example requirements:**
```markdown
# Bug: Login form crashes on submit
@@ -169,23 +161,18 @@ Error message: "Cannot read property 'email' of undefined"
- [ ] Add validation to prevent similar issues
- [ ] Test the fix with various inputs
- [ ] Update error handling
-
-### Task Status Legend
-- [ ] Pending
-- [>] In Progress
-- [x] Completed
```
## When to Use This Skill
Use this Skill when:
-1. **Complex multi-step tasks** - Issue requires 3+ distinct steps
+1. **Complex multi-step tasks** - Request requires 3+ distinct steps
2. **Feature implementation** - Building new functionality from requirements
3. **Bug fixing** - Need to investigate, fix, and verify
4. **Refactoring** - Multiple files or components need changes
-5. **User provides requirements** - Issue document contains specifications
-6. **Need progress tracking** - Want visible progress without editing the issue document
+5. **Detailed requirements** - Specifications need to be translated into concrete tasks
+6. **Need progress tracking** - Want visible progress without editing source files
## When NOT to Use This Skill
@@ -194,7 +181,7 @@ Skip this Skill when:
1. **Single simple task** - Just one straightforward action needed
2. **Trivial changes** - Quick fixes that don't need planning
3. **Informational requests** - User just wants explanation, not execution
-4. **No document provided** - User hasn't created an issue document
+4. **No execution requested** - User only wants brainstorming or a high-level explanation
## Best Practices
@@ -243,7 +230,7 @@ Add implementation notes or findings:
This Skill uses standard tools:
-- **Read**: To read the issue document
+- **Read**: To inspect referenced files when needed
- **UpdatePlan**: To create and update the markdown task list
- **Bash**: To run tests, builds, or other commands
- **Write**: To create new files if needed
@@ -252,8 +239,8 @@ No additional dependencies required.
## Workflow Summary
-1. Ask user for issue document path
-2. Read and analyze the document
+1. Analyze the requirements
+2. Read referenced files when needed
3. Call UpdatePlan with the structured markdown task list
4. For each task:
- Update to `[>]` with UpdatePlan
@@ -261,4 +248,4 @@ No additional dependencies required.
- Update to `[x]` with UpdatePlan
5. Call UpdatePlan with all tasks completed and summarize the result
-This approach keeps planning and progress tracking in the UpdatePlan display, leaving the issue document unchanged unless the actual task requires editing it.
+This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
diff --git a/templates/tools/update-plan.md b/templates/tools/update-plan.md
index c3e08f0..28d12f7 100644
--- a/templates/tools/update-plan.md
+++ b/templates/tools/update-plan.md
@@ -8,13 +8,6 @@ Usage:
- The `plan` argument is a markdown string, not an array of step objects.
- Keep exactly one task marked `[>]` while work is in progress.
- Update the plan before starting a task, immediately after completing a task, and whenever tasks are split, merged, reordered, blocked, or changed.
-- Do not edit issue documents just to maintain task status; use `UpdatePlan` for the task list instead.
-
-Task markers:
-- `[ ]` Pending
-- `[>]` In progress
-- `[x]` Completed
-- `[!]` Blocked
```json
{
From c638114bfc2ab23c502322955403e4c8dc1bff62 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 11:00:13 +0800
Subject: [PATCH 045/129] feat: update update-plan prompt draft
---
docs/SKILL.md | 47 ++++++++++++++---------------
docs/SKILL_new.md | 54 ++++++++++++++--------------------
templates/tools/update-plan.md | 2 ++
3 files changed, 48 insertions(+), 55 deletions(-)
diff --git a/docs/SKILL.md b/docs/SKILL.md
index 8f45c3b..deb8c5e 100644
--- a/docs/SKILL.md
+++ b/docs/SKILL.md
@@ -11,16 +11,16 @@ This Skill helps you automatically plan and execute requirements. It creates a s
When you need to work through a multi-step request:
-1. Understand the requirements
-2. Read referenced files only when they are needed for context
-3. Create a markdown task list by calling the UpdatePlan tool
-4. Execute tasks one by one, updating the tool plan in real time
+1. Analyze the requirements and explore enough project context
+2. Create a markdown task list by calling the UpdatePlan tool
+3. Execute tasks one by one, updating the tool plan in real time
+4. Revise the remaining plan as new context appears
## Instructions
-### Step 1: Gather the requirements
+### Step 1: Analyze the requirements
-Identify the requirements from the available context. Do not require the requirements to be moved into a separate document.
+Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate.
If a required referenced file path is missing, ask for it:
@@ -30,16 +30,13 @@ What is the path to the referenced file?
Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
-### Step 2: Read and analyze the requirements
-
-Analyze the requirements and read any referenced files needed for context:
-
- What are the main requirements?
- What tasks need to be completed?
- Are there dependencies between tasks?
- What is the complexity level?
+- Which files, modules, commands, or tests are relevant?
-### Step 3: Create the task list
+### Step 2: Create the task list
Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
@@ -59,25 +56,28 @@ Use this markdown format for the `plan` content:
- [ ] Task 3 description
```
-Do not append the task list to a source file. Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
+Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
-### Step 4: Execute tasks systematically
+### Step 3: Execute tasks systematically
For each task in the list:
-1. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
-2. **Execute the task**: Use appropriate tools to complete the work
-3. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
-4. **Move to next task**: Only ONE task should be in progress at a time
+1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes.
+2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
+3. **Execute the task**: Use appropriate tools to complete the work
+4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
+5. **Move to next task**: Only ONE task should be in progress at a time
Important rules:
+- Always keep the plan aligned with the latest context before executing the next task
- Always call UpdatePlan BEFORE starting work on a task
- Always call UpdatePlan IMMEDIATELY after completing a task
- Always pass the complete current markdown task list, not a partial diff
- Never work on multiple tasks simultaneously
+- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them
- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
-### Step 5: Handle task breakdown
+### Step 4: Handle task breakdown
If during execution you discover a task is more complex than expected:
@@ -90,7 +90,7 @@ If during execution you discover a task is more complex than expected:
```
3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
-### Step 6: Final verification
+### Step 5: Final verification
After all tasks are completed (`[x]`):
@@ -230,7 +230,7 @@ Add implementation notes or findings:
This Skill uses standard tools:
-- **Read**: To inspect referenced files when needed
+- **Read**: To inspect relevant files and explore project context
- **UpdatePlan**: To create and update the markdown task list
- **Bash**: To run tests, builds, or other commands
- **Write**: To create new files if needed
@@ -239,13 +239,14 @@ No additional dependencies required.
## Workflow Summary
-1. Analyze the requirements
-2. Read referenced files when needed
-3. Call UpdatePlan with the structured markdown task list
+1. Analyze the requirements and relevant project context
+2. Call UpdatePlan with the structured markdown task list
+3. Refresh the remaining plan before the first task
4. For each task:
- Update to `[>]` with UpdatePlan
- Execute the task
- Update to `[x]` with UpdatePlan
+ - Re-evaluate and revise remaining tasks before moving on
5. Call UpdatePlan with all tasks completed and summarize the result
This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md
index 6efbee9..41ed251 100644
--- a/docs/SKILL_new.md
+++ b/docs/SKILL_new.md
@@ -11,16 +11,16 @@ This Skill helps you automatically plan and execute requirements. It creates a s
When you need to work through a multi-step request:
-1. Understand the requirements
-2. Read referenced files when they are needed for context
-3. Create a markdown task list by calling the UpdatePlan tool
-4. Execute tasks one by one, updating the tool plan in real time
+1. Analyze the requirements and explore enough project context
+2. Create a markdown task list by calling the UpdatePlan tool
+3. Execute tasks one by one, updating the tool plan in real time
+4. Revise the remaining plan as new context appears
## Instructions
-### Step 1: Gather the requirements
+### Step 1: Analyze the requirements
-Identify the requirements from the available context. Do not require the requirements to be moved into a separate document.
+Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate.
If a required referenced file path is missing, ask for it:
@@ -30,16 +30,13 @@ What is the path to the referenced file?
Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
-### Step 2: Read and analyze the requirements
-
-Analyze the requirements and read any referenced files needed for context:
-
- What are the main requirements?
- What tasks need to be completed?
- Are there dependencies between tasks?
- What is the complexity level?
+- Which files, modules, commands, or tests are relevant?
-### Step 3: Create the task list
+### Step 2: Create the task list
Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
@@ -61,23 +58,26 @@ Use this markdown format for the `plan` content:
Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
-### Step 4: Execute tasks systematically
+### Step 3: Execute tasks systematically
For each task in the list:
-1. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
-2. **Execute the task**: Use appropriate tools to complete the work
-3. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
-4. **Move to next task**: Only ONE task should be in progress at a time
+1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes.
+2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
+3. **Execute the task**: Use appropriate tools to complete the work
+4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
+5. **Move to next task**: Only ONE task should be in progress at a time
Important rules:
+- Always keep the plan aligned with the latest context before executing the next task
- Always call UpdatePlan BEFORE starting work on a task
- Always call UpdatePlan IMMEDIATELY after completing a task
- Always pass the complete current markdown task list, not a partial diff
- Never work on multiple tasks simultaneously
+- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them
- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
-### Step 5: Handle task breakdown
+### Step 4: Handle task breakdown
If during execution you discover a task is more complex than expected:
@@ -90,7 +90,7 @@ If during execution you discover a task is more complex than expected:
```
3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
-### Step 6: Final verification
+### Step 5: Final verification
After all tasks are completed (`[x]`):
@@ -226,26 +226,16 @@ Add implementation notes or findings:
- Solution: Added dataloader batching
```
-## Requirements
-
-This Skill uses standard tools:
-
-- **Read**: To inspect referenced files when needed
-- **UpdatePlan**: To create and update the markdown task list
-- **Bash**: To run tests, builds, or other commands
-- **Write**: To create new files if needed
-
-No additional dependencies required.
-
## Workflow Summary
-1. Analyze the requirements
-2. Read referenced files when needed
-3. Call UpdatePlan with the structured markdown task list
+1. Analyze the requirements and relevant project context
+2. Call UpdatePlan with the structured markdown task list
+3. Refresh the remaining plan before the first task
4. For each task:
- Update to `[>]` with UpdatePlan
- Execute the task
- Update to `[x]` with UpdatePlan
+ - Re-evaluate and revise remaining tasks before moving on
5. Call UpdatePlan with all tasks completed and summarize the result
This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
diff --git a/templates/tools/update-plan.md b/templates/tools/update-plan.md
index 28d12f7..a0f2fd6 100644
--- a/templates/tools/update-plan.md
+++ b/templates/tools/update-plan.md
@@ -8,6 +8,8 @@ Usage:
- The `plan` argument is a markdown string, not an array of step objects.
- Keep exactly one task marked `[>]` while work is in progress.
- Update the plan before starting a task, immediately after completing a task, and whenever tasks are split, merged, reordered, blocked, or changed.
+- Before executing the first task and after completing each task, re-evaluate the latest conversation and project context, then revise the remaining plan if needed.
+- Remove tasks that are no longer relevant, and add newly discovered follow-up tasks before working on them.
```json
{
From 33bcd484aad094b45a48d6808d017d3868b5fe78 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 11:06:06 +0800
Subject: [PATCH 046/129] feat: update update-plan prompt draft
---
docs/SKILL.md | 252 ----------------------------------------------
docs/SKILL_new.md | 21 ++--
2 files changed, 13 insertions(+), 260 deletions(-)
delete mode 100644 docs/SKILL.md
diff --git a/docs/SKILL.md b/docs/SKILL.md
deleted file mode 100644
index deb8c5e..0000000
--- a/docs/SKILL.md
+++ /dev/null
@@ -1,252 +0,0 @@
----
-name: plan-and-execute
-description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements.
----
-
-# Plan and Execute
-
-This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible.
-
-## Quick Start
-
-When you need to work through a multi-step request:
-
-1. Analyze the requirements and explore enough project context
-2. Create a markdown task list by calling the UpdatePlan tool
-3. Execute tasks one by one, updating the tool plan in real time
-4. Revise the remaining plan as new context appears
-
-## Instructions
-
-### Step 1: Analyze the requirements
-
-Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate.
-
-If a required referenced file path is missing, ask for it:
-
-```
-What is the path to the referenced file?
-```
-
-Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
-
-- What are the main requirements?
-- What tasks need to be completed?
-- Are there dependencies between tasks?
-- What is the complexity level?
-- Which files, modules, commands, or tests are relevant?
-
-### Step 2: Create the task list
-
-Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
-
-```json
-{
- "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description"
-}
-```
-
-Use this markdown format for the `plan` content:
-
-```markdown
-## Task List
-
-- [ ] Task 1 description
-- [ ] Task 2 description
-- [ ] Task 3 description
-```
-
-Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
-
-### Step 3: Execute tasks systematically
-
-For each task in the list:
-
-1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes.
-2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
-3. **Execute the task**: Use appropriate tools to complete the work
-4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
-5. **Move to next task**: Only ONE task should be in progress at a time
-
-Important rules:
-- Always keep the plan aligned with the latest context before executing the next task
-- Always call UpdatePlan BEFORE starting work on a task
-- Always call UpdatePlan IMMEDIATELY after completing a task
-- Always pass the complete current markdown task list, not a partial diff
-- Never work on multiple tasks simultaneously
-- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them
-- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
-
-### Step 4: Handle task breakdown
-
-If during execution you discover a task is more complex than expected:
-
-1. Keep the current task as `[>]`
-2. Call UpdatePlan with new sub-tasks below it with indentation:
- ```markdown
- - [>] Main task
- - [ ] Sub-task 1
- - [ ] Sub-task 2
- ```
-3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
-
-### Step 5: Final verification
-
-After all tasks are completed (`[x]`):
-
-1. Review the original requirements to ensure everything is addressed
-2. Run any final checks (tests, builds, linting)
-3. Call UpdatePlan with every task marked `[x]`
-4. Provide a concise completion summary in the final response
-
-## Task State Symbols
-
-- `[ ]` - Pending
-- `[>]` - In progress
-- `[x]` - Completed
-- `[!]` - Blocked
-
-## Examples
-
-### Example 1: Simple feature request
-
-**Example requirements:**
-```markdown
-# Feature: Add dark mode toggle
-
-Users should be able to switch between light and dark themes.
-The toggle should be in the settings page.
-```
-
-**UpdatePlan call after analysis:**
-```markdown
-## Task List
-
-- [ ] Create dark mode toggle component in Settings page
-- [ ] Add dark mode state management (context/store)
-- [ ] Implement CSS-in-JS styles for dark theme
-- [ ] Update existing components to support theme switching
-- [ ] Run tests and verify functionality
-```
-
-**UpdatePlan call during execution:**
-```markdown
-## Task List
-
-- [x] Create dark mode toggle component in Settings page
-- [>] Add dark mode state management (context/store)
-- [ ] Implement CSS-in-JS styles for dark theme
-- [ ] Update existing components to support theme switching
-- [ ] Run tests and verify functionality
-```
-
-### Example 2: Bug fix with investigation
-
-**Example requirements:**
-```markdown
-# Bug: Login form crashes on submit
-
-When users click submit, the app crashes.
-Error message: "Cannot read property 'email' of undefined"
-```
-
-**UpdatePlan call after analysis:**
-```markdown
-## Task List
-
-- [ ] Reproduce the bug locally
-- [ ] Investigate the error in login form component
-- [ ] Identify root cause of undefined email property
-- [ ] Implement fix
-- [ ] Add validation to prevent similar issues
-- [ ] Test the fix with various inputs
-- [ ] Update error handling
-```
-
-## When to Use This Skill
-
-Use this Skill when:
-
-1. **Complex multi-step tasks** - Request requires 3+ distinct steps
-2. **Feature implementation** - Building new functionality from requirements
-3. **Bug fixing** - Need to investigate, fix, and verify
-4. **Refactoring** - Multiple files or components need changes
-5. **Detailed requirements** - Specifications need to be translated into concrete tasks
-6. **Need progress tracking** - Want visible progress without editing source files
-
-## When NOT to Use This Skill
-
-Skip this Skill when:
-
-1. **Single simple task** - Just one straightforward action needed
-2. **Trivial changes** - Quick fixes that don't need planning
-3. **Informational requests** - User just wants explanation, not execution
-4. **No execution requested** - User only wants brainstorming or a high-level explanation
-
-## Best Practices
-
-1. **Be specific with tasks**: "Add login button to navbar" not "Update UI"
-2. **Keep tasks atomic**: Each task should be independently completable
-3. **Update immediately**: Don't batch status updates, do them in real-time
-4. **One task at a time**: Never mark multiple tasks as `[>]`
-5. **Handle blockers**: If stuck, create new tasks to resolve the blocker
-6. **Verify completion**: Only mark `[x]` when task is fully done
-
-## Advanced Usage
-
-### Handling dependencies
-
-When tasks have dependencies, order them properly:
-
-```markdown
-- [ ] Create database schema
-- [ ] Implement API endpoints (depends on schema)
-- [ ] Build frontend forms (depends on API)
-```
-
-### Using sub-tasks
-
-For complex tasks, break them down:
-
-```markdown
-- [>] Implement authentication system
- - [x] Set up JWT library
- - [>] Create login endpoint
- - [ ] Create logout endpoint
- - [ ] Add token refresh logic
-```
-
-### Adding notes
-
-Add implementation notes or findings:
-
-```markdown
-- [x] Investigate performance issue
- - Note: Found N+1 query in user loader
- - Solution: Added dataloader batching
-```
-
-## Requirements
-
-This Skill uses standard tools:
-
-- **Read**: To inspect relevant files and explore project context
-- **UpdatePlan**: To create and update the markdown task list
-- **Bash**: To run tests, builds, or other commands
-- **Write**: To create new files if needed
-
-No additional dependencies required.
-
-## Workflow Summary
-
-1. Analyze the requirements and relevant project context
-2. Call UpdatePlan with the structured markdown task list
-3. Refresh the remaining plan before the first task
-4. For each task:
- - Update to `[>]` with UpdatePlan
- - Execute the task
- - Update to `[x]` with UpdatePlan
- - Re-evaluate and revise remaining tasks before moving on
-5. Call UpdatePlan with all tasks completed and summarize the result
-
-This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md
index 41ed251..9037a00 100644
--- a/docs/SKILL_new.md
+++ b/docs/SKILL_new.md
@@ -12,9 +12,10 @@ This Skill helps you automatically plan and execute requirements. It creates a s
When you need to work through a multi-step request:
1. Analyze the requirements and explore enough project context
-2. Create a markdown task list by calling the UpdatePlan tool
-3. Execute tasks one by one, updating the tool plan in real time
-4. Revise the remaining plan as new context appears
+2. Clarify unclear or ambiguous requirements with AskUserQuestion
+3. Create a markdown task list by calling the UpdatePlan tool
+4. Execute tasks one by one, updating the tool plan in real time
+5. Revise the remaining plan as new context appears
## Instructions
@@ -22,7 +23,9 @@ When you need to work through a multi-step request:
Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate.
-If a required referenced file path is missing, ask for it:
+If the original requirements are unclear, incomplete, or ambiguous, call the AskUserQuestion tool before creating the task list. Ask only the questions needed to avoid implementing the wrong behavior, and keep each question specific to the decision that affects the plan or acceptance criteria.
+
+If a required referenced file path is missing, ask for it with AskUserQuestion:
```
What is the path to the referenced file?
@@ -35,6 +38,7 @@ Referenced files can be in any text format (.md, .txt, etc.) that contains task
- Are there dependencies between tasks?
- What is the complexity level?
- Which files, modules, commands, or tests are relevant?
+- What ambiguity would change the implementation or acceptance criteria?
### Step 2: Create the task list
@@ -229,13 +233,14 @@ Add implementation notes or findings:
## Workflow Summary
1. Analyze the requirements and relevant project context
-2. Call UpdatePlan with the structured markdown task list
-3. Refresh the remaining plan before the first task
-4. For each task:
+2. Call AskUserQuestion if the original requirements are unclear or ambiguous
+3. Call UpdatePlan with the structured markdown task list
+4. Refresh the remaining plan before the first task
+5. For each task:
- Update to `[>]` with UpdatePlan
- Execute the task
- Update to `[x]` with UpdatePlan
- Re-evaluate and revise remaining tasks before moving on
-5. Call UpdatePlan with all tasks completed and summarize the result
+6. Call UpdatePlan with all tasks completed and summarize the result
This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
From 32e5796c9e188defdb69c8f3db75a539f5c9dc0c Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 11:57:39 +0800
Subject: [PATCH 047/129] feat: update update-plan prompt draft
---
docs/SKILL_new.md | 48 +++++++++++++++++-----------------
templates/tools/update-plan.md | 2 +-
2 files changed, 25 insertions(+), 25 deletions(-)
diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md
index 9037a00..9fc8bd2 100644
--- a/docs/SKILL_new.md
+++ b/docs/SKILL_new.md
@@ -116,55 +116,55 @@ After all tasks are completed (`[x]`):
**Example requirements:**
```markdown
-# Feature: Add dark mode toggle
+# 新功能:添加深色模式切换
-Users should be able to switch between light and dark themes.
-The toggle should be in the settings page.
+用户应该能够在浅色和深色主题之间切换。
+切换开关应放在设置页面中。
```
-**UpdatePlan call after analysis:**
+**分析后的 UpdatePlan 调用:**
```markdown
## Task List
-- [ ] Create dark mode toggle component in Settings page
-- [ ] Add dark mode state management (context/store)
-- [ ] Implement CSS-in-JS styles for dark theme
-- [ ] Update existing components to support theme switching
-- [ ] Run tests and verify functionality
+- [ ] 在设置页面创建深色模式切换组件
+- [ ] 添加深色模式状态管理(context/store)
+- [ ] 实现深色主题的 CSS-in-JS 样式
+- [ ] 更新现有组件以支持主题切换
+- [ ] 运行测试并验证功能
```
**UpdatePlan call during execution:**
```markdown
## Task List
-- [x] Create dark mode toggle component in Settings page
-- [>] Add dark mode state management (context/store)
-- [ ] Implement CSS-in-JS styles for dark theme
-- [ ] Update existing components to support theme switching
-- [ ] Run tests and verify functionality
+- [x] 在设置页面创建深色模式切换组件
+- [>] 添加深色模式状态管理(context/store)
+- [ ] 实现深色主题的 CSS-in-JS 样式
+- [ ] 更新现有组件以支持主题切换
+- [ ] 运行测试并验证功能
```
### Example 2: Bug fix with investigation
**Example requirements:**
```markdown
-# Bug: Login form crashes on submit
+# Fix bug:登录表单提交时崩溃
-When users click submit, the app crashes.
-Error message: "Cannot read property 'email' of undefined"
+当用户点击提交时,应用崩溃。
+错误信息:"Cannot read property 'email' of undefined"
```
**UpdatePlan call after analysis:**
```markdown
## Task List
-- [ ] Reproduce the bug locally
-- [ ] Investigate the error in login form component
-- [ ] Identify root cause of undefined email property
-- [ ] Implement fix
-- [ ] Add validation to prevent similar issues
-- [ ] Test the fix with various inputs
-- [ ] Update error handling
+- [ ] 在本地复现缺陷
+- [ ] 调查登录表单组件中的错误
+- [ ] 定位 undefined email 属性的根本原因
+- [ ] 实施修复
+- [ ] 添加验证以防止类似问题
+- [ ] 使用各种输入测试修复
+- [ ] 更新错误处理
```
## When to Use This Skill
diff --git a/templates/tools/update-plan.md b/templates/tools/update-plan.md
index a0f2fd6..0c74b36 100644
--- a/templates/tools/update-plan.md
+++ b/templates/tools/update-plan.md
@@ -5,7 +5,7 @@ Updates the current task plan and progress display.
Usage:
- Use this tool for non-trivial multi-step tasks when a task list helps track execution progress.
- Pass the complete current task list every time. The latest call replaces the previous visible plan.
-- The `plan` argument is a markdown string, not an array of step objects.
+- The `plan` argument is a markdown string, not an array of step objects. If the requirement is in Chinese, then use Chinese for the markdown as well.
- Keep exactly one task marked `[>]` while work is in progress.
- Update the plan before starting a task, immediately after completing a task, and whenever tasks are split, merged, reordered, blocked, or changed.
- Before executing the first task and after completing each task, re-evaluate the latest conversation and project context, then revise the remaining plan if needed.
From 77a779fe341aaa81a726dea319bd2b85c7fb2ab9 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 14:29:34 +0800
Subject: [PATCH 048/129] feat: change the UpdatePlan display
---
src/session.ts | 2 ++
src/tests/session.test.ts | 21 +++++++++++++++++++++
2 files changed, 23 insertions(+)
diff --git a/src/session.ts b/src/session.ts
index 431eb40..dde29b3 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -2032,6 +2032,8 @@ ${skillMd}
if (description) {
return description;
}
+ } else if (toolName === "UpdatePlan") {
+ return typeof args.explanation === "string" ? args.explanation.trim() : "";
}
const firstKey = Object.keys(args)[0];
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index e5ab740..7bc98f9 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -1160,6 +1160,27 @@ test("buildOpenAIMessages preserves a real failed tool result", () => {
assert.doesNotMatch(openAIMessages[1]?.content ?? "", /Previous tool call did not complete/);
});
+test("UpdatePlan tool params only show explanation when provided", () => {
+ const manager = createSessionManager(process.cwd(), "machine-id-update-plan-params");
+ const plan = "## Task List\n\n- [ ] Inspect project";
+
+ const withExplanation = (manager as any).buildToolMessage(
+ "session-1",
+ "call-plan-1",
+ JSON.stringify({ ok: true, name: "UpdatePlan", output: "Plan updated." }),
+ { name: "UpdatePlan", arguments: JSON.stringify({ plan, explanation: "Start planning" }) }
+ ) as SessionMessage;
+ const withoutExplanation = (manager as any).buildToolMessage(
+ "session-1",
+ "call-plan-2",
+ JSON.stringify({ ok: true, name: "UpdatePlan", output: "Plan updated." }),
+ { name: "UpdatePlan", arguments: JSON.stringify({ plan }) }
+ ) as SessionMessage;
+
+ assert.equal(withExplanation.meta?.paramsMd, "Start planning");
+ assert.equal(withoutExplanation.meta?.paramsMd, "");
+});
+
test("buildOpenAIMessages repairs mixed missing duplicate and orphan tool messages", () => {
const manager = createSessionManager(process.cwd(), "machine-id-mixed-tool-badcase");
const assistantMessage = (manager as any).buildAssistantMessage(
From e691b3efcf32e24d4521a29e17b0a512960d2a79 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 15:39:26 +0800
Subject: [PATCH 049/129] feat: add default skill templates and update session
management to include skill prompts
---
package.json | 1 +
src/prompt.ts | 198 ++++-----------------
src/session.ts | 28 ++-
src/tests/prompt.test.ts | 36 +++-
src/tests/session.test.ts | 35 ++++
templates/skills/agent-drift-guard.md | 152 ++++++++++++++++
templates/skills/plan-and-execute.md | 246 ++++++++++++++++++++++++++
7 files changed, 528 insertions(+), 168 deletions(-)
create mode 100644 templates/skills/agent-drift-guard.md
create mode 100644 templates/skills/plan-and-execute.md
diff --git a/package.json b/package.json
index 90c2b51..e61d81d 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"dist/cli.js",
"templates/tools/**",
"templates/prompts/**",
+ "templates/skills/**",
"README.md",
"LICENSE"
],
diff --git a/src/prompt.ts b/src/prompt.ts
index 50aa2a3..717991b 100644
--- a/src/prompt.ts
+++ b/src/prompt.ts
@@ -8,161 +8,6 @@ import type { SessionMessage } from "./session";
import { findGitBashPath, resolveShellPath } from "./common/shell-utils";
import { supportsMultimodal } from "./common/model-capabilities";
-export const AGENT_DRIFT_GUARD_SKILL = `
----
-name: agent-drift-guard
-description: Detect and correct execution drift while working on user requests. Use when you are actively implementing, debugging, reviewing, or investigating and there is a risk of wandering beyond the user's goal, adding unrequested work, touching live systems, over-exploring, or ignoring repeated user boundary corrections. Especially useful during multi-step coding tasks, production-adjacent requests, ambiguous scopes, and anytime you should self-check whether it is still solving the requested problem.
----
-
-# Agent Drift Guard
-
-Keep execution tightly aligned with the user's actual request.
-
-## Quick Start
-
-Run this mental check before substantial work and again whenever the plan expands:
-
-1. State the user's requested outcome in one sentence.
-2. List explicit non-goals or boundaries the user has set.
-3. Ask whether the next action directly advances the requested outcome.
-4. If not, either cut it or pause to confirm.
-
-## Drift Signals
-
-Treat these as warning signs that execution may be drifting:
-
-- Exploring broadly before opening the most relevant file, command, or artifact.
-- Solving adjacent operational issues when the user asked only for code changes.
-- Adding extra safeguards, scripts, docs, refactors, or cleanup that the user did not ask for.
-- Reframing the task around what seems "better" instead of what was requested.
-- Continuing with a broader plan after the user narrows the scope.
-- Repeating searches or tool calls without increasing certainty.
-- Mixing diagnosis, remediation, and feature work when the user asked for only one of them.
-- Touching production-like state, external systems, or live data without explicit permission.
-
-## Severity Levels
-
-### Level 1: Mild Drift
-
-Examples:
-- One or two extra exploratory commands.
-- Considering a broader solution but not acting on it yet.
-- Briefly over-explaining instead of moving the task forward.
-
-Response:
-- Auto-correct silently.
-- Narrow to the smallest next action.
-- Do not interrupt the user.
-
-### Level 2: Material Drift
-
-Examples:
-- Planning additional deliverables not requested.
-- Writing helper scripts, migrations, docs, or tests outside the asked scope.
-- Expanding from code changes into operational fixes.
-- Continuing after the user has already corrected the scope once.
-
-Response:
-- Stop and realign internally first.
-- If the broader action is avoidable, drop it and continue on scope.
-- If the broader action has non-obvious tradeoffs, ask a brief confirmation question.
-
-### Level 3: Boundary or Risk Violation
-
-Examples:
-- Modifying live systems, production data, external services, or user-owned state without being asked.
-- Taking destructive or hard-to-reverse actions outside the requested scope.
-- Ignoring repeated user instructions about what not to do.
-
-Response:
-- Pause before acting.
-- Surface the exact boundary and ask for confirmation.
-- Offer the smallest on-scope option first.
-
-## Self-Check Loop
-
-Use this loop during execution:
-
-### Before the first meaningful action
-
-Write down mentally:
-- Requested outcome
-- Allowed scope
-- Forbidden scope
-- Smallest useful next step
-
-### After each non-trivial step
-
-Ask:
-- Did this step directly help deliver the requested outcome?
-- Did I learn something that changes scope, or only implementation?
-- Am I about to do more than the user asked?
-
-### After a user correction
-
-Treat the correction as a hard boundary update.
-
-Then:
-- Remove the old broader plan.
-- Do not defend the discarded work.
-- Continue from the narrowed scope.
-- If needed, acknowledge briefly and move on.
-
-## Decision Rules
-
-Use these rules in order:
-
-1. Prefer the most direct artifact first.
- - Open the relevant file before scanning the whole repo.
- - Inspect the specific failing path before designing a general framework.
-
-2. Prefer the smallest complete fix.
- - Solve the asked problem before improving related systems.
- - Avoid bonus work unless it is required for correctness.
-
-3. Prefer internal correction over user interruption.
- - If you can shrink back to scope confidently, do it.
- - Ask only when the next step changes deliverables, risk, or ownership.
-
-4. Treat repeated user constraints as priority signals.
- - A repeated instruction means your execution style is currently misaligned.
- - Tighten scope immediately.
-
-5. Separate categories of work.
- - Code change, investigation, production remediation, cleanup, and documentation are distinct tasks unless the user explicitly combines them.
-
-## Good Intervention Style
-
-When you must pause, keep it short and specific:
-
-- State the potential drift in one sentence.
-- Name the tradeoff or boundary.
-- Offer the smallest on-scope option first.
-
-Example:
-
-"Quick alignment check: I can keep this to the code fix only, or also add an ops cleanup step. I'll stick to the code fix unless you want both."
-
-## Anti-Patterns
-
-Do not:
-
-- Create cleanup scripts, docs, or side tools just because they seem useful.
-- Broaden the task after discovering a neighboring problem.
-- Continue with a plan the user has already rejected.
-- Justify drift with "best practice" when the user asked for a narrower deliverable.
-- Hide extra work inside a larger patch.
-
-## Final Check Before Responding
-
-Before sending the final answer, verify:
-
-- The delivered work matches the requested outcome.
-- No extra deliverables were added without confirmation.
-- Any assumptions are stated briefly.
-- Suggested next steps are optional, not bundled into the completed work.
-`;
-
const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
@@ -254,6 +99,8 @@ type PromptToolOptions = {
webSearchEnabled?: boolean;
};
+const DEFAULT_SKILL_TEMPLATES = ["agent-drift-guard.md", "plan-and-execute.md"];
+
function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): string {
const toolsDir = path.join(extensionRoot, "templates", "tools");
if (!fs.existsSync(toolsDir)) {
@@ -281,6 +128,35 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s
return docs.join("\n\n");
}
+function readDefaultSkillDocs(extensionRoot: string): Array<{ name: string; content: string }> {
+ const skillsDir = path.join(extensionRoot, "templates", "skills");
+ return DEFAULT_SKILL_TEMPLATES.map((entry) => {
+ const fullPath = path.join(skillsDir, entry);
+ try {
+ return {
+ name: path.basename(entry, ".md"),
+ content: fs.readFileSync(fullPath, "utf8").trim(),
+ };
+ } catch {
+ return null;
+ }
+ }).filter((skill): skill is { name: string; content: string } => Boolean(skill?.content));
+}
+
+export function getDefaultSkillPrompt(): string {
+ const skillDocs = readDefaultSkillDocs(getExtensionRoot());
+ if (skillDocs.length === 0) {
+ return "";
+ }
+
+ const blocks = skillDocs.map(
+ (skill) => `<${skill.name}-skill>
+${skill.content}
+${skill.name}-skill>`
+ );
+ return `Use the skill documents below to assist the user:\n${blocks.join("\n\n")}`;
+}
+
function getCurrentDateAndModelPrompt(model?: string): string {
const date = new Date();
let prompt = `今天是${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日。随着对话的进行,时间在流逝。`;
@@ -288,10 +164,10 @@ function getCurrentDateAndModelPrompt(model?: string): string {
return prompt;
}
-export function getSystemPrompt(projectRoot: string, options: PromptToolOptions = {}): string {
+export function getSystemPrompt(_projectRoot: string, options: PromptToolOptions = {}): string {
const toolDocs = readToolDocs(getExtensionRoot(), options);
const basePrompt = toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE;
- return `${basePrompt}\n\n${getCurrentDateAndModelPrompt(options.model)}\n\n${getRuntimeContext(projectRoot)}`;
+ return basePrompt;
}
export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
@@ -310,7 +186,7 @@ export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
return `${COMPACT_PROMPT_BASE}\n\nconversation below:\n\n\`\`\`jsonl\n${jsonl}\n\`\`\``;
}
-function getRuntimeContext(projectRoot: string): string {
+export function getRuntimeContext(projectRoot: string, model?: string): string {
const uname = getUnameInfo();
const shellPath = getShellPathInfo();
const shellModeOpts = process.platform === "win32" ? { "shell mode": "git-bash" } : {};
@@ -328,7 +204,11 @@ function getRuntimeContext(projectRoot: string): string {
jq: checkToolInstalled("jq"),
},
};
- return `# Local Workspace Environment\n\n\`\`\`json
+ return `${getCurrentDateAndModelPrompt(model)}
+
+# Local Workspace Environment
+
+\`\`\`json
${JSON.stringify(env, null, 2)}
\`\`\``;
}
diff --git a/src/session.ts b/src/session.ts
index dde29b3..1c9d3b6 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -9,7 +9,14 @@ import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "open
import { launchNotifyScript } from "./common/notify";
import { buildThinkingRequestOptions } from "./common/openai-thinking";
import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities";
-import { getCompactPrompt, getSystemPrompt, getTools, AGENT_DRIFT_GUARD_SKILL, type ToolDefinition } from "./prompt";
+import {
+ getCompactPrompt,
+ getDefaultSkillPrompt,
+ getRuntimeContext,
+ getSystemPrompt,
+ getTools,
+ type ToolDefinition,
+} from "./prompt";
import { ToolExecutor, type CreateOpenAIClient } from "./tools/executor";
import { McpManager } from "./mcp/mcp-manager";
import type { McpServerConfig } from "./settings";
@@ -907,20 +914,29 @@ The candidate skills are as follows:\n\n`;
this.saveSessionsIndex(index);
this.removeSessionMessages(droppedEntries.map((item) => item.id));
- const systemPrompt = getSystemPrompt(this.projectRoot, this.getPromptToolOptions());
+ const promptToolOptions = this.getPromptToolOptions();
+ const systemPrompt = getSystemPrompt(this.projectRoot, promptToolOptions);
const systemMessage = this.buildSystemMessage(sessionId, systemPrompt);
this.appendSessionMessage(sessionId, systemMessage);
+ const defaultSkillPrompt = getDefaultSkillPrompt();
+ if (defaultSkillPrompt) {
+ const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt);
+ this.appendSessionMessage(sessionId, defaultSkillMessage);
+ }
+
+ const runtimeContextMessage = this.buildSystemMessage(
+ sessionId,
+ getRuntimeContext(this.projectRoot, promptToolOptions.model)
+ );
+ this.appendSessionMessage(sessionId, runtimeContextMessage);
+
const agentInstructions = this.loadAgentInstructions();
if (agentInstructions) {
const instructionsMessage = this.buildSystemMessage(sessionId, agentInstructions);
this.appendSessionMessage(sessionId, instructionsMessage);
}
- const defaultSkillPrompt = `Use the skill document below to assist the user:\n${AGENT_DRIFT_GUARD_SKILL}`;
- const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt);
- this.appendSessionMessage(sessionId, defaultSkillMessage);
-
const userMessage = this.buildUserMessage(sessionId, userPrompt);
this.appendSessionMessage(sessionId, userMessage);
diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts
index b7c9178..cc86712 100644
--- a/src/tests/prompt.test.ts
+++ b/src/tests/prompt.test.ts
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
-import { getSystemPrompt, getTools } from "../prompt";
+import { getDefaultSkillPrompt, getRuntimeContext, getSystemPrompt, getTools } from "../prompt";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -30,11 +30,39 @@ test("getSystemPrompt includes UpdatePlan docs", () => {
assert.equal(prompt.includes("The `plan` argument is a markdown string, not an array of step objects."), true);
});
-test("getSystemPrompt includes current date guidance", () => {
+test("getSystemPrompt does not include runtime context", () => {
+ const prompt = getSystemPrompt("/tmp/project");
+ assert.equal(prompt.includes("# Local Workspace Environment"), false);
+ assert.equal(prompt.includes('"root path": "/tmp/project"'), false);
+});
+
+test("getDefaultSkillPrompt loads default skill templates in order", () => {
+ const prompt = getDefaultSkillPrompt();
+ const agentDriftIndex = prompt.indexOf("");
+ const planIndex = prompt.indexOf("");
+
+ assert.notEqual(agentDriftIndex, -1);
+ assert.notEqual(planIndex, -1);
+ assert.equal(agentDriftIndex < planIndex, true);
+ assert.equal(prompt.includes("Use the skill documents below to assist the user:"), true);
+ assert.equal(prompt.includes('path="templates/skills/'), false);
+});
+
+test("getSystemPrompt does not include current date guidance", () => {
const now = new Date();
const expected = `今天是${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日。随着对话的进行,时间在流逝。`;
const prompt = getSystemPrompt("/tmp/project");
- assert.equal(prompt.includes(expected), true);
+ assert.equal(prompt.includes(expected), false);
+});
+
+test("getRuntimeContext includes current date and model guidance", () => {
+ const now = new Date();
+ const expectedDate = `今天是${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日。随着对话的进行,时间在流逝。`;
+ const prompt = getRuntimeContext("/tmp/project", "deepseek-v4-pro");
+ assert.equal(prompt.includes(expectedDate), true);
+ assert.equal(prompt.includes("当前LLM模型为deepseek-v4-pro,对话中可通过/model命令切换模型。"), true);
+ assert.equal(prompt.includes("# Local Workspace Environment"), true);
+ assert.equal(prompt.includes('"root path": "/tmp/project"'), true);
});
test("getSystemPrompt renders Read docs for non-multimodal models", () => {
@@ -47,6 +75,8 @@ test("runtime prompt assets live under templates", () => {
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "web-search.md")), true);
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md.ejs")), true);
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "prompts", "init_command.md.ejs")), true);
+ assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "agent-drift-guard.md")), true);
+ assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "plan-and-execute.md")), true);
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md")), false);
assert.equal(fs.existsSync(path.join(repoRoot, "docs", "tools")), false);
assert.equal(fs.existsSync(path.join(repoRoot, "docs", "prompts")), false);
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index 7bc98f9..10e3b2c 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -609,6 +609,37 @@ test("createSession stores /init and sends the active .deepcode project AGENTS p
assert.ok(!systemContents.includes("root project instructions"));
});
+test("createSession appends default system prompts in prefix-cache-friendly order", async () => {
+ const workspace = createTempDir("deepcode-system-order-workspace-");
+ const home = createTempDir("deepcode-system-order-home-");
+ setHomeDir(home);
+ globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch;
+
+ fs.writeFileSync(path.join(workspace, "AGENTS.md"), "root project instructions", "utf8");
+
+ const manager = createSessionManager(workspace, "machine-id-system-order");
+ (manager as any).activateSession = async () => {};
+
+ const sessionId = await manager.createSession({ text: "hello" });
+ const systemContents = manager
+ .listSessionMessages(sessionId)
+ .filter((message) => message.role === "system")
+ .map((message) => message.content ?? "");
+
+ assert.equal(systemContents.length >= 4, true);
+ assert.match(systemContents[0] ?? "", /# Available Tools/);
+ assert.doesNotMatch(systemContents[0] ?? "", /# Local Workspace Environment/);
+ assert.doesNotMatch(systemContents[0] ?? "", /当前LLM模型为test-model/);
+ assert.match(systemContents[1] ?? "", //);
+ assert.match(systemContents[1] ?? "", //);
+ assert.doesNotMatch(systemContents[1] ?? "", /path="templates\/skills\//);
+ assert.doesNotMatch(systemContents[1] ?? "", /当前LLM模型为test-model/);
+ assert.match(systemContents[2] ?? "", /# Local Workspace Environment/);
+ assert.match(systemContents[2] ?? "", /当前LLM模型为test-model/);
+ assert.match(systemContents[2] ?? "", new RegExp(escapeRegExp(`"root path": "${workspace}"`)));
+ assert.equal(systemContents[3], "root project instructions");
+});
+
test("replySession stores /init and sends the active root project AGENTS path to the LLM", async () => {
const workspace = createTempDir("deepcode-init-root-workspace-");
const home = createTempDir("deepcode-init-root-home-");
@@ -1686,6 +1717,10 @@ function createTempDir(prefix: string): string {
return dir;
}
+function escapeRegExp(value: string): string {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
async function flushPromises(): Promise {
await new Promise((resolve) => setImmediate(resolve));
}
diff --git a/templates/skills/agent-drift-guard.md b/templates/skills/agent-drift-guard.md
new file mode 100644
index 0000000..c6711b1
--- /dev/null
+++ b/templates/skills/agent-drift-guard.md
@@ -0,0 +1,152 @@
+---
+name: agent-drift-guard
+description: Detect and correct execution drift while working on user requests. Use when you are actively implementing, debugging, reviewing, or investigating and there is a risk of wandering beyond the user's goal, adding unrequested work, touching live systems, over-exploring, or ignoring repeated user boundary corrections. Especially useful during multi-step coding tasks, production-adjacent requests, ambiguous scopes, and anytime you should self-check whether it is still solving the requested problem.
+---
+
+# Agent Drift Guard
+
+Keep execution tightly aligned with the user's actual request.
+
+## Quick Start
+
+Run this mental check before substantial work and again whenever the plan expands:
+
+1. State the user's requested outcome in one sentence.
+2. List explicit non-goals or boundaries the user has set.
+3. Ask whether the next action directly advances the requested outcome.
+4. If not, either cut it or pause to confirm.
+
+## Drift Signals
+
+Treat these as warning signs that execution may be drifting:
+
+- Exploring broadly before opening the most relevant file, command, or artifact.
+- Solving adjacent operational issues when the user asked only for code changes.
+- Adding extra safeguards, scripts, docs, refactors, or cleanup that the user did not ask for.
+- Reframing the task around what seems "better" instead of what was requested.
+- Continuing with a broader plan after the user narrows the scope.
+- Repeating searches or tool calls without increasing certainty.
+- Mixing diagnosis, remediation, and feature work when the user asked for only one of them.
+- Touching production-like state, external systems, or live data without explicit permission.
+
+## Severity Levels
+
+### Level 1: Mild Drift
+
+Examples:
+- One or two extra exploratory commands.
+- Considering a broader solution but not acting on it yet.
+- Briefly over-explaining instead of moving the task forward.
+
+Response:
+- Auto-correct silently.
+- Narrow to the smallest next action.
+- Do not interrupt the user.
+
+### Level 2: Material Drift
+
+Examples:
+- Planning additional deliverables not requested.
+- Writing helper scripts, migrations, docs, or tests outside the asked scope.
+- Expanding from code changes into operational fixes.
+- Continuing after the user has already corrected the scope once.
+
+Response:
+- Stop and realign internally first.
+- If the broader action is avoidable, drop it and continue on scope.
+- If the broader action has non-obvious tradeoffs, ask a brief confirmation question.
+
+### Level 3: Boundary or Risk Violation
+
+Examples:
+- Modifying live systems, production data, external services, or user-owned state without being asked.
+- Taking destructive or hard-to-reverse actions outside the requested scope.
+- Ignoring repeated user instructions about what not to do.
+
+Response:
+- Pause before acting.
+- Surface the exact boundary and ask for confirmation.
+- Offer the smallest on-scope option first.
+
+## Self-Check Loop
+
+Use this loop during execution:
+
+### Before the first meaningful action
+
+Write down mentally:
+- Requested outcome
+- Allowed scope
+- Forbidden scope
+- Smallest useful next step
+
+### After each non-trivial step
+
+Ask:
+- Did this step directly help deliver the requested outcome?
+- Did I learn something that changes scope, or only implementation?
+- Am I about to do more than the user asked?
+
+### After a user correction
+
+Treat the correction as a hard boundary update.
+
+Then:
+- Remove the old broader plan.
+- Do not defend the discarded work.
+- Continue from the narrowed scope.
+- If needed, acknowledge briefly and move on.
+
+## Decision Rules
+
+Use these rules in order:
+
+1. Prefer the most direct artifact first.
+ - Open the relevant file before scanning the whole repo.
+ - Inspect the specific failing path before designing a general framework.
+
+2. Prefer the smallest complete fix.
+ - Solve the asked problem before improving related systems.
+ - Avoid bonus work unless it is required for correctness.
+
+3. Prefer internal correction over user interruption.
+ - If you can shrink back to scope confidently, do it.
+ - Ask only when the next step changes deliverables, risk, or ownership.
+
+4. Treat repeated user constraints as priority signals.
+ - A repeated instruction means your execution style is currently misaligned.
+ - Tighten scope immediately.
+
+5. Separate categories of work.
+ - Code change, investigation, production remediation, cleanup, and documentation are distinct tasks unless the user explicitly combines them.
+
+## Good Intervention Style
+
+When you must pause, keep it short and specific:
+
+- State the potential drift in one sentence.
+- Name the tradeoff or boundary.
+- Offer the smallest on-scope option first.
+
+Example:
+
+"Quick alignment check: I can keep this to the code fix only, or also add an ops cleanup step. I'll stick to the code fix unless you want both."
+
+## Anti-Patterns
+
+Do not:
+
+- Create cleanup scripts, docs, or side tools just because they seem useful.
+- Broaden the task after discovering a neighboring problem.
+- Continue with a plan the user has already rejected.
+- Justify drift with "best practice" when the user asked for a narrower deliverable.
+- Hide extra work inside a larger patch.
+
+## Final Check Before Responding
+
+Before sending the final answer, verify:
+
+- The delivered work matches the requested outcome.
+- No extra deliverables were added without confirmation.
+- Any assumptions are stated briefly.
+- Suggested next steps are optional, not bundled into the completed work.
diff --git a/templates/skills/plan-and-execute.md b/templates/skills/plan-and-execute.md
new file mode 100644
index 0000000..9fc8bd2
--- /dev/null
+++ b/templates/skills/plan-and-execute.md
@@ -0,0 +1,246 @@
+---
+name: plan-and-execute
+description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements.
+---
+
+# Plan and Execute
+
+This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible.
+
+## Quick Start
+
+When you need to work through a multi-step request:
+
+1. Analyze the requirements and explore enough project context
+2. Clarify unclear or ambiguous requirements with AskUserQuestion
+3. Create a markdown task list by calling the UpdatePlan tool
+4. Execute tasks one by one, updating the tool plan in real time
+5. Revise the remaining plan as new context appears
+
+## Instructions
+
+### Step 1: Analyze the requirements
+
+Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate.
+
+If the original requirements are unclear, incomplete, or ambiguous, call the AskUserQuestion tool before creating the task list. Ask only the questions needed to avoid implementing the wrong behavior, and keep each question specific to the decision that affects the plan or acceptance criteria.
+
+If a required referenced file path is missing, ask for it with AskUserQuestion:
+
+```
+What is the path to the referenced file?
+```
+
+Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements.
+
+- What are the main requirements?
+- What tasks need to be completed?
+- Are there dependencies between tasks?
+- What is the complexity level?
+- Which files, modules, commands, or tests are relevant?
+- What ambiguity would change the implementation or acceptance criteria?
+
+### Step 2: Create the task list
+
+Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape:
+
+```json
+{
+ "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description"
+}
+```
+
+Use this markdown format for the `plan` content:
+
+```markdown
+## Task List
+
+- [ ] Task 1 description
+- [ ] Task 2 description
+- [ ] Task 3 description
+```
+
+Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list.
+
+### Step 3: Execute tasks systematically
+
+For each task in the list:
+
+1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes.
+2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]`
+3. **Execute the task**: Use appropriate tools to complete the work
+4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished
+5. **Move to next task**: Only ONE task should be in progress at a time
+
+Important rules:
+- Always keep the plan aligned with the latest context before executing the next task
+- Always call UpdatePlan BEFORE starting work on a task
+- Always call UpdatePlan IMMEDIATELY after completing a task
+- Always pass the complete current markdown task list, not a partial diff
+- Never work on multiple tasks simultaneously
+- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them
+- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers
+
+### Step 4: Handle task breakdown
+
+If during execution you discover a task is more complex than expected:
+
+1. Keep the current task as `[>]`
+2. Call UpdatePlan with new sub-tasks below it with indentation:
+ ```markdown
+ - [>] Main task
+ - [ ] Sub-task 1
+ - [ ] Sub-task 2
+ ```
+3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan
+
+### Step 5: Final verification
+
+After all tasks are completed (`[x]`):
+
+1. Review the original requirements to ensure everything is addressed
+2. Run any final checks (tests, builds, linting)
+3. Call UpdatePlan with every task marked `[x]`
+4. Provide a concise completion summary in the final response
+
+## Task State Symbols
+
+- `[ ]` - Pending
+- `[>]` - In progress
+- `[x]` - Completed
+- `[!]` - Blocked
+
+## Examples
+
+### Example 1: Simple feature request
+
+**Example requirements:**
+```markdown
+# 新功能:添加深色模式切换
+
+用户应该能够在浅色和深色主题之间切换。
+切换开关应放在设置页面中。
+```
+
+**分析后的 UpdatePlan 调用:**
+```markdown
+## Task List
+
+- [ ] 在设置页面创建深色模式切换组件
+- [ ] 添加深色模式状态管理(context/store)
+- [ ] 实现深色主题的 CSS-in-JS 样式
+- [ ] 更新现有组件以支持主题切换
+- [ ] 运行测试并验证功能
+```
+
+**UpdatePlan call during execution:**
+```markdown
+## Task List
+
+- [x] 在设置页面创建深色模式切换组件
+- [>] 添加深色模式状态管理(context/store)
+- [ ] 实现深色主题的 CSS-in-JS 样式
+- [ ] 更新现有组件以支持主题切换
+- [ ] 运行测试并验证功能
+```
+
+### Example 2: Bug fix with investigation
+
+**Example requirements:**
+```markdown
+# Fix bug:登录表单提交时崩溃
+
+当用户点击提交时,应用崩溃。
+错误信息:"Cannot read property 'email' of undefined"
+```
+
+**UpdatePlan call after analysis:**
+```markdown
+## Task List
+
+- [ ] 在本地复现缺陷
+- [ ] 调查登录表单组件中的错误
+- [ ] 定位 undefined email 属性的根本原因
+- [ ] 实施修复
+- [ ] 添加验证以防止类似问题
+- [ ] 使用各种输入测试修复
+- [ ] 更新错误处理
+```
+
+## When to Use This Skill
+
+Use this Skill when:
+
+1. **Complex multi-step tasks** - Request requires 3+ distinct steps
+2. **Feature implementation** - Building new functionality from requirements
+3. **Bug fixing** - Need to investigate, fix, and verify
+4. **Refactoring** - Multiple files or components need changes
+5. **Detailed requirements** - Specifications need to be translated into concrete tasks
+6. **Need progress tracking** - Want visible progress without editing source files
+
+## When NOT to Use This Skill
+
+Skip this Skill when:
+
+1. **Single simple task** - Just one straightforward action needed
+2. **Trivial changes** - Quick fixes that don't need planning
+3. **Informational requests** - User just wants explanation, not execution
+4. **No execution requested** - User only wants brainstorming or a high-level explanation
+
+## Best Practices
+
+1. **Be specific with tasks**: "Add login button to navbar" not "Update UI"
+2. **Keep tasks atomic**: Each task should be independently completable
+3. **Update immediately**: Don't batch status updates, do them in real-time
+4. **One task at a time**: Never mark multiple tasks as `[>]`
+5. **Handle blockers**: If stuck, create new tasks to resolve the blocker
+6. **Verify completion**: Only mark `[x]` when task is fully done
+
+## Advanced Usage
+
+### Handling dependencies
+
+When tasks have dependencies, order them properly:
+
+```markdown
+- [ ] Create database schema
+- [ ] Implement API endpoints (depends on schema)
+- [ ] Build frontend forms (depends on API)
+```
+
+### Using sub-tasks
+
+For complex tasks, break them down:
+
+```markdown
+- [>] Implement authentication system
+ - [x] Set up JWT library
+ - [>] Create login endpoint
+ - [ ] Create logout endpoint
+ - [ ] Add token refresh logic
+```
+
+### Adding notes
+
+Add implementation notes or findings:
+
+```markdown
+- [x] Investigate performance issue
+ - Note: Found N+1 query in user loader
+ - Solution: Added dataloader batching
+```
+
+## Workflow Summary
+
+1. Analyze the requirements and relevant project context
+2. Call AskUserQuestion if the original requirements are unclear or ambiguous
+3. Call UpdatePlan with the structured markdown task list
+4. Refresh the remaining plan before the first task
+5. For each task:
+ - Update to `[>]` with UpdatePlan
+ - Execute the task
+ - Update to `[x]` with UpdatePlan
+ - Re-evaluate and revise remaining tasks before moving on
+6. Call UpdatePlan with all tasks completed and summarize the result
+
+This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them.
From 486e649adbe93f4dd7db62a0b433bc54351d254a Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 16:29:20 +0800
Subject: [PATCH 050/129] feat: update createSession test to validate
environment JSON structure and root path
---
src/tests/session.test.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index 10e3b2c..a178726 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -636,7 +636,10 @@ test("createSession appends default system prompts in prefix-cache-friendly orde
assert.doesNotMatch(systemContents[1] ?? "", /当前LLM模型为test-model/);
assert.match(systemContents[2] ?? "", /# Local Workspace Environment/);
assert.match(systemContents[2] ?? "", /当前LLM模型为test-model/);
- assert.match(systemContents[2] ?? "", new RegExp(escapeRegExp(`"root path": "${workspace}"`)));
+ const environmentJsonMatch = (systemContents[2] ?? "").match(/```json\n([\s\S]+?)\n```/);
+ assert.ok(environmentJsonMatch);
+ const environmentInfo = JSON.parse(environmentJsonMatch[1] ?? "{}") as { "root path"?: string };
+ assert.equal(environmentInfo["root path"], workspace);
assert.equal(systemContents[3], "root project instructions");
});
From 08ba8d34b298f045f0fd908317e543dcfa6644e6 Mon Sep 17 00:00:00 2001
From: Ji Zhang
Date: Mon, 18 May 2026 18:05:10 +0800
Subject: [PATCH 051/129] feat: add file mention functionality with scanning
and filtering capabilities
---
src/tests/fileMentions.test.ts | 157 +++++++++++++
src/ui/App.tsx | 1 +
src/ui/PromptInput.tsx | 110 ++++++++-
src/ui/fileMentions.ts | 410 +++++++++++++++++++++++++++++++++
src/ui/index.ts | 9 +
5 files changed, 682 insertions(+), 5 deletions(-)
create mode 100644 src/tests/fileMentions.test.ts
create mode 100644 src/ui/fileMentions.ts
diff --git a/src/tests/fileMentions.test.ts b/src/tests/fileMentions.test.ts
new file mode 100644
index 0000000..57f078e
--- /dev/null
+++ b/src/tests/fileMentions.test.ts
@@ -0,0 +1,157 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import * as fs from "fs";
+import * as os from "os";
+import * as path from "path";
+import {
+ filterFileMentionItems,
+ formatFileMentionPath,
+ getCurrentFileMentionToken,
+ replaceCurrentFileMentionToken,
+ scanFileMentionItems,
+ type FileMentionItem,
+} from "../ui/fileMentions";
+
+test("getCurrentFileMentionToken detects bare @file tokens under the cursor", () => {
+ assert.deepEqual(getCurrentFileMentionToken({ text: "review @src/app.ts please", cursor: 10 }), {
+ query: "src/app.ts",
+ start: 7,
+ end: 18,
+ quoted: false,
+ });
+ assert.deepEqual(getCurrentFileMentionToken({ text: "@", cursor: 1 }), {
+ query: "",
+ start: 0,
+ end: 1,
+ quoted: false,
+ });
+ assert.equal(getCurrentFileMentionToken({ text: "foo@bar", cursor: 7 }), null);
+});
+
+test("getCurrentFileMentionToken supports quoted paths with spaces", () => {
+ assert.deepEqual(getCurrentFileMentionToken({ text: 'open @"docs/my file.md"', cursor: 22 }), {
+ query: "docs/my file.md",
+ start: 5,
+ end: 23,
+ quoted: true,
+ });
+ assert.deepEqual(getCurrentFileMentionToken({ text: 'open @"docs/my', cursor: 14 }), {
+ query: "docs/my",
+ start: 5,
+ end: 14,
+ quoted: true,
+ });
+ assert.equal(getCurrentFileMentionToken({ text: 'open @"docs/my file.md" now', cursor: 24 }), null);
+});
+
+test("formatFileMentionPath quotes only paths that need it", () => {
+ assert.equal(formatFileMentionPath("src/App.tsx"), "@src/App.tsx");
+ assert.equal(formatFileMentionPath("docs/my file.md"), '@"docs/my file.md"');
+ assert.equal(formatFileMentionPath('docs/a"b.md'), '@"docs/a\\"b.md"');
+});
+
+test("replaceCurrentFileMentionToken inserts a trailing-space mention", () => {
+ const state = { text: "read @sr then", cursor: 8 };
+ const token = getCurrentFileMentionToken(state);
+ assert.ok(token);
+ assert.deepEqual(replaceCurrentFileMentionToken(state, token, "src/index.ts"), {
+ text: "read @src/index.ts then",
+ cursor: 19,
+ });
+
+ const quotedState = { text: 'read @"doc', cursor: 10 };
+ const quotedToken = getCurrentFileMentionToken(quotedState);
+ assert.ok(quotedToken);
+ assert.deepEqual(replaceCurrentFileMentionToken(quotedState, quotedToken, "docs/my file.md"), {
+ text: 'read @"docs/my file.md" ',
+ cursor: 24,
+ });
+});
+
+test("filterFileMentionItems prioritizes prefix and basename matches", () => {
+ const items: FileMentionItem[] = [
+ { path: "src/PromptInput.tsx", type: "file" },
+ { path: "docs/prompt guide.md", type: "file" },
+ { path: "templates/prompts/init.md", type: "file" },
+ ];
+
+ assert.deepEqual(
+ filterFileMentionItems(items, "prompt").map((item) => item.path),
+ ["docs/prompt guide.md", "src/PromptInput.tsx", "templates/prompts/init.md"]
+ );
+});
+
+test("scanFileMentionItems returns relative slash-separated files and directories", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
+ try {
+ fs.mkdirSync(path.join(root, "src"));
+ fs.writeFileSync(path.join(root, "src", "index.ts"), "");
+ fs.mkdirSync(path.join(root, "node_modules"));
+ fs.writeFileSync(path.join(root, "node_modules", "ignored.js"), "");
+
+ assert.deepEqual(
+ scanFileMentionItems(root).map((item) => item.path),
+ ["node_modules/", "node_modules/ignored.js", "src/", "src/index.ts"]
+ );
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("scanFileMentionItems respects project gitignore patterns inside git repositories", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
+ try {
+ fs.mkdirSync(path.join(root, ".git"));
+ fs.mkdirSync(path.join(root, ".mypy_cache"), { recursive: true });
+ fs.writeFileSync(path.join(root, ".mypy_cache", "ignored.json"), "");
+ fs.mkdirSync(path.join(root, "tmp"));
+ fs.writeFileSync(path.join(root, "tmp", "ignored.txt"), "");
+ fs.mkdirSync(path.join(root, "docs"));
+ fs.writeFileSync(path.join(root, "docs", "guide.md"), "");
+ fs.writeFileSync(path.join(root, ".gitignore"), ".mypy_cache/\ntmp/\n");
+
+ assert.deepEqual(
+ scanFileMentionItems(root).map((item) => item.path),
+ ["docs/", "docs/guide.md", ".gitignore"]
+ );
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("scanFileMentionItems ignores gitignore files outside git repositories", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
+ try {
+ fs.mkdirSync(path.join(root, "tmp"));
+ fs.writeFileSync(path.join(root, "tmp", "visible.txt"), "");
+ fs.writeFileSync(path.join(root, ".gitignore"), "tmp/\n");
+
+ assert.deepEqual(
+ scanFileMentionItems(root).map((item) => item.path),
+ ["tmp/", "tmp/visible.txt", ".gitignore"]
+ );
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("scanFileMentionItems applies parent and nested ignore files", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
+ try {
+ fs.mkdirSync(path.join(root, ".git"));
+ fs.writeFileSync(path.join(root, ".gitignore"), "ignored-from-parent/\n");
+ fs.mkdirSync(path.join(root, "sub", "ignored-from-parent"), { recursive: true });
+ fs.writeFileSync(path.join(root, "sub", "ignored-from-parent", "hidden.txt"), "");
+ fs.mkdirSync(path.join(root, "sub", "nested", "ignored-from-nested"), { recursive: true });
+ fs.writeFileSync(path.join(root, "sub", "nested", ".gitignore"), "ignored-from-nested/\n");
+ fs.writeFileSync(path.join(root, "sub", "nested", "ignored-from-nested", "hidden.txt"), "");
+ fs.writeFileSync(path.join(root, "sub", "nested", "visible.txt"), "");
+
+ assert.deepEqual(
+ scanFileMentionItems(path.join(root, "sub")).map((item) => item.path),
+ ["nested/", "nested/.gitignore", "nested/visible.txt"]
+ );
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index e56111f..8d8dca1 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -512,6 +512,7 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
/>
) : isExiting ? null : (
(null);
const [modelDropdownIndex, setModelDropdownIndex] = useState(0);
const [pendingModel, setPendingModel] = useState(null);
+ const [fileMentionIndex, setFileMentionIndex] = useState(0);
+ const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState(null);
const [historyCursor, setHistoryCursor] = useState(-1);
const [draftBeforeHistory, setDraftBeforeHistory] = useState(null);
const [hasTerminalFocus, setHasTerminalFocus] = useState(true);
const lastCtrlDAt = React.useRef(0);
const undoRedoRef = React.useRef(createPromptUndoRedoState());
+ const fileMentionItems = React.useMemo(() => scanFileMentionItems(projectRoot), [projectRoot]);
+ const fileMentionToken = getCurrentFileMentionToken(buffer);
+ const fileMentionKey = fileMentionToken ? `${fileMentionToken.start}:${fileMentionToken.query}` : null;
+ const fileMentionMatches = React.useMemo(
+ () => (fileMentionToken ? filterFileMentionItems(fileMentionItems, fileMentionToken.query) : []),
+ [fileMentionItems, fileMentionToken]
+ );
+ const showFileMentionMenu =
+ !showSkillsDropdown &&
+ !modelDropdownStep &&
+ fileMentionToken !== null &&
+ fileMentionKey !== dismissedFileMentionKey;
const slashItems = React.useMemo(() => buildSlashCommands(skills), [skills]);
const slashToken = getCurrentSlashToken(buffer);
const slashMenu = React.useMemo(
() =>
- showSkillsDropdown || modelDropdownStep ? [] : slashToken ? filterSlashCommands(slashItems, slashToken) : [],
- [showSkillsDropdown, modelDropdownStep, slashToken, slashItems]
+ showSkillsDropdown || modelDropdownStep || showFileMentionMenu
+ ? []
+ : slashToken
+ ? filterSlashCommands(slashItems, slashToken)
+ : [],
+ [showSkillsDropdown, modelDropdownStep, showFileMentionMenu, slashToken, slashItems]
);
const showMenu = slashMenu.length > 0;
const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]);
@@ -153,7 +180,7 @@ export const PromptInput = React.memo(function PromptInput({
? loadingText && loadingText.trim()
? `${loadingText}${processHint}`
: `esc to interrupt · ctrl+c to cancel input${processHint}`
- : `enter send · shift+enter newline · ctrl+v image · / commands · ctrl+d exit${processHint}`;
+ : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processHint}`;
useTerminalFocusReporting(stdout, !disabled);
useTerminalExtendedKeys(stdout, !disabled);
useHiddenTerminalCursor(stdout, !disabled);
@@ -168,6 +195,22 @@ export const PromptInput = React.memo(function PromptInput({
}
}, [slashMenu, showMenu, menuIndex]);
+ useEffect(() => {
+ if (!fileMentionKey) {
+ setDismissedFileMentionKey(null);
+ }
+ }, [fileMentionKey]);
+
+ useEffect(() => {
+ if (!showFileMentionMenu) {
+ setFileMentionIndex(0);
+ return;
+ }
+ if (fileMentionIndex >= fileMentionMatches.length) {
+ setFileMentionIndex(Math.max(0, fileMentionMatches.length - 1));
+ }
+ }, [fileMentionMatches.length, fileMentionIndex, showFileMentionMenu]);
+
useEffect(() => {
if (skillsDropdownIndex >= skills.length) {
setSkillsDropdownIndex(Math.max(0, skills.length - 1));
@@ -222,6 +265,10 @@ export const PromptInput = React.memo(function PromptInput({
setShowSkillsDropdown(false);
return;
}
+ if (showFileMentionMenu && fileMentionKey) {
+ setDismissedFileMentionKey(fileMentionKey);
+ return;
+ }
if (busy) {
onInterrupt();
setStatusMessage("Interrupting…");
@@ -353,6 +400,35 @@ export const PromptInput = React.memo(function PromptInput({
const returnAction = getPromptReturnKeyAction(key);
const isPlainReturn = returnAction === "submit";
+ if (showFileMentionMenu) {
+ if (key.upArrow) {
+ if (fileMentionMatches.length > 0) {
+ setFileMentionIndex((idx) => (idx - 1 + fileMentionMatches.length) % fileMentionMatches.length);
+ }
+ return;
+ }
+ if (key.downArrow) {
+ if (fileMentionMatches.length > 0) {
+ setFileMentionIndex((idx) => (idx + 1) % fileMentionMatches.length);
+ }
+ return;
+ }
+ if (key.tab || returnAction === "submit") {
+ const selected = fileMentionMatches[fileMentionIndex];
+ if (selected && fileMentionToken) {
+ insertFileMentionSelection(selected);
+ return;
+ }
+ if (key.tab) {
+ setDismissedFileMentionKey(fileMentionKey);
+ return;
+ }
+ if (fileMentionKey) {
+ setDismissedFileMentionKey(fileMentionKey);
+ }
+ }
+ }
+
if (showMenu) {
if (key.upArrow) {
setMenuIndex((idx) => (idx - 1 + slashMenu.length) % slashMenu.length);
@@ -585,6 +661,14 @@ export const PromptInput = React.memo(function PromptInput({
setHistoryCursor(nextCursor);
}
+ function insertFileMentionSelection(item: FileMentionItem): void {
+ if (!fileMentionToken) {
+ return;
+ }
+ updateBuffer((state) => replaceCurrentFileMentionToken(state, fileMentionToken, item.path));
+ setDismissedFileMentionKey(null);
+ }
+
function handleSlashSelection(item: SlashCommandItem): void {
if (busy && item.kind !== "exit") {
setStatusMessage("wait for the current response or press esc to interrupt");
@@ -760,8 +844,8 @@ export const PromptInput = React.memo(function PromptInput({
}));
const showFooterText = useMemo(
- () => showMenu || showSkillsDropdown || modelDropdownStep !== null,
- [showMenu, showSkillsDropdown, modelDropdownStep]
+ () => showMenu || showSkillsDropdown || modelDropdownStep !== null || showFileMentionMenu,
+ [showMenu, showSkillsDropdown, modelDropdownStep, showFileMentionMenu]
);
return (
@@ -830,6 +914,22 @@ export const PromptInput = React.memo(function PromptInput({
maxVisible={6}
/>
) : null}
+ {showFileMentionMenu ? (
+ ({
+ key: item.path,
+ label: item.path,
+ description: item.type === "directory" ? "directory" : "file",
+ }))}
+ activeIndex={fileMentionIndex}
+ activeColor="#229ac3"
+ maxVisible={8}
+ />
+ ) : null}
{!showFooterText && (
diff --git a/src/ui/fileMentions.ts b/src/ui/fileMentions.ts
new file mode 100644
index 0000000..cbacbe6
--- /dev/null
+++ b/src/ui/fileMentions.ts
@@ -0,0 +1,410 @@
+import * as fs from "fs";
+import * as path from "path";
+import ignore from "ignore";
+import type { PromptBufferState } from "./promptBuffer";
+
+export type FileMentionItem = {
+ path: string;
+ type: "file" | "directory";
+};
+
+export type FileMentionToken = {
+ query: string;
+ start: number;
+ end: number;
+ quoted: boolean;
+};
+
+const DEFAULT_MAX_ITEMS = 2000;
+const DEFAULT_MAX_DEPTH = 8;
+
+type IgnoreMatcher = {
+ base: string;
+ matcher: ignore.Ignore;
+};
+
+export function scanFileMentionItems(root: string, maxItems = DEFAULT_MAX_ITEMS): FileMentionItem[] {
+ const items: FileMentionItem[] = [];
+ const seen = new Set();
+ const gitRoot = findGitRoot(root);
+ const visitedDirectories = new Set();
+
+ function addItem(item: FileMentionItem): void {
+ if (items.length >= maxItems || seen.has(item.path)) {
+ return;
+ }
+ seen.add(item.path);
+ items.push(item);
+ }
+
+ function visit(directory: string, depth: number, matchers: IgnoreMatcher[]): void {
+ if (items.length >= maxItems || depth > DEFAULT_MAX_DEPTH) {
+ return;
+ }
+
+ const currentMatchers = [...matchers, ...loadDirectoryIgnoreMatchers(directory, gitRoot)];
+
+ let entries: fs.Dirent[];
+ try {
+ entries = fs.readdirSync(directory, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ entries.sort((a, b) => {
+ if (a.isDirectory() !== b.isDirectory()) {
+ return a.isDirectory() ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+ for (const entry of entries) {
+ if (items.length >= maxItems) {
+ return;
+ }
+ if (entry.name === "." || entry.name === ".." || entry.name === ".git") {
+ continue;
+ }
+
+ const absolute = path.join(directory, entry.name);
+ const relative = toMentionPath(path.relative(root, absolute));
+ if (!relative) {
+ continue;
+ }
+
+ const entryType = getMentionEntryType(entry, absolute);
+ if (!entryType) {
+ continue;
+ }
+
+ if (matchesAnyIgnore(absolute, entryType === "directory", currentMatchers)) {
+ continue;
+ }
+
+ if (entryType === "directory") {
+ const realPath = safeRealpath(absolute);
+ if (realPath) {
+ if (visitedDirectories.has(realPath)) {
+ continue;
+ }
+ visitedDirectories.add(realPath);
+ }
+ addItem({ path: `${relative}/`, type: "directory" });
+ visit(absolute, depth + 1, currentMatchers);
+ continue;
+ }
+
+ if (entryType === "file") {
+ addItem({ path: relative, type: "file" });
+ }
+ }
+ }
+
+ const rootRealPath = safeRealpath(root);
+ if (rootRealPath) {
+ visitedDirectories.add(rootRealPath);
+ }
+ visit(root, 0, loadAncestorIgnoreMatchers(root, gitRoot));
+ return items;
+}
+
+function getMentionEntryType(entry: fs.Dirent, absolute: string): FileMentionItem["type"] | null {
+ if (entry.isDirectory()) {
+ return "directory";
+ }
+ if (entry.isFile()) {
+ return "file";
+ }
+ if (!entry.isSymbolicLink()) {
+ return null;
+ }
+ try {
+ const stat = fs.statSync(absolute);
+ if (stat.isDirectory()) {
+ return "directory";
+ }
+ if (stat.isFile()) {
+ return "file";
+ }
+ } catch {
+ return null;
+ }
+ return null;
+}
+
+function safeRealpath(absolute: string): string | null {
+ try {
+ return fs.realpathSync(absolute);
+ } catch {
+ return null;
+ }
+}
+
+function loadDirectoryIgnoreMatchers(directory: string, gitRoot: string | null): IgnoreMatcher[] {
+ const matchers: IgnoreMatcher[] = [];
+ if (gitRoot && isPathInsideOrEqual(directory, gitRoot)) {
+ const gitignoreMatcher = loadIgnoreFileMatcher(directory, path.join(directory, ".gitignore"));
+ if (gitignoreMatcher) {
+ matchers.push(gitignoreMatcher);
+ }
+ if (path.resolve(directory) === path.resolve(gitRoot)) {
+ const gitExcludeMatcher = loadIgnoreFileMatcher(directory, path.join(directory, ".git", "info", "exclude"));
+ if (gitExcludeMatcher) {
+ matchers.push(gitExcludeMatcher);
+ }
+ }
+ }
+
+ const ignoreMatcher = loadIgnoreFileMatcher(directory, path.join(directory, ".ignore"));
+ if (ignoreMatcher) {
+ matchers.push(ignoreMatcher);
+ }
+ return matchers;
+}
+
+function loadAncestorIgnoreMatchers(root: string, gitRoot: string | null): IgnoreMatcher[] {
+ const resolvedRoot = path.resolve(root);
+ const ancestors: string[] = [];
+ let current = path.dirname(resolvedRoot);
+ while (gitRoot && isPathInsideOrEqual(current, gitRoot)) {
+ ancestors.push(current);
+ if (path.resolve(current) === path.resolve(gitRoot)) {
+ break;
+ }
+ current = path.dirname(current);
+ }
+ return ancestors.reverse().flatMap((directory) => loadDirectoryIgnoreMatchers(directory, gitRoot));
+}
+
+function loadIgnoreFileMatcher(base: string, ignoreFilePath: string): IgnoreMatcher | null {
+ try {
+ if (!fs.existsSync(ignoreFilePath)) {
+ return null;
+ }
+ const content = fs.readFileSync(ignoreFilePath, "utf8");
+ if (!content.trim()) {
+ return null;
+ }
+ return { base, matcher: ignore().add(content) };
+ } catch {
+ return null;
+ }
+}
+
+function matchesAnyIgnore(absolute: string, isDir: boolean, matchers: IgnoreMatcher[]): boolean {
+ let ignored = false;
+ for (const { base, matcher } of matchers) {
+ const relative = toMentionPath(path.relative(base, absolute));
+ if (!relative || relative.startsWith("../")) {
+ continue;
+ }
+ const result = matcher.test(isDir ? `${relative}/` : relative);
+ if (result.ignored) {
+ ignored = true;
+ }
+ if (result.unignored) {
+ ignored = false;
+ }
+ }
+ return ignored;
+}
+
+function findGitRoot(start: string): string | null {
+ let current = path.resolve(start);
+ while (true) {
+ if (fs.existsSync(path.join(current, ".git"))) {
+ return current;
+ }
+ const parent = path.dirname(current);
+ if (parent === current) {
+ return null;
+ }
+ current = parent;
+ }
+}
+
+function isPathInsideOrEqual(candidate: string, parent: string): boolean {
+ const relative = path.relative(parent, candidate);
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
+}
+
+export function filterFileMentionItems(items: FileMentionItem[], query: string, maxResults = 12): FileMentionItem[] {
+ const normalizedQuery = normalizeForSearch(query);
+ const scored = items
+ .map((item, index) => ({ item, index, score: scoreFileMention(item.path, normalizedQuery) }))
+ .filter((entry) => entry.score !== Number.POSITIVE_INFINITY)
+ .sort((a, b) => a.score - b.score || a.item.path.length - b.item.path.length || a.index - b.index);
+
+ return scored.slice(0, maxResults).map((entry) => entry.item);
+}
+
+export function getCurrentFileMentionToken(state: PromptBufferState): FileMentionToken | null {
+ const text = state.text;
+ const cursor = clampCursorToBoundary(text, state.cursor);
+ const quoted = getCurrentQuotedFileMentionToken(text, cursor);
+ if (quoted) {
+ return quoted;
+ }
+ return getCurrentBareFileMentionToken(text, cursor);
+}
+
+export function replaceCurrentFileMentionToken(
+ state: PromptBufferState,
+ token: FileMentionToken,
+ selectedPath: string
+): PromptBufferState {
+ const inserted = `${formatFileMentionPath(selectedPath)} `;
+ const end = token.end < state.text.length && isWhitespace(state.text[token.end] ?? "") ? token.end + 1 : token.end;
+ const text = `${state.text.slice(0, token.start)}${inserted}${state.text.slice(end)}`;
+ return { text, cursor: token.start + inserted.length };
+}
+
+export function formatFileMentionPath(filePath: string): string {
+ if (!/[\s"]/.test(filePath)) {
+ return `@${filePath}`;
+ }
+ return `@"${filePath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
+}
+
+function getCurrentBareFileMentionToken(text: string, cursor: number): FileMentionToken | null {
+ const beforeCursor = text.slice(0, cursor);
+ const afterCursor = text.slice(cursor);
+ const start = findTokenStart(beforeCursor);
+ const end = cursor + findTokenEnd(afterCursor);
+ const token = text.slice(start, end);
+
+ if (!token.startsWith("@") || token.startsWith('@"')) {
+ return null;
+ }
+ if (start > 0 && !isWhitespace(text[start - 1] ?? "")) {
+ return null;
+ }
+ return { query: token.slice(1), start, end, quoted: false };
+}
+
+function getCurrentQuotedFileMentionToken(text: string, cursor: number): FileMentionToken | null {
+ for (let index = cursor; index >= 0; index--) {
+ if (text[index] !== "@" || text[index + 1] !== '"') {
+ continue;
+ }
+ if (index > 0 && !isWhitespace(text[index - 1] ?? "")) {
+ continue;
+ }
+
+ const closeQuote = findClosingQuote(text, index + 2);
+ if (closeQuote !== -1 && cursor > closeQuote) {
+ continue;
+ }
+
+ const end = closeQuote === -1 ? cursor : closeQuote + 1;
+ return {
+ query: unescapeQuotedMentionQuery(
+ text.slice(index + 2, Math.min(cursor, closeQuote === -1 ? cursor : closeQuote))
+ ),
+ start: index,
+ end,
+ quoted: true,
+ };
+ }
+ return null;
+}
+
+function findTokenStart(beforeCursor: string): number {
+ const whitespaceIndex = findLastWhitespaceIndex(beforeCursor);
+ return whitespaceIndex === -1 ? 0 : whitespaceIndex + 1;
+}
+
+function findTokenEnd(afterCursor: string): number {
+ const whitespaceIndex = afterCursor.search(/\s/);
+ return whitespaceIndex === -1 ? afterCursor.length : whitespaceIndex;
+}
+
+function findLastWhitespaceIndex(value: string): number {
+ for (let index = value.length - 1; index >= 0; index--) {
+ if (isWhitespace(value[index] ?? "")) {
+ return index;
+ }
+ }
+ return -1;
+}
+
+function findClosingQuote(text: string, start: number): number {
+ let escaped = false;
+ for (let index = start; index < text.length; index++) {
+ const char = text[index];
+ if (escaped) {
+ escaped = false;
+ continue;
+ }
+ if (char === "\\") {
+ escaped = true;
+ continue;
+ }
+ if (char === '"') {
+ return index;
+ }
+ }
+ return -1;
+}
+
+function unescapeQuotedMentionQuery(query: string): string {
+ return query.replace(/\\(["\\])/g, "$1");
+}
+
+function clampCursorToBoundary(text: string, cursor: number): number {
+ return Math.max(0, Math.min(cursor, text.length));
+}
+
+function scoreFileMention(itemPath: string, normalizedQuery: string): number {
+ if (!normalizedQuery) {
+ return itemPath.endsWith("/") ? 5 : 10;
+ }
+
+ const normalizedPath = normalizeForSearch(itemPath);
+ const normalizedBase = normalizeForSearch(path.posix.basename(itemPath.replace(/\/$/, "")));
+ if (normalizedPath === normalizedQuery) {
+ return 0;
+ }
+ if (normalizedPath.startsWith(normalizedQuery)) {
+ return 1;
+ }
+ if (normalizedBase.startsWith(normalizedQuery)) {
+ return isQueryBoundary(normalizedBase[normalizedQuery.length] ?? "") ? 2 : 3;
+ }
+ const pathIndex = normalizedPath.indexOf(normalizedQuery);
+ if (pathIndex !== -1) {
+ return 20 + pathIndex;
+ }
+ const fuzzyScore = fuzzyMatchScore(normalizedPath, normalizedQuery);
+ return fuzzyScore === null ? Number.POSITIVE_INFINITY : 100 + fuzzyScore;
+}
+
+function fuzzyMatchScore(value: string, query: string): number | null {
+ let valueIndex = 0;
+ let score = 0;
+ for (const char of query) {
+ const nextIndex = value.indexOf(char, valueIndex);
+ if (nextIndex === -1) {
+ return null;
+ }
+ score += nextIndex - valueIndex;
+ valueIndex = nextIndex + 1;
+ }
+ return score;
+}
+
+function normalizeForSearch(value: string): string {
+ return value.trim().toLocaleLowerCase();
+}
+
+function isQueryBoundary(value: string): boolean {
+ return value === "" || /[\s._/-]/.test(value);
+}
+
+function toMentionPath(value: string): string {
+ return value.split(path.sep).join("/");
+}
+
+function isWhitespace(value: string): boolean {
+ return /\s/.test(value);
+}
diff --git a/src/ui/index.ts b/src/ui/index.ts
index 5b4ff8f..5bcde40 100644
--- a/src/ui/index.ts
+++ b/src/ui/index.ts
@@ -79,5 +79,14 @@ export {
type SlashCommandKind,
type SlashCommandItem,
} from "./slashCommands";
+export {
+ filterFileMentionItems,
+ formatFileMentionPath,
+ getCurrentFileMentionToken,
+ replaceCurrentFileMentionToken,
+ scanFileMentionItems,
+ type FileMentionItem,
+ type FileMentionToken,
+} from "./fileMentions";
export { findExpandedThinkingId } from "./thinkingState";
export { buildExitSummaryText } from "./exitSummary";
From 47d3c21abe3c3582d24e7c1109bdf19e0818c90d Mon Sep 17 00:00:00 2001
From: hcyang
Date: Mon, 18 May 2026 18:13:34 +0800
Subject: [PATCH 052/129] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20/raw?=
=?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 RawMode 功能,包括 Normal、Lite 和 Raw scrollback 模式
- App 组件中集成 RawMode 上下文及切换逻辑,支持在 Raw 模式下直接向 stdout 渲染消息
- 增加 RawModeExitPrompt 组件,支持按 ESC 退出原始模式
- 新增 RawModelDropdown 组件,提供原始模式选择下拉菜单
- 在 PromptInput 中集成原始模式选择交互及状态管理
- 调整消息视图实现,拆分 MessageView 到 compoments 目录,支持根据 RawMode 呈现不同内容
- 新建 AppContainer 组件,包装 App 并提供版本上下文和 RawModeProvider
- 修改 SlashCommand 体系,支持内置 /raw 命令及对应测试覆盖
- 更新 cli 入口,使用 AppContainer 替换直接渲染 App,传递版本信息
- 移除旧 MessageView 文件,重构消息渲染逻辑
- 优化 SlashCommandMenu 显示,支持命令参数提示显示
- 更新相关测试,支持原始模式功能验证
---
src/cli.tsx | 4 +-
src/tests/messageView.test.ts | 51 +--
src/tests/slashCommands.test.ts | 9 +-
src/ui/App.tsx | 69 +++-
src/ui/AppContainer.tsx | 21 ++
src/ui/MessageView.tsx | 355 ------------------
src/ui/PromptInput.tsx | 27 +-
src/ui/SlashCommandMenu.tsx | 5 +-
src/ui/WelcomeScreen.tsx | 11 +-
src/ui/compoments/MessageView/index.tsx | 183 +++++++++
.../{ => compoments/MessageView}/markdown.ts | 0
src/ui/compoments/MessageView/types.ts | 19 +
src/ui/compoments/MessageView/utils.ts | 255 +++++++++++++
src/ui/compoments/RawModeExitPrompt/index.tsx | 15 +
src/ui/compoments/RawModelDropdown/index.tsx | 55 +++
src/ui/compoments/index.ts | 3 +
src/ui/contexts/AppContext.tsx | 15 +
src/ui/contexts/RawModeContext.tsx | 40 ++
src/ui/contexts/index.ts | 3 +
src/ui/index.ts | 5 +-
src/ui/slashCommands.ts | 22 +-
21 files changed, 750 insertions(+), 417 deletions(-)
create mode 100644 src/ui/AppContainer.tsx
delete mode 100644 src/ui/MessageView.tsx
create mode 100644 src/ui/compoments/MessageView/index.tsx
rename src/ui/{ => compoments/MessageView}/markdown.ts (100%)
create mode 100644 src/ui/compoments/MessageView/types.ts
create mode 100644 src/ui/compoments/MessageView/utils.ts
create mode 100644 src/ui/compoments/RawModeExitPrompt/index.tsx
create mode 100644 src/ui/compoments/RawModelDropdown/index.tsx
create mode 100644 src/ui/compoments/index.ts
create mode 100644 src/ui/contexts/AppContext.tsx
create mode 100644 src/ui/contexts/RawModeContext.tsx
create mode 100644 src/ui/contexts/index.ts
diff --git a/src/cli.tsx b/src/cli.tsx
index 435499a..e8e8659 100644
--- a/src/cli.tsx
+++ b/src/cli.tsx
@@ -1,8 +1,8 @@
import React from "react";
import { render } from "ink";
-import { App } from "./ui";
import { setShellIfWindows } from "./common/shell-utils";
import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck";
+import AppContainer from "./ui/AppContainer";
const args = process.argv.slice(2);
const packageInfo = readPackageInfo();
@@ -81,7 +81,7 @@ async function main(): Promise {
const appInitialPrompt = initialPrompt;
initialPrompt = undefined;
const inkInstance = render(
- {
const lines = parseDiffPreview(
@@ -25,45 +26,29 @@ test("parseDiffPreview keeps nonstandard context lines", () => {
test("MessageView summarizes thinking content across lines", () => {
assert.equal(
- getThinkingParams({
- content: "Plan:\n\nInspect the code and update tests",
- }),
+ buildThinkingSummary("Plan:\n\nInspect the code and update tests", null, RawMode.Lite),
"Plan: Inspect the code and update tests"
);
});
-test("MessageView removes a trailing colon from thinking summaries", () => {
- assert.equal(getThinkingParams({ content: "Planning:" }), "Planning");
+test("MessageView removes a trailing colon from thinking summary", () => {
+ assert.equal(buildThinkingSummary("Planning:", null, RawMode.Lite), "Planning");
});
-test("MessageView falls back to a reasoning placeholder for hidden reasoning content", () => {
+test("MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode", () => {
assert.equal(
- getThinkingParams({
- content: "",
- messageParams: { reasoning_content: "hidden chain of thought" },
- }),
+ buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Lite),
"(reasoning...)"
);
});
-function getThinkingParams(overrides: Partial): string {
- const view = MessageView({ message: buildAssistantMessage(overrides) }) as any;
- return view.props.children.props.params;
-}
-
-function buildAssistantMessage(overrides: Partial): SessionMessage {
- return {
- id: "message-1",
- sessionId: "session-1",
- role: "assistant",
- content: "",
- contentParams: null,
- messageParams: null,
- compacted: false,
- visible: true,
- createTime: "2026-01-01T00:00:00.000Z",
- updateTime: "2026-01-01T00:00:00.000Z",
- meta: { asThinking: true },
- ...overrides,
- };
-}
+test("MessageView shows full reasoning content in Normal/Raw mode", () => {
+ assert.equal(
+ buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.None),
+ "hidden chain of thought"
+ );
+ assert.equal(
+ buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Raw),
+ "hidden chain of thought"
+ );
+});
diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts
index bba5244..34b48d0 100644
--- a/src/tests/slashCommands.test.ts
+++ b/src/tests/slashCommands.test.ts
@@ -19,7 +19,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
assert.equal(items[0].kind, "skill");
assert.equal(items[0].name, "skill-writer");
const builtinNames = items.filter((i) => i.kind !== "skill").map((i) => i.name);
- assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "exit"]);
+ assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "raw", "exit"]);
});
test("filterSlashCommands matches partial prefixes", () => {
@@ -80,6 +80,13 @@ test("findExactSlashCommand returns built-in /model", () => {
assert.equal(item?.kind, "model");
});
+test("findExactSlashCommand returns built-in /raw", () => {
+ const items = buildSlashCommands(skills);
+ const item = findExactSlashCommand(items, "/raw");
+ assert.ok(item);
+ assert.equal(item?.kind, "raw");
+});
+
test("findExactSlashCommand returns the matching skill", () => {
const items = buildSlashCommands(skills);
const item = findExactSlashCommand(items, "/code-review");
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index e56111f..1c9bac4 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -6,10 +6,10 @@ import * as os from "os";
import * as path from "path";
import OpenAI from "openai";
import {
- SessionManager,
type LlmStreamProgress,
type MessageMeta,
type SessionEntry,
+ SessionManager,
type SessionMessage,
type SessionStatus,
type SkillInfo,
@@ -17,13 +17,13 @@ import {
} from "../session";
import {
applyModelConfigSelection,
- resolveSettingsSources,
type DeepcodingSettings,
type ModelConfigSelection,
type ResolvedDeepcodingSettings,
+ resolveSettingsSources,
} from "../settings";
import { PromptInput, type PromptSubmission } from "./PromptInput";
-import { MessageView } from "./MessageView";
+import { MessageView, RawModeExitPrompt } from "./compoments";
import { SessionList } from "./SessionList";
import { buildLoadingText } from "./loadingText";
import { findExpandedThinkingId } from "./thinkingState";
@@ -32,11 +32,13 @@ import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
import { McpStatusList } from "./McpStatusList";
import { ProcessStdoutView } from "./ProcessStdoutView";
import {
+ type AskUserQuestionAnswers,
findPendingAskUserQuestion,
formatAskUserQuestionAnswers,
- type AskUserQuestionAnswers,
} from "./askUserQuestion";
import { buildExitSummaryText } from "./exitSummary";
+import { RawMode, useRawModeContext } from "./contexts";
+import { renderMessageToStdout } from "./compoments/MessageView/utils";
const DEFAULT_MODEL = "deepseek-v4-pro";
const DEFAULT_BASE_URL = "https://api.deepseek.com";
@@ -45,12 +47,11 @@ type View = "chat" | "session-list" | "mcp-status";
type AppProps = {
projectRoot: string;
- version?: string;
initialPrompt?: string;
onRestart?: () => void;
};
-export function App({ projectRoot, version = "", initialPrompt, onRestart }: AppProps): React.ReactElement {
+export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement {
const { exit } = useApp();
const { stdout, write } = useStdout();
const { columns } = useWindowSize();
@@ -75,6 +76,10 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
const [showProcessStdout, setShowProcessStdout] = useState(false);
const processStdoutRef = useRef
Deep Code CLI
-[English](./README_en.md) · 中文
+[English](README-en.md) · 中文
From 3fef0fc5137af49f218237fa0e919159fb231122 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Tue, 19 May 2026 10:09:38 +0800
Subject: [PATCH 066/129] feat(notify): pass STATUS, FAIL_REASON, BODY as env
vars to notify hook
- Add NotifyContext type with status, failReason, body fields
- buildNotifyEnv injects STATUS, FAIL_REASON, BODY when provided
- maybeNotifyTaskCompletion extracts last assistant message as BODY
- launchNotifyScript accepts optional context parameter
- Add unit tests for new context env var injection
- Update docs with env variable table and iTerm2/macOS notify examples
---
docs/configuration.md | 34 ++++++
docs/configuration_en.md | 34 ++++++
src/common/notify.ts | 38 ++++++-
src/session.ts | 18 +++-
src/tests/session.test.ts | 144 ++++++++++++++++++++++++++
src/tests/settings-and-notify.test.ts | 65 +++++++++++-
6 files changed, 324 insertions(+), 9 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index f8e52c3..45aaab0 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -67,12 +67,46 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。
+通知脚本执行时,会通过环境变量注入以下上下文信息:
+
+| 环境变量 | 说明 |
+|----------|------|
+| `DURATION` | 会话耗时,单位秒(整数) |
+| `STATUS` | 会话状态:`"completed"` 或 `"failed"` |
+| `FAIL_REASON` | 失败原因(仅失败时设置) |
+| `BODY` | 最后一条 AI 助手回复的文本内容 |
+| `TITLE` | 会话标题(对应 resume 列表中的标题) |
+
```json
{
"notify": "/path/to/slack-notify.sh"
}
```
+**iTerm2 终端通知示例**:
+
+如果你的终端是 iTerm2,可以直接通过 OSC 9 转义序列弹出通知,无需额外脚本。创建以下脚本(如 `~/.deepcode/notify.sh`):
+
+```bash
+#!/bin/bash
+# iTerm2 OSC 9 通知
+echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
+```
+
+```json
+{
+ "notify": "/Users/you/.deepcode/notify.sh"
+}
+```
+
+**macOS 系统通知示例**:
+
+```bash
+#!/bin/bash
+# macOS 系统通知
+osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\""
+```
+
#### `webSearchTool` — 自定义联网搜索
Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径:
diff --git a/docs/configuration_en.md b/docs/configuration_en.md
index 369f8e4..606fcab 100644
--- a/docs/configuration_en.md
+++ b/docs/configuration_en.md
@@ -67,12 +67,46 @@ When thinking mode is enabled, controls the depth of the model’s reasoning:
Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message).
+The following context is injected as environment variables when the notify script runs:
+
+| Variable | Description |
+|----------|-------------|
+| `DURATION` | Session duration in seconds (integer) |
+| `STATUS` | Session status: `"completed"` or `"failed"` |
+| `FAIL_REASON` | Failure reason (only set on failure) |
+| `BODY` | The text content of the last AI assistant reply |
+| `TITLE` | Session title (matches the resume list title) |
+
```json
{
"notify": "/path/to/slack-notify.sh"
}
```
+**iTerm2 Notification Example**:
+
+On iTerm2 you can use the OSC 9 escape sequence for native notifications. Create a script (e.g., `~/.deepcode/notify.sh`):
+
+```bash
+#!/bin/bash
+# iTerm2 OSC 9 notification
+echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
+```
+
+```json
+{
+ "notify": "/Users/you/.deepcode/notify.sh"
+}
+```
+
+**macOS System Notification Example**:
+
+```bash
+#!/bin/bash
+# macOS system notification
+osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\""
+```
+
#### `webSearchTool` — Custom Web Search
Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script:
diff --git a/src/common/notify.ts b/src/common/notify.ts
index 8878c50..d1b541b 100644
--- a/src/common/notify.ts
+++ b/src/common/notify.ts
@@ -16,11 +16,40 @@ export function formatDurationSeconds(durationMs: number): string {
return String(Math.floor(safeMs / 1000));
}
-export function buildNotifyEnv(durationMs: number, baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
- return {
+export type NotifyContext = {
+ status?: string;
+ failReason?: string;
+ body?: string;
+ title?: string;
+};
+
+export function buildNotifyEnv(
+ durationMs: number,
+ baseEnv: NodeJS.ProcessEnv = process.env,
+ context: NotifyContext = {}
+): NodeJS.ProcessEnv {
+ const env: NodeJS.ProcessEnv = {
...baseEnv,
DURATION: formatDurationSeconds(durationMs),
};
+ delete env.STATUS;
+ delete env.FAIL_REASON;
+ delete env.BODY;
+ delete env.TITLE;
+
+ if (context.status) {
+ env.STATUS = context.status;
+ }
+ if (context.failReason) {
+ env.FAIL_REASON = context.failReason;
+ }
+ if (context.body) {
+ env.BODY = context.body;
+ }
+ if (context.title) {
+ env.TITLE = context.title;
+ }
+ return env;
}
export function launchNotifyScript(
@@ -28,7 +57,8 @@ export function launchNotifyScript(
durationMs: number,
workingDirectory?: string,
spawnProcess: NotifySpawn = spawn as unknown as NotifySpawn,
- configuredEnv: Record = {}
+ configuredEnv: Record = {},
+ context: NotifyContext = {}
): void {
const commandPath = notifyPath?.trim();
if (!commandPath) {
@@ -38,7 +68,7 @@ export function launchNotifyScript(
const options = {
cwd: workingDirectory,
detached: process.platform !== "win32",
- env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }),
+ env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }, context),
stdio: "ignore" as const,
};
diff --git a/src/session.ts b/src/session.ts
index 96a9adb..3a6e13b 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -2124,7 +2124,23 @@ ${skillMd}
return;
}
- launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot, undefined, configuredEnv);
+ // Find the last assistant message body for the BODY env variable.
+ let body: string | undefined;
+ const messages = this.listSessionMessages(sessionId);
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const msg = messages[i];
+ if (msg && msg.role === "assistant" && msg.content) {
+ body = msg.content;
+ break;
+ }
+ }
+
+ launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot, undefined, configuredEnv, {
+ status: session.status,
+ failReason: session.failReason ?? undefined,
+ body,
+ title: session.summary ?? undefined,
+ });
}
private addSessionProcess(sessionId: string, processId: string | number, command: string): void {
diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts
index b7eadae..d079949 100644
--- a/src/tests/session.test.ts
+++ b/src/tests/session.test.ts
@@ -783,6 +783,68 @@ test("reporting a new prompt does not warn when the background request fails", a
assert.deepEqual(warnings, []);
});
+test(
+ "SessionManager notifies successful completion with session context",
+ { skip: process.platform === "win32" },
+ async () => {
+ const workspace = createTempDir("deepcode-notify-success-workspace-");
+ const home = createTempDir("deepcode-notify-success-home-");
+ setHomeDir(home);
+
+ const notifyOutput = path.join(workspace, "notify.jsonl");
+ const notifyScript = createNotifyRecorderScript(workspace);
+ const manager = createNotifyingSessionManager(
+ workspace,
+ [createChatResponse("final answer", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 })],
+ notifyScript,
+ notifyOutput
+ );
+
+ await manager.createSession({ text: "notify success" });
+
+ const records = await waitForNotifyRecords(notifyOutput, 1);
+ assert.equal(records[0]?.STATUS, "completed");
+ assert.equal(records[0]?.FAIL_REASON, null);
+ assert.equal(records[0]?.BODY, "final answer");
+ assert.equal(records[0]?.TITLE, "notify success");
+ assert.match(String(records[0]?.DURATION), /^\d+$/);
+ }
+);
+
+test(
+ "SessionManager notifies failed completion with failure context",
+ { skip: process.platform === "win32" },
+ async () => {
+ const workspace = createTempDir("deepcode-notify-failure-workspace-");
+ const home = createTempDir("deepcode-notify-failure-home-");
+ setHomeDir(home);
+
+ const notifyOutput = path.join(workspace, "notify.jsonl");
+ const notifyScript = createNotifyRecorderScript(workspace);
+ const manager = createNotifyingSessionManager(
+ workspace,
+ [
+ createChatResponse("first answer", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }),
+ new Error("second request failed"),
+ ],
+ notifyScript,
+ notifyOutput
+ );
+
+ const sessionId = await manager.createSession({ text: "notify failure" });
+ await waitForNotifyRecords(notifyOutput, 1);
+ await manager.replySession(sessionId, { text: "second prompt" });
+
+ const records = await waitForNotifyRecords(notifyOutput, 2);
+ const failedRecord = records[1];
+ assert.equal(failedRecord?.STATUS, "failed");
+ assert.equal(failedRecord?.FAIL_REASON, "second request failed");
+ assert.equal(failedRecord?.BODY, "first answer");
+ assert.notEqual(failedRecord?.BODY, "stale-body");
+ assert.equal(failedRecord?.TITLE, "notify failure");
+ }
+);
+
test("replySession continues without appending /continue as a user message", async () => {
const workspace = createTempDir("deepcode-continue-workspace-");
const home = createTempDir("deepcode-continue-home-");
@@ -1657,6 +1719,49 @@ function createSessionManager(projectRoot: string, machineId: string): SessionMa
});
}
+function createNotifyingSessionManager(
+ projectRoot: string,
+ responses: unknown[],
+ notifyPath: string,
+ notifyOutput: string
+): SessionManager {
+ const client = {
+ chat: {
+ completions: {
+ create: async () => {
+ const response = responses.shift();
+ assert.ok(response, "expected a queued chat response");
+ if (response instanceof Error) {
+ throw response;
+ }
+ return response;
+ },
+ },
+ },
+ };
+
+ return new SessionManager({
+ projectRoot,
+ createOpenAIClient: () => ({
+ client: client as any,
+ model: "test-model",
+ baseURL: "https://api.deepseek.com",
+ thinkingEnabled: false,
+ notify: notifyPath,
+ env: {
+ NOTIFY_OUTPUT: notifyOutput,
+ STATUS: "stale-status",
+ FAIL_REASON: "stale-failure",
+ BODY: "stale-body",
+ TITLE: "stale-title",
+ },
+ }),
+ getResolvedSettings: () => ({ model: "test-model" }),
+ renderMarkdown: (text) => text,
+ onAssistantMessage: () => {},
+ });
+}
+
function createMockedClientSessionManager(projectRoot: string, responses: unknown[]): SessionManager {
const client = {
chat: {
@@ -1740,6 +1845,45 @@ function createTempDir(prefix: string): string {
return dir;
}
+function createNotifyRecorderScript(dir: string): string {
+ const scriptPath = path.join(dir, "notify-recorder.cjs");
+ fs.writeFileSync(
+ scriptPath,
+ `#!/usr/bin/env node
+const fs = require("fs");
+const keys = ["DURATION", "STATUS", "FAIL_REASON", "BODY", "TITLE"];
+const record = {};
+for (const key of keys) {
+ record[key] = Object.prototype.hasOwnProperty.call(process.env, key) ? process.env[key] : null;
+}
+fs.appendFileSync(process.env.NOTIFY_OUTPUT, JSON.stringify(record) + "\\n", "utf8");
+`,
+ "utf8"
+ );
+ fs.chmodSync(scriptPath, 0o755);
+ return scriptPath;
+}
+
+async function waitForNotifyRecords(
+ outputPath: string,
+ expectedCount: number
+): Promise>> {
+ for (let attempt = 0; attempt < 100; attempt += 1) {
+ if (fs.existsSync(outputPath)) {
+ const records = fs
+ .readFileSync(outputPath, "utf8")
+ .split(/\r?\n/)
+ .filter(Boolean)
+ .map((line) => JSON.parse(line) as Record);
+ if (records.length >= expectedCount) {
+ return records;
+ }
+ }
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ }
+ assert.fail(`expected ${expectedCount} notify records in ${outputPath}`);
+}
+
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts
index 6990288..202f849 100644
--- a/src/tests/settings-and-notify.test.ts
+++ b/src/tests/settings-and-notify.test.ts
@@ -1,6 +1,12 @@
import { test } from "node:test";
import assert from "node:assert/strict";
-import { buildNotifyEnv, formatDurationSeconds, launchNotifyScript, type NotifySpawn } from "../common/notify";
+import {
+ buildNotifyEnv,
+ formatDurationSeconds,
+ launchNotifyScript,
+ type NotifyContext,
+ type NotifySpawn,
+} from "../common/notify";
import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings";
const TEST_PROCESS_ENV = {};
@@ -358,14 +364,52 @@ test("formatDurationSeconds preserves sub-second precision and trims trailing ze
assert.equal(formatDurationSeconds(4000), "4");
});
-test("buildNotifyEnv injects DURATION", () => {
+test("buildNotifyEnv injects DURATION without context", () => {
const env = buildNotifyEnv(2750, { HOME: "/tmp/home" });
assert.equal(env.HOME, "/tmp/home");
assert.equal(env.DURATION, "2");
+ assert.equal(env.STATUS, undefined);
+ assert.equal(env.FAIL_REASON, undefined);
+ assert.equal(env.BODY, undefined);
+ assert.equal(env.TITLE, undefined);
+});
+
+test("buildNotifyEnv injects STATUS, FAIL_REASON, BODY, and TITLE from context", () => {
+ const context: NotifyContext = {
+ status: "failed",
+ failReason: "API key not found",
+ body: "Hello, this is the last assistant message.",
+ title: "Fix login bug",
+ };
+ const env = buildNotifyEnv(5000, { HOME: "/tmp/home" }, context);
+ assert.equal(env.HOME, "/tmp/home");
+ assert.equal(env.DURATION, "5");
+ assert.equal(env.STATUS, "failed");
+ assert.equal(env.FAIL_REASON, "API key not found");
+ assert.equal(env.BODY, "Hello, this is the last assistant message.");
+ assert.equal(env.TITLE, "Fix login bug");
+});
+
+test("buildNotifyEnv omits optional context fields when not provided", () => {
+ const env = buildNotifyEnv(
+ 1000,
+ {
+ HOME: "/tmp/home",
+ STATUS: "stale-status",
+ FAIL_REASON: "stale-failure",
+ BODY: "stale-body",
+ TITLE: "stale-title",
+ },
+ { status: "completed" }
+ );
+ assert.equal(env.STATUS, "completed");
+ assert.equal(env.FAIL_REASON, undefined);
+ assert.equal(env.BODY, undefined);
+ assert.equal(env.TITLE, undefined);
});
test(
- "launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts",
+ "launchNotifyScript passes DURATION, context vars, and falls back to /bin/sh for non-executable scripts",
{ skip: process.platform === "win32" },
() => {
const calls: Array<{
@@ -390,7 +434,13 @@ test(
};
};
- launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" });
+ const context: NotifyContext = {
+ status: "completed",
+ body: "Task finished successfully.",
+ title: "Fix login bug",
+ };
+
+ launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" }, context);
assert.equal(calls.length, 2);
assert.equal(calls[0]?.command, "/tmp/notify.sh");
@@ -398,9 +448,16 @@ test(
assert.equal(calls[0]?.options.cwd, "/tmp/project");
assert.equal(calls[0]?.options.env?.DURATION, "2");
assert.equal(calls[0]?.options.env?.WEBHOOK, "configured");
+ assert.equal(calls[0]?.options.env?.STATUS, "completed");
+ assert.equal(calls[0]?.options.env?.FAIL_REASON, undefined);
+ assert.equal(calls[0]?.options.env?.BODY, "Task finished successfully.");
+ assert.equal(calls[0]?.options.env?.TITLE, "Fix login bug");
assert.equal(calls[1]?.command, "/bin/sh");
assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]);
assert.equal(calls[1]?.options.cwd, "/tmp/project");
assert.equal(calls[1]?.options.env?.DURATION, "2");
+ assert.equal(calls[1]?.options.env?.STATUS, "completed");
+ assert.equal(calls[1]?.options.env?.BODY, "Task finished successfully.");
+ assert.equal(calls[1]?.options.env?.TITLE, "Fix login bug");
}
);
From a3ff70e82d548a8c1273ea377844f078cbd0ae00 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Tue, 19 May 2026 10:43:55 +0800
Subject: [PATCH 067/129] docs(notify): add Windows Terminal, Linux, and msg
popup notification examples; add edge-case tests
- Expand OSC 9 example to cover both iTerm2 and Windows Terminal
- Add .bat example for Windows Terminal users
- Add Linux notify-send example
- Add Windows msg popup notification example
- Add tests for empty-string rejection and special character preservation
---
docs/configuration.md | 32 +++++++++++++++++++++++----
docs/configuration_en.md | 32 +++++++++++++++++++++++----
src/tests/settings-and-notify.test.ts | 27 ++++++++++++++++++++++
3 files changed, 83 insertions(+), 8 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index 45aaab0..7c2880c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -83,14 +83,14 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
}
```
-**iTerm2 终端通知示例**:
+**终端内通知示例(支持 iTerm2 / Windows Terminal)**:
-如果你的终端是 iTerm2,可以直接通过 OSC 9 转义序列弹出通知,无需额外脚本。创建以下脚本(如 `~/.deepcode/notify.sh`):
+如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。创建以下脚本(如 `~/.deepcode/notify.sh`):
```bash
#!/bin/bash
-# iTerm2 OSC 9 通知
-echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
+# iTerm2 / Windows Terminal OSC 9 通知
+printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}"
```
```json
@@ -99,6 +99,14 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
}
```
+Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本:
+
+```batch
+@echo off
+REM Windows Terminal OSC 9 通知
+echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07
+```
+
**macOS 系统通知示例**:
```bash
@@ -107,6 +115,22 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\""
```
+**Linux 系统通知示例**(需安装 `libnotify-bin`):
+
+```bash
+#!/bin/bash
+# Linux notify-send 通知
+notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s"
+```
+
+**Windows msg 弹窗通知示例**:
+
+```batch
+@echo off
+REM Windows msg 弹窗通知
+msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)"
+```
+
#### `webSearchTool` — 自定义联网搜索
Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径:
diff --git a/docs/configuration_en.md b/docs/configuration_en.md
index 606fcab..5d931f4 100644
--- a/docs/configuration_en.md
+++ b/docs/configuration_en.md
@@ -83,14 +83,14 @@ The following context is injected as environment variables when the notify scrip
}
```
-**iTerm2 Notification Example**:
+**Terminal Notification Example (iTerm2 / Windows Terminal)**:
-On iTerm2 you can use the OSC 9 escape sequence for native notifications. Create a script (e.g., `~/.deepcode/notify.sh`):
+On iTerm2 or Windows Terminal you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. Create a script (e.g., `~/.deepcode/notify.sh`):
```bash
#!/bin/bash
-# iTerm2 OSC 9 notification
-echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
+# iTerm2 / Windows Terminal OSC 9 notification
+printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}"
```
```json
@@ -99,6 +99,14 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
}
```
+Windows users on Git Bash can use the same script; alternatively create a `.bat` script:
+
+```batch
+@echo off
+REM Windows Terminal OSC 9 notification
+echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07
+```
+
**macOS System Notification Example**:
```bash
@@ -107,6 +115,22 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07"
osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\""
```
+**Linux System Notification Example** (requires `libnotify-bin`):
+
+```bash
+#!/bin/bash
+# Linux notify-send notification
+notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s"
+```
+
+**Windows msg Popup Notification Example**:
+
+```batch
+@echo off
+REM Windows msg popup notification
+msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)"
+```
+
#### `webSearchTool` — Custom Web Search
Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script:
diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts
index 202f849..1707aff 100644
--- a/src/tests/settings-and-notify.test.ts
+++ b/src/tests/settings-and-notify.test.ts
@@ -408,6 +408,33 @@ test("buildNotifyEnv omits optional context fields when not provided", () => {
assert.equal(env.TITLE, undefined);
});
+test("buildNotifyEnv ignores empty strings in context", () => {
+ const env = buildNotifyEnv(
+ 1000,
+ { HOME: "/tmp/home" },
+ {
+ status: "",
+ failReason: "",
+ body: "",
+ title: "",
+ }
+ );
+ assert.equal(env.STATUS, undefined);
+ assert.equal(env.FAIL_REASON, undefined);
+ assert.equal(env.BODY, undefined);
+ assert.equal(env.TITLE, undefined);
+});
+
+test("buildNotifyEnv preserves special characters in body and title", () => {
+ const context: NotifyContext = {
+ body: 'Line 1\nLine 2\tindented "quoted"',
+ title: "Fix: login & signup (urgent)",
+ };
+ const env = buildNotifyEnv(1000, {}, context);
+ assert.equal(env.BODY, 'Line 1\nLine 2\tindented "quoted"');
+ assert.equal(env.TITLE, "Fix: login & signup (urgent)");
+});
+
test(
"launchNotifyScript passes DURATION, context vars, and falls back to /bin/sh for non-executable scripts",
{ skip: process.platform === "win32" },
From 479606f6a7087398302334996e95cb8eb2d841b3 Mon Sep 17 00:00:00 2001
From: lellansin
Date: Tue, 19 May 2026 13:33:28 +0800
Subject: [PATCH 068/129] docs(notify): replace terminal notification examples
with Feishu webhook example
- Remove iTerm2/Windows Terminal OSC 9, macOS osascript, Linux notify-send, and Windows msg examples (OSC 9 is not compatible with current spawn+stdio:ignore architecture)
- Add Feishu (Lark) webhook notification example in both Chinese and English docs
- Keep the env variable table (DURATION, STATUS, FAIL_REASON, BODY, TITLE) unchanged
---
docs/configuration.md | 62 ++++++++++++++--------------------------
docs/configuration_en.md | 62 ++++++++++++++--------------------------
2 files changed, 44 insertions(+), 80 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index 7c2880c..b05a44f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -83,53 +83,35 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
}
```
-**终端内通知示例(支持 iTerm2 / Windows Terminal)**:
+**飞书 Webhook 通知示例**:
-如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。创建以下脚本(如 `~/.deepcode/notify.sh`):
+`node` 构建 JSON(自动转义特殊字符),`curl` 发送:
```bash
#!/bin/bash
-# iTerm2 / Windows Terminal OSC 9 通知
-printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}"
-```
-
-```json
-{
- "notify": "/Users/you/.deepcode/notify.sh"
-}
-```
-
-Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本:
-
-```batch
-@echo off
-REM Windows Terminal OSC 9 通知
-echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07
-```
-
-**macOS 系统通知示例**:
-
-```bash
-#!/bin/bash
-# macOS 系统通知
-osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\""
-```
-
-**Linux 系统通知示例**(需安装 `libnotify-bin`):
+WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx"
+
+STATUS="${STATUS:-completed}"
+TITLE="${TITLE:-Untitled}"
+DURATION="${DURATION:-0}"
+BODY="${BODY:-(no output)}"
+
+PAYLOAD=$(node -e "
+process.stdout.write(JSON.stringify({
+ msg_type: 'interactive',
+ card: {
+ header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } },
+ elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }]
+ }
+}))
+")
-```bash
-#!/bin/bash
-# Linux notify-send 通知
-notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s"
+curl -s -X POST "$WEBHOOK_URL" \
+ -H "Content-Type: application/json" \
+ -d "$PAYLOAD"
```
-**Windows msg 弹窗通知示例**:
-
-```batch
-@echo off
-REM Windows msg 弹窗通知
-msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)"
-```
+将 `WEBHOOK_URL` 替换为你的飞书机器人 Webhook 地址。更多变量参考上表。同样适用于 Slack、企业微信等 webhook 类通知,只需修改 JSON payload 格式。
#### `webSearchTool` — 自定义联网搜索
diff --git a/docs/configuration_en.md b/docs/configuration_en.md
index 5d931f4..4f2f94d 100644
--- a/docs/configuration_en.md
+++ b/docs/configuration_en.md
@@ -83,53 +83,35 @@ The following context is injected as environment variables when the notify scrip
}
```
-**Terminal Notification Example (iTerm2 / Windows Terminal)**:
+**Feishu (Lark) Webhook Notification Example**:
-On iTerm2 or Windows Terminal you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. Create a script (e.g., `~/.deepcode/notify.sh`):
+`node` builds the JSON (auto-escapes special characters), `curl` sends it:
```bash
#!/bin/bash
-# iTerm2 / Windows Terminal OSC 9 notification
-printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}"
-```
-
-```json
-{
- "notify": "/Users/you/.deepcode/notify.sh"
-}
-```
-
-Windows users on Git Bash can use the same script; alternatively create a `.bat` script:
-
-```batch
-@echo off
-REM Windows Terminal OSC 9 notification
-echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07
-```
-
-**macOS System Notification Example**:
-
-```bash
-#!/bin/bash
-# macOS system notification
-osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\""
-```
-
-**Linux System Notification Example** (requires `libnotify-bin`):
+WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx"
+
+STATUS="${STATUS:-completed}"
+TITLE="${TITLE:-Untitled}"
+DURATION="${DURATION:-0}"
+BODY="${BODY:-(no output)}"
+
+PAYLOAD=$(node -e "
+process.stdout.write(JSON.stringify({
+ msg_type: 'interactive',
+ card: {
+ header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } },
+ elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }]
+ }
+}))
+")
-```bash
-#!/bin/bash
-# Linux notify-send notification
-notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s"
+curl -s -X POST "$WEBHOOK_URL" \
+ -H "Content-Type: application/json" \
+ -d "$PAYLOAD"
```
-**Windows msg Popup Notification Example**:
-
-```batch
-@echo off
-REM Windows msg popup notification
-msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)"
-```
+Replace `WEBHOOK_URL` with your Feishu bot webhook URL. See the table above for all available variables. This pattern also works for other webhook-based notifications (Slack, WeCom, etc.) — just adjust the JSON payload format.
#### `webSearchTool` — Custom Web Search
From 7e5eeda26829b14eb3ed503b550db06c1145acf6 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Tue, 19 May 2026 15:05:00 +0800
Subject: [PATCH 069/129] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20raw=20?=
=?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=B6=88=E6=81=AF=E7=9B=B4=E6=8E=A5?=
=?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 Raw 模式下,使用 process.stdout.write 直接输出所有可见消息
- 清屏并重置光标位置,避免 Ink 组件干扰
- 显示提示信息,指导用户按 ESC 退出 raw 模式
- 优化终端尺寸变化时的重绘逻辑
- 更新依赖,确保 raw 模式变动触发重新渲染
---
src/ui/App.tsx | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 9189df6..e39fd03 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -434,8 +434,31 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
}
lastRenderedColumnsRef.current = stableColumns;
+ if (mode === RawMode.Raw) {
+ // In raw mode, re-render all messages directly to stdout at the new width.
+ // Use process.stdout.write instead of writeRef to avoid Ink interference.
+ process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
+ const activeSessionId = sessionManager.getActiveSessionId();
+ const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : [];
+ for (const msg of allMessages) {
+ process.stdout.write("\n");
+ process.stdout.write(renderMessageToStdout(msg, mode) + "\n\n");
+ }
+ if (allMessages.length > 0) {
+ process.stdout.write("\n\n");
+ process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
+ } else {
+ process.stdout.write("\n");
+ process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)"));
+ process.stdout.write("\n\n");
+ process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
+ }
+ return;
+ }
+
// Force full redraw on terminal resize to avoid stale wrapped rows.
writeRef.current("\u001B[2J\u001B[H");
+
setMessages([]);
setShowWelcome(false);
setWelcomeNonce((n) => n + 1);
@@ -447,7 +470,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
setMessages(nextMessages);
setShowWelcome(true);
}, 0);
- }, [busy, sessionManager, stableColumns, stdout]);
+ }, [busy, mode, sessionManager, stableColumns, stdout]);
+
const screenWidth = useMemo(() => stableColumns ?? stdout?.columns ?? 80, [stableColumns, stdout]);
const promptHistory = useMemo(() => {
return messages
From faf10c3e087d214bf863e9df14040176e30de821 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Tue, 19 May 2026 15:12:08 +0800
Subject: [PATCH 070/129] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=AE=BD=E5=BA=A6=E7=9B=B8=E5=85=B3=E7=9A=84?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E5=BC=95=E7=94=A8=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 合并并调整了关于窗口宽度columns的使用,去除了stableColumns状态
- 引用lastRenderedColumnsRef改为直接使用columns,避免延迟更新
- 将多个相关的useRef(writeRef、rawModeRef、messagesRef、processStdoutRef)移至同一位置声明
- 调整useEffect依赖项,改为监听columns代替stableColumns
- 优化RawMode下消息重绘逻辑,确保宽度变化时重新渲染
- 统一了screenWidth的计算逻辑,简化代码结构
---
src/ui/App.tsx | 30 ++++++++++++------------------
1 file changed, 12 insertions(+), 18 deletions(-)
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index e39fd03..582abaf 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -55,7 +55,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
const { exit } = useApp();
const { stdout, write } = useStdout();
const { columns } = useWindowSize();
+ const { mode, setMode } = useRawModeContext();
const initialPromptSubmittedRef = useRef(false);
+ const processStdoutRef = useRef