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.
@@ -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 { extensionKey, ClientStateStore, } from "./client-state.js";
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.includes(extensionKey(e))),
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
- ...(await createAgentSessionFromServices({
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: 0,
592
- queueFollowUp: 0,
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.length;
851
- conv.queueFollowUp = event.followUp.length;
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 ??
@@ -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;
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 3;
11
+ export const PROTOCOL_VERSION = 5;
@@ -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",
@@ -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: "create persistent interactive PTY terminals",
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" })),
@@ -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) => ({ id: f.slice(0, -4), name: f.slice(0, -4), builtin }));
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.29.3",
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
+ }