pi-web-ui 0.63.1 → 0.63.3

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.
Files changed (63) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +504 -504
  3. package/README.zh-CN.md +427 -427
  4. package/bin/pi-web-ui.mjs +1809 -1809
  5. package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
  6. package/deploy/nginx-subpath.conf +88 -88
  7. package/deploy/pi-web-ui-task.xml +54 -54
  8. package/deploy/pi-web-ui.service +31 -31
  9. package/dist/server/agent-service.js +228 -11
  10. package/dist/server/attachments.js +3 -3
  11. package/dist/server/client-state.js +18 -0
  12. package/dist/server/dsh/dsh-agent-service.js +21 -0
  13. package/dist/server/dsh/runtime/cordis.yml +1 -1
  14. package/dist/server/dsh/runtime/goal-rpc.mjs +645 -645
  15. package/dist/server/dsh/runtime/launcher.mjs +164 -164
  16. package/dist/server/dsh/runtime/override.patch.yml +71 -71
  17. package/dist/server/dsh/runtime/runtime-root.mjs +86 -86
  18. package/dist/server/index.js +11 -0
  19. package/dist/server/marker-service.js +270 -0
  20. package/dist/server/markers/builtins/notify.js +20 -0
  21. package/dist/server/markers/builtins/rename.js +102 -0
  22. package/dist/server/markers/builtins/services.js +119 -0
  23. package/dist/server/markers/builtins/todo.js +130 -0
  24. package/dist/server/markers/index.js +24 -0
  25. package/dist/server/markers/marker.js +53 -0
  26. package/dist/server/markers/registry.js +28 -0
  27. package/dist/server/markers/store.js +47 -0
  28. package/dist/server/settings-service.js +3 -0
  29. package/dist/server/slash-commands.js +34 -0
  30. package/dist/server/terminals.js +15 -4
  31. package/dist/server/themes.js +24 -5
  32. package/dist/server/vision-bridge.js +9 -9
  33. package/extensions/webui.ts +190 -190
  34. package/package.json +1 -1
  35. package/themes/cyberpunk.css +81 -80
  36. package/themes/dazzle.css +81 -80
  37. package/themes/md-preview.css +98 -97
  38. package/themes/white.css +160 -159
  39. package/web/dist/assets/TerminalPanel-BFrV6B8W.js +2 -0
  40. package/web/dist/assets/index-BFoSybNe.js +324 -0
  41. package/web/dist/assets/index-D9G_7fPE.css +10 -0
  42. package/web/dist/favicon.svg +8 -8
  43. package/web/dist/icons/icon-1024.png +0 -0
  44. package/web/dist/icons/icon-192.png +0 -0
  45. package/web/dist/icons/icon-512.png +0 -0
  46. package/web/dist/icons/maskable-1024.png +0 -0
  47. package/web/dist/icons/maskable-192.png +0 -0
  48. package/web/dist/icons/maskable-512.png +0 -0
  49. package/web/dist/index.html +23 -17
  50. package/web/dist/manifest.webmanifest +50 -0
  51. package/web/dist/sw.js +126 -0
  52. package/web/public/favicon.svg +8 -8
  53. package/web/public/icons/icon-1024.png +0 -0
  54. package/web/public/icons/icon-192.png +0 -0
  55. package/web/public/icons/icon-512.png +0 -0
  56. package/web/public/icons/maskable-1024.png +0 -0
  57. package/web/public/icons/maskable-192.png +0 -0
  58. package/web/public/icons/maskable-512.png +0 -0
  59. package/web/public/manifest.webmanifest +50 -0
  60. package/web/public/sw.js +126 -0
  61. package/web/dist/assets/TerminalPanel-DwutHqgi.js +0 -2
  62. package/web/dist/assets/index-CJoS1bmZ.js +0 -323
  63. package/web/dist/assets/index-CrDJOsa5.css +0 -10
@@ -0,0 +1,24 @@
1
+ /**
2
+ * markers/index.ts — 聚合内置标记并提供便捷初始化。
3
+ */
4
+ import { registerMarker } from "./registry.js";
5
+ import { todoMarker } from "./builtins/todo.js";
6
+ import { servicesMarker } from "./builtins/services.js";
7
+ import { notifyMarker } from "./builtins/notify.js";
8
+ import { renameMarker, renameAliasMarker, titleAliasMarker } from "./builtins/rename.js";
9
+ let initialized = false;
10
+ export function ensureMarkersRegistered() {
11
+ if (initialized)
12
+ return;
13
+ registerMarker(todoMarker);
14
+ registerMarker(servicesMarker);
15
+ registerMarker(notifyMarker);
16
+ registerMarker(renameMarker);
17
+ registerMarker(renameAliasMarker);
18
+ registerMarker(titleAliasMarker);
19
+ initialized = true;
20
+ }
21
+ export { todoMarker, servicesMarker, notifyMarker, renameMarker, renameAliasMarker, titleAliasMarker };
22
+ export * from "./marker.js";
23
+ export * from "./registry.js";
24
+ export * from "./store.js";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * marker.ts — 通用内联标记核心抽象(内置版)。
3
+ * 复刻自 pi-marker-tools,保持相同解析语义,便于 AI 无缝迁移。
4
+ */
5
+ export const MARKER_OPEN = "[[";
6
+ export const MARKER_CLOSE = "]]";
7
+ // ---------------------------------------------------------------------------
8
+ // 解析器
9
+ // ---------------------------------------------------------------------------
10
+ const TOKEN_RE = /\[\[\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*([A-Za-z][A-Za-z0-9_-]*)\s*:(.*?)\s*\]\]/g;
11
+ function splitArgs(body) {
12
+ const args = [];
13
+ const kwargs = {};
14
+ for (const piece of body.split(",")) {
15
+ const trimmed = piece.trim();
16
+ if (!trimmed)
17
+ continue;
18
+ const eq = trimmed.indexOf("=");
19
+ if (eq > 0 && /^[A-Za-z][A-Za-z0-9_-]*$/.test(trimmed.slice(0, eq))) {
20
+ kwargs[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
21
+ }
22
+ else {
23
+ args.push(trimmed);
24
+ }
25
+ }
26
+ return { args, kwargs };
27
+ }
28
+ export function parseMarkers(text) {
29
+ const tokens = [];
30
+ TOKEN_RE.lastIndex = 0;
31
+ let m;
32
+ while ((m = TOKEN_RE.exec(text)) !== null) {
33
+ const [, tool, op, body] = m;
34
+ if (body.includes("[["))
35
+ continue;
36
+ const { args, kwargs } = splitArgs(body);
37
+ tokens.push({ tool, op, args, kwargs, raw: m[0] });
38
+ }
39
+ return tokens;
40
+ }
41
+ export function stripMarkers(text) {
42
+ return text.replace(TOKEN_RE, () => "");
43
+ }
44
+ export function replaceToken(text, raw, replacement) {
45
+ return text.split(raw).join(replacement);
46
+ }
47
+ export function serializeToken(token) {
48
+ const parts = [token.tool, token.op, ...token.args];
49
+ const kwargs = Object.entries(token.kwargs)
50
+ .sort(([a], [b]) => (a < b ? -1 : 1))
51
+ .map(([k, v]) => `${k}=${v}`);
52
+ return `${MARKER_OPEN}${[...parts, ...kwargs].join(":")}${MARKER_CLOSE}`;
53
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * registry.ts — 标记工具注册表(内置版)。
3
+ */
4
+ const registry = new Map();
5
+ export function registerMarker(marker) {
6
+ registry.set(marker.name, marker);
7
+ }
8
+ export function getMarker(name) {
9
+ return registry.get(name);
10
+ }
11
+ export function allMarkers() {
12
+ return [...registry.values()];
13
+ }
14
+ export function lookupToken(name) {
15
+ return registry.get(name);
16
+ }
17
+ export function collectGuidance(disabled = new Set()) {
18
+ const out = [];
19
+ for (const m of allMarkers()) {
20
+ if (disabled.has(m.name))
21
+ continue;
22
+ out.push(...m.guidance);
23
+ }
24
+ return out;
25
+ }
26
+ export function listMarkerNames() {
27
+ return [...registry.keys()];
28
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * store.ts — 标记状态持久化(内置版)。
3
+ *
4
+ * 策略:优先用 SDK 会话的 custom entry(appendCustomEntry + getBranch 扫描),
5
+ * 失败时回退到内存 Map(保证 rename 等轻量标记仍可用)。custom entry 类型与
6
+ * pi-marker-tools 保持兼容(marker-tools/store),便于复用既有会话数据。
7
+ */
8
+ export const STORE_CUSTOM_TYPE = "marker-tools/store";
9
+ const SNAPSHOT_VERSION = 1;
10
+ /** 从分支重建某命名空间最新快照;branch 为 SessionManager.getBranch() 返回的数组。 */
11
+ export function loadStateFromBranch(branch, namespace) {
12
+ let latest;
13
+ for (const entry of branch) {
14
+ if (entry.type !== "custom")
15
+ continue;
16
+ if (entry.customType !== STORE_CUSTOM_TYPE)
17
+ continue;
18
+ const snap = entry.data;
19
+ if (!snap || snap.namespace !== namespace || snap.state === undefined)
20
+ continue;
21
+ if (!latest || snap.ts >= latest.ts)
22
+ latest = snap;
23
+ }
24
+ return latest?.state;
25
+ }
26
+ export function hasStateInBranch(branch, namespace) {
27
+ return loadStateFromBranch(branch, namespace) !== undefined;
28
+ }
29
+ /** 追加快照:优先走 sessionManager.appendCustomEntry,否则回退到回调。 */
30
+ export function appendSnapshot(mgr, namespace, state, fallbackSave) {
31
+ const snapshot = {
32
+ namespace,
33
+ version: SNAPSHOT_VERSION,
34
+ state,
35
+ ts: Date.now(),
36
+ };
37
+ if (mgr?.appendCustomEntry) {
38
+ try {
39
+ mgr.appendCustomEntry(STORE_CUSTOM_TYPE, snapshot);
40
+ return;
41
+ }
42
+ catch {
43
+ // 回退到内存
44
+ }
45
+ }
46
+ fallbackSave?.(snapshot);
47
+ }
@@ -210,6 +210,9 @@ export class SettingsService {
210
210
  reviewSkills,
211
211
  extensions,
212
212
  presets: this.presets.map((p) => ({ ...p })),
213
+ ...(this.host.getMarkerState
214
+ ? this.host.getMarkerState()
215
+ : { markersEnabled: true, disabledMarkers: [], markers: [] }),
213
216
  subagentTemplates: this.templates.list(),
214
217
  subagentDefaultTemplates: DEFAULT_TEMPLATES.map((t) => t.name),
215
218
  },
@@ -4,6 +4,13 @@
4
4
  * sync with exec(). */
5
5
  export const NATIVE_COMMANDS = [
6
6
  { name: "new", description: "新建对话", descriptionEn: "New chat" },
7
+ {
8
+ name: "name",
9
+ description: "重命名当前会话",
10
+ descriptionEn: "Set session display name",
11
+ argumentHint: "<名称>",
12
+ argumentHintEn: "<name>",
13
+ },
7
14
  {
8
15
  name: "model",
9
16
  description: "切换模型",
@@ -131,6 +138,33 @@ export class SlashCommandsService {
131
138
  case "new":
132
139
  await this.host.newChat();
133
140
  return true;
141
+ case "name": {
142
+ const trimmed = args.trim();
143
+ if (!trimmed) {
144
+ const current = this.host.getSession().sessionName;
145
+ this.host.emit({
146
+ type: "notice",
147
+ level: "info",
148
+ text: current ? `当前会话名称:${current}。用法:/name <名称>` : `用法:/name <名称>`,
149
+ textEn: current ? `Current session name: ${current}. Usage: /name <name>` : `Usage: /name <name>`,
150
+ });
151
+ return true;
152
+ }
153
+ if (this.host.renameSession) {
154
+ await this.host.renameSession(trimmed);
155
+ }
156
+ else {
157
+ this.host.getSession().setSessionName(trimmed);
158
+ await this.host.refreshSessions();
159
+ this.host.emit({
160
+ type: "notice",
161
+ level: "info",
162
+ text: `已重命名当前会话为「${trimmed}」`,
163
+ textEn: `Renamed current session to "${trimmed}"`,
164
+ });
165
+ }
166
+ return true;
167
+ }
134
168
  case "model": {
135
169
  if (!args) {
136
170
  const current = this.host.getSession().model;
@@ -1240,6 +1240,17 @@ export class TerminalManager {
1240
1240
  if (this.history.delete(id))
1241
1241
  this.emitList();
1242
1242
  }
1243
+ /** Rename a terminal tab (live or retained history). Empty names ignored. */
1244
+ rename(id, title) {
1245
+ const trimmed = (title ?? "").trim();
1246
+ if (!trimmed)
1247
+ return;
1248
+ const entry = this.find(id);
1249
+ if (!entry)
1250
+ return;
1251
+ entry.title = trimmed;
1252
+ this.emitList();
1253
+ }
1243
1254
  /** Kill every terminal owned by this conversation. */
1244
1255
  killAll() {
1245
1256
  for (const entry of this.terms.values()) {
@@ -1521,10 +1532,10 @@ export const TERMINAL_TOOL_NAMES = [
1521
1532
  /** System-prompt guidance teaching the model WHEN to prefer the terminal tools
1522
1533
  * over one-shot bash. Without it models almost never pick them — bash returns
1523
1534
  * complete output in a single call, so it always wins on convenience. */
1524
- 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:
1525
- - 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'.
1526
- - 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).
1527
- - The user explicitly asks you to work in the visible terminal panel.
1535
+ 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:
1536
+ - 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'.
1537
+ - 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).
1538
+ - The user explicitly asks you to work in the visible terminal panel.
1528
1539
  Use head/tail on bash to trim verbose output instead of piping through head/tail. 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.`;
1529
1540
  /** Build the agent-facing persistent terminal tools for one conversation. */
1530
1541
  export function makePersistentTerminalTools(terminals, cwd) {
@@ -16,9 +16,12 @@ import { join } from "node:path";
16
16
  const ID_RE = /^[A-Za-z0-9_-]+$/;
17
17
  /** Display-name marker inside a theme css file (first lines):
18
18
  * `/* theme-name: 中文名 *∕` — falls back to the file id when absent.
19
+ * `/* theme-name-en: English Name *∕` — optional English label; falls back
20
+ * to `name` when absent.
19
21
  * Lets built-in AND user themes carry a human-readable label while the
20
22
  * filename stays ASCII (id must match ID_RE). */
21
23
  const THEME_NAME_RE = /\/\*\s*theme-name:\s*(.+?)\s*\*\//;
24
+ const THEME_NAME_EN_RE = /\/\*\s*theme-name-en:\s*(.+?)\s*\*\//;
22
25
  function readDisplayName(path, fallback) {
23
26
  try {
24
27
  const head = readFileSync(path, "utf8").slice(0, 300);
@@ -28,6 +31,15 @@ function readDisplayName(path, fallback) {
28
31
  return fallback;
29
32
  }
30
33
  }
34
+ function readDisplayNameEn(path) {
35
+ try {
36
+ const head = readFileSync(path, "utf8").slice(0, 300);
37
+ return head.match(THEME_NAME_EN_RE)?.[1]?.trim() || undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
31
43
  export function listThemes(builtinDir, userDir) {
32
44
  const scan = (dir, builtin) => {
33
45
  if (!existsSync(dir))
@@ -36,11 +48,18 @@ export function listThemes(builtinDir, userDir) {
36
48
  .filter((f) => f.endsWith(".css"))
37
49
  .filter((f) => ID_RE.test(f.slice(0, -4)))
38
50
  .sort()
39
- .map((f) => ({
40
- id: f.slice(0, -4),
41
- name: readDisplayName(join(dir, f), f.slice(0, -4)),
42
- builtin,
43
- }));
51
+ .map((f) => {
52
+ const path = join(dir, f);
53
+ const id = f.slice(0, -4);
54
+ const name = readDisplayName(path, id);
55
+ const nameEn = readDisplayNameEn(path);
56
+ return {
57
+ id,
58
+ name,
59
+ builtin,
60
+ ...(nameEn ? { nameEn } : {}),
61
+ };
62
+ });
44
63
  };
45
64
  const builtin = scan(builtinDir, true);
46
65
  const user = scan(userDir, false);
@@ -35,15 +35,15 @@ export function findVisionModels(runtime) {
35
35
  * Exported so the settings panel can offer a custom prompt (append to this
36
36
  * default or replace it entirely).
37
37
  */
38
- export const SYSTEM_PROMPT = `You are a vision bridge for a text-only language model. You receive one or more images and must transcribe them into precise, structured text evidence so another model that cannot see images can answer questions about them accurately.
39
-
40
- Follow these rules:
41
- 1. Transcribe ALL visible text verbatim, preserving wording, spelling, punctuation and line breaks. This is the most important part — the reader relies on your transcription, not on the image.
42
- 2. Describe the layout in reading order: headers, paragraphs, lists, tables, buttons, panels — say what appears where.
43
- 3. For tables/charts/diagrams: read axes, scales (note log scale), legend entries, series names, highlighted points and their coordinates, and any data values you can discern.
44
- 4. Name entities: people, products, companies, colors, style, objects, actions.
45
- 5. If part of the image is too blurry/low-resolution to read, say "(读不清)" or "unclear" for that part — NEVER invent or guess content you cannot see.
46
- 6. If there are multiple images, address them in order (图 1 / Image 1, 图 2 / Image 2, ...).
38
+ export const SYSTEM_PROMPT = `You are a vision bridge for a text-only language model. You receive one or more images and must transcribe them into precise, structured text evidence so another model that cannot see images can answer questions about them accurately.
39
+
40
+ Follow these rules:
41
+ 1. Transcribe ALL visible text verbatim, preserving wording, spelling, punctuation and line breaks. This is the most important part — the reader relies on your transcription, not on the image.
42
+ 2. Describe the layout in reading order: headers, paragraphs, lists, tables, buttons, panels — say what appears where.
43
+ 3. For tables/charts/diagrams: read axes, scales (note log scale), legend entries, series names, highlighted points and their coordinates, and any data values you can discern.
44
+ 4. Name entities: people, products, companies, colors, style, objects, actions.
45
+ 5. If part of the image is too blurry/low-resolution to read, say "(读不清)" or "unclear" for that part — NEVER invent or guess content you cannot see.
46
+ 6. If there are multiple images, address them in order (图 1 / Image 1, 图 2 / Image 2, ...).
47
47
  7. Output only the transcript. No preamble, no commentary about the image itself.`;
48
48
  /**
49
49
  * Assemble the final vision-model system prompt from the settings-panel prefs.