pi-web-ui 0.68.2 → 0.70.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/dist/server/agent-service.js +170 -54
- package/dist/server/attachments.js +7 -2
- package/dist/server/client-state.js +71 -17
- package/dist/server/dsh/dsh-agent-service.js +142 -38
- package/dist/server/dsh/dsh-client.js +9 -8
- package/dist/server/dsh/dsh-sessions.js +4 -3
- package/dist/server/edit-soft-tool.js +33 -26
- package/dist/server/files-service.js +12 -7
- package/dist/server/goal-service.js +85 -24
- package/dist/server/i18n.js +157 -0
- package/dist/server/index.js +77 -18
- package/dist/server/locales.js +55 -1
- package/dist/server/managed.js +61 -0
- package/dist/server/marker-service.js +37 -20
- package/dist/server/markers/builtins/notify.js +19 -6
- package/dist/server/markers/builtins/rename.js +41 -8
- package/dist/server/markers/builtins/todo.js +107 -30
- package/dist/server/markers/registry.js +2 -2
- package/dist/server/mcp-bridge.js +3 -1
- package/dist/server/model-admin.js +25 -14
- package/dist/server/plugin-catalog.js +7 -3
- package/dist/server/plugin-updater.js +6 -2
- package/dist/server/plugins.js +40 -17
- package/dist/server/prompt-composer.js +42 -16
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/scm.js +18 -25
- package/dist/server/serialize.js +1 -0
- package/dist/server/settings-service.js +21 -1
- package/dist/server/subagent-templates.js +105 -0
- package/dist/server/subagents.js +164 -56
- package/dist/server/tabs.js +87 -0
- package/dist/server/terminals.js +99 -48
- package/dist/server/update-check.js +7 -2
- package/dist/server/vision-bridge.js +34 -12
- package/dist/server/webui-context.js +9 -0
- package/package.json +2 -1
- package/themes/cyberpunk.css +88 -81
- package/themes/dazzle.css +88 -81
- package/themes/md-preview.css +105 -98
- package/themes/white.css +167 -160
- package/web/dist/assets/{TerminalPanel-BQ5NTB9Y.js → TerminalPanel-UxgczH4c.js} +1 -1
- package/web/dist/assets/index-BH5QutUc.css +10 -0
- package/web/dist/assets/index-CaiIOwy7.js +335 -0
- package/web/dist/assets/{markdown-DOsihKaR.js → markdown-Cpo0pNcR.js} +1 -1
- package/web/dist/assets/{react-DIP6JKYk.js → react-CtudoG1_.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-C_I-6Zul.css +0 -10
- package/web/dist/assets/index-Ck5pa3XK.js +0 -333
package/dist/server/terminals.js
CHANGED
|
@@ -24,6 +24,7 @@ import "./patch-node-pty.js";
|
|
|
24
24
|
import { spawn } from "node-pty";
|
|
25
25
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { Type } from "typebox";
|
|
27
|
+
import { bilingual, pick } from "./i18n.js";
|
|
27
28
|
/** Location of the command list for a project: <workspaceRoot>/.pi/commands.json */
|
|
28
29
|
export function commandsFilePath(workspaceRoot) {
|
|
29
30
|
return join(workspaceRoot, ".pi", "commands.json");
|
|
@@ -642,6 +643,7 @@ export function encodeTerminalKey(key, modifiers = {}) {
|
|
|
642
643
|
export class TerminalManager {
|
|
643
644
|
emit;
|
|
644
645
|
workspaceRoot;
|
|
646
|
+
lang;
|
|
645
647
|
/** Live PTYs only. Exited entries move to history so they no longer consume
|
|
646
648
|
* the live-terminal limit while their output remains readable/replayable. */
|
|
647
649
|
terms = new Map();
|
|
@@ -651,9 +653,12 @@ export class TerminalManager {
|
|
|
651
653
|
/** 宿主回调:AI 触碰过的终端静默 ≥ 阈值时触发(一次性/纪元语义见
|
|
652
654
|
* noteAgentActivity)。宿主自行判断会话是否在运行并决定是否注入。 */
|
|
653
655
|
onAgentIdle = null;
|
|
654
|
-
constructor(emit, workspaceRoot
|
|
656
|
+
constructor(emit, workspaceRoot,
|
|
657
|
+
// issue #91:按键/输入校验错误按客户端 UI 语言出中英(英文默认)。
|
|
658
|
+
lang) {
|
|
655
659
|
this.emit = emit;
|
|
656
660
|
this.workspaceRoot = workspaceRoot;
|
|
661
|
+
this.lang = lang;
|
|
657
662
|
}
|
|
658
663
|
/** Start a plain interactive shell in the given directory. */
|
|
659
664
|
create(id, cwd, cols, rows, fallbackCwd, title, opts) {
|
|
@@ -1004,6 +1009,17 @@ export class TerminalManager {
|
|
|
1004
1009
|
list() {
|
|
1005
1010
|
return [...this.terms.values(), ...this.history.values()].map((entry) => this.info(entry));
|
|
1006
1011
|
}
|
|
1012
|
+
/** Count of LIVE PTYs only — exited terminals that merely retain their
|
|
1013
|
+
* output for review (history) do NOT count. Conversations are kept in the
|
|
1014
|
+
* running list while a live terminal exists; exited leftovers must not
|
|
1015
|
+
* pin an idle conversation forever. */
|
|
1016
|
+
countLive() {
|
|
1017
|
+
let n = 0;
|
|
1018
|
+
for (const entry of this.terms.values())
|
|
1019
|
+
if (!entry.exited)
|
|
1020
|
+
n++;
|
|
1021
|
+
return n;
|
|
1022
|
+
}
|
|
1007
1023
|
emitList() {
|
|
1008
1024
|
this.emit({ type: "terminal_list", terminals: this.list() });
|
|
1009
1025
|
}
|
|
@@ -1053,7 +1069,7 @@ export class TerminalManager {
|
|
|
1053
1069
|
return `输入过长(上限 ${MAX_INPUT} 字符) Input too long (max ${MAX_INPUT} chars)`;
|
|
1054
1070
|
const entry = this.terms.get(id);
|
|
1055
1071
|
if (!entry || entry.exited)
|
|
1056
|
-
return "终端不存在或进程已退出";
|
|
1072
|
+
return pick(this.lang?.() ?? "en", "终端不存在或进程已退出", "Terminal not found or its process has exited", "terminals.not.found.exited");
|
|
1057
1073
|
// 已武装的纪元里任何人(含用户手动敲键盘)写了输入都算新活动,重置倒计时。
|
|
1058
1074
|
entry.lastActivityAt = Date.now();
|
|
1059
1075
|
if (entry.idleTimer)
|
|
@@ -1377,29 +1393,33 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1377
1393
|
return defineTool({
|
|
1378
1394
|
name: "bash",
|
|
1379
1395
|
label: "Run bash command",
|
|
1380
|
-
description: "Run a shell command and return its full output plus exit code. Commands run in a visible terminal.\n" +
|
|
1396
|
+
description: bilingual("Run a shell command and return its full output plus exit code. Commands run in a visible terminal.\n" +
|
|
1381
1397
|
"persist=false (default, one-shot): a fresh terminal is created per call, run to completion, then the shell exits (the process ends) while its output stays in the terminal list for later review — like a normal bash call, but each command also leaves a viewable terminal record.\n" +
|
|
1382
1398
|
"persist=true: commands run in the PERSISTENT visible terminal 'ai-bash' — shell state such as cd, venv activation or ssh sessions is retained across calls; you can use terminal_wait to re-block on a backgrounded command, or terminal_read / terminal_input / terminal_key on 'ai-bash' to observe or interact anytime.\n" +
|
|
1383
|
-
"Run the bare command — do NOT pipe through head/tail/more/less (output is returned complete anyway, and pipes hide live progress in the terminal). Use the head/tail parameters instead to trim the returned output. For interactive commands (REPLs, prompts, installers asking y/n) set persist=true and drive them with terminal_input / terminal_key.",
|
|
1384
|
-
|
|
1399
|
+
"Run the bare command — do NOT pipe through head/tail/more/less (output is returned complete anyway, and pipes hide live progress in the terminal). Use the head/tail parameters instead to trim the returned output. For interactive commands (REPLs, prompts, installers asking y/n) set persist=true and drive them with terminal_input / terminal_key.", "运行 shell 命令并返回完整输出与退出码。命令在可见终端中运行。\n" +
|
|
1400
|
+
"persist=false(默认,一次性):每次调用新建一个终端,运行至结束,然后 shell 退出(进程结束),输出保留在终端列表中供稍后查看——如同普通 bash 调用,但每条命令都会留下一条可查看的终端记录。\n" +
|
|
1401
|
+
"persist=true:命令在常驻可见终端 'ai-bash' 中运行——cd、venv 激活、ssh 会话等 shell 状态跨调用保留;可用 terminal_wait 重新阻塞等待后台命令,或随时用 terminal_read / terminal_input / terminal_key 观察或交互。\n" +
|
|
1402
|
+
"直接运行裸命令——不要经 head/tail/more/less 管道(反正会返回完整输出,管道还会挡住终端里的实时进度)。用 head/tail 参数截断返回的输出。交互式命令(REPL、提示符、问 y/n 的安装程序)请设 persist=true 并用 terminal_input / terminal_key 驱动。"),
|
|
1403
|
+
promptSnippet: bilingual("run shell commands (persist=true keeps the terminal alive across calls)", "运行 shell 命令(persist=true 让终端跨调用保持存活)"),
|
|
1385
1404
|
parameters: Type.Object({
|
|
1386
|
-
command: Type.String({ description: "The shell command to run" }),
|
|
1387
|
-
timeout: Type.Optional(Type.Number({ description: "Optional timeout in seconds" })),
|
|
1405
|
+
command: Type.String({ description: bilingual("The shell command to run", "要运行的 shell 命令") }),
|
|
1406
|
+
timeout: Type.Optional(Type.Number({ description: bilingual("Optional timeout in seconds", "可选的超时秒数") })),
|
|
1388
1407
|
persist: Type.Optional(Type.Boolean({
|
|
1389
|
-
description: "Keep the terminal alive after the command (default: false → a one-shot terminal that exits when the command finishes while its output is retained for review). true runs in the persistent 'ai-bash' terminal so shell state (cd/venv/ssh) is retained across calls and the terminal stays interactive.",
|
|
1408
|
+
description: bilingual("Keep the terminal alive after the command (default: false → a one-shot terminal that exits when the command finishes while its output is retained for review). true runs in the persistent 'ai-bash' terminal so shell state (cd/venv/ssh) is retained across calls and the terminal stays interactive.", "运行后保持终端存活(默认 false → 命令结束时退出的、输出保留供查看的一次性终端)。true 则在常驻 'ai-bash' 终端中运行,shell 状态(cd/venv/ssh)跨调用保留、终端保持可交互。"),
|
|
1390
1409
|
})),
|
|
1391
1410
|
head: Type.Optional(Type.Integer({
|
|
1392
1411
|
minimum: 1,
|
|
1393
1412
|
maximum: 5000,
|
|
1394
|
-
description: "Only return the FIRST N lines of output (like `| head -N`). Use this for verbose commands instead of piping through head.",
|
|
1413
|
+
description: bilingual("Only return the FIRST N lines of output (like `| head -N`). Use this for verbose commands instead of piping through head.", "只返回输出的前 N 行(如 `| head -N`)。输出冗长的命令请用它,而不要经 head 管道。"),
|
|
1395
1414
|
})),
|
|
1396
1415
|
tail: Type.Optional(Type.Integer({
|
|
1397
1416
|
minimum: 1,
|
|
1398
1417
|
maximum: 5000,
|
|
1399
|
-
description: "Only return the LAST N lines of output (like `| tail -N`). Use this for verbose commands instead of piping through tail.",
|
|
1418
|
+
description: bilingual("Only return the LAST N lines of output (like `| tail -N`). Use this for verbose commands instead of piping through tail.", "只返回输出的后 N 行(如 `| tail -N`)。输出冗长的命令请用它,而不要经 tail 管道。"),
|
|
1400
1419
|
})),
|
|
1401
1420
|
}),
|
|
1402
1421
|
execute: async (_id, p, signal) => {
|
|
1422
|
+
const lang = opts.lang?.() ?? "en";
|
|
1403
1423
|
const persist = p.persist ?? opts.defaultPersist();
|
|
1404
1424
|
// create() 对已存活的同名终端原样返回、对已退出的原地重启。
|
|
1405
1425
|
// forceBash:该终端永远跑 bash(而非用户登录 shell),模型写的
|
|
@@ -1412,7 +1432,7 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1412
1432
|
forceBash: true,
|
|
1413
1433
|
agentBash: true,
|
|
1414
1434
|
}) === null) {
|
|
1415
|
-
throw new Error(`无法打开 AI bash 终端(${termId}
|
|
1435
|
+
throw new Error(pick(lang, `无法打开 AI bash 终端(${termId})`, `Failed to open the AI bash terminal (${termId})`, "terminals.bash.open.failed", { termId }));
|
|
1416
1436
|
}
|
|
1417
1437
|
// 阻塞等待期间挂起活力提醒(我们自己在检测静默,避免双重通知)。
|
|
1418
1438
|
terminals.suspendIdleWatch(termId);
|
|
@@ -1439,12 +1459,15 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1439
1459
|
// tail 文件让模型看到日志尾部与真实退出码(否则输出为空)。
|
|
1440
1460
|
const redirect = stripped && limiter.kind === "tail" ? detectStdoutRedirect(runCommand) : null;
|
|
1441
1461
|
const tailFile = redirect ? { file: redirect.file, lines: limiter.lines ?? 10 } : undefined;
|
|
1462
|
+
// 复杂子表达式先 hoist 成干净 const(issue #91 v2:vars key 不写复杂表达式)。
|
|
1463
|
+
const limiterSegment = limiter.segment;
|
|
1464
|
+
const limiterTailLines = limiter.lines ?? 10;
|
|
1465
|
+
const limiterTailZh = limiter.kind === "tail" ? `本次返回末尾 ${limiterTailLines} 行。` : "本次返回全部输出。";
|
|
1466
|
+
const limiterTailEn = limiter.kind === "tail"
|
|
1467
|
+
? `Returning the last ${limiterTailLines} lines this time.`
|
|
1468
|
+
: "Returning the full output this time.";
|
|
1442
1469
|
const limiterNote = stripped
|
|
1443
|
-
?
|
|
1444
|
-
limiter.segment +
|
|
1445
|
-
"」这类限输出/过滤管道——已在终端里直跑底层命令(实时可见 + 真实退出码),只按参数返回片段。" +
|
|
1446
|
-
(limiter.kind === "tail" ? "本次返回末尾 " + (limiter.lines ?? 10) + " 行。" : "本次返回全部输出。") +
|
|
1447
|
-
" 后续直接用 bash(command, tail=N) 参数限输出。]"
|
|
1470
|
+
? pick(lang, `\n[注:检测到你带了「${limiterSegment}」这类限输出/过滤管道——已在终端里直跑底层命令(实时可见 + 真实退出码),只按参数返回片段。${limiterTailZh} 后续直接用 bash(command, tail=N) 参数限输出。]`, `\n[Note: detected a trailing output-limiting pipe "${limiterSegment}" — ran the underlying command directly in the terminal (live output + real exit code) and trimmed only the returned slice. ${limiterTailEn} Next time use the bash(command, tail=N) parameter to limit output.]`, "terminals.bash.limiter.note", { limiterSegment, limiterTailZh, limiterTailEn })
|
|
1448
1471
|
: "";
|
|
1449
1472
|
try {
|
|
1450
1473
|
let collected = "";
|
|
@@ -1461,7 +1484,7 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1461
1484
|
terminals.setSentinelPending(termId, false);
|
|
1462
1485
|
terminals.inputChecked(termId, "\x03");
|
|
1463
1486
|
closeOneShot();
|
|
1464
|
-
throw new Error("Command aborted");
|
|
1487
|
+
throw new Error(pick(lang, "命令已中止", "Command aborted", "terminals.bash.aborted"));
|
|
1465
1488
|
}
|
|
1466
1489
|
await sleep(60);
|
|
1467
1490
|
const read = terminals.read(termId, cursor);
|
|
@@ -1489,11 +1512,12 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1489
1512
|
terminals.setSentinelPending(termId, false);
|
|
1490
1513
|
terminals.inputChecked(termId, "\x03");
|
|
1491
1514
|
closeOneShot();
|
|
1492
|
-
|
|
1515
|
+
const timeoutPartial = truncateMiddle(stripAnsi(collected), 4000);
|
|
1516
|
+
throw new Error(pick(lang, `Command timed out after ${p.timeout}s(已发 Ctrl+C;已有输出:${timeoutPartial})`, `Command timed out after ${p.timeout}s (sent Ctrl+C; partial output: ${timeoutPartial})`, "terminals.bash.timeout", { "p.timeout": p.timeout, timeoutPartial }));
|
|
1493
1517
|
}
|
|
1494
1518
|
// 静默解阻(仅持久终端):转后台 + 注册完成观察器,立即把控制权还给模型。
|
|
1495
1519
|
if (persist && idleMs > 0 && Date.now() - lastDataAt >= idleMs) {
|
|
1496
|
-
return backgroundResult(terminals, opts, runCommand, applyHeadTail(cleanBashOutput(collected), p.head, effectiveTail), Math.round((Date.now() - lastDataAt) / 1000));
|
|
1520
|
+
return backgroundResult(terminals, opts, runCommand, applyHeadTail(cleanBashOutput(collected), p.head, effectiveTail), Math.round((Date.now() - lastDataAt) / 1000), lang);
|
|
1497
1521
|
}
|
|
1498
1522
|
}
|
|
1499
1523
|
}
|
|
@@ -1504,7 +1528,7 @@ export function makeTerminalBashTool(terminals, opts) {
|
|
|
1504
1528
|
});
|
|
1505
1529
|
}
|
|
1506
1530
|
/** 静默解阻路径:注册完成观察器后立即返回「仍在后台运行」。 */
|
|
1507
|
-
function backgroundResult(terminals, opts, command, partialText, silentSeconds) {
|
|
1531
|
+
function backgroundResult(terminals, opts, command, partialText, silentSeconds, lang) {
|
|
1508
1532
|
terminals.watchOutput("ai-bash", BASH_SENTINEL_RE, (m) => {
|
|
1509
1533
|
// 后台命令最终结束(或终端被关)→ 清除待决标记,terminal_wait 不再适用。
|
|
1510
1534
|
terminals.setSentinelPending("ai-bash", false);
|
|
@@ -1516,14 +1540,20 @@ function backgroundResult(terminals, opts, command, partialText, silentSeconds)
|
|
|
1516
1540
|
});
|
|
1517
1541
|
// partialText 已在调用方做过 cleanBashOutput + applyTail。
|
|
1518
1542
|
const partial = truncateMiddle(partialText, 6000);
|
|
1543
|
+
// 空输出占位按语言预渲染(issue #91 v2:vars 只收干净标识)。
|
|
1544
|
+
const partialZh = partial || "(暂无输出)";
|
|
1545
|
+
const partialEn = partial || "(no output yet)";
|
|
1519
1546
|
return {
|
|
1520
1547
|
content: [
|
|
1521
1548
|
{
|
|
1522
1549
|
type: "text",
|
|
1523
|
-
text: `命令仍在持久终端 ai-bash 中运行(已连续 ${silentSeconds} 秒无输出,未结束)。` +
|
|
1550
|
+
text: pick(lang, `命令仍在持久终端 ai-bash 中运行(已连续 ${silentSeconds} 秒无输出,未结束)。` +
|
|
1524
1551
|
`本次调用不阻塞——命令继续在后台执行,结束时你会收到自动通知。\n` +
|
|
1525
|
-
`已有输出:\n${
|
|
1526
|
-
`要重新阻塞等它结束就用 terminal_wait(terminalId="ai-bash")(无需反复轮询);需要交互用 terminal_input / terminal_key(Ctrl+C 可终止)。`,
|
|
1552
|
+
`已有输出:\n${partialZh}\n` +
|
|
1553
|
+
`要重新阻塞等它结束就用 terminal_wait(terminalId="ai-bash")(无需反复轮询);需要交互用 terminal_input / terminal_key(Ctrl+C 可终止)。`, `Command still running in the persistent terminal ai-bash (no output for ${silentSeconds}s, not finished). ` +
|
|
1554
|
+
`This call does not block — the command keeps running in the background and you will be notified automatically when it finishes.\n` +
|
|
1555
|
+
`Partial output:\n${partialEn}\n` +
|
|
1556
|
+
`To block until it finishes, use terminal_wait(terminalId="ai-bash") (no polling needed); use terminal_input / terminal_key to interact (Ctrl+C aborts).`, "terminals.bash.background.running", { silentSeconds, partialZh, partialEn }),
|
|
1527
1557
|
},
|
|
1528
1558
|
],
|
|
1529
1559
|
details: { running: true, terminalId: "ai-bash", silentSeconds },
|
|
@@ -1548,7 +1578,10 @@ export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools ar
|
|
|
1548
1578
|
- The user explicitly asks you to work in the visible terminal panel.
|
|
1549
1579
|
Liveness watchdog: terminals you touched (create/input/key) are monitored - if one goes silent with no new output while you are working (default 15s), an automatic system reminder is injected into the conversation. Treat it as a prompt to check that terminal (terminal_read), respond to an input prompt (terminal_input / terminal_key), or close it (terminal_close) if it is no longer needed.`;
|
|
1550
1580
|
/** Build the agent-facing persistent terminal tools for one conversation. */
|
|
1551
|
-
export function makePersistentTerminalTools(terminals, cwd
|
|
1581
|
+
export function makePersistentTerminalTools(terminals, cwd,
|
|
1582
|
+
/** per-call 返回文本的服务端语言(默认英文);工具 definition 走 bilingual 内联双语。 */
|
|
1583
|
+
lang) {
|
|
1584
|
+
const getLang = lang ?? (() => "en");
|
|
1552
1585
|
const result = (text, details = {}) => ({ content: [{ type: "text", text }], details });
|
|
1553
1586
|
const failIf = (error) => {
|
|
1554
1587
|
if (error)
|
|
@@ -1558,62 +1591,70 @@ export function makePersistentTerminalTools(terminals, cwd) {
|
|
|
1558
1591
|
defineTool({
|
|
1559
1592
|
name: "terminal_create",
|
|
1560
1593
|
label: "Create terminal",
|
|
1561
|
-
description: "Create a named persistent interactive PTY in the current workspace. Use terminal_input or terminal_key to interact with it and terminal_read to inspect incremental output. Prefer this over bash when the program is interactive/TUI-based (REPLs, vim/htop, y/n prompts), when starting a long-running server you want to keep observing or interrupt, or when the user asks to work in the visible terminal. For simple one-shot commands use bash instead.",
|
|
1562
|
-
promptSnippet: "run interactive programs or long-running servers in a persistent visible PTY (multi-step: create → input/key → read)",
|
|
1594
|
+
description: bilingual("Create a named persistent interactive PTY in the current workspace. Use terminal_input or terminal_key to interact with it and terminal_read to inspect incremental output. Prefer this over bash when the program is interactive/TUI-based (REPLs, vim/htop, y/n prompts), when starting a long-running server you want to keep observing or interrupt, or when the user asks to work in the visible terminal. For simple one-shot commands use bash instead.", "在当前工作区创建具名常驻交互式 PTY。用 terminal_input 或 terminal_key 与之交互,用 terminal_read 查看增量输出。程序是交互式/TUI(REPL、vim/htop、y/n 提示)、要启动长驻服务并持续观察或中断、或用户明确要求在可见终端里操作时,优先用它而非 bash。简单的一次性命令请用 bash。"),
|
|
1595
|
+
promptSnippet: bilingual("run interactive programs or long-running servers in a persistent visible PTY (multi-step: create → input/key → read)", "在常驻可见 PTY 中运行交互式程序或长驻服务(多步:create → input/key → read)"),
|
|
1563
1596
|
parameters: Type.Object({
|
|
1564
|
-
terminalId: Type.String({ description: "Stable terminal name" }),
|
|
1565
|
-
cwd: Type.Optional(Type.String({ description: "Workspace-relative directory" })),
|
|
1597
|
+
terminalId: Type.String({ description: bilingual("Stable terminal name", "稳定的终端名称") }),
|
|
1598
|
+
cwd: Type.Optional(Type.String({ description: bilingual("Workspace-relative directory", "工作区相对目录") })),
|
|
1566
1599
|
cols: Type.Optional(Type.Integer({ minimum: 2, maximum: 500 })),
|
|
1567
1600
|
rows: Type.Optional(Type.Integer({ minimum: 2, maximum: 200 })),
|
|
1568
1601
|
}),
|
|
1569
1602
|
execute: async (_id, p) => {
|
|
1603
|
+
const lang = getLang();
|
|
1570
1604
|
const info = terminals.create(p.terminalId, p.cwd ?? cwd, p.cols ?? 120, p.rows ?? 40, cwd, p.terminalId);
|
|
1605
|
+
const infoJson = JSON.stringify(info);
|
|
1571
1606
|
if (!info)
|
|
1572
|
-
throw new Error(`创建终端失败:${p.terminalId}`);
|
|
1607
|
+
throw new Error(pick(lang, `创建终端失败:${p.terminalId}`, `Failed to create terminal: ${p.terminalId}`, "terminals.create.failed", { "p.terminalId": p.terminalId }));
|
|
1573
1608
|
// AI 创建 → 启动活力检测纪元(静默提醒只针对 agent 触碰过的终端)。
|
|
1574
1609
|
terminals.noteAgentActivity(p.terminalId);
|
|
1575
|
-
return result(`终端已创建:${
|
|
1610
|
+
return result(pick(lang, `终端已创建:${infoJson}`, `Terminal created: ${infoJson}`, "terminals.create.done", { infoJson }), info);
|
|
1576
1611
|
},
|
|
1577
1612
|
}),
|
|
1578
1613
|
defineTool({
|
|
1579
1614
|
name: "terminal_list",
|
|
1580
1615
|
label: "List terminals",
|
|
1581
|
-
description: "List all persistent PTY terminals owned by this conversation.",
|
|
1582
|
-
promptSnippet: "list persistent terminals",
|
|
1616
|
+
description: bilingual("List all persistent PTY terminals owned by this conversation.", "列出本对话拥有的全部常驻 PTY 终端。"),
|
|
1617
|
+
promptSnippet: bilingual("list persistent terminals", "列出常驻终端"),
|
|
1583
1618
|
parameters: Type.Object({}),
|
|
1584
1619
|
execute: async () => result(JSON.stringify(terminals.list()), terminals.list()),
|
|
1585
1620
|
}),
|
|
1586
1621
|
defineTool({
|
|
1587
1622
|
name: "terminal_close",
|
|
1588
1623
|
label: "Close terminal",
|
|
1589
|
-
description: "Close a persistent PTY and terminate its process tree.",
|
|
1624
|
+
description: bilingual("Close a persistent PTY and terminate its process tree.", "关闭常驻 PTY 并终止其进程树。"),
|
|
1590
1625
|
parameters: Type.Object({ terminalId: Type.String() }),
|
|
1591
1626
|
execute: async (_id, p) => {
|
|
1627
|
+
const lang = getLang();
|
|
1592
1628
|
if (!terminals.has(p.terminalId))
|
|
1593
|
-
throw new Error(`终端不存在:${p.terminalId}`);
|
|
1629
|
+
throw new Error(pick(lang, `终端不存在:${p.terminalId}`, `Terminal not found: ${p.terminalId}`, "terminals.close.not.found", { "p.terminalId": p.terminalId }));
|
|
1594
1630
|
terminals.kill(p.terminalId);
|
|
1595
|
-
return result(`终端已关闭:${p.terminalId}`
|
|
1631
|
+
return result(pick(lang, `终端已关闭:${p.terminalId}`, `Terminal closed: ${p.terminalId}`, "terminals.close.done", {
|
|
1632
|
+
"p.terminalId": p.terminalId,
|
|
1633
|
+
}));
|
|
1596
1634
|
},
|
|
1597
1635
|
}),
|
|
1598
1636
|
defineTool({
|
|
1599
1637
|
name: "terminal_input",
|
|
1600
1638
|
label: "Send terminal input",
|
|
1601
|
-
description: "Send arbitrary text to a persistent PTY. Include newline when a command should be submitted.",
|
|
1639
|
+
description: bilingual("Send arbitrary text to a persistent PTY. Include newline when a command should be submitted.", "向常驻 PTY 发送任意文本。需要提交命令时带上换行。"),
|
|
1602
1640
|
parameters: Type.Object({ terminalId: Type.String(), data: Type.String() }),
|
|
1603
1641
|
execute: async (_id, p) => {
|
|
1642
|
+
const lang = getLang();
|
|
1604
1643
|
failIf(terminals.inputChecked(p.terminalId, p.data));
|
|
1605
1644
|
// AI 发了输入 = 在等结果,重开一个静默纪元。
|
|
1606
1645
|
terminals.noteAgentActivity(p.terminalId);
|
|
1607
|
-
return result(`已发送 ${p.data.length} 个字符到 ${p.terminalId}`);
|
|
1646
|
+
return result(pick(lang, `已发送 ${p.data.length} 个字符到 ${p.terminalId}`, `Sent ${p.data.length} chars to ${p.terminalId}`, "terminals.input.sent", { "p.data.length": p.data.length, "p.terminalId": p.terminalId }));
|
|
1608
1647
|
},
|
|
1609
1648
|
}),
|
|
1610
1649
|
defineTool({
|
|
1611
1650
|
name: "terminal_key",
|
|
1612
1651
|
label: "Send terminal key",
|
|
1613
|
-
description: "Send Enter, Tab, arrows, function keys, or Ctrl/Alt combinations to a persistent PTY.",
|
|
1652
|
+
description: bilingual("Send Enter, Tab, arrows, function keys, or Ctrl/Alt combinations to a persistent PTY.", "向常驻 PTY 发送 Enter、Tab、方向键、功能键或 Ctrl/Alt 组合键。"),
|
|
1614
1653
|
parameters: Type.Object({
|
|
1615
1654
|
terminalId: Type.String(),
|
|
1616
|
-
key: Type.String({
|
|
1655
|
+
key: Type.String({
|
|
1656
|
+
description: bilingual("Enter, Tab, ArrowUp, c, etc.", "按键名,如 Enter、Tab、ArrowUp、c 等"),
|
|
1657
|
+
}),
|
|
1617
1658
|
modifiers: Type.Optional(Type.Object({
|
|
1618
1659
|
ctrl: Type.Optional(Type.Boolean()),
|
|
1619
1660
|
alt: Type.Optional(Type.Boolean()),
|
|
@@ -1621,16 +1662,17 @@ export function makePersistentTerminalTools(terminals, cwd) {
|
|
|
1621
1662
|
})),
|
|
1622
1663
|
}),
|
|
1623
1664
|
execute: async (_id, p) => {
|
|
1665
|
+
const lang = getLang();
|
|
1624
1666
|
failIf(terminals.key(p.terminalId, p.key, p.modifiers));
|
|
1625
1667
|
// 同 terminal_input:AI 主动交互后重新计时。
|
|
1626
1668
|
terminals.noteAgentActivity(p.terminalId);
|
|
1627
|
-
return result(`已发送按键 ${p.key} 到 ${p.terminalId}`);
|
|
1669
|
+
return result(pick(lang, `已发送按键 ${p.key} 到 ${p.terminalId}`, `Sent key ${p.key} to ${p.terminalId}`, "terminals.key.sent", { "p.key": p.key, "p.terminalId": p.terminalId }));
|
|
1628
1670
|
},
|
|
1629
1671
|
}),
|
|
1630
1672
|
defineTool({
|
|
1631
1673
|
name: "terminal_read",
|
|
1632
1674
|
label: "Read terminal output",
|
|
1633
|
-
description: "Read incremental output from a persistent PTY. Keep the returned cursor and pass it on the next read; optionally wait for new output or process exit.",
|
|
1675
|
+
description: bilingual("Read incremental output from a persistent PTY. Keep the returned cursor and pass it on the next read; optionally wait for new output or process exit.", "从常驻 PTY 读取增量输出。保留返回的 cursor,下次读取时传回;可选择等待新输出或进程退出。"),
|
|
1634
1676
|
parameters: Type.Object({
|
|
1635
1677
|
terminalId: Type.String(),
|
|
1636
1678
|
cursor: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
@@ -1638,35 +1680,44 @@ export function makePersistentTerminalTools(terminals, cwd) {
|
|
|
1638
1680
|
waitMs: Type.Optional(Type.Integer({ minimum: 0, maximum: 120000 })),
|
|
1639
1681
|
}),
|
|
1640
1682
|
execute: async (_id, p, signal) => {
|
|
1683
|
+
const lang = getLang();
|
|
1641
1684
|
const cursor = p.cursor ?? 0;
|
|
1642
1685
|
if (p.waitMs)
|
|
1643
1686
|
await terminals.waitForOutput(p.terminalId, cursor, p.waitMs, signal);
|
|
1644
1687
|
const read = terminals.read(p.terminalId, cursor, p.maxBytes ?? 20000);
|
|
1645
1688
|
if (!read)
|
|
1646
|
-
throw new Error(`终端不存在:${p.terminalId}`);
|
|
1689
|
+
throw new Error(pick(lang, `终端不存在:${p.terminalId}`, `Terminal not found: ${p.terminalId}`, "terminals.read.not.found", { "p.terminalId": p.terminalId }));
|
|
1647
1690
|
return result(JSON.stringify(read), read);
|
|
1648
1691
|
},
|
|
1649
1692
|
}),
|
|
1650
1693
|
defineTool({
|
|
1651
1694
|
name: "terminal_wait",
|
|
1652
1695
|
label: "Wait for terminal command",
|
|
1653
|
-
description: "Block until a command started THROUGH THE BASH TOOL finishes (its exit marker appears) or the timeout expires — no polling needed. Only applies to terminals with a pending bash-tool command; terminals driven manually via terminal_input (e.g. interactive programs) have no completion marker — use terminal_read(waitMs=…) to observe those instead. Returns {finished, exitCode} plus the output produced while waiting; finished=false means it is STILL running (call again to keep waiting).",
|
|
1654
|
-
promptSnippet: "block until a terminal's current command finishes (no polling)",
|
|
1696
|
+
description: bilingual("Block until a command started THROUGH THE BASH TOOL finishes (its exit marker appears) or the timeout expires — no polling needed. Only applies to terminals with a pending bash-tool command; terminals driven manually via terminal_input (e.g. interactive programs) have no completion marker — use terminal_read(waitMs=…) to observe those instead. Returns {finished, exitCode} plus the output produced while waiting; finished=false means it is STILL running (call again to keep waiting).", "阻塞等待经 BASH 工具启动的命令结束(出现退出标记)或超时——无需轮询。仅适用于有待决 bash 工具命令的终端;经 terminal_input 手动驱动的终端(如交互式程序)没有完成标记——观察它们请用 terminal_read(waitMs=…)。返回 {finished, exitCode} 及等待期间产生的输出;finished=false 表示仍在运行(可再次调用继续等)。"),
|
|
1697
|
+
promptSnippet: bilingual("block until a terminal's current command finishes (no polling)", "阻塞等待终端当前命令结束(无需轮询)"),
|
|
1655
1698
|
parameters: Type.Object({
|
|
1656
1699
|
terminalId: Type.String(),
|
|
1657
|
-
cursor: Type.Optional(Type.Integer({
|
|
1658
|
-
|
|
1700
|
+
cursor: Type.Optional(Type.Integer({
|
|
1701
|
+
minimum: 0,
|
|
1702
|
+
description: bilingual("Ignore exit markers before this absolute offset (default: now)", "忽略该绝对偏移之前的退出标记(默认:现在)"),
|
|
1703
|
+
})),
|
|
1704
|
+
maxWaitMs: Type.Optional(Type.Integer({
|
|
1705
|
+
minimum: 100,
|
|
1706
|
+
maximum: 600000,
|
|
1707
|
+
description: bilingual("Max wait in ms (default 300000)", "最长等待毫秒数(默认 300000)"),
|
|
1708
|
+
})),
|
|
1659
1709
|
}),
|
|
1660
1710
|
execute: async (_id, p, signal) => {
|
|
1711
|
+
const lang = getLang();
|
|
1661
1712
|
if (!terminals.has(p.terminalId)) {
|
|
1662
|
-
throw new Error(`终端不存在:${p.terminalId}(可能已被关闭或会话重置,请先 terminal_create
|
|
1713
|
+
throw new Error(pick(lang, `终端不存在:${p.terminalId}(可能已被关闭或会话重置,请先 terminal_create)`, `Terminal not found: ${p.terminalId} (it may have been closed or the session was reset — run terminal_create first)`, "terminals.wait.not.found", { "p.terminalId": p.terminalId }));
|
|
1663
1714
|
}
|
|
1664
1715
|
// 没有带哨兵的待决命令:shell 空闲在提示符,或该终端的命令是经
|
|
1665
1716
|
// terminal_input 手动发的(无完成标记)——等哨兵永远等不到,直接
|
|
1666
1717
|
// 说明并引导改用 terminal_read,避免 AI 无限重试。(显式传 cursor
|
|
1667
1718
|
// 的调用是有目的的追溯查询,不拦。)
|
|
1668
1719
|
if (p.cursor === undefined && !terminals.isSentinelPending(p.terminalId)) {
|
|
1669
|
-
const why = `终端 ${p.terminalId} 当前没有正在等待完成的 bash 工具命令(shell 空闲,或该命令是通过 terminal_input 发出的、没有完成标记)。terminal_wait 不适用;要观察输出请用 terminal_read(terminalId="${p.terminalId}", waitMs=…)
|
|
1720
|
+
const why = pick(lang, `终端 ${p.terminalId} 当前没有正在等待完成的 bash 工具命令(shell 空闲,或该命令是通过 terminal_input 发出的、没有完成标记)。terminal_wait 不适用;要观察输出请用 terminal_read(terminalId="${p.terminalId}", waitMs=…)。`, `Terminal ${p.terminalId} has no pending bash-tool command to wait for (the shell is idle, or the command was sent via terminal_input and has no completion marker). terminal_wait does not apply; use terminal_read(terminalId="${p.terminalId}", waitMs=…) to observe output.`, "terminals.wait.no.pending", { "p.terminalId": p.terminalId });
|
|
1670
1721
|
return result(JSON.stringify({ applicable: false, reason: why }), { applicable: false });
|
|
1671
1722
|
}
|
|
1672
1723
|
const cursor = p.cursor ?? terminals.endCursor(p.terminalId) ?? 0;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readdirSync, readFileSync, realpathSync, existsSync } from "node:fs";
|
|
11
11
|
import { delimiter, dirname, join } from "node:path";
|
|
12
|
+
import { pick } from "./i18n.js";
|
|
12
13
|
const PI_CORE_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
13
14
|
const REGISTRY = "https://registry.npmjs.org";
|
|
14
15
|
const FETCH_TIMEOUT_MS = 8_000;
|
|
@@ -274,7 +275,10 @@ export async function fetchLatest(fetcher, name) {
|
|
|
274
275
|
* error item (upToDate: false) without failing the rest. Results keep the
|
|
275
276
|
* input order. Bounded concurrency (CONCURRENCY) keeps registry load polite.
|
|
276
277
|
*/
|
|
277
|
-
export async function checkAll(targets, fetcher = defaultFetcher
|
|
278
|
+
export async function checkAll(targets, fetcher = defaultFetcher,
|
|
279
|
+
/** 单项 registry 查询失败时的 error 文案语言(默认英文)。 */
|
|
280
|
+
lang) {
|
|
281
|
+
const l = lang?.() ?? "en";
|
|
278
282
|
const results = Array.from({ length: targets.length });
|
|
279
283
|
let cursor = 0;
|
|
280
284
|
async function worker() {
|
|
@@ -293,6 +297,7 @@ export async function checkAll(targets, fetcher = defaultFetcher) {
|
|
|
293
297
|
};
|
|
294
298
|
}
|
|
295
299
|
catch (err) {
|
|
300
|
+
const errMessage = err.message;
|
|
296
301
|
results[i] = {
|
|
297
302
|
name: t.name,
|
|
298
303
|
kind: t.kind,
|
|
@@ -300,7 +305,7 @@ export async function checkAll(targets, fetcher = defaultFetcher) {
|
|
|
300
305
|
latest: null,
|
|
301
306
|
latestPublishedAt: null,
|
|
302
307
|
upToDate: false,
|
|
303
|
-
error: `检查更新失败:${
|
|
308
|
+
error: pick(l, `检查更新失败:${errMessage}`, `Failed to check for updates: ${errMessage}`, "updatecheck.check.failed", { errMessage }),
|
|
304
309
|
};
|
|
305
310
|
}
|
|
306
311
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getServerBlock, pick } from "./i18n.js";
|
|
1
2
|
/** Per-batch timeout; a slow vision provider shouldn't stall a prompt forever. */
|
|
2
3
|
const TRANSCRIBE_TIMEOUT_MS = Number(process.env.PI_WEB_VISION_TIMEOUT_MS ?? 90_000);
|
|
3
4
|
/** Cap the transcript length so it doesn't blow up the main context. */
|
|
@@ -45,6 +46,24 @@ Follow these rules:
|
|
|
45
46
|
5. If part of the image is too blurry/low-resolution to read, say "(读不清)" or "unclear" for that part — NEVER invent or guess content you cannot see.
|
|
46
47
|
6. If there are multiple images, address them in order (图 1 / Image 1, 图 2 / Image 2, ...).
|
|
47
48
|
7. Output only the transcript. No preamble, no commentary about the image itself.`;
|
|
49
|
+
/**
|
|
50
|
+
* 中文版内置转写提示词(调用方按 lang 选用,默认英文)。与 SYSTEM_PROMPT 同义,
|
|
51
|
+
* 仅语言不同——英文版是默认行为,保持原有转写质量不变。
|
|
52
|
+
*/
|
|
53
|
+
export const SYSTEM_PROMPT_ZH = `你是文本模型的视觉桥。你会收到一张或多张图片,必须把它们转写成精确的结构化文字证据,让看不到图片的模型也能准确回答相关问题。
|
|
54
|
+
|
|
55
|
+
规则:
|
|
56
|
+
1. 逐字转写所有可见文本,保留措辞、拼写、标点与换行。这是最重要的部分——阅读者依赖你的转写,而非图片本身。
|
|
57
|
+
2. 按阅读顺序描述版式:标题、段落、列表、表格、按钮、面板——说明它们出现的位置。
|
|
58
|
+
3. 表格/图表/示意图:读出坐标轴、刻度(注意对数轴)、图例项、系列名、高亮点及其坐标,以及能辨认的数据值。
|
|
59
|
+
4. 点名实体:人物、产品、公司、颜色、风格、物体、动作。
|
|
60
|
+
5. 图片局部太模糊/分辨率太低读不清时,对该部分写“(读不清)”或 "unclear”——绝不编造或猜测看不见的内容。
|
|
61
|
+
6. 多张图片时按顺序逐张处理(图 1 / Image 1,图 2 / Image 2,……)。
|
|
62
|
+
7. 只输出转写结果,不加开场白,不评论图片本身。`;
|
|
63
|
+
/** 按服务端语言选用内置转写提示词(默认英文)。 */
|
|
64
|
+
export function getVisionSystemPrompt(lang = "en") {
|
|
65
|
+
return getServerBlock(lang, "vision.system", SYSTEM_PROMPT_ZH.split("\n"), SYSTEM_PROMPT.split("\n")).join("\n");
|
|
66
|
+
}
|
|
48
67
|
/**
|
|
49
68
|
* Assemble the final vision-model system prompt from the settings-panel prefs.
|
|
50
69
|
* mode "append": custom text appended after the default prompt (empty custom =
|
|
@@ -52,36 +71,38 @@ Follow these rules:
|
|
|
52
71
|
* an empty custom text still falls back to the default (never send an empty
|
|
53
72
|
* system prompt to the vision model).
|
|
54
73
|
*/
|
|
55
|
-
export function buildVisionBridgePrompt(mode, custom) {
|
|
74
|
+
export function buildVisionBridgePrompt(mode, custom, lang = "en") {
|
|
56
75
|
const text = custom?.trim() ?? "";
|
|
76
|
+
const base = getVisionSystemPrompt(lang);
|
|
57
77
|
if (mode === "replace" && text)
|
|
58
78
|
return text;
|
|
59
79
|
if (text)
|
|
60
|
-
return `${
|
|
61
|
-
return
|
|
80
|
+
return `${base}\n\n${text}`;
|
|
81
|
+
return base;
|
|
62
82
|
}
|
|
63
83
|
/** Per-batch user instruction appended after the images. */
|
|
64
|
-
function buildUserPrompt(count) {
|
|
84
|
+
function buildUserPrompt(count, lang = "en") {
|
|
65
85
|
if (count <= 1) {
|
|
66
|
-
return "请逐字转写这张图片的内容,并按上述规则输出结构化文字证据。";
|
|
86
|
+
return pick(lang, "请逐字转写这张图片的内容,并按上述规则输出结构化文字证据。", "Transcribe this image verbatim and output structured text evidence following the rules above.", "vision.transcribe.single");
|
|
67
87
|
}
|
|
68
|
-
return `请按图片顺序(图 1 到 图 ${count}
|
|
88
|
+
return pick(lang, `请按图片顺序(图 1 到 图 ${count})逐张转写每张图片的内容,并按上述规则输出结构化文字证据。`, `Transcribe each image in order (Image 1 to Image ${count}) and output structured text evidence following the rules above.`, "vision.transcribe.batch", { count });
|
|
69
89
|
}
|
|
70
90
|
/**
|
|
71
91
|
* Send one batch of images to a vision model and return its transcript.
|
|
72
92
|
* Throws on timeout, abort, provider error or an empty response.
|
|
73
93
|
*/
|
|
74
94
|
export async function transcribeImages(runtime, images, options = {}) {
|
|
95
|
+
const lang = options.lang ?? "en";
|
|
75
96
|
const model = options.model ??
|
|
76
97
|
(() => {
|
|
77
98
|
const found = findVisionModels(runtime);
|
|
78
99
|
if (found.length === 0) {
|
|
79
|
-
throw new Error("未找到可用的视觉模型(models.json 中没有任何 input 含 image 的模型)");
|
|
100
|
+
throw new Error(pick(lang, "未找到可用的视觉模型(models.json 中没有任何 input 含 image 的模型)", "No vision-capable model found (no model with image input in models.json)", "vision.model.not.found"));
|
|
80
101
|
}
|
|
81
102
|
return runtime.getModel(found[0].provider, found[0].id);
|
|
82
103
|
})();
|
|
83
104
|
if (!model)
|
|
84
|
-
throw new Error("视觉模型不可用(ModelRuntime.getModel 返回空)");
|
|
105
|
+
throw new Error(pick(lang, "视觉模型不可用(ModelRuntime.getModel 返回空)", "Vision model unavailable (ModelRuntime.getModel returned empty)", "vision.model.unavailable"));
|
|
85
106
|
const ac = new AbortController();
|
|
86
107
|
const timer = setTimeout(() => ac.abort(), TRANSCRIBE_TIMEOUT_MS);
|
|
87
108
|
const onOuterAbort = () => ac.abort();
|
|
@@ -93,12 +114,12 @@ export async function transcribeImages(runtime, images, options = {}) {
|
|
|
93
114
|
mimeType: img.mimeType?.startsWith("image/") ? img.mimeType : "image/png",
|
|
94
115
|
}));
|
|
95
116
|
const context = {
|
|
96
|
-
systemPrompt: options.systemPrompt ??
|
|
117
|
+
systemPrompt: options.systemPrompt ?? getVisionSystemPrompt(lang),
|
|
97
118
|
messages: [
|
|
98
119
|
{
|
|
99
120
|
role: "user",
|
|
100
121
|
timestamp: Date.now(),
|
|
101
|
-
content: [...imageBlocks, { type: "text", text: buildUserPrompt(images.length) }],
|
|
122
|
+
content: [...imageBlocks, { type: "text", text: buildUserPrompt(images.length, lang) }],
|
|
102
123
|
},
|
|
103
124
|
],
|
|
104
125
|
};
|
|
@@ -107,7 +128,8 @@ export async function transcribeImages(runtime, images, options = {}) {
|
|
|
107
128
|
maxTokens: MAX_TRANSCRIBE_TOKENS,
|
|
108
129
|
});
|
|
109
130
|
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
110
|
-
throw new Error(msg.errorMessage ||
|
|
131
|
+
throw new Error(msg.errorMessage ||
|
|
132
|
+
pick(lang, `视觉模型异常终止(${msg.stopReason})`, `Vision model terminated abnormally (${msg.stopReason})`, "vision.model.terminated", { "msg.stopReason": msg.stopReason }));
|
|
111
133
|
}
|
|
112
134
|
const text = msg.content
|
|
113
135
|
.filter((b) => b.type === "text")
|
|
@@ -115,7 +137,7 @@ export async function transcribeImages(runtime, images, options = {}) {
|
|
|
115
137
|
.join("\n")
|
|
116
138
|
.trim();
|
|
117
139
|
if (!text) {
|
|
118
|
-
throw new Error("视觉模型返回了空的转写结果");
|
|
140
|
+
throw new Error(pick(lang, "视觉模型返回了空的转写结果", "Vision model returned an empty transcript", "vision.transcript.empty"));
|
|
119
141
|
}
|
|
120
142
|
return text;
|
|
121
143
|
}
|
|
@@ -45,6 +45,15 @@ export class WebUIContext {
|
|
|
45
45
|
this.emit = emit;
|
|
46
46
|
}
|
|
47
47
|
// -- widgets -------------------------------------------------------------
|
|
48
|
+
/** Register a widget whose lines come from a plain getter, re-evaluated on
|
|
49
|
+
* every render/refresh (no SDK Component required). Used for
|
|
50
|
+
* conversation-scoped overlays (e.g. markers) so switching conversations
|
|
51
|
+
* re-renders them without re-registering. */
|
|
52
|
+
setDynamicWidget(key, lines) {
|
|
53
|
+
this.widgets.set(key, { render: () => lines() });
|
|
54
|
+
this.lastLines.delete(key);
|
|
55
|
+
this.push();
|
|
56
|
+
}
|
|
48
57
|
/** Matches ExtensionUIContext's overloaded setWidget exactly. */
|
|
49
58
|
setWidget = (key, content, options) => {
|
|
50
59
|
void options;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"react-markdown": "^9.0.1",
|
|
86
86
|
"rehype-highlight": "^7.0.1",
|
|
87
87
|
"rehype-raw": "^7.0.0",
|
|
88
|
+
"remark-breaks": "^4.0.0",
|
|
88
89
|
"remark-gfm": "^4.0.0",
|
|
89
90
|
"typebox": "^1.3.14",
|
|
90
91
|
"ws": "^8.18.0"
|