pi-web-ui 0.29.3 → 0.31.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 +57 -19
- package/dist/server/client-state.js +31 -0
- package/dist/server/index.js +4 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/settings-service.js +9 -1
- package/dist/server/slash-commands.js +2 -0
- package/dist/server/terminals.js +19 -2
- package/dist/server/themes.js +20 -2
- package/package.json +96 -96
- package/themes/light.css +6965 -6749
- package/themes/md-preview.css +6911 -0
- package/themes/white.css +6972 -0
- package/web/dist/assets/{TerminalPanel-D7lQ3g5k.js → TerminalPanel-CVOwLwwF.js} +1 -1
- package/web/dist/assets/index-BYW5b0ZE.js +19 -0
- package/web/dist/assets/index-C9LigvHH.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BiJTtKxQ.js +0 -18
- package/web/dist/assets/index-DxO8_hl1.css +0 -10
|
@@ -22,9 +22,9 @@ import { GoalService } from "./goal-service.js";
|
|
|
22
22
|
import { SlashCommandsService, parseSlash } from "./slash-commands.js";
|
|
23
23
|
import { ModelAdminService } from "./model-admin.js";
|
|
24
24
|
import { FilesService, workspacePath } from "./files-service.js";
|
|
25
|
-
import {
|
|
25
|
+
import { isExtensionDisabled, ClientStateStore, } from "./client-state.js";
|
|
26
26
|
import { saveUpload } from "./uploads.js";
|
|
27
|
-
import { makePersistentTerminalTools } from "./terminals.js";
|
|
27
|
+
import { makePersistentTerminalTools, TERMINAL_TOOLS_GUIDANCE, TERMINAL_TOOL_NAMES, } from "./terminals.js";
|
|
28
28
|
import { WebUIContext } from "./webui-context.js";
|
|
29
29
|
import { buildAttachmentMessages, parseModelSpec, } from "./attachments.js";
|
|
30
30
|
import { serializeMessage, serializeStreamingMessage, } from "./serialize.js";
|
|
@@ -415,6 +415,8 @@ export class ClientSession {
|
|
|
415
415
|
isStreaming: () => this.session.isStreaming,
|
|
416
416
|
reloadSession: async () => {
|
|
417
417
|
await this.session.reload();
|
|
418
|
+
// reload() 会把 custom 工具重新加回活跃集——重放终端开关。
|
|
419
|
+
this.applyTerminalToolGating(this.session);
|
|
418
420
|
await this.pushSlashCommands();
|
|
419
421
|
},
|
|
420
422
|
effectiveDefaultSystemPrompt: () => this.effectiveDefaultSystemPrompt(),
|
|
@@ -520,6 +522,11 @@ export class ClientSession {
|
|
|
520
522
|
// GBK 老中文文件让模型改用终端按正确编码读(iconv/chcp/Get-Content)。
|
|
521
523
|
out.push(WINDOWS_PERSONA);
|
|
522
524
|
}
|
|
525
|
+
if (this.settingsSvc.current.terminalToolsEnabled !== false) {
|
|
526
|
+
// 终端工具使用引导(全平台):告诉模型什么场景该用持久终端
|
|
527
|
+
// 而不是一次性 bash——没有这段模型几乎从不主动选终端工具。
|
|
528
|
+
out.push(TERMINAL_TOOLS_GUIDANCE);
|
|
529
|
+
}
|
|
523
530
|
return out;
|
|
524
531
|
},
|
|
525
532
|
// 技能开关:禁用的技能从系统提示词和 /skill: 目录中剔除。
|
|
@@ -528,24 +535,29 @@ export class ClientSession {
|
|
|
528
535
|
skills: res.skills.filter((s) => !this.settingsSvc.current.disabledSkills.includes(s.name)),
|
|
529
536
|
}),
|
|
530
537
|
// 插件开关:禁用的扩展整个卸载(工具 / 命令随之消失)。
|
|
538
|
+
// 注意 SDK 在 extensionsOverride 之后才补 sourceInfo,包扩展此处只能靠路径
|
|
539
|
+
// 匹配 —— isExtensionDisabled 同时比对 npm:<pkg> 候选键。
|
|
531
540
|
extensionsOverride: (res) => ({
|
|
532
541
|
...res,
|
|
533
|
-
extensions: res.extensions.filter((e) => !this.settingsSvc.current.disabledExtensions
|
|
542
|
+
extensions: res.extensions.filter((e) => !isExtensionDisabled(e, this.settingsSvc.current.disabledExtensions)),
|
|
534
543
|
}),
|
|
535
544
|
},
|
|
536
545
|
});
|
|
546
|
+
const created = await createAgentSessionFromServices({
|
|
547
|
+
services,
|
|
548
|
+
sessionManager,
|
|
549
|
+
// 可手动停止的 bash 工具:覆盖 SDK 内置 bash(customTools 按 name
|
|
550
|
+
// 覆盖),执行时把自己的 AbortController 注册进客户端集合——
|
|
551
|
+
// abortBash() 只杀这些命令,agent run 与对话继续。
|
|
552
|
+
customTools: [
|
|
553
|
+
makeKillableBashTool(effectiveCwd, this.bashKills),
|
|
554
|
+
...makePersistentTerminalTools(terminals, effectiveCwd),
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
// 终端工具开关从创建起就生效(工具始终注册进注册表,只调活跃集)。
|
|
558
|
+
this.applyTerminalToolGating(created.session);
|
|
537
559
|
return {
|
|
538
|
-
...
|
|
539
|
-
services,
|
|
540
|
-
sessionManager,
|
|
541
|
-
// 可手动停止的 bash 工具:覆盖 SDK 内置 bash(customTools 按 name
|
|
542
|
-
// 覆盖),执行时把自己的 AbortController 注册进客户端集合——
|
|
543
|
-
// abortBash() 只杀这些命令,agent run 与对话继续。
|
|
544
|
-
customTools: [
|
|
545
|
-
makeKillableBashTool(effectiveCwd, this.bashKills),
|
|
546
|
-
...makePersistentTerminalTools(terminals, effectiveCwd),
|
|
547
|
-
],
|
|
548
|
-
})),
|
|
560
|
+
...created,
|
|
549
561
|
services,
|
|
550
562
|
diagnostics: services.diagnostics,
|
|
551
563
|
};
|
|
@@ -588,8 +600,8 @@ export class ClientSession {
|
|
|
588
600
|
uiMessageCache: new Map(),
|
|
589
601
|
lastMessagesSig: "",
|
|
590
602
|
lastMessagesArray: [],
|
|
591
|
-
queueSteering:
|
|
592
|
-
queueFollowUp:
|
|
603
|
+
queueSteering: [],
|
|
604
|
+
queueFollowUp: [],
|
|
593
605
|
toolStartTimes: new Map(),
|
|
594
606
|
toolWatchdogs: new Map(),
|
|
595
607
|
};
|
|
@@ -847,8 +859,8 @@ export class ClientSession {
|
|
|
847
859
|
break;
|
|
848
860
|
}
|
|
849
861
|
case "queue_update":
|
|
850
|
-
conv.queueSteering = event.steering
|
|
851
|
-
conv.queueFollowUp = event.followUp
|
|
862
|
+
conv.queueSteering = [...event.steering];
|
|
863
|
+
conv.queueFollowUp = [...event.followUp];
|
|
852
864
|
break;
|
|
853
865
|
// A run finished or a new entry was persisted — keep the session list fresh
|
|
854
866
|
// (new chat + first message, completed turns, compaction, etc.).
|
|
@@ -1451,6 +1463,7 @@ export class ClientSession {
|
|
|
1451
1463
|
setCwd: (path) => this.setCwd(path),
|
|
1452
1464
|
setThinking: (level) => this.setThinking(level),
|
|
1453
1465
|
refreshSessions: () => this.refreshSessions(),
|
|
1466
|
+
afterReload: () => this.applyTerminalToolGating(this.session),
|
|
1454
1467
|
onQuit: () => this.onQuit?.() ?? false,
|
|
1455
1468
|
});
|
|
1456
1469
|
/** Catalog push — index.ts get_commands / attach / cwd 切换等都会调用。 */
|
|
@@ -1493,6 +1506,12 @@ export class ClientSession {
|
|
|
1493
1506
|
pushSettings() {
|
|
1494
1507
|
this.settingsSvc.push();
|
|
1495
1508
|
}
|
|
1509
|
+
/** Extensions/skills changed externally (e.g. `pi remove` finished in the
|
|
1510
|
+
* terminal): re-run session.reload() and re-push state. Streaming-safe —
|
|
1511
|
+
* deferred to agent_end, same as settings reloads. */
|
|
1512
|
+
async reloadExtensions() {
|
|
1513
|
+
return this.settingsSvc.applyRuntime();
|
|
1514
|
+
}
|
|
1496
1515
|
/** Persist + apply a partial settings update (prompt text/mode, toggles). */
|
|
1497
1516
|
async setSettings(partial) {
|
|
1498
1517
|
await this.settingsSvc.set(partial);
|
|
@@ -1513,6 +1532,25 @@ export class ClientSession {
|
|
|
1513
1532
|
async applyRuntimeSettings() {
|
|
1514
1533
|
return this.settingsSvc.applyRuntime();
|
|
1515
1534
|
}
|
|
1535
|
+
/** 把终端工具开关应用到 session 的活跃工具集:关闭时从活跃集中剔除
|
|
1536
|
+
* terminal_*(工具仍留在注册表,重开时可直接加回)。session.reload() 与新
|
|
1537
|
+
* 会话创建都会把 custom 工具加回活跃集,所以这两条路径之后都要重放本方法。 */
|
|
1538
|
+
applyTerminalToolGating(session) {
|
|
1539
|
+
try {
|
|
1540
|
+
const enabled = this.settingsSvc.current.terminalToolsEnabled !== false;
|
|
1541
|
+
const names = new Set(session.getActiveToolNames());
|
|
1542
|
+
for (const n of TERMINAL_TOOL_NAMES) {
|
|
1543
|
+
if (enabled)
|
|
1544
|
+
names.add(n);
|
|
1545
|
+
else
|
|
1546
|
+
names.delete(n);
|
|
1547
|
+
}
|
|
1548
|
+
session.setActiveToolsByName([...names]);
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// Session 未就绪——下次创建/reload 会再应用。
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1516
1554
|
async applySettingsReload() {
|
|
1517
1555
|
// 兼容旧入口:reload + 刷目录在宿主回调里完成
|
|
1518
1556
|
return this.settingsSvc.applyRuntime();
|
|
@@ -1555,7 +1593,7 @@ export class ClientSession {
|
|
|
1555
1593
|
pendingMessages() {
|
|
1556
1594
|
let n = 0;
|
|
1557
1595
|
for (const c of this.convs.values())
|
|
1558
|
-
n += c.queueFollowUp + c.queueSteering;
|
|
1596
|
+
n += c.queueFollowUp.length + c.queueSteering.length;
|
|
1559
1597
|
return n;
|
|
1560
1598
|
}
|
|
1561
1599
|
async prompt(text, attachments,
|
|
@@ -17,6 +17,35 @@ export function extensionKey(e) {
|
|
|
17
17
|
return src.source;
|
|
18
18
|
return src?.path ?? e.path;
|
|
19
19
|
}
|
|
20
|
+
/** All identities an extension may be disabled by. The SDK applies
|
|
21
|
+
* `sourceInfo` only AFTER extensionsOverride runs (resource-loader reload():
|
|
22
|
+
* override first, applyExtensionSourceInfo second), so inside the override a
|
|
23
|
+
* package extension still has no sourceInfo and extensionKey() falls back to
|
|
24
|
+
* the raw entry path — which never matches the "npm:<pkg>" id the settings
|
|
25
|
+
* panel stores. Derive the package name from the entry path
|
|
26
|
+
* (.../node_modules/<pkg>/... or .../node_modules/@scope/<pkg>/...) so both
|
|
27
|
+
* sides agree. */
|
|
28
|
+
export function extensionKeyCandidates(e) {
|
|
29
|
+
const keys = new Set([extensionKey(e)]);
|
|
30
|
+
const norm = e.path.replace(/\\/g, "/");
|
|
31
|
+
const marker = "/node_modules/";
|
|
32
|
+
const idx = norm.lastIndexOf(marker);
|
|
33
|
+
if (idx !== -1) {
|
|
34
|
+
const segs = norm.slice(idx + marker.length).split("/");
|
|
35
|
+
// Scoped package @scope/name spans two segments.
|
|
36
|
+
const name = segs[0]?.startsWith("@") && segs[1] ? `${segs[0]}/${segs[1]}` : segs[0];
|
|
37
|
+
if (name)
|
|
38
|
+
keys.add(`npm:${name}`);
|
|
39
|
+
}
|
|
40
|
+
return [...keys];
|
|
41
|
+
}
|
|
42
|
+
/** Whether an extension is covered by the disabled list (any identity match). */
|
|
43
|
+
export function isExtensionDisabled(e, disabled) {
|
|
44
|
+
if (disabled.length === 0)
|
|
45
|
+
return false;
|
|
46
|
+
const keys = extensionKeyCandidates(e);
|
|
47
|
+
return disabled.some((d) => keys.includes(d));
|
|
48
|
+
}
|
|
20
49
|
/**
|
|
21
50
|
* Persists which workspace each browser client last used + which workspaces it
|
|
22
51
|
* has opened, so a server restart / page reload restores the same project and
|
|
@@ -122,6 +151,7 @@ export class ClientStateStore {
|
|
|
122
151
|
customSystemPrompt: s?.settings?.customSystemPrompt ?? "",
|
|
123
152
|
disabledSkills: s?.settings?.disabledSkills ?? [],
|
|
124
153
|
disabledExtensions: s?.settings?.disabledExtensions ?? [],
|
|
154
|
+
terminalToolsEnabled: s?.settings?.terminalToolsEnabled ?? true,
|
|
125
155
|
visionBridgeEnabled: s?.settings?.visionBridgeEnabled ?? true,
|
|
126
156
|
visionBridgeModel: s?.settings?.visionBridgeModel ?? null,
|
|
127
157
|
visionBridgePromptMode: s?.settings?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
@@ -140,6 +170,7 @@ export class ClientStateStore {
|
|
|
140
170
|
customSystemPrompt: settings.customSystemPrompt ?? cur.customSystemPrompt ?? "",
|
|
141
171
|
disabledSkills: settings.disabledSkills ?? cur.disabledSkills ?? [],
|
|
142
172
|
disabledExtensions: settings.disabledExtensions ?? cur.disabledExtensions ?? [],
|
|
173
|
+
terminalToolsEnabled: settings.terminalToolsEnabled ?? cur.terminalToolsEnabled ?? true,
|
|
143
174
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
144
175
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
145
176
|
visionBridgePromptMode: settings.visionBridgePromptMode ??
|
package/dist/server/index.js
CHANGED
|
@@ -660,6 +660,7 @@ wss.on("connection", (ws) => {
|
|
|
660
660
|
customSystemPrompt: msg.customSystemPrompt,
|
|
661
661
|
disabledSkills: msg.disabledSkills,
|
|
662
662
|
disabledExtensions: msg.disabledExtensions,
|
|
663
|
+
terminalToolsEnabled: msg.terminalToolsEnabled,
|
|
663
664
|
visionBridgeEnabled: msg.visionBridgeEnabled,
|
|
664
665
|
visionBridgeModel: msg.visionBridgeModel,
|
|
665
666
|
visionBridgePromptMode: msg.visionBridgePromptMode,
|
|
@@ -668,6 +669,9 @@ wss.on("connection", (ws) => {
|
|
|
668
669
|
reviewDisabledSkills: msg.reviewDisabledSkills,
|
|
669
670
|
});
|
|
670
671
|
break;
|
|
672
|
+
case "extensions_reload":
|
|
673
|
+
void cs.reloadExtensions();
|
|
674
|
+
break;
|
|
671
675
|
case "save_preset":
|
|
672
676
|
void cs.savePreset(msg.name);
|
|
673
677
|
break;
|
|
@@ -99,6 +99,7 @@ export class SettingsService {
|
|
|
99
99
|
settings: {
|
|
100
100
|
promptMode: this.settings.promptMode,
|
|
101
101
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
102
|
+
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
102
103
|
visionBridgeEnabled: this.settings.visionBridgeEnabled,
|
|
103
104
|
visionBridgeModel: this.settings.visionBridgeModel,
|
|
104
105
|
visionBridgePromptMode: this.settings.visionBridgePromptMode,
|
|
@@ -139,7 +140,8 @@ export class SettingsService {
|
|
|
139
140
|
const needsReload = partial.promptMode !== undefined ||
|
|
140
141
|
partial.customSystemPrompt !== undefined ||
|
|
141
142
|
partial.disabledSkills !== undefined ||
|
|
142
|
-
partial.disabledExtensions !== undefined
|
|
143
|
+
partial.disabledExtensions !== undefined ||
|
|
144
|
+
partial.terminalToolsEnabled !== undefined;
|
|
143
145
|
if (partial.promptMode !== undefined)
|
|
144
146
|
this.settings.promptMode = partial.promptMode;
|
|
145
147
|
if (partial.customSystemPrompt !== undefined) {
|
|
@@ -151,6 +153,9 @@ export class SettingsService {
|
|
|
151
153
|
if (partial.disabledExtensions !== undefined) {
|
|
152
154
|
this.settings.disabledExtensions = partial.disabledExtensions;
|
|
153
155
|
}
|
|
156
|
+
if (partial.terminalToolsEnabled !== undefined) {
|
|
157
|
+
this.settings.terminalToolsEnabled = partial.terminalToolsEnabled;
|
|
158
|
+
}
|
|
154
159
|
if (partial.visionBridgeEnabled !== undefined) {
|
|
155
160
|
this.settings.visionBridgeEnabled = partial.visionBridgeEnabled;
|
|
156
161
|
}
|
|
@@ -187,6 +192,7 @@ export class SettingsService {
|
|
|
187
192
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
188
193
|
disabledSkills: [...this.settings.disabledSkills],
|
|
189
194
|
disabledExtensions: [...this.settings.disabledExtensions],
|
|
195
|
+
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
190
196
|
reviewPrompt: this.settings.reviewPrompt,
|
|
191
197
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
192
198
|
};
|
|
@@ -210,6 +216,8 @@ export class SettingsService {
|
|
|
210
216
|
customSystemPrompt: p.customSystemPrompt,
|
|
211
217
|
disabledSkills: [...p.disabledSkills],
|
|
212
218
|
disabledExtensions: [...p.disabledExtensions],
|
|
219
|
+
// 旧版持久化的预设可能没有该字段——保留当前值。
|
|
220
|
+
terminalToolsEnabled: p.terminalToolsEnabled ?? this.settings.terminalToolsEnabled,
|
|
213
221
|
reviewPrompt: p.reviewPrompt ?? this.settings.reviewPrompt,
|
|
214
222
|
reviewDisabledSkills: [
|
|
215
223
|
...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills),
|
|
@@ -204,6 +204,8 @@ export class SlashCommandsService {
|
|
|
204
204
|
// Re-discovers extensions / skills / prompt templates from disk and
|
|
205
205
|
// re-pushes the picker catalog (the CLI's /reload semantics).
|
|
206
206
|
await this.host.getSession().reload();
|
|
207
|
+
// reload() 会把 custom 工具加回活跃集——重放设置门控(终端开关等)。
|
|
208
|
+
this.host.afterReload?.();
|
|
207
209
|
await this.push();
|
|
208
210
|
this.host.emit({
|
|
209
211
|
type: "notice",
|
package/dist/server/terminals.js
CHANGED
|
@@ -778,6 +778,23 @@ export class TerminalManager {
|
|
|
778
778
|
this.emitList();
|
|
779
779
|
}
|
|
780
780
|
}
|
|
781
|
+
/** Names of the agent-facing persistent-terminal tools(设置开关门控用)。 */
|
|
782
|
+
export const TERMINAL_TOOL_NAMES = [
|
|
783
|
+
"terminal_create",
|
|
784
|
+
"terminal_list",
|
|
785
|
+
"terminal_close",
|
|
786
|
+
"terminal_input",
|
|
787
|
+
"terminal_key",
|
|
788
|
+
"terminal_read",
|
|
789
|
+
];
|
|
790
|
+
/** System-prompt guidance teaching the model WHEN to prefer the terminal tools
|
|
791
|
+
* over one-shot bash. Without it models almost never pick them — bash returns
|
|
792
|
+
* complete output in a single call, so it always wins on convenience. */
|
|
793
|
+
export const TERMINAL_TOOLS_GUIDANCE = `Persistent interactive terminal tools are available (terminal_create / terminal_list / terminal_close / terminal_input / terminal_key / terminal_read). The one-shot bash tool stays the DEFAULT for ordinary commands - it runs once and returns the full output. Switch to the terminal tools only when:
|
|
794
|
+
- The program is interactive or TUI-based (REPLs like python/node, vim/htop, installers asking y/n, anything waiting on stdin).
|
|
795
|
+
- You start a long-running server or watcher and want to keep watching its output (terminal_read with waitMs) or send keys to it later (e.g. interrupt via terminal_key with Ctrl+c).
|
|
796
|
+
- The user explicitly asks you to work in the visible terminal panel.
|
|
797
|
+
Do NOT use them for simple one-shot commands; bash remains cheaper and simpler there.`;
|
|
781
798
|
/** Build the agent-facing persistent terminal tools for one conversation. */
|
|
782
799
|
export function makePersistentTerminalTools(terminals, cwd) {
|
|
783
800
|
const result = (text, details = {}) => ({ content: [{ type: "text", text }], details });
|
|
@@ -789,8 +806,8 @@ export function makePersistentTerminalTools(terminals, cwd) {
|
|
|
789
806
|
defineTool({
|
|
790
807
|
name: "terminal_create",
|
|
791
808
|
label: "Create terminal",
|
|
792
|
-
description: "Create a named persistent interactive PTY in the current workspace. Use terminal_input or terminal_key to interact with it and terminal_read to inspect incremental output.",
|
|
793
|
-
promptSnippet: "
|
|
809
|
+
description: "Create a named persistent interactive PTY in the current workspace. Use terminal_input or terminal_key to interact with it and terminal_read to inspect incremental output. Prefer this over bash when the program is interactive/TUI-based (REPLs, vim/htop, y/n prompts), when starting a long-running server you want to keep observing or interrupt, or when the user asks to work in the visible terminal. For simple one-shot commands use bash instead.",
|
|
810
|
+
promptSnippet: "run interactive programs or long-running servers in a persistent visible PTY (multi-step: create → input/key → read)",
|
|
794
811
|
parameters: Type.Object({
|
|
795
812
|
terminalId: Type.String({ description: "Stable terminal name" }),
|
|
796
813
|
cwd: Type.Optional(Type.String({ description: "Workspace-relative directory" })),
|
package/dist/server/themes.js
CHANGED
|
@@ -7,10 +7,24 @@
|
|
|
7
7
|
* - builtin: <pkgRoot>/themes/*.css (ships with the npm package)
|
|
8
8
|
* - user : <dataDir>/themes/*.css (drop a css file here to add a theme)
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
/** Only simple file ids — no path traversal. */
|
|
13
13
|
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
/** Display-name marker inside a theme css file (first lines):
|
|
15
|
+
* `/* theme-name: 中文名 *∕` — falls back to the file id when absent.
|
|
16
|
+
* Lets built-in AND user themes carry a human-readable label while the
|
|
17
|
+
* filename stays ASCII (id must match ID_RE). */
|
|
18
|
+
const THEME_NAME_RE = /\/\*\s*theme-name:\s*(.+?)\s*\*\//;
|
|
19
|
+
function readDisplayName(path, fallback) {
|
|
20
|
+
try {
|
|
21
|
+
const head = readFileSync(path, "utf8").slice(0, 300);
|
|
22
|
+
return head.match(THEME_NAME_RE)?.[1]?.trim() || fallback;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
export function listThemes(builtinDir, userDir) {
|
|
15
29
|
const scan = (dir, builtin) => {
|
|
16
30
|
if (!existsSync(dir))
|
|
@@ -19,7 +33,11 @@ export function listThemes(builtinDir, userDir) {
|
|
|
19
33
|
.filter((f) => f.endsWith(".css"))
|
|
20
34
|
.filter((f) => ID_RE.test(f.slice(0, -4)))
|
|
21
35
|
.sort()
|
|
22
|
-
.map((f) => ({
|
|
36
|
+
.map((f) => ({
|
|
37
|
+
id: f.slice(0, -4),
|
|
38
|
+
name: readDisplayName(join(dir, f), f.slice(0, -4)),
|
|
39
|
+
builtin,
|
|
40
|
+
}));
|
|
23
41
|
};
|
|
24
42
|
const builtin = scan(builtinDir, true);
|
|
25
43
|
const user = scan(userDir, false);
|
package/package.json
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"pi-web-ui": "bin/pi-web-ui.mjs"
|
|
9
|
-
},
|
|
10
|
-
"main": "dist/server/index.js",
|
|
11
|
-
"files": [
|
|
12
|
-
"bin/",
|
|
13
|
-
"dist/",
|
|
14
|
-
"web/dist/",
|
|
15
|
-
"web/public/",
|
|
16
|
-
"themes/",
|
|
17
|
-
"deploy/",
|
|
18
|
-
"extensions/",
|
|
19
|
-
"README.md",
|
|
20
|
-
"LICENSE"
|
|
21
|
-
],
|
|
22
|
-
"keywords": [
|
|
23
|
-
"pi",
|
|
24
|
-
"pi-package",
|
|
25
|
-
"coding-agent",
|
|
26
|
-
"ai",
|
|
27
|
-
"chat",
|
|
28
|
-
"web-ui",
|
|
29
|
-
"terminal",
|
|
30
|
-
"llm"
|
|
31
|
-
],
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
|
|
35
|
-
},
|
|
36
|
-
"pi": {
|
|
37
|
-
"extensions": [
|
|
38
|
-
"./extensions"
|
|
39
|
-
]
|
|
40
|
-
},
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=22.19.0"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"prepublishOnly": "npm run build",
|
|
46
|
-
"dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
|
|
47
|
-
"dev:server": "cross-env PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
|
|
48
|
-
"dev:web": "vite --config web/vite.config.ts",
|
|
49
|
-
"build": "npm run build:web && npm run build:server",
|
|
50
|
-
"build:web": "vite build --config web/vite.config.ts",
|
|
51
|
-
"build:server": "tsc -p tsconfig.server.json",
|
|
52
|
-
"typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
|
|
53
|
-
"test": "vitest run",
|
|
54
|
-
"test:unit": "vitest run",
|
|
55
|
-
"test:smoke": "node tests/run-smoke.mjs",
|
|
56
|
-
"check:protocol": "node scripts/check-protocol-sync.mjs",
|
|
57
|
-
"test:freeze": "node tests/freeze-test.mjs",
|
|
58
|
-
"start": "node dist/server/index.js"
|
|
59
|
-
},
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
62
|
-
"@xterm/addon-fit": "^0.11.0",
|
|
63
|
-
"@xterm/xterm": "^6.0.0",
|
|
64
|
-
"compression": "^1.8.1",
|
|
65
|
-
"express": "^4.21.2",
|
|
66
|
-
"highlight.js": "^11.10.0",
|
|
67
|
-
"node-pty": "^1.1.0",
|
|
68
|
-
"react": "^18.3.1",
|
|
69
|
-
"react-dom": "^18.3.1",
|
|
70
|
-
"react-icons": "^5.7.0",
|
|
71
|
-
"react-markdown": "^9.0.1",
|
|
72
|
-
"rehype-highlight": "^7.0.1",
|
|
73
|
-
"remark-gfm": "^4.0.0",
|
|
74
|
-
"typebox": "^1.3.14",
|
|
75
|
-
"ws": "^8.18.0"
|
|
76
|
-
},
|
|
77
|
-
"devDependencies": {
|
|
78
|
-
"@types/compression": "^1.8.1",
|
|
79
|
-
"@types/express": "^4.17.21",
|
|
80
|
-
"@types/node": "^22.10.0",
|
|
81
|
-
"@types/react": "^18.3.12",
|
|
82
|
-
"@types/react-dom": "^18.3.1",
|
|
83
|
-
"@types/ws": "^8.5.13",
|
|
84
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
85
|
-
"concurrently": "^9.1.0",
|
|
86
|
-
"cross-env": "^10.1.0",
|
|
87
|
-
"playwright-core": "^1.62.1",
|
|
88
|
-
"tsx": "^4.19.2",
|
|
89
|
-
"typescript": "^5.7.2",
|
|
90
|
-
"vite": "^6.0.3",
|
|
91
|
-
"vitest": "^4.1.11"
|
|
92
|
-
},
|
|
93
|
-
"allowScripts": {
|
|
94
|
-
"node-pty@1.1.0": true
|
|
95
|
-
}
|
|
96
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-web-ui",
|
|
3
|
+
"version": "0.31.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pi-web-ui": "bin/pi-web-ui.mjs"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/server/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"dist/",
|
|
14
|
+
"web/dist/",
|
|
15
|
+
"web/public/",
|
|
16
|
+
"themes/",
|
|
17
|
+
"deploy/",
|
|
18
|
+
"extensions/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi",
|
|
24
|
+
"pi-package",
|
|
25
|
+
"coding-agent",
|
|
26
|
+
"ai",
|
|
27
|
+
"chat",
|
|
28
|
+
"web-ui",
|
|
29
|
+
"terminal",
|
|
30
|
+
"llm"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
|
|
35
|
+
},
|
|
36
|
+
"pi": {
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./extensions"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.19.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepublishOnly": "npm run build",
|
|
46
|
+
"dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
|
|
47
|
+
"dev:server": "cross-env PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
|
|
48
|
+
"dev:web": "vite --config web/vite.config.ts",
|
|
49
|
+
"build": "npm run build:web && npm run build:server",
|
|
50
|
+
"build:web": "vite build --config web/vite.config.ts",
|
|
51
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:unit": "vitest run",
|
|
55
|
+
"test:smoke": "node tests/run-smoke.mjs",
|
|
56
|
+
"check:protocol": "node scripts/check-protocol-sync.mjs",
|
|
57
|
+
"test:freeze": "node tests/freeze-test.mjs",
|
|
58
|
+
"start": "node dist/server/index.js"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
62
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
63
|
+
"@xterm/xterm": "^6.0.0",
|
|
64
|
+
"compression": "^1.8.1",
|
|
65
|
+
"express": "^4.21.2",
|
|
66
|
+
"highlight.js": "^11.10.0",
|
|
67
|
+
"node-pty": "^1.1.0",
|
|
68
|
+
"react": "^18.3.1",
|
|
69
|
+
"react-dom": "^18.3.1",
|
|
70
|
+
"react-icons": "^5.7.0",
|
|
71
|
+
"react-markdown": "^9.0.1",
|
|
72
|
+
"rehype-highlight": "^7.0.1",
|
|
73
|
+
"remark-gfm": "^4.0.0",
|
|
74
|
+
"typebox": "^1.3.14",
|
|
75
|
+
"ws": "^8.18.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/compression": "^1.8.1",
|
|
79
|
+
"@types/express": "^4.17.21",
|
|
80
|
+
"@types/node": "^22.10.0",
|
|
81
|
+
"@types/react": "^18.3.12",
|
|
82
|
+
"@types/react-dom": "^18.3.1",
|
|
83
|
+
"@types/ws": "^8.5.13",
|
|
84
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
85
|
+
"concurrently": "^9.1.0",
|
|
86
|
+
"cross-env": "^10.1.0",
|
|
87
|
+
"playwright-core": "^1.62.1",
|
|
88
|
+
"tsx": "^4.19.2",
|
|
89
|
+
"typescript": "^5.7.2",
|
|
90
|
+
"vite": "^6.0.3",
|
|
91
|
+
"vitest": "^4.1.11"
|
|
92
|
+
},
|
|
93
|
+
"allowScripts": {
|
|
94
|
+
"node-pty@1.1.0": true
|
|
95
|
+
}
|
|
96
|
+
}
|