pi-web-ui 0.65.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/README.md +42 -0
- package/README.zh-CN.md +466 -427
- package/dist/server/agent-service.js +237 -108
- 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/dist/server/update-check.js +55 -16
- package/package.json +1 -1
- package/web/dist/assets/{TerminalPanel-B_ACe6Lp.js → TerminalPanel-B-4FU7T5.js} +1 -1
- package/web/dist/assets/index-BGIkWTsd.css +10 -0
- package/web/dist/assets/index-CqXS2SQU.js +332 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bq4P6EhG.css +0 -10
- package/web/dist/assets/index-CaJbqnT7.js +0 -326
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主会话系统提示词 = 自由组合模板(compose)。
|
|
3
|
+
*
|
|
4
|
+
* 模板里的 `{{token}}` 在每次 agent run 前展开为对应「来源」的提示词块;每个
|
|
5
|
+
* token 可单独覆盖——overrides 里有内容就用覆盖文本,否则用该来源的自动内容。
|
|
6
|
+
* 这样既可自由排序/增删/穿插自己的话,也可只替换某一个来源而不影响其他自动段
|
|
7
|
+
* (工具列表、项目上下文等仍由 SDK 用最新数据重新生成)。
|
|
8
|
+
*
|
|
9
|
+
* token 列表及默认顺序镜像 buildSystemPrompt(SDK dist/core/system-prompt.js)
|
|
10
|
+
* 默认分支的拼装顺序:
|
|
11
|
+
* soul → tools → guidelines → pi_docs → append → persona → terminal →
|
|
12
|
+
* markers → context → skills → cwd
|
|
13
|
+
*
|
|
14
|
+
* (bash 管道限制不需要独立段:它属于 bash 工具的用法说明,已写进工具自身
|
|
15
|
+
* description,随工具走;compose 里不再单设 {{pipe}} 来源。)
|
|
16
|
+
*
|
|
17
|
+
* 本模块是纯函数(不 import SDK / node),浏览器端可复用(SettingsModal 需要
|
|
18
|
+
* DEFAULT_PROMPT_TEMPLATE 与 token 元数据)。
|
|
19
|
+
*/
|
|
20
|
+
/** 全部来源 token。默认模板顺序即此数组顺序。 */
|
|
21
|
+
export const PROMPT_TOKENS = [
|
|
22
|
+
"soul", // 内置灵魂提示词(persona;有 SYSTEM.md 时其内容)
|
|
23
|
+
"tools", // Available tools 工具列表(含各工具 snippet + "In addition…" 句)
|
|
24
|
+
"guidelines", // Guidelines 行为准则段
|
|
25
|
+
"pi_docs", // Pi documentation 文档指引(指向 pi 包路径)
|
|
26
|
+
"append", // 追加段(APPEND_SYSTEM.md 内容;覆盖 = 自定义追加文字)
|
|
27
|
+
"persona", // Windows persona(仅 win32)
|
|
28
|
+
"terminal", // 终端工具使用引导(「终端工具」开关开时)
|
|
29
|
+
"markers", // 内置标记工具引导(markers 开启时)
|
|
30
|
+
"context", // 项目上下文 <project_context>(AGENTS.md 等)
|
|
31
|
+
"skills", // 技能段 <available_skills>
|
|
32
|
+
"cwd", // Current working directory 行
|
|
33
|
+
];
|
|
34
|
+
/** 默认模板:全部 token 按自然顺序以空行连接 —— 无覆盖、不改动时渲染结果 ≈
|
|
35
|
+
* SDK 默认拼装的完整提示词。 */
|
|
36
|
+
export const DEFAULT_PROMPT_TEMPLATE = PROMPT_TOKENS.map((t) => `{{${t}}}`).join("\n\n");
|
|
37
|
+
const TOKEN_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
38
|
+
export function isKnownToken(name) {
|
|
39
|
+
return PROMPT_TOKENS.includes(name);
|
|
40
|
+
}
|
|
41
|
+
/** 模板里出现的全部 token(含未知名,供 UI 提示)。 */
|
|
42
|
+
export function collectTemplateTokens(template) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const m of template.matchAll(TOKEN_RE)) {
|
|
45
|
+
if (!out.includes(m[1]))
|
|
46
|
+
out.push(m[1]);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** 空模板 = 默认模板。 */
|
|
51
|
+
export function effectiveTemplate(template) {
|
|
52
|
+
const t = (template ?? "").trim();
|
|
53
|
+
return t || DEFAULT_PROMPT_TEMPLATE;
|
|
54
|
+
}
|
|
55
|
+
/** 该来源是否有「自动内容」之外的覆盖。 */
|
|
56
|
+
export function overrideOf(overrides, token) {
|
|
57
|
+
const v = overrides?.[token];
|
|
58
|
+
return v && v.trim() ? v : "";
|
|
59
|
+
}
|
|
60
|
+
/** 内置默认灵魂段落(buildSystemPrompt 默认分支的开头,与 SDK 同步维护)。 */
|
|
61
|
+
export const BUILTIN_SOUL = "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
62
|
+
/** Pi documentation 段模板 —— 与 SDK buildSystemPrompt 默认分支一致(路径由调用方注入)。 */
|
|
63
|
+
export function buildPiDocsText(readme, docs, examples) {
|
|
64
|
+
return [
|
|
65
|
+
"Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
|
|
66
|
+
`- Main documentation: ${readme}`,
|
|
67
|
+
`- Additional docs: ${docs}`,
|
|
68
|
+
`- Examples: ${examples} (extensions, custom tools, SDK)`,
|
|
69
|
+
"- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
|
|
70
|
+
"- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)",
|
|
71
|
+
"- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing",
|
|
72
|
+
"- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
/** Guidelines 段:文件探索引导 + 工具 promptGuidelines(去重)+ 固定两行。
|
|
76
|
+
* 与 buildSystemPrompt 默认分支的聚合规则一致。 */
|
|
77
|
+
function buildGuidelinesText(inputs) {
|
|
78
|
+
const selected = new Set(inputs.selectedTools);
|
|
79
|
+
const lines = [];
|
|
80
|
+
const add = (g) => {
|
|
81
|
+
const t = g.trim();
|
|
82
|
+
if (t && !lines.includes(t))
|
|
83
|
+
lines.push(t);
|
|
84
|
+
};
|
|
85
|
+
const has = (n) => selected.has(n);
|
|
86
|
+
if ((has("bash") || has("powershell")) && !has("grep") && !has("find") && !has("ls")) {
|
|
87
|
+
add(has("bash") && has("powershell")
|
|
88
|
+
? "Use bash or PowerShell for file operations like listing, searching, and finding files"
|
|
89
|
+
: "Use bash for file operations like ls, rg, find");
|
|
90
|
+
}
|
|
91
|
+
for (const g of inputs.toolGuidelines)
|
|
92
|
+
add(g);
|
|
93
|
+
add("Be concise in your responses");
|
|
94
|
+
add("Show file paths clearly when working with files");
|
|
95
|
+
return `Guidelines:\n${lines.map((l) => `- ${l}`).join("\n")}`;
|
|
96
|
+
}
|
|
97
|
+
function escapeXml(s) {
|
|
98
|
+
return s
|
|
99
|
+
.replace(/&/g, "&")
|
|
100
|
+
.replace(/</g, "<")
|
|
101
|
+
.replace(/>/g, ">")
|
|
102
|
+
.replace(/"/g, """)
|
|
103
|
+
.replace(/'/g, "'");
|
|
104
|
+
}
|
|
105
|
+
/** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。 */
|
|
106
|
+
export function buildSkillsText(skills) {
|
|
107
|
+
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
108
|
+
if (visible.length === 0)
|
|
109
|
+
return "";
|
|
110
|
+
const lines = [
|
|
111
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
112
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
113
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
114
|
+
"",
|
|
115
|
+
"<available_skills>",
|
|
116
|
+
];
|
|
117
|
+
for (const skill of visible) {
|
|
118
|
+
lines.push(" <skill>");
|
|
119
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
120
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
121
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
122
|
+
lines.push(" </skill>");
|
|
123
|
+
}
|
|
124
|
+
lines.push("</available_skills>");
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
/** 项目上下文块(不含前导空行)。 */
|
|
128
|
+
function buildContextText(files) {
|
|
129
|
+
if (files.length === 0)
|
|
130
|
+
return "";
|
|
131
|
+
return [
|
|
132
|
+
"<project_context>",
|
|
133
|
+
"",
|
|
134
|
+
"Project-specific instructions and guidelines:",
|
|
135
|
+
"",
|
|
136
|
+
...files.map((f) => `<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>`),
|
|
137
|
+
"",
|
|
138
|
+
"</project_context>",
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
/** 工具列表块:工具列表 + "In addition…" 句。 */
|
|
142
|
+
function buildToolsText(inputs) {
|
|
143
|
+
const visible = inputs.selectedTools.filter((n) => !!inputs.toolSnippets[n]);
|
|
144
|
+
const toolsList = visible.length > 0 ? visible.map((n) => `- ${n}: ${inputs.toolSnippets[n]}`).join("\n") : "(none)";
|
|
145
|
+
return [
|
|
146
|
+
`Available tools:\n${toolsList}`,
|
|
147
|
+
"In addition to the tools above, you may have access to other custom tools depending on the project.",
|
|
148
|
+
].join("\n\n");
|
|
149
|
+
}
|
|
150
|
+
/** 计算每个 token 的自动内容(无覆盖时的展开值)。 */
|
|
151
|
+
export function resolveSectionTexts(inputs) {
|
|
152
|
+
const cwd = inputs.cwd.replace(/\\/g, "/");
|
|
153
|
+
return {
|
|
154
|
+
soul: inputs.systemPromptFile?.trim() ? inputs.systemPromptFile : inputs.builtinSoul,
|
|
155
|
+
tools: buildToolsText(inputs),
|
|
156
|
+
guidelines: buildGuidelinesText(inputs),
|
|
157
|
+
pi_docs: buildPiDocsText(inputs.piReadme, inputs.piDocs, inputs.piExamples),
|
|
158
|
+
append: inputs.appendFiles.join("\n\n"),
|
|
159
|
+
persona: inputs.windowsPersona,
|
|
160
|
+
terminal: inputs.terminalGuidance,
|
|
161
|
+
markers: inputs.markersGuidance,
|
|
162
|
+
context: buildContextText(inputs.contextFiles),
|
|
163
|
+
skills: buildSkillsText(inputs.skills),
|
|
164
|
+
cwd: `Current working directory: ${cwd}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** 渲染模板:{{token}} → 覆盖文本(有)或自动内容(无/空覆盖);未知名 token
|
|
168
|
+
* 保留原文;没有 content 的 token 展开为空串。 */
|
|
169
|
+
export function renderPromptTemplate(template, texts, overrides) {
|
|
170
|
+
return effectiveTemplate(template).replace(TOKEN_RE, (full, name) => {
|
|
171
|
+
const ov = overrideOf(overrides, name);
|
|
172
|
+
if (ov)
|
|
173
|
+
return ov;
|
|
174
|
+
return Object.prototype.hasOwnProperty.call(texts, name) ? (texts[name] ?? "") : full;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** 用默认模板渲染(不覆盖)——等价于「恢复默认」后的成品。 */
|
|
178
|
+
export function renderDefaultPrompt(texts) {
|
|
179
|
+
return renderPromptTemplate(DEFAULT_PROMPT_TEMPLATE, texts, undefined);
|
|
180
|
+
}
|
|
@@ -180,14 +180,19 @@ export class SettingsService {
|
|
|
180
180
|
const extensions = [...this.knownExtensions.values()]
|
|
181
181
|
.map((e) => ({ ...e, enabled: !disabledExts.has(e.id) }))
|
|
182
182
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
183
|
+
// 当前会话提示词快照:完整生效文本 + 各来源默认(自动)内容(只读预览)。
|
|
184
|
+
const promptSnap = this.host.promptSnapshot();
|
|
183
185
|
this.host.emit({
|
|
184
186
|
type: "settings_state",
|
|
185
187
|
settings: {
|
|
186
188
|
promptMode: this.settings.promptMode,
|
|
187
189
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
190
|
+
promptTemplate: this.settings.promptTemplate ?? "",
|
|
191
|
+
promptOverrides: { ...this.settings.promptOverrides },
|
|
188
192
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
189
193
|
terminalBash: this.settings.terminalBash,
|
|
190
194
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
195
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
191
196
|
thinkingWrap: this.settings.thinkingWrap,
|
|
192
197
|
toolsWrap: this.settings.toolsWrap,
|
|
193
198
|
visionBridgeEnabled: this.settings.visionBridgeEnabled,
|
|
@@ -197,11 +202,10 @@ export class SettingsService {
|
|
|
197
202
|
reviewPrompt: this.settings.reviewPrompt,
|
|
198
203
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
199
204
|
disabledPlugins: [...(this.settings.disabledPlugins ?? [])],
|
|
200
|
-
// The
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
effectiveSystemPrompt: this.host.effectiveSystemPrompt(),
|
|
205
|
+
// The composed system prompt actually in effect (read-only view).
|
|
206
|
+
effectiveSystemPrompt: promptSnap.full,
|
|
207
|
+
// 每个来源未覆盖时的默认(自动)内容(「各来源」行预览用)。
|
|
208
|
+
promptSourceDefaults: promptSnap.texts,
|
|
205
209
|
visionBridgeDefaultPrompt: SYSTEM_PROMPT,
|
|
206
210
|
visionModels: this.collectVisionModels(),
|
|
207
211
|
disabledSkills: [...this.settings.disabledSkills],
|
|
@@ -262,18 +266,36 @@ export class SettingsService {
|
|
|
262
266
|
return [];
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
|
-
/** Persist + apply a partial settings update (
|
|
269
|
+
/** Persist + apply a partial settings update (compose template / per-source
|
|
270
|
+
* overrides, skill/extension toggles). */
|
|
266
271
|
async set(partial) {
|
|
267
272
|
const needsReload = partial.promptMode !== undefined ||
|
|
268
273
|
partial.customSystemPrompt !== undefined ||
|
|
274
|
+
partial.promptTemplate !== undefined ||
|
|
275
|
+
partial.promptOverrides !== undefined ||
|
|
269
276
|
partial.disabledSkills !== undefined ||
|
|
270
277
|
partial.disabledExtensions !== undefined ||
|
|
271
|
-
partial.terminalToolsEnabled !== undefined
|
|
278
|
+
partial.terminalToolsEnabled !== undefined ||
|
|
279
|
+
partial.editSoftEnabled !== undefined;
|
|
272
280
|
if (partial.promptMode !== undefined)
|
|
273
281
|
this.settings.promptMode = partial.promptMode;
|
|
274
282
|
if (partial.customSystemPrompt !== undefined) {
|
|
275
283
|
this.settings.customSystemPrompt = partial.customSystemPrompt;
|
|
276
284
|
}
|
|
285
|
+
if (partial.promptTemplate !== undefined) {
|
|
286
|
+
this.settings.promptTemplate = partial.promptTemplate;
|
|
287
|
+
}
|
|
288
|
+
if (partial.promptOverrides !== undefined) {
|
|
289
|
+
// 只合并给出的 key;空串 = 清除该来源覆盖。
|
|
290
|
+
const next = { ...this.settings.promptOverrides };
|
|
291
|
+
for (const [k, v] of Object.entries(partial.promptOverrides)) {
|
|
292
|
+
if (v && v.trim())
|
|
293
|
+
next[k] = v;
|
|
294
|
+
else
|
|
295
|
+
delete next[k];
|
|
296
|
+
}
|
|
297
|
+
this.settings.promptOverrides = next;
|
|
298
|
+
}
|
|
277
299
|
if (partial.disabledSkills !== undefined) {
|
|
278
300
|
this.settings.disabledSkills = partial.disabledSkills;
|
|
279
301
|
}
|
|
@@ -293,6 +315,9 @@ export class SettingsService {
|
|
|
293
315
|
if (partial.terminalBashIdleMs !== undefined) {
|
|
294
316
|
this.settings.terminalBashIdleMs = Math.max(0, Math.floor(partial.terminalBashIdleMs) || 0);
|
|
295
317
|
}
|
|
318
|
+
if (partial.editSoftEnabled !== undefined) {
|
|
319
|
+
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
320
|
+
}
|
|
296
321
|
if (partial.thinkingWrap !== undefined) {
|
|
297
322
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
298
323
|
}
|
|
@@ -343,11 +368,14 @@ export class SettingsService {
|
|
|
343
368
|
name: n,
|
|
344
369
|
promptMode: this.settings.promptMode,
|
|
345
370
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
371
|
+
promptTemplate: this.settings.promptTemplate ?? "",
|
|
372
|
+
promptOverrides: { ...this.settings.promptOverrides },
|
|
346
373
|
disabledSkills: [...this.settings.disabledSkills],
|
|
347
374
|
disabledExtensions: [...this.settings.disabledExtensions],
|
|
348
375
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
349
376
|
terminalBash: this.settings.terminalBash,
|
|
350
377
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
378
|
+
editSoftEnabled: this.settings.editSoftEnabled,
|
|
351
379
|
reviewPrompt: this.settings.reviewPrompt,
|
|
352
380
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
353
381
|
};
|
|
@@ -374,6 +402,8 @@ export class SettingsService {
|
|
|
374
402
|
this.settings = {
|
|
375
403
|
promptMode: p.promptMode,
|
|
376
404
|
customSystemPrompt: p.customSystemPrompt,
|
|
405
|
+
promptTemplate: p.promptTemplate ?? this.settings.promptTemplate ?? "",
|
|
406
|
+
promptOverrides: { ...(p.promptOverrides ?? this.settings.promptOverrides) },
|
|
377
407
|
disabledSkills: [...p.disabledSkills],
|
|
378
408
|
disabledExtensions: [...p.disabledExtensions],
|
|
379
409
|
// 旧版持久化的预设可能没有该字段——保留当前值。
|
|
@@ -381,6 +411,7 @@ export class SettingsService {
|
|
|
381
411
|
// 终端接管偏好随预设走;旧预设缺字段时保留当前值。
|
|
382
412
|
terminalBash: p.terminalBash ?? this.settings.terminalBash,
|
|
383
413
|
terminalBashIdleMs: p.terminalBashIdleMs ?? this.settings.terminalBashIdleMs,
|
|
414
|
+
editSoftEnabled: p.editSoftEnabled ?? this.settings.editSoftEnabled,
|
|
384
415
|
reviewPrompt: p.reviewPrompt ?? this.settings.reviewPrompt,
|
|
385
416
|
reviewDisabledSkills: [...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills)],
|
|
386
417
|
// 纯 UI 偏好不进预设——保留当前值。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 灵魂提示词(persona)替换纯函数。
|
|
3
|
+
*
|
|
4
|
+
* pi 的 buildSystemPrompt(SDK dist/core/system-prompt.js)【默认分支】把系统
|
|
5
|
+
* 提示词拼成:
|
|
6
|
+
*
|
|
7
|
+
* <人物设定/soul>
|
|
8
|
+
* \n\nAvailable tools:\n<工具列表>
|
|
9
|
+
* \n\nIn addition to the tools above…\n\nGuidelines:\n<指南>
|
|
10
|
+
* \n\nPi documentation (…):\n<文档路径提示>
|
|
11
|
+
* [追加段] [<project_context>] [技能段] \nCurrent working directory: <cwd>
|
|
12
|
+
*
|
|
13
|
+
* 「灵魂提示词」= 开头的人物设定段落(内置模板的 "You are an expert coding
|
|
14
|
+
* assistant operating inside pi…")。设置面板的「替换」模式只替换这一段落——
|
|
15
|
+
* 工具列表 / Guidelines / 文档指引 / 项目上下文 / 技能段属于自动拼装段,每次由
|
|
16
|
+
* SDK 用最新数据重新生成,不进入被替换的内容(见 docs/architecture-system-prompt.md)。
|
|
17
|
+
*
|
|
18
|
+
* 这两个纯函数以 "\n\nAvailable tools:" 为边界完成 提取/替换:该边界只在默认
|
|
19
|
+
* 分支出现(存在 SYSTEM.md / 自定义 base 时走 customPrompt 分支、没有工具列表),
|
|
20
|
+
* 因此天然区分「可做灵魂替换」与「base 已整体替换、无需再动」两种情形。
|
|
21
|
+
*/
|
|
22
|
+
/** 默认模板中分割「灵魂提示词」与自动拼装段的固定标记。 */
|
|
23
|
+
export const SYSTEM_PROMPT_SOUL_BOUNDARY = "\n\nAvailable tools:";
|
|
24
|
+
/** 从(默认分支拼出的)系统提示词里提取内置灵魂提示词段落——replace 模式编辑器
|
|
25
|
+
* 的种子。找不到边界(customPrompt 分支)返回 undefined,调用方此时退回
|
|
26
|
+
* SYSTEM.md 文件内容作种子。 */
|
|
27
|
+
export function extractSystemPromptSoul(composed) {
|
|
28
|
+
const boundary = composed.indexOf(SYSTEM_PROMPT_SOUL_BOUNDARY);
|
|
29
|
+
if (boundary === -1)
|
|
30
|
+
return undefined;
|
|
31
|
+
return composed.slice(0, boundary);
|
|
32
|
+
}
|
|
33
|
+
/** 把系统提示词开头的灵魂段落替换为自定义内容,其余自动拼装段原样保留。
|
|
34
|
+
* 找不到边界返回 undefined = 本次替换不适用(调用方原样使用)。 */
|
|
35
|
+
export function swapSystemPromptSoul(composed, soul) {
|
|
36
|
+
const boundary = composed.indexOf(SYSTEM_PROMPT_SOUL_BOUNDARY);
|
|
37
|
+
if (boundary === -1)
|
|
38
|
+
return undefined;
|
|
39
|
+
const next = soul.trimEnd() + composed.slice(boundary);
|
|
40
|
+
return next === composed ? undefined : next;
|
|
41
|
+
}
|
package/dist/server/terminals.js
CHANGED
|
@@ -1542,11 +1542,11 @@ export const TERMINAL_TOOL_NAMES = [
|
|
|
1542
1542
|
/** System-prompt guidance teaching the model WHEN to prefer the terminal tools
|
|
1543
1543
|
* over one-shot bash. Without it models almost never pick them — bash returns
|
|
1544
1544
|
* complete output in a single call, so it always wins on convenience. */
|
|
1545
|
-
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands - it runs in a visible terminal and returns the full output (persist=false, one-shot terminal that exits when the command finishes). Switch to the bash tool's persist=true, or to the terminal tools, when:
|
|
1546
|
-
- The program is interactive or TUI-based (REPLs like python/node, vim/htop, installers asking y/n, anything waiting on stdin). For these, prefer bash({ persist: true }) which runs it in the persistent 'ai-bash' terminal and returns immediately; then drive it with terminal_input / terminal_key (and terminal_read) on terminalId='ai-bash'.
|
|
1547
|
-
- You start a long-running server or watcher and want to keep watching its output (terminal_read with waitMs), send keys to it later (e.g. interrupt via terminal_key with Ctrl+c), or block until a backgrounded command finishes without polling (terminal_wait).
|
|
1548
|
-
- The user explicitly asks you to work in the visible terminal panel.
|
|
1549
|
-
|
|
1545
|
+
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read / terminal_wait). The bash tool stays the DEFAULT for ordinary commands - it runs in a visible terminal and returns the full output (persist=false, one-shot terminal that exits when the command finishes). Switch to the bash tool's persist=true, or to the terminal tools, when:
|
|
1546
|
+
- The program is interactive or TUI-based (REPLs like python/node, vim/htop, installers asking y/n, anything waiting on stdin). For these, prefer bash({ persist: true }) which runs it in the persistent 'ai-bash' terminal and returns immediately; then drive it with terminal_input / terminal_key (and terminal_read) on terminalId='ai-bash'.
|
|
1547
|
+
- You start a long-running server or watcher and want to keep watching its output (terminal_read with waitMs), send keys to it later (e.g. interrupt via terminal_key with Ctrl+c), or block until a backgrounded command finishes without polling (terminal_wait).
|
|
1548
|
+
- The user explicitly asks you to work in the visible terminal panel.
|
|
1549
|
+
Liveness watchdog: terminals you touched (create/input/key) are monitored - if one goes silent with no new output while you are working (default 15s), an automatic system reminder is injected into the conversation. Treat it as a prompt to check that terminal (terminal_read), respond to an input prompt (terminal_input / terminal_key), or close it (terminal_close) if it is no longer needed.`;
|
|
1550
1550
|
/** Build the agent-facing persistent terminal tools for one conversation. */
|
|
1551
1551
|
export function makePersistentTerminalTools(terminals, cwd) {
|
|
1552
1552
|
const result = (text, details = {}) => ({ content: [{ type: "text", text }], details });
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (and an injected pi-core probe); ClientSession only wires it to the wire
|
|
8
8
|
* protocol.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
11
|
import { readdirSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
const PI_CORE_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
@@ -154,32 +154,71 @@ function readLocalPackage(dir) {
|
|
|
154
154
|
return null;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
157
|
+
/** How long a pi probe result stays hot (mirrors ClientSession.piCliProbe). */
|
|
158
|
+
const PI_PROBE_TTL_MS = 10_000;
|
|
159
|
+
let piCoreProbe = null;
|
|
160
|
+
let piCoreProbePending = false;
|
|
161
|
+
function refreshPiCoreProbe() {
|
|
162
|
+
if (piCoreProbePending)
|
|
163
|
+
return;
|
|
164
|
+
piCoreProbePending = true;
|
|
165
|
+
let proc;
|
|
159
166
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
proc = spawn("pi", ["--version"], {
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
+
// Windows: `pi` resolves to a pi.cmd shim — spawn can only
|
|
170
|
+
// exec those through a shell (else ENOENT).
|
|
163
171
|
shell: process.platform === "win32",
|
|
164
172
|
});
|
|
165
|
-
if (res.error || res.status !== 0)
|
|
166
|
-
return null;
|
|
167
|
-
return parsePiVersionOutput(res.stdout?.toString() ?? "");
|
|
168
173
|
}
|
|
169
174
|
catch {
|
|
170
|
-
|
|
175
|
+
piCoreProbe = { at: Date.now(), version: null };
|
|
176
|
+
piCoreProbePending = false;
|
|
177
|
+
return;
|
|
171
178
|
}
|
|
179
|
+
let out = "";
|
|
180
|
+
const finish = (version) => {
|
|
181
|
+
piCoreProbe = { at: Date.now(), version };
|
|
182
|
+
piCoreProbePending = false;
|
|
183
|
+
};
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
try {
|
|
186
|
+
proc.kill();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* already exited */
|
|
190
|
+
}
|
|
191
|
+
finish(null);
|
|
192
|
+
}, 5000);
|
|
193
|
+
proc.stdout?.on("data", (d) => (out += d.toString()));
|
|
194
|
+
proc.on("error", () => {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
finish(null);
|
|
197
|
+
});
|
|
198
|
+
proc.on("close", (code) => {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
finish(code === 0 ? parsePiVersionOutput(out) : null);
|
|
201
|
+
});
|
|
172
202
|
}
|
|
173
|
-
/** How long a pi probe result stays hot (mirrors ClientSession.piCliProbe). */
|
|
174
|
-
const PI_PROBE_TTL_MS = 10_000;
|
|
175
203
|
/**
|
|
176
204
|
* Default pi core probe: run the globally installed `pi --version`, memoized
|
|
177
205
|
* machine-wide for PI_PROBE_TTL_MS so repeated collectTargets calls never
|
|
178
|
-
* re-
|
|
179
|
-
*
|
|
180
|
-
*
|
|
206
|
+
* re-probe. Serves the last known value and refreshes ASYNCHRONOUSLY in the
|
|
207
|
+
* background — never blocks the event loop. (It previously used spawnSync
|
|
208
|
+
* here, which can deadlock the whole server on Android/Termux: fork() in a
|
|
209
|
+
* multi-threaded process occasionally leaves the forked child stuck between
|
|
210
|
+
* fork and exec while the main thread sits in spawnSync's pipe_read. Mirrors
|
|
211
|
+
* ClientSession.isPiCliInstalled(); Windows resolves `pi` to a pi.cmd shim
|
|
212
|
+
* that only execs through a shell.)
|
|
181
213
|
*/
|
|
182
|
-
export
|
|
214
|
+
export function defaultProbePiCore() {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const cached = piCoreProbe;
|
|
217
|
+
if (cached && now - cached.at < PI_PROBE_TTL_MS)
|
|
218
|
+
return cached.version;
|
|
219
|
+
refreshPiCoreProbe();
|
|
220
|
+
return cached?.version ?? null;
|
|
221
|
+
}
|
|
183
222
|
/**
|
|
184
223
|
* Fallback when the CLI probe yields nothing: the version of the vendored pi
|
|
185
224
|
* core copy in <agentDir>/npm/node_modules, or null if that is absent too.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as l,j as n}from"./markdown-DRBrS2Nf.js";import{u as O,b as M,T as z,a as W,F as Y,c as H,d as Z,e as q,f as ee,g as ne,h as te,i as se,r as ae}from"./index-
|
|
1
|
+
import{a as l,j as n}from"./markdown-DRBrS2Nf.js";import{u as O,b as M,T as z,a as W,F as Y,c as H,d as Z,e as q,f as ee,g as ne,h as te,i as se,r as ae}from"./index-CqXS2SQU.js";import{D as re,o as ie}from"./xterm-D1D2FVe3.js";import"./react-C9ovnpIm.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,h)=>(c=Number(h),`\r
|
|
2
2
|
`)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
|
|
3
3
|
`),exitCode:c}}function ce({conversationId:t,terminalId:c,command:f,cwd:a,title:h,active:u,running:b,exitCode:y,send:j,register:T}){const k=l.useRef(null),N=l.useRef(null),{locale:o}=O(),w=l.useRef(o);w.current=o;const F=f?JSON.stringify(f):"";l.useEffect(()=>{const m=k.current;if(!m)return;const r=new re({theme:M(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),g=new ie;r.loadAddon(g),r.open(m),N.current={term:r,fit:g},u&&r.focus();const C=()=>{r.options.theme=M()};window.addEventListener(z,C),r.attachCustomKeyEventHandler(d=>{var A;if(d.type!=="keydown")return!0;const D=(A=d.key)==null?void 0:A.toLowerCase();if((d.ctrlKey||d.metaKey)&&D==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&D==="c"&&r.hasSelection()){const E=r.textarea;return E&&(E.value=r.getSelection(),E.select()),!1}return!0});const _=T(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{g.fit(),j({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{g.fit()}catch{}j(f?{type:"run_command",terminalId:c,conversationId:t,command:f,cols:r.cols,rows:r.rows}:{type:"terminal_create",terminalId:c,title:h,locale:w.current,conversationId:t,cwd:a,cols:r.cols,rows:r.rows})}),S=r.onData(d=>{j({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let v=null;return typeof ResizeObserver<"u"&&(v=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),v.observe(m)),()=>{cancelAnimationFrame(B),S.dispose(),window.removeEventListener(z,C),v==null||v.disconnect(),_(),r.dispose(),N.current=null}},[t,c,F,j,T]),l.useEffect(()=>{if(!u)return;const m=requestAnimationFrame(()=>{const r=N.current;if(r){try{r.fit.fit(),j({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[u]);const{t:R}=O(),x=l.useRef(void 0);return l.useEffect(()=>{if(b===x.current||(x.current=b,b!==!1))return;const m=N.current;m&&m.term.write(`\r
|
|
4
4
|
\x1B[90m${R("exitBanner",{code:y??""})}\x1B[0m\r
|