pi-web-ui 0.20.0 → 0.21.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.
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync, watch, } from "node:fs";
|
|
15
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createLocalBashOperations, defineTool, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { Type } from "typebox";
|
|
@@ -225,14 +225,14 @@ function decodeText(buf) {
|
|
|
225
225
|
* programs wait for input that never comes. Legacy Chinese files are often
|
|
226
226
|
* GBK/GB2312 — read them with the right encoding, never paste mojibake into
|
|
227
227
|
* reasoning/answers. */
|
|
228
|
-
const WINDOWS_PERSONA = `You are a coding agent running on Windows. The bash tool runs Git Bash (bash.exe), not PowerShell. Follow these rules to avoid hanging the session:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- ALWAYS pass a timeout parameter to the bash tool (in seconds). There is NO default timeout — a command that never finishes (servers, watchers, infinite loops, slow downloads/installs) will hang the entire conversation indefinitely. Pick a generous timeout for long-running work, but never omit it.
|
|
233
|
-
- NEVER run interactive or foreground long-running commands through the bash tool (vi, less, top, python -, node -, npm run dev, sleep 10000). For servers/daemons use background execution with output redirected to a log file, then poll the log; stop them when done.
|
|
234
|
-
- 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\`).
|
|
235
|
-
|
|
228
|
+
const WINDOWS_PERSONA = `You are a coding agent running on Windows. The bash tool runs Git Bash (bash.exe), not PowerShell. Follow these rules to avoid hanging the session:
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
- ALWAYS pass a timeout parameter to the bash tool (in seconds). There is NO default timeout — a command that never finishes (servers, watchers, infinite loops, slow downloads/installs) will hang the entire conversation indefinitely. Pick a generous timeout for long-running work, but never omit it.
|
|
233
|
+
- NEVER run interactive or foreground long-running commands through the bash tool (vi, less, top, python -, node -, npm run dev, sleep 10000). For servers/daemons use background execution with output redirected to a log file, then poll the log; stop them when done.
|
|
234
|
+
- 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\`).
|
|
235
|
+
|
|
236
236
|
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.`;
|
|
237
237
|
/**
|
|
238
238
|
* Killable bash tool: wraps the SDK bash tool with operations that register
|
|
@@ -750,6 +750,15 @@ async function readDirForUI(abs, rel) {
|
|
|
750
750
|
out.length = MAX;
|
|
751
751
|
return { entries: out, truncated };
|
|
752
752
|
}
|
|
753
|
+
/** Stable identity of an extension for the enable/disable toggle: the npm
|
|
754
|
+
* spec for packages (survives version bumps), the resolved entry path
|
|
755
|
+
* otherwise. */
|
|
756
|
+
function extensionKey(e) {
|
|
757
|
+
const src = e.sourceInfo;
|
|
758
|
+
if (src?.origin === "package" && src.source)
|
|
759
|
+
return src.source;
|
|
760
|
+
return src?.path ?? e.path;
|
|
761
|
+
}
|
|
753
762
|
/**
|
|
754
763
|
* Persists which workspace each browser client last used + which workspaces it
|
|
755
764
|
* has opened, so a server restart / page reload restores the same project and
|
|
@@ -820,6 +829,40 @@ class ClientStateStore {
|
|
|
820
829
|
};
|
|
821
830
|
this.save();
|
|
822
831
|
}
|
|
832
|
+
/** Last-used settings-panel state for a client, or defaults. */
|
|
833
|
+
getSettings(clientId) {
|
|
834
|
+
const s = this.load()[clientId];
|
|
835
|
+
return {
|
|
836
|
+
promptMode: s?.settings?.promptMode === "replace" ? "replace" : "append",
|
|
837
|
+
customSystemPrompt: s?.settings?.customSystemPrompt ?? "",
|
|
838
|
+
disabledSkills: s?.settings?.disabledSkills ?? [],
|
|
839
|
+
disabledExtensions: s?.settings?.disabledExtensions ?? [],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/** Persist the client's settings-panel state (partial merge). */
|
|
843
|
+
saveSettings(clientId, settings) {
|
|
844
|
+
const all = this.load();
|
|
845
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
846
|
+
const cur = state.settings ?? {};
|
|
847
|
+
state.settings = {
|
|
848
|
+
promptMode: settings.promptMode ?? cur.promptMode ?? "append",
|
|
849
|
+
customSystemPrompt: settings.customSystemPrompt ?? cur.customSystemPrompt ?? "",
|
|
850
|
+
disabledSkills: settings.disabledSkills ?? cur.disabledSkills ?? [],
|
|
851
|
+
disabledExtensions: settings.disabledExtensions ?? cur.disabledExtensions ?? [],
|
|
852
|
+
};
|
|
853
|
+
this.save();
|
|
854
|
+
}
|
|
855
|
+
/** Named settings presets for a client (empty if never saved). */
|
|
856
|
+
getPresets(clientId) {
|
|
857
|
+
return this.load()[clientId]?.presets ?? [];
|
|
858
|
+
}
|
|
859
|
+
/** Persist the client's named settings presets. */
|
|
860
|
+
savePresets(clientId, presets) {
|
|
861
|
+
const all = this.load();
|
|
862
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
863
|
+
state.presets = presets;
|
|
864
|
+
this.save();
|
|
865
|
+
}
|
|
823
866
|
}
|
|
824
867
|
/** Hard cap on how long ONE tool call may run before the watchdog aborts the
|
|
825
868
|
* session. The SDK bash tool has NO default timeout, so a command that never
|
|
@@ -914,6 +957,20 @@ export class ClientSession {
|
|
|
914
957
|
/** Guard: the goal wizard and the review loop are mutually exclusive — a
|
|
915
958
|
* wizard in flight stops review triggers (and vice versa). */
|
|
916
959
|
goalWizardRunning = false;
|
|
960
|
+
/** Settings-panel state (system prompt + disabled skills/extensions). The
|
|
961
|
+
* resource-loader overrides in makeRuntimeFactory() read this at every
|
|
962
|
+
* reload(), so session.reload() applies changes to the running runtime. */
|
|
963
|
+
settings;
|
|
964
|
+
/** Named settings presets (saved combos the user can re-apply). */
|
|
965
|
+
presets = [];
|
|
966
|
+
/** Settings changed while the run was streaming — the runtime reload is
|
|
967
|
+
* deferred to the next agent_end so an in-flight run is never torn down. */
|
|
968
|
+
pendingSettingsReload = false;
|
|
969
|
+
/** Full (incl. disabled) skill/extension lists seen so far — disabled
|
|
970
|
+
* entries disappear from the loader after reload, so this cache keeps them
|
|
971
|
+
* visible (and re-enableable) in the settings panel. */
|
|
972
|
+
knownSkills = new Map();
|
|
973
|
+
knownExtensions = new Map();
|
|
917
974
|
/** Aborts the currently-running goal wizard (user clicked ✗ / timed out). Drives
|
|
918
975
|
* the in-flight goal_ask dialog to resolve as cancelled and (via the run
|
|
919
976
|
* signal) stops the wizard session's agent run. Recreated per wizard. */
|
|
@@ -993,6 +1050,8 @@ export class ClientSession {
|
|
|
993
1050
|
this.cwd = cwd;
|
|
994
1051
|
this.agentDir = agentDir;
|
|
995
1052
|
this.stateStore = stateStore;
|
|
1053
|
+
this.settings = stateStore.getSettings(clientId);
|
|
1054
|
+
this.presets = stateStore.getPresets(clientId);
|
|
996
1055
|
}
|
|
997
1056
|
static async create(clientId, cwd, stateStore) {
|
|
998
1057
|
const agentDir = process.env.PI_CODING_AGENT_DIR ?? getAgentDir();
|
|
@@ -1040,16 +1099,41 @@ export class ClientSession {
|
|
|
1040
1099
|
const services = await createAgentSessionServices({
|
|
1041
1100
|
cwd: effectiveCwd,
|
|
1042
1101
|
modelRuntime: this.sharedModelRuntime,
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1102
|
+
// 设置面板钩子(官方 SDK 的 resourceLoader overrides):三个 override
|
|
1103
|
+
// 在每次 resourceLoader.reload() 时重放,且读取 this.settings 的当前
|
|
1104
|
+
// 值——因此 session.reload() 即可让系统提示词 / 技能 / 插件开关生效,
|
|
1105
|
+
// 新对话(新 runtime)也会自动带上当前设置。
|
|
1106
|
+
resourceLoaderOptions: {
|
|
1107
|
+
// 系统提示词:replace 模式整体替换;append 模式追加到提示词末尾。
|
|
1108
|
+
systemPromptOverride: (base) => this.settings.promptMode === "replace" &&
|
|
1109
|
+
this.settings.customSystemPrompt.trim()
|
|
1110
|
+
? this.settings.customSystemPrompt
|
|
1111
|
+
: base,
|
|
1112
|
+
appendSystemPromptOverride: (base) => {
|
|
1113
|
+
const out = [...base];
|
|
1114
|
+
const custom = this.settings.customSystemPrompt.trim();
|
|
1115
|
+
if (this.settings.promptMode === "append" && custom) {
|
|
1116
|
+
out.push(custom);
|
|
1117
|
+
}
|
|
1118
|
+
if (process.platform === "win32") {
|
|
1119
|
+
// Windows 专属 persona:bash 工具跑 Git Bash 且无默认超时、终端
|
|
1120
|
+
// 是交互式 TTY——注入约束避免 heredoc/交互/长驻命令挂死整个会话;
|
|
1121
|
+
// GBK 老中文文件让模型改用终端按正确编码读(iconv/chcp/Get-Content)。
|
|
1122
|
+
out.push(WINDOWS_PERSONA);
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1125
|
+
},
|
|
1126
|
+
// 技能开关:禁用的技能从系统提示词和 /skill: 目录中剔除。
|
|
1127
|
+
skillsOverride: (res) => ({
|
|
1128
|
+
...res,
|
|
1129
|
+
skills: res.skills.filter((s) => !this.settings.disabledSkills.includes(s.name)),
|
|
1130
|
+
}),
|
|
1131
|
+
// 插件开关:禁用的扩展整个卸载(工具 / 命令随之消失)。
|
|
1132
|
+
extensionsOverride: (res) => ({
|
|
1133
|
+
...res,
|
|
1134
|
+
extensions: res.extensions.filter((e) => !this.settings.disabledExtensions.includes(extensionKey(e))),
|
|
1135
|
+
}),
|
|
1136
|
+
},
|
|
1053
1137
|
});
|
|
1054
1138
|
return {
|
|
1055
1139
|
...(await createAgentSessionFromServices({
|
|
@@ -1117,6 +1201,9 @@ export class ClientSession {
|
|
|
1117
1201
|
// Reconnect: push the remembered goal prefs (model choice, rounds cap,
|
|
1118
1202
|
// locked) so the goal bar restores them on reload — "全局记忆".
|
|
1119
1203
|
this.emitGoalStatus();
|
|
1204
|
+
// Reconnect: push the settings panel state (prompt text/mode, skill &
|
|
1205
|
+
// extension toggles, saved presets).
|
|
1206
|
+
this.pushSettings();
|
|
1120
1207
|
}
|
|
1121
1208
|
detachSink(send) {
|
|
1122
1209
|
this.sinks.delete(send);
|
|
@@ -1327,6 +1414,13 @@ export class ClientSession {
|
|
|
1327
1414
|
!this.disposed) {
|
|
1328
1415
|
void this.runGoalReview(conv);
|
|
1329
1416
|
}
|
|
1417
|
+
// Deferred settings reload: settings (system prompt / skills /
|
|
1418
|
+
// extensions) changed while the run was streaming — applying now
|
|
1419
|
+
// would have torn down the in-flight run.
|
|
1420
|
+
if (this.pendingSettingsReload && !this.disposed) {
|
|
1421
|
+
this.pendingSettingsReload = false;
|
|
1422
|
+
void this.applySettingsReload();
|
|
1423
|
+
}
|
|
1330
1424
|
break;
|
|
1331
1425
|
}
|
|
1332
1426
|
case "entry_appended":
|
|
@@ -1565,6 +1659,8 @@ export class ClientSession {
|
|
|
1565
1659
|
* the process is going to restart itself (so the notice can say so).
|
|
1566
1660
|
*/
|
|
1567
1661
|
onUpdateReady = undefined;
|
|
1662
|
+
/** Set by index.ts: called when /pi-web-ui:quit is invoked. */
|
|
1663
|
+
onQuit = undefined;
|
|
1568
1664
|
/** Ask the npm registry for the latest pi-web-ui version and report it. */
|
|
1569
1665
|
async checkUpdate() {
|
|
1570
1666
|
const current = ClientSession.currentAppVersion();
|
|
@@ -2038,19 +2134,22 @@ export class ClientSession {
|
|
|
2038
2134
|
* (they never reach the server) but stay listed so the picker shows them.
|
|
2039
2135
|
*/
|
|
2040
2136
|
static NATIVE_COMMANDS = [
|
|
2041
|
-
{ name: "new", description: "新建对话" },
|
|
2042
|
-
{ name: "model", description: "切换模型", argumentHint: "[名称]" },
|
|
2043
|
-
{ name: "compact", description: "压缩上下文", argumentHint: "[说明]" },
|
|
2044
|
-
{ name: "cwd", description: "切换工作目录", argumentHint: "<路径>" },
|
|
2137
|
+
{ name: "new", description: "新建对话", descriptionEn: "New chat" },
|
|
2138
|
+
{ name: "model", description: "切换模型", descriptionEn: "Switch model", argumentHint: "[名称]", argumentHintEn: "[name]" },
|
|
2139
|
+
{ name: "compact", description: "压缩上下文", descriptionEn: "Compact context", argumentHint: "[说明]", argumentHintEn: "[instructions]" },
|
|
2140
|
+
{ name: "cwd", description: "切换工作目录", descriptionEn: "Switch workspace", argumentHint: "<路径>", argumentHintEn: "<path>" },
|
|
2045
2141
|
{
|
|
2046
2142
|
name: "thinking",
|
|
2047
2143
|
description: "设置思考强度",
|
|
2048
|
-
|
|
2144
|
+
descriptionEn: "Set thinking level",
|
|
2145
|
+
argumentHint: "<off|low|medium|high|xhigh|max>",
|
|
2146
|
+
argumentHintEn: "<off|low|medium|high|xhigh|max>",
|
|
2049
2147
|
},
|
|
2050
|
-
{ name: "resume", description: "刷新会话列表" },
|
|
2051
|
-
{ name: "reload", description: "重新加载扩展、技能与模板" },
|
|
2052
|
-
{ name: "help", description: "显示全部命令" },
|
|
2053
|
-
{ name: "copy", description: "复制上一条助手回复" },
|
|
2148
|
+
{ name: "resume", description: "刷新会话列表", descriptionEn: "Refresh session list" },
|
|
2149
|
+
{ name: "reload", description: "重新加载扩展、技能与模板", descriptionEn: "Reload extensions, skills & templates" },
|
|
2150
|
+
{ name: "help", description: "显示全部命令", descriptionEn: "Show all commands" },
|
|
2151
|
+
{ name: "copy", description: "复制上一条助手回复", descriptionEn: "Copy last assistant reply" },
|
|
2152
|
+
{ name: "pi-web-ui:quit", description: "退出服务", descriptionEn: "Quit server (supervisor will restart)" },
|
|
2054
2153
|
];
|
|
2055
2154
|
/** Parse a prompt into "/command args" — returns null when it isn't one. */
|
|
2056
2155
|
parseSlash(text) {
|
|
@@ -2190,6 +2289,20 @@ export class ClientSession {
|
|
|
2190
2289
|
});
|
|
2191
2290
|
}
|
|
2192
2291
|
return true;
|
|
2292
|
+
case "pi-web-ui:quit": {
|
|
2293
|
+
this.emit({
|
|
2294
|
+
type: "notice",
|
|
2295
|
+
level: "info",
|
|
2296
|
+
text: "正在退出 pi-web-ui… supervisor 将自动重启服务",
|
|
2297
|
+
});
|
|
2298
|
+
setTimeout(() => {
|
|
2299
|
+
const didSchedule = this.onQuit?.() ?? false;
|
|
2300
|
+
if (!didSchedule) {
|
|
2301
|
+
setTimeout(() => process.exit(0), 100);
|
|
2302
|
+
}
|
|
2303
|
+
}, 300);
|
|
2304
|
+
return true;
|
|
2305
|
+
}
|
|
2193
2306
|
case "help":
|
|
2194
2307
|
case "copy":
|
|
2195
2308
|
// Client-side UI actions — the client handles them before sending;
|
|
@@ -2256,6 +2369,174 @@ export class ClientSession {
|
|
|
2256
2369
|
this.emit({ type: "slash_commands", commands });
|
|
2257
2370
|
}
|
|
2258
2371
|
// ---------------------------------------------------------------------------
|
|
2372
|
+
// Settings (system prompt / skills / extensions / presets)
|
|
2373
|
+
// ---------------------------------------------------------------------------
|
|
2374
|
+
/** Push the full settings state (current settings + loaded skills/extensions
|
|
2375
|
+
* with enabled flags + saved presets). Pushed on attach and after every
|
|
2376
|
+
* settings change. */
|
|
2377
|
+
pushSettings() {
|
|
2378
|
+
const disabledSkills = new Set(this.settings.disabledSkills);
|
|
2379
|
+
const disabledExts = new Set(this.settings.disabledExtensions);
|
|
2380
|
+
try {
|
|
2381
|
+
// Refresh the cache with the CURRENTLY loaded set (post-filter).
|
|
2382
|
+
for (const s of this.session.resourceLoader.getSkills().skills) {
|
|
2383
|
+
this.knownSkills.set(s.name, {
|
|
2384
|
+
name: s.name,
|
|
2385
|
+
description: s.description,
|
|
2386
|
+
enabled: true,
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
for (const e of this.session.resourceLoader.getExtensions().extensions) {
|
|
2390
|
+
const id = extensionKey(e);
|
|
2391
|
+
const p = e.sourceInfo?.path ?? e.path;
|
|
2392
|
+
this.knownExtensions.set(id, {
|
|
2393
|
+
id,
|
|
2394
|
+
name: e.sourceInfo?.origin === "package" && e.sourceInfo.source
|
|
2395
|
+
? e.sourceInfo.source
|
|
2396
|
+
: basename(p),
|
|
2397
|
+
path: p,
|
|
2398
|
+
enabled: true,
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
catch {
|
|
2403
|
+
// Session not ready yet — keep whatever we already know.
|
|
2404
|
+
}
|
|
2405
|
+
// Disabled entries are filtered out of the loader — keep them in the
|
|
2406
|
+
// panel (with the last-known description) so they can be re-enabled.
|
|
2407
|
+
for (const name of this.settings.disabledSkills) {
|
|
2408
|
+
if (!this.knownSkills.has(name)) {
|
|
2409
|
+
this.knownSkills.set(name, { name, description: "", enabled: false });
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
for (const id of this.settings.disabledExtensions) {
|
|
2413
|
+
if (!this.knownExtensions.has(id)) {
|
|
2414
|
+
this.knownExtensions.set(id, {
|
|
2415
|
+
id,
|
|
2416
|
+
name: id.startsWith("npm:") ? id : basename(id),
|
|
2417
|
+
path: "",
|
|
2418
|
+
enabled: false,
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
const skills = [...this.knownSkills.values()]
|
|
2423
|
+
.map((s) => ({ ...s, enabled: !disabledSkills.has(s.name) }))
|
|
2424
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2425
|
+
const extensions = [...this.knownExtensions.values()]
|
|
2426
|
+
.map((e) => ({ ...e, enabled: !disabledExts.has(e.id) }))
|
|
2427
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2428
|
+
this.emit({
|
|
2429
|
+
type: "settings_state",
|
|
2430
|
+
settings: {
|
|
2431
|
+
promptMode: this.settings.promptMode,
|
|
2432
|
+
customSystemPrompt: this.settings.customSystemPrompt,
|
|
2433
|
+
disabledSkills: [...this.settings.disabledSkills],
|
|
2434
|
+
disabledExtensions: [...this.settings.disabledExtensions],
|
|
2435
|
+
skills,
|
|
2436
|
+
extensions,
|
|
2437
|
+
presets: this.presets.map((p) => ({ ...p })),
|
|
2438
|
+
},
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
/** Persist + apply a partial settings update (prompt text/mode, toggles). */
|
|
2442
|
+
async setSettings(partial) {
|
|
2443
|
+
if (partial.promptMode !== undefined)
|
|
2444
|
+
this.settings.promptMode = partial.promptMode;
|
|
2445
|
+
if (partial.customSystemPrompt !== undefined) {
|
|
2446
|
+
this.settings.customSystemPrompt = partial.customSystemPrompt;
|
|
2447
|
+
}
|
|
2448
|
+
if (partial.disabledSkills !== undefined) {
|
|
2449
|
+
this.settings.disabledSkills = partial.disabledSkills;
|
|
2450
|
+
}
|
|
2451
|
+
if (partial.disabledExtensions !== undefined) {
|
|
2452
|
+
this.settings.disabledExtensions = partial.disabledExtensions;
|
|
2453
|
+
}
|
|
2454
|
+
this.stateStore.saveSettings(this.clientId, this.settings);
|
|
2455
|
+
this.pushSettings();
|
|
2456
|
+
await this.applyRuntimeSettings();
|
|
2457
|
+
}
|
|
2458
|
+
/** Save the CURRENT settings as a named preset (overwrites if exists). */
|
|
2459
|
+
async savePreset(name) {
|
|
2460
|
+
const n = name.trim();
|
|
2461
|
+
if (!n) {
|
|
2462
|
+
this.emit({ type: "notice", level: "error", text: "预设名称不能为空" });
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const preset = {
|
|
2466
|
+
name: n,
|
|
2467
|
+
promptMode: this.settings.promptMode,
|
|
2468
|
+
customSystemPrompt: this.settings.customSystemPrompt,
|
|
2469
|
+
disabledSkills: [...this.settings.disabledSkills],
|
|
2470
|
+
disabledExtensions: [...this.settings.disabledExtensions],
|
|
2471
|
+
};
|
|
2472
|
+
const existing = this.presets.findIndex((p) => p.name === n);
|
|
2473
|
+
if (existing >= 0)
|
|
2474
|
+
this.presets[existing] = preset;
|
|
2475
|
+
else
|
|
2476
|
+
this.presets.push(preset);
|
|
2477
|
+
this.stateStore.savePresets(this.clientId, this.presets);
|
|
2478
|
+
this.pushSettings();
|
|
2479
|
+
}
|
|
2480
|
+
/** Replace the current settings with the named preset and apply it. */
|
|
2481
|
+
async applyPreset(name) {
|
|
2482
|
+
const p = this.presets.find((x) => x.name === name);
|
|
2483
|
+
if (!p) {
|
|
2484
|
+
this.emit({ type: "notice", level: "error", text: `预设不存在:${name}` });
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
this.settings = {
|
|
2488
|
+
promptMode: p.promptMode,
|
|
2489
|
+
customSystemPrompt: p.customSystemPrompt,
|
|
2490
|
+
disabledSkills: [...p.disabledSkills],
|
|
2491
|
+
disabledExtensions: [...p.disabledExtensions],
|
|
2492
|
+
};
|
|
2493
|
+
this.stateStore.saveSettings(this.clientId, this.settings);
|
|
2494
|
+
this.pushSettings();
|
|
2495
|
+
await this.applyRuntimeSettings();
|
|
2496
|
+
}
|
|
2497
|
+
/** Remove a named preset. */
|
|
2498
|
+
async deletePreset(name) {
|
|
2499
|
+
this.presets = this.presets.filter((p) => p.name !== name);
|
|
2500
|
+
this.stateStore.savePresets(this.clientId, this.presets);
|
|
2501
|
+
this.pushSettings();
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Make settings changes effective in the running runtime. The resource-loader
|
|
2505
|
+
* overrides read this.settings at call time, so a reload re-applies them.
|
|
2506
|
+
* Reloading mid-stream would tear down the in-flight run — defer instead.
|
|
2507
|
+
*/
|
|
2508
|
+
async applyRuntimeSettings() {
|
|
2509
|
+
if (this.disposed)
|
|
2510
|
+
return;
|
|
2511
|
+
if (this.session.isStreaming) {
|
|
2512
|
+
this.pendingSettingsReload = true;
|
|
2513
|
+
this.emit({
|
|
2514
|
+
type: "notice",
|
|
2515
|
+
level: "info",
|
|
2516
|
+
text: "当前回复进行中,设置将在回复结束后自动应用",
|
|
2517
|
+
});
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
await this.applySettingsReload();
|
|
2521
|
+
}
|
|
2522
|
+
/** session.reload() + refresh the slash-command catalog + push state. */
|
|
2523
|
+
async applySettingsReload() {
|
|
2524
|
+
try {
|
|
2525
|
+
await this.session.reload();
|
|
2526
|
+
await this.pushSlashCommands();
|
|
2527
|
+
this.pushSettings();
|
|
2528
|
+
this.flushSnapshot();
|
|
2529
|
+
this.emit({ type: "notice", level: "info", text: "设置已应用" });
|
|
2530
|
+
}
|
|
2531
|
+
catch (err) {
|
|
2532
|
+
this.emit({
|
|
2533
|
+
type: "notice",
|
|
2534
|
+
level: "error",
|
|
2535
|
+
text: `设置应用失败:${err.message}`,
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
// ---------------------------------------------------------------------------
|
|
2259
2540
|
// Commands
|
|
2260
2541
|
// ---------------------------------------------------------------------------
|
|
2261
2542
|
async prompt(text, attachments) {
|
|
@@ -4349,6 +4630,8 @@ export class AgentService {
|
|
|
4349
4630
|
* self-update; returns whether the process will restart itself.
|
|
4350
4631
|
*/
|
|
4351
4632
|
onUpdateReady = undefined;
|
|
4633
|
+
/** Set by index.ts: called when /pi-web-ui:quit is invoked. */
|
|
4634
|
+
onQuit = undefined;
|
|
4352
4635
|
constructor(cwd, stateFile) {
|
|
4353
4636
|
this.cwd = cwd;
|
|
4354
4637
|
this.stateStore = new ClientStateStore(stateFile);
|
|
@@ -4394,8 +4677,9 @@ export class AgentService {
|
|
|
4394
4677
|
}
|
|
4395
4678
|
}
|
|
4396
4679
|
cs.attachSink(send);
|
|
4397
|
-
// Forward
|
|
4680
|
+
// Forward hooks (set once by index.ts) to every session.
|
|
4398
4681
|
cs.onUpdateReady = this.onUpdateReady;
|
|
4682
|
+
cs.onQuit = this.onQuit;
|
|
4399
4683
|
return cs;
|
|
4400
4684
|
}
|
|
4401
4685
|
/** Remove a socket from a client's broadcast set (called on socket close). */
|
package/dist/server/index.js
CHANGED
|
@@ -189,6 +189,26 @@ function scheduleUpdateRestart() {
|
|
|
189
189
|
return true;
|
|
190
190
|
}
|
|
191
191
|
service.onUpdateReady = scheduleUpdateRestart;
|
|
192
|
+
function scheduleQuit() {
|
|
193
|
+
const isLaunchd = process.platform === "darwin" && process.ppid === 1;
|
|
194
|
+
const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
|
|
195
|
+
const inDocker = existsSync("/.dockerenv");
|
|
196
|
+
if (isLaunchd || isSystemd || inDocker) {
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
console.log("pi-web-ui:quit — shutting down (supervisor will restart)…");
|
|
199
|
+
if (isSystemd)
|
|
200
|
+
process.exit(3);
|
|
201
|
+
void shutdown();
|
|
202
|
+
}, 300);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
console.log("pi-web-ui:quit — shutting down (restart to reload)…");
|
|
207
|
+
void shutdown();
|
|
208
|
+
}, 300);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
service.onQuit = scheduleQuit;
|
|
192
212
|
wss.on("connection", (ws) => {
|
|
193
213
|
let clientId = null;
|
|
194
214
|
let closed = false;
|
|
@@ -343,6 +363,26 @@ wss.on("connection", (ws) => {
|
|
|
343
363
|
locked: msg.locked,
|
|
344
364
|
});
|
|
345
365
|
break;
|
|
366
|
+
case "get_settings":
|
|
367
|
+
cs.pushSettings();
|
|
368
|
+
break;
|
|
369
|
+
case "set_settings":
|
|
370
|
+
void cs.setSettings({
|
|
371
|
+
promptMode: msg.promptMode,
|
|
372
|
+
customSystemPrompt: msg.customSystemPrompt,
|
|
373
|
+
disabledSkills: msg.disabledSkills,
|
|
374
|
+
disabledExtensions: msg.disabledExtensions,
|
|
375
|
+
});
|
|
376
|
+
break;
|
|
377
|
+
case "save_preset":
|
|
378
|
+
void cs.savePreset(msg.name);
|
|
379
|
+
break;
|
|
380
|
+
case "apply_preset":
|
|
381
|
+
void cs.applyPreset(msg.name);
|
|
382
|
+
break;
|
|
383
|
+
case "delete_preset":
|
|
384
|
+
void cs.deletePreset(msg.name);
|
|
385
|
+
break;
|
|
346
386
|
default:
|
|
347
387
|
break;
|
|
348
388
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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
|
"type": "module",
|