pi-web-ui 0.64.0 → 0.64.1
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/LICENSE +21 -21
- package/README.md +504 -504
- package/README.zh-CN.md +427 -427
- package/bin/pi-web-ui.mjs +1809 -1809
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/nginx-subpath.conf +88 -88
- package/deploy/pi-web-ui-task.xml +54 -54
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +45 -22
- package/dist/server/attachments.js +3 -3
- package/dist/server/dsh/dsh-agent-service.js +27 -7
- package/dist/server/dsh/runtime/cordis.yml +1 -1
- package/dist/server/dsh/runtime/goal-rpc.mjs +645 -645
- package/dist/server/dsh/runtime/launcher.mjs +164 -164
- package/dist/server/dsh/runtime/override.patch.yml +71 -71
- package/dist/server/dsh/runtime/runtime-root.mjs +86 -86
- package/dist/server/files-service.js +6 -6
- package/dist/server/goal-service.js +4 -1
- package/dist/server/index.js +2 -2
- package/dist/server/marker-service.js +8 -3
- package/dist/server/markers/builtins/rename.js +3 -3
- package/dist/server/model-admin.js +12 -6
- package/dist/server/plugins.js +4 -4
- package/dist/server/slash-commands.js +3 -0
- package/dist/server/terminals.js +25 -15
- package/dist/server/vision-bridge.js +9 -9
- package/dist/server/webui-context.js +2 -2
- package/extensions/webui.ts +190 -190
- package/package.json +109 -109
- package/themes/cyberpunk.css +81 -81
- package/themes/dazzle.css +81 -81
- package/themes/md-preview.css +98 -98
- package/themes/white.css +160 -160
- package/web/dist/assets/TerminalPanel-C5VIdPmn.js +6 -0
- package/web/dist/assets/{index-_1vyEugI.js → index-BmDs6ukf.js} +19 -19
- package/web/dist/assets/{index-CMgDryBL.css → index-DVnuQrK5.css} +1 -1
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +21 -21
- package/web/dist/manifest.webmanifest +50 -50
- package/web/dist/sw.js +126 -126
- package/web/public/favicon.svg +8 -8
- package/web/public/manifest.webmanifest +50 -50
- package/web/public/sw.js +126 -126
- package/web/dist/assets/TerminalPanel-DNkAT34Z.js +0 -2
|
@@ -657,14 +657,14 @@ export class FilesService {
|
|
|
657
657
|
* for the target dir (the recursive watcher may not cover it on posix).
|
|
658
658
|
*/
|
|
659
659
|
async uploadFile(relDir, name, data) {
|
|
660
|
-
const emitErr = (text) => this.host.emit({ type: "notice", level: "error", text });
|
|
660
|
+
const emitErr = (text, textEn) => this.host.emit({ type: "notice", level: "error", text, textEn });
|
|
661
661
|
try {
|
|
662
662
|
const root = this.host.getCwd();
|
|
663
663
|
let wp;
|
|
664
664
|
if (relDir) {
|
|
665
665
|
wp = workspacePath(resolve(root), relDir);
|
|
666
666
|
if (!wp) {
|
|
667
|
-
emitErr(`路径超出工作区:${relDir}`);
|
|
667
|
+
emitErr(`路径超出工作区:${relDir}`, `Path outside workspace: ${relDir}`);
|
|
668
668
|
return;
|
|
669
669
|
}
|
|
670
670
|
}
|
|
@@ -678,16 +678,16 @@ export class FilesService {
|
|
|
678
678
|
const abs = resolve(wp.abs, safe);
|
|
679
679
|
const rawRel = relative(root, abs);
|
|
680
680
|
if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
|
|
681
|
-
emitErr(`文件名不合法:${name}`);
|
|
681
|
+
emitErr(`文件名不合法:${name}`, `Invalid file name: ${name}`);
|
|
682
682
|
return;
|
|
683
683
|
}
|
|
684
684
|
const buf = Buffer.from(data, "base64");
|
|
685
685
|
if (buf.length === 0) {
|
|
686
|
-
emitErr(`空文件:${name}`);
|
|
686
|
+
emitErr(`空文件:${name}`, `Empty file: ${name}`);
|
|
687
687
|
return;
|
|
688
688
|
}
|
|
689
689
|
if (buf.length > MAX_UPLOAD_BYTES) {
|
|
690
|
-
emitErr(`文件过大:${name}(上限 ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB
|
|
690
|
+
emitErr(`文件过大:${name}(上限 ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`, `File too large: ${name} (max ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`);
|
|
691
691
|
return;
|
|
692
692
|
}
|
|
693
693
|
mkdirSync(wp.abs, { recursive: true });
|
|
@@ -707,7 +707,7 @@ export class FilesService {
|
|
|
707
707
|
});
|
|
708
708
|
}
|
|
709
709
|
catch (err) {
|
|
710
|
-
emitErr(`上传文件失败:${err.message}`);
|
|
710
|
+
emitErr(`上传文件失败:${err.message}`, `Upload failed: ${err.message}`);
|
|
711
711
|
}
|
|
712
712
|
}
|
|
713
713
|
/**
|
|
@@ -611,7 +611,10 @@ export class GoalService {
|
|
|
611
611
|
g.status = "已手动停止,目标审查已中止";
|
|
612
612
|
g.statusEn = "Stopped manually, goal review aborted";
|
|
613
613
|
this.emitGoalStatus();
|
|
614
|
-
return
|
|
614
|
+
return {
|
|
615
|
+
text: "⏹ 已手动停止,目标审查已中止(想继续可重新设定目标)",
|
|
616
|
+
textEn: "⏹ Stopped manually, goal review aborted (set a new goal to continue)",
|
|
617
|
+
};
|
|
615
618
|
}
|
|
616
619
|
return null;
|
|
617
620
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -803,10 +803,10 @@ wss.on("connection", (ws) => {
|
|
|
803
803
|
case "plugin_settings": {
|
|
804
804
|
const r = pluginMgr.savePluginSettings(msg.pluginId, msg.values ?? {});
|
|
805
805
|
if (r.error) {
|
|
806
|
-
cs?.emitNotice("error", `插件设置保存失败:${r.error}`);
|
|
806
|
+
cs?.emitNotice("error", `插件设置保存失败:${r.error}`, `Failed to save plugin settings: ${r.error}`);
|
|
807
807
|
}
|
|
808
808
|
else {
|
|
809
|
-
cs?.emitNotice("info", "插件设置已保存");
|
|
809
|
+
cs?.emitNotice("info", "插件设置已保存", "Plugin settings saved");
|
|
810
810
|
}
|
|
811
811
|
break;
|
|
812
812
|
}
|
|
@@ -155,8 +155,8 @@ export class MarkerService {
|
|
|
155
155
|
const state = getOrInit(token.tool);
|
|
156
156
|
const ctx = {
|
|
157
157
|
conversationId,
|
|
158
|
-
notify: (msg, level) => {
|
|
159
|
-
this.host.emit({ type: "notice", level: level ?? "info", text: msg });
|
|
158
|
+
notify: (msg, level, msgEn) => {
|
|
159
|
+
this.host.emit({ type: "notice", level: level ?? "info", text: msg, textEn: msgEn });
|
|
160
160
|
},
|
|
161
161
|
renameConversation: (title) => {
|
|
162
162
|
this.host.renameConversation(conversationId, title);
|
|
@@ -183,7 +183,12 @@ export class MarkerService {
|
|
|
183
183
|
dirty.add(token.tool);
|
|
184
184
|
}
|
|
185
185
|
else if (result.error) {
|
|
186
|
-
this.host.emit({
|
|
186
|
+
this.host.emit({
|
|
187
|
+
type: "notice",
|
|
188
|
+
level: "warning",
|
|
189
|
+
text: `[${token.tool}] ${result.error}`,
|
|
190
|
+
textEn: `[${token.tool}] ${result.error}`,
|
|
191
|
+
});
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
194
|
for (const ns of dirty) {
|
|
@@ -43,7 +43,7 @@ export const renameMarker = {
|
|
|
43
43
|
return { applied: false, error: "当前环境不支持重命名" };
|
|
44
44
|
try {
|
|
45
45
|
ctx.renameConversation(title);
|
|
46
|
-
ctx.notify(`已重命名为:${title}`, "info");
|
|
46
|
+
ctx.notify(`已重命名为:${title}`, "info", `Renamed to: ${title}`);
|
|
47
47
|
return { applied: true, feedback: `renamed to "${title}"` };
|
|
48
48
|
}
|
|
49
49
|
catch (e) {
|
|
@@ -69,7 +69,7 @@ export const renameAliasMarker = {
|
|
|
69
69
|
return { applied: false, error: "当前环境不支持重命名" };
|
|
70
70
|
try {
|
|
71
71
|
ctx.renameConversation(trimmed);
|
|
72
|
-
ctx.notify(`已重命名为:${trimmed}`, "info");
|
|
72
|
+
ctx.notify(`已重命名为:${trimmed}`, "info", `Renamed to: ${trimmed}`);
|
|
73
73
|
return { applied: true, feedback: `renamed to "${trimmed}"` };
|
|
74
74
|
}
|
|
75
75
|
catch (e) {
|
|
@@ -92,7 +92,7 @@ export const titleAliasMarker = {
|
|
|
92
92
|
if (!ctx.renameConversation)
|
|
93
93
|
return { applied: false, error: "当前环境不支持重命名" };
|
|
94
94
|
ctx.renameConversation(title.slice(0, 80));
|
|
95
|
-
ctx.notify(`已重命名为:${title.slice(0, 80)}`, "info");
|
|
95
|
+
ctx.notify(`已重命名为:${title.slice(0, 80)}`, "info", `Renamed to: ${title.slice(0, 80)}`);
|
|
96
96
|
return { applied: true, feedback: `renamed` };
|
|
97
97
|
},
|
|
98
98
|
overlay: undefined,
|
|
@@ -575,19 +575,19 @@ export class ModelAdminService {
|
|
|
575
575
|
*/
|
|
576
576
|
async cloneProvider(providerId, reqId) {
|
|
577
577
|
const pid = providerId.trim();
|
|
578
|
-
const fail = (error) => {
|
|
579
|
-
this.host.emit({ type: "notice", level: "error", text: error });
|
|
578
|
+
const fail = (error, errorEn) => {
|
|
579
|
+
this.host.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
|
|
580
580
|
this.host.emit({ type: "clone_provider_result", reqId, ok: false, error });
|
|
581
581
|
};
|
|
582
582
|
try {
|
|
583
583
|
if (!pid) {
|
|
584
|
-
fail("请填写服务商 ID");
|
|
584
|
+
fail("请填写服务商 ID", "Enter a provider ID");
|
|
585
585
|
return;
|
|
586
586
|
}
|
|
587
587
|
const mr = this.host.modelRuntime();
|
|
588
588
|
const p = mr.getProvider(pid);
|
|
589
589
|
if (!p) {
|
|
590
|
-
fail(`供应商 ${pid}
|
|
590
|
+
fail(`供应商 ${pid} 不存在`, `Provider ${pid} does not exist`);
|
|
591
591
|
return;
|
|
592
592
|
}
|
|
593
593
|
const noBaseUrl = !p.baseUrl;
|
|
@@ -617,7 +617,7 @@ export class ModelAdminService {
|
|
|
617
617
|
models = readModels();
|
|
618
618
|
}
|
|
619
619
|
if (models.length === 0) {
|
|
620
|
-
fail(`${pid}
|
|
620
|
+
fail(`${pid} 的模型列表为空,无法复制(请稍后重试)`, `Model list for ${pid} is empty, cannot clone (retry later)`);
|
|
621
621
|
return;
|
|
622
622
|
}
|
|
623
623
|
// 供应商级 api 取占比最高,模型保留全量去重(避免 muse-spark 被过滤)
|
|
@@ -652,11 +652,14 @@ export class ModelAdminService {
|
|
|
652
652
|
text: noBaseUrl
|
|
653
653
|
? `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),该供应商无远程 baseUrl,已生成模板请手动填写 baseUrl 和新的 API 密钥后保存`
|
|
654
654
|
: `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),请填入新的 API 密钥后保存`,
|
|
655
|
+
textEn: noBaseUrl
|
|
656
|
+
? `📋 Cloned ${pid} → ${newId} (${kept.length} models); this provider has no remote baseUrl — template generated, fill in baseUrl and a new API key, then save`
|
|
657
|
+
: `📋 Cloned ${pid} → ${newId} (${kept.length} models); fill in the new API key, then save`,
|
|
655
658
|
});
|
|
656
659
|
this.host.emit({ type: "clone_provider_result", reqId, ok: true, config, configs: [config] });
|
|
657
660
|
}
|
|
658
661
|
catch (err) {
|
|
659
|
-
fail(`复制服务商失败:${err.message}`);
|
|
662
|
+
fail(`复制服务商失败:${err.message}`, `Failed to clone provider: ${err.message}`);
|
|
660
663
|
}
|
|
661
664
|
this.host.flushSnapshot();
|
|
662
665
|
}
|
|
@@ -1015,6 +1018,9 @@ export class ModelAdminService {
|
|
|
1015
1018
|
text: added > 0
|
|
1016
1019
|
? `🔄 已刷新 ${pid}:新增 ${added} 个模型,共 ${merged.length} 个`
|
|
1017
1020
|
: `🔄 已刷新 ${pid}:无新增模型(共 ${merged.length} 个)`,
|
|
1021
|
+
textEn: added > 0
|
|
1022
|
+
? `🔄 Refreshed ${pid}: ${added} new models, ${merged.length} total`
|
|
1023
|
+
: `🔄 Refreshed ${pid}: no new models (${merged.length} total)`,
|
|
1018
1024
|
});
|
|
1019
1025
|
return done(true, { added, total: merged.length });
|
|
1020
1026
|
}
|
package/dist/server/plugins.js
CHANGED
|
@@ -308,7 +308,7 @@ export class PluginManager {
|
|
|
308
308
|
if (prev === key)
|
|
309
309
|
return; // 同版本能力清单,不再打扰
|
|
310
310
|
const list = perms.length ? perms.join(", ") : "无";
|
|
311
|
-
this.notifyAll(perms.length ? "warning" : "info", `插件「${info.name}」已激活(${prev ? "能力清单变更" : "首次安装"};声明能力:${list}
|
|
311
|
+
this.notifyAll(perms.length ? "warning" : "info", `插件「${info.name}」已激活(${prev ? "能力清单变更" : "首次安装"};声明能力:${list})——请确认来源可信`, `Plugin "${info.name}" activated (${prev ? "capability list changed" : "first install"}; declared: ${list}) — verify the source is trusted`);
|
|
312
312
|
writeFileSync(markerFile, JSON.stringify({ v: 1, key, perms }), "utf8");
|
|
313
313
|
}
|
|
314
314
|
catch (err) {
|
|
@@ -344,8 +344,8 @@ export class PluginManager {
|
|
|
344
344
|
this.deliverAll({ type: "plugin_data", pluginId, payload });
|
|
345
345
|
}
|
|
346
346
|
/** 系统通知:发给所有 socket(复用 notice 消息,前端 toast 展示)。 */
|
|
347
|
-
notifyAll(level, text) {
|
|
348
|
-
this.deliverAll({ type: "notice", level, text });
|
|
347
|
+
notifyAll(level, text, textEn) {
|
|
348
|
+
this.deliverAll({ type: "notice", level, text, textEn });
|
|
349
349
|
}
|
|
350
350
|
/** 给指定客户端定向发一条插件消息;找不到该 socket 时静默忽略。 */
|
|
351
351
|
sendTo(clientId, pluginId, payload) {
|
|
@@ -708,7 +708,7 @@ export class PluginManager {
|
|
|
708
708
|
const self = this; // 对象字面量 getter 里不能用插件宿主的 this (oxlint no-this-alias: 誤報, getter closure 需要 host)
|
|
709
709
|
const host = {
|
|
710
710
|
broadcast: (payload) => this.broadcast(info.id, payload),
|
|
711
|
-
notify: (level, text) => this.notifyAll(level, text),
|
|
711
|
+
notify: (level, text, textEn) => this.notifyAll(level, text, textEn),
|
|
712
712
|
sendTo: (clientId, payload) => this.sendTo(clientId, info.id, payload),
|
|
713
713
|
onMessage: (h) => {
|
|
714
714
|
handlers.add(h);
|
|
@@ -174,6 +174,9 @@ export class SlashCommandsService {
|
|
|
174
174
|
text: current
|
|
175
175
|
? `当前模型:${current.name}(${current.provider}/${current.id})。用法:/model <名称>`
|
|
176
176
|
: `用法:/model <名称>`,
|
|
177
|
+
textEn: current
|
|
178
|
+
? `Current model: ${current.name} (${current.provider}/${current.id}). Usage: /model <name>`
|
|
179
|
+
: `Usage: /model <name>`,
|
|
177
180
|
});
|
|
178
181
|
return true;
|
|
179
182
|
}
|
package/dist/server/terminals.js
CHANGED
|
@@ -47,8 +47,8 @@ export function resolveCommandCwd(cwd, pwd) {
|
|
|
47
47
|
/** Read the command list; missing file → empty list; malformed → empty list + warning text. */
|
|
48
48
|
export async function loadCommands(workspaceRoot) {
|
|
49
49
|
const path = commandsFilePath(workspaceRoot);
|
|
50
|
-
const { commands, warning } = await readCommandsFile(path);
|
|
51
|
-
return { commands, path, warning };
|
|
50
|
+
const { commands, warning, warningEn } = await readCommandsFile(path);
|
|
51
|
+
return { commands, path, warning, warningEn };
|
|
52
52
|
}
|
|
53
53
|
async function readCommandsFile(path) {
|
|
54
54
|
if (!existsSync(path))
|
|
@@ -61,6 +61,7 @@ async function readCommandsFile(path) {
|
|
|
61
61
|
return {
|
|
62
62
|
commands: [],
|
|
63
63
|
warning: `读取命令文件失败:${err.message}`,
|
|
64
|
+
warningEn: `Failed to read commands file: ${err.message}`,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
let parsed;
|
|
@@ -68,7 +69,11 @@ async function readCommandsFile(path) {
|
|
|
68
69
|
parsed = JSON.parse(raw);
|
|
69
70
|
}
|
|
70
71
|
catch {
|
|
71
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
commands: [],
|
|
74
|
+
warning: `命令文件不是有效 JSON:${path}`,
|
|
75
|
+
warningEn: `Commands file is not valid JSON: ${path}`,
|
|
76
|
+
};
|
|
72
77
|
}
|
|
73
78
|
if (Array.isArray(parsed)) {
|
|
74
79
|
// Tolerate a bare array: [{name, command, cwd}]
|
|
@@ -92,7 +97,7 @@ async function readCommandsFile(path) {
|
|
|
92
97
|
.map((c) => ({ name: c.name, command: c.command, cwd: c.cwd })),
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
|
-
return { commands: [], warning: `命令文件格式不正确:${path}` };
|
|
100
|
+
return { commands: [], warning: `命令文件格式不正确:${path}`, warningEn: `Commands file has a bad format: ${path}` };
|
|
96
101
|
}
|
|
97
102
|
/** Persist the command list, creating .pi/ if needed. */
|
|
98
103
|
export async function saveCommandsFile(workspaceRoot, commands) {
|
|
@@ -104,7 +109,11 @@ export async function saveCommandsFile(workspaceRoot, commands) {
|
|
|
104
109
|
return { path };
|
|
105
110
|
}
|
|
106
111
|
catch (err) {
|
|
107
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
path,
|
|
114
|
+
error: `保存命令文件失败:${err.message}`,
|
|
115
|
+
errorEn: `Failed to save commands file: ${err.message}`,
|
|
116
|
+
};
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
const isWindows = process.platform === "win32";
|
|
@@ -278,6 +287,7 @@ export function queryTerminalOutput(output, q, running = false, exitCode = null)
|
|
|
278
287
|
.filter((l) => {
|
|
279
288
|
const t = l.trim();
|
|
280
289
|
return (!/^\[pi-exit:\d+\]$/.test(t) &&
|
|
290
|
+
!/^\[pi-term-exit:-?\d+\]$/.test(t) &&
|
|
281
291
|
!t.startsWith("[进程已退出") &&
|
|
282
292
|
!t.startsWith("[Process exited") &&
|
|
283
293
|
!/^\[.*(已退出|exited)/.test(t));
|
|
@@ -1173,13 +1183,13 @@ export class TerminalManager {
|
|
|
1173
1183
|
if (!entry || entry.exited)
|
|
1174
1184
|
return;
|
|
1175
1185
|
this.disarmIdleWatch(entry);
|
|
1176
|
-
// Flush queued output BEFORE the exit
|
|
1186
|
+
// Flush queued output BEFORE the exit marker so ordering is preserved.
|
|
1187
|
+
// Banner text is CLIENT-rendered (live locale); the stream carries only
|
|
1188
|
+
// a machine sentinel the client strips and replaces.
|
|
1177
1189
|
this.flushPending(entry);
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
this.appendOutput(entry, banner);
|
|
1182
|
-
this.emit({ type: "terminal_output", terminalId: id, data: banner });
|
|
1190
|
+
const marker = `\r\n[pi-term-exit:${exitCode}]\r\n`;
|
|
1191
|
+
this.appendOutput(entry, marker);
|
|
1192
|
+
this.emit({ type: "terminal_output", terminalId: id, data: marker });
|
|
1183
1193
|
entry.exited = true;
|
|
1184
1194
|
// 终端退出 → 未命中的输出观察器以 null 回调(宿主可据此通知「终端已关闭」)。
|
|
1185
1195
|
const pendingWatches = entry.watches;
|
|
@@ -1532,10 +1542,10 @@ export const TERMINAL_TOOL_NAMES = [
|
|
|
1532
1542
|
/** System-prompt guidance teaching the model WHEN to prefer the terminal tools
|
|
1533
1543
|
* over one-shot bash. Without it models almost never pick them — bash returns
|
|
1534
1544
|
* complete output in a single call, so it always wins on convenience. */
|
|
1535
|
-
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands - it runs in a visible terminal and returns the full output (persist=false, one-shot terminal that exits when the command finishes). Switch to the bash tool's persist=true, or to the terminal tools, when:
|
|
1536
|
-
- The program is interactive or TUI-based (REPLs like python/node, vim/htop, installers asking y/n, anything waiting on stdin). For these, prefer bash({ persist: true }) which runs it in the persistent 'ai-bash' terminal and returns immediately; then drive it with terminal_input / terminal_key (and terminal_read) on terminalId='ai-bash'.
|
|
1537
|
-
- You start a long-running server or watcher and want to keep watching its output (terminal_read with waitMs), send keys to it later (e.g. interrupt via terminal_key with Ctrl+c), or block until a backgrounded command finishes without polling (terminal_wait).
|
|
1538
|
-
- The user explicitly asks you to work in the visible terminal panel.
|
|
1545
|
+
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands - it runs in a visible terminal and returns the full output (persist=false, one-shot terminal that exits when the command finishes). Switch to the bash tool's persist=true, or to the terminal tools, when:
|
|
1546
|
+
- The program is interactive or TUI-based (REPLs like python/node, vim/htop, installers asking y/n, anything waiting on stdin). For these, prefer bash({ persist: true }) which runs it in the persistent 'ai-bash' terminal and returns immediately; then drive it with terminal_input / terminal_key (and terminal_read) on terminalId='ai-bash'.
|
|
1547
|
+
- You start a long-running server or watcher and want to keep watching its output (terminal_read with waitMs), send keys to it later (e.g. interrupt via terminal_key with Ctrl+c), or block until a backgrounded command finishes without polling (terminal_wait).
|
|
1548
|
+
- The user explicitly asks you to work in the visible terminal panel.
|
|
1539
1549
|
Use head/tail on bash to trim verbose output instead of piping through head/tail. 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.`;
|
|
1540
1550
|
/** Build the agent-facing persistent terminal tools for one conversation. */
|
|
1541
1551
|
export function makePersistentTerminalTools(terminals, cwd) {
|
|
@@ -35,15 +35,15 @@ export function findVisionModels(runtime) {
|
|
|
35
35
|
* Exported so the settings panel can offer a custom prompt (append to this
|
|
36
36
|
* default or replace it entirely).
|
|
37
37
|
*/
|
|
38
|
-
export const SYSTEM_PROMPT = `You are a vision bridge for a text-only language model. You receive one or more images and must transcribe them into precise, structured text evidence so another model that cannot see images can answer questions about them accurately.
|
|
39
|
-
|
|
40
|
-
Follow these rules:
|
|
41
|
-
1. Transcribe ALL visible text verbatim, preserving wording, spelling, punctuation and line breaks. This is the most important part — the reader relies on your transcription, not on the image.
|
|
42
|
-
2. Describe the layout in reading order: headers, paragraphs, lists, tables, buttons, panels — say what appears where.
|
|
43
|
-
3. For tables/charts/diagrams: read axes, scales (note log scale), legend entries, series names, highlighted points and their coordinates, and any data values you can discern.
|
|
44
|
-
4. Name entities: people, products, companies, colors, style, objects, actions.
|
|
45
|
-
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
|
-
6. If there are multiple images, address them in order (图 1 / Image 1, 图 2 / Image 2, ...).
|
|
38
|
+
export const SYSTEM_PROMPT = `You are a vision bridge for a text-only language model. You receive one or more images and must transcribe them into precise, structured text evidence so another model that cannot see images can answer questions about them accurately.
|
|
39
|
+
|
|
40
|
+
Follow these rules:
|
|
41
|
+
1. Transcribe ALL visible text verbatim, preserving wording, spelling, punctuation and line breaks. This is the most important part — the reader relies on your transcription, not on the image.
|
|
42
|
+
2. Describe the layout in reading order: headers, paragraphs, lists, tables, buttons, panels — say what appears where.
|
|
43
|
+
3. For tables/charts/diagrams: read axes, scales (note log scale), legend entries, series names, highlighted points and their coordinates, and any data values you can discern.
|
|
44
|
+
4. Name entities: people, products, companies, colors, style, objects, actions.
|
|
45
|
+
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
|
+
6. If there are multiple images, address them in order (图 1 / Image 1, 图 2 / Image 2, ...).
|
|
47
47
|
7. Output only the transcript. No preamble, no commentary about the image itself.`;
|
|
48
48
|
/**
|
|
49
49
|
* Assemble the final vision-model system prompt from the settings-panel prefs.
|
|
@@ -115,8 +115,8 @@ export class WebUIContext {
|
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
// -- notifications --------------------------------------------------------
|
|
118
|
-
notify = (message, type) => {
|
|
119
|
-
this.emit({ type: "notice", level: type ?? "info", text: message });
|
|
118
|
+
notify = (message, type, messageEn) => {
|
|
119
|
+
this.emit({ type: "notice", level: type ?? "info", text: message, textEn: messageEn });
|
|
120
120
|
};
|
|
121
121
|
// -- footer status (pi-lens "LSP Inactive", pi-cache-optimizer cache stats) --
|
|
122
122
|
statuses = new Map();
|