pi-web-ui 0.76.0 → 0.77.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/CHANGELOG.md +36 -1
- package/dist/server/agent-service.js +120 -48
- package/dist/server/client-state.js +19 -3
- package/dist/server/delegate-task.js +174 -0
- package/dist/server/dsh/dsh-agent-service.js +57 -20
- package/dist/server/edit-soft-tool.js +3 -1
- package/dist/server/index.js +17 -0
- package/dist/server/markers/builtins/todo.js +2 -2
- package/dist/server/orchestrator.js +109 -0
- package/dist/server/prompt-composer.js +14 -3
- package/dist/server/queue-utils.js +18 -0
- package/dist/server/settings-service.js +67 -19
- package/dist/server/subagent-templates.js +251 -2
- package/dist/server/terminals.js +2 -10
- package/dist/server/tool-manager.js +217 -0
- package/package.json +1 -1
- package/web/dist/assets/TerminalPanel-CDsBHVQ5.js +6 -0
- package/web/dist/assets/index-BsATrxLx.js +346 -0
- package/web/dist/assets/index-DeuPu917.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +22 -7
- package/web/public/sw.js +22 -7
- package/web/dist/assets/TerminalPanel-CKvAH2c_.js +0 -6
- package/web/dist/assets/index-BBvR_HDI.js +0 -335
- package/web/dist/assets/index-ChcqaOWi.css +0 -10
|
@@ -134,6 +134,9 @@ export class DshClientSession {
|
|
|
134
134
|
convs = new Map();
|
|
135
135
|
activeId = "";
|
|
136
136
|
convSeq = 0;
|
|
137
|
+
/** 待答问卷快照(见 attachRuntimeEvents 的 question.pending 与
|
|
138
|
+
* UiState.pendingQuestion):重连/刷新后由快照恢复对话框。 */
|
|
139
|
+
pendingQuestion = null;
|
|
137
140
|
/** 客户端级目标/审查偏好(跨会话共享的默认值,per-conversation goal 用它初始化)。 */
|
|
138
141
|
goalPrefs = { reviewModel: null, maxRounds: 2, locked: false };
|
|
139
142
|
sinks = new Set();
|
|
@@ -454,30 +457,39 @@ export class DshClientSession {
|
|
|
454
457
|
void this.answerQuestion(params0.id, [], true);
|
|
455
458
|
return;
|
|
456
459
|
}
|
|
460
|
+
const mapped = (params0.questions ?? []).map((q) => ({
|
|
461
|
+
id: String(q.id ?? ""),
|
|
462
|
+
question: String(q.question ?? ""),
|
|
463
|
+
...(typeof q.detail === "string"
|
|
464
|
+
? { detail: q.detail }
|
|
465
|
+
: {}),
|
|
466
|
+
...(typeof q.header === "string"
|
|
467
|
+
? { header: q.header }
|
|
468
|
+
: {}),
|
|
469
|
+
...(Array.isArray(q.options)
|
|
470
|
+
? {
|
|
471
|
+
options: q.options.map((o) => ({
|
|
472
|
+
label: String(o.label ?? ""),
|
|
473
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
474
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
475
|
+
})),
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
478
|
+
...(q.multiSelect ? { multiSelect: true } : {}),
|
|
479
|
+
}));
|
|
480
|
+
// 记下待答问卷:`question_pending` 只推给「当时在线」的连接,刷新页面
|
|
481
|
+
// /WS 重连后靠快照(UiState.pendingQuestion)把对话框恢复出来。
|
|
482
|
+
// DSH 的提问桥是 runtime 级的(无 conversationId),故不分对话。
|
|
483
|
+
this.pendingQuestion = {
|
|
484
|
+
id: params0.id,
|
|
485
|
+
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
486
|
+
questions: mapped,
|
|
487
|
+
};
|
|
457
488
|
this.emit({
|
|
458
489
|
type: "question_pending",
|
|
459
490
|
id: params0.id,
|
|
460
491
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
461
|
-
questions:
|
|
462
|
-
id: String(q.id ?? ""),
|
|
463
|
-
question: String(q.question ?? ""),
|
|
464
|
-
...(typeof q.detail === "string"
|
|
465
|
-
? { detail: q.detail }
|
|
466
|
-
: {}),
|
|
467
|
-
...(typeof q.header === "string"
|
|
468
|
-
? { header: q.header }
|
|
469
|
-
: {}),
|
|
470
|
-
...(Array.isArray(q.options)
|
|
471
|
-
? {
|
|
472
|
-
options: q.options.map((o) => ({
|
|
473
|
-
label: String(o.label ?? ""),
|
|
474
|
-
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
475
|
-
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
476
|
-
})),
|
|
477
|
-
}
|
|
478
|
-
: {}),
|
|
479
|
-
...(q.multiSelect ? { multiSelect: true } : {}),
|
|
480
|
-
})),
|
|
492
|
+
questions: mapped,
|
|
481
493
|
});
|
|
482
494
|
}
|
|
483
495
|
else if (method === "tools.call.request") {
|
|
@@ -490,8 +502,24 @@ export class DshClientSession {
|
|
|
490
502
|
}
|
|
491
503
|
});
|
|
492
504
|
}
|
|
505
|
+
/** 快照侧的待答问卷(UiState.pendingQuestion):重连/刷新后靠它恢复对话框。
|
|
506
|
+
* DSH 的提问自带超时(goal-rpc 到点 reject),deadline 已过的不再下发——
|
|
507
|
+
* 否则已经没人等的问卷会被重连的客户端当成活的弹出来。标准引擎不限时,
|
|
508
|
+
* 没有 deadline,生命周期由回答/取消/dispose 精确终止。 */
|
|
509
|
+
pendingQuestionForSnapshot() {
|
|
510
|
+
const p = this.pendingQuestion;
|
|
511
|
+
if (!p)
|
|
512
|
+
return null;
|
|
513
|
+
if (p.deadline !== undefined && p.deadline <= Date.now())
|
|
514
|
+
return null;
|
|
515
|
+
return p;
|
|
516
|
+
}
|
|
493
517
|
/** 前端回答模型提问(question/answer → runtime 恢复工具结果)。 */
|
|
494
518
|
async answerQuestion(id, answers, cancelled) {
|
|
519
|
+
// 无论成功失败都清掉待答快照:同 id 不会再有下一次,留着会让重连的客户端
|
|
520
|
+
// 恢复到一张已经没人在等的问卷。
|
|
521
|
+
if (this.pendingQuestion?.id === id)
|
|
522
|
+
this.pendingQuestion = null;
|
|
495
523
|
try {
|
|
496
524
|
await this.runtime.answerQuestion(id, answers, cancelled);
|
|
497
525
|
}
|
|
@@ -1028,6 +1056,7 @@ export class DshClientSession {
|
|
|
1028
1056
|
thinkingLevel: this.thinkingLevel,
|
|
1029
1057
|
availableThinkingLevels: ["high"],
|
|
1030
1058
|
queue: { steering: conv.queue.steering, followUp: conv.queue.followUp },
|
|
1059
|
+
pendingQuestion: this.pendingQuestionForSnapshot(),
|
|
1031
1060
|
tools: [],
|
|
1032
1061
|
version: ++this.version,
|
|
1033
1062
|
piConfigured: !!loadDeepSeekKey(),
|
|
@@ -2191,6 +2220,8 @@ export class DshClientSession {
|
|
|
2191
2220
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
2192
2221
|
disabledSkills: this.settings.disabledSkills,
|
|
2193
2222
|
disabledExtensions: this.settings.disabledExtensions,
|
|
2223
|
+
// DSH engine: no unified tool gating (no subagent/edit_soft); empty keeps protocol complete.
|
|
2224
|
+
disabledAgentTools: [],
|
|
2194
2225
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2195
2226
|
terminalBash: this.settings.terminalBash,
|
|
2196
2227
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
@@ -2201,6 +2232,8 @@ export class DshClientSession {
|
|
|
2201
2232
|
goalModeEnabled: this.settings.goalModeEnabled,
|
|
2202
2233
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2203
2234
|
toolsWrap: this.settings.toolsWrap,
|
|
2235
|
+
// DSH 无 skill 全文注入概念,给空保协议完整。
|
|
2236
|
+
skillsFullText: [],
|
|
2204
2237
|
visionBridgeEnabled: false,
|
|
2205
2238
|
visionBridgeModel: null,
|
|
2206
2239
|
visionBridgePromptMode: "append",
|
|
@@ -2342,11 +2375,15 @@ export class DshClientSession {
|
|
|
2342
2375
|
promptOverrides: {},
|
|
2343
2376
|
disabledSkills: this.settings.disabledSkills,
|
|
2344
2377
|
disabledExtensions: this.settings.disabledExtensions,
|
|
2378
|
+
// DSH engine: no unified tool gating (no subagent/edit_soft); empty keeps protocol complete.
|
|
2379
|
+
disabledAgentTools: [],
|
|
2345
2380
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2346
2381
|
terminalBash: this.settings.terminalBash,
|
|
2347
2382
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2348
2383
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2349
2384
|
// DSH 无独立重试配置,预设沿用默认值。
|
|
2385
|
+
// DSH 无 skill 全文注入概念,给空保预设类型完整。
|
|
2386
|
+
skillsFullText: [],
|
|
2350
2387
|
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
2351
2388
|
visionBridgePromptMode: "append",
|
|
2352
2389
|
visionBridgePrompt: "",
|
|
@@ -23,7 +23,9 @@ import { isAbsolute, join, resolve as nodeResolve } from "node:path";
|
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
import { defineTool, generateDiffString, generateUnifiedPatch, withFileMutationQueue, } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { bilingual, pick } from "./i18n.js";
|
|
26
|
-
|
|
26
|
+
import { EDIT_SOFT_TOOL_NAME } from "./tool-manager.js";
|
|
27
|
+
/** 独立宽松编辑工具名(唯一登记见 tool-manager.ts;此处别名保兼容)。 */
|
|
28
|
+
export const SOFT_EDIT_TOOL_NAME = EDIT_SOFT_TOOL_NAME;
|
|
27
29
|
const replaceEditSchema = Type.Object({
|
|
28
30
|
oldText: Type.String({
|
|
29
31
|
description: bilingual("Text to replace. Loose matching: the content of each non-empty line (trimmed of leading/trailing whitespace) must match the corresponding file lines; leading-indentation (spaces/tabs) differences are ignored. Prefer whole lines/blocks.", "要替换的文本。宽松匹配:每个非空行的内容(去掉首尾空白)需与文件中对应行一致;行首缩进(空格/制表符)的差异会被忽略。建议按整行/整块提供。"),
|
package/dist/server/index.js
CHANGED
|
@@ -402,6 +402,21 @@ if (existsSync(webDist)) {
|
|
|
402
402
|
}
|
|
403
403
|
},
|
|
404
404
|
}));
|
|
405
|
+
// 缺失的静态文件必须 404(不能落进下面的 SPA catch-all):缺少的 hash 产物若
|
|
406
|
+
// 回 index.html(200),浏览器会把 HTML 当 JS/CSS 执行失败黑屏,SW 还会把
|
|
407
|
+
// 它按 200 缓进 STATIC_CACHE,之后即使文件恢复也要清缓存才能好。
|
|
408
|
+
app.use((req, res, next) => {
|
|
409
|
+
const p = req.path;
|
|
410
|
+
if (p.startsWith("/assets/") ||
|
|
411
|
+
p.startsWith("/icons/") ||
|
|
412
|
+
p === "/favicon.svg" ||
|
|
413
|
+
p === "/icon.ico" ||
|
|
414
|
+
p === "/manifest.webmanifest") {
|
|
415
|
+
res.status(404).end();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
next();
|
|
419
|
+
});
|
|
405
420
|
app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
|
|
406
421
|
// Callback form: a failed stat here (npm i -g is mid-replacement of the
|
|
407
422
|
// package dir) responds 503 instead of crashing the request pipeline
|
|
@@ -936,6 +951,7 @@ wss.on("connection", (ws) => {
|
|
|
936
951
|
promptOverrides: msg.promptOverrides,
|
|
937
952
|
disabledSkills: msg.disabledSkills,
|
|
938
953
|
disabledExtensions: msg.disabledExtensions,
|
|
954
|
+
disabledAgentTools: msg.disabledAgentTools,
|
|
939
955
|
disabledPlugins: msg.disabledPlugins,
|
|
940
956
|
terminalToolsEnabled: msg.terminalToolsEnabled,
|
|
941
957
|
terminalBash: msg.terminalBash,
|
|
@@ -945,6 +961,7 @@ wss.on("connection", (ws) => {
|
|
|
945
961
|
goalModeEnabled: msg.goalModeEnabled,
|
|
946
962
|
thinkingWrap: msg.thinkingWrap,
|
|
947
963
|
toolsWrap: msg.toolsWrap,
|
|
964
|
+
skillsFullText: msg.skillsFullText,
|
|
948
965
|
visionBridgeEnabled: msg.visionBridgeEnabled,
|
|
949
966
|
visionBridgeModel: msg.visionBridgeModel,
|
|
950
967
|
visionBridgePromptMode: msg.visionBridgePromptMode,
|
|
@@ -43,14 +43,14 @@ const TODO_GUIDANCE_ZH = [
|
|
|
43
43
|
"# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
|
|
44
44
|
"- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
|
|
45
45
|
"- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
|
|
46
|
-
"- 想查看/list 当前任务列表时,才用 `
|
|
46
|
+
"- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
|
|
47
47
|
"- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
|
|
48
48
|
];
|
|
49
49
|
const TODO_GUIDANCE_EN = [
|
|
50
50
|
"# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
|
|
51
51
|
"- Marker syntax: [[todo:new:<subject>]] to create; [[todo:set:<id>,completed|in_progress|pending]] for status; [[todo:remove:<id>]] to delete; [[todo:dep:<id>,blocks=<dep ids, comma-separated>]] to set dependencies.",
|
|
52
52
|
"- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
|
|
53
|
-
"- Only use the `
|
|
53
|
+
"- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
|
|
54
54
|
"- Never invent task ids; ids are assigned by [[todo:new:...]], starting from incrementing integers.",
|
|
55
55
|
];
|
|
56
56
|
/** 语言感知的 todo guidance(issue #91):en 用英译、zh 用中文,默认英文。 */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const EN = [
|
|
2
|
+
"<orchestrator>",
|
|
3
|
+
"You are an orchestrator, not just an implementer. Your value is decomposition,",
|
|
4
|
+
"delegation, and quality control. Default bias: DELEGATE. Work directly only when",
|
|
5
|
+
"the task is trivially small (single file, known location, direct answer).",
|
|
6
|
+
"",
|
|
7
|
+
"## Intent gate (every message)",
|
|
8
|
+
"- Check skills FIRST: if the request matches a skill trigger, read that skill file",
|
|
9
|
+
" before classifying or acting. Skills handle their tasks better than ad-hoc work.",
|
|
10
|
+
"- Verbalize routing BEFORE acting: state the detected intent (research /",
|
|
11
|
+
" implementation / investigation / evaluation / fix / open-ended) and your approach",
|
|
12
|
+
" (e.g. explore recon first, then answer; plan then delegate).",
|
|
13
|
+
"- Implement ONLY when the current message explicitly asks for implementation",
|
|
14
|
+
" (implement/add/create/fix/change/write), the scope is concrete, and no pending",
|
|
15
|
+
" specialist result blocks you. Otherwise research/clarify and wait.",
|
|
16
|
+
"- Reclassify intent from the CURRENT message only; never auto-carry",
|
|
17
|
+
" implementation mode from prior turns.",
|
|
18
|
+
"",
|
|
19
|
+
"## Delegation protocol",
|
|
20
|
+
"Available specialist templates (subagent_spawn template=):",
|
|
21
|
+
"- oracle: architecture decisions, hard debugging, multi-system tradeoffs",
|
|
22
|
+
"- metis: pre-planning analysis when scope is unclear",
|
|
23
|
+
"- momus: review a work plan for gaps before implementing",
|
|
24
|
+
"- explore: codebase recon (where is X, which file has Y)",
|
|
25
|
+
"- librarian: external docs / library usage / open-source examples",
|
|
26
|
+
"- sisyphus-junior: well-defined single-scope implementation (already researched)",
|
|
27
|
+
"- multimodal-looker: PDFs, images, diagrams needing interpretation",
|
|
28
|
+
"- review / implement / research / scout / audit: built-in execution and review roles",
|
|
29
|
+
"Match the task domain to the template. Visual work goes to frontend-capable",
|
|
30
|
+
"execution; hard logic and architecture go to oracle; unclear scope goes to metis first.",
|
|
31
|
+
"BEFORE each delegation, declare: which template, WHY its description matches the",
|
|
32
|
+
"task domain, and the expected outcome. Then call subagent_spawn.",
|
|
33
|
+
"Delegation prompts MUST include all six sections:",
|
|
34
|
+
"1. TASK (atomic, one action) 2. EXPECTED OUTCOME (concrete deliverables +",
|
|
35
|
+
"done criteria) 3. REQUIRED TOOLS (explicit whitelist) 4. MUST DO (leave nothing",
|
|
36
|
+
"implicit) 5. MUST NOT DO (forbidden actions) 6. CONTEXT (file paths, patterns,",
|
|
37
|
+
"constraints). Vague prompts fail; if your prompt is shorter than 5 lines it is too vague.",
|
|
38
|
+
"For follow-ups continue the SAME subagent session instead of starting fresh.",
|
|
39
|
+
"After delegation ALWAYS verify: does it work, does it follow codebase patterns,",
|
|
40
|
+
"did it respect MUST DO / MUST NOT DO. Never start implementing work that a",
|
|
41
|
+
"pending oracle/momus result was asked to decide.",
|
|
42
|
+
"Anti-duplication: once explore/librarian are tasked, do NOT redo their search yourself.",
|
|
43
|
+
"",
|
|
44
|
+
"## Task management",
|
|
45
|
+
"Multi-step task (2+ steps) -> create a todo list IMMEDIATELY, in detail.",
|
|
46
|
+
"Mark exactly ONE item in_progress before starting it; mark completed IMMEDIATELY",
|
|
47
|
+
"after (never batch). If scope changes, update todos before proceeding.",
|
|
48
|
+
"No todos on non-trivial work = incomplete work.",
|
|
49
|
+
"",
|
|
50
|
+
"## Constraints",
|
|
51
|
+
"NEVER: suppress type errors (as any, @ts-ignore); commit without an explicit",
|
|
52
|
+
"request; speculate about unread code; leave code broken after failures;",
|
|
53
|
+
"use empty catch blocks; delete failing tests to pass.",
|
|
54
|
+
"Prefer existing libraries, small focused changes, and minimal bugfixes",
|
|
55
|
+
"(never refactor while fixing). Run diagnostics/build/tests on changed files",
|
|
56
|
+
"before reporting completion.",
|
|
57
|
+
"</orchestrator>",
|
|
58
|
+
].join("\n");
|
|
59
|
+
const ZH = [
|
|
60
|
+
"<orchestrator>",
|
|
61
|
+
"你是编排者(orchestrator),而不只是一个执行者。你的价值在于任务分解、",
|
|
62
|
+
"委派和质量把关。默认倾向:能委派就委派。只有任务极小(单文件、位置明确、",
|
|
63
|
+
"直接可答)时才亲自动手。",
|
|
64
|
+
"",
|
|
65
|
+
"## 意图门(每条消息先过这一关)",
|
|
66
|
+
"- 先查技能:请求命中某技能触发条件时,先用 read 读该技能文件,再分类或动手。",
|
|
67
|
+
" 技能覆盖的任务,照技能流程做比临场发挥更可靠。",
|
|
68
|
+
"- 先说路由再动手:明确本轮意图(调研 / 实现 / 排查 / 评估 / 修 bug / 开放式),",
|
|
69
|
+
" 并说出你的路线(例如先 explore 摸底再回答、先计划再委派)。",
|
|
70
|
+
"- 只有同时满足才实现:本轮消息有明确的实现动词(实现/加/创建/修/改/写)、",
|
|
71
|
+
" 范围足够具体、不依赖尚未返回的 specialist 结果。否则只做调研/澄清然后等待。",
|
|
72
|
+
"- 每轮只按当前消息重判意图,不把上一轮的“实现模式”自动带过来。",
|
|
73
|
+
"",
|
|
74
|
+
"## 委派协议",
|
|
75
|
+
"可用 specialist 模板(subagent_spawn 的 template 参数):",
|
|
76
|
+
"- oracle:架构决策、难调的 bug、多系统权衡",
|
|
77
|
+
"- metis:范围不清时先做预分析",
|
|
78
|
+
"- momus:实现前先评审计划查缺补漏",
|
|
79
|
+
"- explore:代码库侦察(X 在哪、Y 在哪个文件)",
|
|
80
|
+
"- librarian:外部文档 / 第三方库用法 / 开源实现参考",
|
|
81
|
+
"- sisyphus-junior:已调研清楚的单点实现任务",
|
|
82
|
+
"- multimodal-looker:需要解读的 PDF / 图片 / 图表",
|
|
83
|
+
"- review / implement / research / scout / audit:内置的执行与审查角色",
|
|
84
|
+
"按任务领域选模板:界面视觉走前端执行、硬逻辑与架构走 oracle、范围不清先走 metis。",
|
|
85
|
+
"每次委派前必须声明:选哪个模板、它的简介与任务哪里匹配、期望产出,然后再调 subagent_spawn。",
|
|
86
|
+
"派单词必须包含六段:1. TASK(原子目标) 2. EXPECTED OUTCOME(交付物+完成标准)",
|
|
87
|
+
"3. REQUIRED TOOLS(可用工具白名单) 4. MUST DO(要求写尽) 5. MUST NOT DO(禁区)",
|
|
88
|
+
"6. CONTEXT(文件路径、既有模式、约束)。派单词短于 5 行就是太含糊,一定失败。",
|
|
89
|
+
"追问要在同一个子代理会话里继续,不要另起炉灶。",
|
|
90
|
+
"委派后必须验证:能跑吗、符合代码库既有模式吗、遵守 MUST DO / MUST NOT DO 了吗。",
|
|
91
|
+
"oracle/momus 还没回来之前,不准先把它们要定的实现写了。",
|
|
92
|
+
"禁重复劳动:explore/librarian 已经在查的东西,自己不要再查一遍。",
|
|
93
|
+
"",
|
|
94
|
+
"## 任务管理",
|
|
95
|
+
"多步任务(2 步以上)→ 立刻建 todo 列表,越细越好。",
|
|
96
|
+
"一次只把一项标 in_progress,做完立刻标 completed(不批量)。范围变了先更新 todo 再动手。",
|
|
97
|
+
"复杂任务没有 todo = 没做完。",
|
|
98
|
+
"",
|
|
99
|
+
"## 禁区",
|
|
100
|
+
"绝不:压类型错误(as any、@ts-ignore);未经明确要求就 commit;臆测没读过的代码;",
|
|
101
|
+
"失败后留下一堆 broken 代码;空 catch;删掉失败的测试来“通过”。",
|
|
102
|
+
"优先用现成库、小而聚焦的改动、最小化修 bug(修 bug 时不顺手重构)。",
|
|
103
|
+
"汇报完成前,先对改动过的文件跑一遍诊断/构建/测试。",
|
|
104
|
+
"</orchestrator>",
|
|
105
|
+
].join("\n");
|
|
106
|
+
/** 编排指导块(按语言选;Oh-my-pi Behavior/Delegation/Task/Constraints 的原生改写版)。 */
|
|
107
|
+
export function buildOrchestratorText(lang = "en") {
|
|
108
|
+
return lang === "zh" ? ZH : EN;
|
|
109
|
+
}
|
|
@@ -140,8 +140,10 @@ function escapeXml(s) {
|
|
|
140
140
|
.replace(/"/g, """)
|
|
141
141
|
.replace(/'/g, "'");
|
|
142
142
|
}
|
|
143
|
-
/** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。
|
|
144
|
-
|
|
143
|
+
/** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。
|
|
144
|
+
* fullText = true 全员全文注入(oh-my-pi 式:标题 + 引用描述 + body 全文);
|
|
145
|
+
* 传技能名数组 = 只注入名单里的;content 缺失的条目回落列表行,不中断渲染。 */
|
|
146
|
+
export function buildSkillsText(skills, lang = "en", fullText = false) {
|
|
145
147
|
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
146
148
|
if (visible.length === 0)
|
|
147
149
|
return "";
|
|
@@ -153,6 +155,15 @@ export function buildSkillsText(skills, lang = "en") {
|
|
|
153
155
|
"<available_skills>",
|
|
154
156
|
];
|
|
155
157
|
for (const skill of visible) {
|
|
158
|
+
// 全文模式且有内容:oh-my-pi 注入格式(### Skill: 名 / > 描述 / 全文)。
|
|
159
|
+
const inject = fullText === true || (Array.isArray(fullText) && fullText.includes(skill.name));
|
|
160
|
+
if (inject && skill.content?.trim()) {
|
|
161
|
+
lines.push(`### Skill: ${skill.name}`);
|
|
162
|
+
if (skill.description.trim())
|
|
163
|
+
lines.push(`> ${skill.description.trim()}`);
|
|
164
|
+
lines.push("", skill.content.trim(), "");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
156
167
|
lines.push(" <skill>");
|
|
157
168
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
158
169
|
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
@@ -220,7 +231,7 @@ export function resolveSectionTexts(inputs) {
|
|
|
220
231
|
terminal: inputs.terminalGuidance,
|
|
221
232
|
markers: inputs.markersGuidance,
|
|
222
233
|
context: buildContextText(inputs.contextFiles, lang),
|
|
223
|
-
skills: buildSkillsText(inputs.skills, lang),
|
|
234
|
+
skills: buildSkillsText(inputs.skills, lang, inputs.skillsFullText ?? false),
|
|
224
235
|
cwd: `Current working directory: ${cwd}`,
|
|
225
236
|
};
|
|
226
237
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 排队消息(插队 steer / 排队 followUp)纯函数 —— 独立成模块是为了可单测
|
|
3
|
+
* (`AgentService` 依赖 SDK 与运行时,不适合在单测里 import)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:pi SDK 没有「按条操作队列」的 API,服务端移除一条排队消息的做法是
|
|
6
|
+
* `clearQueue()` 之后把幸存者按原顺序重新入队(见 `AgentService.removeQueued`)。
|
|
7
|
+
* 重建时若用值过滤(`list.filter((t) => t !== text)`),会把**所有**同文本项一起删掉;
|
|
8
|
+
* 而气泡上的 ✕ 只对应一条消息、本地显示镜像也只移除一条 → 同一条文本被排队两次时
|
|
9
|
+
* 「点一次 ✕ 删两条」,且显示与真实队列在下次 `queue_update` 之前不一致。
|
|
10
|
+
* 因此统一按「只移除第一处匹配」处理。
|
|
11
|
+
*/
|
|
12
|
+
/** 移除列表中第一处等于 `text` 的项;找不到时返回入参的拷贝(不修改入参)。 */
|
|
13
|
+
export function removeFirstOccurrence(list, text) {
|
|
14
|
+
const index = list.indexOf(text);
|
|
15
|
+
if (index < 0)
|
|
16
|
+
return [...list];
|
|
17
|
+
return [...list.slice(0, index), ...list.slice(index + 1)];
|
|
18
|
+
}
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, readdirSync } from "node:fs";
|
|
10
10
|
import { basename, dirname, join } from "node:path";
|
|
11
|
-
import { extensionKey, normalizeRetryMaxAttempts, } from "./client-state.js";
|
|
11
|
+
import { extensionKey, normalizeRetryMaxAttempts, normalizeSkillList, } from "./client-state.js";
|
|
12
12
|
import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
|
|
13
13
|
import { DEFAULT_TEMPLATES } from "./subagent-templates.js";
|
|
14
|
+
import { deriveLegacy, foldLegacyIntoDisabled, normalizeDisabledAgentTools } from "./tool-manager.js";
|
|
14
15
|
export class SettingsService {
|
|
15
16
|
host;
|
|
16
17
|
templates;
|
|
@@ -141,13 +142,21 @@ export class SettingsService {
|
|
|
141
142
|
// 推送都会把已删除的 skill 以灰条形式永恒地补回面板(“关闭过的
|
|
142
143
|
// skill 被一直记录”)。session 未就绪时保守跳过。
|
|
143
144
|
if (loadedSkillNames !== null) {
|
|
144
|
-
const stale = [
|
|
145
|
+
const stale = [
|
|
146
|
+
...new Set([
|
|
147
|
+
...this.settings.disabledSkills,
|
|
148
|
+
...this.settings.reviewDisabledSkills,
|
|
149
|
+
...normalizeSkillList(this.settings.skillsFullText),
|
|
150
|
+
]),
|
|
151
|
+
].filter((name) => !loadedSkillNames.has(name) && !this.skillStillOnDisk(name));
|
|
145
152
|
if (stale.length > 0) {
|
|
146
153
|
this.settings.disabledSkills = this.settings.disabledSkills.filter((n) => !stale.includes(n));
|
|
147
154
|
this.settings.reviewDisabledSkills = this.settings.reviewDisabledSkills.filter((n) => !stale.includes(n));
|
|
155
|
+
this.settings.skillsFullText = normalizeSkillList(this.settings.skillsFullText).filter((n) => !stale.includes(n));
|
|
148
156
|
this.host.stateStore.saveSettings(this.host.clientId, {
|
|
149
157
|
disabledSkills: this.settings.disabledSkills,
|
|
150
158
|
reviewDisabledSkills: this.settings.reviewDisabledSkills,
|
|
159
|
+
skillsFullText: this.settings.skillsFullText,
|
|
151
160
|
});
|
|
152
161
|
}
|
|
153
162
|
}
|
|
@@ -180,6 +189,8 @@ export class SettingsService {
|
|
|
180
189
|
const extensions = [...this.knownExtensions.values()]
|
|
181
190
|
.map((e) => ({ ...e, enabled: !disabledExts.has(e.id) }))
|
|
182
191
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
192
|
+
// 统一工具开关是单源(disabledAgentTools),遗留三开关推送时推导,保证面板一致。
|
|
193
|
+
const legacyTools = deriveLegacy(this.settings.disabledAgentTools ?? []);
|
|
183
194
|
// 当前会话提示词快照:完整生效文本 + 各来源默认(自动)内容(只读预览)。
|
|
184
195
|
const promptSnap = this.host.promptSnapshot();
|
|
185
196
|
this.host.emit({
|
|
@@ -189,11 +200,12 @@ export class SettingsService {
|
|
|
189
200
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
190
201
|
promptTemplate: this.settings.promptTemplate ?? "",
|
|
191
202
|
promptOverrides: { ...this.settings.promptOverrides },
|
|
192
|
-
|
|
203
|
+
disabledAgentTools: [...normalizeDisabledAgentTools(this.settings.disabledAgentTools)],
|
|
204
|
+
terminalToolsEnabled: legacyTools.terminalToolsEnabled,
|
|
193
205
|
terminalBash: this.settings.terminalBash,
|
|
194
206
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
195
|
-
editSoftEnabled:
|
|
196
|
-
questionnaireEnabled:
|
|
207
|
+
editSoftEnabled: legacyTools.editSoftEnabled,
|
|
208
|
+
questionnaireEnabled: legacyTools.questionnaireEnabled,
|
|
197
209
|
goalModeEnabled: this.settings.goalModeEnabled,
|
|
198
210
|
thinkingWrap: this.settings.thinkingWrap,
|
|
199
211
|
toolsWrap: this.settings.toolsWrap,
|
|
@@ -204,6 +216,7 @@ export class SettingsService {
|
|
|
204
216
|
reviewPrompt: this.settings.reviewPrompt,
|
|
205
217
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
206
218
|
disabledPlugins: [...(this.settings.disabledPlugins ?? [])],
|
|
219
|
+
skillsFullText: [...normalizeSkillList(this.settings.skillsFullText)],
|
|
207
220
|
// The composed system prompt actually in effect (read-only view).
|
|
208
221
|
effectiveSystemPrompt: promptSnap.full,
|
|
209
222
|
// 每个来源未覆盖时的默认(自动)内容(「各来源」行预览用)。
|
|
@@ -282,9 +295,12 @@ export class SettingsService {
|
|
|
282
295
|
partial.promptTemplate !== undefined ||
|
|
283
296
|
partial.promptOverrides !== undefined ||
|
|
284
297
|
partial.disabledSkills !== undefined ||
|
|
285
|
-
partial.disabledExtensions !== undefined
|
|
298
|
+
partial.disabledExtensions !== undefined;
|
|
299
|
+
// 统一工具开关 live 生效(ActiveSet 加减,无需 reload),见末尾 applyToolGating。
|
|
300
|
+
const toolGatingChanged = partial.disabledAgentTools !== undefined ||
|
|
286
301
|
partial.terminalToolsEnabled !== undefined ||
|
|
287
|
-
partial.editSoftEnabled !== undefined
|
|
302
|
+
partial.editSoftEnabled !== undefined ||
|
|
303
|
+
partial.questionnaireEnabled !== undefined;
|
|
288
304
|
if (partial.promptMode !== undefined)
|
|
289
305
|
this.settings.promptMode = partial.promptMode;
|
|
290
306
|
if (partial.customSystemPrompt !== undefined) {
|
|
@@ -314,8 +330,25 @@ export class SettingsService {
|
|
|
314
330
|
if (partial.disabledPlugins !== undefined) {
|
|
315
331
|
this.settings.disabledPlugins = partial.disabledPlugins;
|
|
316
332
|
}
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
// 统一工具开关:新字段优先;只给遗留单开关时折回新字段。两边写完再由
|
|
334
|
+
// deriveLegacy 回填遗留别名,保证内存/推送/落盘三处一致。
|
|
335
|
+
if (partial.disabledAgentTools !== undefined) {
|
|
336
|
+
this.settings.disabledAgentTools = normalizeDisabledAgentTools(partial.disabledAgentTools);
|
|
337
|
+
}
|
|
338
|
+
if (partial.terminalToolsEnabled !== undefined ||
|
|
339
|
+
partial.editSoftEnabled !== undefined ||
|
|
340
|
+
partial.questionnaireEnabled !== undefined) {
|
|
341
|
+
this.settings.disabledAgentTools = foldLegacyIntoDisabled(this.settings.disabledAgentTools ?? [], {
|
|
342
|
+
terminalToolsEnabled: partial.terminalToolsEnabled,
|
|
343
|
+
editSoftEnabled: partial.editSoftEnabled,
|
|
344
|
+
questionnaireEnabled: partial.questionnaireEnabled,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
{
|
|
348
|
+
const legacy = deriveLegacy(this.settings.disabledAgentTools ?? []);
|
|
349
|
+
this.settings.terminalToolsEnabled = legacy.terminalToolsEnabled;
|
|
350
|
+
this.settings.editSoftEnabled = legacy.editSoftEnabled;
|
|
351
|
+
this.settings.questionnaireEnabled = legacy.questionnaireEnabled;
|
|
319
352
|
}
|
|
320
353
|
if (partial.terminalBash !== undefined) {
|
|
321
354
|
this.settings.terminalBash = partial.terminalBash;
|
|
@@ -323,13 +356,6 @@ export class SettingsService {
|
|
|
323
356
|
if (partial.terminalBashIdleMs !== undefined) {
|
|
324
357
|
this.settings.terminalBashIdleMs = Math.max(0, Math.floor(partial.terminalBashIdleMs) || 0);
|
|
325
358
|
}
|
|
326
|
-
if (partial.editSoftEnabled !== undefined) {
|
|
327
|
-
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
328
|
-
}
|
|
329
|
-
// 问卷开关:运行时无需重载(bridge 处实时读取)。
|
|
330
|
-
if (partial.questionnaireEnabled !== undefined) {
|
|
331
|
-
this.settings.questionnaireEnabled = partial.questionnaireEnabled;
|
|
332
|
-
}
|
|
333
359
|
// 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
|
|
334
360
|
if (partial.goalModeEnabled !== undefined) {
|
|
335
361
|
this.settings.goalModeEnabled = partial.goalModeEnabled;
|
|
@@ -340,6 +366,11 @@ export class SettingsService {
|
|
|
340
366
|
if (partial.toolsWrap !== undefined) {
|
|
341
367
|
this.settings.toolsWrap = partial.toolsWrap;
|
|
342
368
|
}
|
|
369
|
+
// 编排模式 / skill 全文注入:before_agent_start 逐 run 实时读取(agent-service
|
|
370
|
+
// composeInputs + 指导块追加),开关下一轮即生效,无需 reload runtime。
|
|
371
|
+
if (partial.skillsFullText !== undefined) {
|
|
372
|
+
this.settings.skillsFullText = normalizeSkillList(partial.skillsFullText);
|
|
373
|
+
}
|
|
343
374
|
if (partial.visionBridgeEnabled !== undefined) {
|
|
344
375
|
this.settings.visionBridgeEnabled = partial.visionBridgeEnabled;
|
|
345
376
|
}
|
|
@@ -382,6 +413,9 @@ export class SettingsService {
|
|
|
382
413
|
}
|
|
383
414
|
this.host.stateStore.saveSettings(this.host.clientId, this.settings);
|
|
384
415
|
this.push();
|
|
416
|
+
// 统一工具开关 live 生效(ActiveSet 加减;失败静默,下次创建/reload 重放)。
|
|
417
|
+
if (toolGatingChanged)
|
|
418
|
+
this.host.applyToolGating();
|
|
385
419
|
if (needsReload)
|
|
386
420
|
await this.applyRuntime();
|
|
387
421
|
}
|
|
@@ -405,6 +439,7 @@ export class SettingsService {
|
|
|
405
439
|
promptOverrides: { ...this.settings.promptOverrides },
|
|
406
440
|
disabledSkills: [...this.settings.disabledSkills],
|
|
407
441
|
disabledExtensions: [...this.settings.disabledExtensions],
|
|
442
|
+
disabledAgentTools: [...normalizeDisabledAgentTools(this.settings.disabledAgentTools)],
|
|
408
443
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
409
444
|
terminalBash: this.settings.terminalBash,
|
|
410
445
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
@@ -412,6 +447,7 @@ export class SettingsService {
|
|
|
412
447
|
retryMaxAttempts: this.settings.retryMaxAttempts,
|
|
413
448
|
reviewPrompt: this.settings.reviewPrompt,
|
|
414
449
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
450
|
+
skillsFullText: [...normalizeSkillList(this.settings.skillsFullText)],
|
|
415
451
|
};
|
|
416
452
|
const existing = this.presets.findIndex((p) => p.name === n);
|
|
417
453
|
if (existing >= 0)
|
|
@@ -433,6 +469,14 @@ export class SettingsService {
|
|
|
433
469
|
});
|
|
434
470
|
return;
|
|
435
471
|
}
|
|
472
|
+
// 统一工具开关随预设走;旧预设缺新字段时按遗留两开关折算(问卷不进预设,
|
|
473
|
+
// 从当前禁用名单继承,即保留当前问卷状态)。
|
|
474
|
+
const presetDisabled = normalizeDisabledAgentTools(p.disabledAgentTools ??
|
|
475
|
+
foldLegacyIntoDisabled(this.settings.disabledAgentTools ?? [], {
|
|
476
|
+
terminalToolsEnabled: p.terminalToolsEnabled,
|
|
477
|
+
editSoftEnabled: p.editSoftEnabled,
|
|
478
|
+
}));
|
|
479
|
+
const presetLegacy = deriveLegacy(presetDisabled);
|
|
436
480
|
this.settings = {
|
|
437
481
|
promptMode: p.promptMode,
|
|
438
482
|
customSystemPrompt: p.customSystemPrompt,
|
|
@@ -440,12 +484,12 @@ export class SettingsService {
|
|
|
440
484
|
promptOverrides: { ...(p.promptOverrides ?? this.settings.promptOverrides) },
|
|
441
485
|
disabledSkills: [...p.disabledSkills],
|
|
442
486
|
disabledExtensions: [...p.disabledExtensions],
|
|
443
|
-
|
|
444
|
-
terminalToolsEnabled:
|
|
487
|
+
disabledAgentTools: presetDisabled,
|
|
488
|
+
terminalToolsEnabled: presetLegacy.terminalToolsEnabled,
|
|
445
489
|
// 终端接管偏好随预设走;旧预设缺字段时保留当前值。
|
|
446
490
|
terminalBash: p.terminalBash ?? this.settings.terminalBash,
|
|
447
491
|
terminalBashIdleMs: p.terminalBashIdleMs ?? this.settings.terminalBashIdleMs,
|
|
448
|
-
editSoftEnabled:
|
|
492
|
+
editSoftEnabled: presetLegacy.editSoftEnabled,
|
|
449
493
|
// 重试次数随预设走;旧预设缺字段时保留当前值,应用后即时注入各会话。
|
|
450
494
|
retryMaxAttempts: p.retryMaxAttempts ?? this.settings.retryMaxAttempts,
|
|
451
495
|
// 问卷开关不进预设——保留当前值。
|
|
@@ -454,6 +498,8 @@ export class SettingsService {
|
|
|
454
498
|
goalModeEnabled: this.settings.goalModeEnabled,
|
|
455
499
|
reviewPrompt: p.reviewPrompt ?? this.settings.reviewPrompt,
|
|
456
500
|
reviewDisabledSkills: [...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills)],
|
|
501
|
+
// 全文注入名单随预设走;旧预设缺字段时保留当前值。
|
|
502
|
+
skillsFullText: normalizeSkillList(p.skillsFullText ?? this.settings.skillsFullText),
|
|
457
503
|
// 纯 UI 偏好不进预设——保留当前值。
|
|
458
504
|
thinkingWrap: this.settings.thinkingWrap,
|
|
459
505
|
toolsWrap: this.settings.toolsWrap,
|
|
@@ -472,6 +518,8 @@ export class SettingsService {
|
|
|
472
518
|
// 预设可能改了重试次数:即时注入(流式中延迟的 reload 之后还会由调用方重放)。
|
|
473
519
|
this.host.applyRetryOverrides();
|
|
474
520
|
this.push();
|
|
521
|
+
// 预设带了工具开关:live 应用(reload 路径会重放,流式中延迟到 agent_end)。
|
|
522
|
+
this.host.applyToolGating();
|
|
475
523
|
await this.applyRuntime();
|
|
476
524
|
}
|
|
477
525
|
/** Remove a named preset. */
|