mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
bun.lock
49
bun.lock
@@ -28,6 +28,9 @@
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@claude-code-best/agent-tools": "workspace:*",
|
||||
"@claude-code-best/builtin-tools": "workspace:*",
|
||||
"@claude-code-best/mcp-client": "workspace:*",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@langfuse/otel": "^5.1.0",
|
||||
@@ -176,10 +179,24 @@
|
||||
"wrap-ansi": "^10.0.0",
|
||||
},
|
||||
},
|
||||
"packages/agent-tools": {
|
||||
"name": "@claude-code-best/agent-tools",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"zod": "^3.25.0",
|
||||
},
|
||||
},
|
||||
"packages/audio-capture-napi": {
|
||||
"name": "audio-capture-napi",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"packages/builtin-tools": {
|
||||
"name": "@claude-code-best/builtin-tools",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@claude-code-best/agent-tools": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/color-diff-napi": {
|
||||
"name": "color-diff-napi",
|
||||
"version": "1.0.0",
|
||||
@@ -194,6 +211,18 @@
|
||||
"sharp": "^0.33.5",
|
||||
},
|
||||
},
|
||||
"packages/mcp-client": {
|
||||
"name": "@claude-code-best/mcp-client",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@claude-code-best/agent-tools": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^10.0.0",
|
||||
"p-map": "^4.0.0",
|
||||
"zod": "^3.25.0",
|
||||
},
|
||||
},
|
||||
"packages/modifiers-napi": {
|
||||
"name": "modifiers-napi",
|
||||
"version": "1.0.0",
|
||||
@@ -410,6 +439,12 @@
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "https://registry.npmmirror.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.11.tgz", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
|
||||
|
||||
"@claude-code-best/agent-tools": ["@claude-code-best/agent-tools@workspace:packages/agent-tools"],
|
||||
|
||||
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
|
||||
|
||||
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
|
||||
|
||||
"@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "https://registry.npmmirror.com/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.9.2", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
@@ -1074,6 +1109,8 @@
|
||||
|
||||
"agent-base": ["agent-base@8.0.0", "https://registry.npmmirror.com/agent-base/-/agent-base-8.0.0.tgz", {}, "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg=="],
|
||||
|
||||
"aggregate-error": ["aggregate-error@3.1.0", "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -1160,6 +1197,8 @@
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
|
||||
|
||||
"clean-stack": ["clean-stack@2.2.0", "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@4.0.1", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
|
||||
|
||||
"cli-highlight": ["cli-highlight@2.1.11", "https://registry.npmmirror.com/cli-highlight/-/cli-highlight-2.1.11.tgz", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
|
||||
@@ -2142,6 +2181,14 @@
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@claude-code-best/agent-tools/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@claude-code-best/mcp-client/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"@claude-code-best/mcp-client/p-map": ["p-map@4.0.0", "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
|
||||
|
||||
"@claude-code-best/mcp-client/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
|
||||
|
||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
@@ -2282,6 +2329,8 @@
|
||||
|
||||
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"aggregate-error/indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"ansi-escapes/type-fest": ["type-fest@0.21.3", "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"chrome-mcp-shared/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
346
docs/extensibility/mcp-configuration.mdx
Normal file
346
docs/extensibility/mcp-configuration.mdx
Normal file
@@ -0,0 +1,346 @@
|
||||
---
|
||||
title: "MCP 配置 - 多来源合并、作用域与策略管控"
|
||||
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
|
||||
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
|
||||
---
|
||||
|
||||
## 配置来源与作用域
|
||||
|
||||
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
|
||||
|
||||
### 来源列表
|
||||
|
||||
| 来源 | Scope | 文件/接口 | 说明 |
|
||||
|------|-------|----------|------|
|
||||
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | **排他模式**:存在时忽略所有其他来源 |
|
||||
| 本地项目 | `local` | `<project>/.claude/settings.local.json` | 项目级私有配置(不提交到 VCS) |
|
||||
| 项目配置 | `project` | `<project>/.mcp.json` | 项目级共享配置(可提交到 VCS) |
|
||||
| 用户全局 | `user` | `~/.claude/settings.json` | 用户级配置,所有项目共享 |
|
||||
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` / `.mcpb` | 插件提供的 MCP 服务器 |
|
||||
| claude.ai | `claudeai` | 通过 API 获取 | claude.ai 网页端配置的连接器 |
|
||||
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
|
||||
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
|
||||
|
||||
### 合并优先级(从低到高)
|
||||
|
||||
```
|
||||
claude.ai 连接器 ← 最低优先级
|
||||
↓ 去重
|
||||
插件服务器
|
||||
↓ 去重
|
||||
用户全局配置
|
||||
↓
|
||||
项目配置(.mcp.json) ← 需要用户审批
|
||||
↓
|
||||
本地项目配置
|
||||
↓
|
||||
动态配置(内置 MCP) ← 最高优先级
|
||||
```
|
||||
|
||||
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
|
||||
|
||||
## 企业管控模式
|
||||
|
||||
当 `managed-mcp.json` 文件存在时,进入 **排他模式**:
|
||||
|
||||
```typescript
|
||||
// config.ts:1084
|
||||
if (doesEnterpriseMcpConfigExist()) {
|
||||
// 只返回企业配置,忽略所有用户/项目/插件/claude.ai 配置
|
||||
return { servers: filtered, errors: [] }
|
||||
}
|
||||
```
|
||||
|
||||
特性:
|
||||
- 路径由系统管理决定(`getManagedFilePath()` + `managed-mcp.json`)
|
||||
- 覆盖所有用户级、项目级、插件和 claude.ai 配置
|
||||
- 仍然应用策略过滤(allowlist/denylist)
|
||||
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
|
||||
|
||||
## 传输类型与配置 Schema
|
||||
|
||||
### stdio(默认)
|
||||
|
||||
启动子进程,通过 stdin/stdout JSON-RPC 通信。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@my-org/mcp-server"],
|
||||
"env": { "API_KEY": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
|
||||
|
||||
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
|
||||
|
||||
### SSE(Server-Sent Events)
|
||||
|
||||
通过 HTTP SSE 连接远程 MCP 服务器。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-remote": {
|
||||
"type": "sse",
|
||||
"url": "https://mcp.example.com/sse",
|
||||
"headers": { "Authorization": "Bearer ..." },
|
||||
"oauth": {
|
||||
"clientId": "...",
|
||||
"authServerMetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,15 分钟 TTL 缓存避免重复提示。
|
||||
|
||||
### HTTP(Streamable HTTP)
|
||||
|
||||
HTTP 流式传输。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-http": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.example.com/mcp",
|
||||
"headers": { "X-API-Key": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
支持与 SSE 相同的 OAuth 配置。
|
||||
|
||||
### WebSocket
|
||||
|
||||
```json
|
||||
{
|
||||
"my-ws": {
|
||||
"type": "ws",
|
||||
"url": "wss://mcp.example.com/ws"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IDE 专用类型(内部)
|
||||
|
||||
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
|
||||
|
||||
- `sse-ide`:使用 lockfile token 认证
|
||||
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
|
||||
|
||||
### SDK 类型(内部)
|
||||
|
||||
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
|
||||
|
||||
### claude.ai 代理类型(内部)
|
||||
|
||||
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
|
||||
|
||||
## 配置操作
|
||||
|
||||
### 添加 MCP 服务器
|
||||
|
||||
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`:
|
||||
|
||||
```bash
|
||||
# 添加到用户配置
|
||||
claude mcp add my-server -s user -- npx @my-org/mcp-server
|
||||
|
||||
# 添加到项目配置
|
||||
claude mcp add my-server -s project -- npx @my-org/mcp-server
|
||||
|
||||
# 添加 HTTP 类型
|
||||
claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
|
||||
```
|
||||
|
||||
添加时的验证流程:
|
||||
|
||||
1. **名称校验**:只允许字母、数字、连字符和下划线
|
||||
2. **保留名检查**:`claude-in-chrome` 和 `computer-use` 被保留
|
||||
3. **企业管控检查**:企业模式下拒绝添加
|
||||
4. **Schema 验证**:Zod 校验配置格式
|
||||
5. **策略检查**:denylist 拒绝、allowlist 验证
|
||||
|
||||
### 移除 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp remove my-server -s user
|
||||
```
|
||||
|
||||
### 列出 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp list
|
||||
```
|
||||
|
||||
## 项目配置审批
|
||||
|
||||
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
|
||||
|
||||
```typescript
|
||||
// config.ts:1166
|
||||
const approvedProjectServers: Record<string, ScopedMcpServerConfig> = {}
|
||||
for (const [name, config] of Object.entries(projectServers)) {
|
||||
if (getProjectMcpServerStatus(name) === 'approved') {
|
||||
approvedProjectServers[name] = config
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
首次打开项目时,Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
|
||||
|
||||
## 插件 MCP 集成
|
||||
|
||||
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
|
||||
|
||||
```typescript
|
||||
// 插件 MCP 加载流程
|
||||
const pluginResult = await loadAllPluginsCacheOnly()
|
||||
const pluginServerResults = await Promise.all(
|
||||
pluginResult.enabled.map(plugin => getPluginMcpServers(plugin, mcpErrors))
|
||||
)
|
||||
```
|
||||
|
||||
### 插件命名空间
|
||||
|
||||
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
|
||||
|
||||
### 去重机制
|
||||
|
||||
插件服务器通过内容签名去重(`dedupPluginMcpServers`):
|
||||
|
||||
- **stdio 类型**:签名 = `stdio:` + JSON.stringify([command, ...args])
|
||||
- **URL 类型**:签名 = `url:` + 原始 URL(unwrap CCR proxy URL)
|
||||
- **sdk 类型**:签名为 null,不去重
|
||||
|
||||
去重规则:
|
||||
1. 手动配置优先于插件配置
|
||||
2. 先加载的插件优先于后加载的
|
||||
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
|
||||
|
||||
### claude.ai 连接器去重
|
||||
|
||||
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`):
|
||||
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
|
||||
- 连接器名格式为 `claude.ai <DisplayName>`
|
||||
|
||||
## 策略管控
|
||||
|
||||
### Allowlist / Denylist
|
||||
|
||||
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
|
||||
|
||||
```typescript
|
||||
// config.ts:1243 - 最终策略过滤
|
||||
for (const [name, serverConfig] of Object.entries(configs)) {
|
||||
if (!isMcpServerAllowedByPolicy(name, serverConfig)) {
|
||||
continue // 跳过策略禁止的服务器
|
||||
}
|
||||
filtered[name] = serverConfig
|
||||
}
|
||||
```
|
||||
|
||||
策略检查考虑:
|
||||
- 服务器名称匹配
|
||||
- stdio 类型的 command + args 匹配
|
||||
- URL 类型的 URL 模式匹配(支持通配符)
|
||||
|
||||
### 插件专用模式
|
||||
|
||||
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
|
||||
|
||||
## 环境变量展开
|
||||
|
||||
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-server": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/mcp-server"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_KEY",
|
||||
"DB_URL": "${DATABASE_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
展开时缺失的变量会生成警告信息,但不阻止配置加载。
|
||||
|
||||
## 内置 MCP 动态注册
|
||||
|
||||
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
|
||||
|
||||
### Computer Use MCP
|
||||
|
||||
```typescript
|
||||
// src/utils/computerUse/setup.ts
|
||||
export function setupComputerUseMCP(): {
|
||||
mcpConfig: Record<string, ScopedMcpServerConfig>
|
||||
allowedTools: string[]
|
||||
} {
|
||||
return {
|
||||
mcpConfig: {
|
||||
"computer-use": {
|
||||
type: "stdio",
|
||||
command: process.execPath,
|
||||
args: ["--computer-use-mcp"],
|
||||
scope: "dynamic",
|
||||
}
|
||||
},
|
||||
allowedTools: ["mcp__computer-use__screenshot", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用条件:
|
||||
- Feature flag `CHICAGO_MCP` 开启
|
||||
- `getPlatform() !== "unknown"`(macOS/Windows/Linux)
|
||||
- 非非交互式会话
|
||||
- GrowthBook gate `getChicagoEnabled()` 返回 true
|
||||
|
||||
### Claude in Chrome MCP
|
||||
|
||||
```typescript
|
||||
// 类似 Computer Use,在 main.tsx 中注册
|
||||
const { mcpConfig, allowedTools, systemPrompt } = setupClaudeInChrome()
|
||||
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
|
||||
```
|
||||
|
||||
启用条件:
|
||||
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
|
||||
- Chrome 扩展已安装
|
||||
|
||||
### VSCode SDK MCP
|
||||
|
||||
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
|
||||
|
||||
## 保留名称
|
||||
|
||||
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
|
||||
|
||||
| 名称 | 用途 | 检查条件 |
|
||||
|------|------|---------|
|
||||
| `claude-in-chrome` | Chrome 浏览器控制 | 始终检查 |
|
||||
| `computer-use` | 桌面自动化 | `CHICAGO_MCP` feature flag 开启时检查 |
|
||||
| `claude-vscode` | VSCode IDE 集成 | 由 SDK 传入,不经过名称检查 |
|
||||
|
||||
保留名检查在两个位置:
|
||||
1. `addMcpConfig()`(`config.ts:636-648`)— 运行时拒绝
|
||||
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
|
||||
|
||||
## 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/mcp/config.ts` | 配置管理核心:合并、去重、策略、添加/删除 |
|
||||
| `src/services/mcp/types.ts` | Zod Schema 定义、类型声明 |
|
||||
| `src/services/mcp/client.ts` | 连接管理、传输层选择 |
|
||||
| `src/utils/plugins/mcpPluginIntegration.ts` | 插件 MCP 配置加载 |
|
||||
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
|
||||
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
|
||||
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |
|
||||
@@ -1,25 +1,32 @@
|
||||
---
|
||||
title: "MCP 协议 - 连接管理、工具发现与执行链路"
|
||||
description: "从源码角度解析 Claude Code 的 MCP 集成:7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"]
|
||||
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */}
|
||||
{/* 本章目标:从源码角度揭示 MCP 客户端的两种运行模式(内置/外部)、连接管理、工具发现协议和执行链路 */}
|
||||
|
||||
## 架构总览:从配置到可用工具
|
||||
|
||||
```
|
||||
settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
|
||||
配置层(多来源合并)
|
||||
├── settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } ← 外部
|
||||
├── .mcp.json: 项目级 MCP 配置 ← 外部
|
||||
├── 插件 manifest (.mcp.json / .mcpb) ← 外部(插件)
|
||||
├── claude.ai connectors ← 外部(远程)
|
||||
├── enterprise managed-mcp.json ← 外部(企业管控)
|
||||
├── setupComputerUseMCP() / setupClaudeInChrome() ← 内置(动态注册)
|
||||
└── SDK 传入 (type:'sdk') ← 内置(IDE 嵌入)
|
||||
↓
|
||||
getAllMcpConfigs() ← enterprise 独占或合并 user/project/local + plugin + claude.ai
|
||||
getAllMcpConfigs() ← enterprise 独占 或 合并 user/project/local + plugin + claude.ai
|
||||
↓
|
||||
useManageMCPConnections() ← React Hook 管理连接生命周期
|
||||
↓
|
||||
connectToServer(name, config) ← memoize 缓存(lodash memoize)
|
||||
├── 创建 Transport(stdio/sse/http/...)
|
||||
├── new Client() ← @modelcontextprotocol/sdk
|
||||
├── client.connect(transport) ← 超时控制(MCP_TIMEOUT, 默认 30s)
|
||||
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending }
|
||||
├── 判断:内置 MCP → InProcessTransport(同进程)
|
||||
├── 判断:外部 stdio → StdioClientTransport(子进程)
|
||||
├── 判断:远程 SSE/HTTP/WS → 网络传输
|
||||
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending | disabled }
|
||||
↓
|
||||
fetchToolsForClient(client) ← LRU(20) 缓存
|
||||
├── client.request({ method: 'tools/list' })
|
||||
@@ -30,19 +37,208 @@ assembleToolPool() ← 合并内置工具 + MCP 工具
|
||||
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
|
||||
```
|
||||
|
||||
## 两种 MCP 模式:内置 vs 外部
|
||||
|
||||
Claude Code 的 MCP 实现区分 **内置 MCP 服务器** 和 **外部 MCP 服务器**。两者使用相同的客户端协议和工具发现机制,但在连接方式、生命周期管理和配置来源上完全不同。
|
||||
|
||||
### 内置 MCP 服务器
|
||||
|
||||
内置 MCP 服务器由 Claude Code 自身提供,无需用户手动配置。它们在启动时自动注册为 `dynamic` scope 的配置,并在同进程内运行。
|
||||
|
||||
| 服务器 | 名称 | 包路径 | Feature Flag | 启用方式 |
|
||||
|--------|------|--------|-------------|---------|
|
||||
| Computer Use | `computer-use` | `@ant/computer-use-mcp` | `CHICAGO_MCP` | GrowthBook gate + macOS + interactive |
|
||||
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置 |
|
||||
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
|
||||
|
||||
#### InProcessTransport:零开销同进程通信
|
||||
|
||||
内置服务器通过 `InProcessTransport`(`src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**:
|
||||
|
||||
```typescript
|
||||
// 创建一对 linked transport —— 消息在两端之间直接传递
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
|
||||
// server 端连接到 serverTransport
|
||||
inProcessServer = createComputerUseMcpServerForCli()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
|
||||
// client 端使用 clientTransport(与外部 MCP 的 Client 相同接口)
|
||||
transport = clientTransport
|
||||
```
|
||||
|
||||
`InProcessTransport` 的核心设计:
|
||||
- `send()` 通过 `queueMicrotask()` 异步投递消息到对端,避免同步请求/响应的栈深度问题
|
||||
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
|
||||
- 无网络开销、无 IPC 序列化、无进程启动时间
|
||||
|
||||
#### 动态注册流程
|
||||
|
||||
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`:
|
||||
|
||||
```typescript
|
||||
// main.tsx: Computer Use MCP 动态注册
|
||||
if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiveSession()) {
|
||||
const { getChicagoEnabled } = await import("src/utils/computerUse/gates.js")
|
||||
if (getChicagoEnabled()) {
|
||||
const { setupComputerUseMCP } = await import("src/utils/computerUse/setup.js")
|
||||
const { mcpConfig, allowedTools } = setupComputerUseMCP()
|
||||
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
|
||||
allowedTools.push(...cuTools)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`setupComputerUseMCP()` 返回的配置(`src/utils/computerUse/setup.ts`):
|
||||
|
||||
```typescript
|
||||
{
|
||||
"computer-use": {
|
||||
type: "stdio", // 类型标记为 stdio(但 client.ts 会拦截为 InProcessTransport)
|
||||
command: process.execPath,
|
||||
args: ["--computer-use-mcp"],
|
||||
scope: "dynamic", // 动态作用域,不持久化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 连接时拦截
|
||||
|
||||
`connectToServer()` 在 `client.ts:906-944` 中根据服务器名拦截内置服务器:
|
||||
|
||||
```typescript
|
||||
// Chrome MCP — 在 process 内运行,避免 ~325MB 子进程
|
||||
if (isClaudeInChromeMCPServer(name)) {
|
||||
const { createChromeContext } = await import('../../utils/claudeInChrome/mcpServer.js')
|
||||
const { createClaudeForChromeMcpServer } = await import('@ant/claude-for-chrome-mcp')
|
||||
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
|
||||
const context = createChromeContext(config.env)
|
||||
inProcessServer = createClaudeForChromeMcpServer(context)
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
transport = clientTransport
|
||||
}
|
||||
|
||||
// Computer Use MCP — 同理
|
||||
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
const { createComputerUseMcpServerForCli } = await import('../../utils/computerUse/mcpServer.js')
|
||||
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
|
||||
inProcessServer = await createComputerUseMcpServerForCli()
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
transport = clientTransport
|
||||
}
|
||||
```
|
||||
|
||||
#### 保留名称保护
|
||||
|
||||
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`):
|
||||
|
||||
```typescript
|
||||
// 添加 MCP 配置时检查保留名
|
||||
if (isClaudeInChromeMCPServer(name)) {
|
||||
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
|
||||
}
|
||||
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
|
||||
}
|
||||
```
|
||||
|
||||
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
|
||||
|
||||
#### VSCode SDK MCP
|
||||
|
||||
VSCode SDK MCP 是特殊的内置模式。IDE(如 VS Code、JetBrains)通过嵌入方式启动 Claude Code,并传入 `type:'sdk'` 的 MCP 配置。这类配置:
|
||||
- 不经过保留名称检查(IDE 可以使用任意名称)
|
||||
- 不参与 enterprise MCP 的排他控制
|
||||
- 通过 VSCode SDK transport 连接
|
||||
- 支持双向通知(如 `file_updated`、`experiment_gates`)
|
||||
|
||||
```typescript
|
||||
// src/services/mcp/vscodeSdkMcp.ts
|
||||
export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
const client = sdkClients.find(client => client.name === 'claude-vscode')
|
||||
if (client && client.type === 'connected') {
|
||||
// 注册 log_event 通知处理器
|
||||
client.client.setNotificationHandler(LogEventNotificationSchema(), ...)
|
||||
// 发送实验门控到 VSCode
|
||||
client.client.notification({ method: 'experiment_gates', params: { gates } })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 外部 MCP 服务器
|
||||
|
||||
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
|
||||
|
||||
#### 配置来源
|
||||
|
||||
| 来源 | Scope | 文件位置 | 优先级 |
|
||||
|------|-------|---------|--------|
|
||||
| 项目配置 | `project` | `<project>/.mcp.json` | 最高(同名覆盖) |
|
||||
| 本地配置 | `local` | `<project>/.claude/settings.local.json` | 高 |
|
||||
| 用户配置 | `user` | `~/.claude/settings.json` | 中 |
|
||||
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` | 中 |
|
||||
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
|
||||
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
|
||||
|
||||
#### 配置示例
|
||||
|
||||
```json
|
||||
// settings.json / .mcp.json 中的 MCP 配置
|
||||
{
|
||||
"mcpServers": {
|
||||
// stdio 类型 — 启动子进程
|
||||
"my-database": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/db-mcp-server"],
|
||||
"env": { "DB_URL": "postgres://..." }
|
||||
},
|
||||
|
||||
// HTTP 流类型 — 远程服务器
|
||||
"remote-api": {
|
||||
"type": "http",
|
||||
"url": "https://api.example.com/mcp"
|
||||
},
|
||||
|
||||
// SSE 类型 — Server-Sent Events
|
||||
"realtime-feed": {
|
||||
"type": "sse",
|
||||
"url": "https://feed.example.com/sse"
|
||||
},
|
||||
|
||||
// WebSocket 类型
|
||||
"ws-service": {
|
||||
"type": "ws",
|
||||
"url": "wss://ws.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置合并与去重
|
||||
|
||||
`getAllMcpConfigs()`(`config.ts`)按优先级合并多个来源的配置:
|
||||
|
||||
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
|
||||
2. 否则合并:user → project → local → plugin → claude.ai
|
||||
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url),插件配置被同名手动配置抑制
|
||||
4. `addScopeToServers()` 为每个配置项标注来源 scope
|
||||
|
||||
## 7 种传输层实现
|
||||
|
||||
`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
|
||||
|
||||
| 传输类型 | Transport 类 | 适用场景 | 认证方式 |
|
||||
|----------|-------------|---------|---------|
|
||||
| `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 |
|
||||
| `stdio`(默认) | `StdioClientTransport` | 外部本地子进程 | 无 |
|
||||
| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
|
||||
| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
|
||||
| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
|
||||
| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
|
||||
| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
|
||||
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
|
||||
| InProcess(内置) | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
|
||||
|
||||
### stdio 传输的进程管理
|
||||
|
||||
@@ -112,9 +308,17 @@ timer.unref?.() // 不阻止进程退出
|
||||
|
||||
```typescript
|
||||
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||
// 结果: "mcp__my-db__query"
|
||||
// 结果: "mcp__my-database__query"
|
||||
```
|
||||
|
||||
### 内置 MCP 的工具发现
|
||||
|
||||
内置 MCP 服务器虽然使用 InProcessTransport,但工具发现流程与外部服务器完全一致:
|
||||
|
||||
- **Computer Use**:`createComputerUseMcpServerForCli()` 在 `src/utils/computerUse/mcpServer.ts` 中构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表(1s 超时枚举)。
|
||||
- **Claude in Chrome**:`createClaudeForChromeMcpServer()` 在 `@ant/claude-for-chrome-mcp` 包中构建 Server,提供 17+ 个浏览器控制工具。
|
||||
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
|
||||
|
||||
### 工具描述截断
|
||||
|
||||
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
|
||||
@@ -134,6 +338,8 @@ MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI
|
||||
|
||||
MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
|
||||
|
||||
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
|
||||
|
||||
## MCP 工具的执行链路
|
||||
|
||||
```
|
||||
@@ -169,23 +375,33 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
|
||||
本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
|
||||
|
||||
## 实际配置示例
|
||||
## 内置 vs 外部 MCP 对比总结
|
||||
|
||||
```json
|
||||
// settings.json 中的 MCP 配置
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-database": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/db-mcp-server"],
|
||||
"env": { "DB_URL": "postgres://..." }
|
||||
},
|
||||
"remote-api": {
|
||||
"type": "http",
|
||||
"url": "https://api.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
| 维度 | 内置 MCP | 外部 MCP |
|
||||
|------|---------|---------|
|
||||
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
|
||||
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
|
||||
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
|
||||
| **进程模型** | 同进程,零开销 | 子进程(stdio)或网络连接 |
|
||||
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_`) |
|
||||
| **生命周期** | 随 CLI 启停 | 连接缓存 + 按需重连 |
|
||||
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
|
||||
| **Feature Flag** | `CHICAGO_MCP`(Computer Use)等 | 无(始终可用) |
|
||||
| **工具发现** | 与外部相同(MCP 协议) | 标准 MCP `tools/list` |
|
||||
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
|
||||
|
||||
配置后,AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。
|
||||
## 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/mcp/client.ts` | 核心客户端:connectToServer、fetchToolsForClient、MCPTool.call |
|
||||
| `src/services/mcp/config.ts` | 配置管理:getAllMcpConfigs、addMcpConfig、removeMcpConfig |
|
||||
| `src/services/mcp/types.ts` | 类型定义:配置 Schema、连接状态类型 |
|
||||
| `src/services/mcp/InProcessTransport.ts` | 内置 MCP 传输层:linked transport pair |
|
||||
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK MCP:双向通知、实验门控 |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | React Hook:连接生命周期、重连 |
|
||||
| `src/utils/computerUse/mcpServer.ts` | Computer Use MCP Server 构建 |
|
||||
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
|
||||
| `src/utils/claudeInChrome/mcpServer.ts` | Chrome MCP Server 构建 + Bridge 配置 |
|
||||
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具包装:统一 Tool 接口 |
|
||||
| `src/entrypoints/mcp.ts` | MCP server 入口(Claude Code 作为 MCP server) |
|
||||
|
||||
17
mint.json
17
mint.json
@@ -86,6 +86,7 @@
|
||||
"group": "可扩展性",
|
||||
"pages": [
|
||||
"docs/extensibility/mcp-protocol",
|
||||
"docs/extensibility/mcp-configuration",
|
||||
"docs/extensibility/hooks",
|
||||
"docs/extensibility/skills",
|
||||
"docs/extensibility/custom-agents"
|
||||
@@ -177,21 +178,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"excludes": [
|
||||
"docs/test-plans/**",
|
||||
"docs/testing-spec.md",
|
||||
"docs/REVISION-PLAN.md",
|
||||
"docs/feature-exploration-plan.md",
|
||||
"docs/ultraplan-implementation.md",
|
||||
"docs/features/feature-flags-audit-complete.md",
|
||||
"docs/features/feature-flags-codex-review.md",
|
||||
"docs/features/growthbook-enablement-plan.md",
|
||||
"docs/features/computer-use-architecture-v2.md",
|
||||
"docs/features/computer-use-mcp-test-report.md",
|
||||
"docs/features/computer-use-tools-reference.md",
|
||||
"docs/features/computer-use-windows-enhancement.md",
|
||||
"docs/features/lan-pipes-implementation.md"
|
||||
],
|
||||
"excludes": [],
|
||||
"footerSocials": {
|
||||
"github": "https://github.com/anthropics/claude-code"
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||
"@anthropic/ink": "workspace:*",
|
||||
"@claude-code-best/builtin-tools": "workspace:*",
|
||||
"@claude-code-best/agent-tools": "workspace:*",
|
||||
"@claude-code-best/mcp-client": "workspace:*",
|
||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||
"@aws-sdk/client-sts": "^3.1020.0",
|
||||
|
||||
176
packages/@ant/ink/docs/01-getting-started.md
Normal file
176
packages/@ant/ink/docs/01-getting-started.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Chapter 1: Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
`@anthropic/ink` is a workspace package. It is consumed internally and not published to npm.
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@anthropic/ink": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Peer Dependencies
|
||||
|
||||
- `react` ^19.2.4
|
||||
- `react-reconciler` ^0.33.0
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `chalk` | ANSI color generation |
|
||||
| `cli-boxes` | Border style definitions |
|
||||
| `get-east-asian-width` | CJK character width measurement |
|
||||
| `wrap-ansi` | ANSI-aware word wrapping |
|
||||
| `bidi-js` | Bidirectional text support |
|
||||
| `lodash-es` | Utility functions (throttle, noop) |
|
||||
| `signal-exit` | Process exit handler cleanup |
|
||||
| `emoji-regex` | Emoji width handling |
|
||||
|
||||
## Basic Rendering
|
||||
|
||||
### `render(node, options?)`
|
||||
|
||||
The primary entry point. Renders a React element tree to the terminal.
|
||||
|
||||
```tsx
|
||||
import { render } from '@anthropic/ink'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
|
||||
const { unmount, rerender, waitUntilExit } = await render(
|
||||
<Box>
|
||||
<Text>Hello, World!</Text>
|
||||
</Box>
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `node` -- `ReactNode` to render
|
||||
- `options` -- `RenderOptions | NodeJS.WriteStream` (optional)
|
||||
|
||||
**Returns:** `Promise<Instance>` with:
|
||||
- `rerender(node)` -- Replace the root node
|
||||
- `unmount()` -- Unmount and clean up
|
||||
- `waitUntilExit()` -- `Promise<void>` that resolves on unmount
|
||||
- `cleanup()` -- Remove from instance registry
|
||||
|
||||
### `renderSync(node, options?)`
|
||||
|
||||
Synchronous version of render. Same API, returns `Instance` directly (no Promise).
|
||||
|
||||
```tsx
|
||||
import { renderSync } from '@anthropic/ink'
|
||||
|
||||
const instance = renderSync(<App />)
|
||||
// instance.rerender, instance.unmount, etc.
|
||||
```
|
||||
|
||||
### `createRoot(options?)`
|
||||
|
||||
Creates a managed Ink root without immediately rendering. Similar to `react-dom`'s `createRoot`.
|
||||
|
||||
```tsx
|
||||
import { createRoot } from '@anthropic/ink'
|
||||
|
||||
const root = await createRoot({ exitOnCtrlC: false })
|
||||
|
||||
// Later, render into it
|
||||
root.render(<App />)
|
||||
|
||||
// You can re-render into the same root
|
||||
root.render(<DifferentApp />)
|
||||
|
||||
// Clean up
|
||||
root.unmount()
|
||||
```
|
||||
|
||||
**Returns:** `Promise<Root>` with:
|
||||
- `render(node)` -- Mount or update the tree
|
||||
- `unmount()` -- Unmount
|
||||
- `waitUntilExit()` -- `Promise<void>`
|
||||
|
||||
## RenderOptions
|
||||
|
||||
```ts
|
||||
type RenderOptions = {
|
||||
/** Output stream. Default: process.stdout */
|
||||
stdout?: NodeJS.WriteStream
|
||||
|
||||
/** Input stream. Default: process.stdin */
|
||||
stdin?: NodeJS.ReadStream
|
||||
|
||||
/** Error stream. Default: process.stderr */
|
||||
stderr?: NodeJS.WriteStream
|
||||
|
||||
/** Handle Ctrl+C to exit. Default: true */
|
||||
exitOnCtrlC?: boolean
|
||||
|
||||
/** Patch console methods to prevent Ink output mixing. Default: true */
|
||||
patchConsole?: boolean
|
||||
|
||||
/** Called after each frame render with timing info. */
|
||||
onFrame?: (event: FrameEvent) => void
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
### Component Tree
|
||||
|
||||
Ink renders React components to a terminal using a custom reconciler. The tree structure maps to terminal output:
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="green">Header</Text>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text>Left</Text>
|
||||
<Text>Right</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
This produces terminal output with Flexbox layout (via Yoga).
|
||||
|
||||
### Rendering Pipeline
|
||||
|
||||
1. **React Reconciler** -- Standard React reconciliation; diffs virtual tree
|
||||
2. **Yoga Layout** -- Computes Flexbox positions/ sizes for every node
|
||||
3. **Render to Output** -- Walks the DOM tree, emits styled text into an `Output` buffer
|
||||
4. **Screen Diff** -- Compares new frame against previous frame in a screen buffer
|
||||
5. **Terminal Write** -- Emits minimal ANSI escape sequences to update only changed cells
|
||||
|
||||
### Module System
|
||||
|
||||
Import everything from the package root:
|
||||
|
||||
```tsx
|
||||
// Core rendering
|
||||
import { render, createRoot, renderSync } from '@anthropic/ink'
|
||||
|
||||
// Components (base, no theme)
|
||||
import { BaseBox, BaseText, ScrollBox, Button, Link, Newline, Spacer } from '@anthropic/ink'
|
||||
|
||||
// Theme-aware components (recommended)
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
|
||||
// Hooks
|
||||
import { useApp, useInput, useTerminalSize, useInterval } from '@anthropic/ink'
|
||||
|
||||
// Theme
|
||||
import { ThemeProvider, useTheme, color } from '@anthropic/ink'
|
||||
|
||||
// Keybindings
|
||||
import { useKeybinding, KeybindingProvider } from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### Naming Convention: Base vs Theme-aware
|
||||
|
||||
The package exports both raw and theme-aware versions of core components:
|
||||
|
||||
- **`BaseBox`** / **`BaseText`** -- Raw components that only accept raw color values (`rgb(...)`, `#hex`, `ansi:...`, `ansi256(...)`)
|
||||
- **`Box`** / **`Text`** -- Theme-aware wrappers that accept both theme keys (`'claude'`, `'success'`, `'error'`) and raw color values
|
||||
|
||||
Always prefer the theme-aware versions unless you have a specific reason to use raw components.
|
||||
348
packages/@ant/ink/docs/02-layout.md
Normal file
348
packages/@ant/ink/docs/02-layout.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Chapter 2: Layout System
|
||||
|
||||
Ink uses [Yoga](https://yogalayout.com/) (Facebook's cross-platform layout engine) to implement CSS Flexbox in the terminal. Every layout is flexbox-based -- there is no CSS Grid or flow layout.
|
||||
|
||||
## Box Component
|
||||
|
||||
`Box` is the fundamental layout primitive. It is the terminal equivalent of `<div style="display: flex">`.
|
||||
|
||||
```tsx
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text>Left</Text>
|
||||
<Text>Right</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Box Props (Styles)
|
||||
|
||||
All layout props are passed directly as JSX props (no `style={}` wrapper needed):
|
||||
|
||||
#### Flex Direction
|
||||
|
||||
Controls the main axis direction.
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="row">...</Box> // Left to right (default)
|
||||
<Box flexDirection="column">...</Box> // Top to bottom
|
||||
<Box flexDirection="row-reverse">...</Box> // Right to left
|
||||
<Box flexDirection="column-reverse">...</Box> // Bottom to top
|
||||
```
|
||||
|
||||
#### Flex Grow / Shrink / Basis
|
||||
|
||||
```tsx
|
||||
<Box flexGrow={1}>...</Box> // Grow to fill available space
|
||||
<Box flexShrink={0}>...</Box> // Don't shrink below intrinsic size
|
||||
<Box flexBasis={20}>...</Box> // Initial size before flex distribution
|
||||
<Box flexBasis="50%">...</Box> // Percentage basis
|
||||
```
|
||||
|
||||
Default values: `flexGrow={0}`, `flexShrink={1}`, `flexBasis=auto`.
|
||||
|
||||
#### Flex Wrap
|
||||
|
||||
```tsx
|
||||
<Box flexWrap="nowrap">...</Box> // Single line (default)
|
||||
<Box flexWrap="wrap">...</Box> // Multiple lines
|
||||
<Box flexWrap="wrap-reverse">...</Box> // Reverse cross-axis stacking
|
||||
```
|
||||
|
||||
#### Alignment
|
||||
|
||||
```tsx
|
||||
<Box alignItems="flex-start">...</Box> // Cross-axis start
|
||||
<Box alignItems="center">...</Box> // Cross-axis center
|
||||
<Box alignItems="flex-end">...</Box> // Cross-axis end
|
||||
<Box alignItems="stretch">...</Box> // Stretch to fill (default)
|
||||
|
||||
<Box alignSelf="flex-start">...</Box> // Override parent's alignItems
|
||||
<Box alignSelf="center">...</Box>
|
||||
<Box alignSelf="flex-end">...</Box>
|
||||
<Box alignSelf="auto">...</Box> // Inherit from parent
|
||||
```
|
||||
|
||||
#### Justify Content
|
||||
|
||||
```tsx
|
||||
<Box justifyContent="flex-start">...</Box> // Main-axis start (default)
|
||||
<Box justifyContent="flex-end">...</Box> // Main-axis end
|
||||
<Box justifyContent="center">...</Box> // Center
|
||||
<Box justifyContent="space-between">...</Box> // Equal gaps, no edges
|
||||
<Box justifyContent="space-around">...</Box> // Equal gaps with edges
|
||||
<Box justifyContent="space-evenly">...</Box> // Evenly distributed
|
||||
```
|
||||
|
||||
#### Gap
|
||||
|
||||
Spacing between children (only accepts integers):
|
||||
|
||||
```tsx
|
||||
<Box gap={1}>...</Box> // Both row and column gap
|
||||
<Box columnGap={2}>...</Box> // Gap between columns only
|
||||
<Box rowGap={1}>...</Box> // Gap between rows only
|
||||
```
|
||||
|
||||
#### Padding
|
||||
|
||||
Inner spacing (only accepts integers):
|
||||
|
||||
```tsx
|
||||
<Box padding={1}>...</Box> // All sides
|
||||
<Box paddingX={2}>...</Box> // Left and right
|
||||
<Box paddingY={1}>...</Box> // Top and bottom
|
||||
<Box paddingLeft={2}>...</Box> // Left only
|
||||
<Box paddingRight={2}>...</Box> // Right only
|
||||
<Box paddingTop={1}>...</Box> // Top only
|
||||
<Box paddingBottom={1}>...</Box> // Bottom only
|
||||
```
|
||||
|
||||
#### Margin
|
||||
|
||||
Outer spacing (only accepts integers):
|
||||
|
||||
```tsx
|
||||
<Box margin={1}>...</Box> // All sides
|
||||
<Box marginX={2}>...</Box> // Left and right
|
||||
<Box marginY={1}>...</Box> // Top and bottom
|
||||
<Box marginLeft={2}>...</Box> // Left only
|
||||
<Box marginRight={2}>...</Box> // Right only
|
||||
<Box marginTop={1}>...</Box> // Top only
|
||||
<Box marginBottom={1}>...</Box> // Bottom only
|
||||
```
|
||||
|
||||
> **Note:** Fractional values for padding, margin, and gap are not supported. Ink will emit warnings if non-integer values are used.
|
||||
|
||||
#### Width & Height
|
||||
|
||||
```tsx
|
||||
<Box width={40}>...</Box> // Fixed 40 characters wide
|
||||
<Box height={10}>...</Box> // Fixed 10 rows tall
|
||||
<Box width="50%">...</Box> // 50% of parent's width
|
||||
<Box width="100%">...</Box> // Full parent width
|
||||
```
|
||||
|
||||
#### Min/Max Dimensions
|
||||
|
||||
```tsx
|
||||
<Box minWidth={20}>...</Box>
|
||||
<Box maxWidth={80}>...</Box>
|
||||
<Box minHeight={5}>...</Box>
|
||||
<Box maxHeight={20}>...</Box>
|
||||
```
|
||||
|
||||
Percentage values are supported: `minWidth="30%"`.
|
||||
|
||||
#### Position
|
||||
|
||||
```tsx
|
||||
<Box position="absolute" top={0} right={0}>...</Box>
|
||||
<Box position="absolute" top="10%" left="20%">...</Box>
|
||||
<Box position="relative">...</Box> // Default
|
||||
```
|
||||
|
||||
Position `absolute` removes the element from normal flow and positions it relative to its nearest positioned ancestor. Useful for overlays.
|
||||
|
||||
#### Display
|
||||
|
||||
```tsx
|
||||
<Box display="flex">...</Box> // Visible (default)
|
||||
<Box display="none">...</Box> // Hidden (removed from layout)
|
||||
```
|
||||
|
||||
#### Border
|
||||
|
||||
```tsx
|
||||
<Box borderStyle="single">...</Box> // Thin border
|
||||
<Box borderStyle="double">...</Box> // Double-line border
|
||||
<Box borderStyle="round">...</Box> // Rounded corners
|
||||
<Box borderStyle="bold">...</Box> // Bold border
|
||||
<Box borderStyle="singleDouble">...</Box> // Mixed
|
||||
<Box borderStyle="doubleSingle">...</Box> // Mixed
|
||||
<Box borderStyle="classic">...</Box> // ASCII art border
|
||||
```
|
||||
|
||||
Control individual sides and colors:
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderTop={false} // Hide top border
|
||||
borderBottom={true} // Show bottom border
|
||||
borderColor="rgb(255,0,0)" // Red border
|
||||
borderDimColor={true} // Dim the border
|
||||
>
|
||||
...
|
||||
</Box>
|
||||
```
|
||||
|
||||
Per-side colors:
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderTopColor="rgb(255,0,0)"
|
||||
borderBottomColor="ansi:green"
|
||||
borderLeftColor="#0000FF"
|
||||
borderRightColor="ansi256(200)"
|
||||
/>
|
||||
```
|
||||
|
||||
Border text (labels in the border):
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderText={{ title: "My Panel", align: "left" }}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Background
|
||||
|
||||
```tsx
|
||||
<Box backgroundColor="rgb(40,40,40)">...</Box>
|
||||
```
|
||||
|
||||
#### Overflow
|
||||
|
||||
```tsx
|
||||
<Box overflow="visible">...</Box> // Content expands container (default)
|
||||
<Box overflow="hidden">...</Box> // Clip without scrolling
|
||||
<Box overflow="scroll">...</Box> // Enable scrolling (use ScrollBox)
|
||||
```
|
||||
|
||||
`overflowX` and `overflowY` control each axis independently.
|
||||
|
||||
#### Opaque
|
||||
|
||||
```tsx
|
||||
<Box opaque={true}>...</Box>
|
||||
```
|
||||
|
||||
Fills the box interior with spaces (using terminal's default background) before rendering children. Useful for absolute-positioned overlays where gaps would otherwise be transparent.
|
||||
|
||||
#### NoSelect
|
||||
|
||||
```tsx
|
||||
<Box noSelect={true}>...</Box> // Exclude from text selection
|
||||
<Box noSelect="from-left-edge">...</Box> // Exclude from column 0 to box edge
|
||||
```
|
||||
|
||||
Only affects alt-screen text selection. Useful for gutters (line numbers, diff markers).
|
||||
|
||||
## Spacer
|
||||
|
||||
`Spacer` fills all available space along the main axis (equivalent to `flexGrow: 1`).
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="row">
|
||||
<Text>Left</Text>
|
||||
<Spacer />
|
||||
<Text>Right</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Newline
|
||||
|
||||
Inserts line breaks.
|
||||
|
||||
```tsx
|
||||
<Text>
|
||||
Line 1
|
||||
<Newline />
|
||||
Line 2
|
||||
<Newline count={2} />
|
||||
Line 4 (after double break)
|
||||
</Text>
|
||||
```
|
||||
|
||||
## Layout Examples
|
||||
|
||||
### Two-column layout
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="row" width={80}>
|
||||
<Box width="50%" padding={1}>
|
||||
<Text>Left column</Text>
|
||||
</Box>
|
||||
<Box width="50%" padding={1}>
|
||||
<Text>Right column</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Centered content
|
||||
|
||||
```tsx
|
||||
<Box justifyContent="center" alignItems="center" height={20}>
|
||||
<Text>Centered!</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Sticky footer
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="column" height={24}>
|
||||
<Box flexGrow={1}>
|
||||
<Text>Scrollable content area</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Status bar at bottom</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Bordered panel with title
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="rgb(87,105,247)"
|
||||
padding={1}
|
||||
width={60}
|
||||
>
|
||||
<Text bold>Panel Title</Text>
|
||||
<Text>Panel content goes here.</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## NoSelect
|
||||
|
||||
Wraps a region to exclude it from text selection in alt-screen mode. A convenience wrapper around `Box` with `noSelect` set.
|
||||
|
||||
```tsx
|
||||
import { NoSelect } from '@anthropic/ink'
|
||||
|
||||
<Box flexDirection="row">
|
||||
<NoSelect>
|
||||
<Text dimColor>1 │ </Text>
|
||||
</NoSelect>
|
||||
<Text>selectable code here</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactNode` | - | Content |
|
||||
| `fromLeftEdge` | `boolean` | `false` | Extend exclusion from column 0 to box's right edge |
|
||||
|
||||
Accepts all `BoxProps` except `noSelect`.
|
||||
|
||||
## BaseBox vs ThemedBox
|
||||
|
||||
Two versions of Box are exported:
|
||||
|
||||
- **`BaseBox`** (imported as `BaseBox`) -- Raw box, color props accept only raw `Color` values
|
||||
- **`Box`** (themed, imported as `Box`) -- Theme-aware, color props accept `keyof Theme | Color`
|
||||
|
||||
```tsx
|
||||
// Raw
|
||||
<BaseBox borderStyle="single" borderColor="rgb(255,0,0)" />
|
||||
|
||||
// Theme-aware (resolves 'permission' to the current theme's blue)
|
||||
<Box borderStyle="single" borderColor="permission" />
|
||||
```
|
||||
238
packages/@ant/ink/docs/03-text-and-styling.md
Normal file
238
packages/@ant/ink/docs/03-text-and-styling.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Chapter 3: Text & Styling
|
||||
|
||||
## Text Component
|
||||
|
||||
`Text` renders styled text content. It supports colors, emphasis, and text wrapping.
|
||||
|
||||
```tsx
|
||||
import { Text } from '@anthropic/ink'
|
||||
|
||||
<Text bold color="success">Operation complete</Text>
|
||||
```
|
||||
|
||||
### Text Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `color` | `keyof Theme \| Color` | - | Foreground color |
|
||||
| `backgroundColor` | `keyof Theme` | - | Background color (theme-aware) |
|
||||
| `bold` | `boolean` | `false` | Bold text |
|
||||
| `dimColor` | `boolean` | `false` | Dim text (uses theme's `inactive` color) |
|
||||
| `italic` | `boolean` | `false` | Italic text |
|
||||
| `underline` | `boolean` | `false` | Underlined text |
|
||||
| `strikethrough` | `boolean` | `false` | Strikethrough text |
|
||||
| `inverse` | `boolean` | `false` | Swap foreground/background |
|
||||
| `wrap` | `TextWrap` | `'wrap'` | Wrapping/truncation mode |
|
||||
| `children` | `ReactNode` | - | Text content |
|
||||
|
||||
> **Note:** `bold` and `dimColor` are mutually exclusive (ANSI terminals cannot render both simultaneously).
|
||||
|
||||
### BaseText vs ThemedText
|
||||
|
||||
- **`BaseText`** -- Accepts raw `Color` values only
|
||||
- **`Text`** (default export) -- Theme-aware, accepts `keyof Theme | Color` for `color`, and `keyof Theme` for `backgroundColor`
|
||||
|
||||
```tsx
|
||||
// Raw color
|
||||
<BaseText color="rgb(255,0,0)">Red text</BaseText>
|
||||
|
||||
// Theme key (resolved to current theme palette)
|
||||
<Text color="error">Error message</Text>
|
||||
|
||||
// Mixed
|
||||
<Text color="#FF0000">Custom red</Text>
|
||||
```
|
||||
|
||||
### Text Wrap Modes
|
||||
|
||||
```tsx
|
||||
<Text wrap="wrap">...</Text> // Word-wrap at container width (default)
|
||||
<Text wrap="wrap-trim">...</Text> // Wrap + trim trailing whitespace
|
||||
<Text wrap="end">...</Text> // Truncate with "..." at end
|
||||
<Text wrap="truncate-end">...</Text> // Same as "end"
|
||||
<Text wrap="truncate">...</Text> // Truncate (no ellipsis)
|
||||
<Text wrap="middle">...</Text> // "start...end"
|
||||
<Text wrap="truncate-middle">...</Text> // Same as "middle"
|
||||
<Text wrap="truncate-start">...</Text> // "...text"
|
||||
```
|
||||
|
||||
### TextHoverColorContext
|
||||
|
||||
Uncolored `Text` children inherit a hover color from context:
|
||||
|
||||
```tsx
|
||||
import { TextHoverColorContext } from '@anthropic/ink'
|
||||
|
||||
<TextHoverColorContext.Provider value="suggestion">
|
||||
<Text>Uncolored text gets the suggestion color</Text>
|
||||
<Text color="error">This stays red</Text>
|
||||
</TextHoverColorContext.Provider>
|
||||
```
|
||||
|
||||
Precedence: explicit `color` > `TextHoverColorContext` > `dimColor`.
|
||||
|
||||
## Color System
|
||||
|
||||
### Raw Color Formats
|
||||
|
||||
Four formats are supported for raw color values:
|
||||
|
||||
```tsx
|
||||
// RGB
|
||||
<Text color="rgb(255,107,128)">Bright red</Text>
|
||||
|
||||
// Hex
|
||||
<Text color="#FF6B80">Bright red</Text>
|
||||
|
||||
// ANSI 256-color
|
||||
<Text color="ansi256(196)">Red from 256-color palette</Text>
|
||||
|
||||
// Named ANSI 16-color
|
||||
<Text color="ansi:red">Red</Text>
|
||||
<Text color="ansi:greenBright">Bright green</Text>
|
||||
```
|
||||
|
||||
### ANSI Named Colors
|
||||
|
||||
Full list of `ansi:` prefixed names:
|
||||
|
||||
| Name | Color |
|
||||
|------|-------|
|
||||
| `ansi:black` | Black |
|
||||
| `ansi:red` | Red |
|
||||
| `ansi:green` | Green |
|
||||
| `ansi:yellow` | Yellow |
|
||||
| `ansi:blue` | Blue |
|
||||
| `ansi:magenta` | Magenta |
|
||||
| `ansi:cyan` | Cyan |
|
||||
| `ansi:white` | White |
|
||||
| `ansi:blackBright` | Dark gray |
|
||||
| `ansi:redBright` | Bright red |
|
||||
| `ansi:greenBright` | Bright green |
|
||||
| `ansi:yellowBright` | Bright yellow |
|
||||
| `ansi:blueBright` | Bright blue |
|
||||
| `ansi:magentaBright` | Bright magenta |
|
||||
| `ansi:cyanBright` | Bright cyan |
|
||||
| `ansi:whiteBright` | Bright white |
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### `color(colorValue, themeName, type?)`
|
||||
|
||||
Curried theme-aware color function. Resolves theme keys to raw color values.
|
||||
|
||||
```tsx
|
||||
import { color } from '@anthropic/ink'
|
||||
|
||||
const paint = color('error', 'dark') // Returns (text: string) => string
|
||||
console.log(paint('failed')) // 'failed' wrapped in ANSI red codes
|
||||
|
||||
const paintFg = color('rgb(255,0,0)', 'dark', 'foreground')
|
||||
const paintBg = color('success', 'dark', 'background')
|
||||
```
|
||||
|
||||
Parameters:
|
||||
- `c` -- `keyof Theme | Color | undefined` -- Theme key or raw color
|
||||
- `theme` -- `ThemeName` -- Current theme
|
||||
- `type` -- `'foreground' | 'background'` (default `'foreground'`)
|
||||
|
||||
### `stringWidth(text)`
|
||||
|
||||
Measures the visual width of a string in terminal columns, accounting for:
|
||||
- CJK characters (2 columns each)
|
||||
- Emoji (2 columns each)
|
||||
- ANSI escape sequences (0 columns)
|
||||
|
||||
```tsx
|
||||
import { stringWidth } from '@anthropic/ink'
|
||||
|
||||
stringWidth('hello') // 5
|
||||
stringWidth('你好') // 4
|
||||
stringWidth('\x1b[31mhi') // 2 (ANSI codes ignored)
|
||||
```
|
||||
|
||||
### `wrapText(text, width, textWrap)`
|
||||
|
||||
Wraps text to a given width with the specified wrapping mode.
|
||||
|
||||
```tsx
|
||||
import { wrapText } from '@anthropic/ink'
|
||||
|
||||
wrapText('Hello World', 5, 'wrap') // 'Hello\nWorld'
|
||||
wrapText('Hello World', 8, 'end') // 'Hello...'
|
||||
```
|
||||
|
||||
### `wrapAnsi(text, width)`
|
||||
|
||||
Wraps text containing ANSI escape codes while preserving styling.
|
||||
|
||||
```tsx
|
||||
import { wrapAnsi } from '@anthropic/ink'
|
||||
|
||||
wrapAnsi('\x1b[31mHello World\x1b[0m', 5)
|
||||
// Wraps at word boundaries, keeps color codes intact
|
||||
```
|
||||
|
||||
### `measureElement(node)`
|
||||
|
||||
Measures a rendered DOM element's dimensions.
|
||||
|
||||
```tsx
|
||||
import { measureElement } from '@anthropic/ink'
|
||||
|
||||
const { width, height } = measureElement(domElement)
|
||||
```
|
||||
|
||||
## Link Component
|
||||
|
||||
Renders an OSC 8 terminal hyperlink (clickable URL in supported terminals).
|
||||
|
||||
```tsx
|
||||
import { Link } from '@anthropic/ink'
|
||||
|
||||
<Link url="https://example.com">
|
||||
<Text underline color="suggestion">example.com</Text>
|
||||
</Link>
|
||||
```
|
||||
|
||||
Props:
|
||||
- `url` -- `string` (required) -- Target URL
|
||||
- `children` -- `ReactNode` -- Display content
|
||||
- `fallback` -- `ReactNode` -- Shown when hyperlinks are unsupported
|
||||
|
||||
## RawAnsi Component
|
||||
|
||||
Renders pre-formatted ANSI strings directly into the layout.
|
||||
|
||||
```tsx
|
||||
import { RawAnsi } from '@anthropic/ink'
|
||||
|
||||
<RawAnsi
|
||||
lines={['\x1b[31mRed line 1\x1b[0m', '\x1b[32mGreen line 2\x1b[0m']}
|
||||
width={40}
|
||||
/>
|
||||
```
|
||||
|
||||
Props:
|
||||
- `lines` -- `string[]` -- Pre-rendered ANSI lines (one terminal row each)
|
||||
- `width` -- `number` -- Column width the producer wrapped to
|
||||
|
||||
## Border Rendering
|
||||
|
||||
### `renderBorder(box, output, options?)`
|
||||
|
||||
Low-level border rendering function used internally by Box.
|
||||
|
||||
```tsx
|
||||
import { renderBorder } from '@anthropic/ink'
|
||||
import type { BorderTextOptions } from '@anthropic/ink'
|
||||
```
|
||||
|
||||
Border styles available (from `cli-boxes`):
|
||||
- `single` -- Thin lines `─│┌┐└┘`
|
||||
- `double` -- Double lines `═║╔╗╚╝`
|
||||
- `round` -- Rounded corners `─│╭╮╰╯`
|
||||
- `bold` -- Bold lines `━┃┏┓┗┛`
|
||||
- `singleDouble` -- Single horizontal, double vertical
|
||||
- `doubleSingle` -- Double horizontal, single vertical
|
||||
- `classic` -- ASCII `─|++++`
|
||||
213
packages/@ant/ink/docs/04-theme-system.md
Normal file
213
packages/@ant/ink/docs/04-theme-system.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Chapter 4: Theme System
|
||||
|
||||
The theme system provides consistent, accessible color palettes across the application. It supports dark mode, light mode, ANSI-only terminals, and colorblind-accessible variants.
|
||||
|
||||
## ThemeProvider
|
||||
|
||||
Wraps the application to provide theme context.
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider } from '@anthropic/ink'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider initialState="dark" onThemeSave={(setting) => saveConfig(setting)}>
|
||||
<MyComponent />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `children` | `ReactNode` | Child components |
|
||||
| `initialState` | `ThemeSetting` | Initial theme (default: loads from config) |
|
||||
| `onThemeSave` | `(setting: ThemeSetting) => void` | Called when theme is saved |
|
||||
|
||||
### Theme Configuration Injection
|
||||
|
||||
Before mounting, inject config persistence callbacks:
|
||||
|
||||
```tsx
|
||||
import { setThemeConfigCallbacks } from '@anthropic/ink'
|
||||
|
||||
setThemeConfigCallbacks({
|
||||
loadTheme: () => configStore.get('theme', 'dark'),
|
||||
saveTheme: (setting) => configStore.set('theme', setting),
|
||||
})
|
||||
```
|
||||
|
||||
## Theme Settings
|
||||
|
||||
```ts
|
||||
type ThemeSetting = 'auto' | 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
|
||||
type ThemeName = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
|
||||
```
|
||||
|
||||
| Theme | Description |
|
||||
|-------|-------------|
|
||||
| `dark` | Dark theme with RGB colors (default) |
|
||||
| `light` | Light theme with RGB colors |
|
||||
| `dark-daltonized` | Colorblind-accessible dark theme |
|
||||
| `light-daltonized` | Colorblind-accessible light theme |
|
||||
| `dark-ansi` | Dark theme using only 16 ANSI colors |
|
||||
| `light-ansi` | Light theme using only 16 ANSI colors |
|
||||
| `auto` | Follows terminal's dark/light mode (resolved at runtime) |
|
||||
|
||||
## Theme Hooks
|
||||
|
||||
### `useTheme()`
|
||||
|
||||
Returns the resolved theme name and setter.
|
||||
|
||||
```tsx
|
||||
const [currentTheme, setTheme] = useTheme()
|
||||
// currentTheme: ThemeName (never 'auto')
|
||||
// setTheme: (setting: ThemeSetting) => void
|
||||
```
|
||||
|
||||
### `useThemeSetting()`
|
||||
|
||||
Returns the raw setting (may be `'auto'`).
|
||||
|
||||
```tsx
|
||||
const setting = useThemeSetting() // 'auto' | 'dark' | ...
|
||||
```
|
||||
|
||||
### `usePreviewTheme()`
|
||||
|
||||
Returns preview controls for a theme picker UI.
|
||||
|
||||
```tsx
|
||||
const { setPreviewTheme, savePreview, cancelPreview } = usePreviewTheme()
|
||||
|
||||
// Show preview
|
||||
setPreviewTheme('light')
|
||||
|
||||
// User confirms
|
||||
savePreview()
|
||||
|
||||
// User cancels
|
||||
cancelPreview()
|
||||
```
|
||||
|
||||
## Theme Color Palette
|
||||
|
||||
Every theme defines these semantic color keys:
|
||||
|
||||
### Brand & Identity
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `claude` | Brand orange |
|
||||
| `claudeShimmer` | Lighter brand orange (animated) |
|
||||
| `permission` | Permission/blue |
|
||||
| `permissionShimmer` | Lighter permission blue |
|
||||
| `autoAccept` | Electric violet |
|
||||
| `planMode` | Teal/sage |
|
||||
| `ide` | Muted blue |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `text` | Primary text color |
|
||||
| `inverseText` | Text on inverse backgrounds |
|
||||
| `inactive` | Dimmed/disabled elements |
|
||||
| `inactiveShimmer` | Lighter inactive |
|
||||
| `subtle` | Very subtle text |
|
||||
| `suggestion` | Interactive/accent |
|
||||
| `background` | General background accent |
|
||||
| `success` | Positive/success |
|
||||
| `error` | Negative/error |
|
||||
| `warning` | Caution/warning |
|
||||
| `warningShimmer` | Lighter warning |
|
||||
| `merged` | Merged state |
|
||||
|
||||
### Diff Colors
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `diffAdded` | Added lines background |
|
||||
| `diffRemoved` | Removed lines background |
|
||||
| `diffAddedDimmed` | Dimmed added |
|
||||
| `diffRemovedDimmed` | Dimmed removed |
|
||||
| `diffAddedWord` | Word-level added |
|
||||
| `diffRemovedWord` | Word-level removed |
|
||||
|
||||
### UI Colors
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `promptBorder` | Input prompt border |
|
||||
| `promptBorderShimmer` | Lighter prompt border |
|
||||
| `bashBorder` | Shell block border |
|
||||
| `selectionBg` | Text selection highlight background |
|
||||
| `userMessageBackground` | User message background |
|
||||
| `userMessageBackgroundHover` | User message hover |
|
||||
| `messageActionsBackground` | Action buttons background |
|
||||
|
||||
### Agent Colors
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `red_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `blue_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `green_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `yellow_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `purple_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `orange_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `pink_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
| `cyan_FOR_SUBAGENTS_ONLY` | Agent color assignment |
|
||||
|
||||
## Using Theme Colors in Components
|
||||
|
||||
### ThemedText
|
||||
|
||||
```tsx
|
||||
<Text color="success">Operation complete</Text>
|
||||
<Text color="error" bold>Failed!</Text>
|
||||
<Text color="claude">Claude says...</Text>
|
||||
<Text dimColor>Secondary info</Text>
|
||||
<Text backgroundColor="userMessageBackground">Highlighted</Text>
|
||||
```
|
||||
|
||||
### ThemedBox
|
||||
|
||||
```tsx
|
||||
<Box borderStyle="single" borderColor="permission" backgroundColor="userMessageBackground">
|
||||
<Text>Themed content</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### color() Utility
|
||||
|
||||
```tsx
|
||||
import { color, useTheme } from '@anthropic/ink'
|
||||
|
||||
function MyComponent() {
|
||||
const [themeName] = useTheme()
|
||||
const paint = color('success', themeName)
|
||||
// paint('text') returns ANSI-colored string
|
||||
}
|
||||
```
|
||||
|
||||
## Daltonized Themes
|
||||
|
||||
The daltonized themes (`light-daltonized`, `dark-daltonized`) are designed for users with protanopia/deuteranopia:
|
||||
|
||||
- Green/red diffs replaced with blue/red
|
||||
- Status colors use blue instead of green
|
||||
- Warning colors adjusted for better distinction
|
||||
- All color pairs verified for sufficient contrast
|
||||
|
||||
## System Theme Detection
|
||||
|
||||
When `ThemeSetting` is `'auto'`:
|
||||
|
||||
1. Seeds from `$COLORFGBG` environment variable
|
||||
2. Queries terminal via OSC 11 for live background color
|
||||
3. Watches for changes (terminal theme switch) in real-time
|
||||
4. Resolves to `'dark'` or `'light'` based on detected brightness
|
||||
390
packages/@ant/ink/docs/05-design-system.md
Normal file
390
packages/@ant/ink/docs/05-design-system.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# Chapter 5: Design System Components
|
||||
|
||||
Pre-built theme-aware UI components for common terminal interface patterns.
|
||||
|
||||
## Dialog
|
||||
|
||||
Modal dialog with border, title, and keyboard navigation.
|
||||
|
||||
```tsx
|
||||
import { Dialog } from '@anthropic/ink'
|
||||
|
||||
<Dialog
|
||||
title="Confirm Action"
|
||||
subtitle="This cannot be undone"
|
||||
onCancel={() => setShowDialog(false)}
|
||||
color="warning"
|
||||
>
|
||||
<Text>Are you sure you want to proceed?</Text>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `ReactNode` | - | Dialog title (required) |
|
||||
| `subtitle` | `ReactNode` | - | Optional subtitle |
|
||||
| `children` | `ReactNode` | - | Dialog body content |
|
||||
| `onCancel` | `() => void` | - | Called on Esc/n (required) |
|
||||
| `color` | `keyof Theme` | `'permission'` | Title and border color |
|
||||
| `hideInputGuide` | `boolean` | `false` | Hide the keyboard hint footer |
|
||||
| `hideBorder` | `boolean` | `false` | Render without Pane border |
|
||||
| `inputGuide` | `(exitState) => ReactNode` | - | Custom input guide footer |
|
||||
| `isCancelActive` | `boolean` | `true` | Enable/disable cancel keybindings |
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **Enter** -- Confirm (consumer handles this)
|
||||
- **Esc / n** -- Cancel (calls `onCancel`)
|
||||
- **Ctrl+C / Ctrl+D** -- Double-press to exit
|
||||
|
||||
### Custom Input Guide
|
||||
|
||||
```tsx
|
||||
<Dialog
|
||||
title="Save file?"
|
||||
onCancel={handleCancel}
|
||||
inputGuide={(exitState) => (
|
||||
exitState.pending
|
||||
? <Text>Press {exitState.keyName} again to exit</Text>
|
||||
: <Text>Press Enter to save, Esc to cancel</Text>
|
||||
)}
|
||||
>
|
||||
...
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Pane
|
||||
|
||||
Bordered container with themed top border.
|
||||
|
||||
```tsx
|
||||
import { Pane } from '@anthropic/ink'
|
||||
|
||||
<Pane color="permission">
|
||||
<Text>Content inside a bordered pane</Text>
|
||||
</Pane>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactNode` | - | Content |
|
||||
| `color` | `keyof Theme` | `'permission'` | Top border color |
|
||||
|
||||
## ProgressBar
|
||||
|
||||
Visual progress indicator.
|
||||
|
||||
```tsx
|
||||
import { ProgressBar } from '@anthropic/ink'
|
||||
|
||||
<ProgressBar
|
||||
ratio={0.65}
|
||||
width={40}
|
||||
fillColor="rate_limit_fill"
|
||||
emptyColor="rate_limit_empty"
|
||||
/>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `ratio` | `number` | - | Progress 0..1 (required) |
|
||||
| `width` | `number` | - | Character width (required) |
|
||||
| `fillColor` | `keyof Theme` | - | Filled portion color |
|
||||
| `emptyColor` | `keyof Theme` | - | Empty portion color |
|
||||
|
||||
## Spinner
|
||||
|
||||
Animated loading spinner. No props.
|
||||
|
||||
```tsx
|
||||
import { Spinner } from '@anthropic/ink'
|
||||
|
||||
<Box gap={1}>
|
||||
<Spinner />
|
||||
<Text>Loading...</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## LoadingState
|
||||
|
||||
Loading message with spinner and optional subtitle.
|
||||
|
||||
```tsx
|
||||
import { LoadingState } from '@anthropic/ink'
|
||||
|
||||
<LoadingState
|
||||
message="Installing dependencies"
|
||||
subtitle="This may take a moment"
|
||||
bold={true}
|
||||
/>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `message` | `string` | - | Loading message (required) |
|
||||
| `bold` | `boolean` | `false` | Bold message |
|
||||
| `dimColor` | `boolean` | `false` | Dimmed message |
|
||||
| `subtitle` | `string` | - | Secondary text below |
|
||||
|
||||
## StatusIcon
|
||||
|
||||
Semantic status indicator with icon and color.
|
||||
|
||||
```tsx
|
||||
import { StatusIcon } from '@anthropic/ink'
|
||||
|
||||
<StatusIcon status="success" withSpace />
|
||||
<Text>Build complete</Text>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `status` | `'success' \| 'error' \| 'warning' \| 'info' \| 'pending' \| 'loading'` | - | Status type (required) |
|
||||
| `withSpace` | `boolean` | `false` | Add trailing space |
|
||||
|
||||
Status icons:
|
||||
- `success` -- Green checkmark
|
||||
- `error` -- Red cross
|
||||
- `warning` -- Yellow warning
|
||||
- `info` -- Blue info
|
||||
- `pending` -- Dimmed circle
|
||||
- `loading` -- Dimmed ellipsis
|
||||
|
||||
## FuzzyPicker
|
||||
|
||||
Full-featured fuzzy search selector with preview support.
|
||||
|
||||
```tsx
|
||||
import { FuzzyPicker } from '@anthropic/ink'
|
||||
|
||||
<FuzzyPicker
|
||||
title="Select a file"
|
||||
items={files}
|
||||
getKey={(f) => f.path}
|
||||
renderItem={(f, focused) => <Text>{f.name}</Text>}
|
||||
onQueryChange={(q) => setFilteredFiles(filterFiles(q))}
|
||||
onSelect={(f) => openFile(f)}
|
||||
onCancel={() => setShowPicker(false)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `title` | `string` | Picker title (required) |
|
||||
| `items` | `readonly T[]` | Items to display (required) |
|
||||
| `getKey` | `(item: T) => string` | Unique key extractor (required) |
|
||||
| `renderItem` | `(item: T, isFocused: boolean) => ReactNode` | Item renderer (required) |
|
||||
| `onQueryChange` | `(query: string) => void` | Filter callback (required) |
|
||||
| `onSelect` | `(item: T) => void` | Enter key handler (required) |
|
||||
| `onCancel` | `() => void` | Esc handler (required) |
|
||||
| `renderPreview` | `(item: T) => ReactNode` | Preview panel renderer |
|
||||
| `previewPosition` | `'bottom' \| 'right'` | Preview placement |
|
||||
| `visibleCount` | `number` | Max visible items |
|
||||
| `direction` | `'down' \| 'up'` | Item ordering |
|
||||
| `onTab` | `PickerAction<T>` | Tab key handler |
|
||||
| `onShiftTab` | `PickerAction<T>` | Shift+Tab handler |
|
||||
| `onFocus` | `(item: T \| undefined) => void` | Focus change callback |
|
||||
| `emptyMessage` | `string \| ((query: string) => string)` | Empty state message |
|
||||
| `matchLabel` | `string` | Status line below list |
|
||||
| `placeholder` | `string` | Input placeholder |
|
||||
| `initialQuery` | `string` | Initial search query |
|
||||
| `selectAction` | `string` | Action label for byline |
|
||||
| `extraHints` | `ReactNode` | Additional keyboard hints |
|
||||
|
||||
## Tabs / Tab
|
||||
|
||||
Tabbed interface with keyboard navigation.
|
||||
|
||||
```tsx
|
||||
import { Tabs, Tab } from '@anthropic/ink'
|
||||
|
||||
<Tabs title="Settings" color="claude">
|
||||
<Tab title="General" id="general">
|
||||
<GeneralSettings />
|
||||
</Tab>
|
||||
<Tab title="Advanced" id="advanced">
|
||||
<AdvancedSettings />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
### Tabs Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactElement<TabProps>[]` | - | Tab elements |
|
||||
| `title` | `string` | - | Header title |
|
||||
| `color` | `keyof Theme` | - | Active tab indicator color |
|
||||
| `defaultTab` | `string` | - | Initial tab id |
|
||||
| `selectedTab` | `string` | - | Controlled selected tab |
|
||||
| `onTabChange` | `(tabId: string) => void` | - | Tab change callback |
|
||||
| `hidden` | `boolean` | `false` | Hide tab headers |
|
||||
| `useFullWidth` | `boolean` | `false` | Use full terminal width |
|
||||
| `banner` | `ReactNode` | - | Banner below tab headers |
|
||||
| `disableNavigation` | `boolean` | `false` | Disable keyboard nav |
|
||||
| `initialHeaderFocused` | `boolean` | `true` | Start with header focused |
|
||||
| `contentHeight` | `number` | - | Fixed content height |
|
||||
| `navFromContent` | `boolean` | `false` | Allow Tab/Arrow from content |
|
||||
|
||||
### Tab Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `title` | `string` | Tab label (required) |
|
||||
| `id` | `string` | Tab identifier |
|
||||
| `children` | `ReactNode` | Tab content |
|
||||
|
||||
### Tab Hooks
|
||||
|
||||
```tsx
|
||||
import { useTabsWidth, useTabHeaderFocus } from '@anthropic/ink'
|
||||
|
||||
const width = useTabsWidth() // Available content width
|
||||
const focused = useTabHeaderFocus() // Whether tab header is focused
|
||||
```
|
||||
|
||||
## ListItem
|
||||
|
||||
Selectable list item with focus/selection indicators.
|
||||
|
||||
```tsx
|
||||
import { ListItem } from '@anthropic/ink'
|
||||
|
||||
<ListItem isFocused={index === focusedIndex} isSelected={item.checked}>
|
||||
<Text>{item.label}</Text>
|
||||
</ListItem>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `isFocused` | `boolean` | - | Keyboard focus (required) |
|
||||
| `isSelected` | `boolean` | `false` | Checked/active state |
|
||||
| `children` | `ReactNode` | - | Content |
|
||||
| `description` | `string` | - | Secondary text below |
|
||||
| `styled` | `boolean` | `true` | Auto-style based on state |
|
||||
| `disabled` | `boolean` | `false` | Dimmed, non-interactive |
|
||||
| `showScrollDown` | `boolean` | `false` | Scroll-down hint arrow |
|
||||
| `showScrollUp` | `boolean` | `false` | Scroll-up hint arrow |
|
||||
| `declareCursor` | `boolean` | `true` | Declare terminal cursor |
|
||||
|
||||
## SearchBox
|
||||
|
||||
Search input with theme-aware styling.
|
||||
|
||||
```tsx
|
||||
import { SearchBox } from '@anthropic/ink'
|
||||
|
||||
<SearchBox
|
||||
query={searchQuery}
|
||||
placeholder="Search..."
|
||||
isFocused={true}
|
||||
isTerminalFocused={true}
|
||||
width="100%"
|
||||
/>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `query` | `string` | - | Current search text |
|
||||
| `placeholder` | `string` | - | Placeholder text |
|
||||
| `isFocused` | `boolean` | - | Focus state |
|
||||
| `isTerminalFocused` | `boolean` | - | Terminal focus state |
|
||||
| `prefix` | `string` | - | Input prefix label |
|
||||
| `width` | `number \| string` | - | Input width |
|
||||
| `cursorOffset` | `number` | - | Cursor position offset |
|
||||
| `borderless` | `boolean` | `false` | Remove border |
|
||||
|
||||
## Divider
|
||||
|
||||
Horizontal/vertical divider line.
|
||||
|
||||
```tsx
|
||||
import { Divider } from '@anthropic/ink'
|
||||
|
||||
<Divider width={60} color="subtle" />
|
||||
<Divider title="Section Title" />
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `width` | `number` | Terminal width | Divider width |
|
||||
| `color` | `keyof Theme` | Dimmed | Line color |
|
||||
| `char` | `string` | `'─'` | Line character |
|
||||
| `padding` | `number` | `0` | Width reduction |
|
||||
| `title` | `string` | - | Centered title text |
|
||||
|
||||
## Byline
|
||||
|
||||
Footer with middot-separated items.
|
||||
|
||||
```tsx
|
||||
import { Byline } from '@anthropic/ink'
|
||||
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
|
||||
</Byline>
|
||||
```
|
||||
|
||||
## KeyboardShortcutHint
|
||||
|
||||
Display a keyboard shortcut with its action.
|
||||
|
||||
```tsx
|
||||
import { KeyboardShortcutHint } from '@anthropic/ink'
|
||||
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<KeyboardShortcutHint shortcut="↑/↓" action="navigate" parens />
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `shortcut` | `string` | - | Key or chord to display |
|
||||
| `action` | `string` | - | Action description |
|
||||
| `parens` | `boolean` | `false` | Wrap in parentheses |
|
||||
| `bold` | `boolean` | `false` | Bold shortcut text |
|
||||
|
||||
## ConfigurableShortcutHint
|
||||
|
||||
Displays a shortcut hint that reads the actual keybinding from config.
|
||||
|
||||
```tsx
|
||||
import { ConfigurableShortcutHint } from '@anthropic/ink'
|
||||
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
```
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `action` | `string` | Keybinding action name |
|
||||
| `context` | `string` | Keybinding context |
|
||||
| `fallback` | `string` | Default shortcut if unbound |
|
||||
| `description` | `string` | Action description |
|
||||
| `parens` | `boolean` | Wrap in parentheses |
|
||||
| `bold` | `boolean` | Bold shortcut text |
|
||||
|
||||
## Ratchet
|
||||
|
||||
Animated counter component that prevents layout jumps.
|
||||
|
||||
```tsx
|
||||
import { Ratchet } from '@anthropic/ink'
|
||||
|
||||
<Ratchet lock="always">
|
||||
<Text>{count}</Text>
|
||||
</Ratchet>
|
||||
```
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactNode` | - | Content |
|
||||
| `lock` | `'always' \| 'offscreen'` | `'always'` | Width locking strategy. `'always'` locks always; `'offscreen'` only locks when the element is scrolled off-screen |
|
||||
189
packages/@ant/ink/docs/06-scrolling.md
Normal file
189
packages/@ant/ink/docs/06-scrolling.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Chapter 6: Scrolling
|
||||
|
||||
## ScrollBox
|
||||
|
||||
A scrollable container with imperative scroll API, viewport culling, and sticky scroll support.
|
||||
|
||||
```tsx
|
||||
import { ScrollBox } from '@anthropic/ink'
|
||||
import type { ScrollBoxHandle } from '@anthropic/ink'
|
||||
|
||||
function MessageList({ messages }) {
|
||||
const scrollRef = useRef<ScrollBoxHandle>(null)
|
||||
|
||||
// Auto-scroll to bottom on new messages
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollToBottom()
|
||||
}, [messages.length])
|
||||
|
||||
return (
|
||||
<ScrollBox ref={scrollRef} stickyScroll flexDirection="column" height={20}>
|
||||
{messages.map(msg => (
|
||||
<Text key={msg.id}>{msg.text}</Text>
|
||||
))}
|
||||
</ScrollBox>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
ScrollBox accepts all Box layout props except `textWrap`, `overflow`, `overflowX`, `overflowY` (these are managed internally):
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `ref` | `Ref<ScrollBoxHandle>` | - | Imperative handle |
|
||||
| `stickyScroll` | `boolean` | `false` | Auto-follow new content |
|
||||
| *(layout props)* | `Styles` | - | Width, height, padding, etc. |
|
||||
|
||||
### ScrollBoxHandle (Imperative API)
|
||||
|
||||
```ts
|
||||
interface ScrollBoxHandle {
|
||||
// Absolute positioning
|
||||
scrollTo(y: number): void
|
||||
scrollToElement(el: DOMElement, offset?: number): void
|
||||
scrollToBottom(): void
|
||||
|
||||
// Relative positioning
|
||||
scrollBy(dy: number): void
|
||||
|
||||
// Query state
|
||||
getScrollTop(): number
|
||||
getPendingDelta(): number
|
||||
getScrollHeight(): number
|
||||
getFreshScrollHeight(): number
|
||||
getViewportHeight(): number
|
||||
getViewportTop(): number
|
||||
isSticky(): boolean
|
||||
|
||||
// Events
|
||||
subscribe(listener: () => void): () => void
|
||||
|
||||
// Virtual scroll support
|
||||
setClampBounds(min?: number, max?: number): void
|
||||
}
|
||||
```
|
||||
|
||||
### Method Details
|
||||
|
||||
#### `scrollTo(y)`
|
||||
|
||||
Jump to an absolute position. Breaks sticky scroll.
|
||||
|
||||
```tsx
|
||||
scrollRef.current?.scrollTo(0) // Scroll to top
|
||||
```
|
||||
|
||||
#### `scrollBy(dy)`
|
||||
|
||||
Scroll by a relative amount. Accumulates deltas for smooth scrolling.
|
||||
|
||||
```tsx
|
||||
scrollRef.current?.scrollBy(3) // Scroll down 3 rows
|
||||
scrollRef.current?.scrollBy(-5) // Scroll up 5 rows
|
||||
```
|
||||
|
||||
#### `scrollToElement(el, offset?)`
|
||||
|
||||
Scroll so a specific DOM element is at the viewport top. More reliable than `scrollTo` because it reads the element's position at render time (avoids stale layout values).
|
||||
|
||||
```tsx
|
||||
const elementRef = useRef<DOMElement>(null)
|
||||
scrollRef.current?.scrollToElement(elementRef.current!, 2)
|
||||
```
|
||||
|
||||
#### `scrollToBottom()`
|
||||
|
||||
Pin scroll to bottom. Enables sticky mode.
|
||||
|
||||
```tsx
|
||||
scrollRef.current?.scrollToBottom()
|
||||
```
|
||||
|
||||
#### `isSticky()`
|
||||
|
||||
Returns `true` when scroll is pinned to the bottom.
|
||||
|
||||
```tsx
|
||||
if (scrollRef.current?.isSticky()) {
|
||||
// User hasn't scrolled up
|
||||
}
|
||||
```
|
||||
|
||||
#### `subscribe(listener)`
|
||||
|
||||
Subscribe to imperative scroll changes. Returns unsubscribe function.
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
return scrollRef.current?.subscribe(() => {
|
||||
console.log('Scroll position changed')
|
||||
})
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Sticky Scroll
|
||||
|
||||
When `stickyScroll` is enabled:
|
||||
|
||||
1. Scroll automatically follows new content at the bottom
|
||||
2. User scroll (via `scrollBy`/`scrollTo`) breaks stickiness
|
||||
3. `scrollToBottom()` re-enables stickiness
|
||||
4. Content growth at the bottom is detected and followed automatically
|
||||
|
||||
```tsx
|
||||
<ScrollBox stickyScroll height={20}>
|
||||
{/* New items auto-scroll to bottom */}
|
||||
{items.map(renderItem)}
|
||||
</ScrollBox>
|
||||
```
|
||||
|
||||
### Viewport Culling
|
||||
|
||||
ScrollBox only renders children that intersect the visible viewport. Children outside the viewport are still mounted in React but skipped during terminal rendering. This makes large lists performant.
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
For very large lists, use `setClampBounds` in combination with a virtual scrolling hook:
|
||||
|
||||
```tsx
|
||||
const scrollRef = useRef<ScrollBoxHandle>(null)
|
||||
|
||||
// After computing visible range
|
||||
scrollRef.current?.setClampBounds(firstVisibleRow, lastVisibleRow)
|
||||
```
|
||||
|
||||
This prevents burst `scrollTo` calls from showing blank space beyond mounted content.
|
||||
|
||||
### Scroll Events
|
||||
|
||||
ScrollBox bypasses React state for scroll operations. Instead:
|
||||
1. `scrollTo`/`scrollBy` mutate `scrollTop` directly on the DOM node
|
||||
2. The node is marked dirty
|
||||
3. A microtask-deferred render fires to coalesce multiple scroll events
|
||||
4. The Ink renderer reads `scrollTop` during layout
|
||||
|
||||
This avoids React reconciler overhead per wheel event.
|
||||
|
||||
### Integration with Mouse Wheel
|
||||
|
||||
In alt-screen mode, mouse wheel events are captured by the `App` component and forwarded to the focused ScrollBox:
|
||||
|
||||
```
|
||||
Wheel event → App.handleMouseEvent → ScrollBox.scrollBy(delta)
|
||||
```
|
||||
|
||||
### Layout Structure
|
||||
|
||||
ScrollBox creates a two-level DOM structure:
|
||||
|
||||
```
|
||||
ink-box (overflow: scroll, constrained height)
|
||||
└── Box (flexGrow: 1, flexShrink: 0, width: 100%)
|
||||
├── Child 1
|
||||
├── Child 2
|
||||
└── ...
|
||||
```
|
||||
|
||||
The outer `ink-box` is the viewport with constrained size. The inner `Box` grows to fit all content. The renderer computes `scrollHeight` from the inner box and translates content by `-scrollTop`.
|
||||
267
packages/@ant/ink/docs/07-user-input.md
Normal file
267
packages/@ant/ink/docs/07-user-input.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Chapter 7: User Input
|
||||
|
||||
## useInput
|
||||
|
||||
The primary hook for handling keyboard input.
|
||||
|
||||
```tsx
|
||||
import { useInput } from '@anthropic/ink'
|
||||
|
||||
function MyComponent() {
|
||||
useInput((input, key, event) => {
|
||||
if (input === 'q') {
|
||||
// 'q' key pressed
|
||||
}
|
||||
if (key.leftArrow) {
|
||||
// Left arrow
|
||||
}
|
||||
if (key.ctrl && input === 'c') {
|
||||
// Ctrl+C (only if exitOnCtrlC is false)
|
||||
}
|
||||
if (key.meta && input === 'b') {
|
||||
// Alt+B (Option+B on Mac)
|
||||
}
|
||||
if (key.shift && input === 'Tab') {
|
||||
// Shift+Tab
|
||||
}
|
||||
})
|
||||
|
||||
return <Text>Press keys...</Text>
|
||||
}
|
||||
```
|
||||
|
||||
### Signature
|
||||
|
||||
```ts
|
||||
function useInput(
|
||||
handler: (input: string, key: Key, event: InputEvent) => void,
|
||||
options?: { isActive?: boolean }
|
||||
): void
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`input`** (`string`) -- The character entered. Empty string for non-printable keys (arrows, function keys). For paste events, the entire pasted text.
|
||||
- **`key`** (`Key`) -- Parsed key metadata (see below)
|
||||
- **`event`** (`InputEvent`) -- Raw event with `stopImmediatePropagation()`
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `isActive` | `boolean` | `true` | Enable/disable input handling |
|
||||
|
||||
### Key Object
|
||||
|
||||
```ts
|
||||
type Key = {
|
||||
upArrow: boolean
|
||||
downArrow: boolean
|
||||
leftArrow: boolean
|
||||
rightArrow: boolean
|
||||
pageDown: boolean
|
||||
pageUp: boolean
|
||||
wheelUp: boolean // Mouse wheel in alt-screen
|
||||
wheelDown: boolean // Mouse wheel in alt-screen
|
||||
home: boolean
|
||||
end: boolean
|
||||
return: boolean
|
||||
escape: boolean
|
||||
ctrl: boolean
|
||||
shift: boolean
|
||||
fn: boolean
|
||||
tab: boolean
|
||||
backspace: boolean
|
||||
delete: boolean
|
||||
meta: boolean // Alt / Option
|
||||
super: boolean // Cmd (macOS) / Win key
|
||||
}
|
||||
```
|
||||
|
||||
### Event Propagation
|
||||
|
||||
Multiple `useInput` handlers form a chain. Call `event.stopImmediatePropagation()` to prevent downstream handlers from receiving the event:
|
||||
|
||||
```tsx
|
||||
useInput((input, key, event) => {
|
||||
if (input === 'j') {
|
||||
// Consumed by this handler
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
// Other handlers won't see 'j'
|
||||
})
|
||||
|
||||
useInput((input, key) => {
|
||||
// This won't fire for 'j'
|
||||
})
|
||||
```
|
||||
|
||||
### Raw Mode
|
||||
|
||||
`useInput` automatically enables raw mode on stdin when active. Raw mode is reference-counted -- it stays enabled as long as any hook has `isActive: true`.
|
||||
|
||||
In raw mode:
|
||||
- Keystrokes don't echo
|
||||
- Ctrl+C is not sent as signal (app must handle it)
|
||||
- Line buffering is disabled
|
||||
|
||||
## InputEvent
|
||||
|
||||
```ts
|
||||
class InputEvent extends Event {
|
||||
readonly input: string
|
||||
readonly key: Key
|
||||
readonly keypress: ParsedKey // Raw parsed keypress data
|
||||
}
|
||||
```
|
||||
|
||||
## KeyboardEvent
|
||||
|
||||
DOM-like keyboard event dispatched to focused elements:
|
||||
|
||||
```ts
|
||||
class KeyboardEvent extends Event {
|
||||
readonly key: Key
|
||||
}
|
||||
```
|
||||
|
||||
Used with `Box`'s `onKeyDown` and `onKeyDownCapture` props:
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={(event) => {
|
||||
if (event.key.return) {
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text>Press Enter to submit</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Key Parsing
|
||||
|
||||
Ink supports multiple keyboard protocols:
|
||||
|
||||
### Standard Escape Sequences
|
||||
- Arrow keys, function keys, Home/End, Page Up/Down
|
||||
- Ctrl+letter combinations
|
||||
- Shift, Alt, Meta modifiers
|
||||
|
||||
### Kitty Keyboard Protocol (CSI u)
|
||||
Extended key reporting with full modifier support:
|
||||
- Distinguishes Ctrl+Shift+A from Ctrl+A
|
||||
- Reports Super (Cmd/Win) key
|
||||
- Sends key release events
|
||||
|
||||
### xterm modifyOtherKeys
|
||||
Alternative extended key reporting for xterm-compatible terminals.
|
||||
|
||||
### Application Keypad Mode
|
||||
Numpad keys mapped to their digit characters.
|
||||
|
||||
## Paste Detection
|
||||
|
||||
When `Bracketed Paste` mode is enabled (DECSET 2004), pasted text is delivered as a single `InputEvent` with the full text in `input`. This distinguishes paste from rapid typing:
|
||||
|
||||
```tsx
|
||||
useInput((input, key, event) => {
|
||||
if (event.keypress.paste) {
|
||||
// User pasted text -- handle as a batch
|
||||
handlePaste(input)
|
||||
} else {
|
||||
// Regular keypress
|
||||
handleKey(input, key)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Mouse Events (Alt-Screen Only)
|
||||
|
||||
In alternate screen mode, mouse events are parsed and dispatched:
|
||||
|
||||
### Click Events
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
onClick={(event) => {
|
||||
console.log(`Clicked at (${event.x}, ${event.y})`)
|
||||
event.stopImmediatePropagation()
|
||||
}}
|
||||
>
|
||||
<Text>Click me</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Hover Events
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
Hover events use `mouseenter`/`mouseleave` semantics (no bubbling between children).
|
||||
|
||||
### Wheel Events
|
||||
|
||||
Mouse wheel events arrive as `Key.wheelUp`/`Key.wheelDown`:
|
||||
|
||||
```tsx
|
||||
useInput((input, key) => {
|
||||
if (key.wheelUp) scrollUp()
|
||||
if (key.wheelDown) scrollDown()
|
||||
})
|
||||
```
|
||||
|
||||
## useStdin
|
||||
|
||||
Lower-level access to the stdin stream.
|
||||
|
||||
```tsx
|
||||
import { useStdin } from '@anthropic/ink'
|
||||
|
||||
const {
|
||||
stdin, // Raw stdin stream
|
||||
setRawMode, // (enabled: boolean) => void
|
||||
isRawModeSupported, // boolean
|
||||
internal_exitOnCtrlC, // boolean
|
||||
internal_eventEmitter, // EventEmitter | undefined
|
||||
internal_querier, // Terminal querier
|
||||
} = useStdin()
|
||||
```
|
||||
|
||||
> **Prefer `useInput` for keyboard handling.** `useStdin` is for advanced use cases like terminal querying or custom event handling.
|
||||
|
||||
## Button Component
|
||||
|
||||
Interactive button that responds to keyboard and mouse:
|
||||
|
||||
```tsx
|
||||
import { Button } from '@anthropic/ink'
|
||||
|
||||
<Button onAction={() => handleClick()} tabIndex={0} autoFocus>
|
||||
{(state) => (
|
||||
<Text bold={state.focused} color={state.focused ? 'claude' : 'text'}>
|
||||
{state.focused ? '> Click Me' : ' Click Me'}
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
```
|
||||
|
||||
Button receives a render prop with state:
|
||||
|
||||
```ts
|
||||
type ButtonState = {
|
||||
focused: boolean // Has keyboard focus
|
||||
hovered: boolean // Mouse is over it (alt-screen)
|
||||
active: boolean // True for 100ms after activation (flash effect)
|
||||
}
|
||||
```
|
||||
|
||||
Activation triggers: Enter key, Space key, or mouse click.
|
||||
302
packages/@ant/ink/docs/08-keybindings.md
Normal file
302
packages/@ant/ink/docs/08-keybindings.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Chapter 8: Keybinding System
|
||||
|
||||
The keybinding system provides configurable, context-aware keyboard shortcuts with chord sequence support.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
KeybindingSetup (loads config)
|
||||
└── KeybindingProvider (provides context)
|
||||
├── useKeybinding(action, handler)
|
||||
├── useKeybindings({ action: handler })
|
||||
├── useKeybindingContext()
|
||||
└── useRegisterKeybindingContext(name, isActive)
|
||||
```
|
||||
|
||||
## KeybindingSetup
|
||||
|
||||
Loads and validates keybinding configuration at app startup.
|
||||
|
||||
```tsx
|
||||
import { KeybindingSetup } from '@anthropic/ink'
|
||||
|
||||
<KeybindingSetup
|
||||
loadBindings={() => parseUserKeybindings(configFile)}
|
||||
subscribeToChanges={(cb) => watchConfigFile(cb)}
|
||||
onWarnings={(warnings, isReload) => {
|
||||
warnings.forEach(w => console.warn(w.message))
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</KeybindingSetup>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `children` | `ReactNode` | App tree |
|
||||
| `loadBindings` | `() => KeybindingsLoadResult` | Load bindings from config |
|
||||
| `subscribeToChanges` | `(cb) => unsubscribe` | Watch for config changes |
|
||||
| `initWatcher` | `() => void \| Promise<void>` | One-time setup (optional) |
|
||||
| `onWarnings` | `(warnings, isReload) => void` | Validation warnings (optional) |
|
||||
| `onDebugLog` | `(message) => void` | Debug logging (optional) |
|
||||
|
||||
### KeybindingsLoadResult
|
||||
|
||||
```ts
|
||||
type KeybindingsLoadResult = {
|
||||
bindings: ParsedBinding[]
|
||||
warnings: KeybindingWarning[]
|
||||
}
|
||||
```
|
||||
|
||||
### KeybindingWarning
|
||||
|
||||
```ts
|
||||
type KeybindingWarning = {
|
||||
type: 'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'
|
||||
severity: 'error' | 'warning'
|
||||
message: string
|
||||
key?: string
|
||||
context?: string
|
||||
action?: string
|
||||
suggestion?: string
|
||||
}
|
||||
```
|
||||
|
||||
## KeybindingProvider
|
||||
|
||||
Context provider that holds binding state and resolution logic. Automatically provided by `KeybindingSetup`.
|
||||
|
||||
## useKeybinding
|
||||
|
||||
Register a handler for a keybinding action.
|
||||
|
||||
```tsx
|
||||
import { useKeybinding } from '@anthropic/ink'
|
||||
|
||||
function MyComponent() {
|
||||
useKeybinding('app:toggleTodos', () => {
|
||||
setShowTodos(prev => !prev)
|
||||
}, { context: 'Global' })
|
||||
|
||||
// Return false to NOT consume the event (allow propagation)
|
||||
useKeybinding('scroll:lineDown', () => {
|
||||
if (!hasContent) return false // Don't consume
|
||||
scrollBy(1)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Signature
|
||||
|
||||
```ts
|
||||
function useKeybinding(
|
||||
action: string,
|
||||
handler: () => void | false | Promise<void>,
|
||||
options?: { context?: string; isActive?: boolean }
|
||||
): void
|
||||
```
|
||||
|
||||
### Handler Return Values
|
||||
|
||||
| Return | Effect |
|
||||
|--------|--------|
|
||||
| `undefined` / `void` | Event consumed, stop propagation |
|
||||
| `false` | Event NOT consumed, propagate to other handlers |
|
||||
| `Promise<void>` | Async handler, treated as consumed |
|
||||
|
||||
## useKeybindings
|
||||
|
||||
Register multiple handlers in one hook (reduces `useInput` overhead).
|
||||
|
||||
```tsx
|
||||
import { useKeybindings } from '@anthropic/ink'
|
||||
|
||||
useKeybindings({
|
||||
'chat:submit': () => handleSubmit(),
|
||||
'chat:cancel': () => handleCancel(),
|
||||
'scroll:pageDown': () => {
|
||||
scrollBy(viewportHeight)
|
||||
},
|
||||
'scroll:lineDown': () => {
|
||||
if (!hasContent) return false
|
||||
scrollBy(1)
|
||||
},
|
||||
}, { context: 'Chat' })
|
||||
```
|
||||
|
||||
## Keybinding Contexts
|
||||
|
||||
Contexts allow the same key to perform different actions depending on what's active.
|
||||
|
||||
```tsx
|
||||
// Register a context as active
|
||||
import { useRegisterKeybindingContext } from '@anthropic/ink'
|
||||
|
||||
function ThemePicker({ isOpen }) {
|
||||
useRegisterKeybindingContext('ThemePicker', isOpen)
|
||||
|
||||
// While open, 'ThemePicker' context bindings take precedence
|
||||
useKeybinding('picker:select', handleSelect, { context: 'ThemePicker' })
|
||||
|
||||
return isOpen ? <PickerUI /> : null
|
||||
}
|
||||
```
|
||||
|
||||
Context resolution order:
|
||||
1. Registered active contexts (most recent first)
|
||||
2. The hook's own `context` parameter
|
||||
3. `'Global'` (always checked last)
|
||||
|
||||
## Chord Sequences
|
||||
|
||||
Keybindings support multi-key sequences (chords):
|
||||
|
||||
```
|
||||
"ctrl+k ctrl+s" → Save (press Ctrl+K, then Ctrl+S)
|
||||
"ctrl+k ctrl+c" → Close (press Ctrl+K, then Ctrl+C)
|
||||
```
|
||||
|
||||
When a chord prefix is pressed:
|
||||
- `result.type === 'chord_started'` -- Show "Ctrl+K ..." pending indicator
|
||||
- Next key completes or cancels the chord
|
||||
- `result.type === 'chord_cancelled'` -- Invalid key, reset
|
||||
|
||||
## KeybindingContext Hook
|
||||
|
||||
```tsx
|
||||
import { useKeybindingContext, useOptionalKeybindingContext } from '@anthropic/ink'
|
||||
|
||||
const ctx = useKeybindingContext()
|
||||
// ctx.resolve(input, key, contexts) → ResolveResult
|
||||
// ctx.bindings → ParsedBinding[]
|
||||
// ctx.pendingChord → ParsedKeystroke[] | null
|
||||
// ctx.activeContexts → Set<string>
|
||||
// ctx.getDisplayText(action, context) → string | undefined
|
||||
// ctx.invokeAction(action) → boolean
|
||||
// ctx.registerHandler(registration) → () => void (unsubscribe)
|
||||
|
||||
// Returns null outside provider (no throw)
|
||||
const optionalCtx = useOptionalKeybindingContext()
|
||||
```
|
||||
|
||||
## Parser Functions
|
||||
|
||||
Parse and format keybinding strings:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
parseKeystroke,
|
||||
parseChord,
|
||||
keystrokeToString,
|
||||
chordToString,
|
||||
keystrokeToDisplayString,
|
||||
chordToDisplayString,
|
||||
parseBindings,
|
||||
} from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### `parseKeystroke(str)`
|
||||
|
||||
Parse a single keystroke string:
|
||||
|
||||
```ts
|
||||
parseKeystroke('ctrl+shift+enter')
|
||||
// → { key: 'enter', ctrl: true, alt: false, shift: true, meta: false, super: false }
|
||||
```
|
||||
|
||||
### `parseChord(str)`
|
||||
|
||||
Parse a chord (space-separated keystrokes):
|
||||
|
||||
```ts
|
||||
parseChord('ctrl+k ctrl+s')
|
||||
// → [{ key: 'k', ctrl: true, ... }, { key: 's', ctrl: true, ... }]
|
||||
```
|
||||
|
||||
### `keystrokeToString(ks)` / `chordToString(chord)`
|
||||
|
||||
Convert parsed keystroke/chord back to string.
|
||||
|
||||
### `keystrokeToDisplayString(ks)` / `chordToDisplayString(chord)`
|
||||
|
||||
Convert to human-readable display string (platform-aware).
|
||||
|
||||
### `parseBindings(blocks)`
|
||||
|
||||
Parse a keybinding configuration:
|
||||
|
||||
```ts
|
||||
parseBindings([
|
||||
{
|
||||
context: 'Global',
|
||||
bindings: {
|
||||
'ctrl+s': 'app:save',
|
||||
'ctrl+k ctrl+s': 'app:saveAs',
|
||||
}
|
||||
}
|
||||
])
|
||||
// → ParsedBinding[]
|
||||
```
|
||||
|
||||
## Match Functions
|
||||
|
||||
```tsx
|
||||
import { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### `getKeyName(input, key)`
|
||||
|
||||
Get the canonical key name from raw input:
|
||||
|
||||
```ts
|
||||
getKeyName('\x1b[A', { upArrow: true }) // 'up'
|
||||
```
|
||||
|
||||
### `matchesKeystroke(input, key, target)`
|
||||
|
||||
Check if raw input matches a parsed keystroke:
|
||||
|
||||
```ts
|
||||
matchesKeystroke('s', { ctrl: true, shift: false }, { key: 's', ctrl: true })
|
||||
```
|
||||
|
||||
### `matchesBinding(input, key, binding)`
|
||||
|
||||
Check if raw input matches any keystroke in a binding's chord.
|
||||
|
||||
## Resolver Functions
|
||||
|
||||
```tsx
|
||||
import { resolveKey, resolveKeyWithChordState, getBindingDisplayText } from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### `resolveKey(input, key, contexts, bindings)`
|
||||
|
||||
Resolve input to a binding action:
|
||||
|
||||
```ts
|
||||
const result = resolveKey('s', { ctrl: true, shift: false }, ['Global'], bindings)
|
||||
// result.type: 'match' | 'none' | 'unbound'
|
||||
// result.action: string (when type === 'match')
|
||||
```
|
||||
|
||||
### `resolveKeyWithChordState(input, key, contexts, bindings, pendingChord)`
|
||||
|
||||
Resolve with chord state:
|
||||
|
||||
```ts
|
||||
const result = resolveKeyWithChordState('k', key, ['Global'], bindings, null)
|
||||
// result.type: 'match' | 'none' | 'unbound' | 'chord_started' | 'chord_cancelled'
|
||||
// result.pending: ParsedKeystroke[] (when type === 'chord_started')
|
||||
```
|
||||
|
||||
### `getBindingDisplayText(action, context, bindings)`
|
||||
|
||||
Get the display string for a binding:
|
||||
|
||||
```ts
|
||||
getBindingDisplayText('app:save', 'Global', bindings) // 'Ctrl+S'
|
||||
```
|
||||
407
packages/@ant/ink/docs/09-hooks-reference.md
Normal file
407
packages/@ant/ink/docs/09-hooks-reference.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# Chapter 9: Hooks Reference
|
||||
|
||||
Complete API reference for all hooks exported by `@anthropic/ink`.
|
||||
|
||||
---
|
||||
|
||||
## Application Hooks
|
||||
|
||||
### `useApp()`
|
||||
|
||||
Access app-level operations.
|
||||
|
||||
```ts
|
||||
function useApp(): {
|
||||
exit: (error?: Error) => void
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
const { exit } = useApp()
|
||||
// Gracefully unmount and exit
|
||||
exit()
|
||||
```
|
||||
|
||||
### `useStdin()`
|
||||
|
||||
Access the stdin stream and raw mode control.
|
||||
|
||||
```ts
|
||||
function useStdin(): {
|
||||
stdin: NodeJS.ReadStream
|
||||
isRawModeSupported: boolean
|
||||
setRawMode: (enabled: boolean) => void
|
||||
internal_exitOnCtrlC: boolean
|
||||
internal_eventEmitter: EventEmitter | undefined
|
||||
internal_querier: TerminalQuerier | null
|
||||
}
|
||||
```
|
||||
|
||||
> Prefer `useInput` for keyboard handling.
|
||||
|
||||
---
|
||||
|
||||
## Input Hooks
|
||||
|
||||
### `useInput(handler, options?)`
|
||||
|
||||
Handle keyboard input. See [Chapter 7](./07-user-input.md) for full details.
|
||||
|
||||
```ts
|
||||
function useInput(
|
||||
handler: (input: string, key: Key, event: InputEvent) => void,
|
||||
options?: { isActive?: boolean }
|
||||
): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Terminal Hooks
|
||||
|
||||
### `useTerminalSize()`
|
||||
|
||||
Get current terminal dimensions.
|
||||
|
||||
```ts
|
||||
function useTerminalSize(): {
|
||||
columns: number
|
||||
rows: number
|
||||
}
|
||||
```
|
||||
|
||||
Throws if used outside `<App>`.
|
||||
|
||||
### `useTerminalFocus()`
|
||||
|
||||
Track whether the terminal window is focused.
|
||||
|
||||
```ts
|
||||
function useTerminalFocus(): boolean
|
||||
```
|
||||
|
||||
Uses DECSET 1004 focus reporting. Returns `true` when focused.
|
||||
|
||||
### `useTerminalTitle(title)`
|
||||
|
||||
Set the terminal window title.
|
||||
|
||||
```ts
|
||||
function useTerminalTitle(title: string | null): void
|
||||
```
|
||||
|
||||
Pass `null` to clear the title.
|
||||
|
||||
### `useTerminalViewport()`
|
||||
|
||||
Track element visibility in the terminal viewport.
|
||||
|
||||
```ts
|
||||
function useTerminalViewport(): [
|
||||
ref: (element: DOMElement | null) => void,
|
||||
entry: { isVisible: boolean }
|
||||
]
|
||||
```
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
const [viewportRef, { isVisible }] = useTerminalViewport()
|
||||
|
||||
<Box ref={viewportRef}>
|
||||
<Text>{isVisible ? 'Visible' : 'Scrolled off'}</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### `useTabStatus(kind)`
|
||||
|
||||
Set tab status indicator in terminal tab bar (OSC 21337).
|
||||
|
||||
```ts
|
||||
type TabStatusKind = 'idle' | 'busy' | 'waiting'
|
||||
function useTabStatus(kind: TabStatusKind | null): void
|
||||
```
|
||||
|
||||
### `useTerminalNotification()`
|
||||
|
||||
Send terminal notifications (iTerm2, Kitty, Ghostty, bell).
|
||||
|
||||
```ts
|
||||
function useTerminalNotification(): {
|
||||
notifyITerm2: (opts: { message: string; title?: string }) => void
|
||||
notifyKitty: (opts: { message: string; title: string; id: number }) => void
|
||||
notifyGhostty: (opts: { message: string; title: string }) => void
|
||||
notifyBell: () => void
|
||||
progress: (state: Progress['state'] | null, percentage?: number) => void
|
||||
}
|
||||
```
|
||||
|
||||
Requires `TerminalWriteProvider` in the tree.
|
||||
|
||||
Progress states: `'running'`, `'completed'`, `'error'`, `'indeterminate'`, `null` (clear).
|
||||
|
||||
---
|
||||
|
||||
## Animation & Timing Hooks
|
||||
|
||||
### `useInterval(callback, intervalMs)`
|
||||
|
||||
Clock-backed interval timer.
|
||||
|
||||
```ts
|
||||
function useInterval(callback: () => void, intervalMs: number | null): void
|
||||
```
|
||||
|
||||
Pass `null` to pause. Shares the application clock for efficient batching.
|
||||
|
||||
### `useAnimationTimer(intervalMs)`
|
||||
|
||||
Returns the current clock time, updating at the given interval.
|
||||
|
||||
```ts
|
||||
function useAnimationTimer(intervalMs: number): number
|
||||
```
|
||||
|
||||
Subscribes as non-keepAlive -- won't keep the clock running on its own.
|
||||
|
||||
### `useAnimationFrame(intervalMs?)`
|
||||
|
||||
Synchronized animation hook that pauses when offscreen.
|
||||
|
||||
```ts
|
||||
function useAnimationFrame(
|
||||
intervalMs?: number | null, // default 16
|
||||
): [ref: (element: DOMElement | null) => void, time: number]
|
||||
```
|
||||
|
||||
Returns a ref callback (attach to animated element) and the current animation time. All instances share the same clock. Pass `null` to pause.
|
||||
|
||||
```tsx
|
||||
const [ref, time] = useAnimationFrame(120)
|
||||
const frame = Math.floor(time / 120) % FRAMES.length
|
||||
return <Box ref={ref}>{FRAMES[frame]}</Box>
|
||||
```
|
||||
|
||||
### `useTimeout(delayMs, resetTrigger?)`
|
||||
|
||||
One-shot timer.
|
||||
|
||||
```ts
|
||||
function useTimeout(delay: number, resetTrigger?: number): boolean
|
||||
```
|
||||
|
||||
Returns `true` when the timeout has elapsed. Change `resetTrigger` to restart.
|
||||
|
||||
### `useMinDisplayTime(value, minMs)`
|
||||
|
||||
Ensure a value is displayed for at least `minMs` milliseconds.
|
||||
|
||||
```ts
|
||||
function useMinDisplayTime<T>(value: T, minMs: number): T
|
||||
```
|
||||
|
||||
Holds the previous value until `minMs` has elapsed, then switches to the new value.
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
// Keep showing "Loading" for at least 300ms to prevent flash
|
||||
const displayValue = useMinDisplayTime(isLoading ? 'loading' : 'done', 300)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interaction Hooks
|
||||
|
||||
### `useDoublePress(setPending, onDoublePress, onFirstPress?)`
|
||||
|
||||
Detect double-press (double-click equivalent for keyboard).
|
||||
|
||||
```ts
|
||||
export const DOUBLE_PRESS_TIMEOUT_MS = 800
|
||||
|
||||
function useDoublePress(
|
||||
setPending: (pending: boolean) => void,
|
||||
onDoublePress: () => void,
|
||||
onFirstPress?: () => void
|
||||
): () => void // Returns the press handler
|
||||
```
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
const [pendingExit, setPendingExit] = useState(false)
|
||||
const handlePress = useDoublePress(
|
||||
setPendingExit,
|
||||
() => exit(), // Double press
|
||||
() => {}, // First press
|
||||
)
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape) handlePress()
|
||||
})
|
||||
```
|
||||
|
||||
### `useExitOnCtrlCD(options?)`
|
||||
|
||||
Handle Ctrl+C / Ctrl+D with double-press confirmation.
|
||||
|
||||
```ts
|
||||
type ExitState = {
|
||||
pending: boolean
|
||||
keyName: 'Ctrl-C' | 'Ctrl-D' | null
|
||||
}
|
||||
|
||||
function useExitOnCtrlCDWithKeybindings(
|
||||
onExit?: () => void,
|
||||
onInterrupt?: () => boolean,
|
||||
isActive?: boolean
|
||||
): ExitState
|
||||
```
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
const exitState = useExitOnCtrlCDWithKeybindings(
|
||||
() => exit(),
|
||||
() => { /* return true to prevent exit */ }
|
||||
)
|
||||
|
||||
if (exitState.pending) {
|
||||
return <Text>Press {exitState.keyName} again to exit</Text>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Selection Hooks (Alt-Screen Only)
|
||||
|
||||
### `useSelection()`
|
||||
|
||||
Text selection operations.
|
||||
|
||||
```ts
|
||||
function useSelection(): {
|
||||
copySelection: () => string
|
||||
copySelectionNoClear: () => string
|
||||
clearSelection: () => void
|
||||
hasSelection: () => boolean
|
||||
getState: () => SelectionState | null
|
||||
subscribe: (cb: () => void) => () => void
|
||||
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
|
||||
shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
|
||||
moveFocus: (move: FocusMove) => void
|
||||
captureScrolledRows: (firstRow: number, lastRow: number, side: 'above' | 'below') => void
|
||||
setSelectionBgColor: (color: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
### `useHasSelection()`
|
||||
|
||||
Reactive boolean for selection state.
|
||||
|
||||
```ts
|
||||
function useHasSelection(): boolean
|
||||
```
|
||||
|
||||
Re-renders when selection is created or cleared.
|
||||
|
||||
---
|
||||
|
||||
## Search Hooks
|
||||
|
||||
### `useSearchHighlight()`
|
||||
|
||||
Set and manage search highlighting.
|
||||
|
||||
```ts
|
||||
function useSearchHighlight(): {
|
||||
setQuery: (query: string) => void
|
||||
scanElement: (el: DOMElement) => MatchPosition[]
|
||||
setPositions: (state: { positions: MatchPosition[]; rowOffset: number; currentIdx: number } | null) => void
|
||||
}
|
||||
```
|
||||
|
||||
### `useSearchInput(options)`
|
||||
|
||||
Search input handler with cursor management.
|
||||
|
||||
```ts
|
||||
type UseSearchInputOptions = {
|
||||
isActive: boolean
|
||||
onExit: () => void
|
||||
onCancel?: () => void
|
||||
onExitUp?: () => void
|
||||
columns?: number
|
||||
passthroughCtrlKeys?: string[]
|
||||
initialQuery?: string
|
||||
backspaceExitsOnEmpty?: boolean
|
||||
}
|
||||
|
||||
type UseSearchInputReturn = {
|
||||
query: string
|
||||
setQuery: (q: string) => void
|
||||
cursorOffset: number
|
||||
handleKeyDown: (e: KeyboardEvent) => void
|
||||
}
|
||||
|
||||
function useSearchInput(options: UseSearchInputOptions): UseSearchInputReturn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cursor Hooks
|
||||
|
||||
### `useDeclaredCursor(options)`
|
||||
|
||||
Park the terminal cursor at a specific position for IME and accessibility.
|
||||
|
||||
```ts
|
||||
function useDeclaredCursor({
|
||||
line: number,
|
||||
column: number,
|
||||
active: boolean
|
||||
}): (element: DOMElement | null) => void
|
||||
```
|
||||
|
||||
Returns a ref callback. Position is relative to the ref'd element.
|
||||
|
||||
Example:
|
||||
```tsx
|
||||
const cursorRef = useDeclaredCursor({
|
||||
line: 0,
|
||||
column: cursorPosition,
|
||||
active: isFocused,
|
||||
})
|
||||
|
||||
return <Box ref={cursorRef}>...</Box>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tab Status Hooks
|
||||
|
||||
### `useTabStatus(kind)`
|
||||
|
||||
Set tab status indicator (OSC 21337) for terminal tab bars.
|
||||
|
||||
```ts
|
||||
type TabStatusKind = 'idle' | 'busy' | 'waiting'
|
||||
|
||||
function useTabStatus(kind: TabStatusKind | null): void
|
||||
```
|
||||
|
||||
Pass `null` to clear.
|
||||
|
||||
---
|
||||
|
||||
## Viewport Hooks
|
||||
|
||||
### `useTerminalViewport()`
|
||||
|
||||
Track element visibility within the terminal viewport.
|
||||
|
||||
```ts
|
||||
function useTerminalViewport(): [
|
||||
ref: (element: DOMElement | null) => void,
|
||||
entry: { isVisible: boolean }
|
||||
]
|
||||
```
|
||||
|
||||
Returns a ref callback and visibility state.
|
||||
232
packages/@ant/ink/docs/10-events-and-focus.md
Normal file
232
packages/@ant/ink/docs/10-events-and-focus.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Chapter 10: Events & Focus
|
||||
|
||||
## Event System
|
||||
|
||||
Ink implements a DOM-like event system with capture/bubble phases, propagation control, and prioritized dispatch.
|
||||
|
||||
### Event Classes
|
||||
|
||||
All events extend the base `Event` class:
|
||||
|
||||
```ts
|
||||
class Event {
|
||||
stopImmediatePropagation(): void
|
||||
}
|
||||
```
|
||||
|
||||
### InputEvent
|
||||
|
||||
Emitted for every keystroke or input action.
|
||||
|
||||
```ts
|
||||
class InputEvent extends Event {
|
||||
readonly input: string // Character(s) entered
|
||||
readonly key: Key // Parsed key metadata
|
||||
readonly keypress: ParsedKey // Raw keypress data
|
||||
}
|
||||
```
|
||||
|
||||
### KeyboardEvent
|
||||
|
||||
DOM-like keyboard event for focused elements.
|
||||
|
||||
```ts
|
||||
class KeyboardEvent extends Event {
|
||||
readonly key: Key
|
||||
}
|
||||
```
|
||||
|
||||
Dispatched via `onKeyDown` / `onKeyDownCapture` on `Box`.
|
||||
|
||||
### ClickEvent
|
||||
|
||||
Mouse click event (alt-screen only).
|
||||
|
||||
```ts
|
||||
class ClickEvent extends Event {
|
||||
readonly x: number // Column (0-indexed)
|
||||
readonly y: number // Row (0-indexed)
|
||||
}
|
||||
```
|
||||
|
||||
Clicks bubble from the deepest hit Box up through ancestors.
|
||||
|
||||
### FocusEvent
|
||||
|
||||
Focus change event.
|
||||
|
||||
```ts
|
||||
class FocusEvent extends Event {
|
||||
readonly relatedTarget: DOMElement | null
|
||||
}
|
||||
```
|
||||
|
||||
### TerminalFocusEvent
|
||||
|
||||
Terminal window focus change.
|
||||
|
||||
```ts
|
||||
class TerminalFocusEvent extends Event {
|
||||
readonly type: 'terminalfocus' | 'terminalblur'
|
||||
}
|
||||
```
|
||||
|
||||
### ResizeEvent
|
||||
|
||||
Terminal resize event (internal).
|
||||
|
||||
### PasteEvent
|
||||
|
||||
Pasted text event (bracketed paste mode).
|
||||
|
||||
## Event Dispatch Flow
|
||||
|
||||
```
|
||||
stdin data → parse-keypress → InputEvent
|
||||
↓
|
||||
App.handleInput (useInput handlers)
|
||||
↓
|
||||
Box.onKeyDown (focused element, bubble)
|
||||
```
|
||||
|
||||
### Capture and Bubble Phases
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
onKeyDownCapture={(e) => {
|
||||
// Capture phase: fires top-down
|
||||
console.log('Parent captures key')
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Bubble phase: fires bottom-up
|
||||
console.log('Parent receives bubbled key')
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onKeyDown={(e) => {
|
||||
// Target: fires first in bubble phase
|
||||
console.log('Child handles key')
|
||||
e.stopImmediatePropagation() // Stop here
|
||||
}}
|
||||
>
|
||||
<Text>Focus here</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Event Propagation Methods
|
||||
|
||||
| Method | Effect |
|
||||
|--------|--------|
|
||||
| `event.stopImmediatePropagation()` | Stop all subsequent handlers |
|
||||
| `event.preventDefault()` | Not supported in terminal context |
|
||||
|
||||
## FocusManager
|
||||
|
||||
DOM-like focus management system.
|
||||
|
||||
### How Focus Works
|
||||
|
||||
1. Elements with `tabIndex >= 0` participate in Tab/Shift+Tab cycling
|
||||
2. Elements with `tabIndex === -1` are programmatically focusable only
|
||||
3. Elements with `autoFocus` receive focus on mount
|
||||
4. Clicking a focusable element focuses it
|
||||
|
||||
### Focus API
|
||||
|
||||
```ts
|
||||
class FocusManager {
|
||||
activeElement: DOMElement | null
|
||||
|
||||
focus(node: DOMElement): void
|
||||
blur(): void
|
||||
focusNext(root: DOMElement): void // Tab
|
||||
focusPrevious(root: DOMElement): void // Shift+Tab
|
||||
|
||||
handleNodeRemoved(node: DOMElement, root: DOMElement): void
|
||||
handleAutoFocus(node: DOMElement): void
|
||||
handleClickFocus(node: DOMElement): void
|
||||
|
||||
enable(): void
|
||||
disable(): void
|
||||
}
|
||||
```
|
||||
|
||||
### Tab Navigation
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="column">
|
||||
<Button tabIndex={0} onAction={handleSave}>
|
||||
{(s) => <Text>{s.focused ? '> Save' : ' Save'}</Text>}
|
||||
</Button>
|
||||
<Button tabIndex={0} onAction={handleCancel}>
|
||||
{(s) => <Text>{s.focused ? '> Cancel' : ' Cancel'}</Text>}
|
||||
</Button>
|
||||
<Button tabIndex={-1} onAction={handleSecret}>
|
||||
{/* Not reachable via Tab */}
|
||||
{(s) => <Text>Secret</Text>}
|
||||
</Button>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Auto Focus
|
||||
|
||||
```tsx
|
||||
<Box tabIndex={0} autoFocus onKeyDown={handleKey}>
|
||||
<Text>Receives focus immediately on mount</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Focus Events
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
tabIndex={0}
|
||||
onFocus={(e) => console.log('Got focus')}
|
||||
onBlur={(e) => console.log('Lost focus')}
|
||||
onFocusCapture={(e) => console.log('Capture: focus in')}
|
||||
onBlurCapture={(e) => console.log('Capture: focus out')}
|
||||
>
|
||||
<Text>Focusable element</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Hit Testing
|
||||
|
||||
Mouse click/hover resolution:
|
||||
|
||||
1. Screen coordinates are mapped to DOM elements via Yoga layout
|
||||
2. The deepest element at the click position is the target
|
||||
3. Click events bubble upward through ancestors
|
||||
4. Hover events use `mouseenter`/`mouseleave` semantics (no bubbling between children)
|
||||
|
||||
### Click Hit Testing
|
||||
|
||||
```ts
|
||||
dispatchClick(rootNode, col, row): void
|
||||
```
|
||||
|
||||
Walks the DOM tree, finds the deepest Box at (col, row), fires `onClick`, then bubbles to ancestors.
|
||||
|
||||
### Hover Hit Testing
|
||||
|
||||
```ts
|
||||
dispatchHover(rootNode, col, row, hoveredNodes): void
|
||||
```
|
||||
|
||||
Tracks which nodes are under the pointer. Fires `onMouseEnter`/`onMouseLeave` as the pointer moves between elements.
|
||||
|
||||
## EventEmitter
|
||||
|
||||
Custom event emitter for internal use:
|
||||
|
||||
```ts
|
||||
class EventEmitter {
|
||||
on(event: string, handler: Function): void
|
||||
off(event: string, handler: Function): void
|
||||
emit(event: string, ...args: any[]): void
|
||||
removeListener(event: string, handler: Function): void
|
||||
}
|
||||
```
|
||||
|
||||
Used internally by the Ink instance for `input` events.
|
||||
301
packages/@ant/ink/docs/11-core-architecture.md
Normal file
301
packages/@ant/ink/docs/11-core-architecture.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Chapter 11: Core Architecture
|
||||
|
||||
This chapter covers the internal rendering pipeline, DOM model, and screen buffer system. This is advanced material -- most users only need the component and hooks APIs.
|
||||
|
||||
## Rendering Pipeline
|
||||
|
||||
```
|
||||
React Component Tree
|
||||
↓ (React reconciler)
|
||||
Ink DOM Tree (virtual terminal DOM)
|
||||
↓ (Yoga layout)
|
||||
Positioned DOM Tree (computed x, y, width, height)
|
||||
↓ (renderNodeToOutput)
|
||||
Output Buffer (styled characters)
|
||||
↓ (renderer → Screen)
|
||||
Screen Buffer (Int32Array of cells)
|
||||
↓ (diffEach)
|
||||
ANSI Diff Patches (minimal escape sequences)
|
||||
↓ (writeDiffToTerminal)
|
||||
Terminal stdout
|
||||
```
|
||||
|
||||
### Frame Lifecycle
|
||||
|
||||
Each render cycle (`onRender`) follows these phases:
|
||||
|
||||
1. **React Commit** -- React reconciles the virtual tree; host config updates Ink DOM
|
||||
2. **Yoga Layout** -- All dirty nodes have their styles applied and layout computed
|
||||
3. **Renderer** -- Creates Output buffer, calls `renderNodeToOutput` for the full tree
|
||||
4. **Screen Diff** -- New frame is compared against previous frame cell-by-cell
|
||||
5. **Optimize** -- Patches are merged and ordered for minimal cursor movement
|
||||
6. **Write** -- ANSI escape sequences are written to stdout
|
||||
|
||||
### Frame Timing
|
||||
|
||||
```ts
|
||||
const FRAME_INTERVAL_MS = 16 // ~60fps cap
|
||||
```
|
||||
|
||||
Renders are throttled. Multiple state updates in one frame are batched.
|
||||
|
||||
### Double Buffering
|
||||
|
||||
Two frames are maintained:
|
||||
|
||||
- **`frontFrame`** -- The currently displayed frame
|
||||
- **`backFrame`** -- The frame being rendered
|
||||
|
||||
After rendering, they are swapped. This prevents partial updates from being visible.
|
||||
|
||||
## Ink DOM
|
||||
|
||||
### Node Types
|
||||
|
||||
```ts
|
||||
type ElementNames =
|
||||
| 'ink-root' // Root container
|
||||
| 'ink-box' // Box component
|
||||
| 'ink-text' // Text component
|
||||
| 'ink-virtual-text' // Intermediate text wrapper
|
||||
| 'ink-link' // Link component
|
||||
| 'ink-raw-ansi' // Raw ANSI content
|
||||
```
|
||||
|
||||
### DOMElement
|
||||
|
||||
```ts
|
||||
type DOMElement = {
|
||||
nodeName: ElementNames
|
||||
attributes: Record<string, unknown>
|
||||
childNodes: DOMNode[] // DOMElement | TextNode
|
||||
yogaNode?: LayoutNode // Yoga layout node
|
||||
textStyles?: TextStyles // Inherited text styles
|
||||
|
||||
// Scroll state
|
||||
scrollTop?: number
|
||||
scrollHeight?: number
|
||||
scrollViewportHeight?: number
|
||||
scrollViewportTop?: number
|
||||
stickyScroll?: boolean
|
||||
pendingScrollDelta?: number
|
||||
scrollAnchor?: { el: DOMElement; offset: number }
|
||||
|
||||
// Dirty tracking
|
||||
dirty: boolean
|
||||
|
||||
// Event handlers (stored separately)
|
||||
onClick?: (event: ClickEvent) => void
|
||||
onFocus?: (event: FocusEvent) => void
|
||||
onBlur?: (event: FocusEvent) => void
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
}
|
||||
```
|
||||
|
||||
### TextNode
|
||||
|
||||
```ts
|
||||
type TextNode = {
|
||||
nodeName: '#text'
|
||||
nodeValue: string
|
||||
yogaNode?: LayoutNode
|
||||
}
|
||||
```
|
||||
|
||||
### DOM Operations
|
||||
|
||||
```ts
|
||||
// Node creation
|
||||
createNode(nodeName: string): DOMElement
|
||||
createTextNode(text: string): TextNode
|
||||
|
||||
// Tree manipulation
|
||||
appendChildNode(parent: DOMElement, child: DOMNode): void
|
||||
insertBeforeNode(parent: DOMElement, child: DOMNode, before: DOMNode): void
|
||||
removeChildNode(parent: DOMElement, child: DOMNode): void
|
||||
|
||||
// Attribute manipulation
|
||||
setAttribute(node: DOMElement, key: string, value: unknown): void
|
||||
setStyle(node: DOMElement, style: Styles): void
|
||||
setTextStyles(node: DOMElement, styles: TextStyles): void
|
||||
|
||||
// Dirty tracking
|
||||
markDirty(node: DOMElement): void
|
||||
scheduleRenderFrom(node: DOMElement): void
|
||||
```
|
||||
|
||||
## Screen Buffer
|
||||
|
||||
### Cell Storage
|
||||
|
||||
The screen buffer uses packed `Int32Array` storage for memory efficiency:
|
||||
|
||||
```ts
|
||||
type Screen = {
|
||||
width: number
|
||||
height: number
|
||||
cells: Int32Array // 2 Int32s per cell: [charId, packed_style_hyperlink_width]
|
||||
cells64: BigInt64Array // For bulk fill operations
|
||||
charPool: CharPool // String interning
|
||||
stylePool: StylePool // ANSI code interning
|
||||
hyperlinkPool: HyperlinkPool
|
||||
emptyStyleId: number
|
||||
damage: Rectangle | undefined // Bounding box of changed cells
|
||||
noSelect: Uint8Array // Per-cell no-select bitmap
|
||||
softWrap: Int32Array // Per-row soft-wrap markers
|
||||
}
|
||||
```
|
||||
|
||||
### Cell Width
|
||||
|
||||
```ts
|
||||
enum CellWidth {
|
||||
Narrow = 0, // Regular character (1 column)
|
||||
Wide = 1, // CJK/emoji (2 columns)
|
||||
SpacerTail = 2, // Right half of wide character
|
||||
SpacerHead = 3, // Soft-wrapped wide character
|
||||
}
|
||||
```
|
||||
|
||||
### Style Pool
|
||||
|
||||
ANSI style codes are interned for efficiency:
|
||||
|
||||
```ts
|
||||
class StylePool {
|
||||
intern(codes: AnsiCode[]): number // Returns compact ID
|
||||
get(id: number): AnsiCode[]
|
||||
transition(from: number, to: number): string // Cached ANSI transition
|
||||
withInverse(id: number): number // Selection overlay
|
||||
setSelectionBg(bg: AnsiCode): void // Theme-aware selection bg
|
||||
}
|
||||
```
|
||||
|
||||
### Diff Algorithm
|
||||
|
||||
```ts
|
||||
diffEach(prev: Screen, next: Screen, callback: (x, y, oldCell, newCell) => void): void
|
||||
```
|
||||
|
||||
Only iterates cells within the damage bounding box. Unchanged regions are skipped entirely.
|
||||
|
||||
### Screen Operations
|
||||
|
||||
```ts
|
||||
createScreen(width, height, stylePool, charPool, hyperlinkPool): Screen
|
||||
setCellAt(screen, x, y, cell): void
|
||||
cellAt(screen, x, y): Cell
|
||||
clearRegion(screen, x, y, width, height): void
|
||||
blitRegion(dst, src, x, y, maxX, maxY): void
|
||||
shiftRows(screen, top, bottom, n): void
|
||||
```
|
||||
|
||||
## Layout Engine
|
||||
|
||||
### Yoga Integration
|
||||
|
||||
Ink wraps Facebook's Yoga layout engine for Flexbox computation:
|
||||
|
||||
```ts
|
||||
// Layout node types
|
||||
enum LayoutDisplay { Flex, None }
|
||||
enum LayoutPositionType { Absolute, Relative }
|
||||
enum LayoutOverflow { Visible, Hidden, Scroll }
|
||||
enum LayoutFlexDirection { Row, Column, RowReverse, ColumnReverse }
|
||||
enum LayoutWrap { NoWrap, Wrap, WrapReverse }
|
||||
enum LayoutAlign { FlexStart, Center, FlexEnd, Stretch }
|
||||
enum LayoutJustify { FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly }
|
||||
enum LayoutEdge { Top, Bottom, Left, Right, Start, End, Horizontal, Vertical, All }
|
||||
enum LayoutGutter { Column, Row, All }
|
||||
```
|
||||
|
||||
### Style Application
|
||||
|
||||
Styles from React props are applied to Yoga nodes during the commit phase:
|
||||
|
||||
```ts
|
||||
function styles(node: LayoutNode, style: Styles, resolvedStyle?: Styles): void
|
||||
```
|
||||
|
||||
This function maps each CSS-like prop to the corresponding Yoga setter.
|
||||
|
||||
## Output Buffer
|
||||
|
||||
Intermediate rendering target before screen diff:
|
||||
|
||||
```ts
|
||||
class Output {
|
||||
write(text: string, x: number, y: number, styles: TextStyles): void
|
||||
wrap(width: number, textWrap: TextWrap): void
|
||||
}
|
||||
```
|
||||
|
||||
`renderNodeToOutput` walks the DOM tree and writes styled characters into this buffer.
|
||||
|
||||
## Reconciler
|
||||
|
||||
Custom React reconciler that bridges React and the Ink DOM:
|
||||
|
||||
- **Host config** -- Defines how React operations map to Ink DOM mutations
|
||||
- **Concurrent mode** -- Supports `ConcurrentRoot` for React 19 features
|
||||
- **Yoga integration** -- Applies styles during commit phase
|
||||
- **DevTools** -- Connected in development mode
|
||||
|
||||
### Host Config Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `createInstance` | Create `ink-box`, `ink-text`, etc. |
|
||||
| `createTextInstance` | Create `#text` node |
|
||||
| `appendChildNode` | Add child to parent |
|
||||
| `removeChildNode` | Remove child from parent |
|
||||
| `insertBefore` | Insert child before sibling |
|
||||
| `commitUpdate` | Update element attributes/styles |
|
||||
| `commitTextUpdate` | Update text content |
|
||||
| `getPublicInstance` | Return DOMElement for refs |
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
1. **String Interning** -- CharPool deduplicates character strings across frames
|
||||
2. **Style Caching** -- StylePool caches ANSI transition strings
|
||||
3. **Damage Tracking** -- Only diff cells within the changed bounding box
|
||||
4. **Bulk Operations** -- `Int32Array.set()` for fast region blit
|
||||
5. **Throttled Rendering** -- Frame rate capped at ~60fps
|
||||
6. **Viewport Culling** -- ScrollBox only renders visible children
|
||||
7. **Microtask Coalescing** -- Multiple scroll deltas merged into one render
|
||||
|
||||
## Frame Events
|
||||
|
||||
Debug instrumentation for render performance:
|
||||
|
||||
```ts
|
||||
type FrameEvent = {
|
||||
durationMs: number
|
||||
phases: {
|
||||
renderer: number // Yoga + renderNodeToOutput
|
||||
diff: number // Screen diff
|
||||
optimize: number // Patch optimization
|
||||
write: number // Terminal write
|
||||
patches: number // Number of ANSI patches
|
||||
yoga: number // Yoga layout time
|
||||
commit: number // React commit time
|
||||
yogaVisited: number // Yoga nodes visited
|
||||
yogaMeasured: number // Yoga nodes measured
|
||||
yogaCacheHits: number // Cached measurements
|
||||
yogaLive: number // Active Yoga nodes
|
||||
}
|
||||
flickers: FlickerReason[]
|
||||
}
|
||||
```
|
||||
|
||||
Enable with `onFrame` in RenderOptions:
|
||||
|
||||
```tsx
|
||||
render(<App />, {
|
||||
onFrame: (event) => {
|
||||
console.log(`Frame: ${event.durationMs}ms`)
|
||||
}
|
||||
})
|
||||
```
|
||||
381
packages/@ant/ink/docs/12-terminal-integration.md
Normal file
381
packages/@ant/ink/docs/12-terminal-integration.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# Chapter 12: Terminal Integration
|
||||
|
||||
This chapter covers terminal-specific features: alternate screen, mouse tracking, clipboard, notifications, and terminal querying.
|
||||
|
||||
## Alternate Screen
|
||||
|
||||
Enter a fullscreen alternate screen buffer (like vim, less, htop).
|
||||
|
||||
```tsx
|
||||
import { AlternateScreen } from '@anthropic/ink'
|
||||
|
||||
<AlternateScreen mouseTracking={true}>
|
||||
<Box flexDirection="column" height="100%">
|
||||
<Text>Fullscreen content</Text>
|
||||
</Box>
|
||||
</AlternateScreen>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactNode` | - | Content |
|
||||
| `mouseTracking` | `boolean` | `true` | Enable SGR mouse tracking |
|
||||
|
||||
### Behavior
|
||||
|
||||
On mount:
|
||||
1. Enters DEC 1049 alternate screen buffer
|
||||
2. Hides cursor
|
||||
3. Enables mouse tracking (if `mouseTracking=true`)
|
||||
4. Constrains rendering height to terminal rows
|
||||
|
||||
On unmount:
|
||||
1. Exits alternate screen buffer
|
||||
2. Shows cursor
|
||||
3. Disables mouse tracking
|
||||
4. Restores original terminal content
|
||||
|
||||
### Mouse Tracking Modes
|
||||
|
||||
When enabled:
|
||||
- **Mode 1003** -- Button press/release + motion (hover)
|
||||
- **Mode 1006** -- SGR extended mouse format (coordinates > 223)
|
||||
- **Wheel events** -- Scroll up/down
|
||||
|
||||
### External Editor Handoff
|
||||
|
||||
The Ink instance supports pausing for an external editor:
|
||||
|
||||
```ts
|
||||
// Pause Ink, run external command, resume
|
||||
ink.enterAlternateScreen() // Save state
|
||||
// ... external editor runs ...
|
||||
ink.reassertTerminalModes() // Restore on resume
|
||||
```
|
||||
|
||||
This is triggered by Ctrl+Z (SIGTSTP) and SIGCONT.
|
||||
|
||||
## Mouse Events
|
||||
|
||||
### Click Events
|
||||
|
||||
```tsx
|
||||
<Box onClick={(event) => {
|
||||
console.log(`Clicked at col=${event.x}, row=${event.y}`)
|
||||
event.stopImmediatePropagation()
|
||||
}}>
|
||||
<Text>Clickable area</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Multi-Click
|
||||
|
||||
Double-click selects a word, triple-click selects a line. Handled by the App component:
|
||||
|
||||
```ts
|
||||
// App prop
|
||||
onMultiClick: (col: number, row: number, count: 2 | 3) => void
|
||||
```
|
||||
|
||||
### Hover Events
|
||||
|
||||
```tsx
|
||||
<Box
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
Hover uses `mouseenter`/`mouseleave` semantics (no bubbling between children).
|
||||
|
||||
### Drag-to-Select
|
||||
|
||||
In alt-screen mode, click-drag creates a text selection:
|
||||
|
||||
```ts
|
||||
// App prop
|
||||
onSelectionDrag: (col: number, row: number) => void
|
||||
```
|
||||
|
||||
## Clipboard
|
||||
|
||||
### OSC 52 Clipboard
|
||||
|
||||
```tsx
|
||||
import { setClipboard } from '@anthropic/ink'
|
||||
|
||||
await setClipboard('Copied text')
|
||||
```
|
||||
|
||||
### Copy Selection
|
||||
|
||||
```tsx
|
||||
const { copySelection } = useSelection()
|
||||
const text = copySelection() // Copies to clipboard and clears highlight
|
||||
```
|
||||
|
||||
### Copy Without Clear
|
||||
|
||||
```tsx
|
||||
const { copySelectionNoClear } = useSelection()
|
||||
const text = copySelectionNoClear() // Copies but keeps highlight
|
||||
```
|
||||
|
||||
## Terminal Notifications
|
||||
|
||||
Send desktop notifications from the terminal.
|
||||
|
||||
```tsx
|
||||
import { useTerminalNotification } from '@anthropic/ink'
|
||||
|
||||
function MyComponent() {
|
||||
const { notifyBell, progress } = useTerminalNotification()
|
||||
|
||||
// Terminal bell (audible/system notification)
|
||||
notifyBell()
|
||||
|
||||
// Progress bar in terminal title/tab
|
||||
progress('running', 65) // 65% complete
|
||||
progress('completed') // Done
|
||||
progress('error') // Error state
|
||||
progress('indeterminate') // Unknown progress
|
||||
progress(null) // Clear
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal-Specific Notifications
|
||||
|
||||
```tsx
|
||||
const { notifyITerm2, notifyKitty, notifyGhostty } = useTerminalNotification()
|
||||
|
||||
// iTerm2
|
||||
notifyITerm2({ message: 'Build complete', title: 'My App' })
|
||||
|
||||
// Kitty
|
||||
notifyKitty({ message: 'Build complete', title: 'My App', id: 1 })
|
||||
|
||||
// Ghostty
|
||||
notifyGhostty({ message: 'Build complete', title: 'My App' })
|
||||
```
|
||||
|
||||
## Terminal Queries
|
||||
|
||||
### Background Color (OSC 11)
|
||||
|
||||
Used for auto-theme detection:
|
||||
|
||||
```ts
|
||||
import { getTerminalBackground } from '@anthropic/ink'
|
||||
const bg = await getTerminalBackground()
|
||||
// e.g., 'rgb:0000/0000/0000' (dark) or 'rgb:ffff/ffff/ffff' (light)
|
||||
```
|
||||
|
||||
### Terminal Version (XTVERSION)
|
||||
|
||||
```ts
|
||||
import { isXtermJs, setXtversionName, getXtversionName } from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### Feature Detection
|
||||
|
||||
```ts
|
||||
import { supportsHyperlinks } from '@anthropic/ink'
|
||||
|
||||
if (supportsHyperlinks()) {
|
||||
// OSC 8 hyperlinks supported
|
||||
}
|
||||
|
||||
import { supportsExtendedKeys } from '@anthropic/ink'
|
||||
|
||||
if (supportsExtendedKeys()) {
|
||||
// Kitty keyboard protocol / modifyOtherKeys available
|
||||
}
|
||||
```
|
||||
|
||||
## Terminal Focus
|
||||
|
||||
Track terminal window focus/unfocus:
|
||||
|
||||
```tsx
|
||||
import { useTerminalFocus } from '@anthropic/ink'
|
||||
|
||||
const isFocused = useTerminalFocus()
|
||||
```
|
||||
|
||||
Low-level API:
|
||||
|
||||
```ts
|
||||
import { getTerminalFocused, subscribeTerminalFocus } from '@anthropic/ink'
|
||||
|
||||
getTerminalFocused() // boolean
|
||||
subscribeTerminalFocus((focused: boolean) => {
|
||||
// Called on focus change
|
||||
})
|
||||
```
|
||||
|
||||
Uses DECSET 1004 focus reporting.
|
||||
|
||||
## Terminal Title
|
||||
|
||||
Set the terminal window title:
|
||||
|
||||
```tsx
|
||||
import { useTerminalTitle } from '@anthropic/ink'
|
||||
|
||||
useTerminalTitle('My App - Dashboard')
|
||||
```
|
||||
|
||||
Clear:
|
||||
|
||||
```tsx
|
||||
useTerminalTitle(null)
|
||||
```
|
||||
|
||||
## Terminal I/O Sequences
|
||||
|
||||
Low-level ANSI sequence constants for advanced use.
|
||||
|
||||
### Cursor Control
|
||||
|
||||
```ts
|
||||
import {
|
||||
SHOW_CURSOR,
|
||||
HIDE_CURSOR,
|
||||
CURSOR_HOME,
|
||||
} from '@anthropic/ink'
|
||||
|
||||
// cursorPosition(row, col) -- Move cursor to absolute position
|
||||
// cursorMove(dx, dy) -- Move cursor relative
|
||||
```
|
||||
|
||||
### Screen Control
|
||||
|
||||
```ts
|
||||
import {
|
||||
ENTER_ALT_SCREEN,
|
||||
EXIT_ALT_SCREEN,
|
||||
ERASE_SCREEN,
|
||||
} from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### Mouse Control
|
||||
|
||||
```ts
|
||||
import {
|
||||
ENABLE_MOUSE_TRACKING,
|
||||
DISABLE_MOUSE_TRACKING,
|
||||
} from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### Keyboard Protocols
|
||||
|
||||
```ts
|
||||
import {
|
||||
ENABLE_KITTY_KEYBOARD,
|
||||
DISABLE_KITTY_KEYBOARD,
|
||||
ENABLE_MODIFY_OTHER_KEYS,
|
||||
DISABLE_MODIFY_OTHER_KEYS,
|
||||
} from '@anthropic/ink'
|
||||
```
|
||||
|
||||
### Clipboard & Tab Status
|
||||
|
||||
```ts
|
||||
import {
|
||||
CLEAR_ITERM2_PROGRESS,
|
||||
CLEAR_TAB_STATUS,
|
||||
CLEAR_TERMINAL_TITLE,
|
||||
wrapForMultiplexer,
|
||||
} from '@anthropic/ink'
|
||||
```
|
||||
|
||||
`wrapForMultiplexer` wraps OSC sequences for tmux compatibility.
|
||||
|
||||
## Terminal Compatibility
|
||||
|
||||
### Supported Terminals
|
||||
|
||||
| Terminal | Features |
|
||||
|----------|----------|
|
||||
| iTerm2 | Full support (hyperlinks, notifications, progress) |
|
||||
| Kitty | Full support (keyboard protocol, notifications) |
|
||||
| Ghostty | Full support |
|
||||
| WezTerm | Full support |
|
||||
| Alacritty | Most features |
|
||||
| Windows Terminal | Most features |
|
||||
| Apple Terminal | 256-color fallback |
|
||||
| xterm.js (VS Code) | Detected and special-cased |
|
||||
| tmux | Wrapped sequences via `wrapForMultiplexer` |
|
||||
| Screen | Basic support |
|
||||
|
||||
### Feature Degradation
|
||||
|
||||
The framework gracefully degrades:
|
||||
- No true color → Falls back to ANSI 16-color themes
|
||||
- No OSC 52 → Clipboard operations silently fail
|
||||
- No mouse tracking → Click/hover events are no-ops
|
||||
- No extended keys → Standard escape sequences used
|
||||
- No bracketed paste → Paste detected by timing heuristic
|
||||
|
||||
### Synchronized Output
|
||||
|
||||
```ts
|
||||
import { isSynchronizedOutputSupported } from '@anthropic/ink'
|
||||
|
||||
if (isSynchronizedOutputSupported()) {
|
||||
// BSU/ESU for tear-free rendering
|
||||
}
|
||||
```
|
||||
|
||||
Uses DECSET 2026 synchronized output to prevent partial frame display.
|
||||
|
||||
### Bracketed Paste
|
||||
|
||||
Uses DECSET 2004 to distinguish paste events from rapid typing. Automatically enabled by the App component.
|
||||
|
||||
## Text Selection (Alt-Screen)
|
||||
|
||||
### Selection State
|
||||
|
||||
```ts
|
||||
type SelectionState = {
|
||||
anchor: Point | null // Drag start
|
||||
focus: Point | null // Current position
|
||||
isDragging: boolean
|
||||
anchorSpan: { lo: Point; hi: Point; kind: 'word' | 'line' } | null
|
||||
scrolledOffAbove: string[] // Text scrolled out above
|
||||
scrolledOffBelow: string[] // Text scrolled out below
|
||||
}
|
||||
```
|
||||
|
||||
### Selection Operations
|
||||
|
||||
- **Click-drag** -- Free-form selection
|
||||
- **Double-click** -- Word selection
|
||||
- **Triple-click** -- Line selection
|
||||
- **Shift+Arrow** -- Extend selection from keyboard
|
||||
- **Drag-to-scroll** -- Auto-scroll when dragging near edges
|
||||
|
||||
### noSelect Regions
|
||||
|
||||
Exclude areas from selection (gutters, line numbers):
|
||||
|
||||
```tsx
|
||||
<Box noSelect={true}>
|
||||
<Text>1 │</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>code here</Text> {/* Only this is selectable */}
|
||||
</Box>
|
||||
```
|
||||
|
||||
### Soft-Wrap Awareness
|
||||
|
||||
Selection correctly handles text that was wrapped across multiple rows:
|
||||
- Wrapped lines are joined when copied
|
||||
- Trailing whitespace is trimmed
|
||||
- The `softWrap` bitmap tracks which rows are continuations
|
||||
46
packages/@ant/ink/docs/README.md
Normal file
46
packages/@ant/ink/docs/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# @anthropic/ink Documentation
|
||||
|
||||
A terminal React rendering framework for building rich command-line interfaces.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
`@anthropic/ink` is a forked/internal Ink framework that renders React components directly to the terminal using ANSI escape sequences. It uses Yoga (via a custom layout engine) for Flexbox layout, a custom React reconciler for terminal DOM, and a screen-buffer differ for efficient updates.
|
||||
|
||||
### Three-Layer Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Layer 3: Theme │
|
||||
│ ThemeProvider, ThemedBox, ThemedText, │
|
||||
│ Dialog, FuzzyPicker, ProgressBar, etc. │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Layer 2: Components │
|
||||
│ Box, Text, ScrollBox, Button, Link, │
|
||||
│ Newline, Spacer, AlternateScreen │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Layer 1: Core │
|
||||
│ Reconciler, Layout (Yoga), Terminal │
|
||||
│ I/O, Screen Buffer, Event System │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Core** (`src/core/`) -- Rendering engine: React reconciler, Yoga flexbox layout, terminal I/O, screen buffer with diff-based updates, event system (keyboard, mouse, focus, click).
|
||||
- **Components** (`src/components/`) -- UI primitives: `Box`, `Text`, `ScrollBox`, `Button`, `Link`, `Newline`, `Spacer`, etc. Plus context providers (`App`, `StdinContext`).
|
||||
- **Theme** (`src/theme/`) -- Theme system: `ThemeProvider`, theme-aware `Box`/`Text` wrappers, and design-system components (`Dialog`, `FuzzyPicker`, `ProgressBar`, `Tabs`, etc.).
|
||||
|
||||
### Documentation
|
||||
|
||||
| Chapter | File | Contents |
|
||||
|---------|------|----------|
|
||||
| 1 | [Getting Started](./01-getting-started.md) | Installation, rendering, basic concepts |
|
||||
| 2 | [Layout System](./02-layout.md) | Box, Flexbox, Yoga, positioning, dimensions |
|
||||
| 3 | [Text & Styling](./03-text-and-styling.md) | Text component, colors, text wrapping, ANSI styling |
|
||||
| 4 | [Theme System](./04-theme-system.md) | ThemeProvider, themes, ThemedBox, ThemedText, color() |
|
||||
| 5 | [Design System Components](./05-design-system.md) | Dialog, ProgressBar, FuzzyPicker, Tabs, Spinner, etc. |
|
||||
| 6 | [Scrolling](./06-scrolling.md) | ScrollBox, sticky scroll, imperative scroll API |
|
||||
| 7 | [User Input](./07-user-input.md) | useInput, Key types, raw mode, mouse events |
|
||||
| 8 | [Keybinding System](./08-keybindings.md) | KeybindingProvider, useKeybinding, chord sequences, parser |
|
||||
| 9 | [Hooks Reference](./09-hooks-reference.md) | All hooks with full API signatures |
|
||||
| 10 | [Events & Focus](./10-events-and-focus.md) | Event system, FocusManager, click/hover, tab navigation |
|
||||
| 11 | [Core Architecture](./11-core-architecture.md) | Reconciler, screen buffer, terminal I/O, rendering pipeline |
|
||||
| 12 | [Terminal Integration](./12-terminal-integration.md) | Alternate screen, mouse tracking, clipboard, notifications |
|
||||
11
packages/agent-tools/package.json
Normal file
11
packages/agent-tools/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@claude-code-best/agent-tools",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"zod": "^3.25.0"
|
||||
}
|
||||
}
|
||||
34
packages/agent-tools/src/__tests__/compat.test.ts
Normal file
34
packages/agent-tools/src/__tests__/compat.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
|
||||
import type { Tool as HostTool } from '../../src/Tool.js'
|
||||
|
||||
describe('agent-tools compatibility', () => {
|
||||
test('CoreTool structural compatibility with host Tool', () => {
|
||||
// The host's Tool should structurally satisfy CoreTool
|
||||
// because it has all required fields (name, call, description, etc.)
|
||||
// This test verifies the type-level compatibility at runtime
|
||||
const mockHostTool: HostTool = {
|
||||
name: 'test',
|
||||
aliases: [],
|
||||
searchHint: 'test tool',
|
||||
inputSchema: {} as any,
|
||||
async call() { return { data: 'ok' } as any },
|
||||
async description() { return 'test' },
|
||||
async prompt() { return 'test prompt' },
|
||||
isConcurrencySafe: () => false,
|
||||
isEnabled: () => true,
|
||||
isReadOnly: () => false,
|
||||
async checkPermissions() { return { behavior: 'allow' as const, updatedInput: {} } },
|
||||
toAutoClassifierInput: () => '',
|
||||
userFacingName: () => 'test',
|
||||
maxResultSizeChars: 100000,
|
||||
mapToolResultToToolResultBlockParam: () => ({ type: 'tool_result', tool_use_id: '1', content: 'ok' }),
|
||||
renderToolUseMessage: () => null,
|
||||
}
|
||||
|
||||
// This assignment should work if HostTool structurally extends CoreTool
|
||||
const coreTool: CoreTool = mockHostTool as CoreTool
|
||||
expect(coreTool.name).toBe('test')
|
||||
expect(coreTool.isEnabled()).toBe(true)
|
||||
})
|
||||
})
|
||||
63
packages/agent-tools/src/__tests__/registry.test.ts
Normal file
63
packages/agent-tools/src/__tests__/registry.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { findToolByName, toolMatchesName } from '../registry.js'
|
||||
import type { CoreTool, Tools } from '../types.js'
|
||||
|
||||
describe('toolMatchesName', () => {
|
||||
test('matches primary name', () => {
|
||||
expect(toolMatchesName({ name: 'bash' }, 'bash')).toBe(true)
|
||||
})
|
||||
|
||||
test('does not match different name', () => {
|
||||
expect(toolMatchesName({ name: 'bash' }, 'read')).toBe(false)
|
||||
})
|
||||
|
||||
test('matches alias', () => {
|
||||
expect(toolMatchesName({ name: 'bash', aliases: ['shell', 'sh'] }, 'shell')).toBe(true)
|
||||
expect(toolMatchesName({ name: 'bash', aliases: ['shell', 'sh'] }, 'sh')).toBe(true)
|
||||
})
|
||||
|
||||
test('handles empty aliases', () => {
|
||||
expect(toolMatchesName({ name: 'bash', aliases: [] }, 'bash')).toBe(true)
|
||||
expect(toolMatchesName({ name: 'bash', aliases: [] }, 'shell')).toBe(false)
|
||||
})
|
||||
|
||||
test('handles undefined aliases', () => {
|
||||
expect(toolMatchesName({ name: 'bash' }, 'bash')).toBe(true)
|
||||
expect(toolMatchesName({ name: 'bash' }, 'shell')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('findToolByName', () => {
|
||||
const tools: Tools = [
|
||||
{ name: 'bash' } as CoreTool,
|
||||
{ name: 'read', aliases: ['cat'] } as CoreTool,
|
||||
{ name: 'write', aliases: ['edit'] } as CoreTool,
|
||||
]
|
||||
|
||||
test('finds tool by primary name', () => {
|
||||
expect(findToolByName(tools, 'bash')?.name).toBe('bash')
|
||||
})
|
||||
|
||||
test('finds tool by alias', () => {
|
||||
expect(findToolByName(tools, 'cat')?.name).toBe('read')
|
||||
expect(findToolByName(tools, 'edit')?.name).toBe('write')
|
||||
})
|
||||
|
||||
test('returns undefined for unknown name', () => {
|
||||
expect(findToolByName(tools, 'unknown')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('handles empty tools array', () => {
|
||||
expect(findToolByName([], 'bash')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('returns first match for duplicate names', () => {
|
||||
const dupTools: Tools = [
|
||||
{ name: 'tool', aliases: ['a'] } as CoreTool,
|
||||
{ name: 'tool', aliases: ['b'] } as CoreTool,
|
||||
]
|
||||
const found = findToolByName(dupTools, 'tool')
|
||||
expect(found).toBeDefined()
|
||||
expect(found!.aliases).toContain('a')
|
||||
})
|
||||
})
|
||||
18
packages/agent-tools/src/index.ts
Normal file
18
packages/agent-tools/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// agent-tools — Tool interface definitions and registry utilities
|
||||
// Pure types + pure functions, zero runtime dependencies
|
||||
|
||||
export type {
|
||||
AnyObject,
|
||||
ToolInputJSONSchema,
|
||||
ToolProgressData,
|
||||
ToolProgress,
|
||||
ToolCallProgress,
|
||||
ToolResult,
|
||||
ValidationResult,
|
||||
PermissionResult,
|
||||
CoreTool,
|
||||
Tool,
|
||||
Tools,
|
||||
} from './types.js'
|
||||
|
||||
export { findToolByName, toolMatchesName } from './registry.js'
|
||||
21
packages/agent-tools/src/registry.ts
Normal file
21
packages/agent-tools/src/registry.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CoreTool, Tools } from './types.js'
|
||||
|
||||
/**
|
||||
* Checks if a tool matches the given name (primary name or alias).
|
||||
*/
|
||||
export function toolMatchesName(
|
||||
tool: { name: string; aliases?: string[] },
|
||||
name: string,
|
||||
): boolean {
|
||||
return tool.name === name || (tool.aliases?.includes(name) ?? false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a tool by name or alias from a list of tools.
|
||||
*/
|
||||
export function findToolByName(
|
||||
tools: Tools,
|
||||
name: string,
|
||||
): CoreTool | undefined {
|
||||
return tools.find(t => toolMatchesName(t, name))
|
||||
}
|
||||
221
packages/agent-tools/src/types.ts
Normal file
221
packages/agent-tools/src/types.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
// agent-tools — Core Tool interface definitions
|
||||
// Protocol-level types, independent of any host framework
|
||||
|
||||
import type { z } from 'zod/v4'
|
||||
|
||||
// ============================================================================
|
||||
// Schema types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Zod schema type for any object with string keys.
|
||||
* Used as the Input generic constraint for Tool.
|
||||
*/
|
||||
export type AnyObject = z.ZodType<{ [key: string]: unknown }>
|
||||
|
||||
/**
|
||||
* JSON Schema format for MCP tool input schemas.
|
||||
* MCP servers provide this directly instead of Zod schemas.
|
||||
*/
|
||||
export type ToolInputJSONSchema = {
|
||||
[x: string]: unknown
|
||||
type: 'object'
|
||||
properties?: {
|
||||
[x: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Progress types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Progress data from a running tool. Host defines concrete subtypes.
|
||||
* Typed as `any` at the protocol level — the host assigns real shapes.
|
||||
*/
|
||||
export type ToolProgressData = any
|
||||
|
||||
/**
|
||||
* A progress event from a tool execution.
|
||||
*/
|
||||
export type ToolProgress<P extends ToolProgressData = ToolProgressData> = {
|
||||
toolUseID: string
|
||||
data: P
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for receiving progress updates during tool execution.
|
||||
*/
|
||||
export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (
|
||||
progress: ToolProgress<P>,
|
||||
) => void
|
||||
|
||||
// ============================================================================
|
||||
// Result types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Result returned by a tool's call() method.
|
||||
* @template T - The output data type
|
||||
* @template Message - The message type (host-specific, defaults to unknown)
|
||||
*/
|
||||
export type ToolResult<T, Message = unknown> = {
|
||||
data: T
|
||||
newMessages?: Message[]
|
||||
contextModifier?: (context: any) => any
|
||||
/** MCP protocol metadata (structuredContent, _meta) */
|
||||
mcpMeta?: {
|
||||
_meta?: Record<string, unknown>
|
||||
structuredContent?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation & Permission types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Result of tool input validation.
|
||||
*/
|
||||
export type ValidationResult =
|
||||
| { result: true }
|
||||
| { result: false; message: string; errorCode: number }
|
||||
|
||||
/**
|
||||
* Result of a permission check for a tool invocation.
|
||||
*/
|
||||
export type PermissionResult =
|
||||
| { behavior: 'allow'; updatedInput: Record<string, unknown> }
|
||||
| { behavior: 'deny'; message: string }
|
||||
| { behavior: 'passthrough' }
|
||||
|
||||
// ============================================================================
|
||||
// Core Tool interface
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* The host-agnostic core Tool interface.
|
||||
*
|
||||
* This defines the protocol-level contract for any tool — independent of
|
||||
* React rendering, specific context types, or host infrastructure.
|
||||
*
|
||||
* The host (Claude Code) extends this with render methods, richer context
|
||||
* types, and other host-specific features. Host tools structurally satisfy
|
||||
* this interface because they implement all required fields.
|
||||
*
|
||||
* @template Input - Zod schema type for tool input
|
||||
* @template Output - Tool output data type
|
||||
* @template P - Tool progress data type
|
||||
* @template Context - Tool execution context type (host-specific)
|
||||
*/
|
||||
export interface CoreTool<
|
||||
Input extends AnyObject = AnyObject,
|
||||
Output = unknown,
|
||||
P extends ToolProgressData = ToolProgressData,
|
||||
Context = unknown,
|
||||
> {
|
||||
// ── Identity ──
|
||||
readonly name: string
|
||||
aliases?: string[]
|
||||
searchHint?: string
|
||||
|
||||
// ── Schema ──
|
||||
readonly inputSchema: Input
|
||||
readonly inputJSONSchema?: ToolInputJSONSchema
|
||||
outputSchema?: z.ZodType<unknown>
|
||||
|
||||
// ── Execution ──
|
||||
call(
|
||||
args: z.infer<Input>,
|
||||
context: Context,
|
||||
canUseTool: (...args: any[]) => Promise<any>,
|
||||
parentMessage: any,
|
||||
onProgress?: ToolCallProgress<P>,
|
||||
): Promise<ToolResult<Output>>
|
||||
|
||||
// ── Description ──
|
||||
description(
|
||||
input: z.infer<Input>,
|
||||
options: {
|
||||
isNonInteractiveSession: boolean
|
||||
toolPermissionContext: any
|
||||
tools: readonly CoreTool[]
|
||||
},
|
||||
): Promise<string>
|
||||
|
||||
prompt(options: {
|
||||
getToolPermissionContext: () => Promise<any>
|
||||
tools: readonly CoreTool[]
|
||||
agents: any[]
|
||||
allowedAgentTypes?: string[]
|
||||
}): Promise<string>
|
||||
|
||||
// ── Behavioral properties ──
|
||||
isConcurrencySafe(input: z.infer<Input>): boolean
|
||||
isEnabled(): boolean
|
||||
isReadOnly(input: z.infer<Input>): boolean
|
||||
isDestructive?(input: z.infer<Input>): boolean
|
||||
isOpenWorld?(input: z.infer<Input>): boolean
|
||||
interruptBehavior?(): 'cancel' | 'block'
|
||||
requiresUserInteraction?(): boolean
|
||||
|
||||
// ── MCP markers ──
|
||||
isMcp?: boolean
|
||||
isLsp?: boolean
|
||||
readonly shouldDefer?: boolean
|
||||
readonly alwaysLoad?: boolean
|
||||
mcpInfo?: { serverName: string; toolName: string }
|
||||
|
||||
// ── Permissions ──
|
||||
validateInput?(
|
||||
input: z.infer<Input>,
|
||||
context: Context,
|
||||
): Promise<ValidationResult>
|
||||
|
||||
checkPermissions(
|
||||
input: z.infer<Input>,
|
||||
context: Context,
|
||||
): Promise<PermissionResult>
|
||||
|
||||
// ── Utility ──
|
||||
inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
|
||||
getPath?(input: z.infer<Input>): string
|
||||
toAutoClassifierInput(input: z.infer<Input>): unknown
|
||||
backfillObservableInput?(input: Record<string, unknown>): void
|
||||
|
||||
// ── Output ──
|
||||
maxResultSizeChars: number
|
||||
userFacingName(input: Partial<z.infer<Input>> | undefined): string
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: Output,
|
||||
toolUseID: string,
|
||||
): any
|
||||
|
||||
// ── Optional output helpers ──
|
||||
isResultTruncated?(output: Output): boolean
|
||||
getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
|
||||
getActivityDescription?(
|
||||
input: Partial<z.infer<Input>> | undefined,
|
||||
): string | null
|
||||
isTransparentWrapper?(): boolean
|
||||
isSearchOrReadCommand?(input: z.infer<Input>): {
|
||||
isSearch: boolean
|
||||
isRead: boolean
|
||||
isList?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tool with a generic context type.
|
||||
* This is the default export — hosts can specify their own Context type.
|
||||
*/
|
||||
export type Tool<
|
||||
Input extends AnyObject = AnyObject,
|
||||
Output = unknown,
|
||||
P extends ToolProgressData = ToolProgressData,
|
||||
> = CoreTool<Input, Output, P>
|
||||
|
||||
/**
|
||||
* A collection of tools.
|
||||
*/
|
||||
export type Tools = readonly CoreTool[]
|
||||
16
packages/builtin-tools/package.json
Normal file
16
packages/builtin-tools/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@claude-code-best/builtin-tools",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./tools/*": "./src/tools/*",
|
||||
"./utils": "./src/utils.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@claude-code-best/agent-tools": "workspace:*"
|
||||
}
|
||||
}
|
||||
70
packages/builtin-tools/src/index.ts
Normal file
70
packages/builtin-tools/src/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// builtin-tools — All tool implementations for Claude Code
|
||||
// This barrel file re-exports the main tool constants and utilities.
|
||||
// For specific submodules, use deep imports: 'builtin-tools/tools/XTool/XTool.js'
|
||||
|
||||
// =============================================================================
|
||||
// Main tool exports (used by src/tools.ts)
|
||||
// =============================================================================
|
||||
|
||||
// Core tools
|
||||
export { AgentTool } from './tools/AgentTool/AgentTool.js'
|
||||
export { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
|
||||
export { BashTool } from './tools/BashTool/BashTool.js'
|
||||
export { BriefTool } from './tools/BriefTool/BriefTool.js'
|
||||
export { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
|
||||
export { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
|
||||
export { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
|
||||
export { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
||||
export { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
|
||||
export { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
|
||||
export { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
|
||||
export { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
|
||||
export { GlobTool } from './tools/GlobTool/GlobTool.js'
|
||||
export { GrepTool } from './tools/GrepTool/GrepTool.js'
|
||||
export { LSPTool } from './tools/LSPTool/LSPTool.js'
|
||||
export { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
|
||||
export { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
|
||||
export { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
|
||||
export { SkillTool } from './tools/SkillTool/SkillTool.js'
|
||||
export { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
|
||||
export { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
|
||||
export { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
|
||||
export { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
|
||||
export { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
|
||||
export { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
|
||||
export { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
|
||||
export { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
|
||||
|
||||
// Feature-gated tools
|
||||
export { OVERFLOW_TEST_TOOL_NAME } from './tools/OverflowTestTool/OverflowTestTool.js'
|
||||
export { CtxInspectTool } from './tools/CtxInspectTool/CtxInspectTool.js'
|
||||
export { ListPeersTool } from './tools/ListPeersTool/ListPeersTool.js'
|
||||
export { MonitorTool } from './tools/MonitorTool/MonitorTool.js'
|
||||
export { PowerShellTool } from './tools/PowerShellTool/PowerShellTool.js'
|
||||
export { PushNotificationTool } from './tools/PushNotificationTool/PushNotificationTool.js'
|
||||
export { REPLTool } from './tools/REPLTool/REPLTool.js'
|
||||
export { RemoteTriggerTool } from './tools/RemoteTriggerTool/RemoteTriggerTool.js'
|
||||
export { ReviewArtifactTool } from './tools/ReviewArtifactTool/ReviewArtifactTool.js'
|
||||
export { CronCreateTool } from './tools/ScheduleCronTool/CronCreateTool.js'
|
||||
export { CronDeleteTool } from './tools/ScheduleCronTool/CronDeleteTool.js'
|
||||
export { CronListTool } from './tools/ScheduleCronTool/CronListTool.js'
|
||||
export { SendMessageTool } from './tools/SendMessageTool/SendMessageTool.js'
|
||||
export { SendUserFileTool } from './tools/SendUserFileTool/SendUserFileTool.js'
|
||||
export { SleepTool } from './tools/SleepTool/SleepTool.js'
|
||||
export { SnipTool } from './tools/SnipTool/SnipTool.js'
|
||||
export { SubscribePRTool } from './tools/SubscribePRTool/SubscribePRTool.js'
|
||||
export { SuggestBackgroundPRTool } from './tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js'
|
||||
export { TeamCreateTool } from './tools/TeamCreateTool/TeamCreateTool.js'
|
||||
export { TeamDeleteTool } from './tools/TeamDeleteTool/TeamDeleteTool.js'
|
||||
export { TerminalCaptureTool } from './tools/TerminalCaptureTool/TerminalCaptureTool.js'
|
||||
export { VerifyPlanExecutionTool } from './tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js'
|
||||
export { WebBrowserTool } from './tools/WebBrowserTool/WebBrowserTool.js'
|
||||
export { WorkflowTool } from './tools/WorkflowTool/WorkflowTool.js'
|
||||
export { initBundledWorkflows } from './tools/WorkflowTool/bundled/index.js'
|
||||
export { getWorkflowCommands } from './tools/WorkflowTool/createWorkflowCommand.js'
|
||||
|
||||
// Constants
|
||||
export { SYNTHETIC_OUTPUT_TOOL_NAME, createSyntheticOutputTool } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
|
||||
|
||||
// Shared utilities
|
||||
export { tagMessagesWithToolUseID, getToolUseIDFromParentMessage } from './tools/utils.js'
|
||||
@@ -11,19 +11,19 @@ import { z } from 'zod/v4'
|
||||
import {
|
||||
clearInvokedSkillsForAgent,
|
||||
getSdkAgentProgressSummariesEnabled,
|
||||
} from '../../bootstrap/state.js'
|
||||
} from 'src/bootstrap/state.js'
|
||||
import {
|
||||
enhanceSystemPromptWithEnvDetails,
|
||||
getSystemPrompt,
|
||||
} from '../../constants/prompts.js'
|
||||
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
|
||||
import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
} from 'src/constants/prompts.js'
|
||||
import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'
|
||||
import { startAgentSummarization } from 'src/services/AgentSummary/agentSummary.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { clearDumpState } from '../../services/api/dumpPrompts.js'
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { clearDumpState } from 'src/services/api/dumpPrompts.js'
|
||||
import {
|
||||
completeAgentTask as completeAsyncAgent,
|
||||
createActivityDescriptionResolver,
|
||||
@@ -39,53 +39,53 @@ import {
|
||||
unregisterAgentForeground,
|
||||
updateAgentProgress as updateAsyncAgentProgress,
|
||||
updateProgressFromMessage,
|
||||
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
} from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import {
|
||||
checkRemoteAgentEligibility,
|
||||
formatPreconditionError,
|
||||
getRemoteTaskSessionUrl,
|
||||
registerRemoteAgentTask,
|
||||
type BackgroundRemoteSessionPrecondition,
|
||||
} from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||
import { assembleToolPool } from '../../tools.js'
|
||||
import { asAgentId } from '../../types/ids.js'
|
||||
import { runWithAgentContext, type SubagentContext } from '../../utils/agentContext.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import { getCwd, runWithCwdOverride } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { AbortError, errorMessage, toError } from '../../utils/errors.js'
|
||||
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
} from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||
import { assembleToolPool } from 'src/tools.js'
|
||||
import { asAgentId } from 'src/types/ids.js'
|
||||
import { runWithAgentContext, type SubagentContext } from 'src/utils/agentContext.js'
|
||||
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js'
|
||||
import { getCwd, runWithCwdOverride } from 'src/utils/cwd.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { AbortError, errorMessage, toError } from 'src/utils/errors.js'
|
||||
import type { CacheSafeParams } from 'src/utils/forkedAgent.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import {
|
||||
createUserMessage,
|
||||
extractTextContent,
|
||||
isSyntheticMessage,
|
||||
normalizeMessages,
|
||||
} from '../../utils/messages.js'
|
||||
import { getAgentModel } from '../../utils/model/agent.js'
|
||||
import { permissionModeSchema } from '../../utils/permissions/PermissionMode.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
} from 'src/utils/messages.js'
|
||||
import { getAgentModel } from 'src/utils/model/agent.js'
|
||||
import { permissionModeSchema } from 'src/utils/permissions/PermissionMode.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import {
|
||||
filterDeniedAgents,
|
||||
getDenyRuleForAgent,
|
||||
} from '../../utils/permissions/permissions.js'
|
||||
import { enqueueSdkEvent } from '../../utils/sdkEventQueue.js'
|
||||
import { writeAgentMetadata } from '../../utils/sessionStorage.js'
|
||||
import { sleep } from '../../utils/sleep.js'
|
||||
import { buildEffectiveSystemPrompt } from '../../utils/systemPrompt.js'
|
||||
import { asSystemPrompt } from '../../utils/systemPromptType.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { getParentSessionId, isTeammate } from '../../utils/teammate.js'
|
||||
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
||||
import { teleportToRemote } from '../../utils/teleport.js'
|
||||
import { getAssistantMessageContentLength } from '../../utils/tokens.js'
|
||||
import { createAgentId } from '../../utils/uuid.js'
|
||||
} from 'src/utils/permissions/permissions.js'
|
||||
import { enqueueSdkEvent } from 'src/utils/sdkEventQueue.js'
|
||||
import { writeAgentMetadata } from 'src/utils/sessionStorage.js'
|
||||
import { sleep } from 'src/utils/sleep.js'
|
||||
import { buildEffectiveSystemPrompt } from 'src/utils/systemPrompt.js'
|
||||
import { asSystemPrompt } from 'src/utils/systemPromptType.js'
|
||||
import { getTaskOutputPath } from 'src/utils/task/diskOutput.js'
|
||||
import { getParentSessionId, isTeammate } from 'src/utils/teammate.js'
|
||||
import { isInProcessTeammate } from 'src/utils/teammateContext.js'
|
||||
import { teleportToRemote } from 'src/utils/teleport.js'
|
||||
import { getAssistantMessageContentLength } from 'src/utils/tokens.js'
|
||||
import { createAgentId } from 'src/utils/uuid.js'
|
||||
import {
|
||||
createAgentWorktree,
|
||||
hasWorktreeChanges,
|
||||
removeAgentWorktree,
|
||||
} from '../../utils/worktree.js'
|
||||
} from 'src/utils/worktree.js'
|
||||
import { BASH_TOOL_NAME } from '../BashTool/toolName.js'
|
||||
import { BackgroundHint } from '../BashTool/UI.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
@@ -136,7 +136,7 @@ import {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const proactiveModule =
|
||||
feature('PROACTIVE') || feature('KAIROS')
|
||||
? (require('../../proactive/index.js') as typeof import('../../proactive/index.js'))
|
||||
? (require('src/proactive/index.js') as typeof import('src/proactive/index.js'))
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
@@ -332,7 +332,7 @@ export type RemoteLaunchedOutput = {
|
||||
|
||||
type InternalOutput = Output | TeammateSpawnedOutput | RemoteLaunchedOutput
|
||||
|
||||
import type { AgentToolProgress, ShellProgress } from '../../types/tools.js'
|
||||
import type { AgentToolProgress, ShellProgress } from 'src/types/tools.js'
|
||||
// AgentTool forwards both its own progress events and shell progress
|
||||
// events from the sub-agent so the SDK receives tool_progress updates during bash/powershell runs.
|
||||
export type Progress = AgentToolProgress | ShellProgress
|
||||
@@ -12,37 +12,37 @@ import {
|
||||
} from 'src/components/CtrlOToExpand.js'
|
||||
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
|
||||
import type { z } from 'zod/v4'
|
||||
import { AgentProgressLine } from '../../components/AgentProgressLine.js'
|
||||
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
|
||||
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js'
|
||||
import { Markdown } from '../../components/Markdown.js'
|
||||
import { Message as MessageComponent } from '../../components/Message.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { ToolUseLoader } from '../../components/ToolUseLoader.js'
|
||||
import { AgentProgressLine } from 'src/components/AgentProgressLine.js'
|
||||
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'
|
||||
import { FallbackToolUseRejectedMessage } from 'src/components/FallbackToolUseRejectedMessage.js'
|
||||
import { Markdown } from 'src/components/Markdown.js'
|
||||
import { Message as MessageComponent } from 'src/components/Message.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { ToolUseLoader } from 'src/components/ToolUseLoader.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
|
||||
import { findToolByName, type Tools } from '../../Tool.js'
|
||||
import type { Message, ProgressMessage } from '../../types/message.js'
|
||||
import type { AgentToolProgress } from '../../types/tools.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js'
|
||||
import { findToolByName, type Tools } from 'src/Tool.js'
|
||||
import type { Message, ProgressMessage } from 'src/types/message.js'
|
||||
import type { AgentToolProgress } from 'src/types/tools.js'
|
||||
import { count } from 'src/utils/array.js'
|
||||
import {
|
||||
getSearchOrReadFromContent,
|
||||
getSearchReadSummaryText,
|
||||
} from '../../utils/collapseReadSearch.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import { formatDuration, formatNumber } from '../../utils/format.js'
|
||||
} from 'src/utils/collapseReadSearch.js'
|
||||
import { getDisplayPath } from 'src/utils/file.js'
|
||||
import { formatDuration, formatNumber } from 'src/utils/format.js'
|
||||
import {
|
||||
buildSubagentLookups,
|
||||
createAssistantMessage,
|
||||
EMPTY_LOOKUPS,
|
||||
} from '../../utils/messages.js'
|
||||
import type { ModelAlias } from '../../utils/model/aliases.js'
|
||||
} from 'src/utils/messages.js'
|
||||
import type { ModelAlias } from 'src/utils/model/aliases.js'
|
||||
import {
|
||||
getMainLoopModel,
|
||||
parseUserSpecifiedModel,
|
||||
renderModelName,
|
||||
} from '../../utils/model/model.js'
|
||||
import type { Theme, ThemeName } from '../../utils/theme.js'
|
||||
} from 'src/utils/model/model.js'
|
||||
import type { Theme, ThemeName } from 'src/utils/theme.js'
|
||||
import type {
|
||||
outputSchema,
|
||||
Progress,
|
||||
@@ -1,11 +1,11 @@
|
||||
import { mock, describe, expect, test } from "bun:test";
|
||||
|
||||
// Mock heavy deps
|
||||
mock.module("../../utils/model/agent.js", () => ({
|
||||
mock.module("src/utils/model/agent.js", () => ({
|
||||
getDefaultSubagentModel: () => undefined,
|
||||
}));
|
||||
|
||||
mock.module("../../utils/settings/constants.js", () => ({
|
||||
mock.module("src/utils/settings/constants.js", () => ({
|
||||
getSourceDisplayName: (source: string) => source,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAgentColorMap } from '../../bootstrap/state.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { getAgentColorMap } from 'src/bootstrap/state.js'
|
||||
import type { Theme } from 'src/utils/theme.js'
|
||||
|
||||
export type AgentColorName =
|
||||
| 'red'
|
||||
@@ -3,11 +3,11 @@
|
||||
* Used by both the CLI `claude agents` handler and the interactive `/agents` command.
|
||||
*/
|
||||
|
||||
import { getDefaultSubagentModel } from '../../utils/model/agent.js'
|
||||
import { getDefaultSubagentModel } from 'src/utils/model/agent.js'
|
||||
import {
|
||||
getSourceDisplayName,
|
||||
type SettingSource,
|
||||
} from '../../utils/settings/constants.js'
|
||||
} from 'src/utils/settings/constants.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
type AgentSource = SettingSource | 'built-in' | 'plugin'
|
||||
@@ -1,13 +1,13 @@
|
||||
import { join, normalize, sep } from 'path'
|
||||
import { getProjectRoot } from '../../bootstrap/state.js'
|
||||
import { getProjectRoot } from 'src/bootstrap/state.js'
|
||||
import {
|
||||
buildMemoryPrompt,
|
||||
ensureMemoryDirExists,
|
||||
} from '../../memdir/memdir.js'
|
||||
import { getMemoryBaseDir } from '../../memdir/paths.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { findCanonicalGitRoot } from '../../utils/git.js'
|
||||
import { sanitizePath } from '../../utils/path.js'
|
||||
} from 'src/memdir/memdir.js'
|
||||
import { getMemoryBaseDir } from 'src/memdir/paths.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { findCanonicalGitRoot } from 'src/utils/git.js'
|
||||
import { sanitizePath } from 'src/utils/path.js'
|
||||
|
||||
// Persistent agent memory scope: 'user' (~/.claude/agent-memory/), 'project' (.claude/agent-memory/), or 'local' (.claude/agent-memory-local/)
|
||||
export type AgentMemoryScope = 'user' | 'project' | 'local'
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { z } from 'zod/v4'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import { jsonParse, jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import { type AgentMemoryScope, getAgentMemoryDir } from './agentMemory.js'
|
||||
|
||||
const SNAPSHOT_BASE = 'agent-memory-snapshots'
|
||||
@@ -1,26 +1,26 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { z } from 'zod/v4'
|
||||
import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js'
|
||||
import { clearInvokedSkillsForAgent } from 'src/bootstrap/state.js'
|
||||
import {
|
||||
ALL_AGENT_DISALLOWED_TOOLS,
|
||||
ASYNC_AGENT_ALLOWED_TOOLS,
|
||||
CUSTOM_AGENT_DISALLOWED_TOOLS,
|
||||
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS,
|
||||
} from '../../constants/tools.js'
|
||||
import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
|
||||
} from 'src/constants/tools.js'
|
||||
import { startAgentSummarization } from 'src/services/AgentSummary/agentSummary.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { clearDumpState } from '../../services/api/dumpPrompts.js'
|
||||
import type { AppState } from '../../state/AppState.js'
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { clearDumpState } from 'src/services/api/dumpPrompts.js'
|
||||
import type { AppState } from 'src/state/AppState.js'
|
||||
import type {
|
||||
Tool,
|
||||
ToolPermissionContext,
|
||||
Tools,
|
||||
ToolUseContext,
|
||||
} from '../../Tool.js'
|
||||
import { toolMatchesName } from '../../Tool.js'
|
||||
} from 'src/Tool.js'
|
||||
import { toolMatchesName } from 'src/Tool.js'
|
||||
import {
|
||||
completeAgentTask as completeAsyncAgent,
|
||||
createActivityDescriptionResolver,
|
||||
@@ -34,28 +34,28 @@ import {
|
||||
type ProgressTracker,
|
||||
updateAgentProgress as updateAsyncAgentProgress,
|
||||
updateProgressFromMessage,
|
||||
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { asAgentId } from '../../types/ids.js'
|
||||
import type { Message as MessageType, ContentItem } from '../../types/message.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isInProtectedNamespace } from '../../utils/envUtils.js'
|
||||
import { AbortError, errorMessage } from '../../utils/errors.js'
|
||||
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
} from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { asAgentId } from 'src/types/ids.js'
|
||||
import type { Message as MessageType, ContentItem } from 'src/types/message.js'
|
||||
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { isInProtectedNamespace } from 'src/utils/envUtils.js'
|
||||
import { AbortError, errorMessage } from 'src/utils/errors.js'
|
||||
import type { CacheSafeParams } from 'src/utils/forkedAgent.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import {
|
||||
extractTextContent,
|
||||
getLastAssistantMessage,
|
||||
} from '../../utils/messages.js'
|
||||
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
|
||||
import { permissionRuleValueFromString } from '../../utils/permissions/permissionRuleParser.js'
|
||||
} from 'src/utils/messages.js'
|
||||
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
|
||||
import { permissionRuleValueFromString } from 'src/utils/permissions/permissionRuleParser.js'
|
||||
import {
|
||||
buildTranscriptForClassifier,
|
||||
classifyYoloAction,
|
||||
} from '../../utils/permissions/yoloClassifier.js'
|
||||
import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js'
|
||||
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
||||
import { getTokenCountFromUsage } from '../../utils/tokens.js'
|
||||
} from 'src/utils/permissions/yoloClassifier.js'
|
||||
import { emitTaskProgress as emitTaskProgressEvent } from 'src/utils/task/sdkProgress.js'
|
||||
import { isInProcessTeammate } from 'src/utils/teammateContext.js'
|
||||
import { getTokenCountFromUsage } from 'src/utils/tokens.js'
|
||||
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js'
|
||||
import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
|
||||
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
|
||||
import { SEND_MESSAGE_TOOL_NAME } from 'src/tools/SendMessageTool/constants.js'
|
||||
import { WEB_FETCH_TOOL_NAME } from 'src/tools/WebFetchTool/prompt.js'
|
||||
import { WEB_SEARCH_TOOL_NAME } from 'src/tools/WebSearchTool/prompt.js'
|
||||
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
|
||||
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
|
||||
import { SEND_MESSAGE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SendMessageTool/constants.js'
|
||||
import { WEB_FETCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WebFetchTool/prompt.js'
|
||||
import { WEB_SEARCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WebSearchTool/prompt.js'
|
||||
import { isUsing3PServices } from 'src/utils/auth.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { getSettings_DEPRECATED } from 'src/utils/settings/settings.js'
|
||||
import { jsonStringify } from '../../../utils/slowOperations.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import type {
|
||||
AgentDefinition,
|
||||
BuiltInAgentDefinition,
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
|
||||
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/constants.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { AGENT_TOOL_NAME } from '../constants.js'
|
||||
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
|
||||
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/constants.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { AGENT_TOOL_NAME } from '../constants.js'
|
||||
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
|
||||
import { WEB_FETCH_TOOL_NAME } from 'src/tools/WebFetchTool/prompt.js'
|
||||
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileEditTool/constants.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileWriteTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/constants.js'
|
||||
import { WEB_FETCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WebFetchTool/prompt.js'
|
||||
import { AGENT_TOOL_NAME } from '../constants.js'
|
||||
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { CLAUDE_CODE_GUIDE_AGENT } from './built-in/claudeCodeGuideAgent.js'
|
||||
import { EXPLORE_AGENT } from './built-in/exploreAgent.js'
|
||||
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
|
||||
@@ -36,7 +36,7 @@ export function getBuiltInAgents(): AgentDefinition[] {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { getCoordinatorAgents } =
|
||||
require('../../coordinator/workerAgent.js') as typeof import('../../coordinator/workerAgent.js')
|
||||
require('src/coordinator/workerAgent.js') as typeof import('src/coordinator/workerAgent.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return getCoordinatorAgents()
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { BetaToolUseBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
|
||||
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
||||
import {
|
||||
FORK_BOILERPLATE_TAG,
|
||||
FORK_DIRECTIVE_PREFIX,
|
||||
} from '../../constants/xml.js'
|
||||
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
|
||||
} from 'src/constants/xml.js'
|
||||
import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Message as MessageType,
|
||||
} from '../../types/message.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
} from 'src/types/message.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { createUserMessage } from 'src/utils/messages.js'
|
||||
import type { BuiltInAgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
/**
|
||||
@@ -3,41 +3,41 @@ import memoize from 'lodash-es/memoize.js'
|
||||
import { basename } from 'path'
|
||||
import type { SettingSource } from 'src/utils/settings/constants.js'
|
||||
import { z } from 'zod/v4'
|
||||
import { isAutoMemoryEnabled } from '../../memdir/paths.js'
|
||||
import { isAutoMemoryEnabled } from 'src/memdir/paths.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
} from 'src/services/analytics/index.js'
|
||||
import {
|
||||
type McpServerConfig,
|
||||
McpServerConfigSchema,
|
||||
} from '../../services/mcp/types.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
} from 'src/services/mcp/types.js'
|
||||
import type { ToolUseContext } from 'src/Tool.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import {
|
||||
EFFORT_LEVELS,
|
||||
type EffortValue,
|
||||
parseEffortValue,
|
||||
} from '../../utils/effort.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { parsePositiveIntFromFrontmatter } from '../../utils/frontmatterParser.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
} from 'src/utils/effort.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { parsePositiveIntFromFrontmatter } from 'src/utils/frontmatterParser.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import { logError } from 'src/utils/log.js'
|
||||
import {
|
||||
loadMarkdownFilesForSubdir,
|
||||
parseAgentToolsFromFrontmatter,
|
||||
parseSlashCommandToolsFromFrontmatter,
|
||||
} from '../../utils/markdownConfigLoader.js'
|
||||
} from 'src/utils/markdownConfigLoader.js'
|
||||
import {
|
||||
PERMISSION_MODES,
|
||||
type PermissionMode,
|
||||
} from '../../utils/permissions/PermissionMode.js'
|
||||
} from 'src/utils/permissions/PermissionMode.js'
|
||||
import {
|
||||
clearPluginAgentCache,
|
||||
loadPluginAgents,
|
||||
} from '../../utils/plugins/loadPluginAgents.js'
|
||||
import { HooksSchema, type HooksSettings } from '../../utils/settings/types.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
} from 'src/utils/plugins/loadPluginAgents.js'
|
||||
import { HooksSchema, type HooksSettings } from 'src/utils/settings/types.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js'
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { getSubscriptionType } from '../../utils/auth.js'
|
||||
import { hasEmbeddedSearchTools } from '../../utils/embeddedTools.js'
|
||||
import { isEnvDefinedFalsy, isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isTeammate } from '../../utils/teammate.js'
|
||||
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { getSubscriptionType } from 'src/utils/auth.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { isEnvDefinedFalsy, isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { isTeammate } from 'src/utils/teammate.js'
|
||||
import { isInProcessTeammate } from 'src/utils/teammateContext.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from '../GlobTool/prompt.js'
|
||||
@@ -1,32 +1,32 @@
|
||||
import { promises as fsp } from 'fs'
|
||||
import { getSdkAgentProgressSummariesEnabled } from '../../bootstrap/state.js'
|
||||
import { getSystemPrompt } from '../../constants/prompts.js'
|
||||
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
|
||||
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { registerAsyncAgent } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { assembleToolPool } from '../../tools.js'
|
||||
import { asAgentId } from '../../types/ids.js'
|
||||
import { runWithAgentContext } from '../../utils/agentContext.js'
|
||||
import { runWithCwdOverride } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { getSdkAgentProgressSummariesEnabled } from 'src/bootstrap/state.js'
|
||||
import { getSystemPrompt } from 'src/constants/prompts.js'
|
||||
import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'
|
||||
import type { CanUseToolFn } from 'src/hooks/useCanUseTool.js'
|
||||
import type { ToolUseContext } from 'src/Tool.js'
|
||||
import { registerAsyncAgent } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { assembleToolPool } from 'src/tools.js'
|
||||
import { asAgentId } from 'src/types/ids.js'
|
||||
import { runWithAgentContext } from 'src/utils/agentContext.js'
|
||||
import { runWithCwdOverride } from 'src/utils/cwd.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import {
|
||||
createUserMessage,
|
||||
filterOrphanedThinkingOnlyMessages,
|
||||
filterUnresolvedToolUses,
|
||||
filterWhitespaceOnlyAssistantMessages,
|
||||
} from '../../utils/messages.js'
|
||||
import { getAgentModel } from '../../utils/model/agent.js'
|
||||
import { getQuerySourceForAgent } from '../../utils/promptCategory.js'
|
||||
} from 'src/utils/messages.js'
|
||||
import { getAgentModel } from 'src/utils/model/agent.js'
|
||||
import { getQuerySourceForAgent } from 'src/utils/promptCategory.js'
|
||||
import {
|
||||
getAgentTranscript,
|
||||
readAgentMetadata,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { buildEffectiveSystemPrompt } from '../../utils/systemPrompt.js'
|
||||
import type { SystemPrompt } from '../../utils/systemPromptType.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { getParentSessionId } from '../../utils/teammate.js'
|
||||
import { reconstructForSubagentResume } from '../../utils/toolResultStorage.js'
|
||||
} from 'src/utils/sessionStorage.js'
|
||||
import { buildEffectiveSystemPrompt } from 'src/utils/systemPrompt.js'
|
||||
import type { SystemPrompt } from 'src/utils/systemPromptType.js'
|
||||
import { getTaskOutputPath } from 'src/utils/task/diskOutput.js'
|
||||
import { getParentSessionId } from 'src/utils/teammate.js'
|
||||
import { reconstructForSubagentResume } from 'src/utils/toolResultStorage.js'
|
||||
import { runAsyncAgentLifecycle } from './agentToolUtils.js'
|
||||
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
|
||||
import { FORK_AGENT, isForkSubagentEnabled } from './forkSubagent.js'
|
||||
@@ -3,32 +3,32 @@ import type { UUID } from 'crypto'
|
||||
import { randomUUID } from 'crypto'
|
||||
import uniqBy from 'lodash-es/uniqBy.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { getProjectRoot, getSessionId } from '../../bootstrap/state.js'
|
||||
import { getCommand, getSkillToolCommands, hasCommand } from '../../commands.js'
|
||||
import { getProjectRoot, getSessionId } from 'src/bootstrap/state.js'
|
||||
import { getCommand, getSkillToolCommands, hasCommand } from 'src/commands.js'
|
||||
import {
|
||||
DEFAULT_AGENT_PROMPT,
|
||||
enhanceSystemPromptWithEnvDetails,
|
||||
} from '../../constants/prompts.js'
|
||||
import type { QuerySource } from '../../constants/querySource.js'
|
||||
import { getSystemContext, getUserContext } from '../../context.js'
|
||||
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
||||
import { query } from '../../query.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
|
||||
import { cleanupAgentTracking } from '../../services/api/promptCacheBreakDetection.js'
|
||||
} from 'src/constants/prompts.js'
|
||||
import type { QuerySource } from 'src/constants/querySource.js'
|
||||
import { getSystemContext, getUserContext } from 'src/context.js'
|
||||
import type { CanUseToolFn } from 'src/hooks/useCanUseTool.js'
|
||||
import { query } from 'src/query.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js'
|
||||
import { cleanupAgentTracking } from 'src/services/api/promptCacheBreakDetection.js'
|
||||
import {
|
||||
connectToServer,
|
||||
fetchToolsForClient,
|
||||
} from '../../services/mcp/client.js'
|
||||
import { getMcpConfigByName } from '../../services/mcp/config.js'
|
||||
} from 'src/services/mcp/client.js'
|
||||
import { getMcpConfigByName } from 'src/services/mcp/config.js'
|
||||
import type {
|
||||
MCPServerConnection,
|
||||
ScopedMcpServerConfig,
|
||||
} from '../../services/mcp/types.js'
|
||||
import type { Tool, Tools, ToolUseContext } from '../../Tool.js'
|
||||
import { killShellTasksForAgent } from '../../tasks/LocalShellTask/killShellTasks.js'
|
||||
import type { Command } from '../../types/command.js'
|
||||
import type { AgentId } from '../../types/ids.js'
|
||||
} from 'src/services/mcp/types.js'
|
||||
import type { Tool, Tools, ToolUseContext } from 'src/Tool.js'
|
||||
import { killShellTasksForAgent } from 'src/tasks/LocalShellTask/killShellTasks.js'
|
||||
import type { Command } from 'src/types/command.js'
|
||||
import type { AgentId } from 'src/types/ids.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Message,
|
||||
@@ -39,52 +39,52 @@ import type {
|
||||
TombstoneMessage,
|
||||
ToolUseSummaryMessage,
|
||||
UserMessage,
|
||||
} from '../../types/message.js'
|
||||
import { createAttachmentMessage } from '../../utils/attachments.js'
|
||||
import { AbortError } from '../../utils/errors.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
} from 'src/types/message.js'
|
||||
import { createAttachmentMessage } from 'src/utils/attachments.js'
|
||||
import { AbortError } from 'src/utils/errors.js'
|
||||
import { getDisplayPath } from 'src/utils/file.js'
|
||||
import {
|
||||
cloneFileStateCache,
|
||||
createFileStateCacheWithSizeLimit,
|
||||
READ_FILE_STATE_CACHE_SIZE,
|
||||
} from '../../utils/fileStateCache.js'
|
||||
} from 'src/utils/fileStateCache.js'
|
||||
import {
|
||||
type CacheSafeParams,
|
||||
createSubagentContext,
|
||||
} from '../../utils/forkedAgent.js'
|
||||
import { registerFrontmatterHooks } from '../../utils/hooks/registerFrontmatterHooks.js'
|
||||
import { clearSessionHooks } from '../../utils/hooks/sessionHooks.js'
|
||||
import { executeSubagentStartHooks } from '../../utils/hooks.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
import { getAgentModel } from '../../utils/model/agent.js'
|
||||
import { getAPIProvider } from '../../utils/model/providers.js'
|
||||
} from 'src/utils/forkedAgent.js'
|
||||
import { registerFrontmatterHooks } from 'src/utils/hooks/registerFrontmatterHooks.js'
|
||||
import { clearSessionHooks } from 'src/utils/hooks/sessionHooks.js'
|
||||
import { executeSubagentStartHooks } from 'src/utils/hooks.js'
|
||||
import { createUserMessage } from 'src/utils/messages.js'
|
||||
import { getAgentModel } from 'src/utils/model/agent.js'
|
||||
import { getAPIProvider } from 'src/utils/model/providers.js'
|
||||
import {
|
||||
createSubagentTrace,
|
||||
endTrace,
|
||||
isLangfuseEnabled,
|
||||
} from '../../services/langfuse/index.js'
|
||||
import type { ModelAlias } from '../../utils/model/aliases.js'
|
||||
} from 'src/services/langfuse/index.js'
|
||||
import type { ModelAlias } from 'src/utils/model/aliases.js'
|
||||
import {
|
||||
clearAgentTranscriptSubdir,
|
||||
recordSidechainTranscript,
|
||||
setAgentTranscriptSubdir,
|
||||
writeAgentMetadata,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
} from 'src/utils/sessionStorage.js'
|
||||
import {
|
||||
isRestrictedToPluginOnly,
|
||||
isSourceAdminTrusted,
|
||||
} from '../../utils/settings/pluginOnlyPolicy.js'
|
||||
} from 'src/utils/settings/pluginOnlyPolicy.js'
|
||||
import {
|
||||
asSystemPrompt,
|
||||
type SystemPrompt,
|
||||
} from '../../utils/systemPromptType.js'
|
||||
} from 'src/utils/systemPromptType.js'
|
||||
import {
|
||||
isPerfettoTracingEnabled,
|
||||
registerAgent as registerPerfettoAgent,
|
||||
unregisterAgent as unregisterPerfettoAgent,
|
||||
} from '../../utils/telemetry/perfettoTracing.js'
|
||||
import type { ContentReplacementState } from '../../utils/toolResultStorage.js'
|
||||
import { createAgentId } from '../../utils/uuid.js'
|
||||
} from 'src/utils/telemetry/perfettoTracing.js'
|
||||
import type { ContentReplacementState } from 'src/utils/toolResultStorage.js'
|
||||
import { createAgentId } from 'src/utils/uuid.js'
|
||||
import { resolveAgentTools } from './agentToolUtils.js'
|
||||
import { type AgentDefinition, isBuiltInAgent } from './loadAgentsDir.js'
|
||||
|
||||
@@ -622,7 +622,7 @@ export async function* runAgent({
|
||||
|
||||
// Load all skill contents concurrently and add to initial messages
|
||||
const { formatSkillLoadingMetadata } = await import(
|
||||
'../../utils/processUserInput/processSlashCommand.js'
|
||||
'src/utils/processUserInput/processSlashCommand.js'
|
||||
)
|
||||
const loaded = await Promise.all(
|
||||
validSkills.map(async ({ skillName, skill }) => ({
|
||||
@@ -875,7 +875,7 @@ export async function* runAgent({
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
if (feature('MONITOR_TOOL')) {
|
||||
const mcpMod =
|
||||
require('../../tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('../../tasks/MonitorMcpTask/MonitorMcpTask.js')
|
||||
require('src/tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('src/tasks/MonitorMcpTask/MonitorMcpTask.js')
|
||||
mcpMod.killMonitorMcpTasksForAgent(
|
||||
agentId,
|
||||
toolUseContext.getAppState,
|
||||
@@ -9,9 +9,9 @@ import { BLACK_CIRCLE } from 'src/constants/figures.js'
|
||||
import { getModeColor } from 'src/utils/permissions/PermissionMode.js'
|
||||
import { z } from 'zod/v4'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import type { Tool } from 'src/Tool.js'
|
||||
import { buildTool, type ToolDef } from 'src/Tool.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import {
|
||||
ASK_USER_QUESTION_TOOL_CHIP_WIDTH,
|
||||
ASK_USER_QUESTION_TOOL_NAME,
|
||||
@@ -10,70 +10,70 @@ import * as React from 'react'
|
||||
import type { CanUseToolFn } from 'src/hooks/useCanUseTool.js'
|
||||
import type { AppState } from 'src/state/AppState.js'
|
||||
import { z } from 'zod/v4'
|
||||
import { getKairosActive } from '../../bootstrap/state.js'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
|
||||
import { getKairosActive } from 'src/bootstrap/state.js'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { notifyVscodeFileUpdated } from '../../services/mcp/vscodeSdkMcp.js'
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { notifyVscodeFileUpdated } from 'src/services/mcp/vscodeSdkMcp.js'
|
||||
import type {
|
||||
SetToolJSXFn,
|
||||
ToolCallProgress,
|
||||
ToolUseContext,
|
||||
ValidationResult,
|
||||
} from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
} from 'src/Tool.js'
|
||||
import { buildTool, type ToolDef } from 'src/Tool.js'
|
||||
import {
|
||||
backgroundExistingForegroundTask,
|
||||
markTaskNotified,
|
||||
registerForeground,
|
||||
spawnShellTask,
|
||||
unregisterForeground,
|
||||
} from '../../tasks/LocalShellTask/LocalShellTask.js'
|
||||
import type { AgentId } from '../../types/ids.js'
|
||||
import type { AssistantMessage } from '../../types/message.js'
|
||||
import { parseForSecurity } from '../../utils/bash/ast.js'
|
||||
} from 'src/tasks/LocalShellTask/LocalShellTask.js'
|
||||
import type { AgentId } from 'src/types/ids.js'
|
||||
import type { AssistantMessage } from 'src/types/message.js'
|
||||
import { parseForSecurity } from 'src/utils/bash/ast.js'
|
||||
import {
|
||||
splitCommand_DEPRECATED,
|
||||
splitCommandWithOperators,
|
||||
} from '../../utils/bash/commands.js'
|
||||
import { extractClaudeCodeHints } from '../../utils/claudeCodeHints.js'
|
||||
import { detectCodeIndexingFromCommand } from '../../utils/codeIndexing.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isENOENT, ShellError } from '../../utils/errors.js'
|
||||
} from 'src/utils/bash/commands.js'
|
||||
import { extractClaudeCodeHints } from 'src/utils/claudeCodeHints.js'
|
||||
import { detectCodeIndexingFromCommand } from 'src/utils/codeIndexing.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { isENOENT, ShellError } from 'src/utils/errors.js'
|
||||
import {
|
||||
detectFileEncoding,
|
||||
detectLineEndings,
|
||||
getFileModificationTime,
|
||||
writeTextContent,
|
||||
} from '../../utils/file.js'
|
||||
} from 'src/utils/file.js'
|
||||
import {
|
||||
fileHistoryEnabled,
|
||||
fileHistoryTrackEdit,
|
||||
} from '../../utils/fileHistory.js'
|
||||
import { truncate } from '../../utils/format.js'
|
||||
import { getFsImplementation } from '../../utils/fsOperations.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { expandPath } from '../../utils/path.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import { maybeRecordPluginHint } from '../../utils/plugins/hintRecommendation.js'
|
||||
import { exec } from '../../utils/Shell.js'
|
||||
import type { ExecResult } from '../../utils/ShellCommand.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { semanticBoolean } from '../../utils/semanticBoolean.js'
|
||||
import { semanticNumber } from '../../utils/semanticNumber.js'
|
||||
import { EndTruncatingAccumulator } from '../../utils/stringUtils.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { TaskOutput } from '../../utils/task/TaskOutput.js'
|
||||
import { isOutputLineTruncated } from '../../utils/terminal.js'
|
||||
} from 'src/utils/fileHistory.js'
|
||||
import { truncate } from 'src/utils/format.js'
|
||||
import { getFsImplementation } from 'src/utils/fsOperations.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import { expandPath } from 'src/utils/path.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { maybeRecordPluginHint } from 'src/utils/plugins/hintRecommendation.js'
|
||||
import { exec } from 'src/utils/Shell.js'
|
||||
import type { ExecResult } from 'src/utils/ShellCommand.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import { semanticBoolean } from 'src/utils/semanticBoolean.js'
|
||||
import { semanticNumber } from 'src/utils/semanticNumber.js'
|
||||
import { EndTruncatingAccumulator } from 'src/utils/stringUtils.js'
|
||||
import { getTaskOutputPath } from 'src/utils/task/diskOutput.js'
|
||||
import { TaskOutput } from 'src/utils/task/TaskOutput.js'
|
||||
import { isOutputLineTruncated } from 'src/utils/terminal.js'
|
||||
import {
|
||||
buildLargeToolResultMessage,
|
||||
ensureToolResultsDir,
|
||||
generatePreview,
|
||||
getToolResultPath,
|
||||
PREVIEW_SIZE_BYTES,
|
||||
} from '../../utils/toolResultStorage.js'
|
||||
} from 'src/utils/toolResultStorage.js'
|
||||
import { userFacingName as fileEditUserFacingName } from '../FileEditTool/UI.js'
|
||||
import { trackGitOperations } from '../shared/gitOperationTracking.js'
|
||||
import {
|
||||
@@ -506,9 +506,9 @@ type OutputSchema = ReturnType<typeof outputSchema>
|
||||
export type Out = z.infer<OutputSchema>
|
||||
|
||||
// Re-export BashProgress from centralized types to break import cycles
|
||||
export type { BashProgress } from '../../types/tools.js'
|
||||
export type { BashProgress } from 'src/types/tools.js'
|
||||
|
||||
import type { BashProgress } from '../../types/tools.js'
|
||||
import type { BashProgress } from 'src/types/tools.js'
|
||||
|
||||
/**
|
||||
* Checks if a command is allowed to be automatically backgrounded
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js'
|
||||
import { KeyboardShortcutHint } from '@anthropic/ink'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { OutputLine } from '../../components/shell/OutputLine.js'
|
||||
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { OutputLine } from 'src/components/shell/OutputLine.js'
|
||||
import { ShellTimeDisplay } from 'src/components/shell/ShellTimeDisplay.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { Out as BashOut } from './BashTool.js'
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { KeyboardShortcutHint } from '@anthropic/ink'
|
||||
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js'
|
||||
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { ShellProgressMessage } from 'src/components/shell/ShellProgressMessage.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
import { useAppStateStore, useSetAppState } from '../../state/AppState.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { backgroundAll } from '../../tasks/LocalShellTask/LocalShellTask.js'
|
||||
import type { ProgressMessage } from '../../types/message.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
import { useKeybinding } from 'src/keybindings/useKeybinding.js'
|
||||
import { useShortcutDisplay } from 'src/keybindings/useShortcutDisplay.js'
|
||||
import { useAppStateStore, useSetAppState } from 'src/state/AppState.js'
|
||||
import type { Tool } from 'src/Tool.js'
|
||||
import { backgroundAll } from 'src/tasks/LocalShellTask/LocalShellTask.js'
|
||||
import type { ProgressMessage } from 'src/types/message.js'
|
||||
import { env } from 'src/utils/env.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { getDisplayPath } from 'src/utils/file.js'
|
||||
import { isFullscreenEnvEnabled } from 'src/utils/fullscreen.js'
|
||||
import type { ThemeName } from 'src/utils/theme.js'
|
||||
import type { BashProgress, BashToolInput, Out } from './BashTool.js'
|
||||
import BashToolResultMessage from './BashToolResultMessage.js'
|
||||
import { extractBashCommentLabel } from './commentLabel.js'
|
||||
@@ -2,16 +2,16 @@ import type { z } from 'zod/v4'
|
||||
import {
|
||||
isUnsafeCompoundCommand_DEPRECATED,
|
||||
splitCommand_DEPRECATED,
|
||||
} from '../../utils/bash/commands.js'
|
||||
} from 'src/utils/bash/commands.js'
|
||||
import {
|
||||
buildParsedCommandFromRoot,
|
||||
type IParsedCommand,
|
||||
ParsedCommand,
|
||||
} from '../../utils/bash/ParsedCommand.js'
|
||||
import { type Node, PARSE_ABORTED } from '../../utils/bash/parser.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { createPermissionRequestMessage } from '../../utils/permissions/permissions.js'
|
||||
} from 'src/utils/bash/ParsedCommand.js'
|
||||
import { type Node, PARSE_ABORTED } from 'src/utils/bash/parser.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import type { PermissionUpdate } from 'src/utils/permissions/PermissionUpdateSchema.js'
|
||||
import { createPermissionRequestMessage } from 'src/utils/permissions/permissions.js'
|
||||
import { BashTool } from './BashTool.js'
|
||||
import { bashCommandIsSafeAsync_DEPRECATED } from './bashSecurity.js'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { APIUserAbortError } from '@anthropic-ai/sdk'
|
||||
import type { z } from 'zod/v4'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import type { ToolPermissionContext, ToolUseContext } from '../../Tool.js'
|
||||
import type { PendingClassifierCheck } from '../../types/permissions.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
} from 'src/services/analytics/index.js'
|
||||
import type { ToolPermissionContext, ToolUseContext } from 'src/Tool.js'
|
||||
import type { PendingClassifierCheck } from 'src/types/permissions.js'
|
||||
import { count } from 'src/utils/array.js'
|
||||
import {
|
||||
checkSemantics,
|
||||
nodeTypeId,
|
||||
@@ -16,45 +16,45 @@ import {
|
||||
parseForSecurityFromAst,
|
||||
type Redirect,
|
||||
type SimpleCommand,
|
||||
} from '../../utils/bash/ast.js'
|
||||
} from 'src/utils/bash/ast.js'
|
||||
import {
|
||||
type CommandPrefixResult,
|
||||
extractOutputRedirections,
|
||||
getCommandSubcommandPrefix,
|
||||
splitCommand_DEPRECATED,
|
||||
} from '../../utils/bash/commands.js'
|
||||
import { parseCommandRaw } from '../../utils/bash/parser.js'
|
||||
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { AbortError } from '../../utils/errors.js'
|
||||
} from 'src/utils/bash/commands.js'
|
||||
import { parseCommandRaw } from 'src/utils/bash/parser.js'
|
||||
import { tryParseShellCommand } from 'src/utils/bash/shellQuote.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { AbortError } from 'src/utils/errors.js'
|
||||
import type {
|
||||
ClassifierBehavior,
|
||||
ClassifierResult,
|
||||
} from '../../utils/permissions/bashClassifier.js'
|
||||
} from 'src/utils/permissions/bashClassifier.js'
|
||||
import {
|
||||
classifyBashCommand,
|
||||
getBashPromptAllowDescriptions,
|
||||
getBashPromptAskDescriptions,
|
||||
getBashPromptDenyDescriptions,
|
||||
isClassifierPermissionsEnabled,
|
||||
} from '../../utils/permissions/bashClassifier.js'
|
||||
} from 'src/utils/permissions/bashClassifier.js'
|
||||
import type {
|
||||
PermissionDecisionReason,
|
||||
PermissionResult,
|
||||
} from '../../utils/permissions/PermissionResult.js'
|
||||
} from 'src/utils/permissions/PermissionResult.js'
|
||||
import type {
|
||||
PermissionRule,
|
||||
PermissionRuleValue,
|
||||
} from '../../utils/permissions/PermissionRule.js'
|
||||
import { extractRules } from '../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
|
||||
} from 'src/utils/permissions/PermissionRule.js'
|
||||
import { extractRules } from 'src/utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from 'src/utils/permissions/PermissionUpdateSchema.js'
|
||||
import { permissionRuleValueToString } from 'src/utils/permissions/permissionRuleParser.js'
|
||||
import {
|
||||
createPermissionRequestMessage,
|
||||
getRuleByContentsForTool,
|
||||
} from '../../utils/permissions/permissions.js'
|
||||
} from 'src/utils/permissions/permissions.js'
|
||||
import {
|
||||
parsePermissionRule,
|
||||
type ShellPermissionRule,
|
||||
@@ -62,11 +62,11 @@ import {
|
||||
permissionRuleExtractPrefix as sharedPermissionRuleExtractPrefix,
|
||||
suggestionForExactCommand as sharedSuggestionForExactCommand,
|
||||
suggestionForPrefix as sharedSuggestionForPrefix,
|
||||
} from '../../utils/permissions/shellRuleMatching.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { windowsPathToPosixPath } from '../../utils/windowsPaths.js'
|
||||
} from 'src/utils/permissions/shellRuleMatching.js'
|
||||
import { getPlatform } from 'src/utils/platform.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import { windowsPathToPosixPath } from 'src/utils/windowsPaths.js'
|
||||
import { BashTool } from './BashTool.js'
|
||||
import { checkCommandOperatorPermissions } from './bashCommandHelpers.js'
|
||||
import {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { extractHeredocs } from '../../utils/bash/heredoc.js'
|
||||
import { ParsedCommand } from '../../utils/bash/ParsedCommand.js'
|
||||
import { extractHeredocs } from 'src/utils/bash/heredoc.js'
|
||||
import { ParsedCommand } from 'src/utils/bash/ParsedCommand.js'
|
||||
import {
|
||||
hasMalformedTokens,
|
||||
hasShellQuoteSingleQuoteBug,
|
||||
tryParseShellCommand,
|
||||
} from '../../utils/bash/shellQuote.js'
|
||||
import type { TreeSitterAnalysis } from '../../utils/bash/treeSitterAnalysis.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
} from 'src/utils/bash/shellQuote.js'
|
||||
import type { TreeSitterAnalysis } from 'src/utils/bash/treeSitterAnalysis.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
|
||||
const HEREDOC_IN_SUBSTITUTION = /\$\(.*<</
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* For example, grep returns 1 when no matches are found, which is not an error condition.
|
||||
*/
|
||||
|
||||
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
||||
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
|
||||
|
||||
export type CommandSemantic = (
|
||||
exitCode: number,
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { z } from 'zod/v4'
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import type { ToolPermissionContext } from 'src/Tool.js'
|
||||
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import type { BashTool } from './BashTool.js'
|
||||
|
||||
const ACCEPT_EDITS_ALLOWED_COMMANDS = [
|
||||
@@ -1,25 +1,25 @@
|
||||
import { homedir } from 'os'
|
||||
import { isAbsolute, resolve } from 'path'
|
||||
import type { z } from 'zod/v4'
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
import type { Redirect, SimpleCommand } from '../../utils/bash/ast.js'
|
||||
import type { ToolPermissionContext } from 'src/Tool.js'
|
||||
import type { Redirect, SimpleCommand } from 'src/utils/bash/ast.js'
|
||||
import {
|
||||
extractOutputRedirections,
|
||||
splitCommand_DEPRECATED,
|
||||
} from '../../utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
||||
import { getDirectoryForPath } from '../../utils/path.js'
|
||||
import { allWorkingDirectories } from '../../utils/permissions/filesystem.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import { createReadRuleSuggestion } from '../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
} from 'src/utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from 'src/utils/bash/shellQuote.js'
|
||||
import { getDirectoryForPath } from 'src/utils/path.js'
|
||||
import { allWorkingDirectories } from 'src/utils/permissions/filesystem.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { createReadRuleSuggestion } from 'src/utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdate } from 'src/utils/permissions/PermissionUpdateSchema.js'
|
||||
import {
|
||||
expandTilde,
|
||||
type FileOperationType,
|
||||
formatDirectoryList,
|
||||
isDangerousRemovalPath,
|
||||
validatePath,
|
||||
} from '../../utils/permissions/pathValidation.js'
|
||||
} from 'src/utils/permissions/pathValidation.js'
|
||||
import type { BashTool } from './BashTool.js'
|
||||
import { stripSafeWrappers } from './bashPermissions.js'
|
||||
import { sedCommandIsAllowedByAllowlist } from './sedValidation.js'
|
||||
@@ -1,20 +1,20 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { prependBullets } from '../../constants/prompts.js'
|
||||
import { getAttributionTexts } from '../../utils/attribution.js'
|
||||
import { hasEmbeddedSearchTools } from '../../utils/embeddedTools.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { shouldIncludeGitInstructions } from '../../utils/gitSettings.js'
|
||||
import { getClaudeTempDir } from '../../utils/permissions/filesystem.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { prependBullets } from 'src/constants/prompts.js'
|
||||
import { getAttributionTexts } from 'src/utils/attribution.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { shouldIncludeGitInstructions } from 'src/utils/gitSettings.js'
|
||||
import { getClaudeTempDir } from 'src/utils/permissions/filesystem.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import {
|
||||
getDefaultBashTimeoutMs,
|
||||
getMaxBashTimeoutMs,
|
||||
} from '../../utils/timeouts.js'
|
||||
} from 'src/utils/timeouts.js'
|
||||
import {
|
||||
getUndercoverInstructions,
|
||||
isUndercover,
|
||||
} from '../../utils/undercover.js'
|
||||
} from 'src/utils/undercover.js'
|
||||
import { AGENT_TOOL_NAME } from '../AgentTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { z } from 'zod/v4'
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js'
|
||||
import { getOriginalCwd } from 'src/bootstrap/state.js'
|
||||
import {
|
||||
extractOutputRedirections,
|
||||
splitCommand_DEPRECATED,
|
||||
} from '../../utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { isCurrentDirectoryBareGitRepo } from '../../utils/git.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
} from 'src/utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from 'src/utils/bash/shellQuote.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { isCurrentDirectoryBareGitRepo } from 'src/utils/git.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { getPlatform } from 'src/utils/platform.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
containsVulnerableUncPath,
|
||||
DOCKER_READ_ONLY_COMMANDS,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
PYRIGHT_READ_ONLY_COMMANDS,
|
||||
RIPGREP_READ_ONLY_COMMANDS,
|
||||
validateFlags,
|
||||
} from '../../utils/shell/readOnlyCommandValidation.js'
|
||||
} from 'src/utils/shell/readOnlyCommandValidation.js'
|
||||
import type { BashTool } from './BashTool.js'
|
||||
import { isNormalizedGitCommand } from './bashPermissions.js'
|
||||
import { bashCommandIsSafe_DEPRECATED } from './bashSecurity.js'
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
||||
import { tryParseShellCommand } from 'src/utils/bash/shellQuote.js'
|
||||
|
||||
// BRE→ERE conversion placeholders (null-byte sentinels, never appear in user input)
|
||||
const BACKSLASH_PLACEHOLDER = '\x00BACKSLASH\x00'
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import type { ToolPermissionContext } from 'src/Tool.js'
|
||||
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
|
||||
import { tryParseShellCommand } from 'src/utils/bash/shellQuote.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
|
||||
/**
|
||||
* Helper: Validate flags against an allowlist
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
||||
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
|
||||
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
|
||||
import { getSettings_DEPRECATED } from 'src/utils/settings/settings.js'
|
||||
import {
|
||||
BINARY_HIJACK_VARS,
|
||||
bashPermissionRule,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user