pi-web-ui 0.66.0 → 0.67.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 +192 -99
- package/dist/server/client-state.js +37 -17
- package/dist/server/dsh/dsh-agent-service.js +14 -1
- package/dist/server/edit-soft-tool.js +289 -0
- package/dist/server/index.js +3 -0
- package/dist/server/marker-service.js +26 -43
- package/dist/server/markers/builtins/rename.js +2 -56
- package/dist/server/markers/index.js +2 -6
- package/dist/server/prompt-composer.js +180 -0
- package/dist/server/settings-service.js +38 -7
- package/dist/server/system-prompt-soul.js +41 -0
- package/dist/server/terminals.js +5 -5
- package/package.json +1 -1
- package/web/dist/assets/{TerminalPanel-Bu0PEPj5.js → TerminalPanel-B-4FU7T5.js} +1 -1
- package/web/dist/assets/{index-DiO-MN6k.css → index-BGIkWTsd.css} +1 -1
- package/web/dist/assets/index-CqXS2SQU.js +332 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DoOQgKox.js +0 -326
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
15
16
|
import { existsSync, readFileSync, rmSync, statSync, mkdirSync, watch } from "node:fs";
|
|
16
17
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
@@ -31,8 +32,10 @@ import { isExtensionDisabled, isExtensionEnabled, ClientStateStore } from "./cli
|
|
|
31
32
|
import { SubagentTemplatesStore } from "./subagent-templates.js";
|
|
32
33
|
import { applyHeadTail, makePersistentTerminalTools, makeTerminalBashTool, stripAnsi, TERMINAL_TOOLS_GUIDANCE, TERMINAL_TOOL_NAMES, } from "./terminals.js";
|
|
33
34
|
import { WebUIContext, mockThemeProxy } from "./webui-context.js";
|
|
35
|
+
import { makeEditSoftTool, SOFT_EDIT_TOOL_NAME } from "./edit-soft-tool.js";
|
|
34
36
|
import { makeSubagentTools, subagentTitle, } from "./subagents.js";
|
|
35
37
|
import { buildAttachmentMessages } from "./attachments.js";
|
|
38
|
+
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
36
39
|
import { serializeMessage, serializeStreamingMessage } from "./serialize.js";
|
|
37
40
|
import { loadCommands, saveCommandsFile, TerminalManager } from "./terminals.js";
|
|
38
41
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
@@ -77,6 +80,20 @@ export class QuiesceRejectedError extends Error {
|
|
|
77
80
|
// /api/file HTTP endpoint instead of the WebSocket, so they are classified
|
|
78
81
|
// here but never read into the snapshot path.
|
|
79
82
|
// ---------------------------------------------------------------------------
|
|
83
|
+
/** 自家内联扩展名(组合模板渲染,见 prompt-composer.ts)。SDK 以其
|
|
84
|
+
* "<inline:<name>>" 作为 path;扩展白名单/禁用过滤必须放行它。 */
|
|
85
|
+
const INLINE_PERSONA_EXT = "<inline:pi-webui-persona>";
|
|
86
|
+
/** Pi 包文档路径(composer 的 {{pi_docs}} 自动内容用)。随安装位置解析一次。 */
|
|
87
|
+
const PI_DOC_PATHS = (() => {
|
|
88
|
+
try {
|
|
89
|
+
const requireLocal = createRequire(import.meta.url);
|
|
90
|
+
const root = dirname(requireLocal.resolve("@earendil-works/pi-coding-agent/package.json"));
|
|
91
|
+
return { readme: join(root, "README.md"), docs: join(root, "docs"), examples: join(root, "examples") };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { readme: "", docs: "", examples: "" };
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
80
97
|
/** Windows persona appendix — appended to the SDK system prompt on win32 only.
|
|
81
98
|
* Two failure modes it guards against: (1) the SDK bash tool has NO default
|
|
82
99
|
* timeout, so a long-running command hangs the whole conversation forever;
|
|
@@ -93,11 +110,6 @@ const WINDOWS_PERSONA = `You are a coding agent running on Windows. The bash too
|
|
|
93
110
|
- In the interactive terminal (TTY) — which is Git Bash too, not PowerShell — NEVER use heredocs (<<'EOF' ... EOF) or here-strings, and NEVER start interactive programs (vi, less, python -, node -, npm init): they wait for keyboard input that never arrives and hang the terminal forever. Prefer writing a temp script file (e.g. .pi-tmp.sh) and running it non-interactively. ALWAYS pass a timeout to long-running commands (e.g. \`timeout 120 npm run dev\`).
|
|
94
111
|
|
|
95
112
|
Many legacy Chinese text files (.html/.txt/.md/.log, exported documents) are GBK/GB2312 encoded: the read tool decodes UTF-8 only and will show mojibake (乱码) for them. If a file's content looks garbled, read it through the terminal instead: in Git Bash use \`cat file | iconv -f GBK -t UTF-8\` (or \`iconv -f GBK -t UTF-8 file\`); in cmd use \`chcp 65001 && type file\`; in PowerShell use \`Get-Content -Encoding Default file\`. Never paste mojibake into your reasoning or answer — describe the decoded content instead.`;
|
|
96
|
-
/** bash 工具输出限制/过滤管道引导:模型习惯套 `| tail/-n`、`| head`、`| grep`、`| less`
|
|
97
|
-
* 等限输出。这些管道在持久终端里会①缓冲(可见终端全程哑火、看不到实时进度)②把退出码
|
|
98
|
-
* 错报成管道末尾命令(tail 恒 0、grep 无命中恒 1,灾难性掩盖真实失败)③长驻/出错命令会挂到超时。
|
|
99
|
-
* 让模型改用 bash 的 `tail` 参数限输出;长驻/交互任务改走持久终端工具。 */
|
|
100
|
-
const PIPELESS_BASH_GUIDANCE = `Bash tool output-limiting/filtering: do NOT chain shell pipes to trim or filter output. Avoid \`| tail\`, \`| head\`, \`| grep\`, \`| less\`, \`| more\`, \`| cat\`, \`| sort\`, \`| awk\`, \`| sed\`. They buffer output (so the visible terminal shows nothing live), turn the real exit code into the last pipe command's (tail always 0, grep 1 when no match — hiding the actual failure), and can hang a long-running or failing command until timeout. Instead:\n- To limit returned output use the bash \`tail\` parameter (e.g. \`bash(command=..., tail=20)\`) — the underlying command still streams live to the visible terminal.\n- For a long-running server / watcher / interactive program, use the persistent terminal tools (terminal_create then terminal_read / terminal_input / terminal_key / terminal_wait) instead of piping through bash.\nThe bash tool auto-detects a trailing \`| tail\`/\`| grep\` etc. and runs the underlying command directly so it never hides a failure — but you should still prefer the \`tail\` parameter.`;
|
|
101
113
|
/**
|
|
102
114
|
* Killable bash tool: wraps the SDK bash tool (native process spawn, NO terminal).
|
|
103
115
|
* Used when the「默认 bash 覆盖」setting is OFF. Registers its own AbortController
|
|
@@ -184,20 +196,15 @@ function makeMarkersListTool(getActiveId, markerSvc) {
|
|
|
184
196
|
return {
|
|
185
197
|
name: "markers_list",
|
|
186
198
|
label: "List marker state",
|
|
187
|
-
description: "只读查询内联标记状态。状态【写】操作请一律用内联标记([[todo:new:...]]
|
|
199
|
+
description: "只读查询内联标记状态。状态【写】操作请一律用内联标记([[todo:new:...]] 等)写在回答正文里,不要调用本工具做写操作。",
|
|
188
200
|
parameters: Type.Object({
|
|
189
201
|
action: Type.Unsafe({ enum: ["list"] }),
|
|
190
|
-
tool: Type.Optional(Type.
|
|
202
|
+
tool: Type.Optional(Type.Literal("todo")),
|
|
191
203
|
includeDeleted: Type.Optional(Type.Boolean({ description: "是否包含已删除任务(tombstone,仅 todo)" })),
|
|
192
204
|
}),
|
|
193
205
|
execute: async (_id, params) => {
|
|
194
206
|
const p = params;
|
|
195
|
-
const which = p.tool ?? "todo";
|
|
196
207
|
const convId = getActiveId();
|
|
197
|
-
if (which === "svc") {
|
|
198
|
-
const text = markerSvc.describe(convId, "svc", false);
|
|
199
|
-
return { content: [{ type: "text", text }], details: { action: "list", tool: "svc" } };
|
|
200
|
-
}
|
|
201
208
|
const text = markerSvc.describe(convId, "todo", !!p.includeDeleted);
|
|
202
209
|
const state = markerSvc.getRawState(convId, "todo");
|
|
203
210
|
const visible = (state?.tasks ?? []).filter((t) => p.includeDeleted || t.status !== "deleted");
|
|
@@ -772,44 +779,93 @@ export class ClientSession {
|
|
|
772
779
|
* prompt with the same images skips the vision API call entirely — editing
|
|
773
780
|
* a question doesn't re-burn tokens on re-transcribing identical screenshots.
|
|
774
781
|
*/
|
|
775
|
-
/**
|
|
776
|
-
*
|
|
777
|
-
* replace-mode editor can show the prompt it would otherwise replace.
|
|
778
|
-
* Only non-empty when the user has a system-prompt file. */
|
|
782
|
+
/** SYSTEM.md 文件内容(最近一次 loader reload 观察到的 base;组合模板下仅作
|
|
783
|
+
* {{soul}} 自动内容,SDK 默认分支不受影响)。非空 = 用户有系统提示词文件。 */
|
|
779
784
|
lastBaseSystemPrompt = "";
|
|
780
|
-
/**
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
785
|
+
/** SDK APPEND_SYSTEM.md 内容(appendSystemPromptOverride 收到的 base)——
|
|
786
|
+
* composer 的 {{append}} 自动内容。仅主会话(无模板)记录。 */
|
|
787
|
+
lastSdkAppendFiles = [];
|
|
788
|
+
/** 当前活动会话的工具/资源快照 → composer 输入。cwd 取活动对话的。 */
|
|
789
|
+
composeInputs(src) {
|
|
790
|
+
return {
|
|
791
|
+
cwd: src.cwd,
|
|
792
|
+
systemPromptFile: this.lastBaseSystemPrompt || undefined,
|
|
793
|
+
builtinSoul: BUILTIN_SOUL,
|
|
794
|
+
selectedTools: src.selectedTools,
|
|
795
|
+
toolSnippets: src.toolSnippets,
|
|
796
|
+
toolGuidelines: src.toolGuidelines,
|
|
797
|
+
piReadme: PI_DOC_PATHS.readme,
|
|
798
|
+
piDocs: PI_DOC_PATHS.docs,
|
|
799
|
+
piExamples: PI_DOC_PATHS.examples,
|
|
800
|
+
appendFiles: this.lastSdkAppendFiles,
|
|
801
|
+
windowsPersona: process.platform === "win32" ? WINDOWS_PERSONA : "",
|
|
802
|
+
terminalGuidance: this.settingsSvc.current.terminalToolsEnabled !== false ? TERMINAL_TOOLS_GUIDANCE : "",
|
|
803
|
+
markersGuidance: this.markerSvc.buildGuidance(),
|
|
804
|
+
contextFiles: src.contextFiles,
|
|
805
|
+
skills: src.skills,
|
|
806
|
+
};
|
|
798
807
|
}
|
|
799
|
-
/**
|
|
800
|
-
*
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
808
|
+
/** 渲染当前组合模板。模板为空且无任何覆盖时返回 undefined(用 SDK 默认拼装,
|
|
809
|
+
* 零开销且与原始行为逐字节一致)。 */
|
|
810
|
+
renderMainCompose(src) {
|
|
811
|
+
const tpl = (this.settingsSvc.current.promptTemplate ?? "").trim();
|
|
812
|
+
const ovs = this.settingsSvc.current.promptOverrides ?? {};
|
|
813
|
+
const hasOverride = Object.values(ovs).some((v) => typeof v === "string" && v.trim());
|
|
814
|
+
if (!tpl && !hasOverride)
|
|
815
|
+
return undefined;
|
|
816
|
+
const texts = resolveSectionTexts(this.composeInputs(src));
|
|
817
|
+
return renderPromptTemplate(tpl || DEFAULT_PROMPT_TEMPLATE, texts, ovs);
|
|
818
|
+
}
|
|
819
|
+
/** 从活动会话收集工具/资源快照 → 一次算出 ①各来源默认(自动)内容 ②实际生效的
|
|
820
|
+
* 完整提示词。会话未就绪(或出错)返回 undefined,调用方给空值。 */
|
|
821
|
+
sessionPromptSnapshot() {
|
|
804
822
|
try {
|
|
805
|
-
const
|
|
806
|
-
|
|
823
|
+
const sess = this.session;
|
|
824
|
+
if (!sess)
|
|
825
|
+
return undefined;
|
|
826
|
+
const cwd = this.conv?.cwd ?? this.cwd;
|
|
827
|
+
const active = sess.getActiveToolNames();
|
|
828
|
+
const snippets = {};
|
|
829
|
+
const guidelines = [];
|
|
830
|
+
for (const name of active) {
|
|
831
|
+
const def = sess.getToolDefinition(name);
|
|
832
|
+
if (!def)
|
|
833
|
+
continue;
|
|
834
|
+
if (def.promptSnippet && def.promptSnippet.trim())
|
|
835
|
+
snippets[name] = def.promptSnippet;
|
|
836
|
+
if (def.promptGuidelines)
|
|
837
|
+
guidelines.push(...def.promptGuidelines);
|
|
838
|
+
}
|
|
839
|
+
const loader = sess.resourceLoader;
|
|
840
|
+
const texts = resolveSectionTexts(this.composeInputs({
|
|
841
|
+
cwd,
|
|
842
|
+
selectedTools: active,
|
|
843
|
+
toolSnippets: snippets,
|
|
844
|
+
toolGuidelines: guidelines,
|
|
845
|
+
contextFiles: loader.getAgentsFiles().agentsFiles,
|
|
846
|
+
skills: loader.getSkills().skills.map((s) => ({
|
|
847
|
+
name: s.name,
|
|
848
|
+
description: s.description ?? "",
|
|
849
|
+
filePath: s.filePath ?? "",
|
|
850
|
+
})),
|
|
851
|
+
}));
|
|
852
|
+
// 模板/覆盖渲染(无则保持 SDK 默认拼装,与 renderMainCompose 同规则)。
|
|
853
|
+
const tpl = (this.settingsSvc.current.promptTemplate ?? "").trim();
|
|
854
|
+
const ovs = this.settingsSvc.current.promptOverrides ?? {};
|
|
855
|
+
const hasOverride = Object.values(ovs).some((v) => typeof v === "string" && v.trim());
|
|
856
|
+
const rendered = !tpl && !hasOverride ? undefined : renderPromptTemplate(tpl || DEFAULT_PROMPT_TEMPLATE, texts, ovs);
|
|
857
|
+
return { texts, full: rendered ?? sess.systemPrompt };
|
|
807
858
|
}
|
|
808
859
|
catch {
|
|
809
860
|
// Session not ready yet.
|
|
810
|
-
return
|
|
861
|
+
return undefined;
|
|
811
862
|
}
|
|
812
863
|
}
|
|
864
|
+
/** 设置面板预览用的 host 回调(见 SettingsHost.promptSnapshot):完整生效提示词
|
|
865
|
+
* + 各来源默认(自动)内容。会话未就绪时给空值,面板保持可编辑但不预览。 */
|
|
866
|
+
promptSnapshot() {
|
|
867
|
+
return this.sessionPromptSnapshot() ?? { full: "", texts: {} };
|
|
868
|
+
}
|
|
813
869
|
/** Web-facing extension UI context (widgets, notifications). */
|
|
814
870
|
webUi = new WebUIContext((msg) => this.emit(msg));
|
|
815
871
|
/**
|
|
@@ -939,11 +995,10 @@ export class ClientSession {
|
|
|
939
995
|
reloadSession: async () => {
|
|
940
996
|
await this.session.reload();
|
|
941
997
|
// reload() 会把 custom 工具重新加回活跃集——重放终端开关。
|
|
942
|
-
this.
|
|
998
|
+
this.applyToolGating(this.session);
|
|
943
999
|
await this.pushSlashCommands();
|
|
944
1000
|
},
|
|
945
|
-
|
|
946
|
-
effectiveSystemPrompt: () => this.effectiveSystemPrompt(),
|
|
1001
|
+
promptSnapshot: () => this.promptSnapshot(),
|
|
947
1002
|
getMarkerState: () => ({
|
|
948
1003
|
markersEnabled: this.markerSvc.current.markersEnabled,
|
|
949
1004
|
disabledMarkers: [...this.markerSvc.current.disabledMarkers],
|
|
@@ -1034,36 +1089,34 @@ export class ClientSession {
|
|
|
1034
1089
|
// 值——因此 session.reload() 即可让系统提示词 / 技能 / 插件开关生效,
|
|
1035
1090
|
// 新对话(新 runtime)也会自动带上当前设置。
|
|
1036
1091
|
// 子代理带模板(apply)时:prompt/skills/extensions 改读模板视图——
|
|
1037
|
-
// replace
|
|
1092
|
+
// replace 模式:无 SYSTEM.md 时把灵魂段替换为模板提示词(见下方
|
|
1093
|
+
// pi-webui-persona 内联扩展);有 SYSTEM.md 时仍由 systemPromptOverride
|
|
1094
|
+
// 整体替换 base。append 模式把模板提示词追加到
|
|
1038
1095
|
// 末尾(此时主会话的自定义 prompt 不再叠加,角色由模板定义);非空
|
|
1039
1096
|
// 白名单取代主会话开关(只启用这些),空白名单 = 跟随主会话。
|
|
1040
1097
|
resourceLoaderOptions: {
|
|
1041
|
-
//
|
|
1098
|
+
// 系统提示词 base:主会话(组合模板)恒返回 undefined → SDK 走默认分支,
|
|
1099
|
+
// 工具列表/Guidelines/文档指引等自动段照常拼装;SYSTEM.md 内容仅在
|
|
1100
|
+
// 此处捕获(lastBaseSystemPrompt)作 {{soul}} 自动内容。子代理模板
|
|
1101
|
+
// replace 在存在 SYSTEM.md base 时整体替换该 base。
|
|
1042
1102
|
systemPromptOverride: (base) => {
|
|
1043
|
-
// Remember the built-in default so the settings panel can show
|
|
1044
|
-
// it when the user edits in replace mode.
|
|
1045
1103
|
if (typeof base === "string" && base) {
|
|
1046
1104
|
this.lastBaseSystemPrompt = base;
|
|
1105
|
+
if (apply && apply.promptMode === "replace" && apply.systemPrompt.trim()) {
|
|
1106
|
+
return apply.systemPrompt;
|
|
1107
|
+
}
|
|
1047
1108
|
}
|
|
1048
|
-
|
|
1049
|
-
return apply.systemPrompt;
|
|
1050
|
-
}
|
|
1051
|
-
return this.settingsSvc.current.promptMode === "replace" &&
|
|
1052
|
-
this.settingsSvc.current.customSystemPrompt.trim()
|
|
1053
|
-
? this.settingsSvc.current.customSystemPrompt
|
|
1054
|
-
: base;
|
|
1109
|
+
return undefined;
|
|
1055
1110
|
},
|
|
1056
1111
|
appendSystemPromptOverride: (base) => {
|
|
1112
|
+
// 记录 SDK APPEND_SYSTEM.md base(composer {{append}} 自动内容)。
|
|
1113
|
+
if (!apply)
|
|
1114
|
+
this.lastSdkAppendFiles = base.slice();
|
|
1057
1115
|
const out = [...base];
|
|
1058
1116
|
if (apply && apply.promptMode === "append" && apply.systemPrompt.trim()) {
|
|
1059
1117
|
out.push(apply.systemPrompt);
|
|
1060
1118
|
}
|
|
1061
|
-
|
|
1062
|
-
const custom = this.settingsSvc.current.customSystemPrompt.trim();
|
|
1063
|
-
if (this.settingsSvc.current.promptMode === "append" && custom) {
|
|
1064
|
-
out.push(custom);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1119
|
+
// 主会话自定义「追加」已并入组合模板的 {{append}} 覆盖,不再在此注入。
|
|
1067
1120
|
if (process.platform === "win32") {
|
|
1068
1121
|
// Windows 专属 persona:bash 工具跑 Git Bash 且无默认超时、终端
|
|
1069
1122
|
// 是交互式 TTY——注入约束避免 heredoc/交互/长驻命令挂死整个会话;
|
|
@@ -1075,11 +1128,7 @@ export class ClientSession {
|
|
|
1075
1128
|
// 而不是一次性 bash——没有这段模型几乎从不主动选终端工具。
|
|
1076
1129
|
out.push(TERMINAL_TOOLS_GUIDANCE);
|
|
1077
1130
|
}
|
|
1078
|
-
// bash
|
|
1079
|
-
// `| grep` 等限输出。这些管道会缓冲(终端全程哑火)、把退出码错报成
|
|
1080
|
-
// 管道末尾命令(tail 恒 0、grep 无命中 1)、长驻/出错命令挂到超时。
|
|
1081
|
-
// 让模型改用 bash 的 tail 参数,长驻/交互改走持久终端。
|
|
1082
|
-
out.push(PIPELESS_BASH_GUIDANCE);
|
|
1131
|
+
// bash 管道限制已并入 bash 工具自身的 description,不再作为独立提示段注入。
|
|
1083
1132
|
// 内置标记工具引导(按总开关/分组开关过滤)
|
|
1084
1133
|
const markerGuidance = this.markerSvc.buildGuidance();
|
|
1085
1134
|
if (markerGuidance)
|
|
@@ -1101,15 +1150,61 @@ export class ClientSession {
|
|
|
1101
1150
|
// 注意 SDK 在 extensionsOverride 之后才补 sourceInfo,包扩展此处只能靠路径
|
|
1102
1151
|
// 匹配 —— isExtensionDisabled / isExtensionEnabled 同时比对 npm:<pkg> 候选键。
|
|
1103
1152
|
extensionsOverride: (res) => {
|
|
1153
|
+
// 自家内联扩展(灵魂替换)是基础设施,不参与白名单/禁用过滤。
|
|
1154
|
+
const keepOwn = (e) => !e.path.startsWith(INLINE_PERSONA_EXT);
|
|
1104
1155
|
if (apply && apply.enabledExtensions.length > 0) {
|
|
1105
1156
|
const set = new Set(apply.enabledExtensions);
|
|
1106
|
-
return {
|
|
1157
|
+
return {
|
|
1158
|
+
...res,
|
|
1159
|
+
extensions: res.extensions.filter((e) => keepOwn(e) || isExtensionEnabled(e, [...set])),
|
|
1160
|
+
};
|
|
1107
1161
|
}
|
|
1108
1162
|
return {
|
|
1109
1163
|
...res,
|
|
1110
|
-
extensions: res.extensions.filter((e) => !isExtensionDisabled(e, this.settingsSvc.current.disabledExtensions)),
|
|
1164
|
+
extensions: res.extensions.filter((e) => keepOwn(e) || !isExtensionDisabled(e, this.settingsSvc.current.disabledExtensions)),
|
|
1111
1165
|
};
|
|
1112
1166
|
},
|
|
1167
|
+
// 组合模板渲染(主会话)+ 模板灵魂替换(子代理):before_agent_start 在每个
|
|
1168
|
+
// agent run 前触发,SDK 此时已用最新工具/资源拼好基础提示词;若配置了模板或
|
|
1169
|
+
// 覆盖,则用 composer 把 {{token}} 展开为各来源文本(工具列表/项目上下文/技能
|
|
1170
|
+
// 等都取自本次 run 的 systemPromptOptions,永远最新)。
|
|
1171
|
+
extensionFactories: [
|
|
1172
|
+
{
|
|
1173
|
+
name: "pi-webui-persona",
|
|
1174
|
+
hidden: true,
|
|
1175
|
+
factory: (pi) => {
|
|
1176
|
+
pi.on("before_agent_start", (event) => {
|
|
1177
|
+
// 子代理模板 replace(无 SYSTEM.md 时):默认分支拼好的提示词里
|
|
1178
|
+
// 把灵魂段换成模板提示词,自动段保留;SYSTEM.md 情形已在
|
|
1179
|
+
// systemPromptOverride 整体替换,此处边界不存在会自然跳过。
|
|
1180
|
+
if (apply) {
|
|
1181
|
+
if (apply.promptMode !== "replace" || !apply.systemPrompt.trim())
|
|
1182
|
+
return undefined;
|
|
1183
|
+
const boundary = event.systemPrompt.indexOf("\n\nAvailable tools:");
|
|
1184
|
+
if (boundary === -1)
|
|
1185
|
+
return undefined;
|
|
1186
|
+
const swapped = apply.systemPrompt.trimEnd() + event.systemPrompt.slice(boundary);
|
|
1187
|
+
return swapped === event.systemPrompt ? undefined : { systemPrompt: swapped };
|
|
1188
|
+
}
|
|
1189
|
+
// 主会话:组合模板渲染(模板为空且无覆盖时返回 undefined = 用 SDK 默认)。
|
|
1190
|
+
const opts = event.systemPromptOptions;
|
|
1191
|
+
const rendered = this.renderMainCompose({
|
|
1192
|
+
cwd: typeof opts?.cwd === "string" ? opts.cwd : this.cwd,
|
|
1193
|
+
selectedTools: opts?.selectedTools ?? [],
|
|
1194
|
+
toolSnippets: opts?.toolSnippets ?? {},
|
|
1195
|
+
toolGuidelines: opts?.promptGuidelines ?? [],
|
|
1196
|
+
contextFiles: opts?.contextFiles ?? [],
|
|
1197
|
+
skills: (opts?.skills ?? []).map((s) => ({
|
|
1198
|
+
name: s.name,
|
|
1199
|
+
description: s.description ?? "",
|
|
1200
|
+
filePath: s.filePath ?? "",
|
|
1201
|
+
})),
|
|
1202
|
+
});
|
|
1203
|
+
return rendered ? { systemPrompt: rendered } : undefined;
|
|
1204
|
+
});
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
],
|
|
1113
1208
|
},
|
|
1114
1209
|
});
|
|
1115
1210
|
const created = await createAgentSessionFromServices({
|
|
@@ -1130,6 +1225,8 @@ export class ClientSession {
|
|
|
1130
1225
|
// 设置关 → 原生 bash;开 → 终端 bash。
|
|
1131
1226
|
() => this.settingsSvc.current.terminalBash),
|
|
1132
1227
|
...makePersistentTerminalTools(terminals, effectiveCwd),
|
|
1228
|
+
// 不覆盖内置 edit 的独立宽松编辑工具(缩进不敏感匹配;开关看设置)。
|
|
1229
|
+
makeEditSoftTool(effectiveCwd),
|
|
1133
1230
|
// 插件注册的 AI 工具(创建时刻的实时快照;后续注册经
|
|
1134
1231
|
// refreshPluginTools 动态补入已有会话)。
|
|
1135
1232
|
...(this.pluginToolsProvider?.() ?? []).map(pluginToolToDefinition),
|
|
@@ -1141,7 +1238,7 @@ export class ClientSession {
|
|
|
1141
1238
|
],
|
|
1142
1239
|
});
|
|
1143
1240
|
// 终端工具开关从创建起就生效(工具始终注册进注册表,只调活跃集)。
|
|
1144
|
-
this.
|
|
1241
|
+
this.applyToolGating(created.session);
|
|
1145
1242
|
return {
|
|
1146
1243
|
...created,
|
|
1147
1244
|
services,
|
|
@@ -1505,23 +1602,8 @@ export class ClientSession {
|
|
|
1505
1602
|
case "agent_end": {
|
|
1506
1603
|
this.scheduleSessionsRefresh();
|
|
1507
1604
|
this.refreshConversationTitle(conv);
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
const msgs = event.messages;
|
|
1511
|
-
if (Array.isArray(msgs)) {
|
|
1512
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1513
|
-
const m = msgs[i];
|
|
1514
|
-
if (m?.role === "assistant") {
|
|
1515
|
-
const text = extractAssistantTextFromContent(m.content);
|
|
1516
|
-
if (text && text.includes("[[")) {
|
|
1517
|
-
void this.markerSvc.handleAssistantText(conv.id, text);
|
|
1518
|
-
}
|
|
1519
|
-
break;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
catch { }
|
|
1605
|
+
// 内联标记不在此兜底扫最后一条 assistant:每条气泡结束已走 message_end
|
|
1606
|
+
// 即时解析(含中间文本块);这里再扫会把最后一条标记重复执行(todo 重复建号)。
|
|
1525
1607
|
// Manual interrupt (Stop button / abort): the last assistant message
|
|
1526
1608
|
// carries stopReason "aborted". A half-finished run should NOT be
|
|
1527
1609
|
// reviewed (it would fail and inject a revision, only to be stopped
|
|
@@ -1570,15 +1652,21 @@ export class ClientSession {
|
|
|
1570
1652
|
break;
|
|
1571
1653
|
}
|
|
1572
1654
|
case "entry_appended": {
|
|
1655
|
+
// SDK 仅在扩展 appendEntry 时发 entry_appended(entry 恒为 custom),
|
|
1656
|
+
// assistant 消息不会走这里——气泡级解析见 case "message_end"。
|
|
1573
1657
|
this.scheduleSessionsRefresh();
|
|
1574
1658
|
this.refreshConversationTitle(conv);
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
case "message_end": {
|
|
1662
|
+
// 每条 assistant 气泡流式结束 → 立即解析其中的内联标记:每个气泡各自
|
|
1663
|
+
// 生效(不再等整轮 agent_end),同一轮里先前消息的标记也不再丢。
|
|
1664
|
+
const mm = event.message;
|
|
1665
|
+
if (mm?.role !== "assistant")
|
|
1666
|
+
break;
|
|
1667
|
+
const text = extractAssistantTextFromContent(mm.content);
|
|
1668
|
+
if (text && text.includes("[["))
|
|
1669
|
+
void this.markerSvc.handleAssistantText(conv.id, text);
|
|
1582
1670
|
break;
|
|
1583
1671
|
}
|
|
1584
1672
|
case "message_update": {
|
|
@@ -2160,7 +2248,7 @@ export class ClientSession {
|
|
|
2160
2248
|
this.flushSnapshot();
|
|
2161
2249
|
},
|
|
2162
2250
|
refreshSessions: () => this.refreshSessions(),
|
|
2163
|
-
afterReload: () => this.
|
|
2251
|
+
afterReload: () => this.applyToolGating(this.session),
|
|
2164
2252
|
pluginCommands: () => this.pluginCommandsProvider?.() ?? [],
|
|
2165
2253
|
execPluginCommand: async (name, args) => {
|
|
2166
2254
|
const def = this.pluginCommandsProvider?.().find((c) => c.name === name);
|
|
@@ -2371,7 +2459,7 @@ export class ClientSession {
|
|
|
2371
2459
|
if (!this.session.isStreaming) {
|
|
2372
2460
|
try {
|
|
2373
2461
|
await this.session.reload();
|
|
2374
|
-
this.
|
|
2462
|
+
this.applyToolGating(this.session);
|
|
2375
2463
|
await this.pushSlashCommands();
|
|
2376
2464
|
this.pushSettings();
|
|
2377
2465
|
}
|
|
@@ -2406,16 +2494,21 @@ export class ClientSession {
|
|
|
2406
2494
|
/** 把终端工具开关应用到 session 的活跃工具集:关闭时从活跃集中剔除
|
|
2407
2495
|
* terminal_*(工具仍留在注册表,重开时可直接加回)。session.reload() 与新
|
|
2408
2496
|
* 会话创建都会把 custom 工具加回活跃集,所以这两条路径之后都要重放本方法。 */
|
|
2409
|
-
|
|
2497
|
+
applyToolGating(session) {
|
|
2410
2498
|
try {
|
|
2411
|
-
const
|
|
2499
|
+
const terminalEnabled = this.settingsSvc.current.terminalToolsEnabled !== false;
|
|
2500
|
+
const softEditEnabled = this.settingsSvc.current.editSoftEnabled !== false;
|
|
2412
2501
|
const names = new Set(session.getActiveToolNames());
|
|
2413
2502
|
for (const n of TERMINAL_TOOL_NAMES) {
|
|
2414
|
-
if (
|
|
2503
|
+
if (terminalEnabled)
|
|
2415
2504
|
names.add(n);
|
|
2416
2505
|
else
|
|
2417
2506
|
names.delete(n);
|
|
2418
2507
|
}
|
|
2508
|
+
if (softEditEnabled)
|
|
2509
|
+
names.add(SOFT_EDIT_TOOL_NAME);
|
|
2510
|
+
else
|
|
2511
|
+
names.delete(SOFT_EDIT_TOOL_NAME);
|
|
2419
2512
|
session.setActiveToolsByName([...names]);
|
|
2420
2513
|
}
|
|
2421
2514
|
catch {
|
|
@@ -179,24 +179,41 @@ export class ClientStateStore {
|
|
|
179
179
|
/** Last-used settings-panel state for a client, or defaults. */
|
|
180
180
|
getSettings(clientId) {
|
|
181
181
|
const s = this.load()[clientId];
|
|
182
|
+
const stored = s?.settings;
|
|
183
|
+
// 旧存档(promptMode/customSystemPrompt)迁移到 compose:追加文字成为独立
|
|
184
|
+
// {{append}} 覆盖、替换文字成为 {{soul}} 覆盖;无自定义则用默认模板。
|
|
185
|
+
let promptTemplate = "";
|
|
186
|
+
let promptOverrides = {};
|
|
187
|
+
if (stored?.promptTemplate !== undefined) {
|
|
188
|
+
promptTemplate = stored.promptTemplate ?? "";
|
|
189
|
+
promptOverrides = { ...stored?.promptOverrides };
|
|
190
|
+
}
|
|
191
|
+
else if (stored && typeof stored.customSystemPrompt === "string" && stored.customSystemPrompt.trim()) {
|
|
192
|
+
promptOverrides = {
|
|
193
|
+
[stored.promptMode === "replace" ? "soul" : "append"]: stored.customSystemPrompt,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
182
196
|
return {
|
|
183
|
-
promptMode:
|
|
184
|
-
customSystemPrompt:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
promptMode: stored?.promptMode === "replace" ? "replace" : "append",
|
|
198
|
+
customSystemPrompt: stored?.customSystemPrompt ?? "",
|
|
199
|
+
promptTemplate,
|
|
200
|
+
promptOverrides,
|
|
201
|
+
disabledSkills: stored?.disabledSkills ?? [],
|
|
202
|
+
disabledExtensions: stored?.disabledExtensions ?? [],
|
|
203
|
+
terminalToolsEnabled: stored?.terminalToolsEnabled ?? true,
|
|
204
|
+
terminalBash: stored?.terminalBash ?? false,
|
|
205
|
+
terminalBashIdleMs: stored?.terminalBashIdleMs ?? 15_000,
|
|
206
|
+
editSoftEnabled: stored?.editSoftEnabled ?? false,
|
|
207
|
+
thinkingWrap: stored?.thinkingWrap ?? false,
|
|
208
|
+
toolsWrap: stored?.toolsWrap ?? true,
|
|
209
|
+
visionBridgeEnabled: stored?.visionBridgeEnabled ?? true,
|
|
210
|
+
visionBridgeModel: stored?.visionBridgeModel ?? null,
|
|
211
|
+
visionBridgePromptMode: stored?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
212
|
+
visionBridgePrompt: stored?.visionBridgePrompt ?? "",
|
|
213
|
+
subagentDefaultModel: stored?.subagentDefaultModel ?? null,
|
|
214
|
+
reviewPrompt: stored?.reviewPrompt ?? "",
|
|
215
|
+
reviewDisabledSkills: stored?.reviewDisabledSkills ?? [],
|
|
216
|
+
disabledPlugins: stored?.disabledPlugins ?? [],
|
|
200
217
|
};
|
|
201
218
|
}
|
|
202
219
|
/** Persist the client's settings-panel state (partial merge). */
|
|
@@ -207,11 +224,14 @@ export class ClientStateStore {
|
|
|
207
224
|
state.settings = {
|
|
208
225
|
promptMode: settings.promptMode ?? cur.promptMode ?? "append",
|
|
209
226
|
customSystemPrompt: settings.customSystemPrompt ?? cur.customSystemPrompt ?? "",
|
|
227
|
+
promptTemplate: settings.promptTemplate ?? cur.promptTemplate ?? "",
|
|
228
|
+
promptOverrides: { ...(settings.promptOverrides ?? cur.promptOverrides) },
|
|
210
229
|
disabledSkills: settings.disabledSkills ?? cur.disabledSkills ?? [],
|
|
211
230
|
disabledExtensions: settings.disabledExtensions ?? cur.disabledExtensions ?? [],
|
|
212
231
|
terminalToolsEnabled: settings.terminalToolsEnabled ?? cur.terminalToolsEnabled ?? true,
|
|
213
232
|
terminalBash: settings.terminalBash ?? cur.terminalBash ?? false,
|
|
214
233
|
terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
|
|
234
|
+
editSoftEnabled: settings.editSoftEnabled ?? cur.editSoftEnabled ?? false,
|
|
215
235
|
thinkingWrap: settings.thinkingWrap ?? cur.thinkingWrap ?? false,
|
|
216
236
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
217
237
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
@@ -107,6 +107,7 @@ const DEFAULT_SETTINGS = {
|
|
|
107
107
|
terminalToolsEnabled: true,
|
|
108
108
|
terminalBash: false,
|
|
109
109
|
terminalBashIdleMs: 15_000,
|
|
110
|
+
editSoftEnabled: false,
|
|
110
111
|
thinkingWrap: false,
|
|
111
112
|
toolsWrap: true,
|
|
112
113
|
disabledPlugins: [],
|
|
@@ -217,6 +218,7 @@ export class DshClientSession {
|
|
|
217
218
|
terminalToolsEnabled: savedSettings.terminalToolsEnabled,
|
|
218
219
|
terminalBash: savedSettings.terminalBash,
|
|
219
220
|
terminalBashIdleMs: savedSettings.terminalBashIdleMs,
|
|
221
|
+
editSoftEnabled: savedSettings.editSoftEnabled,
|
|
220
222
|
thinkingWrap: savedSettings.thinkingWrap,
|
|
221
223
|
toolsWrap: savedSettings.toolsWrap,
|
|
222
224
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
@@ -2053,6 +2055,7 @@ export class DshClientSession {
|
|
|
2053
2055
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2054
2056
|
terminalBash: this.settings.terminalBash,
|
|
2055
2057
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2058
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2056
2059
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2057
2060
|
toolsWrap: this.settings.toolsWrap,
|
|
2058
2061
|
visionBridgeEnabled: false,
|
|
@@ -2062,8 +2065,10 @@ export class DshClientSession {
|
|
|
2062
2065
|
reviewPrompt: this.settings.reviewPrompt,
|
|
2063
2066
|
reviewDisabledSkills: [],
|
|
2064
2067
|
disabledPlugins: this.settings.disabledPlugins,
|
|
2065
|
-
|
|
2068
|
+
promptTemplate: "",
|
|
2069
|
+
promptOverrides: {},
|
|
2066
2070
|
effectiveSystemPrompt: this.settings.customSystemPrompt,
|
|
2071
|
+
promptSourceDefaults: {},
|
|
2067
2072
|
visionBridgeDefaultPrompt: "",
|
|
2068
2073
|
visionModels: [],
|
|
2069
2074
|
skills: this.skillsCache,
|
|
@@ -2095,6 +2100,8 @@ export class DshClientSession {
|
|
|
2095
2100
|
this.settings.terminalBash = partial.terminalBash;
|
|
2096
2101
|
if (partial.terminalBashIdleMs !== undefined)
|
|
2097
2102
|
this.settings.terminalBashIdleMs = partial.terminalBashIdleMs;
|
|
2103
|
+
if (partial.editSoftEnabled !== undefined)
|
|
2104
|
+
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
2098
2105
|
if (partial.thinkingWrap !== undefined)
|
|
2099
2106
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
2100
2107
|
if (partial.toolsWrap !== undefined)
|
|
@@ -2112,6 +2119,7 @@ export class DshClientSession {
|
|
|
2112
2119
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2113
2120
|
terminalBash: this.settings.terminalBash,
|
|
2114
2121
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2122
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2115
2123
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2116
2124
|
toolsWrap: this.settings.toolsWrap,
|
|
2117
2125
|
disabledPlugins: this.settings.disabledPlugins,
|
|
@@ -2161,11 +2169,14 @@ export class DshClientSession {
|
|
|
2161
2169
|
name,
|
|
2162
2170
|
promptMode: this.settings.promptMode,
|
|
2163
2171
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
2172
|
+
promptTemplate: "",
|
|
2173
|
+
promptOverrides: {},
|
|
2164
2174
|
disabledSkills: this.settings.disabledSkills,
|
|
2165
2175
|
disabledExtensions: this.settings.disabledExtensions,
|
|
2166
2176
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2167
2177
|
terminalBash: this.settings.terminalBash,
|
|
2168
2178
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2179
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2169
2180
|
visionBridgePromptMode: "append",
|
|
2170
2181
|
visionBridgePrompt: "",
|
|
2171
2182
|
reviewPrompt: this.settings.reviewPrompt,
|
|
@@ -2198,6 +2209,7 @@ export class DshClientSession {
|
|
|
2198
2209
|
this.settings.terminalToolsEnabled = preset.terminalToolsEnabled;
|
|
2199
2210
|
this.settings.terminalBash = preset.terminalBash;
|
|
2200
2211
|
this.settings.terminalBashIdleMs = preset.terminalBashIdleMs;
|
|
2212
|
+
this.settings.editSoftEnabled = preset.editSoftEnabled ?? this.settings.editSoftEnabled;
|
|
2201
2213
|
this.settings.reviewPrompt = preset.reviewPrompt ?? "";
|
|
2202
2214
|
this.stateStore.saveSettings(this.clientId, {
|
|
2203
2215
|
promptMode: this.settings.promptMode,
|
|
@@ -2207,6 +2219,7 @@ export class DshClientSession {
|
|
|
2207
2219
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2208
2220
|
terminalBash: this.settings.terminalBash,
|
|
2209
2221
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2222
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2210
2223
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2211
2224
|
toolsWrap: this.settings.toolsWrap,
|
|
2212
2225
|
disabledPlugins: this.settings.disabledPlugins,
|