@autopilot-agent/cli 0.0.0-stage → 0.1.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 +180 -2
- package/dist/activity-animation.d.ts +57 -0
- package/dist/activity-animation.js +145 -0
- package/dist/animation-clock.d.ts +22 -0
- package/dist/animation-clock.js +76 -0
- package/dist/composition-root.d.ts +105 -0
- package/dist/composition-root.js +588 -0
- package/dist/context-report.d.ts +20 -0
- package/dist/context-report.js +174 -0
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +681 -0
- package/dist/design-tokens.d.ts +34 -0
- package/dist/design-tokens.js +34 -0
- package/dist/error-diagnostics.d.ts +8 -0
- package/dist/error-diagnostics.js +62 -0
- package/dist/event-projector.d.ts +26 -0
- package/dist/event-projector.js +333 -0
- package/dist/file-mention-context.d.ts +34 -0
- package/dist/file-mention-context.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +40 -0
- package/dist/main.js +543 -0
- package/dist/parser.d.ts +17 -0
- package/dist/parser.js +391 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.js +48 -0
- package/dist/scroll-viewport.d.ts +22 -0
- package/dist/scroll-viewport.js +62 -0
- package/dist/termination-format.d.ts +4 -0
- package/dist/termination-format.js +17 -0
- package/dist/timeline-lines.d.ts +38 -0
- package/dist/timeline-lines.js +104 -0
- package/dist/tui/app.d.ts +30 -0
- package/dist/tui/app.js +410 -0
- package/dist/tui/approval.d.ts +19 -0
- package/dist/tui/approval.js +37 -0
- package/dist/tui/brand/brand-header.d.ts +20 -0
- package/dist/tui/brand/brand-header.js +22 -0
- package/dist/tui/brand/intro.d.ts +12 -0
- package/dist/tui/brand/intro.js +88 -0
- package/dist/tui/brand/pixel.d.ts +8 -0
- package/dist/tui/brand/pixel.js +50 -0
- package/dist/tui/constants.d.ts +56 -0
- package/dist/tui/constants.js +57 -0
- package/dist/tui/context.d.ts +11 -0
- package/dist/tui/context.js +12 -0
- package/dist/tui/diff-stats.d.ts +8 -0
- package/dist/tui/diff-stats.js +7 -0
- package/dist/tui/format.d.ts +75 -0
- package/dist/tui/format.js +176 -0
- package/dist/tui/hooks/use-file-mention-index.d.ts +15 -0
- package/dist/tui/hooks/use-file-mention-index.js +59 -0
- package/dist/tui/hooks/use-pickers.d.ts +47 -0
- package/dist/tui/hooks/use-pickers.js +288 -0
- package/dist/tui/hooks/use-prompt-input.d.ts +54 -0
- package/dist/tui/hooks/use-prompt-input.js +258 -0
- package/dist/tui/hooks/use-status-view.d.ts +8 -0
- package/dist/tui/hooks/use-status-view.js +13 -0
- package/dist/tui/hooks/use-viewport.d.ts +22 -0
- package/dist/tui/hooks/use-viewport.js +117 -0
- package/dist/tui/input/file-mention.d.ts +33 -0
- package/dist/tui/input/file-mention.js +112 -0
- package/dist/tui/input/graphemes.d.ts +5 -0
- package/dist/tui/input/graphemes.js +10 -0
- package/dist/tui/input/keys.d.ts +16 -0
- package/dist/tui/input/keys.js +58 -0
- package/dist/tui/input/prompt-layout.d.ts +27 -0
- package/dist/tui/input/prompt-layout.js +84 -0
- package/dist/tui/input-area.d.ts +53 -0
- package/dist/tui/input-area.js +89 -0
- package/dist/tui/jobs.d.ts +10 -0
- package/dist/tui/jobs.js +24 -0
- package/dist/tui/lifecycle.d.ts +23 -0
- package/dist/tui/lifecycle.js +114 -0
- package/dist/tui/markdown/diff.d.ts +8 -0
- package/dist/tui/markdown/diff.js +19 -0
- package/dist/tui/markdown/inline.d.ts +4 -0
- package/dist/tui/markdown/inline.js +32 -0
- package/dist/tui/markdown/markdown.d.ts +17 -0
- package/dist/tui/markdown/markdown.js +117 -0
- package/dist/tui/markdown/table.d.ts +19 -0
- package/dist/tui/markdown/table.js +74 -0
- package/dist/tui/performance-diagnostics.d.ts +7 -0
- package/dist/tui/performance-diagnostics.js +57 -0
- package/dist/tui/pickers/effort.d.ts +10 -0
- package/dist/tui/pickers/effort.js +12 -0
- package/dist/tui/pickers/file-mention.d.ts +11 -0
- package/dist/tui/pickers/file-mention.js +22 -0
- package/dist/tui/pickers/model.d.ts +10 -0
- package/dist/tui/pickers/model.js +12 -0
- package/dist/tui/pickers/permission.d.ts +10 -0
- package/dist/tui/pickers/permission.js +13 -0
- package/dist/tui/pickers/skill.d.ts +13 -0
- package/dist/tui/pickers/skill.js +25 -0
- package/dist/tui/pickers/slash.d.ts +10 -0
- package/dist/tui/pickers/slash.js +11 -0
- package/dist/tui/prompt-input.d.ts +11 -0
- package/dist/tui/prompt-input.js +73 -0
- package/dist/tui/render-mode.d.ts +20 -0
- package/dist/tui/render-mode.js +31 -0
- package/dist/tui/run.d.ts +6 -0
- package/dist/tui/run.js +237 -0
- package/dist/tui/status-line.d.ts +16 -0
- package/dist/tui/status-line.js +37 -0
- package/dist/tui/store.d.ts +4 -0
- package/dist/tui/store.js +344 -0
- package/dist/tui/timeline/live.d.ts +16 -0
- package/dist/tui/timeline/live.js +151 -0
- package/dist/tui/timeline/presentation-timeline.d.ts +14 -0
- package/dist/tui/timeline/presentation-timeline.js +109 -0
- package/dist/tui/timeline/progress.d.ts +12 -0
- package/dist/tui/timeline/progress.js +43 -0
- package/dist/tui/timeline/static-timeline.d.ts +13 -0
- package/dist/tui/timeline/static-timeline.js +121 -0
- package/dist/tui/timeline/tool-tree.d.ts +7 -0
- package/dist/tui/timeline/tool-tree.js +25 -0
- package/dist/tui/timeline/turn.d.ts +19 -0
- package/dist/tui/timeline/turn.js +21 -0
- package/dist/tui/todo-list.d.ts +50 -0
- package/dist/tui/todo-list.js +120 -0
- package/dist/tui/transcript.d.ts +24 -0
- package/dist/tui/transcript.js +34 -0
- package/dist/tui/trust-dialog.d.ts +51 -0
- package/dist/tui/trust-dialog.js +127 -0
- package/dist/tui/types.d.ts +175 -0
- package/dist/tui/types.js +5 -0
- package/dist/tui/viewport/selection.d.ts +27 -0
- package/dist/tui/viewport/selection.js +79 -0
- package/dist/tui/viewport/sources.d.ts +29 -0
- package/dist/tui/viewport/sources.js +585 -0
- package/dist/tui/viewport/viewport.d.ts +12 -0
- package/dist/tui/viewport/viewport.js +87 -0
- package/dist/tui-input-history.d.ts +23 -0
- package/dist/tui-input-history.js +42 -0
- package/dist/tui-input-router.d.ts +24 -0
- package/dist/tui-input-router.js +115 -0
- package/dist/tui.d.ts +15 -0
- package/dist/tui.js +13 -0
- package/dist/types.d.ts +454 -0
- package/dist/types.js +7 -0
- package/package.json +58 -4
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Controller。
|
|
3
|
+
*
|
|
4
|
+
* Controller 是 Action 到 Core Runtime Public API 的唯一映射层;它不实现
|
|
5
|
+
* Session/Turn 状态机,审批只从当前安全 projection 取得 approvalId,绝不
|
|
6
|
+
* 生成 callId、toolName、normalizedInputHash 或冻结工具输入。
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { AgentRuntimeError } from "@autopilot-agent/core";
|
|
10
|
+
import { createCliEventProjector } from "./event-projector.js";
|
|
11
|
+
import { formatPlainContextReport } from "./context-report.js";
|
|
12
|
+
/** 让 Ink 在连续微任务事件之间获得一次绘制机会,避免回答结束后才整段出现。 */
|
|
13
|
+
const CLI_RENDER_YIELD_EVERY_ANSWER_EVENTS = 4;
|
|
14
|
+
/** 只有成功完成的 Turn 才自动领取 queue;失败、中断和达到上限都保留给用户处理。 */
|
|
15
|
+
const CLI_QUEUE_CLAIMABLE_STATUSES = new Set(["completed"]);
|
|
16
|
+
/** 单次 consume 后自动连续启动的排队 Turn 上限,防止病态积压无限循环。 */
|
|
17
|
+
const CLI_MAX_AUTO_QUEUED_TURNS = 8;
|
|
18
|
+
/** 与 Web Continue 入口保持一致的确定性继续词;模糊自然语言仍按普通 prompt 处理。 */
|
|
19
|
+
const CLI_CONTINUE_PROMPTS = new Set(["继续", "继续执行", "继续任务"]);
|
|
20
|
+
function yieldToCliRenderer() {
|
|
21
|
+
return new Promise(function resolveOnNextCheck(resolve) {
|
|
22
|
+
setImmediate(resolve);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** 判断输入是否是与 Web 相同的明确继续词,避免把新任务误接到旧 Turn。 */
|
|
26
|
+
function isCliContinuePrompt(content) {
|
|
27
|
+
return CLI_CONTINUE_PROMPTS.has(content.trim());
|
|
28
|
+
}
|
|
29
|
+
/** 创建无 React 依赖的 Controller;Renderer 和 Runtime 均通过参数注入。 */
|
|
30
|
+
export function createCliController(options) {
|
|
31
|
+
const projector = createCliEventProjector({ sessionId: options.sessionId, now: options.now });
|
|
32
|
+
let eventListener = options.onEvent;
|
|
33
|
+
let queuedInputListener = options.onQueuedInputStarted;
|
|
34
|
+
let sessionId = options.sessionId;
|
|
35
|
+
let turnId;
|
|
36
|
+
let pendingUndoTurnId;
|
|
37
|
+
let selectedSkills = [];
|
|
38
|
+
/** 创建不写 IO 的基础结果。 */
|
|
39
|
+
function result(kind, exitCode, lines = [], optionsOrMessage = {}) {
|
|
40
|
+
const optional = typeof optionsOrMessage === "string" ? { message: optionsOrMessage } : optionsOrMessage;
|
|
41
|
+
return { kind, exitCode, lines, ...optional, projection: projector.getProjection() };
|
|
42
|
+
}
|
|
43
|
+
/** 将 Core 权限模式转换为用户可读的稳定中文标签。 */
|
|
44
|
+
function permissionLabel(mode) {
|
|
45
|
+
if (mode === "auto_approve")
|
|
46
|
+
return "为我审批";
|
|
47
|
+
if (mode === "full_access")
|
|
48
|
+
return "完全访问";
|
|
49
|
+
return "询问";
|
|
50
|
+
}
|
|
51
|
+
/** 返回三态权限目录;选中项来自 Session 默认值而非活动 Turn 的可变状态。 */
|
|
52
|
+
function createPermissionOptions(selectedMode) {
|
|
53
|
+
return [
|
|
54
|
+
{ mode: "ask", label: "询问", description: "查看和新建自动执行;编辑、覆写和每次命令执行询问。", selected: selectedMode === "ask" },
|
|
55
|
+
{ mode: "auto_approve", label: "为我审批", description: "文件工具沿用既有自动审批;命令仅自动执行只读,受信任工作区内的已识别变更命令可自动通过,未知或外部命令仍需审批。", selected: selectedMode === "auto_approve" },
|
|
56
|
+
{ mode: "full_access", label: "完全访问", description: "可执行需审批命令;仍拒绝破坏性命令,并遵守工作区路径、Trust 和参数校验。", selected: selectedMode === "full_access" },
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/** 读取 Composition Root 的 settings-filtered 模型目录,缺失时不假造可切换模型。 */
|
|
60
|
+
function getModelCapabilities() {
|
|
61
|
+
return options.getRuntimeStatusDefaults?.().modelCapabilities ?? [];
|
|
62
|
+
}
|
|
63
|
+
/** 以 provider/modelId 查找当前允许切换的模型目录项。 */
|
|
64
|
+
function findModelCapability(modelId, provider) {
|
|
65
|
+
return getModelCapabilities().find(function matchesModel(capability) {
|
|
66
|
+
return capability.modelId === modelId && (provider === undefined || capability.provider === provider);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** 将模型与推理配置收窄为状态栏可展示的稳定字符串。 */
|
|
70
|
+
function getConfigLabels(config) {
|
|
71
|
+
const effective = config.effectiveReasoning;
|
|
72
|
+
return {
|
|
73
|
+
model: config.modelRef ? `${config.modelRef.provider}/${config.modelRef.modelId}` : "runtime default",
|
|
74
|
+
effort: effective
|
|
75
|
+
? effective.source === "clamped" ? `${effective.effort} (max clamped)` : effective.effort
|
|
76
|
+
: config.reasoningProfile?.effort ?? "runtime default",
|
|
77
|
+
permissionMode: config.permissionMode ?? "ask",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** 读取 Session 默认配置,并只保留权限 picker 所需的安全字段。 */
|
|
81
|
+
async function getSession() {
|
|
82
|
+
if (!sessionId || !options.runtime?.sessions)
|
|
83
|
+
return undefined;
|
|
84
|
+
return (await options.runtime.sessions.get(sessionId)) ?? undefined;
|
|
85
|
+
}
|
|
86
|
+
/** 读取当前状态栏的脱敏配置;活动 Turn 优先使用 frozen 值。 */
|
|
87
|
+
async function getStatusView() {
|
|
88
|
+
const session = await getSession();
|
|
89
|
+
const sessionConfig = session?.defaultRunConfig;
|
|
90
|
+
const runtimeDefaults = options.getRuntimeStatusDefaults?.() ?? {};
|
|
91
|
+
const currentProjection = projector.getProjection();
|
|
92
|
+
const isActive = (currentProjection.status === "running" || currentProjection.status === "waiting_approval") && Boolean(sessionId && turnId);
|
|
93
|
+
const frozen = isActive && options.runtime && sessionId && turnId
|
|
94
|
+
? await options.runtime.turns.getFrozenRunConfig({ sessionId, turnId })
|
|
95
|
+
: undefined;
|
|
96
|
+
const hasSessionDefaults = Boolean(sessionConfig && Object.keys(sessionConfig).length > 0);
|
|
97
|
+
const labels = getConfigLabels(frozen ?? { ...runtimeDefaults, ...sessionConfig });
|
|
98
|
+
const sessionPermission = sessionConfig?.permissionMode ?? runtimeDefaults.permissionMode ?? "ask";
|
|
99
|
+
const nextLabels = getConfigLabels({ ...runtimeDefaults, ...sessionConfig });
|
|
100
|
+
// 无 Runtime/Session 时省略字段;测试 double 缺 turns 或 getContextUsage 能力时同样跳过;查询失败 fail closed 为 null(unknown),不伪造 0%。
|
|
101
|
+
const contextUsage = session && options.runtime?.turns && typeof options.runtime.turns.getContextUsage === "function"
|
|
102
|
+
? await options.runtime.turns.getContextUsage({ sessionId: session.id }).catch(function failClosed() { return null; })
|
|
103
|
+
: undefined;
|
|
104
|
+
return {
|
|
105
|
+
...labels,
|
|
106
|
+
workspaceRoot: options.workspaceRoot,
|
|
107
|
+
source: frozen ? "turn_frozen" : hasSessionDefaults ? "session_default" : "runtime_default",
|
|
108
|
+
nextPermissionMode: frozen && sessionPermission !== labels.permissionMode ? sessionPermission : undefined,
|
|
109
|
+
nextModel: frozen && nextLabels.model !== labels.model ? nextLabels.model : undefined,
|
|
110
|
+
nextEffort: frozen && nextLabels.effort !== labels.effort ? nextLabels.effort : undefined,
|
|
111
|
+
contextUsage,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** 返回权限 picker 的目录和当前状态栏视图;不读取 checkpoint 或审批凭证。 */
|
|
115
|
+
async function openPermissions() {
|
|
116
|
+
const statusView = await getStatusView();
|
|
117
|
+
const session = await getSession();
|
|
118
|
+
const selectedMode = session?.defaultRunConfig?.permissionMode ?? statusView.permissionMode;
|
|
119
|
+
const permissionOptions = createPermissionOptions(selectedMode);
|
|
120
|
+
const lines = [
|
|
121
|
+
"permission modes:",
|
|
122
|
+
...permissionOptions.map(function formatPermissionOption(option, index) {
|
|
123
|
+
return `${index + 1}. ${option.mode} — ${option.label}:${option.description}`;
|
|
124
|
+
}),
|
|
125
|
+
"interactive: ↑↓ select · Enter save · Esc cancel; plain: /permissions <mode>",
|
|
126
|
+
];
|
|
127
|
+
return result("ok", 0, lines, { permissionOptions, statusView });
|
|
128
|
+
}
|
|
129
|
+
/** 读-合并-写入 Session 默认权限,只改变 permissionMode 并保留其他配置字段。 */
|
|
130
|
+
async function setPermissionMode(mode) {
|
|
131
|
+
if (!sessionId)
|
|
132
|
+
return result("error", 2, [], "CLI_SESSION_REQUIRED");
|
|
133
|
+
if (!options.runtime?.sessions)
|
|
134
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
135
|
+
const session = await options.runtime.sessions.get(sessionId);
|
|
136
|
+
if (!session)
|
|
137
|
+
return result("error", 1, [], "SESSION_NOT_FOUND");
|
|
138
|
+
await options.runtime.sessions.setDefaultRunConfig(sessionId, {
|
|
139
|
+
...(session.defaultRunConfig ?? {}),
|
|
140
|
+
permissionMode: mode,
|
|
141
|
+
});
|
|
142
|
+
const statusView = await getStatusView();
|
|
143
|
+
const isActive = statusView.source === "turn_frozen";
|
|
144
|
+
const suffix = isActive ? ";当前 Turn 保持 frozen,下一个 Turn 生效" : ";下一个 Turn 生效";
|
|
145
|
+
return result("ok", 0, [`permission: ${permissionLabel(mode)} (${mode})${suffix}`], { statusView });
|
|
146
|
+
}
|
|
147
|
+
/** 消费一段 Core AgentEvent,统一补本地 seq、归约和渲染。 */
|
|
148
|
+
async function consume(events) {
|
|
149
|
+
const lines = [];
|
|
150
|
+
await consumeStream(events, lines);
|
|
151
|
+
// Turn 到达终态后自动领取遗留 queue 输入启动新 Turn;对齐 Web start_next 的 claim→consume→start 语义。
|
|
152
|
+
await drainQueuedInputs(lines).catch(function ignoreDrainFailure(error) {
|
|
153
|
+
// 领取是 best-effort 增强;任何失败都不应把已完成的 Turn 翻转为错误退出码。
|
|
154
|
+
lines.push(`queued input drain skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
});
|
|
156
|
+
return result("ok", 0, lines);
|
|
157
|
+
}
|
|
158
|
+
/** 消费一个事件流并把渲染行追加到同一 lines;不改变调用方的结果语义。 */
|
|
159
|
+
async function consumeStream(events, lines) {
|
|
160
|
+
let answerEventsSinceYield = 0;
|
|
161
|
+
for await (const event of events) {
|
|
162
|
+
turnId = event.turnId;
|
|
163
|
+
const previousCount = projector.getProjection().events.length;
|
|
164
|
+
const projected = projector.project(event);
|
|
165
|
+
if (projected.added) {
|
|
166
|
+
const newEvents = projected.projection.events.slice(previousCount);
|
|
167
|
+
for (const publicEvent of newEvents) {
|
|
168
|
+
eventListener?.(publicEvent, projected.projection);
|
|
169
|
+
const rendered = options.renderer.render(publicEvent);
|
|
170
|
+
// context_usage 在 text 模式渲染为空:用量变化只刷新状态栏,不产生输出行。
|
|
171
|
+
if (rendered)
|
|
172
|
+
lines.push(rendered);
|
|
173
|
+
if (eventListener) {
|
|
174
|
+
if (publicEvent.type === "answer_delta") {
|
|
175
|
+
answerEventsSinceYield += 1;
|
|
176
|
+
if (answerEventsSinceYield < CLI_RENDER_YIELD_EVERY_ANSWER_EVENTS)
|
|
177
|
+
continue;
|
|
178
|
+
answerEventsSinceYield = 0;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
answerEventsSinceYield = 0;
|
|
182
|
+
}
|
|
183
|
+
await yieldToCliRenderer();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Turn 终态后领取遗留排队输入并启动新 Turn;CLI 无 start_next 按钮,不领取则排队输入永久滞留。 */
|
|
190
|
+
async function drainQueuedInputs(lines) {
|
|
191
|
+
if (!sessionId || !options.runtime)
|
|
192
|
+
return;
|
|
193
|
+
const inbox = options.runtime.inbox;
|
|
194
|
+
// 测试 double 或宿主未注入可用 inbox(缺 claimNext 能力)时跳过领取,不影响主流程。
|
|
195
|
+
if (!inbox || typeof inbox.claimNext !== "function")
|
|
196
|
+
return;
|
|
197
|
+
for (let iteration = 0; iteration < CLI_MAX_AUTO_QUEUED_TURNS; iteration += 1) {
|
|
198
|
+
if (!CLI_QUEUE_CLAIMABLE_STATUSES.has(projector.getProjection().status))
|
|
199
|
+
return;
|
|
200
|
+
const claimed = await inbox.claimNext({ sessionId });
|
|
201
|
+
if (!claimed)
|
|
202
|
+
return;
|
|
203
|
+
const nextTurnId = randomUUID();
|
|
204
|
+
if (!(await inbox.consume({ sessionId, inputId: claimed.inputId, turnId: nextTurnId })))
|
|
205
|
+
continue;
|
|
206
|
+
const startedInput = {
|
|
207
|
+
inputId: claimed.inputId,
|
|
208
|
+
turnId: nextTurnId,
|
|
209
|
+
content: claimed.content,
|
|
210
|
+
delivery: "queue",
|
|
211
|
+
};
|
|
212
|
+
queuedInputListener?.(startedInput);
|
|
213
|
+
lines.push(`queued input started: ${claimed.inputId}`);
|
|
214
|
+
const activeSkills = claimed.selectedSkills
|
|
215
|
+
.filter(function hasIdentity(skill) { return Boolean(skill.skillId && skill.contentHash); })
|
|
216
|
+
.map(function toIdentity(skill) { return { skillId: skill.skillId, contentHash: skill.contentHash }; });
|
|
217
|
+
try {
|
|
218
|
+
await consumeStream(options.runtime.turns.start({
|
|
219
|
+
sessionId,
|
|
220
|
+
turnId: nextTurnId,
|
|
221
|
+
content: claimed.content,
|
|
222
|
+
activeSkills: activeSkills.length > 0 ? activeSkills : undefined,
|
|
223
|
+
continuity: { source: "claimed_queue" },
|
|
224
|
+
}), lines);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
lines.push(`queued input failed: ${claimed.inputId} (${error instanceof Error ? error.message : String(error)})`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** 将已钉住 Skill 转为持续输入收件箱所需的安全身份引用。 */
|
|
233
|
+
function toInboxSkillRefs() {
|
|
234
|
+
return selectedSkills.map(function toInboxSkill(skill) {
|
|
235
|
+
return {
|
|
236
|
+
skillRef: skill.qualifiedName,
|
|
237
|
+
skillId: skill.skillId,
|
|
238
|
+
contentHash: skill.contentHash,
|
|
239
|
+
pinned: true,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** 启动新 turn,或将运行中的普通输入按 Web 同一语义投递为 steer/queue。 */
|
|
244
|
+
async function submitPrompt(content, delivery) {
|
|
245
|
+
if (!sessionId)
|
|
246
|
+
return result("error", 1, [], "CLI_SESSION_REQUIRED");
|
|
247
|
+
if (!options.runtime)
|
|
248
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
249
|
+
const status = projector.getProjection().status;
|
|
250
|
+
if (delivery === undefined && isCliContinuePrompt(content) && status !== "running") {
|
|
251
|
+
const recoverable = await options.runtime.turns.getRecoverableState({ sessionId, turnId });
|
|
252
|
+
if (recoverable?.recoverable)
|
|
253
|
+
return resume();
|
|
254
|
+
}
|
|
255
|
+
if ((status === "running" || status === "waiting_approval") && turnId) {
|
|
256
|
+
const input = await options.runtime.turns.steer({
|
|
257
|
+
sessionId,
|
|
258
|
+
turnId,
|
|
259
|
+
content,
|
|
260
|
+
delivery: delivery ?? "steer",
|
|
261
|
+
selectedSkills: toInboxSkillRefs(),
|
|
262
|
+
});
|
|
263
|
+
return result("ok", 0, [`input queued: ${input.inputId} (${input.delivery})`], { submittedInput: { inputId: input.inputId, delivery: input.delivery } });
|
|
264
|
+
}
|
|
265
|
+
if (delivery === "queue")
|
|
266
|
+
return result("error", 2, [], "CLI_QUEUE_REQUIRES_ACTIVE_TURN");
|
|
267
|
+
const input = {
|
|
268
|
+
sessionId,
|
|
269
|
+
content,
|
|
270
|
+
activeSkills: selectedSkills.length > 0 ? selectedSkills.map(function toActiveSkill(skill) {
|
|
271
|
+
return { skillId: skill.skillId, contentHash: skill.contentHash };
|
|
272
|
+
}) : undefined,
|
|
273
|
+
};
|
|
274
|
+
return consume(options.runtime.turns.start(input));
|
|
275
|
+
}
|
|
276
|
+
/** 使用当前安全审批视图提交决定,并把 allow 类映射为 Core continue 的 allow。 */
|
|
277
|
+
async function approve(decision) {
|
|
278
|
+
const pending = projector.getProjection().pendingApproval;
|
|
279
|
+
if (!sessionId || !turnId || !pending)
|
|
280
|
+
return result("error", 2, [], "CLI_APPROVAL_NOT_AVAILABLE");
|
|
281
|
+
if (!options.runtime)
|
|
282
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
283
|
+
if (pending.toolName === "run_command" && (decision === "allow_session" || decision === "allow_workspace") && !pending.allowedDecisions?.includes(decision)) {
|
|
284
|
+
return result("error", 2, [], "CLI_COMMAND_APPROVAL_ALLOW_ONCE_ONLY");
|
|
285
|
+
}
|
|
286
|
+
const approvalInput = { sessionId, turnId, approvalId: pending.approvalId, decision };
|
|
287
|
+
if (options.approvalSubmit)
|
|
288
|
+
await options.approvalSubmit(approvalInput);
|
|
289
|
+
else
|
|
290
|
+
await options.runtime.approvals.submit(approvalInput);
|
|
291
|
+
const continueInput = {
|
|
292
|
+
sessionId,
|
|
293
|
+
turnId,
|
|
294
|
+
approval: { approvalId: pending.approvalId, decision: decision === "deny" ? "deny" : "allow" },
|
|
295
|
+
};
|
|
296
|
+
return consume(options.runtime.turns.continue(continueInput));
|
|
297
|
+
}
|
|
298
|
+
/** 取消当前 turn;取消细节由 Core Runtime 和 checkpoint 负责。 */
|
|
299
|
+
async function cancelTurn() {
|
|
300
|
+
if (!sessionId || !turnId)
|
|
301
|
+
return result("error", 2, [], "CLI_ACTIVE_TURN_REQUIRED");
|
|
302
|
+
if (!options.runtime)
|
|
303
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
304
|
+
await options.runtime.turns.cancel({ sessionId, turnId });
|
|
305
|
+
const state = await options.runtime.turns.getRecoverableState({ sessionId, turnId });
|
|
306
|
+
if (state)
|
|
307
|
+
projector.restore(state);
|
|
308
|
+
return result("ok", 0, ["turn cancellation requested"]);
|
|
309
|
+
}
|
|
310
|
+
/** 从 Core 脱敏恢复视图恢复当前投影,并继续同一个 turn。 */
|
|
311
|
+
async function resume() {
|
|
312
|
+
if (!sessionId)
|
|
313
|
+
return result("error", 1, [], "CLI_SESSION_REQUIRED");
|
|
314
|
+
if (!options.runtime)
|
|
315
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
316
|
+
const state = await options.runtime.turns.getRecoverableState({ sessionId, turnId });
|
|
317
|
+
if (!state)
|
|
318
|
+
return result("error", 1, [], "CLI_RECOVERABLE_TURN_NOT_FOUND");
|
|
319
|
+
turnId = state.turnId;
|
|
320
|
+
projector.restore(state);
|
|
321
|
+
if (!state.recoverable)
|
|
322
|
+
return result("error", 1, [], `CLI_TURN_NOT_RESUMABLE: ${state.status}`);
|
|
323
|
+
if (state.status === "awaiting_approval") {
|
|
324
|
+
return result("ok", 0, [
|
|
325
|
+
`approval required: ${state.approval?.approvalId ?? "unknown"}${state.approval ? ` (${state.approval.toolName})` : ""}`,
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
if (state.status === "awaiting_user_input") {
|
|
329
|
+
return result("ok", 0, ["请在支持结构化回答的 Web 中处理"]);
|
|
330
|
+
}
|
|
331
|
+
if (state.status === "interrupted_recoverable" || state.status === "limit_reached") {
|
|
332
|
+
return consume(options.runtime.turns.continue({ sessionId, turnId }));
|
|
333
|
+
}
|
|
334
|
+
return result("error", 1, [], `CLI_TURN_NOT_RESUMABLE: ${state.status}`);
|
|
335
|
+
}
|
|
336
|
+
/** 只读查询当前 Session 的上下文用量安全 DTO;未知输出固定为 unknown,exit 0。 */
|
|
337
|
+
async function showContext(all) {
|
|
338
|
+
if (!options.runtime)
|
|
339
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: context_usage");
|
|
340
|
+
if (!sessionId)
|
|
341
|
+
return result("ok", 0, ["Context: unknown"], { contextView: { usage: null, expanded: all } });
|
|
342
|
+
let usage;
|
|
343
|
+
try {
|
|
344
|
+
usage = await options.runtime.turns.getContextUsage({ sessionId });
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// 查询失败不伪造 0% 或猜测窗口,统一 fail closed 为 unknown。
|
|
348
|
+
usage = null;
|
|
349
|
+
}
|
|
350
|
+
const lines = options.renderer.format === "jsonl"
|
|
351
|
+
? [JSON.stringify({ type: "context_usage_report", usage, expanded: all })]
|
|
352
|
+
: formatPlainContextReport(usage, all);
|
|
353
|
+
return result("ok", 0, lines, { contextView: { usage, expanded: all } });
|
|
354
|
+
}
|
|
355
|
+
/** 让 Core 从 durable checkpoint 读取完整消息并执行一次手动 compact。 */
|
|
356
|
+
async function compact() {
|
|
357
|
+
if (!options.runtime)
|
|
358
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: compact_input");
|
|
359
|
+
if (!sessionId || !turnId)
|
|
360
|
+
return result("error", 2, [], "CLI_ACTIVE_TURN_REQUIRED");
|
|
361
|
+
try {
|
|
362
|
+
const compacted = await options.runtime.turns.compact({
|
|
363
|
+
sessionId,
|
|
364
|
+
turnId,
|
|
365
|
+
trigger: "manual",
|
|
366
|
+
});
|
|
367
|
+
if (compacted.outcome === "skipped") {
|
|
368
|
+
return result("ok", 0, [`context compact skipped: no compactable history; projection unchanged (estimated ${compacted.preTokens} tokens)`]);
|
|
369
|
+
}
|
|
370
|
+
return result("ok", 0, [`context compacted: estimated ${compacted.preTokens} -> ${compacted.postTokens}`]);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
// 活动 Turn 中 manual compact 被 Core 直接拒绝;必须显示明确拒绝而不是伪排队。
|
|
374
|
+
if (error instanceof AgentRuntimeError && error.code === "TURN_ALREADY_RUNNING") {
|
|
375
|
+
return result("error", 1, [], "TURN_ALREADY_RUNNING: 当前 Turn 仍在运行,请等待结束或中断后再压缩");
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** 仅由显式 slash action 调用 Core 的脱敏 Undo preview;模型 prompt 永远不会走此路径。 */
|
|
381
|
+
async function previewUndo() {
|
|
382
|
+
if (!sessionId)
|
|
383
|
+
return result("error", 2, [], "CLI_SESSION_REQUIRED");
|
|
384
|
+
if (!options.runtime?.undo)
|
|
385
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: undo");
|
|
386
|
+
const preview = await options.runtime.undo.preview({ sessionId });
|
|
387
|
+
pendingUndoTurnId = preview.status === "available" ? preview.turnId : undefined;
|
|
388
|
+
if (preview.status !== "available")
|
|
389
|
+
return result("error", 2, [], `UNDO_NOT_AVAILABLE: ${preview.reason ?? "UNKNOWN"}`);
|
|
390
|
+
return result("ok", 0, [
|
|
391
|
+
`undo preview: turn ${preview.turnId}`,
|
|
392
|
+
...preview.files.map(function formatFile(file) { return `- ${file.operation}: ${file.path}`; }),
|
|
393
|
+
"输入 /undo confirm 执行;不会撤销命令、MCP、网络或数据库副作用。",
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
/** 确认时重新由 Core 校验所有 SHA;preview 过期、外部修改或无 preview 都 fail closed。 */
|
|
397
|
+
async function confirmUndo() {
|
|
398
|
+
if (!sessionId)
|
|
399
|
+
return result("error", 2, [], "CLI_SESSION_REQUIRED");
|
|
400
|
+
if (!pendingUndoTurnId || !options.runtime?.undo)
|
|
401
|
+
return result("error", 2, [], "UNDO_CONFIRMATION_REQUIRED");
|
|
402
|
+
const current = await options.runtime.undo.preview({ sessionId });
|
|
403
|
+
if (current.status !== "available" || current.turnId !== pendingUndoTurnId) {
|
|
404
|
+
pendingUndoTurnId = undefined;
|
|
405
|
+
return result("error", 2, [], "UNDO_CONFLICT");
|
|
406
|
+
}
|
|
407
|
+
const completed = await options.runtime.undo.execute({ sessionId, confirmed: true });
|
|
408
|
+
pendingUndoTurnId = undefined;
|
|
409
|
+
if (completed.status !== "completed")
|
|
410
|
+
return result("error", 1, [], `UNDO_FAILED: ${completed.conflicts.join(",") || "compensation required"}`);
|
|
411
|
+
return result("ok", 0, [`undo completed: ${completed.restoredPaths.join(", ")}`]);
|
|
412
|
+
}
|
|
413
|
+
/** 返回 settings/capability 允许的模型目录,并标记 Session 下一 Turn 默认值。 */
|
|
414
|
+
async function openModelPicker() {
|
|
415
|
+
const session = await getSession();
|
|
416
|
+
const statusView = await getStatusView();
|
|
417
|
+
const selected = session?.defaultRunConfig?.modelRef ?? options.getRuntimeStatusDefaults?.().modelRef;
|
|
418
|
+
const modelOptions = getModelCapabilities().map(function toModelOption(capability) {
|
|
419
|
+
return { provider: capability.provider, modelId: capability.modelId, selected: selected?.provider === capability.provider && selected.modelId === capability.modelId };
|
|
420
|
+
});
|
|
421
|
+
if (modelOptions.length === 0)
|
|
422
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: model_catalog");
|
|
423
|
+
return result("ok", 0, [
|
|
424
|
+
"models:",
|
|
425
|
+
...modelOptions.map(function formatModel(option, index) { return `${index + 1}. ${option.provider}/${option.modelId}${option.selected ? " (selected)" : ""}`; }),
|
|
426
|
+
"interactive: ↑↓ select · Enter save · Esc cancel; plain: /model <model>",
|
|
427
|
+
], { statusView, modelOptions });
|
|
428
|
+
}
|
|
429
|
+
/** 校验模型与既有 CC effort 请求的组合后,写入仅供下一 Turn 冻结的 Session 默认模型。 */
|
|
430
|
+
async function setModel(modelId, provider) {
|
|
431
|
+
if (!sessionId)
|
|
432
|
+
return result("error", 2, [], "CLI_SESSION_REQUIRED");
|
|
433
|
+
if (!options.runtime?.sessions)
|
|
434
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
435
|
+
const capability = findModelCapability(modelId, provider);
|
|
436
|
+
if (!capability)
|
|
437
|
+
return result("error", 2, [], "MODEL_NOT_SUPPORTED");
|
|
438
|
+
const session = await options.runtime.sessions.get(sessionId);
|
|
439
|
+
if (!session)
|
|
440
|
+
return result("error", 1, [], "SESSION_NOT_FOUND");
|
|
441
|
+
const currentEffort = session.defaultRunConfig?.reasoningProfile?.effort;
|
|
442
|
+
if (currentEffort && !capability.requestableEfforts.includes(currentEffort))
|
|
443
|
+
return result("error", 2, [], "REASONING_EFFORT_NOT_SUPPORTED");
|
|
444
|
+
await options.runtime.sessions.setDefaultRunConfig(sessionId, {
|
|
445
|
+
...(session.defaultRunConfig ?? {}),
|
|
446
|
+
modelRef: { provider: capability.provider, modelId: capability.modelId },
|
|
447
|
+
});
|
|
448
|
+
const statusView = await getStatusView();
|
|
449
|
+
const suffix = statusView.source === "turn_frozen" ? ";当前 Turn 保持 frozen,下一个 Turn 生效" : ";下一个 Turn 生效";
|
|
450
|
+
return result("ok", 0, [`model: ${capability.provider}/${capability.modelId}${suffix}`], { statusView });
|
|
451
|
+
}
|
|
452
|
+
/** 返回当前模型可明确请求的 CC effort 目录;端点不支持时由其返回明确错误。 */
|
|
453
|
+
async function openEffortPicker() {
|
|
454
|
+
const session = await getSession();
|
|
455
|
+
const statusView = await getStatusView();
|
|
456
|
+
const modelRef = session?.defaultRunConfig?.modelRef ?? options.getRuntimeStatusDefaults?.().modelRef;
|
|
457
|
+
const capability = modelRef ? findModelCapability(modelRef.modelId, modelRef.provider) : undefined;
|
|
458
|
+
if (!capability)
|
|
459
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: effort_catalog");
|
|
460
|
+
const selectedEffort = session?.defaultRunConfig?.reasoningProfile?.effort ?? options.getRuntimeStatusDefaults?.().reasoningProfile?.effort;
|
|
461
|
+
const effortOptions = capability.requestableEfforts.map(function toEffortOption(effort) {
|
|
462
|
+
return { effort, selected: effort === selectedEffort };
|
|
463
|
+
});
|
|
464
|
+
return result("ok", 0, [
|
|
465
|
+
`efforts for ${capability.provider}/${capability.modelId}:`,
|
|
466
|
+
...effortOptions.map(function formatEffort(option, index) { return `${index + 1}. ${option.effort}${option.selected ? " (selected)" : ""}`; }),
|
|
467
|
+
"interactive: ↑↓ select · Enter save · Esc cancel; plain: /effort <effort>",
|
|
468
|
+
], { statusView, effortOptions });
|
|
469
|
+
}
|
|
470
|
+
/** 只接受当前 settings 目录允许的 effort,并写入下一 Turn 的 Session 默认配置。 */
|
|
471
|
+
async function setEffort(effort) {
|
|
472
|
+
if (!sessionId)
|
|
473
|
+
return result("error", 2, [], "CLI_SESSION_REQUIRED");
|
|
474
|
+
if (!options.runtime?.sessions)
|
|
475
|
+
return result("error", 1, [], "CLI_RUNTIME_NOT_ATTACHED");
|
|
476
|
+
const session = await options.runtime.sessions.get(sessionId);
|
|
477
|
+
if (!session)
|
|
478
|
+
return result("error", 1, [], "SESSION_NOT_FOUND");
|
|
479
|
+
const modelRef = session.defaultRunConfig?.modelRef ?? options.getRuntimeStatusDefaults?.().modelRef;
|
|
480
|
+
const capability = modelRef ? findModelCapability(modelRef.modelId, modelRef.provider) : undefined;
|
|
481
|
+
if (!capability || !capability.requestableEfforts.includes(effort))
|
|
482
|
+
return result("error", 2, [], "REASONING_EFFORT_NOT_SUPPORTED");
|
|
483
|
+
await options.runtime.sessions.setDefaultRunConfig(sessionId, {
|
|
484
|
+
...(session.defaultRunConfig ?? {}),
|
|
485
|
+
reasoningProfile: { effort: effort },
|
|
486
|
+
});
|
|
487
|
+
const statusView = await getStatusView();
|
|
488
|
+
const suffix = statusView.source === "turn_frozen" ? ";当前 Turn 保持 frozen,下一个 Turn 生效" : ";下一个 Turn 生效";
|
|
489
|
+
return result("ok", 0, [`effort: ${effort}${suffix}`], { statusView });
|
|
490
|
+
}
|
|
491
|
+
/** 通过 Core Public API 读取当前 turn 的脱敏冻结配置。 */
|
|
492
|
+
async function getFrozenRunConfig() {
|
|
493
|
+
if (!options.runtime)
|
|
494
|
+
return undefined;
|
|
495
|
+
if (!sessionId || !turnId)
|
|
496
|
+
return undefined;
|
|
497
|
+
return options.runtime.turns.getFrozenRunConfig({ sessionId, turnId });
|
|
498
|
+
}
|
|
499
|
+
/** 通过 Composition Root 校验 canonical Skill 并只保存身份/hash。 */
|
|
500
|
+
async function selectSkill(skill) {
|
|
501
|
+
if (!options.resolveSkill)
|
|
502
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: skill_selector");
|
|
503
|
+
try {
|
|
504
|
+
const resolved = await options.resolveSkill(skill);
|
|
505
|
+
selectedSkills = [...selectedSkills.filter(function keepSkill(item) { return item.skillId !== resolved.skillId; }), { ...resolved, qualifiedName: skill }];
|
|
506
|
+
return result("ok", 0, [`skill selected: ${skill}`]);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return result("error", 1, [], "CLI_SKILL_NOT_FOUND");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** 切换一个 canonical Skill 的上下文钉住状态,并把最新安全列表返回给 TUI。 */
|
|
513
|
+
async function toggleSkill(skill) {
|
|
514
|
+
if (!options.resolveSkill || !options.listSkills)
|
|
515
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: skill_selector");
|
|
516
|
+
try {
|
|
517
|
+
const resolved = await options.resolveSkill(skill);
|
|
518
|
+
const isSelected = selectedSkills.some(function isSameSkill(item) { return item.skillId === resolved.skillId; });
|
|
519
|
+
selectedSkills = isSelected
|
|
520
|
+
? selectedSkills.filter(function removeSkill(item) { return item.skillId !== resolved.skillId; })
|
|
521
|
+
: [...selectedSkills, { ...resolved, qualifiedName: skill }];
|
|
522
|
+
const skills = await options.listSkills();
|
|
523
|
+
const skillOptions = skills.map(function markSelected(entry) {
|
|
524
|
+
return { ...entry, selected: selectedSkills.some(function isSelectedEntry(item) { return item.qualifiedName === entry.qualifiedName; }) };
|
|
525
|
+
});
|
|
526
|
+
return result("ok", 0, [isSelected ? `skill disabled: ${skill}` : `skill enabled: ${skill}`], { skillOptions });
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return result("error", 1, [], "CLI_SKILL_NOT_FOUND");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** 展示可选择的 Skill 安全摘要,避免要求用户记忆 canonical 名称。 */
|
|
533
|
+
async function listSkills() {
|
|
534
|
+
if (!options.listSkills)
|
|
535
|
+
return result("unavailable", 1, [], "CAPABILITY_UNAVAILABLE: skill_catalog");
|
|
536
|
+
try {
|
|
537
|
+
const skills = (await options.listSkills()).map(function markSelected(entry) {
|
|
538
|
+
return { ...entry, selected: selectedSkills.some(function isSelectedEntry(item) { return item.qualifiedName === entry.qualifiedName; }) };
|
|
539
|
+
});
|
|
540
|
+
if (skills.length === 0)
|
|
541
|
+
return result("ok", 0, ["no skills available"], { skillOptions: skills });
|
|
542
|
+
return result("ok", 0, [
|
|
543
|
+
"available skills:",
|
|
544
|
+
...skills.map(function formatSkill(skill, index) {
|
|
545
|
+
const description = skill.description.trim() ? ` — ${skill.description.trim()}` : "";
|
|
546
|
+
return `${index + 1}. ${skill.displayName} (${skill.qualifiedName})${description}`;
|
|
547
|
+
}),
|
|
548
|
+
"use /skill <canonical name> to select one",
|
|
549
|
+
], { skillOptions: skills });
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return result("error", 1, [], "CLI_SKILL_CATALOG_UNAVAILABLE");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** 返回绑定当前 Work Session 的 Job 服务;跨 Session/Workspace 校验继续由 Core 执行。 */
|
|
556
|
+
function getSessionJobs() {
|
|
557
|
+
if (!sessionId)
|
|
558
|
+
throw new AgentRuntimeError("SESSION_NOT_FOUND", "CLI Session 不存在");
|
|
559
|
+
if (!options.runtime || !options.workspaceRoot)
|
|
560
|
+
throw new AgentRuntimeError("WORKSPACE_REQUIRED", "CLI 工作区不可用");
|
|
561
|
+
return options.runtime.jobs.forSession({ workspaceId: options.workspaceRoot, sessionId });
|
|
562
|
+
}
|
|
563
|
+
/** 将 Job 管理错误收口为稳定 CLI 结果,不向 TUI 暴露底层异常。 */
|
|
564
|
+
function jobError(error) {
|
|
565
|
+
const message = error instanceof AgentRuntimeError ? error.code : "CLI_RUNTIME_ERROR";
|
|
566
|
+
return result("error", 1, [], message);
|
|
567
|
+
}
|
|
568
|
+
/** 列出当前 Session 的安全 Job 摘要;该操作可与活动 Turn 并发。 */
|
|
569
|
+
async function listJobs() {
|
|
570
|
+
try {
|
|
571
|
+
const list = await getSessionJobs().list({ limit: 100 });
|
|
572
|
+
return result("ok", 0, [JSON.stringify(list)], { jobView: { mode: "list", list } });
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
return jobError(error);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** 读取当前 Session 的安全 Job 详情,不读取日志正文。 */
|
|
579
|
+
async function showJob(jobId) {
|
|
580
|
+
try {
|
|
581
|
+
const read = await getSessionJobs().read({ jobId, waitMs: 0 });
|
|
582
|
+
return result("ok", 0, [JSON.stringify({ job: read.job })], { jobView: { mode: "detail", job: read.job } });
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
return jobError(error);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/** 读取当前 Session 的有限安全双流日志;不会等待或停止 Job。 */
|
|
589
|
+
async function readJobOutput(jobId) {
|
|
590
|
+
try {
|
|
591
|
+
const read = await getSessionJobs().read({ jobId, waitMs: 0 });
|
|
592
|
+
return result("ok", 0, [JSON.stringify(read)], { jobView: { mode: "detail", job: read.job, output: read.output } });
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
return jobError(error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** 根据用户明确给出的 Job ID 停止当前 Runtime 拥有的任务,并返回真实状态。 */
|
|
599
|
+
async function stopJob(jobId) {
|
|
600
|
+
try {
|
|
601
|
+
const job = await getSessionJobs().stop({ jobId, source: "host" });
|
|
602
|
+
return result("ok", 0, [JSON.stringify({ job })], { jobView: { mode: "detail", job } });
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
return jobError(error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/** 处理宿主 Action;未进入对应 Runtime Step 的能力返回明确 unavailable。 */
|
|
609
|
+
async function dispatch(action) {
|
|
610
|
+
if (action.type === "submit_prompt")
|
|
611
|
+
return submitPrompt(action.content, action.delivery);
|
|
612
|
+
if (action.type === "approve")
|
|
613
|
+
return approve(action.decision);
|
|
614
|
+
if (action.type === "cancel_turn")
|
|
615
|
+
return cancelTurn();
|
|
616
|
+
if (action.type === "compact")
|
|
617
|
+
return compact();
|
|
618
|
+
if (action.type === "show_context")
|
|
619
|
+
return showContext(action.all);
|
|
620
|
+
if (action.type === "undo_preview")
|
|
621
|
+
return previewUndo();
|
|
622
|
+
if (action.type === "undo_confirm")
|
|
623
|
+
return confirmUndo();
|
|
624
|
+
if (action.type === "select_skill")
|
|
625
|
+
return selectSkill(action.skill);
|
|
626
|
+
if (action.type === "toggle_skill")
|
|
627
|
+
return toggleSkill(action.skill);
|
|
628
|
+
if (action.type === "list_skills")
|
|
629
|
+
return listSkills();
|
|
630
|
+
if (action.type === "open_permissions")
|
|
631
|
+
return openPermissions();
|
|
632
|
+
if (action.type === "set_permission_mode")
|
|
633
|
+
return setPermissionMode(action.mode);
|
|
634
|
+
if (action.type === "open_model_picker")
|
|
635
|
+
return openModelPicker();
|
|
636
|
+
if (action.type === "set_model")
|
|
637
|
+
return setModel(action.modelId, action.provider);
|
|
638
|
+
if (action.type === "open_effort_picker")
|
|
639
|
+
return openEffortPicker();
|
|
640
|
+
if (action.type === "set_effort")
|
|
641
|
+
return setEffort(action.effort);
|
|
642
|
+
if (action.type === "list_jobs")
|
|
643
|
+
return listJobs();
|
|
644
|
+
if (action.type === "show_job")
|
|
645
|
+
return showJob(action.jobId);
|
|
646
|
+
if (action.type === "read_job_output")
|
|
647
|
+
return readJobOutput(action.jobId);
|
|
648
|
+
if (action.type === "stop_job")
|
|
649
|
+
return stopJob(action.jobId);
|
|
650
|
+
if (action.type === "exit") {
|
|
651
|
+
if (projector.getProjection().status === "running" || projector.getProjection().status === "waiting_approval") {
|
|
652
|
+
await cancelTurn();
|
|
653
|
+
}
|
|
654
|
+
return result("exit", 0, []);
|
|
655
|
+
}
|
|
656
|
+
return result("error", 2, [], "CLI_UNKNOWN_ACTION");
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
dispatch,
|
|
660
|
+
consume,
|
|
661
|
+
resume,
|
|
662
|
+
getProjection: function getProjection() { return projector.getProjection(); },
|
|
663
|
+
getStatusView,
|
|
664
|
+
setSession: function setSession(nextSessionId) {
|
|
665
|
+
sessionId = nextSessionId;
|
|
666
|
+
projector.setSessionId(nextSessionId);
|
|
667
|
+
},
|
|
668
|
+
setEventListener: function setEventListener(listener) {
|
|
669
|
+
eventListener = listener;
|
|
670
|
+
},
|
|
671
|
+
setQueuedInputListener: function setQueuedInputListener(listener) {
|
|
672
|
+
queuedInputListener = listener;
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** 将 argv 命令转换为 Step 5.2 可执行范围内的 Controller Action。 */
|
|
677
|
+
export function commandToAction(command) {
|
|
678
|
+
if (command.type === "chat" && command.prompt)
|
|
679
|
+
return { type: "submit_prompt", content: command.prompt };
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|