@epoch-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 +25 -0
- package/dist/{chunk-3SCZQI5W.js → chunk-UEHBHZCZ.js} +6 -1
- package/dist/{env-file-JUOFDESY.js → env-file-O6P454IQ.js} +1 -1
- package/dist/index.js +1073 -981
- package/dist/tui-entry.js +817 -290
- package/package.json +7 -7
package/dist/tui-entry.js
CHANGED
|
@@ -1,55 +1,19 @@
|
|
|
1
1
|
// src/tui-entry.ts
|
|
2
2
|
import { getCapability as getCapability2, loadKeybindings } from "@epoch-agent/core";
|
|
3
|
-
import { keybindingsPath } from "@epoch-agent/infra";
|
|
4
|
-
import { diagnosticToLine } from "@epoch-agent/protocol";
|
|
3
|
+
import { keybindingsPath, t as t14 } from "@epoch-agent/infra";
|
|
5
4
|
import { buildRuntime, withModelScope, withToolScope } from "@epoch-agent/runtime";
|
|
6
5
|
import { DEFAULT_KEYBINDINGS, renderApp } from "@epoch-agent/tui";
|
|
7
|
-
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
import { t } from "@epoch-agent/infra";
|
|
8
8
|
// src/exit-codes.ts
|
|
9
9
|
var EXIT_CODES = {
|
|
10
|
-
/** 任务完成 */
|
|
11
10
|
SUCCESS: 0,
|
|
12
|
-
/** 通用失败(provider 起不来、运行时报错……) */
|
|
13
11
|
FAILURE: 1,
|
|
14
|
-
/**
|
|
15
|
-
* 非交互下有操作因**缺少预授权**被拒。
|
|
16
|
-
*
|
|
17
|
-
* 只在非交互路径出现:交互模式下用户亲手点的「拒绝」是他的决定,
|
|
18
|
-
* 不算 agent 被卡住,仍然退 0。
|
|
19
|
-
*/
|
|
20
12
|
PERMISSION_DENIED: 3,
|
|
21
|
-
/**
|
|
22
|
-
* 企业托管设置挡下了这次启动(方案 22)。
|
|
23
|
-
*
|
|
24
|
-
* 和 FAILURE 分开的理由同上:这不是「epoch 坏了」,是**策略生效了**。
|
|
25
|
-
* 管理员按机器批量铺开一条 `disableBypassPermissionsMode` 之后,
|
|
26
|
-
* 要能从退出码上一眼看出哪些机器是被自己的策略拦下的,
|
|
27
|
-
* 而不是去每台机器上读 stderr 猜。
|
|
28
|
-
*/
|
|
29
13
|
MANAGED_POLICY: 4,
|
|
30
|
-
/**
|
|
31
|
-
* `--max-turns` / `--max-budget-usd` 触顶,任务**没做完**就停了(方案 28)。
|
|
32
|
-
*
|
|
33
|
-
* 和 FAILURE 分开:CI 里这不是「epoch 坏了」,是**限额生效了** ——
|
|
34
|
-
* 该做的是看一眼输出决定要不要加额重跑,而不是去查日志。
|
|
35
|
-
* 跨会话预算(`budget.maxCostUsd`)触顶**不走这个码**,仍然退 0:
|
|
36
|
-
* 那是长期配置,每次都退非 0 会让流水线天天红。
|
|
37
|
-
*
|
|
38
|
-
* ⚠️ 方案 28 立项时写的是 `4`,那是 2026-08-08 的现状(当时只有 0/1/3)。
|
|
39
|
-
* `4` 后来被企业托管策略(方案 22)占了,而那个码已经在根 README 和
|
|
40
|
-
* MDM 文档里对外承诺过,不能挪。所以这里往后排。
|
|
41
|
-
*/
|
|
42
14
|
LIMIT_EXCEEDED: 5,
|
|
43
|
-
/**
|
|
44
|
-
* `--input-format stream-json` 的输入有坏行(方案 28)。
|
|
45
|
-
*
|
|
46
|
-
* **不是**用法错误(那是 1):命令敲对了,是宿主写进 stdin 的那一行不合协议。
|
|
47
|
-
* 坏行会被报到 stderr 并**跳过**(NDJSON 是按行分帧的,跳一行不会失步),
|
|
48
|
-
* 进程接着跑;这个码在收尾时才落。
|
|
49
|
-
*/
|
|
50
15
|
INPUT_ERROR: 6
|
|
51
16
|
};
|
|
52
|
-
|
|
53
17
|
// src/errors.ts
|
|
54
18
|
var CliError = class extends Error {
|
|
55
19
|
constructor(message, exitCode = EXIT_CODES.FAILURE, hint) {
|
|
@@ -63,15 +27,12 @@ var CliError = class extends Error {
|
|
|
63
27
|
};
|
|
64
28
|
function isCancellation(err) {
|
|
65
29
|
if (!(err instanceof Error)) return false;
|
|
66
|
-
return err.name === "ExitPromptError" || err.name === "AbortError" ||
|
|
30
|
+
return err.name === "ExitPromptError" || err.name === "AbortError" ||
|
|
67
31
|
err.name === "Error" && err.message === "The operation was aborted.";
|
|
68
32
|
}
|
|
69
33
|
var EXPECTED_FAILURES = {
|
|
70
|
-
/** 企业托管策略挡下的启动(方案 22 §2.6)。不是故障,是策略生效了 */
|
|
71
34
|
ManagedPolicyError: EXIT_CODES.MANAGED_POLICY,
|
|
72
|
-
/** `--agent` 给了不认识的角色名(方案 29 验收 #13)。可用角色都在 message 里 */
|
|
73
35
|
AgentRoleError: EXIT_CODES.FAILURE,
|
|
74
|
-
/** `--settings` 指的文件不是合法 JSON(方案 29 验收 #16)。hint 里带行号 */
|
|
75
36
|
SettingsFileError: EXIT_CODES.FAILURE
|
|
76
37
|
};
|
|
77
38
|
function expectedFailureExitCode(err) {
|
|
@@ -84,12 +45,14 @@ function hintOf(err) {
|
|
|
84
45
|
}
|
|
85
46
|
function reportFatal(err) {
|
|
86
47
|
if (isCancellation(err)) {
|
|
87
|
-
process.stderr.write(
|
|
48
|
+
process.stderr.write(`
|
|
49
|
+
${t("errors.cancelled")}
|
|
50
|
+
`);
|
|
88
51
|
process.exit(130);
|
|
89
52
|
}
|
|
90
53
|
const exitCode = err instanceof CliError ? err.exitCode : expectedFailureExitCode(err);
|
|
91
54
|
if (exitCode !== void 0 && err instanceof Error) {
|
|
92
|
-
process.stderr.write(
|
|
55
|
+
process.stderr.write(`${t("errors.fatal", { message: err.message })}
|
|
93
56
|
`);
|
|
94
57
|
const hint = hintOf(err);
|
|
95
58
|
if (hint) process.stderr.write(`${hint}
|
|
@@ -100,7 +63,6 @@ function reportFatal(err) {
|
|
|
100
63
|
`);
|
|
101
64
|
process.exit(EXIT_CODES.FAILURE);
|
|
102
65
|
}
|
|
103
|
-
|
|
104
66
|
// src/import-prompt.ts
|
|
105
67
|
import {
|
|
106
68
|
createImportGate,
|
|
@@ -112,8 +74,7 @@ import {
|
|
|
112
74
|
setImportGate,
|
|
113
75
|
TrustManager as TrustManager2
|
|
114
76
|
} from "@epoch-agent/core";
|
|
115
|
-
import { trustedImportsPath, trustPath as trustPath2 } from "@epoch-agent/infra";
|
|
116
|
-
|
|
77
|
+
import { t as t3, trustedImportsPath, trustPath as trustPath2 } from "@epoch-agent/infra";
|
|
117
78
|
// src/trust-prompt.ts
|
|
118
79
|
import {
|
|
119
80
|
findProjectInstructions,
|
|
@@ -122,7 +83,7 @@ import {
|
|
|
122
83
|
resolveProjectRoot,
|
|
123
84
|
TrustManager
|
|
124
85
|
} from "@epoch-agent/core";
|
|
125
|
-
import { trustPath } from "@epoch-agent/infra";
|
|
86
|
+
import { t as t2, trustPath } from "@epoch-agent/infra";
|
|
126
87
|
function trustPromptTarget(deps) {
|
|
127
88
|
if (!deps.gateEnabled) return null;
|
|
128
89
|
if (!deps.interactive) return null;
|
|
@@ -135,30 +96,31 @@ function trustPromptTarget(deps) {
|
|
|
135
96
|
function applyTrustChoice(store, root, choice) {
|
|
136
97
|
if (choice.kind === "trust") {
|
|
137
98
|
store.record(root, "trusted", choice.scope);
|
|
138
|
-
const suffix = choice.scope === "directory-tree" ? "
|
|
139
|
-
return
|
|
99
|
+
const suffix = choice.scope === "directory-tree" ? t2("trust_prompt.granted_tree_suffix") : "";
|
|
100
|
+
return t2("trust_prompt.granted", { root, suffix });
|
|
140
101
|
}
|
|
141
102
|
if (choice.kind === "never") {
|
|
142
103
|
store.record(root, "untrusted", "directory");
|
|
143
|
-
return
|
|
104
|
+
return t2("trust_prompt.denied", { root });
|
|
144
105
|
}
|
|
145
|
-
return "
|
|
106
|
+
return t2("trust_prompt.skipped");
|
|
146
107
|
}
|
|
147
108
|
function trustPromptMessage(target) {
|
|
148
109
|
return [
|
|
149
110
|
`
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
" \u6307\u6325\u4F60\u7684 agent\u3002\u6240\u4EE5\u672A\u7ECF\u786E\u8BA4\u4E0D\u4F1A\u52A0\u8F7D\u3002",
|
|
111
|
+
${t2("trust_prompt.head", { path: target.instructionsPath })}`,
|
|
112
|
+
` ${t2("trust_prompt.why")}`,
|
|
153
113
|
""
|
|
154
114
|
].join("\n");
|
|
155
115
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
116
|
+
function choices() {
|
|
117
|
+
return [
|
|
118
|
+
{ name: t2("trust_prompt.choice_skip"), value: { kind: "skip" } },
|
|
119
|
+
{ name: t2("trust_prompt.choice_dir"), value: { kind: "trust", scope: "directory" } },
|
|
120
|
+
{ name: t2("trust_prompt.choice_tree"), value: { kind: "trust", scope: "directory-tree" } },
|
|
121
|
+
{ name: t2("trust_prompt.choice_never"), value: { kind: "never" } }
|
|
122
|
+
];
|
|
123
|
+
}
|
|
162
124
|
function restoreStdinDefault() {
|
|
163
125
|
const stdin = process.stdin;
|
|
164
126
|
if (stdin.isTTY) stdin.setRawMode?.(false);
|
|
@@ -186,23 +148,27 @@ async function maybePromptForTrust(opts = {}) {
|
|
|
186
148
|
try {
|
|
187
149
|
print(applyTrustChoice(store, target.root, choice));
|
|
188
150
|
} catch (err) {
|
|
189
|
-
print(
|
|
151
|
+
print(
|
|
152
|
+
t2("trust_prompt.write_failed", {
|
|
153
|
+
message: err instanceof Error ? err.message : String(err)
|
|
154
|
+
})
|
|
155
|
+
);
|
|
190
156
|
}
|
|
191
157
|
return true;
|
|
192
158
|
}
|
|
193
159
|
async function askTrustChoice(_target) {
|
|
194
160
|
const { select } = await import("@inquirer/prompts");
|
|
195
161
|
try {
|
|
162
|
+
const options = choices();
|
|
196
163
|
return await select({
|
|
197
|
-
message: "
|
|
198
|
-
choices: [...
|
|
199
|
-
default:
|
|
164
|
+
message: t2("trust_prompt.ask"),
|
|
165
|
+
choices: [...options],
|
|
166
|
+
default: options[0]?.value
|
|
200
167
|
});
|
|
201
168
|
} catch {
|
|
202
169
|
return null;
|
|
203
170
|
}
|
|
204
171
|
}
|
|
205
|
-
|
|
206
172
|
// src/import-prompt.ts
|
|
207
173
|
function externalImportTargets(deps) {
|
|
208
174
|
if (!deps.gateEnabled) return [];
|
|
@@ -228,35 +194,36 @@ function externalImportTargets(deps) {
|
|
|
228
194
|
function importPromptMessage(target) {
|
|
229
195
|
return [
|
|
230
196
|
`
|
|
231
|
-
|
|
197
|
+
${t3("import_prompt.head", { from: target.from })}`,
|
|
232
198
|
` ${target.path}`,
|
|
233
|
-
`
|
|
234
|
-
|
|
235
|
-
" \u52A0\u8F7D\u5B83\u7B49\u4E8E\u628A\u4E00\u4EFD\u6CA1\u88AB\u5BA1\u8FC7\u7684\u5185\u5BB9\u62FC\u8FDB system prompt\u3002",
|
|
199
|
+
` ${t3("import_prompt.spec", { spec: target.spec })}`,
|
|
200
|
+
` ${t3("import_prompt.why")}`,
|
|
236
201
|
""
|
|
237
202
|
].join("\n");
|
|
238
203
|
}
|
|
239
204
|
function applyImportChoice(store, target, choice) {
|
|
240
205
|
if (choice.kind === "once") {
|
|
241
206
|
store.allowOnce(target.path);
|
|
242
|
-
return
|
|
207
|
+
return t3("import_prompt.once", { path: target.path });
|
|
243
208
|
}
|
|
244
209
|
if (choice.kind === "remember") {
|
|
245
210
|
store.remember(target.path);
|
|
246
|
-
return
|
|
211
|
+
return t3("import_prompt.remembered", { path: target.path });
|
|
247
212
|
}
|
|
248
213
|
if (choice.kind === "never") {
|
|
249
214
|
store.denyAll();
|
|
250
|
-
return "
|
|
215
|
+
return t3("import_prompt.never");
|
|
251
216
|
}
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
217
|
+
return t3("import_prompt.skipped", { path: target.path });
|
|
218
|
+
}
|
|
219
|
+
function choices2() {
|
|
220
|
+
return [
|
|
221
|
+
{ name: t3("import_prompt.choice_skip"), value: { kind: "skip" } },
|
|
222
|
+
{ name: t3("import_prompt.choice_once"), value: { kind: "once" } },
|
|
223
|
+
{ name: t3("import_prompt.choice_remember"), value: { kind: "remember" } },
|
|
224
|
+
{ name: t3("import_prompt.choice_never"), value: { kind: "never" } }
|
|
225
|
+
];
|
|
226
|
+
}
|
|
260
227
|
async function maybePromptForExternalImports(opts = {}) {
|
|
261
228
|
const config = opts.config ?? loadConfig2();
|
|
262
229
|
const store = opts.store ?? new ImportTrustStore(trustedImportsPath(config.homeDir));
|
|
@@ -281,7 +248,9 @@ async function maybePromptForExternalImports(opts = {}) {
|
|
|
281
248
|
print(applyImportChoice(store, target, choice));
|
|
282
249
|
} catch (err) {
|
|
283
250
|
print(
|
|
284
|
-
|
|
251
|
+
t3("import_prompt.write_failed", {
|
|
252
|
+
message: err instanceof Error ? err.message : String(err)
|
|
253
|
+
})
|
|
285
254
|
);
|
|
286
255
|
}
|
|
287
256
|
if (choice.kind === "never") break;
|
|
@@ -295,16 +264,123 @@ async function maybePromptForExternalImports(opts = {}) {
|
|
|
295
264
|
async function askImportChoice(_target) {
|
|
296
265
|
const { select } = await import("@inquirer/prompts");
|
|
297
266
|
try {
|
|
267
|
+
const options = choices2();
|
|
298
268
|
return await select({
|
|
299
|
-
message: "
|
|
300
|
-
choices: [...
|
|
301
|
-
default:
|
|
269
|
+
message: t3("import_prompt.ask"),
|
|
270
|
+
choices: [...options],
|
|
271
|
+
default: options[0]?.value
|
|
302
272
|
});
|
|
303
273
|
} catch {
|
|
304
274
|
return null;
|
|
305
275
|
}
|
|
306
276
|
}
|
|
307
|
-
|
|
277
|
+
// src/keybindings-view.ts
|
|
278
|
+
import { t as t4 } from "@epoch-agent/infra";
|
|
279
|
+
import {
|
|
280
|
+
ACTION_CONTEXTS,
|
|
281
|
+
KEY_ACTIONS,
|
|
282
|
+
RESERVED_CHORDS,
|
|
283
|
+
sequenceId
|
|
284
|
+
} from "@epoch-agent/protocol";
|
|
285
|
+
import { SEQUENCE_WINDOW_MS } from "@epoch-agent/tui";
|
|
286
|
+
var CONTEXT_TITLE_KEYS = {
|
|
287
|
+
global: "keybindings.list_context_global",
|
|
288
|
+
input: "keybindings.list_context_input",
|
|
289
|
+
dialog: "keybindings.list_context_dialog"
|
|
290
|
+
};
|
|
291
|
+
var REJECT_KEYS = {
|
|
292
|
+
reserved: "keybindings.list_rejected_reserved",
|
|
293
|
+
conflict: "keybindings.list_rejected_conflict",
|
|
294
|
+
invalid: "keybindings.list_rejected_invalid",
|
|
295
|
+
"unknown-action": "keybindings.list_rejected_unknown",
|
|
296
|
+
unsupported: "keybindings.list_rejected_unsupported"
|
|
297
|
+
};
|
|
298
|
+
function groupOf(action) {
|
|
299
|
+
return ACTION_CONTEXTS[action][0] ?? "global";
|
|
300
|
+
}
|
|
301
|
+
function describeKeybindings(report2) {
|
|
302
|
+
const lines = [t4("keybindings.list_title", { path: report2.path })];
|
|
303
|
+
if (!report2.present) lines.push(t4("keybindings.list_no_file"));
|
|
304
|
+
const width = Math.max(...KEY_ACTIONS.map((a) => a.length)) + 2;
|
|
305
|
+
const keysWidth = Math.max(
|
|
306
|
+
...KEY_ACTIONS.map((a) => keysTextOf(report2, a).length),
|
|
307
|
+
t4("keybindings.list_unbound").length
|
|
308
|
+
) + 2;
|
|
309
|
+
for (const context of ["global", "input", "dialog"]) {
|
|
310
|
+
const actions = KEY_ACTIONS.filter((action) => groupOf(action) === context);
|
|
311
|
+
if (actions.length === 0) continue;
|
|
312
|
+
lines.push("", t4(CONTEXT_TITLE_KEYS[context]));
|
|
313
|
+
for (const action of actions) {
|
|
314
|
+
lines.push(
|
|
315
|
+
` ${action.padEnd(width)}${keysTextOf(report2, action).padEnd(keysWidth)}${sourceTextOf(report2, action)}`.trimEnd()
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (report2.rejected.length > 0) {
|
|
320
|
+
lines.push("", t4("keybindings.list_rejected_title"));
|
|
321
|
+
for (const item of report2.rejected) {
|
|
322
|
+
lines.push(` ${item.action.padEnd(width)}${rejectText(item)}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
lines.push(
|
|
326
|
+
"",
|
|
327
|
+
t4("keybindings.list_reserved_note", { keys: RESERVED_CHORDS.join(" \xB7 ") }),
|
|
328
|
+
t4("keybindings.list_sequence_note", { ms: SEQUENCE_WINDOW_MS })
|
|
329
|
+
);
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
}
|
|
332
|
+
function keysTextOf(report2, action) {
|
|
333
|
+
const bound = report2.bindings[action];
|
|
334
|
+
if (bound.length === 0) return t4("keybindings.list_unbound");
|
|
335
|
+
return bound.map((b) => sequenceId(b.keys)).join(" \xB7 ");
|
|
336
|
+
}
|
|
337
|
+
function sourceTextOf(report2, action) {
|
|
338
|
+
const bound = report2.bindings[action];
|
|
339
|
+
if (bound.length === 0) return "";
|
|
340
|
+
const sources = [...new Set(bound.map((b) => b.source))];
|
|
341
|
+
return sources.map(
|
|
342
|
+
(s) => s === "user" ? t4("keybindings.list_source_user") : t4("keybindings.list_source_default")
|
|
343
|
+
).join("+");
|
|
344
|
+
}
|
|
345
|
+
function rejectText(item) {
|
|
346
|
+
return t4(REJECT_KEYS[item.reason], { keys: item.keys, winner: item.winner ?? "" });
|
|
347
|
+
}
|
|
348
|
+
// src/language.ts
|
|
349
|
+
import { loadConfig as loadConfig3 } from "@epoch-agent/core";
|
|
350
|
+
import { resolveLang, setLang } from "@epoch-agent/infra";
|
|
351
|
+
function applyConfiguredLanguage() {
|
|
352
|
+
try {
|
|
353
|
+
setLang(resolveLang(loadConfig3().display?.language));
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// src/schedule/labels.ts
|
|
358
|
+
import { t as t5 } from "@epoch-agent/infra";
|
|
359
|
+
import {
|
|
360
|
+
collectPendingApprovals
|
|
361
|
+
} from "@epoch-agent/protocol";
|
|
362
|
+
function pendingNotice(defs, runs) {
|
|
363
|
+
const items = collectPendingApprovals(defs, runs);
|
|
364
|
+
const first = items[0];
|
|
365
|
+
if (!first) return null;
|
|
366
|
+
const rest = items.length - 1;
|
|
367
|
+
return t5(rest > 0 ? "schedule.startup_pending_more" : "schedule.startup_pending", {
|
|
368
|
+
name: first.scheduleName,
|
|
369
|
+
n: first.approvals.length,
|
|
370
|
+
rest,
|
|
371
|
+
id: first.scheduleId.slice(0, 12)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
// src/startup-notices.ts
|
|
375
|
+
import { diagnosticToLine } from "@epoch-agent/protocol";
|
|
376
|
+
function composeStartupNotices(parts) {
|
|
377
|
+
return [
|
|
378
|
+
...parts.runtimeDiags,
|
|
379
|
+
...parts.keyDiags.map(diagnosticToLine),
|
|
380
|
+
...parts.pending ? [parts.pending] : [],
|
|
381
|
+
...parts.goalNotice ? [parts.goalNotice] : []
|
|
382
|
+
];
|
|
383
|
+
}
|
|
308
384
|
// src/tui-host.ts
|
|
309
385
|
import {
|
|
310
386
|
getCapability,
|
|
@@ -314,12 +390,12 @@ import {
|
|
|
314
390
|
setPluginEnabled,
|
|
315
391
|
suggestRuleFromApproval
|
|
316
392
|
} from "@epoch-agent/core";
|
|
317
|
-
import { pluginsStatePath as pluginsStatePath2 } from "@epoch-agent/infra";
|
|
393
|
+
import { pluginsStatePath as pluginsStatePath2, t as t11 } from "@epoch-agent/infra";
|
|
318
394
|
import { listBackgroundTasks } from "@epoch-agent/runtime";
|
|
319
|
-
|
|
320
395
|
// src/clipboard.ts
|
|
321
396
|
import { execFile } from "child_process";
|
|
322
397
|
import { readImageSize, sniffMediaType } from "@epoch-agent/core";
|
|
398
|
+
import { t as t6 } from "@epoch-agent/infra";
|
|
323
399
|
var MAX_BYTES = 32 * 1024 * 1024;
|
|
324
400
|
var TIMEOUT_MS = 5e3;
|
|
325
401
|
var WINDOWS_SCRIPT = [
|
|
@@ -338,7 +414,7 @@ function clipboardImageCandidates(platform = process.platform, env = process.env
|
|
|
338
414
|
command: "osascript",
|
|
339
415
|
args: ["-e", "get the clipboard as \xABclass PNGf\xBB"],
|
|
340
416
|
decode: "applescript-hex",
|
|
341
|
-
install: "
|
|
417
|
+
install: t6("clipboard.install_macos")
|
|
342
418
|
}
|
|
343
419
|
];
|
|
344
420
|
}
|
|
@@ -348,20 +424,20 @@ function clipboardImageCandidates(platform = process.platform, env = process.env
|
|
|
348
424
|
command,
|
|
349
425
|
args: flags,
|
|
350
426
|
decode: "base64",
|
|
351
|
-
install: "
|
|
427
|
+
install: t6("clipboard.install_windows")
|
|
352
428
|
}));
|
|
353
429
|
}
|
|
354
430
|
const wayland = {
|
|
355
431
|
command: "wl-paste",
|
|
356
432
|
args: ["--no-newline", "--type", "image/png"],
|
|
357
433
|
decode: "raw",
|
|
358
|
-
install: "
|
|
434
|
+
install: t6("clipboard.install_wayland")
|
|
359
435
|
};
|
|
360
436
|
const x11 = {
|
|
361
437
|
command: "xclip",
|
|
362
438
|
args: ["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
363
439
|
decode: "raw",
|
|
364
|
-
install: "
|
|
440
|
+
install: t6("clipboard.install_x11")
|
|
365
441
|
};
|
|
366
442
|
return env["WAYLAND_DISPLAY"] ? [wayland, x11] : [x11, wayland];
|
|
367
443
|
}
|
|
@@ -414,23 +490,26 @@ async function readClipboardImage(platform = process.platform, env = process.env
|
|
|
414
490
|
const image = decodeImage(bytes);
|
|
415
491
|
if (!image) continue;
|
|
416
492
|
if (image.bytes > MAX_BYTES) {
|
|
417
|
-
throw new Error(
|
|
493
|
+
throw new Error(t6("clipboard.too_large", { mb: Math.round(image.bytes / 1024 / 1024) }));
|
|
418
494
|
}
|
|
419
495
|
return image;
|
|
420
496
|
}
|
|
421
497
|
if (!anyRan) {
|
|
422
|
-
throw new Error(
|
|
498
|
+
throw new Error(
|
|
499
|
+
t6("clipboard.unreadable", {
|
|
500
|
+
hints: candidates.map((c) => c.install).join(t6("clipboard.hint_sep"))
|
|
501
|
+
})
|
|
502
|
+
);
|
|
423
503
|
}
|
|
424
504
|
return null;
|
|
425
505
|
}
|
|
426
|
-
|
|
427
506
|
// src/goal-view.ts
|
|
428
507
|
import {
|
|
429
508
|
DEFAULT_GOAL_MAX_ROUNDS,
|
|
430
509
|
MAX_GOAL_MAX_ROUNDS,
|
|
431
510
|
MAX_OBJECTIVE_CHARS
|
|
432
511
|
} from "@epoch-agent/core";
|
|
433
|
-
import { t } from "@epoch-agent/infra";
|
|
512
|
+
import { t as t7 } from "@epoch-agent/infra";
|
|
434
513
|
var PHASE_LABEL_KEYS = {
|
|
435
514
|
active: "goal.phase_active",
|
|
436
515
|
paused: "goal.phase_paused",
|
|
@@ -438,28 +517,28 @@ var PHASE_LABEL_KEYS = {
|
|
|
438
517
|
complete: "goal.phase_complete"
|
|
439
518
|
};
|
|
440
519
|
function phaseLabel(phase) {
|
|
441
|
-
return
|
|
520
|
+
return t7(PHASE_LABEL_KEYS[phase]);
|
|
442
521
|
}
|
|
443
522
|
function usage() {
|
|
444
|
-
return
|
|
523
|
+
return t7("goal.usage", { rounds: DEFAULT_GOAL_MAX_ROUNDS, max: MAX_GOAL_MAX_ROUNDS });
|
|
445
524
|
}
|
|
446
525
|
function extraOf(goal) {
|
|
447
526
|
switch (goal.phase) {
|
|
448
527
|
case "blocked":
|
|
449
|
-
return
|
|
528
|
+
return t7("goal.extra_blocked", {
|
|
450
529
|
code: goal.block?.code ?? "",
|
|
451
530
|
message: goal.block?.message ?? ""
|
|
452
531
|
});
|
|
453
532
|
case "complete":
|
|
454
|
-
return
|
|
533
|
+
return t7("goal.extra_complete", { evidence: goal.completeEvidence ?? "" });
|
|
455
534
|
case "paused":
|
|
456
|
-
return
|
|
535
|
+
return t7("goal.extra_paused");
|
|
457
536
|
case "active":
|
|
458
|
-
return
|
|
537
|
+
return t7("goal.extra_active");
|
|
459
538
|
}
|
|
460
539
|
}
|
|
461
540
|
function describeGoal(goal) {
|
|
462
|
-
return
|
|
541
|
+
return t7("goal.status", {
|
|
463
542
|
objective: goal.objective,
|
|
464
543
|
phase: phaseLabel(goal.phase),
|
|
465
544
|
used: goal.roundsUsed,
|
|
@@ -471,27 +550,24 @@ function refusalText(reason, phase) {
|
|
|
471
550
|
const at = phase ? phaseLabel(phase) : "";
|
|
472
551
|
switch (reason) {
|
|
473
552
|
case "busy":
|
|
474
|
-
return
|
|
553
|
+
return t7("goal.refuse_busy", { phase: at });
|
|
475
554
|
case "no-goal":
|
|
476
|
-
return
|
|
555
|
+
return t7("goal.refuse_no_goal");
|
|
477
556
|
case "empty-objective":
|
|
478
|
-
return
|
|
557
|
+
return t7("goal.refuse_empty_objective");
|
|
479
558
|
case "objective-too-long":
|
|
480
|
-
return
|
|
559
|
+
return t7("goal.refuse_objective_too_long", { max: MAX_OBJECTIVE_CHARS });
|
|
481
560
|
case "bad-budget":
|
|
482
|
-
return
|
|
561
|
+
return t7("goal.refuse_bad_budget", { max: MAX_GOAL_MAX_ROUNDS });
|
|
483
562
|
case "empty-evidence":
|
|
484
|
-
return
|
|
563
|
+
return t7("goal.refuse_empty_evidence");
|
|
485
564
|
case "wrong-phase":
|
|
486
|
-
return
|
|
565
|
+
return t7("goal.refuse_wrong_phase", { phase: at });
|
|
487
566
|
case "unavailable":
|
|
488
|
-
return
|
|
489
|
-
// 这两个码只有**模型**那条路(`goal` 工具)产得出来 —— `/goal` 的
|
|
490
|
-
// 每一支都不带 block 参数。留着分支而不是 `never`:将来给人也开一个
|
|
491
|
-
// 「报卡住」的入口时,这里会是编译期就该改的地方,不是运行期才发现的
|
|
567
|
+
return t7("goal.refuse_unavailable");
|
|
492
568
|
case "bad-block-code":
|
|
493
569
|
case "empty-block-message":
|
|
494
|
-
return
|
|
570
|
+
return t7("goal.refuse_wrong_phase", { phase: at });
|
|
495
571
|
}
|
|
496
572
|
}
|
|
497
573
|
function report(control, outcome, toText) {
|
|
@@ -503,18 +579,18 @@ function buildGoalActions(control) {
|
|
|
503
579
|
return {
|
|
504
580
|
describe: () => {
|
|
505
581
|
const goal = control.current();
|
|
506
|
-
return goal ? describeGoal(goal) :
|
|
582
|
+
return goal ? describeGoal(goal) : t7("goal.none", { usage: usage() });
|
|
507
583
|
},
|
|
508
584
|
usage,
|
|
509
585
|
create: (objective) => report(
|
|
510
586
|
control,
|
|
511
587
|
control.create(objective),
|
|
512
|
-
(goal) =>
|
|
588
|
+
(goal) => t7("goal.created", { objective: goal.objective, max: goal.maxRounds })
|
|
513
589
|
),
|
|
514
590
|
edit: (objective) => report(
|
|
515
591
|
control,
|
|
516
592
|
control.edit(objective),
|
|
517
|
-
(goal) =>
|
|
593
|
+
(goal) => t7("goal.edited", {
|
|
518
594
|
objective: goal.objective,
|
|
519
595
|
used: goal.roundsUsed,
|
|
520
596
|
max: goal.maxRounds
|
|
@@ -524,52 +600,44 @@ function buildGoalActions(control) {
|
|
|
524
600
|
control,
|
|
525
601
|
control.setBudget(rounds),
|
|
526
602
|
(goal) => (
|
|
527
|
-
|
|
528
|
-
// 这里照着**写完之后**那份 phase 说话,而不是照着用户的意图说
|
|
529
|
-
t(goal.phase === "blocked" ? "goal.budget_set_exhausted" : "goal.budget_set", {
|
|
603
|
+
t7(goal.phase === "blocked" ? "goal.budget_set_exhausted" : "goal.budget_set", {
|
|
530
604
|
used: goal.roundsUsed,
|
|
531
605
|
max: goal.maxRounds
|
|
532
606
|
})
|
|
533
607
|
)
|
|
534
608
|
),
|
|
535
|
-
pause: () => report(control, control.pause(), () =>
|
|
609
|
+
pause: () => report(control, control.pause(), () => t7("goal.paused")),
|
|
536
610
|
resume: () => report(
|
|
537
611
|
control,
|
|
538
612
|
control.resume(),
|
|
539
|
-
(goal) =>
|
|
613
|
+
(goal) => t7("goal.resumed", { used: goal.roundsUsed, max: goal.maxRounds })
|
|
540
614
|
),
|
|
541
615
|
done: (evidence) => report(
|
|
542
616
|
control,
|
|
543
617
|
control.complete(evidence),
|
|
544
|
-
(goal) =>
|
|
618
|
+
(goal) => t7("goal.completed", { evidence: goal.completeEvidence ?? "" })
|
|
545
619
|
),
|
|
546
620
|
clear: () => {
|
|
547
621
|
const outcome = control.clear();
|
|
548
622
|
if (!outcome.ok) return { ok: false, message: refusalText(outcome.reason) };
|
|
549
|
-
return { ok: true, message:
|
|
623
|
+
return { ok: true, message: t7(outcome.cleared ? "goal.cleared" : "goal.clear_noop") };
|
|
550
624
|
},
|
|
551
|
-
/**
|
|
552
|
-
* 开机那一行(验收 8)。**只有 blocked 才出现**:`active` / `paused` /
|
|
553
|
-
* `complete` 三档都不是「需要人现在做点什么」,而开机提示那一栏是给
|
|
554
|
-
* 「有事要你处理」用的,塞满常态信息会让真有事的那次被淹掉。
|
|
555
|
-
*/
|
|
556
625
|
blockedNotice: () => {
|
|
557
626
|
const goal = control.current();
|
|
558
627
|
if (!goal || goal.phase !== "blocked" || !goal.block) return null;
|
|
559
|
-
return
|
|
628
|
+
return t7("goal.startup_blocked", {
|
|
560
629
|
code: goal.block.code,
|
|
561
630
|
message: goal.block.message
|
|
562
631
|
});
|
|
563
632
|
}
|
|
564
633
|
};
|
|
565
634
|
}
|
|
566
|
-
|
|
567
635
|
// src/host-actions.ts
|
|
568
636
|
import { execFile as execFile2 } from "child_process";
|
|
569
637
|
import { writeFileSync } from "fs";
|
|
570
638
|
import { isAbsolute, resolve } from "path";
|
|
571
639
|
import { loadPlugins, readPluginRecords, scanPluginDir } from "@epoch-agent/core";
|
|
572
|
-
import { pluginsStatePath } from "@epoch-agent/infra";
|
|
640
|
+
import { pluginsStatePath, t as t8 } from "@epoch-agent/infra";
|
|
573
641
|
var TIMEOUT_MS2 = 1e4;
|
|
574
642
|
var MAX_BUFFER = 1024 * 1024;
|
|
575
643
|
function run(command, args, cwd) {
|
|
@@ -613,7 +681,7 @@ async function writeClipboardText(text) {
|
|
|
613
681
|
(err) => {
|
|
614
682
|
const e = err;
|
|
615
683
|
if (e?.code === "ENOENT") return resolve_("missing");
|
|
616
|
-
resolve_(err ? err.message ?? "
|
|
684
|
+
resolve_(err ? err.message ?? t8("host_actions.clipboard_write_failed") : "ok");
|
|
617
685
|
}
|
|
618
686
|
);
|
|
619
687
|
child.stdin?.end(text, "utf-8");
|
|
@@ -623,15 +691,15 @@ async function writeClipboardText(text) {
|
|
|
623
691
|
throw new Error(done);
|
|
624
692
|
}
|
|
625
693
|
throw new Error(
|
|
626
|
-
process.platform === "linux" ? "
|
|
694
|
+
process.platform === "linux" ? t8("host_actions.clipboard_missing_linux") : t8("host_actions.clipboard_missing")
|
|
627
695
|
);
|
|
628
696
|
}
|
|
629
697
|
async function gitDiff(cwd) {
|
|
630
698
|
const result = await run("git", ["--no-pager", "diff", "HEAD"], cwd);
|
|
631
|
-
if (result.missing) return { text: "", ok: false, message: "
|
|
699
|
+
if (result.missing) return { text: "", ok: false, message: t8("host_actions.git_missing") };
|
|
632
700
|
if (!result.ok) {
|
|
633
|
-
const why = result.stderr.trim().split("\n")[0] ?? "
|
|
634
|
-
return { text: "", ok: false, message:
|
|
701
|
+
const why = result.stderr.trim().split("\n")[0] ?? t8("git.unknown_reason");
|
|
702
|
+
return { text: "", ok: false, message: t8("host_actions.diff_failed", { why }) };
|
|
635
703
|
}
|
|
636
704
|
return { text: result.stdout, ok: true };
|
|
637
705
|
}
|
|
@@ -643,20 +711,20 @@ async function gitDirty(cwd) {
|
|
|
643
711
|
}
|
|
644
712
|
function renderSessionMarkdown(sessionId, messages, now) {
|
|
645
713
|
const ROLE_LABEL = {
|
|
646
|
-
user: "
|
|
647
|
-
assistant: "
|
|
648
|
-
tool: "
|
|
649
|
-
system: "
|
|
714
|
+
user: t8("host_actions.role_user"),
|
|
715
|
+
assistant: t8("host_actions.role_assistant"),
|
|
716
|
+
tool: t8("host_actions.role_tool"),
|
|
717
|
+
system: t8("host_actions.role_system")
|
|
650
718
|
};
|
|
651
719
|
const lines = [
|
|
652
|
-
|
|
720
|
+
t8("host_actions.export_title", { id: sessionId }),
|
|
653
721
|
"",
|
|
654
|
-
|
|
722
|
+
t8("host_actions.export_meta", { when: now.toISOString(), count: messages.length }),
|
|
655
723
|
""
|
|
656
724
|
];
|
|
657
725
|
for (const m of messages) {
|
|
658
726
|
lines.push(`## ${ROLE_LABEL[m.role] ?? m.role}`, "");
|
|
659
|
-
lines.push(m.content.trim() || "
|
|
727
|
+
lines.push(m.content.trim() || t8("host_actions.export_empty"), "");
|
|
660
728
|
}
|
|
661
729
|
return lines.join("\n");
|
|
662
730
|
}
|
|
@@ -683,8 +751,6 @@ function describePlugins(homeDir) {
|
|
|
683
751
|
enabled: record.enabled,
|
|
684
752
|
loaded,
|
|
685
753
|
...loaded || reason === void 0 ? {} : { reason },
|
|
686
|
-
// 只给加载了的数扩展物:一个目录已经没了的插件扫出来必然是全零,
|
|
687
|
-
// 而「0 条命令」会被读成「这插件是空的」,那是另一个结论
|
|
688
754
|
contributes: loaded ? contributionLines(record.path, record.name) : []
|
|
689
755
|
};
|
|
690
756
|
});
|
|
@@ -693,20 +759,20 @@ function describePlugins(homeDir) {
|
|
|
693
759
|
function contributionLines(dir, name) {
|
|
694
760
|
const inv = scanPluginDir(dir, name);
|
|
695
761
|
const hooks = inv.hooks.reduce((sum, h) => sum + h.count, 0);
|
|
762
|
+
const line = (n, key) => n > 0 ? t8(key, { count: n }) : "";
|
|
696
763
|
const out = [
|
|
697
|
-
inv.commands.length
|
|
698
|
-
inv.roles.length
|
|
699
|
-
inv.skills.length
|
|
700
|
-
inv.denyRules
|
|
701
|
-
hooks
|
|
764
|
+
line(inv.commands.length, "plugin.line_commands"),
|
|
765
|
+
line(inv.roles.length, "plugin.line_roles"),
|
|
766
|
+
line(inv.skills.length, "plugin.line_skills"),
|
|
767
|
+
line(inv.denyRules, "plugin.line_deny"),
|
|
768
|
+
line(hooks, "plugin.line_hooks")
|
|
702
769
|
].filter(Boolean);
|
|
703
|
-
return out.length > 0 ? out : ["
|
|
770
|
+
return out.length > 0 ? out : [t8("plugin.line_nothing")];
|
|
704
771
|
}
|
|
705
|
-
|
|
706
772
|
// src/statusline.ts
|
|
707
773
|
import { execFile as execFile3 } from "child_process";
|
|
708
774
|
import { sanitizeToolOutput } from "@epoch-agent/core";
|
|
709
|
-
import { shellSpawnArgs } from "@epoch-agent/infra";
|
|
775
|
+
import { shellSpawnArgs, t as t9 } from "@epoch-agent/infra";
|
|
710
776
|
var TIMEOUT_MS3 = 2e3;
|
|
711
777
|
var MAX_WIDTH = 60;
|
|
712
778
|
var DEFAULT_INTERVAL_MS = 5e3;
|
|
@@ -728,7 +794,6 @@ function runStatusLineCommand(command, cwd) {
|
|
|
728
794
|
timeout: TIMEOUT_MS3,
|
|
729
795
|
maxBuffer: MAX_BUFFER2,
|
|
730
796
|
encoding: "utf-8",
|
|
731
|
-
// 不弹黑框:Windows 上每 5 秒闪一个控制台窗口是没法用的
|
|
732
797
|
windowsHide: true
|
|
733
798
|
},
|
|
734
799
|
(err, stdout) => {
|
|
@@ -736,7 +801,7 @@ function runStatusLineCommand(command, cwd) {
|
|
|
736
801
|
const killed = err.killed === true;
|
|
737
802
|
settle({
|
|
738
803
|
text: null,
|
|
739
|
-
error: killed ?
|
|
804
|
+
error: killed ? t9("statusline.timeout", { ms: TIMEOUT_MS3 }) : err.message.trim()
|
|
740
805
|
});
|
|
741
806
|
return;
|
|
742
807
|
}
|
|
@@ -768,70 +833,587 @@ function createStatusLine(config, cwd) {
|
|
|
768
833
|
if (!running && Date.now() - idleSince >= interval) refresh();
|
|
769
834
|
return value;
|
|
770
835
|
},
|
|
771
|
-
// 循环而不是一次 await:`pending` 结束的那一刻别人可能已经又 read() 了一次
|
|
772
|
-
// (`intervalMs: 0` 时每次 read 都踢一次),只等一次会漏掉后面那个
|
|
773
836
|
settled: async () => {
|
|
774
837
|
while (pending) await pending;
|
|
775
838
|
}
|
|
776
839
|
};
|
|
777
840
|
}
|
|
778
|
-
|
|
841
|
+
// src/terminal-setup-view.ts
|
|
842
|
+
import { t as t10 } from "@epoch-agent/infra";
|
|
843
|
+
import { classifyTerminal } from "@epoch-agent/tui";
|
|
844
|
+
// src/terminal-setup.ts
|
|
845
|
+
import { execFileSync } from "child_process";
|
|
846
|
+
import {
|
|
847
|
+
copyFileSync,
|
|
848
|
+
existsSync,
|
|
849
|
+
readdirSync,
|
|
850
|
+
readFileSync,
|
|
851
|
+
rmSync,
|
|
852
|
+
unlinkSync,
|
|
853
|
+
writeFileSync as writeFileSync2
|
|
854
|
+
} from "fs";
|
|
855
|
+
import { homedir } from "os";
|
|
856
|
+
import { basename, dirname, join } from "path";
|
|
857
|
+
var VSCODE_ENTRY = `{
|
|
858
|
+
"key": "shift+enter",
|
|
859
|
+
"command": "workbench.action.terminal.sendSequence",
|
|
860
|
+
"args": { "text": "\\u001b[13;2u" },
|
|
861
|
+
"when": "terminalFocus"
|
|
862
|
+
}`;
|
|
863
|
+
var WINDOWS_TERMINAL_ACTION = `{ "command": { "action": "sendInput", "input": "\\u001b[13;2u" }, "keys": "shift+enter" }`;
|
|
864
|
+
var ITERM2_KEYBOARD_MAP_ENTRY = {
|
|
865
|
+
"Key Code": 13,
|
|
866
|
+
Modifiers: 1,
|
|
867
|
+
Action: 26,
|
|
868
|
+
"Hex Code": "0x1b 0x5b 0x31 0x33 0x3b 0x32 0x75"
|
|
869
|
+
};
|
|
870
|
+
function planForTerminal(terminal, opts = {}) {
|
|
871
|
+
const platform = opts.platform ?? process.platform;
|
|
872
|
+
const env = opts.env ?? process.env;
|
|
873
|
+
const home = opts.home ?? homedir();
|
|
874
|
+
switch (terminal.kind) {
|
|
875
|
+
case "vscode": {
|
|
876
|
+
const base = platform === "darwin" ? join(home, "Library/Application Support/Code/User") : platform === "win32" ? join(env["APPDATA"] ?? "", "Code/User") : join(home, ".config/Code/User");
|
|
877
|
+
const target = join(base, "keybindings.json");
|
|
878
|
+
return { kind: "vscode", target, fileExists: existsSync(target), entry: VSCODE_ENTRY };
|
|
879
|
+
}
|
|
880
|
+
case "windows-terminal": {
|
|
881
|
+
const target = join(
|
|
882
|
+
env["LOCALAPPDATA"] ?? "",
|
|
883
|
+
"Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json"
|
|
884
|
+
);
|
|
885
|
+
return {
|
|
886
|
+
kind: "windows-terminal",
|
|
887
|
+
target,
|
|
888
|
+
fileExists: existsSync(target),
|
|
889
|
+
action: WINDOWS_TERMINAL_ACTION
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
case "iterm2": {
|
|
893
|
+
const target = join(home, "Library/Preferences/com.googlecode.iterm2.plist");
|
|
894
|
+
const parsed = existsSync(target) ? readPlistJson(target) : void 0;
|
|
895
|
+
if (parsed === void 0) {
|
|
896
|
+
return { kind: "iterm2", target, readable: false, bookmarkName: "", entry: "" };
|
|
897
|
+
}
|
|
898
|
+
const edited = planIterm2Edit(parsed);
|
|
899
|
+
if (!edited.ok)
|
|
900
|
+
return { kind: "iterm2", target, readable: false, bookmarkName: "", entry: "" };
|
|
901
|
+
return {
|
|
902
|
+
kind: "iterm2",
|
|
903
|
+
target,
|
|
904
|
+
readable: true,
|
|
905
|
+
bookmarkName: edited.bookmarkName,
|
|
906
|
+
entry: entryText(edited.entry)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
case "apple-terminal":
|
|
910
|
+
return { kind: "apple-terminal" };
|
|
911
|
+
case "unknown":
|
|
912
|
+
return { kind: "unknown", label: terminal.label };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function entryText(entry) {
|
|
916
|
+
return JSON.stringify(entry, null, 2);
|
|
917
|
+
}
|
|
918
|
+
function makeBackup(target, ts = Date.now()) {
|
|
919
|
+
const backupPath = `${target}.epoch-backup.${ts}`;
|
|
920
|
+
const originalAbsent = !existsSync(target);
|
|
921
|
+
try {
|
|
922
|
+
if (originalAbsent) {
|
|
923
|
+
writeFileSync2(backupPath, "");
|
|
924
|
+
} else {
|
|
925
|
+
copyFileSync(target, backupPath);
|
|
926
|
+
}
|
|
927
|
+
writeFileSync2(`${backupPath}.json`, JSON.stringify({ originalAbsent }));
|
|
928
|
+
return { ok: true, backupPath, originalAbsent };
|
|
929
|
+
} catch (error) {
|
|
930
|
+
try {
|
|
931
|
+
rmSync(backupPath, { force: true });
|
|
932
|
+
rmSync(`${backupPath}.json`, { force: true });
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
return { ok: false, code: "backup-failed", detail: String(error) };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function applyPlan(plan, ts = Date.now()) {
|
|
939
|
+
if (plan.kind === "vscode" || plan.kind === "windows-terminal") {
|
|
940
|
+
const target = plan.target;
|
|
941
|
+
const entry = plan.kind === "vscode" ? plan.entry : plan.action;
|
|
942
|
+
const key = plan.kind === "vscode" ? void 0 : "actions";
|
|
943
|
+
let next;
|
|
944
|
+
if (plan.fileExists) {
|
|
945
|
+
const current = safeRead(target);
|
|
946
|
+
if (current === void 0) return { ok: false, code: "target-changed", detail: target };
|
|
947
|
+
const inserted = insertIntoJsonArray(current, entry, key);
|
|
948
|
+
if (inserted === null) return { ok: false, code: "target-changed", detail: target };
|
|
949
|
+
next = inserted;
|
|
950
|
+
} else {
|
|
951
|
+
next = key === void 0 ? `[
|
|
952
|
+
${entry}
|
|
953
|
+
]
|
|
954
|
+
` : `{
|
|
955
|
+
"${key}": [
|
|
956
|
+
${indentBlock(entry)}
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
const backup = makeBackup(target, ts);
|
|
962
|
+
if (!backup.ok) return backup;
|
|
963
|
+
try {
|
|
964
|
+
writeFileSync2(target, next);
|
|
965
|
+
writeFileSync2(
|
|
966
|
+
`${backup.backupPath}.json`,
|
|
967
|
+
JSON.stringify({ originalAbsent: backup.originalAbsent, writtenContent: next })
|
|
968
|
+
);
|
|
969
|
+
return { ok: true, target, backupPath: backup.backupPath };
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
ok: false,
|
|
973
|
+
code: "write-failed",
|
|
974
|
+
detail: String(error),
|
|
975
|
+
backupPath: backup.backupPath
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (plan.kind === "iterm2") {
|
|
980
|
+
const parsed = existsSync(plan.target) ? readPlistJson(plan.target) : void 0;
|
|
981
|
+
if (parsed === void 0) return { ok: false, code: "plist-missing" };
|
|
982
|
+
const edited = planIterm2Edit(parsed);
|
|
983
|
+
if (!edited.ok) return { ok: false, code: "target-changed", detail: plan.target };
|
|
984
|
+
const backup = makeBackup(plan.target, ts);
|
|
985
|
+
if (!backup.ok) return backup;
|
|
986
|
+
if (!writePlistBinary(plan.target, edited.newPlist)) {
|
|
987
|
+
return {
|
|
988
|
+
ok: false,
|
|
989
|
+
code: "plutil-failed",
|
|
990
|
+
detail: plan.target,
|
|
991
|
+
backupPath: backup.backupPath
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
writeFileSync2(
|
|
995
|
+
`${backup.backupPath}.json`,
|
|
996
|
+
JSON.stringify({ originalAbsent: backup.originalAbsent })
|
|
997
|
+
);
|
|
998
|
+
return { ok: true, target: plan.target, backupPath: backup.backupPath };
|
|
999
|
+
}
|
|
1000
|
+
return { ok: false, code: "target-changed", detail: plan.kind };
|
|
1001
|
+
}
|
|
1002
|
+
function revertLatest(target) {
|
|
1003
|
+
const latest = findLatestBackup(target);
|
|
1004
|
+
if (latest === void 0) return { ok: false, code: "no-backup", target };
|
|
1005
|
+
let manifest = {};
|
|
1006
|
+
try {
|
|
1007
|
+
manifest = JSON.parse(readFileSync(`${latest}.json`, "utf8"));
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
if (manifest.originalAbsent === true) {
|
|
1011
|
+
if (!existsSync(target)) return { ok: false, code: "nothing-to-do", target };
|
|
1012
|
+
const current = safeRead(target);
|
|
1013
|
+
if (current !== manifest.writtenContent) {
|
|
1014
|
+
return { ok: false, code: "revert-blocked", target, backupPath: latest };
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
unlinkSync(target);
|
|
1018
|
+
return { ok: true, kind: "restored-absent", target, backupPath: latest };
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
return { ok: false, code: "restore-failed", detail: String(error) };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
copyFileSync(latest, target);
|
|
1025
|
+
return { ok: true, kind: "restored", target, backupPath: latest };
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
return { ok: false, code: "restore-failed", detail: String(error) };
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function findLatestBackup(target) {
|
|
1031
|
+
const dir = dirname(target);
|
|
1032
|
+
const base = basename(target);
|
|
1033
|
+
const prefix = `${base}.epoch-backup.`;
|
|
1034
|
+
let best;
|
|
1035
|
+
for (const name of safeReaddir(dir)) {
|
|
1036
|
+
if (!name.startsWith(prefix) || name.endsWith(".json")) continue;
|
|
1037
|
+
const ts = Number(name.slice(prefix.length));
|
|
1038
|
+
if (!Number.isFinite(ts)) continue;
|
|
1039
|
+
if (best === void 0 || ts > best.ts) best = { path: join(dir, name), ts };
|
|
1040
|
+
}
|
|
1041
|
+
return best?.path;
|
|
1042
|
+
}
|
|
1043
|
+
function safeRead(path) {
|
|
1044
|
+
try {
|
|
1045
|
+
return readFileSync(path, "utf8");
|
|
1046
|
+
} catch {
|
|
1047
|
+
return void 0;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function safeReaddir(dir) {
|
|
1051
|
+
try {
|
|
1052
|
+
return readdirSync(dir);
|
|
1053
|
+
} catch {
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function insertIntoJsonArray(text, entry, key) {
|
|
1058
|
+
let start = -1;
|
|
1059
|
+
if (key === void 0) {
|
|
1060
|
+
for (let i = 0; i < text.length; i++) {
|
|
1061
|
+
const c = text[i];
|
|
1062
|
+
if (c === "[") {
|
|
1063
|
+
start = i;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
if (c === "{") break;
|
|
1067
|
+
const skip = skipTrivia(text, i);
|
|
1068
|
+
if (skip !== null) i = skip;
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
const keyAt = text.indexOf(`"${key}"`);
|
|
1072
|
+
if (keyAt === -1) return null;
|
|
1073
|
+
for (let i = keyAt + key.length + 2; i < text.length; i++) {
|
|
1074
|
+
if (text[i] === "[") {
|
|
1075
|
+
start = i;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
const skip = skipTrivia(text, i);
|
|
1079
|
+
if (skip !== null) i = skip;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (start === -1) return null;
|
|
1083
|
+
const end = matchingClose(text, start);
|
|
1084
|
+
if (end === -1) return null;
|
|
1085
|
+
const inner = text.slice(start + 1, end);
|
|
1086
|
+
const isEmpty = inner.trim() === "";
|
|
1087
|
+
const insert = isEmpty ? `
|
|
1088
|
+
${entry}
|
|
1089
|
+
` : `,
|
|
1090
|
+
${key === void 0 ? entry : indentBlock(entry)}
|
|
1091
|
+
`;
|
|
1092
|
+
return text.slice(0, end) + insert + text.slice(end);
|
|
1093
|
+
}
|
|
1094
|
+
function matchingClose(text, start) {
|
|
1095
|
+
let depth = 0;
|
|
1096
|
+
let inString = false;
|
|
1097
|
+
for (let i = start; i < text.length; i++) {
|
|
1098
|
+
const c = text[i];
|
|
1099
|
+
if (inString) {
|
|
1100
|
+
if (c === "\\") i++;
|
|
1101
|
+
else if (c === '"') inString = false;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (c === '"') {
|
|
1105
|
+
inString = true;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const skip = skipTrivia(text, i);
|
|
1109
|
+
if (skip !== null) {
|
|
1110
|
+
i = skip;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (c === "[") depth++;
|
|
1114
|
+
else if (c === "]") {
|
|
1115
|
+
depth--;
|
|
1116
|
+
if (depth === 0) return i;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return -1;
|
|
1120
|
+
}
|
|
1121
|
+
function skipTrivia(text, i) {
|
|
1122
|
+
if (text[i] === "/" && text[i + 1] === "/") {
|
|
1123
|
+
const nl = text.indexOf("\n", i);
|
|
1124
|
+
return nl === -1 ? text.length - 1 : nl;
|
|
1125
|
+
}
|
|
1126
|
+
if (text[i] === "/" && text[i + 1] === "*") {
|
|
1127
|
+
const close = text.indexOf("*/", i + 2);
|
|
1128
|
+
return close === -1 ? text.length - 1 : close + 1;
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
function indentBlock(text) {
|
|
1133
|
+
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
1134
|
+
}
|
|
1135
|
+
function planIterm2Edit(parsed) {
|
|
1136
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { ok: false };
|
|
1137
|
+
const root = parsed;
|
|
1138
|
+
const guid = root["Default Bookmark Guid"];
|
|
1139
|
+
const bookmarks = root["New Bookmarks"];
|
|
1140
|
+
if (typeof guid !== "string" || !Array.isArray(bookmarks)) return { ok: false };
|
|
1141
|
+
const bookmark = bookmarks.find(
|
|
1142
|
+
(b) => typeof b === "object" && b !== null && b["Guid"] === guid
|
|
1143
|
+
);
|
|
1144
|
+
if (bookmark === void 0) return { ok: false };
|
|
1145
|
+
const entry = { ...ITERM2_KEYBOARD_MAP_ENTRY };
|
|
1146
|
+
const existing = Array.isArray(bookmark["Keyboard Map"]) ? bookmark["Keyboard Map"] : [];
|
|
1147
|
+
bookmark["Keyboard Map"] = [...existing, entry];
|
|
1148
|
+
const name = typeof bookmark["Name"] === "string" ? bookmark["Name"] : guid;
|
|
1149
|
+
return { ok: true, bookmarkName: name, entry, newPlist: root };
|
|
1150
|
+
}
|
|
1151
|
+
function readPlistJson(path) {
|
|
1152
|
+
try {
|
|
1153
|
+
const out = execFileSync("plutil", ["-convert", "json", "-o", "-", path], {
|
|
1154
|
+
encoding: "utf8",
|
|
1155
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1156
|
+
});
|
|
1157
|
+
return JSON.parse(out);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function writePlistBinary(path, value) {
|
|
1163
|
+
const tmp = `${path}.epoch-plist-tmp`;
|
|
1164
|
+
try {
|
|
1165
|
+
writeFileSync2(tmp, JSON.stringify(value));
|
|
1166
|
+
execFileSync("plutil", ["-convert", "binary1", "-o", path, tmp], {
|
|
1167
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1168
|
+
});
|
|
1169
|
+
return true;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return false;
|
|
1172
|
+
} finally {
|
|
1173
|
+
rmSync(tmp, { force: true });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// src/terminal-setup-view.ts
|
|
1177
|
+
async function runTerminalSetup(args, opts = {}) {
|
|
1178
|
+
const terminal = classifyTerminal(opts.env ?? process.env);
|
|
1179
|
+
const plan = planForTerminal(terminal, opts);
|
|
1180
|
+
if (args === "--revert") return revert(plan);
|
|
1181
|
+
if (args === "--write") return write(plan);
|
|
1182
|
+
if (args !== "") return { ok: false, message: t10("terminal_setup.usage") };
|
|
1183
|
+
return planView(plan);
|
|
1184
|
+
}
|
|
1185
|
+
function planView(plan) {
|
|
1186
|
+
switch (plan.kind) {
|
|
1187
|
+
case "vscode":
|
|
1188
|
+
return {
|
|
1189
|
+
ok: true,
|
|
1190
|
+
message: [
|
|
1191
|
+
t10("terminal_setup.vscode_plan_head"),
|
|
1192
|
+
t10("terminal_setup.plan_target", { target: plan.target }),
|
|
1193
|
+
plan.fileExists ? t10("terminal_setup.plan_file_exists") : t10("terminal_setup.plan_file_new"),
|
|
1194
|
+
t10("terminal_setup.plan_add"),
|
|
1195
|
+
plan.entry,
|
|
1196
|
+
t10("terminal_setup.plan_confirm")
|
|
1197
|
+
].join("\n")
|
|
1198
|
+
};
|
|
1199
|
+
case "windows-terminal":
|
|
1200
|
+
return {
|
|
1201
|
+
ok: true,
|
|
1202
|
+
message: [
|
|
1203
|
+
t10("terminal_setup.wt_plan_head"),
|
|
1204
|
+
t10("terminal_setup.plan_target", { target: plan.target }),
|
|
1205
|
+
plan.fileExists ? t10("terminal_setup.plan_file_exists") : t10("terminal_setup.plan_file_new"),
|
|
1206
|
+
t10("terminal_setup.plan_add"),
|
|
1207
|
+
plan.action,
|
|
1208
|
+
t10("terminal_setup.plan_confirm"),
|
|
1209
|
+
t10("terminal_setup.wt_honest")
|
|
1210
|
+
].join("\n")
|
|
1211
|
+
};
|
|
1212
|
+
case "iterm2":
|
|
1213
|
+
if (!plan.readable) {
|
|
1214
|
+
return {
|
|
1215
|
+
ok: false,
|
|
1216
|
+
message: t10("terminal_setup.iterm2_unreadable", { target: plan.target })
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
ok: true,
|
|
1221
|
+
message: [
|
|
1222
|
+
t10("terminal_setup.iterm2_plan_head"),
|
|
1223
|
+
t10("terminal_setup.plan_target", { target: plan.target }),
|
|
1224
|
+
t10("terminal_setup.iterm2_plan_add", { name: plan.bookmarkName }),
|
|
1225
|
+
plan.entry,
|
|
1226
|
+
t10("terminal_setup.plan_confirm")
|
|
1227
|
+
].join("\n")
|
|
1228
|
+
};
|
|
1229
|
+
case "apple-terminal":
|
|
1230
|
+
return { ok: false, message: t10("terminal_setup.apple") };
|
|
1231
|
+
case "unknown":
|
|
1232
|
+
return { ok: false, message: t10("terminal_setup.unknown", { label: plan.label }) };
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function write(plan) {
|
|
1236
|
+
switch (plan.kind) {
|
|
1237
|
+
case "apple-terminal":
|
|
1238
|
+
return { ok: false, message: t10("terminal_setup.apple_write") };
|
|
1239
|
+
case "unknown":
|
|
1240
|
+
return { ok: false, message: t10("terminal_setup.unknown_write") };
|
|
1241
|
+
case "iterm2":
|
|
1242
|
+
if (!plan.readable) {
|
|
1243
|
+
return {
|
|
1244
|
+
ok: false,
|
|
1245
|
+
message: t10("terminal_setup.iterm2_unreadable", { target: plan.target })
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
break;
|
|
1249
|
+
default:
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
const result = applyPlan(plan);
|
|
1253
|
+
if (!result.ok) {
|
|
1254
|
+
switch (result.code) {
|
|
1255
|
+
case "backup-failed":
|
|
1256
|
+
return {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
message: t10("terminal_setup.backup_failed", {
|
|
1259
|
+
target: plan.target,
|
|
1260
|
+
detail: result.detail
|
|
1261
|
+
})
|
|
1262
|
+
};
|
|
1263
|
+
case "target-changed":
|
|
1264
|
+
return { ok: false, message: t10("terminal_setup.target_changed", { target: plan.target }) };
|
|
1265
|
+
case "write-failed":
|
|
1266
|
+
return {
|
|
1267
|
+
ok: false,
|
|
1268
|
+
message: t10("terminal_setup.write_failed", {
|
|
1269
|
+
target: plan.target,
|
|
1270
|
+
detail: result.detail,
|
|
1271
|
+
backup: result.backupPath
|
|
1272
|
+
})
|
|
1273
|
+
};
|
|
1274
|
+
case "plutil-failed":
|
|
1275
|
+
return {
|
|
1276
|
+
ok: false,
|
|
1277
|
+
message: t10("terminal_setup.plutil_failed", {
|
|
1278
|
+
target: plan.target,
|
|
1279
|
+
backup: result.backupPath
|
|
1280
|
+
})
|
|
1281
|
+
};
|
|
1282
|
+
case "plist-missing":
|
|
1283
|
+
return { ok: false, message: t10("terminal_setup.plist_missing", { target: plan.target }) };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const reload = plan.kind === "vscode" ? t10("terminal_setup.reload_vscode") : plan.kind === "windows-terminal" ? t10("terminal_setup.reload_wt") : t10("terminal_setup.reload_iterm2");
|
|
1287
|
+
return {
|
|
1288
|
+
ok: true,
|
|
1289
|
+
message: [
|
|
1290
|
+
t10("terminal_setup.write_done", { target: result.target, backup: result.backupPath }),
|
|
1291
|
+
reload,
|
|
1292
|
+
t10("terminal_setup.write_revert_hint")
|
|
1293
|
+
].join("\n")
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function revert(plan) {
|
|
1297
|
+
if (plan.kind === "apple-terminal" || plan.kind === "unknown") {
|
|
1298
|
+
return { ok: false, message: t10("terminal_setup.revert_nowhere") };
|
|
1299
|
+
}
|
|
1300
|
+
const result = revertLatest(plan.target);
|
|
1301
|
+
if (result.ok) {
|
|
1302
|
+
return result.kind === "restored" ? {
|
|
1303
|
+
ok: true,
|
|
1304
|
+
message: t10("terminal_setup.revert_restored", {
|
|
1305
|
+
target: result.target,
|
|
1306
|
+
backup: result.backupPath
|
|
1307
|
+
})
|
|
1308
|
+
} : {
|
|
1309
|
+
ok: true,
|
|
1310
|
+
message: t10("terminal_setup.revert_restored_absent", {
|
|
1311
|
+
target: result.target,
|
|
1312
|
+
backup: result.backupPath
|
|
1313
|
+
})
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
switch (result.code) {
|
|
1317
|
+
case "no-backup":
|
|
1318
|
+
return {
|
|
1319
|
+
ok: false,
|
|
1320
|
+
message: t10("terminal_setup.revert_no_backup", { target: result.target })
|
|
1321
|
+
};
|
|
1322
|
+
case "nothing-to-do":
|
|
1323
|
+
return { ok: false, message: t10("terminal_setup.revert_nothing", { target: result.target }) };
|
|
1324
|
+
case "revert-blocked":
|
|
1325
|
+
return {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
message: t10("terminal_setup.revert_blocked", {
|
|
1328
|
+
target: result.target,
|
|
1329
|
+
backup: result.backupPath
|
|
1330
|
+
})
|
|
1331
|
+
};
|
|
1332
|
+
case "restore-failed":
|
|
1333
|
+
return { ok: false, message: t10("terminal_setup.restore_failed", { detail: result.detail }) };
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
779
1336
|
// src/tui-host.ts
|
|
780
1337
|
function sessionHasImages(runtime) {
|
|
781
1338
|
const history = runtime.session?.getHistory() ?? [];
|
|
782
1339
|
return history.some((m) => m.parts?.some((p) => p.type === "image"));
|
|
783
1340
|
}
|
|
1341
|
+
function sessionMentionDeps(runtime) {
|
|
1342
|
+
const references = runtime.sessionReferences;
|
|
1343
|
+
if (!references) return {};
|
|
1344
|
+
const currentSessionId = runtime.session?.sessionId ?? runtime.sessionId;
|
|
1345
|
+
const root = runtime.workspaces.of(currentSessionId)?.projectContext.rootDir;
|
|
1346
|
+
return {
|
|
1347
|
+
sessions: {
|
|
1348
|
+
references,
|
|
1349
|
+
currentSessionId,
|
|
1350
|
+
workspaceCwd: root,
|
|
1351
|
+
allWorkspaces: runtime.config.sessionSearch?.allWorkspaces === true
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function candidateWhen(ms, now) {
|
|
1356
|
+
const d = new Date(ms);
|
|
1357
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1358
|
+
return new Date(now).toDateString() === d.toDateString() ? `${pad(d.getHours())}:${pad(d.getMinutes())}` : `${d.getMonth() + 1}/${d.getDate()}`;
|
|
1359
|
+
}
|
|
1360
|
+
function toCandidateView(row, now) {
|
|
1361
|
+
const where = row.cwd ? row.cwd.split(/[\\/]/).filter(Boolean).pop() ?? row.cwd : "";
|
|
1362
|
+
return {
|
|
1363
|
+
id: row.sessionId,
|
|
1364
|
+
title: row.title,
|
|
1365
|
+
...row.cwd ? { cwd: row.cwd } : {},
|
|
1366
|
+
hint: row.sameWorkspace ? t11("mention.candidate_here", {
|
|
1367
|
+
when: candidateWhen(row.updatedAt, now),
|
|
1368
|
+
count: row.messageCount
|
|
1369
|
+
}) : t11("mention.candidate_elsewhere", {
|
|
1370
|
+
when: candidateWhen(row.updatedAt, now),
|
|
1371
|
+
count: row.messageCount,
|
|
1372
|
+
where
|
|
1373
|
+
})
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
784
1376
|
function buildHostActions(runtime, supportsImages, workDir) {
|
|
785
1377
|
const { permission, diagnostics, model } = runtime;
|
|
786
1378
|
const statusLine = createStatusLine(runtime.config, workDir);
|
|
787
1379
|
const goals = buildGoalActions(runtime.goals);
|
|
788
1380
|
return {
|
|
789
1381
|
getDiagnostics: () => diagnostics,
|
|
790
|
-
// **不能解构 `tools`**:它是个 getter,解构等于在这里拍一张快照,
|
|
791
|
-
// 之后 MCP 热更新(方案 15)了 `/tools` 还显示旧清单
|
|
792
1382
|
listTools: () => runtime.tools,
|
|
793
|
-
// 多模态的两条宿主能力(方案 12 PR-3)。判定都不在 TUI 里:
|
|
794
|
-
// 能不能打开归 core 的 artifact-open,怎么读剪贴板归 cli 的 clipboard
|
|
795
1383
|
openArtifact: (path) => openArtifact(path),
|
|
796
1384
|
readClipboardImage: () => readClipboardImage(),
|
|
797
1385
|
supportsImages: () => supportsImages,
|
|
798
|
-
// 自定义斜杠命令的展开(方案 23)。插值要用 infra 的 tokenizer,
|
|
799
|
-
// 而 tui 只依赖 protocol —— 所以这件事只能在宿主这一侧做
|
|
800
1386
|
expandCommand: (name, args) => runtime.commands.expand(name, args),
|
|
801
|
-
// `@` 文件补全(方案 25)。清单和解析都在这一侧:前者要 spawn git,
|
|
802
|
-
// 后者要过工作区边界和**权限判定** —— 让 TUI 自己读文件等于给 `@`
|
|
803
|
-
// 开一条绕过 `file_read` 的通道
|
|
804
1387
|
listWorkspaceFiles: () => Promise.resolve(listWorkspaceFiles(workDir).files),
|
|
805
|
-
// 后台任务(方案 36 PR-2):状态栏计数和 `/tasks` 都读它。
|
|
806
|
-
// ⚠️ 会话 id 现在是必给的(表按会话分了),而且要给**活的**那个:
|
|
807
|
-
// `/resume` 会把 `session.sessionId` 换掉,用装配时那个 `runtime.sessionId`
|
|
808
|
-
// 的话,resume 之后状态栏数的是一个已经不存在的会话
|
|
809
1388
|
listBackgroundTasks: () => listBackgroundTasks(runtime.session?.sessionId ?? runtime.sessionId),
|
|
810
|
-
// 用户自配的状态栏那一段(方案 29 §2.6)。没配就不注入这一项
|
|
811
1389
|
...statusLine ? { readStatusLine: () => statusLine.read() } : {},
|
|
812
|
-
// **不能解构** `contextBreakdown`:它是个 getter,解构等于拍快照,
|
|
813
|
-
// `/context` 会一直显示第一次看的那个数
|
|
814
1390
|
getContextBreakdown: () => runtime.contextBreakdown,
|
|
815
|
-
// `!` 和 `#`(方案 25 PR-2)。判定和执行都在引擎侧,TUI 只提供确认框 ——
|
|
816
|
-
// 用户手打的命令和模型生成的命令在危险性上没有区别
|
|
817
1391
|
runShellCommand: (command, approve) => runtime.userActions.runShell(command, approve),
|
|
818
1392
|
writeMemory: (content, target, approve) => runtime.userActions.writeMemory(content, target, approve),
|
|
819
1393
|
...runtime.session ? { noteToSession: (text) => runtime.session.note(text) } : {},
|
|
820
|
-
resolveMentions: (
|
|
1394
|
+
resolveMentions: (mentions) => resolveMentions(mentions, {
|
|
821
1395
|
root: workDir,
|
|
822
|
-
...runtime.permission ? { permission: runtime.permission } : {}
|
|
1396
|
+
...runtime.permission ? { permission: runtime.permission } : {},
|
|
1397
|
+
...sessionMentionDeps(runtime)
|
|
823
1398
|
}),
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1399
|
+
listSessionCandidates: (query, limit) => {
|
|
1400
|
+
const deps = sessionMentionDeps(runtime).sessions;
|
|
1401
|
+
if (!deps) return Promise.resolve([]);
|
|
1402
|
+
const rows = deps.references.candidates({
|
|
1403
|
+
query,
|
|
1404
|
+
limit,
|
|
1405
|
+
currentSessionId: deps.currentSessionId,
|
|
1406
|
+
workspaceCwd: deps.workspaceCwd,
|
|
1407
|
+
...deps.allWorkspaces === void 0 ? {} : { allWorkspaces: deps.allWorkspaces }
|
|
1408
|
+
});
|
|
1409
|
+
const now = Date.now();
|
|
1410
|
+
return Promise.resolve(rows.map((row) => toCandidateView(row, now)));
|
|
1411
|
+
},
|
|
828
1412
|
listSkills: () => runtime.skills,
|
|
829
1413
|
listAgentRoles: () => runtime.agentRoles,
|
|
830
1414
|
listMcpServers: () => runtime.mcpServers.map((s) => ({
|
|
831
1415
|
name: s.name,
|
|
832
|
-
|
|
833
|
-
// 完全不同(前者跑 epoch mcp login,后者查配置),合成一个字会让用户白折腾
|
|
834
|
-
status: s.needsLogin ? "\u8981\u767B\u5F55" : s.connected ? "\u5DF2\u8FDE\u63A5" : "\u672A\u8FDE\u63A5",
|
|
1416
|
+
status: s.needsLogin ? t11("tui_host.mcp_needs_login") : s.connected ? t11("tui_host.mcp_connected") : t11("tui_host.mcp_disconnected"),
|
|
835
1417
|
toolCount: s.toolCount,
|
|
836
1418
|
...s.lastError ? { error: s.lastError } : {}
|
|
837
1419
|
})),
|
|
@@ -839,12 +1421,6 @@ function buildHostActions(runtime, supportsImages, workDir) {
|
|
|
839
1421
|
listMemories: () => runtime.memory.list(),
|
|
840
1422
|
removeMemory: (content) => runtime.memory.remove(content)
|
|
841
1423
|
} : {},
|
|
842
|
-
// 插件(方案 32)。**无条件注入**:插件系统起不来这回事不存在 ——
|
|
843
|
-
// 没装过插件时它就是一份空清单,而那正是 `/plugin` 要显示的东西之一
|
|
844
|
-
// (连带「装一个」的提示)。
|
|
845
|
-
//
|
|
846
|
-
// 两条都现读磁盘、不吃 `runtime.plugins` 那份启动快照,理由写在
|
|
847
|
-
// `describePlugins` 上:用户敲这条命令时,磁盘上的状态可能已经不是启动那一刻了。
|
|
848
1424
|
listPlugins: () => describePlugins(runtime.config.homeDir),
|
|
849
1425
|
setPluginEnabled: async (name, enabled) => {
|
|
850
1426
|
const outcome = await setPluginEnabled(name, enabled, {
|
|
@@ -852,35 +1428,34 @@ function buildHostActions(runtime, supportsImages, workDir) {
|
|
|
852
1428
|
});
|
|
853
1429
|
return outcome.ok ? { ok: true, message: outcome.message } : { ok: false, message: outcome.reason };
|
|
854
1430
|
},
|
|
855
|
-
// 剪贴板写、git diff、会话导出:三样都要 spawn 或写文件,是宿主的活
|
|
856
1431
|
copyToClipboard: (text) => writeClipboardText(text),
|
|
857
1432
|
gitDiff: () => gitDiff(workDir),
|
|
858
1433
|
gitDirty: () => gitDirty(workDir),
|
|
859
|
-
// 检查点与回退(方案 27 PR-3)。**无条件注入**:`runtime.checkpoints` 不可为
|
|
860
|
-
// null(它只是文件系统上的一个目录,不像 provider / SQLite 那样有起不来的可能)。
|
|
861
|
-
//
|
|
862
|
-
// 三个方法逐条转发而不是 `checkpoints: runtime.checkpoints`:TUI 那侧的形状
|
|
863
|
-
// 是 core 那几个类型的**结构性镜像**(tui 不许 import core),直接赋值等于
|
|
864
|
-
// 让两边的类型偷偷绑在一起 —— 而这一行的作用正是让漂移在编译期就红。
|
|
865
|
-
// `list` 的返回值收窄成 readonly 也是在这里发生的
|
|
866
1434
|
checkpoints: {
|
|
867
1435
|
list: () => runtime.checkpoints.list(),
|
|
868
1436
|
preview: (turnIndex) => runtime.checkpoints.preview(turnIndex),
|
|
869
1437
|
rewind: (turnIndex, opts) => runtime.checkpoints.rewind(turnIndex, opts)
|
|
870
1438
|
},
|
|
871
|
-
// 权限规则的可解释性(方案 22 PR-4)。`permissions` 在 runtime 上**不可为 null**,
|
|
872
|
-
// 所以这两条无条件注入 —— 一条规则都没配时它是空表加一句「没配过」
|
|
873
1439
|
listPermissions: () => ({
|
|
874
1440
|
level: runtime.permissions.level(),
|
|
875
1441
|
rules: runtime.permissions.rules(),
|
|
876
1442
|
shadows: runtime.permissions.shadows(),
|
|
877
|
-
managed: runtime.permissions.managed()
|
|
1443
|
+
managed: runtime.permissions.managed(),
|
|
1444
|
+
approvals: runtime.permissions.approvals().map((a) => ({
|
|
1445
|
+
id: a.id,
|
|
1446
|
+
toolName: a.toolName,
|
|
1447
|
+
target: a.target,
|
|
1448
|
+
scope: a.scope,
|
|
1449
|
+
decision: a.decision,
|
|
1450
|
+
createdAt: a.createdAt
|
|
1451
|
+
}))
|
|
878
1452
|
}),
|
|
1453
|
+
revokeApproval: (id) => {
|
|
1454
|
+
const done = runtime.permissions.revokeApproval(id);
|
|
1455
|
+
return done.ok ? { ok: true } : { ok: false, reason: done.reason };
|
|
1456
|
+
},
|
|
879
1457
|
suggestRule: (request) => suggestRuleFromApproval(request),
|
|
880
|
-
// 手动压缩(`/compact`)。会话起不来时不注入 —— 那时压缩也无从谈起
|
|
881
1458
|
...runtime.session ? { compactContext: (instruction) => runtime.session.compact(instruction) } : {},
|
|
882
|
-
// `/resume` 和 Ctrl+R。三条一起给或一起不给:只给列表不给恢复的话,
|
|
883
|
-
// 用户能选中一段会话然后什么也不会发生
|
|
884
1459
|
...runtime.sessions ? {
|
|
885
1460
|
listSessions: () => runtime.sessions.list(),
|
|
886
1461
|
resumeSession: (sessionId) => runtime.sessions.resume(sessionId),
|
|
@@ -895,8 +1470,6 @@ function buildHostActions(runtime, supportsImages, workDir) {
|
|
|
895
1470
|
return { path: writeExport(workDir, name, markdown), messages: messages.length };
|
|
896
1471
|
}
|
|
897
1472
|
} : {},
|
|
898
|
-
// **不能解构 `providerInfo`**:方案 26 之后它是个 getter(`/model` 能换
|
|
899
|
-
// provider),解构等于在这里拍快照,切完之后 `/model` 还显示原来那家和那把 key
|
|
900
1473
|
...runtime.providerInfo ? { getProviderInfo: () => runtime.providerInfo } : {},
|
|
901
1474
|
...model ? {
|
|
902
1475
|
getModelSelection: () => {
|
|
@@ -904,89 +1477,58 @@ function buildHostActions(runtime, supportsImages, workDir) {
|
|
|
904
1477
|
const { contextLength } = getCapability(selection.model, runtime.config.homeDir);
|
|
905
1478
|
return { selection, contextLength };
|
|
906
1479
|
},
|
|
907
|
-
// `hasImages` 由这一侧补,**不能让 TUI 自己判**:TUI 只看得见还没发出去
|
|
908
|
-
// 的那几张(`pendingImages`),已经发过的、以及 `-r` 恢复出来的历史里
|
|
909
|
-
// 那些,只有会话知道。判成 false 的后果很具体:切到不认图的模型,
|
|
910
|
-
// 下一轮把整段带图历史发出去,provider 报 400 且**已经计费**
|
|
911
1480
|
setModel: (ref, ctx) => {
|
|
912
1481
|
const merged = { ...ctx, hasImages: sessionHasImages(runtime) };
|
|
913
1482
|
return ref === "reset" ? model.reset(merged) : model.set(ref, merged);
|
|
914
1483
|
},
|
|
915
|
-
// 引擎攒下的提示(「这个模型连续失败太多次,本会话不再自动重试」)。
|
|
916
|
-
// 拉而不是推:router 在 loop.ts 底下,发不出 AgentEvent;TUI 在一轮
|
|
917
|
-
// 收尾时拉一次,攒着的话用户会在下一轮才看到上一轮的事
|
|
918
1484
|
drainModelNotices: () => model.drainNotices()
|
|
919
1485
|
} : {},
|
|
920
1486
|
...permission ? {
|
|
921
1487
|
getPermissionLevel: () => permission.getLevel(),
|
|
922
|
-
/**
|
|
923
|
-
* 切档位。**判定本体 2026-08-15 搬进了 `runtime.permissions.setLevel()`**,
|
|
924
|
-
* 这里只剩「把码渲染成一句人话」。
|
|
925
|
-
*
|
|
926
|
-
* 搬走的三件事(托管挡 bypass → `setLevel` → `plan.forget()`)原来
|
|
927
|
-
* 逐字长在这儿,而 web 那一格要长出真菜单时需要同一段判定 ——
|
|
928
|
-
* 抄一份的具体代价是第三步会被漏掉,判据写在 `PermissionsControl`
|
|
929
|
-
* 的文件头。**行为一个字没变**:同样的挡、同样的作废、同样的回读确认。
|
|
930
|
-
*
|
|
931
|
-
* 这一层留下的只有那句中文:`LevelChangeResult.reason` 是**码**
|
|
932
|
-
* (它的消费方里有 web 服务端,而中文一律不上网线),而 TUI 这一侧
|
|
933
|
-
* 要的是能直接印在屏幕上的一句话,路径也只有本机这一份说得出来。
|
|
934
|
-
*/
|
|
935
1488
|
setPermissionLevel: (level) => {
|
|
936
1489
|
const result = runtime.permissions.setLevel(level);
|
|
937
1490
|
if (!result.ok) {
|
|
938
1491
|
const managed = runtime.permissions.managed();
|
|
939
1492
|
return {
|
|
940
1493
|
ok: false,
|
|
941
|
-
reason:
|
|
1494
|
+
reason: t11("tui_host.bypass_managed", { path: managed.path })
|
|
942
1495
|
};
|
|
943
1496
|
}
|
|
944
|
-
return permission.getLevel() === level ? { ok: true } : { ok: false, reason:
|
|
1497
|
+
return permission.getLevel() === level ? { ok: true } : { ok: false, reason: t11("tui_host.switch_failed", { level }) };
|
|
945
1498
|
}
|
|
946
1499
|
} : {},
|
|
947
|
-
// 长任务的目标(`/goal`,方案 52)。会话库没起来时整组不注入 ——
|
|
948
|
-
// 那时 `/goal` 压根不注册(目标是**落盘**的状态,建了就没等于没有)。
|
|
949
|
-
//
|
|
950
|
-
// ⚠️ 措辞全在 `goal-view.ts` 里走 `t()`:tui 够不着 catalog,判据写在
|
|
951
|
-
// `HostActions.goals` 上那段。这里只负责把引擎那一份接过去
|
|
952
1500
|
...goals ? { goals } : {},
|
|
953
|
-
|
|
954
|
-
// 它走 `approval-request`,TUI 接的是同一个审批队列
|
|
1501
|
+
terminalSetup: (args) => runTerminalSetup(args),
|
|
955
1502
|
...runtime.plan ? {
|
|
956
1503
|
planMode: {
|
|
957
1504
|
active: () => runtime.plan?.active() ?? false,
|
|
958
1505
|
from: () => runtime.plan?.from() ?? null,
|
|
959
|
-
enter: () => runtime.plan?.enter() ?? { ok: false, reason: "
|
|
1506
|
+
enter: () => runtime.plan?.enter() ?? { ok: false, reason: t11("tui_host.plan_unavailable") },
|
|
960
1507
|
leave: () => runtime.plan?.leave() ?? null,
|
|
961
|
-
// `/plan show` 的数据源(方案 35 PR-2)。`--resume` 之后屏幕上是空的
|
|
962
|
-
// (那张卡片是上一个进程画的),而模型手上那份计划还在 ——
|
|
963
|
-
// 这是用户唯一能问出「现在照着什么在做」的地方
|
|
964
1508
|
approvedPlan: () => runtime.plan?.approvedPlan() ?? null
|
|
965
1509
|
}
|
|
966
1510
|
} : {}
|
|
967
1511
|
};
|
|
968
1512
|
}
|
|
969
|
-
|
|
970
1513
|
// src/version.ts
|
|
971
|
-
import { readFileSync } from "fs";
|
|
972
|
-
import { dirname, join as
|
|
1514
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1515
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
973
1516
|
import { fileURLToPath } from "url";
|
|
974
|
-
|
|
1517
|
+
import { t as t13 } from "@epoch-agent/infra";
|
|
975
1518
|
// src/installation.ts
|
|
976
|
-
import { execFileSync } from "child_process";
|
|
977
|
-
import { existsSync, realpathSync } from "fs";
|
|
978
|
-
import { join } from "path";
|
|
979
|
-
import { normalizeForMatch } from "@epoch-agent/infra";
|
|
980
|
-
|
|
1519
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1520
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
1521
|
+
import { join as join2 } from "path";
|
|
1522
|
+
import { normalizeForMatch, t as t12 } from "@epoch-agent/infra";
|
|
981
1523
|
// src/version.ts
|
|
982
1524
|
function readVersion() {
|
|
983
|
-
const here =
|
|
1525
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
984
1526
|
for (const candidate of [
|
|
985
|
-
|
|
986
|
-
|
|
1527
|
+
join3(here, "..", "package.json"),
|
|
1528
|
+
join3(here, "..", "..", "package.json")
|
|
987
1529
|
]) {
|
|
988
1530
|
try {
|
|
989
|
-
const raw = JSON.parse(
|
|
1531
|
+
const raw = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
990
1532
|
if (raw.version) return raw.version;
|
|
991
1533
|
} catch {
|
|
992
1534
|
}
|
|
@@ -994,9 +1536,10 @@ function readVersion() {
|
|
|
994
1536
|
return "0.0.0-unknown";
|
|
995
1537
|
}
|
|
996
1538
|
var VERSION = readVersion();
|
|
997
|
-
|
|
998
1539
|
// src/tui-entry.ts
|
|
1540
|
+
var PENDING_SCAN_RUNS = 100;
|
|
999
1541
|
async function main() {
|
|
1542
|
+
applyConfiguredLanguage();
|
|
1000
1543
|
if (!await maybePromptForTrust()) {
|
|
1001
1544
|
process.exit(1);
|
|
1002
1545
|
}
|
|
@@ -1010,11 +1553,9 @@ async function main() {
|
|
|
1010
1553
|
});
|
|
1011
1554
|
const { session, config } = runtime;
|
|
1012
1555
|
if (!session) {
|
|
1013
|
-
process.stderr.write(
|
|
1014
|
-
`\u542F\u52A8\u5931\u8D25\uFF1Aprovider \u4E0D\u53EF\u7528\uFF0C\u8FD0\u884C epoch model \u914D\u7F6E
|
|
1556
|
+
process.stderr.write(`${t14("cli.web.no_provider")}
|
|
1015
1557
|
${runtime.diagnostics.join("\n")}
|
|
1016
|
-
`
|
|
1017
|
-
);
|
|
1558
|
+
`);
|
|
1018
1559
|
process.exit(1);
|
|
1019
1560
|
}
|
|
1020
1561
|
const model = config.model || "unknown";
|
|
@@ -1031,11 +1572,15 @@ ${runtime.diagnostics.join("\n")}
|
|
|
1031
1572
|
path: keybindingsPath(config.homeDir),
|
|
1032
1573
|
defaults: DEFAULT_KEYBINDINGS
|
|
1033
1574
|
});
|
|
1034
|
-
const host =
|
|
1575
|
+
const host = {
|
|
1576
|
+
...buildHostActions(runtime, supportsImages, process.cwd()),
|
|
1577
|
+
keybindings: () => describeKeybindings(keys.report)
|
|
1578
|
+
};
|
|
1035
1579
|
const goalNotice = host.goals?.blockedNotice() ?? null;
|
|
1580
|
+
const pending = runtime.schedules.storeAvailable ? pendingNotice(runtime.schedules.list(), runtime.schedules.recentRuns(PENDING_SCAN_RUNS)) : null;
|
|
1036
1581
|
const instance = renderApp({
|
|
1037
1582
|
sessionId: session.sessionId,
|
|
1038
|
-
|
|
1583
|
+
translate: (key, vars) => t14(key, vars),
|
|
1039
1584
|
usageScope: runtime.usageScope,
|
|
1040
1585
|
app: { version: VERSION, cwd: process.cwd() },
|
|
1041
1586
|
config: {
|
|
@@ -1045,32 +1590,17 @@ ${runtime.diagnostics.join("\n")}
|
|
|
1045
1590
|
contextLimit,
|
|
1046
1591
|
getUseBackgroundColor: () => false
|
|
1047
1592
|
},
|
|
1048
|
-
welcomeMessage: "
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
// 放在最靠近输入框的位置
|
|
1056
|
-
startupNotices: [
|
|
1057
|
-
...runtime.diagnostics,
|
|
1058
|
-
...keys.diagnostics.map(diagnosticToLine),
|
|
1059
|
-
...goalNotice ? [goalNotice] : []
|
|
1060
|
-
],
|
|
1593
|
+
welcomeMessage: t14("tui_entry.welcome"),
|
|
1594
|
+
startupNotices: composeStartupNotices({
|
|
1595
|
+
runtimeDiags: runtime.diagnostics,
|
|
1596
|
+
keyDiags: keys.diagnostics,
|
|
1597
|
+
pending,
|
|
1598
|
+
goalNotice
|
|
1599
|
+
}),
|
|
1061
1600
|
keybindings: keys.table,
|
|
1601
|
+
vimMode: keys.vimMode,
|
|
1062
1602
|
host,
|
|
1063
|
-
// 自定义斜杠命令的表(方案 23)。**永远给**:加载不出来时它是空数组,
|
|
1064
|
-
// 不是「没有这个能力」—— TUI 那侧因此不用为「有没有命令系统」分支
|
|
1065
1603
|
customCommands: runtime.commands.list,
|
|
1066
|
-
// 入参是 EpochUserContent 而不是 string —— TUI 里 Ctrl+V 粘的图片
|
|
1067
|
-
// 跟着这一次提交一路走到引擎,`session.run` 本来就收这个类型。
|
|
1068
|
-
//
|
|
1069
|
-
// 两层包装管的都是「这一轮」:工具白名单(方案 23)和临时模型(方案 26)。
|
|
1070
|
-
// 进出成对、且**中断也会还原**,规矩和用例都在 runtime 那一侧。
|
|
1071
|
-
// 模型那层在外面:它要在工具作用域退出之后才还原,顺序和进入时相反 ——
|
|
1072
|
-
// 虽然这两件事眼下互不影响,但把嵌套写成不配对的形状,
|
|
1073
|
-
// 早晚会有第三样东西掉进那个缝里
|
|
1074
1604
|
onRun: (userMessage, opts) => withModelScope(
|
|
1075
1605
|
runtime.model,
|
|
1076
1606
|
opts.model,
|
|
@@ -1080,9 +1610,6 @@ ${runtime.diagnostics.join("\n")}
|
|
|
1080
1610
|
session.run(userMessage, { signal: opts.signal })
|
|
1081
1611
|
)
|
|
1082
1612
|
),
|
|
1083
|
-
// onExit 是同步回调(ink 的键位处理里调),而 dispose 现在要等后台进程收尸。
|
|
1084
|
-
// 先 unmount 把终端还给用户 —— 收尸最多 200ms,但那 200ms 里屏幕不该还挂着
|
|
1085
|
-
// 一个已经退出的 TUI
|
|
1086
1613
|
onExit: () => {
|
|
1087
1614
|
instance.unmount();
|
|
1088
1615
|
void runtime.dispose().finally(() => process.exit(0));
|
|
@@ -1091,7 +1618,7 @@ ${runtime.diagnostics.join("\n")}
|
|
|
1091
1618
|
}
|
|
1092
1619
|
main().catch((err) => {
|
|
1093
1620
|
if (expectedFailureExitCode(err) !== void 0) reportFatal(err);
|
|
1094
|
-
process.stderr.write(
|
|
1621
|
+
process.stderr.write(`${t14("run.tui_spawn_failed", { message: String(err) })}
|
|
1095
1622
|
`);
|
|
1096
1623
|
process.exit(1);
|
|
1097
1624
|
});
|