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
|
@@ -28,12 +28,13 @@ import { MarkerService } from "./marker-service.js";
|
|
|
28
28
|
import { SlashCommandsService, parseSlash } from "./slash-commands.js";
|
|
29
29
|
import { ModelAdminService } from "./model-admin.js";
|
|
30
30
|
import { FilesService, MACHINE_ROOT, workspacePath } from "./files-service.js";
|
|
31
|
-
import { isExtensionDisabled, isExtensionEnabled, ClientStateStore } from "./client-state.js";
|
|
32
|
-
import {
|
|
31
|
+
import { isExtensionDisabled, isExtensionEnabled, normalizeRetryMaxAttempts, ClientStateStore, } from "./client-state.js";
|
|
32
|
+
import { pick, resolveServerLang } from "./i18n.js";
|
|
33
|
+
import { SubagentTemplatesStore, pickTemplatePrompt } from "./subagent-templates.js";
|
|
33
34
|
import { applyHeadTail, makePersistentTerminalTools, makeTerminalBashTool, stripAnsi, TERMINAL_TOOLS_GUIDANCE, TERMINAL_TOOL_NAMES, } from "./terminals.js";
|
|
34
35
|
import { WebUIContext, mockThemeProxy } from "./webui-context.js";
|
|
35
36
|
import { makeEditSoftTool, SOFT_EDIT_TOOL_NAME } from "./edit-soft-tool.js";
|
|
36
|
-
import { makeSubagentTools, subagentTitle, } from "./subagents.js";
|
|
37
|
+
import { makeSubagentTools, subagentTitle, withSubagentOwner, } from "./subagents.js";
|
|
37
38
|
import { buildAttachmentMessages } from "./attachments.js";
|
|
38
39
|
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, buildToolsSchemaText, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
39
40
|
import { serializeMessage, serializeStreamingMessage, stripTransientRetryErrors, } from "./serialize.js";
|
|
@@ -196,11 +197,13 @@ function makeMarkersListTool(getActiveId, markerSvc) {
|
|
|
196
197
|
return {
|
|
197
198
|
name: "markers_list",
|
|
198
199
|
label: "List marker state",
|
|
199
|
-
description: "只读查询内联标记状态。状态【写】操作请一律用内联标记([[todo:new:...]] 等)写在回答正文里,不要调用本工具做写操作。",
|
|
200
|
+
description: "Read-only query of inline marker state. All WRITE operations must use inline markers ([[todo:new:...]] etc.) in the reply body — never use this tool for writes.\n只读查询内联标记状态。状态【写】操作请一律用内联标记([[todo:new:...]] 等)写在回答正文里,不要调用本工具做写操作。",
|
|
200
201
|
parameters: Type.Object({
|
|
201
202
|
action: Type.Unsafe({ enum: ["list"] }),
|
|
202
203
|
tool: Type.Optional(Type.Literal("todo")),
|
|
203
|
-
includeDeleted: Type.Optional(Type.Boolean({
|
|
204
|
+
includeDeleted: Type.Optional(Type.Boolean({
|
|
205
|
+
description: "Whether to include deleted tasks (tombstones, todo only).\n是否包含已删除任务(tombstone,仅 todo)。",
|
|
206
|
+
})),
|
|
204
207
|
}),
|
|
205
208
|
execute: async (_id, params) => {
|
|
206
209
|
const p = params;
|
|
@@ -260,7 +263,7 @@ export function makeAskUserQuestionTool(clientSession) {
|
|
|
260
263
|
aborted: signal?.aborted,
|
|
261
264
|
});
|
|
262
265
|
if (answers === null) {
|
|
263
|
-
throw new Error("
|
|
266
|
+
throw new Error("User cancelled the question.\n用户取消了提问。");
|
|
264
267
|
}
|
|
265
268
|
// 工具结果:把每道题的回答拼成简洁文本给模型,同时留 details 供 UI 展示。
|
|
266
269
|
const lines = answers.map((a) => {
|
|
@@ -575,6 +578,8 @@ export class ClientSession {
|
|
|
575
578
|
isDisposed: () => this.disposed,
|
|
576
579
|
getCwd: () => this.cwd,
|
|
577
580
|
getActiveCwd: () => this.conv?.cwd ?? this.cwd,
|
|
581
|
+
// issue #91:文件服务错误文案按客户端 UI 语言出中英(英文默认)。
|
|
582
|
+
getLang: () => this.getLang(),
|
|
578
583
|
});
|
|
579
584
|
bg = new BgServerTracker({
|
|
580
585
|
emit: (msg) => this.emit(msg),
|
|
@@ -629,7 +634,9 @@ export class ClientSession {
|
|
|
629
634
|
return (conversationId ? this.convs.get(conversationId) : this.conv)?.cwd ?? this.cwd;
|
|
630
635
|
}
|
|
631
636
|
makeTerminalManager(conversationId, cwd) {
|
|
632
|
-
const mgr = new TerminalManager((msg) => this.emitTerminal(conversationId, msg), cwd
|
|
637
|
+
const mgr = new TerminalManager((msg) => this.emitTerminal(conversationId, msg), cwd,
|
|
638
|
+
// issue #91:终端输入错误按客户端 UI 语言出中英(英文默认)。
|
|
639
|
+
() => this.getLang());
|
|
633
640
|
// 终端活力检测:AI 触碰过的终端静默 ≥ 阈值(PI_WEB_TERMINAL_IDLE_MS,
|
|
634
641
|
// 默认 15s)且该对话正在运行时,注入一条 steer 消息唤醒 AI 去检查。
|
|
635
642
|
mgr.onAgentIdle = (terminalId, idleMs, title, lastLines) => this.notifyTerminalIdle(conversationId, terminalId, idleMs, title, lastLines);
|
|
@@ -699,18 +706,20 @@ export class ClientSession {
|
|
|
699
706
|
* `model`(可选):"provider/id",显式指定子代理模型。不传时由调用方决定是否
|
|
700
707
|
* 回退到模板模型 / 设置面板默认模型;null = 跟随主对话当前模型(默认行为,
|
|
701
708
|
* runtime 重建时会继承共享 ModelRuntime 的当前默认)。 */
|
|
702
|
-
async spawnSubagentConversation(prompt, type, cwd, apply, model) {
|
|
709
|
+
async spawnSubagentConversation(prompt, type, cwd, apply, model, parentId) {
|
|
703
710
|
const conversationId = `sa-${randomUUID().slice(0, 8)}`;
|
|
704
711
|
const terminals = this.makeTerminalManager(conversationId, cwd);
|
|
705
|
-
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, apply), {
|
|
712
|
+
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, apply, conversationId), {
|
|
706
713
|
cwd,
|
|
707
714
|
agentDir: this.agentDir,
|
|
708
715
|
sessionManager: SessionManager.inMemory(cwd),
|
|
709
716
|
});
|
|
710
717
|
const conv = this.makeConversation(runtime, conversationId, terminals);
|
|
711
718
|
conv.isSubagent = true;
|
|
712
|
-
//
|
|
713
|
-
|
|
719
|
+
// 父对话 = 真正派发它的会话(按会话归属的 host 包装填入)。直接用 active
|
|
720
|
+
// 会错:后台对话运行时用户可能正看着别的项目对话,孩子会被记到无关
|
|
721
|
+
// 对话名下、沉到别的项目组底部(issue #95)。缺省才回退到 active。
|
|
722
|
+
conv.parentId = parentId ?? this.activeId ?? undefined;
|
|
714
723
|
conv.subagentType = type;
|
|
715
724
|
conv.listed = true;
|
|
716
725
|
conv.title = subagentTitle(prompt);
|
|
@@ -903,6 +912,8 @@ export class ClientSession {
|
|
|
903
912
|
windowsPersona: process.platform === "win32" ? WINDOWS_PERSONA : "",
|
|
904
913
|
terminalGuidance: this.settingsSvc.current.terminalToolsEnabled !== false ? TERMINAL_TOOLS_GUIDANCE : "",
|
|
905
914
|
markersGuidance: this.markerSvc.buildGuidance(),
|
|
915
|
+
// issue #91:组合模板各来源段按客户端 UI 语言渲染(英文默认)。
|
|
916
|
+
lang: this.getLang(),
|
|
906
917
|
contextFiles: src.contextFiles,
|
|
907
918
|
skills: src.skills,
|
|
908
919
|
};
|
|
@@ -983,14 +994,14 @@ export class ClientSession {
|
|
|
983
994
|
* 切换查看 / 输入补充(steer)/ 中止(abort)/ 移出全部复用现有对话机制。
|
|
984
995
|
*/
|
|
985
996
|
subagentHost = {
|
|
986
|
-
spawnSubagent: (prompt, type, cwd, templateName, model) => {
|
|
997
|
+
spawnSubagent: (prompt, type, cwd, templateName, model, parentId) => {
|
|
987
998
|
// 模板:存在且启用时应用;传了名字但不可用 → 抛错让工具转给 AI。
|
|
988
999
|
const tpl = templateName ? this.subagentTemplates.get(templateName) : undefined;
|
|
989
1000
|
if (templateName && (!tpl || !tpl.enabled)) {
|
|
990
|
-
throw new Error(`子代理模板不可用:${templateName}
|
|
1001
|
+
throw new Error(pick(this.getLang(), `子代理模板不可用:${templateName}(不存在或已停用)`, `Subagent template unavailable: ${templateName} (missing or disabled)`, "agent.subagent.template.unavailable", { templateName: templateName }));
|
|
991
1002
|
}
|
|
992
1003
|
// 模型优先级:显式 model 参数 > 模板自带模型 > 设置面板默认模型;都不给 = 跟随主对话。
|
|
993
|
-
return this.spawnSubagentConversation(prompt, type, cwd, tpl, model);
|
|
1004
|
+
return this.spawnSubagentConversation(prompt, type, cwd, tpl, model, parentId);
|
|
994
1005
|
},
|
|
995
1006
|
getSubagent: (convId) => this.getSubagentSnapshot(convId),
|
|
996
1007
|
listSubagents: () => this.listSubagentSnapshots(),
|
|
@@ -1003,14 +1014,16 @@ export class ClientSession {
|
|
|
1003
1014
|
stopSubagent: async (convId) => {
|
|
1004
1015
|
const conv = this.convs.get(convId);
|
|
1005
1016
|
if (conv && (conv.session.isStreaming || !conv.session.isIdle)) {
|
|
1006
|
-
await this.interruptRun(conv, "用户停止子代理");
|
|
1017
|
+
await this.interruptRun(conv, pick(this.getLang(), "用户停止子代理", "User stopped the subagent", "agent.subagent.stop.user"));
|
|
1007
1018
|
}
|
|
1008
1019
|
},
|
|
1020
|
+
// issue #91:子代理工具返回按客户端 UI 语言出中英(英文默认)。
|
|
1021
|
+
lang: () => this.getLang(),
|
|
1009
1022
|
// 只向 AI 暴露 enabled 的模板(停用的对 AI 不可见)。
|
|
1010
1023
|
listTemplates: () => this.subagentTemplates
|
|
1011
1024
|
.list()
|
|
1012
1025
|
.filter((t) => t.enabled)
|
|
1013
|
-
.map((t) => ({ name: t.name, description: t.description, model: t.model })),
|
|
1026
|
+
.map((t) => ({ name: t.name, description: t.description, descriptionEn: t.descriptionEn, model: t.model })),
|
|
1014
1027
|
isTemplateUsable: (name) => {
|
|
1015
1028
|
const t = this.subagentTemplates.get(name);
|
|
1016
1029
|
return !!t && t.enabled;
|
|
@@ -1097,7 +1110,15 @@ export class ClientSession {
|
|
|
1097
1110
|
// 复用现有重命名路径(内存标题 + 磁盘 session_info)
|
|
1098
1111
|
void this.renameConversation(convId, title);
|
|
1099
1112
|
},
|
|
1113
|
+
// 标记 widget 合并进扩展 widget 里,跟随当前活动会话渲染(切换会话即刷新)。
|
|
1114
|
+
refreshMarkers: () => this.webUi.refresh(),
|
|
1115
|
+
// issue #91:标记引导/错误按客户端 UI 语言出中英(英文默认)。
|
|
1116
|
+
lang: () => this.getLang(),
|
|
1100
1117
|
});
|
|
1118
|
+
// 标记 widget 动态渲染「当前活动会话」的 todo/overlay:切换会话时只要刷新
|
|
1119
|
+
// webUi(见 switchConversation/setCwd/newChat)就会显示对应会话的标记,
|
|
1120
|
+
// 且与扩展 widget 合并下发、不会互相覆盖。
|
|
1121
|
+
this.webUi.setDynamicWidget("markers", () => this.markerSvc.overlayLines(this.activeId));
|
|
1101
1122
|
this.settingsSvc = new SettingsService({
|
|
1102
1123
|
clientId,
|
|
1103
1124
|
stateStore,
|
|
@@ -1110,10 +1131,14 @@ export class ClientSession {
|
|
|
1110
1131
|
isStreaming: () => this.session.isStreaming,
|
|
1111
1132
|
reloadSession: async () => {
|
|
1112
1133
|
await this.session.reload();
|
|
1134
|
+
// reload() 重读磁盘 settings.json,会丢掉内存 applyOverrides
|
|
1135
|
+
// (含重试次数覆盖)——依次重放:重试覆盖 → 终端门控。
|
|
1136
|
+
this.applyRetryOverrides();
|
|
1113
1137
|
// reload() 会把 custom 工具重新加回活跃集——重放终端开关。
|
|
1114
1138
|
this.applyToolGating(this.session);
|
|
1115
1139
|
await this.pushSlashCommands();
|
|
1116
1140
|
},
|
|
1141
|
+
applyRetryOverrides: () => this.applyRetryOverrides(),
|
|
1117
1142
|
promptSnapshot: () => this.promptSnapshot(),
|
|
1118
1143
|
getMarkerState: () => ({
|
|
1119
1144
|
markersEnabled: this.markerSvc.current.markersEnabled,
|
|
@@ -1130,6 +1155,10 @@ export class ClientSession {
|
|
|
1130
1155
|
flushSnapshot: () => this.flushSnapshot(),
|
|
1131
1156
|
isDisposed: () => this.disposed,
|
|
1132
1157
|
quiesceBlocked: () => this.quiesceBlocked(),
|
|
1158
|
+
// issue #91:目标/审查文案按客户端 UI 语言出中英(英文默认)。
|
|
1159
|
+
lang: () => this.getLang(),
|
|
1160
|
+
// 目标模式总开关(设置面板「目标审查」页):关 → 目标入口一律拒绝。
|
|
1161
|
+
goalModeEnabled: () => this.settingsSvc.current.goalModeEnabled !== false,
|
|
1133
1162
|
activeConvId: () => this.activeId,
|
|
1134
1163
|
activeConv: () => this.conv,
|
|
1135
1164
|
getConv: (id) => this.convs.get(id),
|
|
@@ -1157,7 +1186,7 @@ export class ClientSession {
|
|
|
1157
1186
|
const cs = new ClientSession(clientId, cwd, agentDir, stateStore);
|
|
1158
1187
|
const conversationId = cs.nextConversationId();
|
|
1159
1188
|
const terminals = cs.makeTerminalManager(conversationId, cwd);
|
|
1160
|
-
const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(terminals), {
|
|
1189
|
+
const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
1161
1190
|
cwd,
|
|
1162
1191
|
agentDir,
|
|
1163
1192
|
// Resume the most recent session for this project — the SDK default
|
|
@@ -1195,7 +1224,7 @@ export class ClientSession {
|
|
|
1195
1224
|
* 应用(prompt replace/append + 白名单),其余(终端接管、Windows persona
|
|
1196
1225
|
* 等)仍跟随主会话设置。undefined = 按主会话设置(普通对话/不选模板的子代理)。
|
|
1197
1226
|
*/
|
|
1198
|
-
makeRuntimeFactory(terminals, apply) {
|
|
1227
|
+
makeRuntimeFactory(terminals, apply, ownerId) {
|
|
1199
1228
|
return async ({ cwd: effectiveCwd, sessionManager }) => {
|
|
1200
1229
|
const services = await createAgentSessionServices({
|
|
1201
1230
|
cwd: effectiveCwd,
|
|
@@ -1218,8 +1247,8 @@ export class ClientSession {
|
|
|
1218
1247
|
systemPromptOverride: (base) => {
|
|
1219
1248
|
if (typeof base === "string" && base) {
|
|
1220
1249
|
this.lastBaseSystemPrompt = base;
|
|
1221
|
-
if (apply && apply.promptMode === "replace" && apply.
|
|
1222
|
-
return apply.
|
|
1250
|
+
if (apply && apply.promptMode === "replace" && pickTemplatePrompt(apply, this.getLang()).trim()) {
|
|
1251
|
+
return pickTemplatePrompt(apply, this.getLang());
|
|
1223
1252
|
}
|
|
1224
1253
|
}
|
|
1225
1254
|
return undefined;
|
|
@@ -1229,8 +1258,8 @@ export class ClientSession {
|
|
|
1229
1258
|
if (!apply)
|
|
1230
1259
|
this.lastSdkAppendFiles = base.slice();
|
|
1231
1260
|
const out = [...base];
|
|
1232
|
-
if (apply && apply.promptMode === "append" && apply.
|
|
1233
|
-
out.push(apply.
|
|
1261
|
+
if (apply && apply.promptMode === "append" && pickTemplatePrompt(apply, this.getLang()).trim()) {
|
|
1262
|
+
out.push(pickTemplatePrompt(apply, this.getLang()));
|
|
1234
1263
|
}
|
|
1235
1264
|
// 主会话自定义「追加」已并入组合模板的 {{append}} 覆盖,不再在此注入。
|
|
1236
1265
|
if (process.platform === "win32") {
|
|
@@ -1294,12 +1323,12 @@ export class ClientSession {
|
|
|
1294
1323
|
// 把灵魂段换成模板提示词,自动段保留;SYSTEM.md 情形已在
|
|
1295
1324
|
// systemPromptOverride 整体替换,此处边界不存在会自然跳过。
|
|
1296
1325
|
if (apply) {
|
|
1297
|
-
if (apply.promptMode !== "replace" || !apply.
|
|
1326
|
+
if (apply.promptMode !== "replace" || !pickTemplatePrompt(apply, this.getLang()).trim())
|
|
1298
1327
|
return undefined;
|
|
1299
1328
|
const boundary = event.systemPrompt.indexOf("\n\nAvailable tools:");
|
|
1300
1329
|
if (boundary === -1)
|
|
1301
1330
|
return undefined;
|
|
1302
|
-
const swapped = apply.
|
|
1331
|
+
const swapped = pickTemplatePrompt(apply, this.getLang()).trimEnd() + event.systemPrompt.slice(boundary);
|
|
1303
1332
|
return swapped === event.systemPrompt ? undefined : { systemPrompt: swapped };
|
|
1304
1333
|
}
|
|
1305
1334
|
// 主会话:组合模板渲染(模板为空且无覆盖时返回 undefined = 用 SDK 默认)。
|
|
@@ -1337,18 +1366,26 @@ export class ClientSession {
|
|
|
1337
1366
|
idleMs: () => Math.max(0, Math.floor(this.settingsSvc.current.terminalBashIdleMs) || 0),
|
|
1338
1367
|
kills: this.bashKills,
|
|
1339
1368
|
notifyBackgroundDone: (info) => this.notifyTerminalBashDone(terminals, info),
|
|
1369
|
+
// issue #91:bash 返回按客户端 UI 语言出中英(英文默认)。
|
|
1370
|
+
lang: () => this.getLang(),
|
|
1340
1371
|
}),
|
|
1341
1372
|
// 设置关 → 原生 bash;开 → 终端 bash。
|
|
1342
1373
|
() => this.settingsSvc.current.terminalBash),
|
|
1343
|
-
...makePersistentTerminalTools(terminals, effectiveCwd),
|
|
1374
|
+
...makePersistentTerminalTools(terminals, effectiveCwd, () => this.getLang()),
|
|
1344
1375
|
// 不覆盖内置 edit 的独立宽松编辑工具(缩进不敏感匹配;开关看设置)。
|
|
1345
|
-
makeEditSoftTool(effectiveCwd),
|
|
1376
|
+
makeEditSoftTool(effectiveCwd, () => this.getLang()),
|
|
1346
1377
|
// 插件注册的 AI 工具(创建时刻的实时快照;后续注册经
|
|
1347
1378
|
// refreshPluginTools 动态补入已有会话)。
|
|
1348
1379
|
...(this.pluginToolsProvider?.() ?? []).map(pluginToolToDefinition),
|
|
1349
1380
|
// 第一方子代理工具(spawn/get_result/steer/list/stop)。子代理会话
|
|
1350
|
-
// 也注册了它们,因此可自然嵌套派发。
|
|
1351
|
-
|
|
1381
|
+
// 也注册了它们,因此可自然嵌套派发。host 按 ownerId 包装:子代理的
|
|
1382
|
+
// 父对话 = 真正调用 spawn 的那个会话(本 runtime 所属会话),而不是
|
|
1383
|
+
// 派发瞬间的 active——后台对话继续产出时用户可能已切到别的项目,用
|
|
1384
|
+
// activeId 会把孩子记到无关会话名下、沉到别的组/底部(issue #95)。
|
|
1385
|
+
// ownerId 即本 runtime 所属会话(创建时就已知,见各调用点)。
|
|
1386
|
+
...(ownerId
|
|
1387
|
+
? makeSubagentTools(withSubagentOwner(this.subagentHost, ownerId))
|
|
1388
|
+
: makeSubagentTools(this.subagentHost)),
|
|
1352
1389
|
// 内置标记只读查询工具(todo/svc 状态查询,写操作走内联标记)。
|
|
1353
1390
|
makeMarkersListTool(() => this.activeId, this.markerSvc),
|
|
1354
1391
|
// 标准引擎的 ask_user_question:模型调用 → 浏览器富渲染问卷(复用 DSH
|
|
@@ -1776,18 +1813,17 @@ export class ClientSession {
|
|
|
1776
1813
|
conv.queueSteering = [...event.steering];
|
|
1777
1814
|
conv.queueFollowUp = [...event.followUp];
|
|
1778
1815
|
break;
|
|
1779
|
-
// 手动 /compact
|
|
1780
|
-
//
|
|
1816
|
+
// 手动 /compact 或阈值/溢出自动压缩开始——常驻进度条(快照 compaction
|
|
1817
|
+
// 字段),而不是一次性 toast(toast 几秒就消失,而摘要生成可能持续
|
|
1818
|
+
// 数十秒,用户会以为「没反应」)。立即 flush 让进度条第一时间出现。
|
|
1781
1819
|
case "compaction_start": {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
text: "正在压缩上下文…(压缩摘要将显示在消息区)",
|
|
1786
|
-
textEn: "Compacting context… (the summary will appear in the message list)",
|
|
1787
|
-
});
|
|
1820
|
+
conv.compactionState = { reason: event.reason, startedAt: Date.now() };
|
|
1821
|
+
conv.lastCompactionTokens = null;
|
|
1822
|
+
this.flushSnapshot();
|
|
1788
1823
|
break;
|
|
1789
1824
|
}
|
|
1790
1825
|
case "compaction_end": {
|
|
1826
|
+
conv.compactionState = null;
|
|
1791
1827
|
if (event.errorMessage) {
|
|
1792
1828
|
this.emit({
|
|
1793
1829
|
type: "notice",
|
|
@@ -1807,6 +1843,8 @@ export class ClientSession {
|
|
|
1807
1843
|
else if (event.result) {
|
|
1808
1844
|
const { tokensBefore, estimatedTokensAfter } = event.result;
|
|
1809
1845
|
const after = estimatedTokensAfter ?? tokensBefore;
|
|
1846
|
+
// 记住压缩后大小:SDK 在下轮响应前报 null,快照用此回填底栏。
|
|
1847
|
+
conv.lastCompactionTokens = estimatedTokensAfter ?? null;
|
|
1810
1848
|
this.emit({
|
|
1811
1849
|
type: "notice",
|
|
1812
1850
|
level: "info",
|
|
@@ -2144,13 +2182,25 @@ export class ClientSession {
|
|
|
2144
2182
|
totalMessages: s.totalMessages,
|
|
2145
2183
|
tokens: s.tokens,
|
|
2146
2184
|
cost: s.cost,
|
|
2147
|
-
contextUsage:
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2185
|
+
contextUsage: (() => {
|
|
2186
|
+
const cu = s.contextUsage;
|
|
2187
|
+
if (!cu)
|
|
2188
|
+
return stats.contextUsage;
|
|
2189
|
+
// 压缩刚结束、下轮响应未到:SDK 报 null,用压缩结果回填约数。
|
|
2190
|
+
if (cu.tokens == null && conv.lastCompactionTokens != null && cu.contextWindow > 0) {
|
|
2191
|
+
return {
|
|
2192
|
+
tokens: conv.lastCompactionTokens,
|
|
2193
|
+
contextWindow: cu.contextWindow,
|
|
2194
|
+
percent: (conv.lastCompactionTokens / cu.contextWindow) * 100,
|
|
2195
|
+
estimated: true,
|
|
2196
|
+
};
|
|
2152
2197
|
}
|
|
2153
|
-
|
|
2198
|
+
return {
|
|
2199
|
+
tokens: cu.tokens,
|
|
2200
|
+
contextWindow: cu.contextWindow,
|
|
2201
|
+
percent: cu.percent,
|
|
2202
|
+
};
|
|
2203
|
+
})(),
|
|
2154
2204
|
};
|
|
2155
2205
|
}
|
|
2156
2206
|
catch {
|
|
@@ -2187,6 +2237,7 @@ export class ClientSession {
|
|
|
2187
2237
|
queue: { steering: conv.queueSteering, followUp: conv.queueFollowUp },
|
|
2188
2238
|
errorMessage: state.errorMessage,
|
|
2189
2239
|
retry: conv.retryState ?? null,
|
|
2240
|
+
compaction: conv.compactionState ?? null,
|
|
2190
2241
|
tools: state.tools.map((t) => t.name),
|
|
2191
2242
|
version: ++this.version,
|
|
2192
2243
|
piConfigured: this.isPiConfigured(),
|
|
@@ -2466,7 +2517,7 @@ export class ClientSession {
|
|
|
2466
2517
|
}
|
|
2467
2518
|
try {
|
|
2468
2519
|
const targets = collectTargets(this.agentDir, ClientSession.currentAppVersion());
|
|
2469
|
-
const items = await checkAllUpdates(targets);
|
|
2520
|
+
const items = await checkAllUpdates(targets, undefined, () => this.getLang());
|
|
2470
2521
|
this.updatesAllCache = { at: Date.now(), items };
|
|
2471
2522
|
this.emit({ type: "update_status_all", items });
|
|
2472
2523
|
}
|
|
@@ -2581,7 +2632,11 @@ export class ClientSession {
|
|
|
2581
2632
|
this.flushSnapshot();
|
|
2582
2633
|
},
|
|
2583
2634
|
refreshSessions: () => this.refreshSessions(),
|
|
2584
|
-
afterReload: () =>
|
|
2635
|
+
afterReload: () => {
|
|
2636
|
+
// /reload 同样重读磁盘 settings.json——重放重试覆盖 + 终端门控。
|
|
2637
|
+
this.applyRetryOverrides();
|
|
2638
|
+
this.applyToolGating(this.session);
|
|
2639
|
+
},
|
|
2585
2640
|
pluginCommands: () => this.pluginCommandsProvider?.() ?? [],
|
|
2586
2641
|
execPluginCommand: async (name, args) => {
|
|
2587
2642
|
const def = this.pluginCommandsProvider?.().find((c) => c.name === name);
|
|
@@ -2626,10 +2681,10 @@ export class ClientSession {
|
|
|
2626
2681
|
return this.modelAdmin.listModelsConfig();
|
|
2627
2682
|
}
|
|
2628
2683
|
fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
|
|
2629
|
-
return this.modelAdmin.fetchModelsList(reqId, baseUrl, apiKey, authHeader, api);
|
|
2684
|
+
return this.modelAdmin.fetchModelsList(reqId, baseUrl, apiKey, authHeader, api, () => this.getLang());
|
|
2630
2685
|
}
|
|
2631
2686
|
refreshProviderModels(providerId, reqId) {
|
|
2632
|
-
return this.modelAdmin.refreshProviderModels(providerId, reqId);
|
|
2687
|
+
return this.modelAdmin.refreshProviderModels(providerId, reqId, () => this.getLang());
|
|
2633
2688
|
}
|
|
2634
2689
|
/** Copy a built-in provider into an editable custom-provider draft
|
|
2635
2690
|
* (clone_provider_result) — lets the user run a second API key without
|
|
@@ -2766,6 +2821,22 @@ export class ClientSession {
|
|
|
2766
2821
|
pushSettings() {
|
|
2767
2822
|
this.settingsSvc.push();
|
|
2768
2823
|
}
|
|
2824
|
+
/** 把设置面板的出错重试次数注入全部存活会话的 SDK SettingsManager。
|
|
2825
|
+
* applyOverrides 只改内存合并视图(不碰 ~/.pi/agent/settings.json),
|
|
2826
|
+
* 且 SDK 每次退避前都重读 getRetrySettings()——即时生效、无需 reload。
|
|
2827
|
+
* 但 session.reload() 会重读磁盘丢掉覆盖,每次 reload 后必须重放
|
|
2828
|
+
* (reloadSession / afterReload / 标记开关直载路径均已接)。 */
|
|
2829
|
+
applyRetryOverrides() {
|
|
2830
|
+
const n = normalizeRetryMaxAttempts(this.settingsSvc.current.retryMaxAttempts);
|
|
2831
|
+
for (const c of this.convs.values()) {
|
|
2832
|
+
try {
|
|
2833
|
+
c.session.settingsManager.applyOverrides({ retry: { maxRetries: n } });
|
|
2834
|
+
}
|
|
2835
|
+
catch {
|
|
2836
|
+
// 会话未就绪或已释放 → 其 runtime 创建时统一注入。
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2769
2840
|
/** Extensions/skills changed externally (e.g. `pi remove` finished in the
|
|
2770
2841
|
* terminal): re-run session.reload() and re-push state. Streaming-safe —
|
|
2771
2842
|
* deferred to agent_end, same as settings reloads. */
|
|
@@ -2774,7 +2845,10 @@ export class ClientSession {
|
|
|
2774
2845
|
}
|
|
2775
2846
|
/** Persist + apply a partial settings update (prompt text/mode, toggles). */
|
|
2776
2847
|
async setSettings(partial) {
|
|
2777
|
-
const { markersEnabled, disabledMarkers, ...rest } = partial;
|
|
2848
|
+
const { markersEnabled, disabledMarkers, quickPhrasesSeeded, ...rest } = partial;
|
|
2849
|
+
// 快捷短语「已 seed」是全局标记(非 per-clientId):置位一次后永久生效。
|
|
2850
|
+
if (quickPhrasesSeeded)
|
|
2851
|
+
this.stateStore.markQuickPhrasesSeeded();
|
|
2778
2852
|
let markerChanged = false;
|
|
2779
2853
|
if (markersEnabled !== undefined || disabledMarkers !== undefined) {
|
|
2780
2854
|
this.markerSvc.setAll({
|
|
@@ -2792,6 +2866,7 @@ export class ClientSession {
|
|
|
2792
2866
|
if (!this.session.isStreaming) {
|
|
2793
2867
|
try {
|
|
2794
2868
|
await this.session.reload();
|
|
2869
|
+
this.applyRetryOverrides();
|
|
2795
2870
|
this.applyToolGating(this.session);
|
|
2796
2871
|
await this.pushSlashCommands();
|
|
2797
2872
|
this.pushSettings();
|
|
@@ -2870,6 +2945,26 @@ export class ClientSession {
|
|
|
2870
2945
|
// 兼容旧入口:reload + 刷目录在宿主回调里完成
|
|
2871
2946
|
return this.settingsSvc.applyRuntime();
|
|
2872
2947
|
}
|
|
2948
|
+
/** Server language for this client (issue #91): resolved LIVE from the
|
|
2949
|
+
* persisted UI locale — "zh" only for zh*; everything else (including
|
|
2950
|
+
* never-reported) is English. Per-call tool return values read this on
|
|
2951
|
+
* every invocation, so they follow language switches with no rebuild. */
|
|
2952
|
+
getLang() {
|
|
2953
|
+
return resolveServerLang(this.stateStore.get(this.clientId).locale);
|
|
2954
|
+
}
|
|
2955
|
+
/** Persist the browser UI locale (hello.locale / set_locale) and refresh
|
|
2956
|
+
* lang-aware prompt segments. Reuses the settings reload path, so it is
|
|
2957
|
+
* streaming-safe (deferred to agent_end mid-run, same as settings). */
|
|
2958
|
+
async setLocale(locale) {
|
|
2959
|
+
const code = locale.trim().slice(0, 16);
|
|
2960
|
+
if (!code)
|
|
2961
|
+
return;
|
|
2962
|
+
const prev = this.getLang();
|
|
2963
|
+
this.stateStore.saveLocale(this.clientId, code);
|
|
2964
|
+
if (this.getLang() === prev)
|
|
2965
|
+
return; // same server language — nothing to re-render
|
|
2966
|
+
await this.applySettingsReload();
|
|
2967
|
+
}
|
|
2873
2968
|
// ---------------------------------------------------------------------------
|
|
2874
2969
|
// Commands
|
|
2875
2970
|
// ---------------------------------------------------------------------------
|
|
@@ -2963,6 +3058,8 @@ export class ClientSession {
|
|
|
2963
3058
|
emit: (msg) => this.emit(msg),
|
|
2964
3059
|
settings: this.settingsSvc.current,
|
|
2965
3060
|
session: this.session,
|
|
3061
|
+
// issue #91:附件/视觉桥文案按客户端 UI 语言出中英(英文默认)。
|
|
3062
|
+
getLang: () => this.getLang(),
|
|
2966
3063
|
}, attachments);
|
|
2967
3064
|
for (const aside of asides) {
|
|
2968
3065
|
await s.sendCustomMessage(aside.message, { deliverAs: "nextTurn" });
|
|
@@ -3224,7 +3321,7 @@ export class ClientSession {
|
|
|
3224
3321
|
this.clearAllToolWatchdogs(conv);
|
|
3225
3322
|
conv.toolStartTimes.clear();
|
|
3226
3323
|
await conv.runtime.dispose();
|
|
3227
|
-
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(conv.terminals), {
|
|
3324
|
+
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(conv.terminals, undefined, conv.id), {
|
|
3228
3325
|
cwd: conv.cwd,
|
|
3229
3326
|
agentDir: this.agentDir,
|
|
3230
3327
|
sessionManager: SessionManager.continueRecent(conv.cwd),
|
|
@@ -3303,7 +3400,7 @@ export class ClientSession {
|
|
|
3303
3400
|
try {
|
|
3304
3401
|
const conversationId = this.nextConversationId();
|
|
3305
3402
|
const terminals = this.makeTerminalManager(conversationId, this.cwd);
|
|
3306
|
-
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals), {
|
|
3403
|
+
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
3307
3404
|
cwd: this.cwd,
|
|
3308
3405
|
agentDir: this.agentDir,
|
|
3309
3406
|
sessionManager: SessionManager.create(this.cwd),
|
|
@@ -3387,7 +3484,7 @@ export class ClientSession {
|
|
|
3387
3484
|
reviewing: conv.goal.reviewing,
|
|
3388
3485
|
wizardRunning: conv.wizardRunning,
|
|
3389
3486
|
streaming: conv.session.isStreaming,
|
|
3390
|
-
openTerminals: conv.terminals.
|
|
3487
|
+
openTerminals: conv.terminals.countLive(),
|
|
3391
3488
|
listed: conv.listed,
|
|
3392
3489
|
promptedSinceActive: conv.promptedSinceActive,
|
|
3393
3490
|
hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
|
|
@@ -3784,7 +3881,7 @@ export class ClientSession {
|
|
|
3784
3881
|
reviewing: conv.goal.reviewing,
|
|
3785
3882
|
wizardRunning: conv.wizardRunning,
|
|
3786
3883
|
streaming: conv.session.isStreaming,
|
|
3787
|
-
openTerminals: conv.terminals.
|
|
3884
|
+
openTerminals: conv.terminals.countLive(),
|
|
3788
3885
|
listed: conv.listed,
|
|
3789
3886
|
promptedSinceActive: conv.promptedSinceActive,
|
|
3790
3887
|
hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
|
|
@@ -3799,7 +3896,7 @@ export class ClientSession {
|
|
|
3799
3896
|
textEn: `Conversation "${conv.title}" is still running — wait for it to finish or press Stop before removing`,
|
|
3800
3897
|
});
|
|
3801
3898
|
}
|
|
3802
|
-
else if (conv.terminals.
|
|
3899
|
+
else if (conv.terminals.countLive() > 0) {
|
|
3803
3900
|
this.emit({
|
|
3804
3901
|
type: "notice",
|
|
3805
3902
|
level: "warning",
|
|
@@ -3857,7 +3954,7 @@ export class ClientSession {
|
|
|
3857
3954
|
const targetCwd = sessionManager.getCwd();
|
|
3858
3955
|
const conversationId = this.nextConversationId();
|
|
3859
3956
|
openedTerminals = this.makeTerminalManager(conversationId, targetCwd);
|
|
3860
|
-
openedRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(openedTerminals), {
|
|
3957
|
+
openedRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(openedTerminals, undefined, conversationId), {
|
|
3861
3958
|
cwd: targetCwd,
|
|
3862
3959
|
agentDir: this.agentDir,
|
|
3863
3960
|
sessionManager,
|
|
@@ -4209,7 +4306,7 @@ export class ClientSession {
|
|
|
4209
4306
|
// First visit to this project: resume its most recent session.
|
|
4210
4307
|
const conversationId = this.nextConversationId();
|
|
4211
4308
|
const terminals = this.makeTerminalManager(conversationId, abs);
|
|
4212
|
-
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals), {
|
|
4309
|
+
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
4213
4310
|
cwd: abs,
|
|
4214
4311
|
agentDir: this.agentDir,
|
|
4215
4312
|
sessionManager: SessionManager.continueRecent(abs),
|
|
@@ -4615,6 +4712,25 @@ export class AgentService {
|
|
|
4615
4712
|
for (const cs of this.clients.values())
|
|
4616
4713
|
cs.refreshPluginTools();
|
|
4617
4714
|
}
|
|
4715
|
+
/** Browser UI locale report (hello.locale / set_locale): persist per client
|
|
4716
|
+
* and refresh lang-aware prompts (streaming-safe via ClientSession). */
|
|
4717
|
+
async setLocale(clientId, locale) {
|
|
4718
|
+
const cs = this.clients.get(clientId);
|
|
4719
|
+
if (cs) {
|
|
4720
|
+
await cs.setLocale(locale);
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
// hello race: session still being created — wait for it, then apply.
|
|
4724
|
+
const inflight = this.pending.get(clientId);
|
|
4725
|
+
if (inflight) {
|
|
4726
|
+
try {
|
|
4727
|
+
await (await inflight).setLocale(locale);
|
|
4728
|
+
}
|
|
4729
|
+
catch {
|
|
4730
|
+
/* attach failed — nothing to apply to */
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4618
4734
|
/** 插件斜杠命令集合变化时由 index.ts 触发:重推各客户端的命令目录。 */
|
|
4619
4735
|
applyPluginCommandCatalog() {
|
|
4620
4736
|
for (const cs of this.clients.values())
|
|
@@ -188,12 +188,16 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
188
188
|
else {
|
|
189
189
|
// Batch hash so re-sending identical images (edit & re-ask) reuses
|
|
190
190
|
// the transcript instead of re-burning tokens on the vision API.
|
|
191
|
+
// issue #91:转写提示词按客户端 UI 语言选用(英文默认),语言进缓存键。
|
|
192
|
+
const vLang = ctx.getLang?.() ?? "en";
|
|
191
193
|
// The active transcription prompt is part of the key: changing
|
|
192
194
|
// the custom prompt must invalidate cached transcripts made with
|
|
193
195
|
// the old prompt.
|
|
194
196
|
const batchHash = bridgedImages.map((b) => `${b.att.name ?? "img"}:${b.raw.slice(0, 48)}`).join("|") +
|
|
195
197
|
"::" +
|
|
196
|
-
buildVisionBridgePrompt(ctx.settings.visionBridgePromptMode, ctx.settings.visionBridgePrompt)
|
|
198
|
+
buildVisionBridgePrompt(ctx.settings.visionBridgePromptMode, ctx.settings.visionBridgePrompt, vLang) +
|
|
199
|
+
"::" +
|
|
200
|
+
vLang;
|
|
197
201
|
let transcript = visionBridgeCache.get(batchHash);
|
|
198
202
|
if (transcript === undefined) {
|
|
199
203
|
ctx.emit({
|
|
@@ -210,7 +214,8 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
210
214
|
name: b.att.name,
|
|
211
215
|
})), {
|
|
212
216
|
model: chosenModel ?? undefined,
|
|
213
|
-
systemPrompt: buildVisionBridgePrompt(ctx.settings.visionBridgePromptMode, ctx.settings.visionBridgePrompt),
|
|
217
|
+
systemPrompt: buildVisionBridgePrompt(ctx.settings.visionBridgePromptMode, ctx.settings.visionBridgePrompt, vLang),
|
|
218
|
+
lang: vLang,
|
|
214
219
|
});
|
|
215
220
|
visionBridgeCache.set(batchHash, transcript);
|
|
216
221
|
ctx.emit({
|