@autopilot-agent/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,181 +1,53 @@
1
- <!-- 本文件用于 npm CLI 用户了解安装、配置和使用方式。 -->
2
1
  # @autopilot-agent/cli
3
2
 
4
- Autopilot Agent 是一个在终端中使用的 AI 编程助手,可以运行在任何支持开启终端操作的IDE、ADE中;Autopilot 可以帮助你分析项目、修改代码、执行命令和管理会话。你可以通过权限设置和操作审批控制它对项目的访问。
5
-
6
- 安装后使用 `ap` 或 `autopilot` 启动,两者功能相同。
7
-
8
- ## 可以做什么
9
-
10
- - **理解项目**:阅读文件、按文件名或内容搜索代码,梳理项目结构、解释实现、定位问题。输入 `@src/index.ts` 这样的相对路径,可以把指定文件带入当前对话。
11
- - **编写和修改代码**:根据需求创建文件、修改已有代码,帮助修复 Bug、补充测试和整理文档;交互界面会展示文件修改信息。
12
- - **执行开发命令**:运行测试、构建和其他命令,根据输出继续排查;耗时命令可以作为后台任务运行,随时查看输出或请求停止。
13
- - **跟踪任务进度**:将复杂需求拆成待办事项,在执行过程中更新完成状态;任务运行时也可以排队提交下一条需求。
14
- - **继续已有对话**:保存并管理项目会话,在已有会话中继续提问;中断后可恢复的任务可以通过 `ap resume` 继续。
15
- - **管理上下文**:查看当前上下文用量,手动压缩对话;具备有效上下文预算时,也会按阈值自动压缩,帮助继续较长的任务。
16
- - **控制操作权限**:确认项目是否可信,选择权限模式,并审批需要确认的操作;通过 `/undo` 预览并撤销当前会话最近一轮可撤销的文件修改。
17
- - **扩展工具和工作流程**:加载已安装的 Skills,通过 `/skill` 选择使用;配置并授权 MCP 服务后,可以调用其提供的工具。
18
- - **查资料和分析图片**:配置搜索服务后可以联网搜索、读取网页;配置可用的视觉模型后,可以分析项目中的图片,例如界面截图。
19
-
20
- 你可以直接用自然语言提出需求,例如:
21
-
22
- ```text
23
- 分析这个项目的入口和主要模块,先不要修改文件。
24
- 查找登录失败的原因,修复后运行相关测试。
25
- 根据这张UI设计图片,还原设计稿。
26
- ```
27
-
28
- ## 常用交互命令
29
-
30
- 启动 `ap` 后,输入 `/` 可以查看命令建议。
31
-
32
- | 命令 | 用途 |
33
- |---|---|
34
- | `/model`、`/effort` | 选择后续任务使用的模型和思考强度 |
35
- | `/skill` | 查看并选择已安装的 Skills |
36
- | `/permissions` | 查看或切换当前会话的权限模式 |
37
- | `/context`、`/context all` | 查看上下文用量,或展开详细信息 |
38
- | `/compact` | 手动压缩当前上下文 |
39
- | `/jobs` | 查看后台任务,也可查看输出或请求停止 |
40
- | `/queue <需求>` | 将下一条需求加入队列 |
41
- | `/undo` | 预览最近一轮可撤销的文件修改,再确认是否撤销 |
42
- | `/cancel`、`/exit` | 取消当前任务,或退出 CLI |
43
-
44
- ## 安装
45
-
46
- 请先安装 **Node.js >=20.11.0 且 <22** 和 npm,再运行以下命令。安装时需要联网。
3
+ Autopilot 的终端宿主包。普通用户安装主包 `autopilot-agent@0.2.0`,无需分别安装各个 scoped 包:
47
4
 
48
5
  ```bash
49
- npm install -g autopilot-agent
6
+ npm install -g autopilot-agent@0.2.0
50
7
  ap --help
51
8
  ```
52
9
 
53
- 只需安装 `autopilot-agent`,所需依赖会自动安装。
54
-
55
- ## 配置 settings.json
10
+ 完整安装、Anthropic / Chat Completions 配置、使用和限制见 [主包手册](https://github.com/C-Clay-W/autopilot/blob/main/packages/autopilot-agent/README.md),完整 argv / slash 语法见 [CLI 参考](https://github.com/C-Clay-W/autopilot/blob/main/agent/doc/CLI-Reference.md)。开发参考随源码更新,可能先于已发布版本的行为。
56
11
 
57
- 首次使用前,需要填写模型服务地址、API 密钥和模型名称。请在以下位置创建或编辑 `settings.json`:
12
+ ## 结构与职责
58
13
 
59
- | 系统 | 配置文件位置 |
60
- |---|---|
61
- | Windows | `%USERPROFILE%\.autopilot\settings.json` |
62
- | macOS | `~/.autopilot/settings.json` |
14
+ - parser.ts 解析 argv 与交互命令,main.ts 编排入口、输出及退出。
15
+ - composition-root.ts 注入共享 Core Runtime 与 Node Adapters,controller.ts 管理任务、审批与恢复。
16
+ - tui/ 使用 React / Ink;renderer.ts 与 event-projector.ts 支持纯文本和 JSONL。
17
+ - 内部依赖为 Core、Adapters、Presentation,0.2.0 使用匹配的精确版本。
63
18
 
64
- `.autopilot` 目录会随 Autopilot Cli首次启动自动创建,如果 `.autopilot` 目录不存在,可先自行创建。配置文件使用标准 JSON,不支持注释或末尾多余的逗号。按下面的示例填写即可,不添加 `schemaVersion` 字段。
19
+ 本包的 bin 提供 ap/autopilot,主入口包再转发至本包的 dist/main.js,不复制 Runtime。TTY 使用交互界面;显式 --output text/jsonl 用于脚本,JSONL 不输出 ANSI 或交互提示。
65
20
 
66
- ### 使用 Anthropic 或兼容服务
21
+ ## 编程入口
67
22
 
68
- 如果服务支持 Anthropic Messages 协议,使用以下配置,将 `YOUR_API_KEY` 和 `YOUR_MODEL_ID` 替换为你的密钥和模型名称。使用兼容服务时,也需要替换服务地址。
23
+ 包根导出 parseArgv、parseSlashAction、renderHelp、createCliCompositionRoot、createCliController、main、事件投影和 TUI 相关类型。优先把它作为终端应用使用;自行嵌入时需要处理输入输出、Trust、审批和退出清理,不在模块导入时自动启动任务。
69
24
 
70
- ```json
71
- {
72
- "env": {
73
- "ANTHROPIC_BASE_URL": "https://api.anthropic.com",
74
- "ANTHROPIC_API_KEY": "YOUR_API_KEY"
75
- },
76
- "model": "YOUR_MODEL_ID"
77
- }
78
- ```
25
+ ```typescript
26
+ import { parseArgv, renderHelp } from "@autopilot-agent/cli";
79
27
 
80
- 如果服务要求使用 `ANTHROPIC_AUTH_TOKEN`,请用它替换 `ANTHROPIC_API_KEY`,不要同时填写两者。服务地址不要追加 `/v1/messages`,也不要包含用户名、密码、查询参数或 `#` 后的内容。
81
-
82
- ### 使用 OpenAI Chat Completions 兼容服务
83
-
84
- 如果服务支持 OpenAI Chat Completions 协议,使用下面的配置。替换密钥、`baseUrl` 和两处 `YOUR_MODEL_ID`;其余字段可以保留示例写法。
85
-
86
- ```json
87
- {
88
- "env": { "USER_SETTINGS_CUSTOM_API_KEY": "YOUR_API_KEY" },
89
- "providerRoutes": {
90
- "custom": {
91
- "id": "custom",
92
- "apiProtocol": "openai-chat-completions",
93
- "baseUrl": "https://api.example.com/v1",
94
- "credentialRef": { "provider": "custom", "key": "USER_SETTINGS_CUSTOM_API_KEY" },
95
- "models": [{ "id": "YOUR_MODEL_ID" }]
96
- }
97
- },
98
- "model": "custom/YOUR_MODEL_ID"
99
- }
28
+ const parsed = parseArgv(["chat", "解释项目结构", "--output", "text"]);
29
+ console.log(parsed);
30
+ console.log(renderHelp());
100
31
  ```
101
32
 
102
- 不要同时设置 `env.ANTHROPIC_MODEL`,以免覆盖这里选择的模型。
103
-
104
- 需要配置多个服务时,可以在 `providerRoutes` 中增加其他条目。每个条目的名称、`id` 和 `credentialRef.provider` 应一致;`credentialRef.key` 对应 `env` 中的密钥名称。`model` 使用 `服务名称/模型名称` 的形式,模型必须列在对应服务的 `models` 中。协议只支持 `anthropic-messages` 和 `openai-chat-completions`。
105
-
106
- ### 常用可选参数
33
+ ## 源码开发
107
34
 
108
- | 字段 | 用途与写法 |
109
- |---|---|
110
- | `model` | 使用的模型名称,或上面示例中的 `服务名称/模型名称`;建议填写服务商提供的完整模型名称 |
111
- | `availableModels` | 默认连接允许使用的模型列表,例如 `["MODEL_A", "MODEL_B"]`;不能为空、不能重复,当前模型也需要在列表中 |
112
- | `effortLevel` | 思考强度,可选 `"low"`、`"medium"`、`"high"`,具体效果取决于模型支持情况 |
113
- | `alwaysThinkingEnabled` | 是否启用思考,填写 `true` 或 `false`,不要加引号;需要模型支持 |
114
- | `maxOutputTokens` | 单次输出的 token 上限,填写 1–1,000,000 的整数;不是上下文大小 |
115
- | `env.API_TIMEOUT_MS` | 默认模型连接的请求超时,单位毫秒,填写 1,000–600,000 的整数字符串,例如 `"60000"` |
116
- | `visionModel` | 图片分析使用的模型,填写已配置的 Chat Completions 服务中的 `服务名称/模型名称`;`false` 关闭,省略时使用主模型 |
117
- | `webSearch` | `false` 关闭模型联网搜索;启用时需另行配置搜索服务 |
118
-
119
- `env` 中的值都需要写成字符串。填写模型名称和密钥时,避免带入首尾空格或换行。建议通过顶层 `model` 和 `effortLevel` 设置模型与思考强度;`env.ANTHROPIC_MODEL` 和 `env.CLAUDE_CODE_EFFORT_LEVEL` 会覆盖对应设置。
120
-
121
- 服务地址、密钥、模型列表等连接设置请放在上面的个人配置文件中,不要把密钥提交到项目或版本控制。项目也可以提供 `.autopilot/settings.json` 和 `.autopilot/settings.local.json`,在你信任该项目后应用允许的设置;项目设置不能替换个人配置中的连接信息。
122
-
123
- ### 修改配置
124
-
125
- 直接编辑个人 `settings.json` 可以修改完整配置。简单配置也可以通过以下命令修改模型、思考强度和可用模型列表:
35
+ 从 monorepo 根安装依赖,依次构建 Core、Adapters、Presentation,再执行:
126
36
 
127
37
  ```bash
128
- ap config set model YOUR_MODEL_ID --scope user
129
- ap config set effortLevel high --scope user
130
- ap config set availableModels MODEL_A,MODEL_B --scope user
38
+ npm run build -w apps/cli
39
+ node apps/cli/dist/main.js --help
40
+ npm run test -w apps/cli -- test/parser.test.ts
131
41
  ```
132
42
 
133
- 命令中的模型列表用逗号分隔,配置文件中则使用 JSON 数组。如果文件中配置了自定义服务的 `credentialRef`、`maxOutputTokens` 等字段,当前版本可能拒绝上述修改命令,请直接编辑文件。修改后用 `ap doctor` 检查配置。
134
-
135
- ## 开始使用
43
+ build 使用 TypeScript,输出 dist/*.js 与声明,不在 npm 包中发布源码映射和测试。当前 Node 声明为 >=20.11.0 <22;建议整仓构建使用 Node 20.20.2。
136
44
 
137
- 进入要处理的项目目录后运行:
45
+ CLI 使用用户级 settings.json,默认工作区为当前目录,可显式指定 --workspace-root。CLI 不读取开发仓库 Web env,也不因配置中含 RAG 字段而自动启用私有知识库工具。重要项目保留版本控制,OS 沙箱、MCP 和平台限制见主包手册。
138
46
 
139
- ```bash
140
- ap
141
- ```
142
-
143
- 首次打开项目时,按提示确认是否信任该项目。确认后,可以检查配置和模型连接:
144
-
145
- ```bash
146
- ap doctor
147
- ap doctor --connectivity
148
- ```
149
-
150
- `ap doctor` 检查本地配置,`--connectivity` 还会联网检查模型服务。未信任项目时,部分检查会跳过。
151
-
152
- `ap` 会进入交互模式,你可以直接输入需求。也可以提交一次任务:
153
-
154
- ```bash
155
- ap chat "分析当前项目结构,不修改文件"
156
- ```
157
-
158
- 常用管理命令:
159
-
160
- ```bash
161
- ap session list
162
- ap trust status
163
- ap sandbox status
164
- ```
165
-
166
- 查看各命令的具体参数与限制:`ap --help`。更新和卸载:
167
-
168
- ```bash
169
- npm install -g autopilot-agent@latest
170
- npm uninstall -g autopilot-agent
171
- ```
47
+ ## 0.2.0 版本
172
48
 
173
- 卸载后,个人配置和会话数据仍会保留。
49
+ 本包随 npm 0.2.0 发布,内部依赖使用匹配的精确版本。Windows 命令、后台 Job 与 host MCP 逐次审批后以当前用户权限直接执行,审批不提供 OS 隔离;macOS 继续使用 Seatbelt。实机与进程树生命周期仍需按实际验证确认。
174
50
 
175
- ## 当前限制
51
+ ## 许可证
176
52
 
177
- - 当前提供终端使用方式,暂不包含图形界面、内置浏览器预览、自动长期记忆或私有知识库搜索。
178
- - 沙箱功能需要额外的平台支持,可通过 `ap doctor` 和 `ap sandbox status` 查看状态。MCP 工具默认在本机运行,不提供操作系统级隔离。
179
- - Windows 后台任务的子进程可能无法全部停止;沙箱中的 MCP 工具也可能遇到输入无法转发的问题。需要可靠停止的长任务,建议先在测试项目中验证。
180
- - macOS 尚未完成完整功能验证,Linux 暂未验证。模型服务的可用性、限额和费用以服务商为准。
181
- - 当前为早期版本,处理重要项目时,请保留版本控制或备份。
53
+ 公开源码采用 [MIT 许可证](https://github.com/C-Clay-W/autopilot/blob/main/LICENSE)。第三方依赖保留各自许可证。
@@ -53,5 +53,7 @@ export declare function TuiActivityLine(props: {
53
53
  reducedMotion: boolean;
54
54
  animationActive: boolean;
55
55
  clock: TuiAnimationClock;
56
+ startedAt?: number;
57
+ activeForm?: string;
56
58
  }): ReactNode;
57
59
  export {};
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  */
9
9
  import { useEffect, useState } from "react";
10
10
  import { Box, Text } from "ink";
11
+ import { formatTuiDuration } from "./tui/format.js";
11
12
  export const CLI_ACTIVITY_STALLED_AFTER_MS = 20_000;
12
13
  export const CLI_ACTIVITY_TRANSITION_MS = 2_000;
13
14
  /** CC v2.1.88 的默认 spinner glyph;首尾重复保证 ping-pong 转向自然。 */
@@ -124,22 +125,24 @@ export function TuiActivityLine(props) {
124
125
  const [clockTime, setClockTime] = useState(props.clock.now());
125
126
  const view = resolveCliActivityView({ activity: props.activity, status: props.status, now: clockTime, waitingForApproval: props.waitingForApproval });
126
127
  const shouldAnimate = props.animationActive && !props.screenReader && !props.reducedMotion && view !== undefined && view.phase !== "terminal";
128
+ const shouldTick = shouldAnimate || (props.startedAt !== undefined && (props.status === "running" || props.status === "waiting_approval"));
127
129
  useEffect(function subscribeActivityClock() {
128
- props.clock.setActive(shouldAnimate);
129
- if (!shouldAnimate) {
130
+ props.clock.setActive(shouldTick);
131
+ if (!shouldTick) {
130
132
  setClockTime(props.clock.now());
131
133
  return;
132
134
  }
133
135
  setClockTime(props.clock.now());
134
136
  return props.clock.subscribe(function updateActivityClock(time) {
135
- setClockTime(time);
137
+ // 禁动画仍按秒刷新累计时间,复用同一枚时钟。
138
+ setClockTime(function next(previous) { return shouldAnimate || Math.floor(previous / 1000) !== Math.floor(time / 1000) ? time : previous; });
136
139
  });
137
- }, [props.clock, shouldAnimate]);
140
+ }, [props.clock, shouldAnimate, shouldTick]);
138
141
  if (!view)
139
142
  return null;
140
143
  if (view.phase === "terminal")
141
144
  return null;
142
145
  const frame = shouldAnimate ? view.frame : "·";
143
146
  const isStalled = view.phase === "stalled";
144
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { bold: isStalled, color: props.useColor ? "ansi256(208)" : undefined, children: frame }) }), _jsx(Text, { bold: isStalled, dimColor: props.useColor && !isStalled, children: view.label })] }));
147
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { bold: isStalled, color: props.useColor ? "ansi256(208)" : undefined, children: frame }) }), _jsxs(Text, { bold: isStalled, dimColor: props.useColor && !isStalled, children: [props.activeForm ?? view.label, props.startedAt !== undefined ? ` · ${formatTuiDuration(Math.max(0, clockTime - props.startedAt))}` : ""] })] }));
145
148
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { WEB_SEARCH_DATA_FLOW_NOTICE } from "@autopilot-agent/presentation";
8
8
  import { createDefaultModelSamplerFactory, createRuntimeBootstrapService, createSafeBootstrapConfig, WorkspaceTrustError, getMcpServerGrantConstraint, } from "@autopilot-agent/core";
9
- import { createNodeCredentialStore, createNodeCapabilityLoader, createNodeSandboxPort, createNodeSettingsLoader, createNodeSettingsWriter, ensureUserConfigDirectory, createNodeRuntimePorts, createNodeTrustPolicy, createNodeWorkspaceRegistry, createNodeWorkspaceAdapter, createNodeWorkspaceId, getNodeStateCorruptionDiagnostics, getNodeStatePaths, getNodeStateSchemaDiagnostic, initializeNodeStateRoot, resolveNodeSettingsPaths, resolveNodeStateRoot, assertNodeStateSchema, } from "@autopilot-agent/adapters";
9
+ import { createNodeCredentialStore, createNodeCapabilityLoader, createNodeHostExecutionSandboxPort, createNodeSandboxPort, createNodeSettingsLoader, createNodeSettingsWriter, ensureUserConfigDirectory, createNodeRuntimePorts, createNodeTrustPolicy, createNodeWorkspaceRegistry, createNodeWorkspaceAdapter, createNodeWorkspaceId, getNodeStateCorruptionDiagnostics, getNodeStatePaths, getNodeStateSchemaDiagnostic, initializeNodeStateRoot, resolveNodeSettingsPaths, resolveNodeStateRoot, assertNodeStateSchema, } from "@autopilot-agent/adapters";
10
10
  import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  /** CLI 专用模型行为补充:无结构化 answerer 时以普通正文完成最少必要澄清。 */
@@ -222,9 +222,19 @@ export function createCliCompositionRoot(options = {}) {
222
222
  let sandboxPort;
223
223
  let sandboxPortRoot;
224
224
  function getSandboxPort(workspaceRoot) {
225
+ // Talk-Plan1022 S1:Windows 宿主执行模式——status/setup/doctor 只报告停用事实,
226
+ // 不加载 SRT、不触发 UAC provision;macOS 装配保持不变。
227
+ if (process.platform === "win32") {
228
+ if (!sandboxPort || sandboxPortRoot !== workspaceRoot) {
229
+ sandboxPort = createNodeHostExecutionSandboxPort();
230
+ sandboxPortRoot = workspaceRoot;
231
+ }
232
+ return sandboxPort;
233
+ }
225
234
  if (!sandboxPort || sandboxPortRoot !== workspaceRoot) {
226
235
  sandboxPort = createNodeSandboxPort({
227
236
  workspaceRoot,
237
+ stateRoot,
228
238
  tempRoot: path.join(workspaceRoot, ".autopilot", "tmp"),
229
239
  });
230
240
  sandboxPortRoot = workspaceRoot;
@@ -338,6 +348,7 @@ export function createCliCompositionRoot(options = {}) {
338
348
  initializeNodeStateRoot(stateRoot);
339
349
  return createNodeRuntimePorts({
340
350
  stateRoot,
351
+ trustRoot: options.trustRoot,
341
352
  workspaceRoot,
342
353
  env,
343
354
  userSettingsPath: resolveNodeSettingsPaths(workspaceRoot).user,
@@ -355,6 +366,7 @@ export function createCliCompositionRoot(options = {}) {
355
366
  const visionRef = resolvedSettings.visionModelRef === false ? undefined : resolvedSettings.visionModelRef ?? resolvedSettings.modelRef;
356
367
  const visionRoute = visionRef && resolvedSettings.providerRoutes[visionRef.provider];
357
368
  return {
369
+ ...(resolvedSettings.maxTurns !== undefined ? { maxSteps: resolvedSettings.maxTurns } : {}),
358
370
  ...(resolvedSettings.modelRef ? { modelRef: resolvedSettings.modelRef } : {}),
359
371
  ...(resolvedSettings.reasoningProfile ? { reasoningProfile: resolvedSettings.reasoningProfile } : {}),
360
372
  ...(selectedProvider?.credentialRef ? { credentialRef: selectedProvider.credentialRef } : {}),
@@ -16,5 +16,5 @@ export declare function formatContextReportLines(usage: ContextUsageView | null,
16
16
  }): string[];
17
17
  /** Plain/非 TTY 的稳定文本报告;始终跳过装饰网格。 */
18
18
  export declare function formatPlainContextReport(usage: ContextUsageView | null, expanded: boolean): string[];
19
- /** 状态栏 Context 行的紧凑文本:`12.3% · measured` / `≈1.2% · estimated` / `unknown`。 */
19
+ /** 状态栏只显示整个窗口的整数剩余百分比;计量来源留在 /context all。 */
20
20
  export declare function formatTuiContextStatus(usage: ContextUsageView | null): string;
@@ -165,10 +165,9 @@ export function formatContextReportLines(usage, expanded, options) {
165
165
  export function formatPlainContextReport(usage, expanded) {
166
166
  return formatContextReportLines(usage, expanded, { grid: false, columns: Number.MAX_SAFE_INTEGER });
167
167
  }
168
- /** 状态栏 Context 行的紧凑文本:`12.3% · measured` / `≈1.2% · estimated` / `unknown`。 */
168
+ /** 状态栏只显示整个窗口的整数剩余百分比;计量来源留在 /context all。 */
169
169
  export function formatTuiContextStatus(usage) {
170
170
  if (!usage)
171
171
  return "unknown";
172
- const percent = formatPercent(usage.usageRatio);
173
- return `${usage.measurement === "measured" ? "" : "≈"}${percent} · ${usage.measurement}`;
172
+ return `${Math.round(Math.max(0, 1 - usage.usageRatio) * 100)}%`;
174
173
  }
@@ -126,6 +126,62 @@ export function createCliController(options) {
126
126
  ];
127
127
  return result("ok", 0, lines, { permissionOptions, statusView });
128
128
  }
129
+ /** 合并 Session 级与 Workspace 级持久授权为当前会话可见列表;按 scope+身份去重。 */
130
+ async function listVisiblePermissionGrants() {
131
+ if (!options.runtime)
132
+ return [];
133
+ const grants = new Map();
134
+ const collect = function collectGrant(grant) {
135
+ grants.set(`${grant.scope}|${grant.toolName}|${grant.constraint}|${grant.workspaceRoot ?? ""}|${grant.sessionId ?? ""}`, grant);
136
+ };
137
+ if (sessionId)
138
+ for (const grant of await options.runtime.approvals.listPersistentGrants({ sessionId }))
139
+ collect(grant);
140
+ if (options.workspaceRoot)
141
+ for (const grant of await options.runtime.approvals.listPersistentGrants({ workspaceRoot: options.workspaceRoot }))
142
+ collect(grant);
143
+ return [...grants.values()];
144
+ }
145
+ /** 列出当前 Session/工作区可见的持久授权;只展示 Core 冻结的脱敏事实。 */
146
+ async function listPermissionGrants() {
147
+ if (!options.runtime)
148
+ return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
149
+ const grants = await listVisiblePermissionGrants();
150
+ if (grants.length === 0)
151
+ return result("ok", 0, ["当前 Session/工作区没有可见的持久授权。"]);
152
+ const lines = grants.map(function formatGrantLine(grant, index) {
153
+ const scope = grant.scope === "allow_workspace" ? "工作区" : "会话";
154
+ const projection = grant.projection === "pending" ? " · 记住未同步(待重新批准)" : "";
155
+ return `${index + 1}. [${scope}] ${grant.toolName} · ${grant.constraint}${projection}`;
156
+ });
157
+ lines.push("撤销:/permissions revoke <constraint>(constraint 请按列表原样复制)");
158
+ return result("ok", 0, lines);
159
+ }
160
+ /** 按 toolName+constraint 精确撤销当前可见授权;Core 先删 grant 权威再删 local 投影规则。 */
161
+ async function revokePermissionGrant(constraint) {
162
+ if (!options.runtime)
163
+ return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
164
+ if (!sessionId || !options.workspaceRoot)
165
+ return result("error", 2, [], "CLI_APPROVAL_NOT_AVAILABLE");
166
+ const grants = await listVisiblePermissionGrants();
167
+ const targets = grants.filter(function matchConstraint(grant) { return grant.constraint === constraint; });
168
+ if (targets.length === 0)
169
+ return result("error", 2, [], "CLI_PERMISSION_GRANT_NOT_FOUND");
170
+ const lines = [];
171
+ for (const grant of targets) {
172
+ const outcome = await options.runtime.approvals.revokePersistentGrant({
173
+ workspaceRoot: options.workspaceRoot,
174
+ sessionId,
175
+ toolName: grant.toolName,
176
+ constraint,
177
+ });
178
+ if (!outcome.deleted)
179
+ return result("error", 1, [], "CLI_PERMISSION_GRANT_REVOKE_FAILED");
180
+ lines.push(`已撤销 [${grant.scope === "allow_workspace" ? "工作区" : "会话"}] ${grant.toolName} · ${grant.constraint}${outcome.ruleRemoved ? ",并已删除工作区记住规则" : ""}`);
181
+ }
182
+ lines.push("后续同范围操作将重新请求授权。");
183
+ return result("ok", 0, lines);
184
+ }
129
185
  /** 读-合并-写入 Session 默认权限,只改变 permissionMode 并保留其他配置字段。 */
130
186
  async function setPermissionMode(mode) {
131
187
  if (!sessionId)
@@ -631,6 +687,10 @@ export function createCliController(options) {
631
687
  return openPermissions();
632
688
  if (action.type === "set_permission_mode")
633
689
  return setPermissionMode(action.mode);
690
+ if (action.type === "list_permission_grants")
691
+ return listPermissionGrants();
692
+ if (action.type === "revoke_permission_grant")
693
+ return revokePermissionGrant(action.constraint);
634
694
  if (action.type === "open_model_picker")
635
695
  return openModelPicker();
636
696
  if (action.type === "set_model")
@@ -20,6 +20,10 @@ export declare const TUI_CHIP_TEXT_COLOR = "ansi256(16)";
20
20
  export declare const TUI_TOOL_SUCCESS_COLOR = "ansi256(46)";
21
21
  /** 工具失败文字的辅助红;状态永远带文字标签,不靠颜色传达。 */
22
22
  export declare const TUI_TOOL_ERROR_COLOR = "ansi256(196)";
23
+ /** 失败徽标的红色背景;与错误文字共用语义红,NO_COLOR 时仍显示状态文字。 */
24
+ export declare const TUI_TOOL_ERROR_BG_COLOR = "ansi256(196)";
25
+ /** 红底失败徽标使用深色粗体文字,便于和错误背景形成明确边界。 */
26
+ export declare const TUI_TOOL_ERROR_TEXT_COLOR = "ansi256(16)";
23
27
  /** Skill 已启用开关的语义色。 */
24
28
  export declare const TUI_SKILL_ON_COLOR = "ansi256(46)";
25
29
  /** Skill 未启用开关的语义色。 */
@@ -20,6 +20,10 @@ export const TUI_CHIP_TEXT_COLOR = "ansi256(16)";
20
20
  export const TUI_TOOL_SUCCESS_COLOR = "ansi256(46)";
21
21
  /** 工具失败文字的辅助红;状态永远带文字标签,不靠颜色传达。 */
22
22
  export const TUI_TOOL_ERROR_COLOR = "ansi256(196)";
23
+ /** 失败徽标的红色背景;与错误文字共用语义红,NO_COLOR 时仍显示状态文字。 */
24
+ export const TUI_TOOL_ERROR_BG_COLOR = TUI_TOOL_ERROR_COLOR;
25
+ /** 红底失败徽标使用深色粗体文字,便于和错误背景形成明确边界。 */
26
+ export const TUI_TOOL_ERROR_TEXT_COLOR = TUI_CHIP_TEXT_COLOR;
23
27
  /** Skill 已启用开关的语义色。 */
24
28
  export const TUI_SKILL_ON_COLOR = "ansi256(46)";
25
29
  /** Skill 未启用开关的语义色。 */
@@ -90,7 +90,7 @@ function cloneProjection(previous, envelope) {
90
90
  ...previous,
91
91
  turnId: envelope.turnId,
92
92
  lastSeq: Math.max(previous.lastSeq ?? 0, envelope.seq),
93
- activeSkills: previous.activeSkills.map(function copySkill(skill) { return { ...skill }; }),
93
+ activeSkills: previous.activeSkills,
94
94
  events: [...previous.events],
95
95
  seenKeys,
96
96
  };
@@ -113,7 +113,7 @@ function applyCheckpoint(projection, checkpoint, seq, turnId) {
113
113
  if (sameSkills(projection.activeSkills, activeSkills))
114
114
  return;
115
115
  projection.activeSkills = activeSkills;
116
- projection.events.push({ type: "active_skills", seq, turnId, skills: activeSkills });
116
+ appendEvent(projection, { type: "active_skills", seq, turnId, skills: activeSkills });
117
117
  }
118
118
  /** 将安全事件追加到投影事件列表。 */
119
119
  function appendEvent(projection, event) {
@@ -187,6 +187,7 @@ function projectEvent(envelope, projection) {
187
187
  toolName: sanitizeText(event.toolName, 160),
188
188
  reason,
189
189
  risk: display?.risk,
190
+ constraint: event.result.approval.constraint,
190
191
  allowedDecisions,
191
192
  display,
192
193
  };
@@ -198,6 +199,7 @@ function projectEvent(envelope, projection) {
198
199
  toolName: sanitizeText(event.toolName, 160),
199
200
  reason,
200
201
  risk: display?.risk,
202
+ constraint: event.result.approval.constraint,
201
203
  allowedDecisions,
202
204
  display,
203
205
  };
package/dist/parser.js CHANGED
@@ -15,7 +15,7 @@ export const CLI_SLASH_COMMANDS = [
15
15
  { name: "/model", usage: "/model", description: "查看或设置下一个 Turn 的模型" },
16
16
  { name: "/effort", usage: "/effort", description: "查看或设置下一个 Turn 的推理强度" },
17
17
  { name: "/skill", usage: "/skill [name]", description: "打开 Skill picker 或选择 Skill" },
18
- { name: "/permissions", usage: "/permissions [mode]", description: "查看或设置 Session 权限模式" },
18
+ { name: "/permissions", usage: "/permissions [mode | grants | revoke <constraint>]", description: "查看/设置权限模式,或查看与撤销持久授权" },
19
19
  { name: "/approve", usage: "/approve <decision>", description: "提交当前工具审批决定" },
20
20
  { name: "/undo", usage: "/undo [confirm]", description: "预览并确认撤销当前会话最近一轮文件修改" },
21
21
  ];
@@ -54,7 +54,7 @@ export function renderHelp() {
54
54
  " -h, --help show this help",
55
55
  "",
56
56
  "Permissions: ask requires confirmation; auto_approve only auto-runs safe writes and workspace commands after Trust; full_access still enforces path, schema, and destructive-command guards.",
57
- "Limits: AGENT_MAX_STEPS is optional (unset means no total step cap); AGENT_MAX_TOOL_RETRIES defaults to 5; Turn overrides win and values are capped at 100 and 10.",
57
+ "Limits: settings.json maxTurns, AGENT_MAX_STEPS and AGENT_MAX_DURATION_MS are optional (unset means no step/time cap); maxTurns maps to per-run steps without a fixed platform cap; AGENT_MAX_TOOL_RETRIES defaults to 5 and is capped at 10.",
58
58
  "Exit codes: 0 success/normal exit, 1 runtime error, 2 usage error, 130 SIGINT, 143 SIGTERM.",
59
59
  "Interactive uses Ink on a TTY and Plain fallback otherwise; --output jsonl never emits ANSI or prompts.",
60
60
  ].join("\n");
@@ -359,12 +359,21 @@ function isApprovalDecision(value) {
359
359
  function invalidSlashArguments(name) {
360
360
  return { kind: "error", message: `/${name} 不接受额外参数` };
361
361
  }
362
- /** 解析权限模式查询与 Session 默认权限更新。 */
362
+ /** 解析权限模式查询、Session 默认权限更新与持久授权查看/撤销(Talk-Plan1013 Stage 4)。 */
363
363
  function parsePermissionsAction(parts) {
364
364
  if (parts.length === 1)
365
365
  return { kind: "action", action: { type: "open_permissions" } };
366
+ if (parts.length === 2 && parts[1] === "grants")
367
+ return { kind: "action", action: { type: "list_permission_grants" } };
368
+ if (parts[1] === "revoke") {
369
+ if (parts.length === 2)
370
+ return { kind: "error", message: "/permissions revoke 需要从 /permissions grants 列表中复制的完整 constraint" };
371
+ // constraint 由 ext-file:/path:/program: 前缀与路径组成,路径可含空格,按原样拼接且不再拆分。
372
+ const constraint = parts.slice(2).join(" ").trim();
373
+ return { kind: "action", action: { type: "revoke_permission_grant", constraint } };
374
+ }
366
375
  if (parts.length !== 2 || !isPermissionMode(parts[1])) {
367
- return { kind: "error", message: "/permissions 需要 ask|auto_approve|full_access" };
376
+ return { kind: "error", message: "/permissions 需要 ask|auto_approve|full_access 或 grants | revoke <constraint>" };
368
377
  }
369
378
  return { kind: "action", action: { type: "set_permission_mode", mode: parts[1] } };
370
379
  }
package/dist/renderer.js CHANGED
@@ -5,6 +5,10 @@
5
5
  * Store、checkpoint 或 Web SSE DTO;因此 text 与 JSONL 的语义来源一致。
6
6
  */
7
7
  import { formatAgentTermination } from "./termination-format.js";
8
+ /** 判断审批约束是否为无沙箱直接执行;与 Web 审批卡同一字符串判定,休眠底座不签发,历史记录仍可能携带。 */
9
+ function isNoSandboxApproval(constraint) {
10
+ return constraint === "no-sandbox-exec:v1";
11
+ }
8
12
  /** 将安全事件渲染为不带 ANSI 的 Plain 文本行。 */
9
13
  function renderTextEvent(event) {
10
14
  switch (event.type) {
@@ -15,8 +19,18 @@ function renderTextEvent(event) {
15
19
  case "answer_reset": return "answer reset";
16
20
  case "tool_started": return `tool ${event.toolName}: ${event.summary}`;
17
21
  case "tool_progress": return `tool ${event.toolName}: ${event.tail}`;
18
- case "tool_result": return `tool ${event.toolName} [${event.status}]: ${event.summary}${event.errorCode ? ` (${event.errorCode})` : ""}${event.diff ? `\n${event.diff}` : ""}`;
19
- case "approval_required": return `approval ${event.approvalId}: ${event.toolName} ${event.reason}`;
22
+ case "tool_result": {
23
+ const isolation = event.display?.executionMode === "unisolated" ? " · 未隔离:宿主直接执行" : "";
24
+ const diagnostics = event.display?.diagnostics?.length ? ` · 诊断:${event.display.diagnostics.join(";")}` : "";
25
+ return `tool ${event.toolName} [${event.status}]: ${event.summary}${event.errorCode ? ` (${event.errorCode})` : ""}${diagnostics}${isolation}${event.diff ? `\n${event.diff}` : ""}`;
26
+ }
27
+ case "approval_required": {
28
+ const cwd = event.display?.cwd ? ` · 工作目录:${event.display.cwd}` : "";
29
+ const noSandbox = isNoSandboxApproval(event.constraint) ? " · 无沙箱直接执行,仅本次有效" : "";
30
+ // Talk-Plan1022 S3:宿主执行审批卡按"将采用"措辞说明无隔离事实,待审批不显示成已执行。
31
+ const hostExecution = event.display?.executionMode === "unisolated" ? " · 将在当前用户权限下运行(未启用沙箱隔离)" : "";
32
+ return `approval ${event.approvalId}: ${event.toolName} ${event.reason}${cwd}${noSandbox}${hostExecution}`;
33
+ }
20
34
  case "input_queued": return `input queued: ${event.inputId} (${event.delivery})`;
21
35
  case "todo_updated": {
22
36
  const total = event.todos.length;
@@ -4,6 +4,7 @@ export function formatAgentTermination(view) {
4
4
  runtime_budget: "执行时长达到上限",
5
5
  token_budget: "Token 预算耗尽",
6
6
  repeated_tool_calls: "检测到重复工具调用",
7
+ write_verification: "等待修改文件读回验证",
7
8
  stale_lease: "执行租约已过期",
8
9
  turn_limit_reached: "达到执行步数上限",
9
10
  };
package/dist/tui/app.js CHANGED
@@ -19,6 +19,7 @@ import { TuiInputArea } from "./input-area.js";
19
19
  import { TuiInfoLines, TuiStatusLine } from "./status-line.js";
20
20
  import { TuiJobs } from "./jobs.js";
21
21
  import { TuiLiveTurn } from "./timeline/turn.js";
22
+ import { getTuiTurnTodo } from "./timeline/turn-items.js";
22
23
  import { MemoizedTuiTranscriptItem, TuiPendingInputItem, TuiPromptLine } from "./transcript.js";
23
24
  import { getTuiSelectedText } from "./viewport/selection.js";
24
25
  import { createTuiViewportSourceCache } from "./viewport/sources.js";
@@ -81,10 +82,11 @@ export function CliTuiApp(props) {
81
82
  .slice(-4)
82
83
  .map(function getNoticeMessage(item) { return item.message; });
83
84
  const approvalHeight = isViewportMode && snapshot.pendingApproval ? createTuiApprovalOptions(snapshot.pendingApproval).length + 6 : 0;
84
- const fixedHeight = inputLayout.promptHeight + inputLayout.pickerHeight + approvalHeight + infoMessages.length + 1 + (statusView ? 5 : 2);
85
- const viewportHeight = Math.max(1, rows - fixedHeight);
86
85
  // Todo 展开态最多占 10 行;矮终端为输入框、状态栏和活动文案至少保留 6 行。
87
86
  const todoRowLimit = Math.max(2, Math.min(10, rows - 6));
87
+ const activeTodoForm = getTuiTurnTodo(snapshot.liveEvents, snapshot.liveTurnId ?? "")?.todos.find(function active(todo) { return todo.status === "in_progress"; })?.activeForm;
88
+ const fixedHeight = inputLayout.promptHeight + inputLayout.pickerHeight + approvalHeight + infoMessages.length + 1 + (statusView ? 5 : 2);
89
+ const viewportHeight = Math.max(1, rows - fixedHeight);
88
90
  const viewportSourceCacheRef = useRef(undefined);
89
91
  if (!viewportSourceCacheRef.current)
90
92
  viewportSourceCacheRef.current = createTuiViewportSourceCache();
@@ -94,9 +96,9 @@ export function CliTuiApp(props) {
94
96
  const viewportContentWidth = Math.max(1, columns - 1);
95
97
  const viewportSources = useMemo(function getCachedViewportSources() {
96
98
  return isViewportMode
97
- ? viewportSourceCacheRef.current.getSources(snapshot, statusView, pickers.approvalPickerIndex, viewportContentWidth, todoExpanded, props.useColor)
99
+ ? viewportSourceCacheRef.current.getSources(snapshot, statusView, pickers.approvalPickerIndex, viewportContentWidth, todoExpanded, props.useColor, todoRowLimit)
98
100
  : EMPTY_VIEWPORT_SOURCES;
99
- }, [isViewportMode, pickers.approvalPickerIndex, snapshot, statusView, viewportContentWidth, todoExpanded, props.useColor]);
101
+ }, [isViewportMode, pickers.approvalPickerIndex, snapshot, statusView, viewportContentWidth, todoExpanded, props.useColor, todoRowLimit]);
100
102
  const viewportLines = useMemo(function getCachedViewportLines() {
101
103
  return isViewportMode
102
104
  ? viewportLineCacheRef.current.getLines(viewportSources, viewportContentWidth)
@@ -120,6 +122,7 @@ export function CliTuiApp(props) {
120
122
  return;
121
123
  const isJobAction = action.type === "list_jobs" || action.type === "show_job" || action.type === "read_job_output" || action.type === "stop_job";
122
124
  const jobGeneration = isJobAction ? ++jobRequestGeneration.current : undefined;
125
+ const inputPosition = prompt && allowWhileRunning ? props.store.captureInputPosition() : undefined;
123
126
  if (prompt) {
124
127
  lastSubmittedPromptRef.current = prompt.trim();
125
128
  props.store.recordPrompt(prompt, !allowWhileRunning);
@@ -177,9 +180,9 @@ export function CliTuiApp(props) {
177
180
  }
178
181
  if (result.contextView)
179
182
  setContextView(result.contextView);
180
- await refreshStatusView();
181
183
  if (prompt && result.submittedInput)
182
- props.store.addPendingPrompt(prompt, result.submittedInput);
184
+ props.store.addPendingPrompt(prompt, result.submittedInput, inputPosition);
185
+ await refreshStatusView();
183
186
  if (result.skillOptions) {
184
187
  pickers.setSkillPickerSkills(result.skillOptions);
185
188
  if (!preserveSkillPickerQuery)
@@ -405,6 +408,6 @@ export function CliTuiApp(props) {
405
408
  }
406
409
  return _jsx(TuiBrandHeader, { statusView: statusView, columns: columns, useColor: props.useColor, screenReader: props.screenReader, reducedMotion: props.reducedMotion ?? false, animationActive: false }, item.key);
407
410
  }), activePrompt ? _jsx(TuiPromptLine, { content: activePrompt.content, columns: columns, useColor: props.useColor, keyValue: activePrompt.key }) : null, _jsx(TuiLiveTurn, { turnId: isLiveTurnTerminal ? undefined : snapshot.liveTurnId, events: snapshot.liveEvents, presentation: snapshot.livePresentation, pendingApproval: snapshot.pendingApproval, approvalSelectedIndex: pickers.approvalPickerIndex, status: snapshot.status, metrics: snapshot.liveMetrics, useColor: props.useColor, screenReader: props.screenReader, todoExpanded: todoExpanded, todoRowLimit: todoRowLimit, maximumWidth: Math.max(1, columns - 2) }), snapshot.pendingInputs.map(function renderPendingInput(item) {
408
- return _jsx(TuiPendingInputItem, { item: item, useColor: props.useColor }, item.key);
409
- })] }), _jsx(TuiActivityLine, { activity: snapshot.activity, status: snapshot.status, waitingForApproval: snapshot.pendingApproval !== undefined, useColor: props.useColor, screenReader: props.screenReader, reducedMotion: props.reducedMotion ?? false, animationActive: animationActive, clock: animationClock }), _jsx(TuiInfoLines, { messages: infoMessages, useColor: props.useColor }), _jsx(TuiInputArea, { store: props.store, columns: columns, busy: busy, keepOpenAfterCancel: keepOpenAfterCancel, lastSubmittedPromptRef: lastSubmittedPromptRef, rawInputSequenceRef: rawInputSequenceRef, dispatchAction: dispatchAction, onEof: props.onEof, onRequestInterrupt: props.onRequestInterrupt, onRequestCancel: props.onRequestCancel, isRawModeSupported: isRawModeSupported, isInputActive: jobView === undefined && contextView === undefined, showInput: jobView === undefined && contextView === undefined, useColor: props.useColor, screenReader: props.screenReader, pickers: pickers, derivedRef: derivedRef, fileMentionIndex: fileMentionIndex, onLayoutChange: handleInputLayoutChange }), _jsx(TuiStatusLine, { snapshot: snapshot, statusView: statusView, useColor: props.useColor, screenReader: props.screenReader, selectionCharacters: selectedText.length })] }));
411
+ return item.delivery === "queue" ? _jsx(TuiPendingInputItem, { item: item, useColor: props.useColor }, item.key) : null;
412
+ })] }), _jsx(TuiActivityLine, { activity: snapshot.activity, status: snapshot.status, waitingForApproval: snapshot.pendingApproval !== undefined, useColor: props.useColor, screenReader: props.screenReader, reducedMotion: props.reducedMotion ?? false, animationActive: animationActive, clock: animationClock, startedAt: snapshot.liveMetrics?.startedAt, activeForm: activeTodoForm }), _jsx(TuiInfoLines, { messages: infoMessages, useColor: props.useColor }), _jsx(TuiInputArea, { store: props.store, columns: columns, busy: busy, keepOpenAfterCancel: keepOpenAfterCancel, lastSubmittedPromptRef: lastSubmittedPromptRef, rawInputSequenceRef: rawInputSequenceRef, dispatchAction: dispatchAction, onEof: props.onEof, onRequestInterrupt: props.onRequestInterrupt, onRequestCancel: props.onRequestCancel, isRawModeSupported: isRawModeSupported, isInputActive: jobView === undefined && contextView === undefined, showInput: jobView === undefined && contextView === undefined, useColor: props.useColor, screenReader: props.screenReader, pickers: pickers, derivedRef: derivedRef, fileMentionIndex: fileMentionIndex, onLayoutChange: handleInputLayoutChange }), _jsx(TuiStatusLine, { snapshot: snapshot, statusView: statusView, useColor: props.useColor, screenReader: props.screenReader, selectionCharacters: selectedText.length })] }));
410
413
  }
@@ -8,7 +8,7 @@ export interface CliTuiApprovalOption {
8
8
  }>["decision"];
9
9
  label: string;
10
10
  }
11
- /** 根据待审批工具收窄菜单;ask 下命令不提供可持久化授权。 */
11
+ /** 根据待审批工具收窄菜单;旧记录缺少冻结范围时保持最保守的单次/拒绝。 */
12
12
  export declare function createTuiApprovalOptions(pendingApproval: NonNullable<CliProjection["pendingApproval"]>): CliTuiApprovalOption[];
13
13
  /** 以 CC 风格上下选择、回车确认渲染当前审批菜单。 */
14
14
  export declare function TuiApprovalPrompt(props: {