pi-web-ui 0.68.2 → 0.69.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 +157 -50
- package/dist/server/attachments.js +7 -2
- package/dist/server/client-state.js +27 -0
- package/dist/server/dsh/dsh-agent-service.js +139 -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 +76 -18
- package/dist/server/locales.js +55 -1
- package/dist/server/managed.js +61 -0
- package/dist/server/marker-service.js +20 -7
- 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 +20 -1
- package/dist/server/subagent-templates.js +105 -0
- package/dist/server/subagents.js +155 -54
- package/dist/server/tabs.js +87 -0
- package/dist/server/terminals.js +88 -48
- package/dist/server/update-check.js +7 -2
- package/dist/server/vision-bridge.js +34 -12
- package/package.json +2 -1
- package/web/dist/assets/{TerminalPanel-BQ5NTB9Y.js → TerminalPanel-Cj8zsjx-.js} +1 -1
- package/web/dist/assets/index-DCOcsPFm.js +334 -0
- package/web/dist/assets/{index-C_I-6Zul.css → index-jH2Bb-0X.css} +1 -1
- 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-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,6 +1110,8 @@ export class ClientSession {
|
|
|
1097
1110
|
// 复用现有重命名路径(内存标题 + 磁盘 session_info)
|
|
1098
1111
|
void this.renameConversation(convId, title);
|
|
1099
1112
|
},
|
|
1113
|
+
// issue #91:标记引导/错误按客户端 UI 语言出中英(英文默认)。
|
|
1114
|
+
lang: () => this.getLang(),
|
|
1100
1115
|
});
|
|
1101
1116
|
this.settingsSvc = new SettingsService({
|
|
1102
1117
|
clientId,
|
|
@@ -1110,10 +1125,14 @@ export class ClientSession {
|
|
|
1110
1125
|
isStreaming: () => this.session.isStreaming,
|
|
1111
1126
|
reloadSession: async () => {
|
|
1112
1127
|
await this.session.reload();
|
|
1128
|
+
// reload() 重读磁盘 settings.json,会丢掉内存 applyOverrides
|
|
1129
|
+
// (含重试次数覆盖)——依次重放:重试覆盖 → 终端门控。
|
|
1130
|
+
this.applyRetryOverrides();
|
|
1113
1131
|
// reload() 会把 custom 工具重新加回活跃集——重放终端开关。
|
|
1114
1132
|
this.applyToolGating(this.session);
|
|
1115
1133
|
await this.pushSlashCommands();
|
|
1116
1134
|
},
|
|
1135
|
+
applyRetryOverrides: () => this.applyRetryOverrides(),
|
|
1117
1136
|
promptSnapshot: () => this.promptSnapshot(),
|
|
1118
1137
|
getMarkerState: () => ({
|
|
1119
1138
|
markersEnabled: this.markerSvc.current.markersEnabled,
|
|
@@ -1130,6 +1149,10 @@ export class ClientSession {
|
|
|
1130
1149
|
flushSnapshot: () => this.flushSnapshot(),
|
|
1131
1150
|
isDisposed: () => this.disposed,
|
|
1132
1151
|
quiesceBlocked: () => this.quiesceBlocked(),
|
|
1152
|
+
// issue #91:目标/审查文案按客户端 UI 语言出中英(英文默认)。
|
|
1153
|
+
lang: () => this.getLang(),
|
|
1154
|
+
// 目标模式总开关(设置面板「目标审查」页):关 → 目标入口一律拒绝。
|
|
1155
|
+
goalModeEnabled: () => this.settingsSvc.current.goalModeEnabled !== false,
|
|
1133
1156
|
activeConvId: () => this.activeId,
|
|
1134
1157
|
activeConv: () => this.conv,
|
|
1135
1158
|
getConv: (id) => this.convs.get(id),
|
|
@@ -1157,7 +1180,7 @@ export class ClientSession {
|
|
|
1157
1180
|
const cs = new ClientSession(clientId, cwd, agentDir, stateStore);
|
|
1158
1181
|
const conversationId = cs.nextConversationId();
|
|
1159
1182
|
const terminals = cs.makeTerminalManager(conversationId, cwd);
|
|
1160
|
-
const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(terminals), {
|
|
1183
|
+
const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
1161
1184
|
cwd,
|
|
1162
1185
|
agentDir,
|
|
1163
1186
|
// Resume the most recent session for this project — the SDK default
|
|
@@ -1195,7 +1218,7 @@ export class ClientSession {
|
|
|
1195
1218
|
* 应用(prompt replace/append + 白名单),其余(终端接管、Windows persona
|
|
1196
1219
|
* 等)仍跟随主会话设置。undefined = 按主会话设置(普通对话/不选模板的子代理)。
|
|
1197
1220
|
*/
|
|
1198
|
-
makeRuntimeFactory(terminals, apply) {
|
|
1221
|
+
makeRuntimeFactory(terminals, apply, ownerId) {
|
|
1199
1222
|
return async ({ cwd: effectiveCwd, sessionManager }) => {
|
|
1200
1223
|
const services = await createAgentSessionServices({
|
|
1201
1224
|
cwd: effectiveCwd,
|
|
@@ -1218,8 +1241,8 @@ export class ClientSession {
|
|
|
1218
1241
|
systemPromptOverride: (base) => {
|
|
1219
1242
|
if (typeof base === "string" && base) {
|
|
1220
1243
|
this.lastBaseSystemPrompt = base;
|
|
1221
|
-
if (apply && apply.promptMode === "replace" && apply.
|
|
1222
|
-
return apply.
|
|
1244
|
+
if (apply && apply.promptMode === "replace" && pickTemplatePrompt(apply, this.getLang()).trim()) {
|
|
1245
|
+
return pickTemplatePrompt(apply, this.getLang());
|
|
1223
1246
|
}
|
|
1224
1247
|
}
|
|
1225
1248
|
return undefined;
|
|
@@ -1229,8 +1252,8 @@ export class ClientSession {
|
|
|
1229
1252
|
if (!apply)
|
|
1230
1253
|
this.lastSdkAppendFiles = base.slice();
|
|
1231
1254
|
const out = [...base];
|
|
1232
|
-
if (apply && apply.promptMode === "append" && apply.
|
|
1233
|
-
out.push(apply.
|
|
1255
|
+
if (apply && apply.promptMode === "append" && pickTemplatePrompt(apply, this.getLang()).trim()) {
|
|
1256
|
+
out.push(pickTemplatePrompt(apply, this.getLang()));
|
|
1234
1257
|
}
|
|
1235
1258
|
// 主会话自定义「追加」已并入组合模板的 {{append}} 覆盖,不再在此注入。
|
|
1236
1259
|
if (process.platform === "win32") {
|
|
@@ -1294,12 +1317,12 @@ export class ClientSession {
|
|
|
1294
1317
|
// 把灵魂段换成模板提示词,自动段保留;SYSTEM.md 情形已在
|
|
1295
1318
|
// systemPromptOverride 整体替换,此处边界不存在会自然跳过。
|
|
1296
1319
|
if (apply) {
|
|
1297
|
-
if (apply.promptMode !== "replace" || !apply.
|
|
1320
|
+
if (apply.promptMode !== "replace" || !pickTemplatePrompt(apply, this.getLang()).trim())
|
|
1298
1321
|
return undefined;
|
|
1299
1322
|
const boundary = event.systemPrompt.indexOf("\n\nAvailable tools:");
|
|
1300
1323
|
if (boundary === -1)
|
|
1301
1324
|
return undefined;
|
|
1302
|
-
const swapped = apply.
|
|
1325
|
+
const swapped = pickTemplatePrompt(apply, this.getLang()).trimEnd() + event.systemPrompt.slice(boundary);
|
|
1303
1326
|
return swapped === event.systemPrompt ? undefined : { systemPrompt: swapped };
|
|
1304
1327
|
}
|
|
1305
1328
|
// 主会话:组合模板渲染(模板为空且无覆盖时返回 undefined = 用 SDK 默认)。
|
|
@@ -1337,18 +1360,26 @@ export class ClientSession {
|
|
|
1337
1360
|
idleMs: () => Math.max(0, Math.floor(this.settingsSvc.current.terminalBashIdleMs) || 0),
|
|
1338
1361
|
kills: this.bashKills,
|
|
1339
1362
|
notifyBackgroundDone: (info) => this.notifyTerminalBashDone(terminals, info),
|
|
1363
|
+
// issue #91:bash 返回按客户端 UI 语言出中英(英文默认)。
|
|
1364
|
+
lang: () => this.getLang(),
|
|
1340
1365
|
}),
|
|
1341
1366
|
// 设置关 → 原生 bash;开 → 终端 bash。
|
|
1342
1367
|
() => this.settingsSvc.current.terminalBash),
|
|
1343
|
-
...makePersistentTerminalTools(terminals, effectiveCwd),
|
|
1368
|
+
...makePersistentTerminalTools(terminals, effectiveCwd, () => this.getLang()),
|
|
1344
1369
|
// 不覆盖内置 edit 的独立宽松编辑工具(缩进不敏感匹配;开关看设置)。
|
|
1345
|
-
makeEditSoftTool(effectiveCwd),
|
|
1370
|
+
makeEditSoftTool(effectiveCwd, () => this.getLang()),
|
|
1346
1371
|
// 插件注册的 AI 工具(创建时刻的实时快照;后续注册经
|
|
1347
1372
|
// refreshPluginTools 动态补入已有会话)。
|
|
1348
1373
|
...(this.pluginToolsProvider?.() ?? []).map(pluginToolToDefinition),
|
|
1349
1374
|
// 第一方子代理工具(spawn/get_result/steer/list/stop)。子代理会话
|
|
1350
|
-
// 也注册了它们,因此可自然嵌套派发。
|
|
1351
|
-
|
|
1375
|
+
// 也注册了它们,因此可自然嵌套派发。host 按 ownerId 包装:子代理的
|
|
1376
|
+
// 父对话 = 真正调用 spawn 的那个会话(本 runtime 所属会话),而不是
|
|
1377
|
+
// 派发瞬间的 active——后台对话继续产出时用户可能已切到别的项目,用
|
|
1378
|
+
// activeId 会把孩子记到无关会话名下、沉到别的组/底部(issue #95)。
|
|
1379
|
+
// ownerId 即本 runtime 所属会话(创建时就已知,见各调用点)。
|
|
1380
|
+
...(ownerId
|
|
1381
|
+
? makeSubagentTools(withSubagentOwner(this.subagentHost, ownerId))
|
|
1382
|
+
: makeSubagentTools(this.subagentHost)),
|
|
1352
1383
|
// 内置标记只读查询工具(todo/svc 状态查询,写操作走内联标记)。
|
|
1353
1384
|
makeMarkersListTool(() => this.activeId, this.markerSvc),
|
|
1354
1385
|
// 标准引擎的 ask_user_question:模型调用 → 浏览器富渲染问卷(复用 DSH
|
|
@@ -1776,18 +1807,17 @@ export class ClientSession {
|
|
|
1776
1807
|
conv.queueSteering = [...event.steering];
|
|
1777
1808
|
conv.queueFollowUp = [...event.followUp];
|
|
1778
1809
|
break;
|
|
1779
|
-
// 手动 /compact
|
|
1780
|
-
//
|
|
1810
|
+
// 手动 /compact 或阈值/溢出自动压缩开始——常驻进度条(快照 compaction
|
|
1811
|
+
// 字段),而不是一次性 toast(toast 几秒就消失,而摘要生成可能持续
|
|
1812
|
+
// 数十秒,用户会以为「没反应」)。立即 flush 让进度条第一时间出现。
|
|
1781
1813
|
case "compaction_start": {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
text: "正在压缩上下文…(压缩摘要将显示在消息区)",
|
|
1786
|
-
textEn: "Compacting context… (the summary will appear in the message list)",
|
|
1787
|
-
});
|
|
1814
|
+
conv.compactionState = { reason: event.reason, startedAt: Date.now() };
|
|
1815
|
+
conv.lastCompactionTokens = null;
|
|
1816
|
+
this.flushSnapshot();
|
|
1788
1817
|
break;
|
|
1789
1818
|
}
|
|
1790
1819
|
case "compaction_end": {
|
|
1820
|
+
conv.compactionState = null;
|
|
1791
1821
|
if (event.errorMessage) {
|
|
1792
1822
|
this.emit({
|
|
1793
1823
|
type: "notice",
|
|
@@ -1807,6 +1837,8 @@ export class ClientSession {
|
|
|
1807
1837
|
else if (event.result) {
|
|
1808
1838
|
const { tokensBefore, estimatedTokensAfter } = event.result;
|
|
1809
1839
|
const after = estimatedTokensAfter ?? tokensBefore;
|
|
1840
|
+
// 记住压缩后大小:SDK 在下轮响应前报 null,快照用此回填底栏。
|
|
1841
|
+
conv.lastCompactionTokens = estimatedTokensAfter ?? null;
|
|
1810
1842
|
this.emit({
|
|
1811
1843
|
type: "notice",
|
|
1812
1844
|
level: "info",
|
|
@@ -2144,13 +2176,25 @@ export class ClientSession {
|
|
|
2144
2176
|
totalMessages: s.totalMessages,
|
|
2145
2177
|
tokens: s.tokens,
|
|
2146
2178
|
cost: s.cost,
|
|
2147
|
-
contextUsage:
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2179
|
+
contextUsage: (() => {
|
|
2180
|
+
const cu = s.contextUsage;
|
|
2181
|
+
if (!cu)
|
|
2182
|
+
return stats.contextUsage;
|
|
2183
|
+
// 压缩刚结束、下轮响应未到:SDK 报 null,用压缩结果回填约数。
|
|
2184
|
+
if (cu.tokens == null && conv.lastCompactionTokens != null && cu.contextWindow > 0) {
|
|
2185
|
+
return {
|
|
2186
|
+
tokens: conv.lastCompactionTokens,
|
|
2187
|
+
contextWindow: cu.contextWindow,
|
|
2188
|
+
percent: (conv.lastCompactionTokens / cu.contextWindow) * 100,
|
|
2189
|
+
estimated: true,
|
|
2190
|
+
};
|
|
2152
2191
|
}
|
|
2153
|
-
|
|
2192
|
+
return {
|
|
2193
|
+
tokens: cu.tokens,
|
|
2194
|
+
contextWindow: cu.contextWindow,
|
|
2195
|
+
percent: cu.percent,
|
|
2196
|
+
};
|
|
2197
|
+
})(),
|
|
2154
2198
|
};
|
|
2155
2199
|
}
|
|
2156
2200
|
catch {
|
|
@@ -2187,6 +2231,7 @@ export class ClientSession {
|
|
|
2187
2231
|
queue: { steering: conv.queueSteering, followUp: conv.queueFollowUp },
|
|
2188
2232
|
errorMessage: state.errorMessage,
|
|
2189
2233
|
retry: conv.retryState ?? null,
|
|
2234
|
+
compaction: conv.compactionState ?? null,
|
|
2190
2235
|
tools: state.tools.map((t) => t.name),
|
|
2191
2236
|
version: ++this.version,
|
|
2192
2237
|
piConfigured: this.isPiConfigured(),
|
|
@@ -2466,7 +2511,7 @@ export class ClientSession {
|
|
|
2466
2511
|
}
|
|
2467
2512
|
try {
|
|
2468
2513
|
const targets = collectTargets(this.agentDir, ClientSession.currentAppVersion());
|
|
2469
|
-
const items = await checkAllUpdates(targets);
|
|
2514
|
+
const items = await checkAllUpdates(targets, undefined, () => this.getLang());
|
|
2470
2515
|
this.updatesAllCache = { at: Date.now(), items };
|
|
2471
2516
|
this.emit({ type: "update_status_all", items });
|
|
2472
2517
|
}
|
|
@@ -2581,7 +2626,11 @@ export class ClientSession {
|
|
|
2581
2626
|
this.flushSnapshot();
|
|
2582
2627
|
},
|
|
2583
2628
|
refreshSessions: () => this.refreshSessions(),
|
|
2584
|
-
afterReload: () =>
|
|
2629
|
+
afterReload: () => {
|
|
2630
|
+
// /reload 同样重读磁盘 settings.json——重放重试覆盖 + 终端门控。
|
|
2631
|
+
this.applyRetryOverrides();
|
|
2632
|
+
this.applyToolGating(this.session);
|
|
2633
|
+
},
|
|
2585
2634
|
pluginCommands: () => this.pluginCommandsProvider?.() ?? [],
|
|
2586
2635
|
execPluginCommand: async (name, args) => {
|
|
2587
2636
|
const def = this.pluginCommandsProvider?.().find((c) => c.name === name);
|
|
@@ -2626,10 +2675,10 @@ export class ClientSession {
|
|
|
2626
2675
|
return this.modelAdmin.listModelsConfig();
|
|
2627
2676
|
}
|
|
2628
2677
|
fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
|
|
2629
|
-
return this.modelAdmin.fetchModelsList(reqId, baseUrl, apiKey, authHeader, api);
|
|
2678
|
+
return this.modelAdmin.fetchModelsList(reqId, baseUrl, apiKey, authHeader, api, () => this.getLang());
|
|
2630
2679
|
}
|
|
2631
2680
|
refreshProviderModels(providerId, reqId) {
|
|
2632
|
-
return this.modelAdmin.refreshProviderModels(providerId, reqId);
|
|
2681
|
+
return this.modelAdmin.refreshProviderModels(providerId, reqId, () => this.getLang());
|
|
2633
2682
|
}
|
|
2634
2683
|
/** Copy a built-in provider into an editable custom-provider draft
|
|
2635
2684
|
* (clone_provider_result) — lets the user run a second API key without
|
|
@@ -2766,6 +2815,22 @@ export class ClientSession {
|
|
|
2766
2815
|
pushSettings() {
|
|
2767
2816
|
this.settingsSvc.push();
|
|
2768
2817
|
}
|
|
2818
|
+
/** 把设置面板的出错重试次数注入全部存活会话的 SDK SettingsManager。
|
|
2819
|
+
* applyOverrides 只改内存合并视图(不碰 ~/.pi/agent/settings.json),
|
|
2820
|
+
* 且 SDK 每次退避前都重读 getRetrySettings()——即时生效、无需 reload。
|
|
2821
|
+
* 但 session.reload() 会重读磁盘丢掉覆盖,每次 reload 后必须重放
|
|
2822
|
+
* (reloadSession / afterReload / 标记开关直载路径均已接)。 */
|
|
2823
|
+
applyRetryOverrides() {
|
|
2824
|
+
const n = normalizeRetryMaxAttempts(this.settingsSvc.current.retryMaxAttempts);
|
|
2825
|
+
for (const c of this.convs.values()) {
|
|
2826
|
+
try {
|
|
2827
|
+
c.session.settingsManager.applyOverrides({ retry: { maxRetries: n } });
|
|
2828
|
+
}
|
|
2829
|
+
catch {
|
|
2830
|
+
// 会话未就绪或已释放 → 其 runtime 创建时统一注入。
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2769
2834
|
/** Extensions/skills changed externally (e.g. `pi remove` finished in the
|
|
2770
2835
|
* terminal): re-run session.reload() and re-push state. Streaming-safe —
|
|
2771
2836
|
* deferred to agent_end, same as settings reloads. */
|
|
@@ -2792,6 +2857,7 @@ export class ClientSession {
|
|
|
2792
2857
|
if (!this.session.isStreaming) {
|
|
2793
2858
|
try {
|
|
2794
2859
|
await this.session.reload();
|
|
2860
|
+
this.applyRetryOverrides();
|
|
2795
2861
|
this.applyToolGating(this.session);
|
|
2796
2862
|
await this.pushSlashCommands();
|
|
2797
2863
|
this.pushSettings();
|
|
@@ -2870,6 +2936,26 @@ export class ClientSession {
|
|
|
2870
2936
|
// 兼容旧入口:reload + 刷目录在宿主回调里完成
|
|
2871
2937
|
return this.settingsSvc.applyRuntime();
|
|
2872
2938
|
}
|
|
2939
|
+
/** Server language for this client (issue #91): resolved LIVE from the
|
|
2940
|
+
* persisted UI locale — "zh" only for zh*; everything else (including
|
|
2941
|
+
* never-reported) is English. Per-call tool return values read this on
|
|
2942
|
+
* every invocation, so they follow language switches with no rebuild. */
|
|
2943
|
+
getLang() {
|
|
2944
|
+
return resolveServerLang(this.stateStore.get(this.clientId).locale);
|
|
2945
|
+
}
|
|
2946
|
+
/** Persist the browser UI locale (hello.locale / set_locale) and refresh
|
|
2947
|
+
* lang-aware prompt segments. Reuses the settings reload path, so it is
|
|
2948
|
+
* streaming-safe (deferred to agent_end mid-run, same as settings). */
|
|
2949
|
+
async setLocale(locale) {
|
|
2950
|
+
const code = locale.trim().slice(0, 16);
|
|
2951
|
+
if (!code)
|
|
2952
|
+
return;
|
|
2953
|
+
const prev = this.getLang();
|
|
2954
|
+
this.stateStore.saveLocale(this.clientId, code);
|
|
2955
|
+
if (this.getLang() === prev)
|
|
2956
|
+
return; // same server language — nothing to re-render
|
|
2957
|
+
await this.applySettingsReload();
|
|
2958
|
+
}
|
|
2873
2959
|
// ---------------------------------------------------------------------------
|
|
2874
2960
|
// Commands
|
|
2875
2961
|
// ---------------------------------------------------------------------------
|
|
@@ -2963,6 +3049,8 @@ export class ClientSession {
|
|
|
2963
3049
|
emit: (msg) => this.emit(msg),
|
|
2964
3050
|
settings: this.settingsSvc.current,
|
|
2965
3051
|
session: this.session,
|
|
3052
|
+
// issue #91:附件/视觉桥文案按客户端 UI 语言出中英(英文默认)。
|
|
3053
|
+
getLang: () => this.getLang(),
|
|
2966
3054
|
}, attachments);
|
|
2967
3055
|
for (const aside of asides) {
|
|
2968
3056
|
await s.sendCustomMessage(aside.message, { deliverAs: "nextTurn" });
|
|
@@ -3224,7 +3312,7 @@ export class ClientSession {
|
|
|
3224
3312
|
this.clearAllToolWatchdogs(conv);
|
|
3225
3313
|
conv.toolStartTimes.clear();
|
|
3226
3314
|
await conv.runtime.dispose();
|
|
3227
|
-
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(conv.terminals), {
|
|
3315
|
+
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(conv.terminals, undefined, conv.id), {
|
|
3228
3316
|
cwd: conv.cwd,
|
|
3229
3317
|
agentDir: this.agentDir,
|
|
3230
3318
|
sessionManager: SessionManager.continueRecent(conv.cwd),
|
|
@@ -3303,7 +3391,7 @@ export class ClientSession {
|
|
|
3303
3391
|
try {
|
|
3304
3392
|
const conversationId = this.nextConversationId();
|
|
3305
3393
|
const terminals = this.makeTerminalManager(conversationId, this.cwd);
|
|
3306
|
-
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals), {
|
|
3394
|
+
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
3307
3395
|
cwd: this.cwd,
|
|
3308
3396
|
agentDir: this.agentDir,
|
|
3309
3397
|
sessionManager: SessionManager.create(this.cwd),
|
|
@@ -3857,7 +3945,7 @@ export class ClientSession {
|
|
|
3857
3945
|
const targetCwd = sessionManager.getCwd();
|
|
3858
3946
|
const conversationId = this.nextConversationId();
|
|
3859
3947
|
openedTerminals = this.makeTerminalManager(conversationId, targetCwd);
|
|
3860
|
-
openedRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(openedTerminals), {
|
|
3948
|
+
openedRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(openedTerminals, undefined, conversationId), {
|
|
3861
3949
|
cwd: targetCwd,
|
|
3862
3950
|
agentDir: this.agentDir,
|
|
3863
3951
|
sessionManager,
|
|
@@ -4209,7 +4297,7 @@ export class ClientSession {
|
|
|
4209
4297
|
// First visit to this project: resume its most recent session.
|
|
4210
4298
|
const conversationId = this.nextConversationId();
|
|
4211
4299
|
const terminals = this.makeTerminalManager(conversationId, abs);
|
|
4212
|
-
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals), {
|
|
4300
|
+
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId), {
|
|
4213
4301
|
cwd: abs,
|
|
4214
4302
|
agentDir: this.agentDir,
|
|
4215
4303
|
sessionManager: SessionManager.continueRecent(abs),
|
|
@@ -4615,6 +4703,25 @@ export class AgentService {
|
|
|
4615
4703
|
for (const cs of this.clients.values())
|
|
4616
4704
|
cs.refreshPluginTools();
|
|
4617
4705
|
}
|
|
4706
|
+
/** Browser UI locale report (hello.locale / set_locale): persist per client
|
|
4707
|
+
* and refresh lang-aware prompts (streaming-safe via ClientSession). */
|
|
4708
|
+
async setLocale(clientId, locale) {
|
|
4709
|
+
const cs = this.clients.get(clientId);
|
|
4710
|
+
if (cs) {
|
|
4711
|
+
await cs.setLocale(locale);
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4714
|
+
// hello race: session still being created — wait for it, then apply.
|
|
4715
|
+
const inflight = this.pending.get(clientId);
|
|
4716
|
+
if (inflight) {
|
|
4717
|
+
try {
|
|
4718
|
+
await (await inflight).setLocale(locale);
|
|
4719
|
+
}
|
|
4720
|
+
catch {
|
|
4721
|
+
/* attach failed — nothing to apply to */
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4618
4725
|
/** 插件斜杠命令集合变化时由 index.ts 触发:重推各客户端的命令目录。 */
|
|
4619
4726
|
applyPluginCommandCatalog() {
|
|
4620
4727
|
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({
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { dirname } from "node:path";
|
|
11
|
+
/** 大模型 API 出错自动重试次数的默认值(SDK 默认 3)。 */
|
|
12
|
+
export const DEFAULT_RETRY_MAX_ATTEMPTS = 6;
|
|
13
|
+
/** 归一化重试次数:非数值回落默认,钳制到 [0, 100] 整数。 */
|
|
14
|
+
export function normalizeRetryMaxAttempts(v) {
|
|
15
|
+
const n = Math.floor(Number(v));
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return DEFAULT_RETRY_MAX_ATTEMPTS;
|
|
18
|
+
return Math.min(100, Math.max(0, n));
|
|
19
|
+
}
|
|
11
20
|
/** Stable identity of an extension for the enable/disable toggle: the npm
|
|
12
21
|
* spec for packages (survives version bumps), the resolved entry path
|
|
13
22
|
* otherwise. */
|
|
@@ -143,6 +152,18 @@ export class ClientStateStore {
|
|
|
143
152
|
locked: s.goalPrefs.locked ?? true,
|
|
144
153
|
};
|
|
145
154
|
}
|
|
155
|
+
/** Persist the client's UI locale code (hello/set_locale; best-effort). */
|
|
156
|
+
saveLocale(clientId, locale) {
|
|
157
|
+
const code = locale.trim().slice(0, 16);
|
|
158
|
+
if (!code)
|
|
159
|
+
return;
|
|
160
|
+
const all = this.load();
|
|
161
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
162
|
+
if (state.locale === code)
|
|
163
|
+
return;
|
|
164
|
+
state.locale = code;
|
|
165
|
+
this.save();
|
|
166
|
+
}
|
|
146
167
|
/** Persist the client's goal/review preferences (model choice, rounds, lock). */
|
|
147
168
|
saveGoalPrefs(clientId, prefs) {
|
|
148
169
|
const all = this.load();
|
|
@@ -205,6 +226,7 @@ export class ClientStateStore {
|
|
|
205
226
|
terminalBashIdleMs: stored?.terminalBashIdleMs ?? 15_000,
|
|
206
227
|
editSoftEnabled: stored?.editSoftEnabled ?? false,
|
|
207
228
|
questionnaireEnabled: stored?.questionnaireEnabled ?? true,
|
|
229
|
+
goalModeEnabled: stored?.goalModeEnabled ?? true,
|
|
208
230
|
thinkingWrap: stored?.thinkingWrap ?? false,
|
|
209
231
|
toolsWrap: stored?.toolsWrap ?? true,
|
|
210
232
|
visionBridgeEnabled: stored?.visionBridgeEnabled ?? true,
|
|
@@ -212,6 +234,7 @@ export class ClientStateStore {
|
|
|
212
234
|
visionBridgePromptMode: stored?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
213
235
|
visionBridgePrompt: stored?.visionBridgePrompt ?? "",
|
|
214
236
|
subagentDefaultModel: stored?.subagentDefaultModel ?? null,
|
|
237
|
+
retryMaxAttempts: normalizeRetryMaxAttempts(stored?.retryMaxAttempts),
|
|
215
238
|
quickPhrases: stored?.quickPhrases ?? [],
|
|
216
239
|
quickPhrasesEnabled: stored?.quickPhrasesEnabled ?? true,
|
|
217
240
|
reviewPrompt: stored?.reviewPrompt ?? "",
|
|
@@ -236,11 +259,13 @@ export class ClientStateStore {
|
|
|
236
259
|
terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
|
|
237
260
|
editSoftEnabled: settings.editSoftEnabled ?? cur.editSoftEnabled ?? false,
|
|
238
261
|
questionnaireEnabled: settings.questionnaireEnabled ?? cur.questionnaireEnabled ?? true,
|
|
262
|
+
goalModeEnabled: settings.goalModeEnabled ?? cur.goalModeEnabled ?? true,
|
|
239
263
|
thinkingWrap: settings.thinkingWrap ?? cur.thinkingWrap ?? false,
|
|
240
264
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
241
265
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
242
266
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
243
267
|
subagentDefaultModel: settings.subagentDefaultModel ?? cur.subagentDefaultModel ?? null,
|
|
268
|
+
retryMaxAttempts: normalizeRetryMaxAttempts(settings.retryMaxAttempts ?? cur.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS),
|
|
244
269
|
visionBridgePromptMode: settings.visionBridgePromptMode ?? cur.visionBridgePromptMode ?? "append",
|
|
245
270
|
visionBridgePrompt: settings.visionBridgePrompt ?? cur.visionBridgePrompt ?? "",
|
|
246
271
|
reviewPrompt: settings.reviewPrompt ?? cur.reviewPrompt ?? "",
|
|
@@ -258,6 +283,8 @@ export class ClientStateStore {
|
|
|
258
283
|
// Older client-state files predate review settings.
|
|
259
284
|
reviewPrompt: p.reviewPrompt ?? "",
|
|
260
285
|
reviewDisabledSkills: p.reviewDisabledSkills ?? [],
|
|
286
|
+
// Older presets predate the configurable retry count.
|
|
287
|
+
retryMaxAttempts: normalizeRetryMaxAttempts(p.retryMaxAttempts),
|
|
261
288
|
}));
|
|
262
289
|
}
|
|
263
290
|
/** Persist the client's named settings presets. */
|