@raingor/pi-web-switch 0.6.1 → 0.7.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/index.html CHANGED
@@ -9,8 +9,8 @@
9
9
  <link rel="manifest" href="./manifest.webmanifest" />
10
10
  <meta name="theme-color" content="#05090d" />
11
11
  <meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
12
- <script type="module" crossorigin src="./assets/main-CqkZxauO.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/main-DiEs78Xc.css">
12
+ <script type="module" crossorigin src="./assets/main-E7tKVWGH.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/main-DHTj6wfH.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@raingor/pi-web-switch",
3
3
  "private": false,
4
- "version": "0.6.1",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.html",
7
7
  "description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
@@ -2754,6 +2754,60 @@ export function readSubagents(): SubagentsData {
2754
2754
  };
2755
2755
  }
2756
2756
 
2757
+ const AGENT_NAME_RE = /^[\w.-]+\.md$/;
2758
+
2759
+ /**
2760
+ * Patch a single agent's frontmatter field (model / thinking) in place while
2761
+ * preserving the rest of the file byte-for-byte. Rewriting from the parsed
2762
+ * AgentDef would be lossy (body is truncated to 500 chars in listAgents), so we
2763
+ * edit the original file's YAML frontmatter line directly.
2764
+ *
2765
+ * - Updating `model` to a non-empty value replaces or inserts the line.
2766
+ * - An empty string removes the field so the agent falls back to the default.
2767
+ */
2768
+ export function updateAgentFields(
2769
+ fileName: string,
2770
+ patch: { model?: string; thinking?: string }
2771
+ ): boolean {
2772
+ try {
2773
+ // Guard against path traversal: only bare *.md names in the agents dir.
2774
+ if (!AGENT_NAME_RE.test(fileName)) return false;
2775
+ const filePath = join(AGENTS_DIR, fileName);
2776
+ if (!existsSync(filePath)) return false;
2777
+
2778
+ const raw = readFileSync(filePath, "utf-8");
2779
+ if (raw.indexOf("---") !== 0) return false;
2780
+ const second = raw.indexOf("---", 3);
2781
+ if (second === -1) return false;
2782
+
2783
+ const fmBlock = raw.slice(3, second);
2784
+ const rest = raw.slice(second); // starts at closing "---"
2785
+ const lines = fmBlock.replace(/^\n/, "").split("\n");
2786
+
2787
+ const setField = (key: string, value: string | undefined) => {
2788
+ const idx = lines.findIndex((l) => l.trimStart().toLowerCase().startsWith(`${key}:`));
2789
+ if (value === undefined || value === "") {
2790
+ // Remove the field entirely when cleared.
2791
+ if (idx !== -1) lines.splice(idx, 1);
2792
+ return;
2793
+ }
2794
+ const newLine = `${key}: ${value}`;
2795
+ if (idx !== -1) lines[idx] = newLine;
2796
+ else lines.push(newLine);
2797
+ };
2798
+
2799
+ if (patch.model !== undefined) setField("model", patch.model.trim());
2800
+ if (patch.thinking !== undefined) setField("thinking", patch.thinking.trim());
2801
+
2802
+ const newFm = lines.join("\n");
2803
+ const out = `---\n${newFm}\n${rest}`;
2804
+ writeFileSync(filePath, out, "utf-8");
2805
+ return true;
2806
+ } catch {
2807
+ return false;
2808
+ }
2809
+ }
2810
+
2757
2811
  // ─── Built-in Provider Catalog (from the local pi install) ───
2758
2812
  // pi ships its full model catalog (same source as pi.dev/models) inside
2759
2813
  // @earendil-works/pi-ai as dist/providers/data/*.json. Reading it locally
@@ -0,0 +1,97 @@
1
+ import { useState } from "react";
2
+ import { HelpCircle, Send } from "lucide-react";
3
+ import { Modal } from "@/components/ui/Modal";
4
+ import { useTranslation } from "@/lib/i18n";
5
+
6
+ // Telegram group invite link. Clicking opens the Telegram app (or web) join
7
+ // confirmation — Telegram has no API to add users to a group without their
8
+ // consent, so an invite link is the compliant "one-click join" path.
9
+ const TELEGRAM_GROUP_URL = "https://t.me/+ODpy7_7NlOE4NzA1";
10
+
11
+ /**
12
+ * Floating help button (bottom-right) that opens a usage guide covering the
13
+ * whole app plus a per-module how-to. All copy is i18n-keyed so it tracks the
14
+ * active locale.
15
+ */
16
+
17
+ // Module sections shown in the guide. Order mirrors the sidebar navigation.
18
+ const SECTIONS: { titleKey: string; descKey: string; stepKeys: string[] }[] = [
19
+ { titleKey: "help.dashboard_title", descKey: "help.dashboard_desc", stepKeys: ["help.dashboard_s1", "help.dashboard_s2"] },
20
+ { titleKey: "help.sessions_title", descKey: "help.sessions_desc", stepKeys: ["help.sessions_s1", "help.sessions_s2", "help.sessions_s3"] },
21
+ { titleKey: "help.memory_title", descKey: "help.memory_desc", stepKeys: ["help.memory_s1", "help.memory_s2", "help.memory_s3"] },
22
+ { titleKey: "help.providers_title", descKey: "help.providers_desc", stepKeys: ["help.providers_s1", "help.providers_s2", "help.providers_s3"] },
23
+ { titleKey: "help.subagents_title", descKey: "help.subagents_desc", stepKeys: ["help.subagents_s1", "help.subagents_s2"] },
24
+ { titleKey: "help.settings_title", descKey: "help.settings_desc", stepKeys: ["help.settings_s1", "help.settings_s2"] },
25
+ { titleKey: "help.speedtest_title", descKey: "help.speedtest_desc", stepKeys: ["help.speedtest_s1", "help.speedtest_s2", "help.speedtest_s3"] },
26
+ ];
27
+
28
+ export function HelpButton() {
29
+ const { t } = useTranslation();
30
+ const [open, setOpen] = useState(false);
31
+
32
+ return (
33
+ <>
34
+ <button
35
+ onClick={() => setOpen(true)}
36
+ className="help-fab"
37
+ aria-label={t("help.button")}
38
+ title={t("help.button")}
39
+ >
40
+ <HelpCircle className="h-5 w-5" />
41
+ </button>
42
+
43
+ <Modal open={open} onClose={() => setOpen(false)} title={t("help.title")} size="xl">
44
+ <div className="space-y-6">
45
+ {/* Project overview */}
46
+ <section>
47
+ <h3 className="text-sm font-semibold" style={{ color: "var(--page-text)" }}>{t("help.overview_title")}</h3>
48
+ <p className="mt-1.5 text-sm leading-relaxed" style={{ color: "var(--muted-text)" }}>{t("help.overview_desc")}</p>
49
+ </section>
50
+
51
+ {/* Per-module guide */}
52
+ <section className="space-y-4">
53
+ <h3 className="text-sm font-semibold" style={{ color: "var(--page-text)" }}>{t("help.modules_title")}</h3>
54
+ {SECTIONS.map((s) => (
55
+ <div
56
+ key={s.titleKey}
57
+ className="rounded-lg border px-4 py-3"
58
+ style={{ borderColor: "var(--card-border)", backgroundColor: "var(--card-bg)" }}
59
+ >
60
+ <h4 className="text-sm font-semibold" style={{ color: "var(--page-text)" }}>{t(s.titleKey)}</h4>
61
+ <p className="mt-1 text-xs leading-relaxed" style={{ color: "var(--muted-text)" }}>{t(s.descKey)}</p>
62
+ {s.stepKeys.length > 0 && (
63
+ <ul className="mt-2 space-y-1">
64
+ {s.stepKeys.map((k) => (
65
+ <li key={k} className="flex gap-2 text-xs leading-relaxed" style={{ color: "var(--subtle-text)" }}>
66
+ <span style={{ color: "var(--signal-cyan, #38bdf8)" }}>›</span>
67
+ <span>{t(k)}</span>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ )}
72
+ </div>
73
+ ))}
74
+ </section>
75
+ {/* Contact / join group */}
76
+ <section
77
+ className="rounded-lg border px-4 py-3"
78
+ style={{ borderColor: "color-mix(in srgb, var(--signal-cyan) 30%, var(--card-border))", backgroundColor: "var(--card-bg)" }}
79
+ >
80
+ <h3 className="text-sm font-semibold" style={{ color: "var(--page-text)" }}>{t("help.contact_title")}</h3>
81
+ <p className="mt-1 text-xs leading-relaxed" style={{ color: "var(--muted-text)" }}>{t("help.contact_desc")}</p>
82
+ <a
83
+ href={TELEGRAM_GROUP_URL}
84
+ target="_blank"
85
+ rel="noreferrer"
86
+ className="mt-3 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors"
87
+ style={{ backgroundColor: "#229ED9" }}
88
+ >
89
+ <Send className="h-4 w-4" />
90
+ {t("help.contact_join")}
91
+ </a>
92
+ </section>
93
+ </div>
94
+ </Modal>
95
+ </>
96
+ );
97
+ }
@@ -2,6 +2,7 @@ import { useState } from "react";
2
2
  import { Outlet, useLocation } from "react-router-dom";
3
3
  import { Menu, RadioTower } from "lucide-react";
4
4
  import { Sidebar } from "./Sidebar";
5
+ import { HelpButton } from "@/components/help/HelpButton";
5
6
 
6
7
  export function AppShell() {
7
8
  const location = useLocation();
@@ -52,6 +53,8 @@ export function AppShell() {
52
53
  )}
53
54
  </main>
54
55
  </div>
56
+
57
+ <HelpButton />
55
58
  </div>
56
59
  );
57
60
  }
@@ -11,11 +11,15 @@ import {
11
11
  X,
12
12
  Orbit,
13
13
  Gauge,
14
+ Send,
14
15
  } from "lucide-react";
15
16
  import { cn } from "@/lib/utils";
16
17
  import { useTranslation, LANGUAGES } from "@/lib/i18n";
17
18
  import { useState } from "react";
18
19
 
20
+ // Telegram group invite link (same as the help dialog).
21
+ const TELEGRAM_GROUP_URL = "https://t.me/+ODpy7_7NlOE4NzA1";
22
+
19
23
  const navItems = [
20
24
  { to: "/", icon: LayoutDashboard, key: "nav.dashboard", code: "01" },
21
25
  { to: "/sessions", icon: History, key: "nav.sessions", code: "02" },
@@ -76,6 +80,16 @@ export function Sidebar({ mobileOpen = false, onClose }: SidebarProps) {
76
80
  </nav>
77
81
 
78
82
  <div className="sidebar-footer">
83
+ <a
84
+ href={TELEGRAM_GROUP_URL}
85
+ target="_blank"
86
+ rel="noreferrer"
87
+ className="sidebar-telegram"
88
+ >
89
+ <Send className="h-4 w-4" />
90
+ <span>{t("help.contact_join")}</span>
91
+ </a>
92
+
79
93
  <div className="node-status">
80
94
  <div className="node-status-icon"><Orbit className="h-4 w-4" /></div>
81
95
  <div>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { useTranslation } from "@/lib/i18n";
3
+ import { useConfigStore } from "@/store/config-store";
3
4
  import { Badge } from "@/components/ui/Badge";
4
5
  import { EmptyState } from "@/components/ui/EmptyState";
5
6
  import { formatTokens } from "@/lib/utils";
@@ -16,6 +17,9 @@ import {
16
17
  Loader2,
17
18
  Search,
18
19
  ExternalLink,
20
+ Pencil,
21
+ Check,
22
+ X,
19
23
  } from "lucide-react";
20
24
 
21
25
  const API_BASE = "/api/pi";
@@ -226,7 +230,7 @@ function AgentList({
226
230
  {/* Right: Agent detail */}
227
231
  <div className="min-w-0 flex-1 p-6">
228
232
  {selected ? (
229
- <AgentDetail agent={selected} />
233
+ <AgentDetail agent={selected} onSaved={onRefresh} />
230
234
  ) : (
231
235
  <div className="flex h-40 items-center justify-center text-sm text-gray-500">
232
236
  {t("providers_models.select_hint")}
@@ -237,8 +241,47 @@ function AgentList({
237
241
  );
238
242
  }
239
243
 
240
- function AgentDetail({ agent }: { agent: AgentDef }) {
244
+ function AgentDetail({ agent, onSaved }: { agent: AgentDef; onSaved: () => void }) {
241
245
  const { t } = useTranslation();
246
+ const { allModels } = useConfigStore();
247
+ const [editing, setEditing] = useState(false);
248
+ const [model, setModel] = useState(agent.model ?? "");
249
+ const [thinking, setThinking] = useState(agent.thinking ?? "");
250
+ const [saving, setSaving] = useState(false);
251
+ const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
252
+
253
+ // Reset the draft whenever a different agent is selected.
254
+ useEffect(() => {
255
+ setModel(agent.model ?? "");
256
+ setThinking(agent.thinking ?? "");
257
+ setEditing(false);
258
+ setMsg(null);
259
+ }, [agent.fileName]);
260
+
261
+ const handleSave = async () => {
262
+ setSaving(true);
263
+ setMsg(null);
264
+ try {
265
+ const res = await fetch(`${API_BASE}/subagents/update-agent`, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/json" },
268
+ body: JSON.stringify({ fileName: agent.fileName, model: model.trim(), thinking: thinking.trim() }),
269
+ });
270
+ const { success } = (await res.json()) as { success: boolean };
271
+ if (success) {
272
+ setMsg({ ok: true, text: t("subagents.saved") });
273
+ setEditing(false);
274
+ onSaved();
275
+ } else {
276
+ setMsg({ ok: false, text: t("subagents.save_failed") });
277
+ }
278
+ } catch {
279
+ setMsg({ ok: false, text: t("subagents.save_failed") });
280
+ } finally {
281
+ setSaving(false);
282
+ }
283
+ };
284
+
242
285
  return (
243
286
  <div className="space-y-4">
244
287
  <div className="flex items-center gap-3">
@@ -246,23 +289,94 @@ function AgentDetail({ agent }: { agent: AgentDef }) {
246
289
  <Badge variant={agent.package === "custom" ? "default" : "info"}>
247
290
  {agent.package}
248
291
  </Badge>
292
+ {agent.package === "custom" && !editing && (
293
+ <button
294
+ onClick={() => { setMsg(null); setEditing(true); }}
295
+ className="ml-auto flex items-center gap-1.5 rounded-lg border border-gray-700 px-2.5 py-1 text-xs text-gray-300 hover:bg-gray-800"
296
+ >
297
+ <Pencil className="h-3.5 w-3.5" />
298
+ {t("subagents.edit")}
299
+ </button>
300
+ )}
249
301
  </div>
250
302
 
251
303
  <p className="text-sm text-gray-400">{agent.description}</p>
252
304
 
253
305
  <div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-800 bg-gray-900/70 p-4">
254
- {agent.model && (
255
- <div>
256
- <span className="text-xs text-gray-500">{t("subagents.model")}</span>
257
- <p className="mt-0.5 text-sm text-gray-200 font-mono">{agent.model}</p>
306
+ {/* Model — editable for custom agents */}
307
+ <div className={editing ? "col-span-2" : ""}>
308
+ <span className="text-xs text-gray-500">{t("subagents.model")}</span>
309
+ {editing ? (
310
+ <>
311
+ <input
312
+ type="text"
313
+ list="agent-model-options"
314
+ value={model}
315
+ onChange={(e) => setModel(e.target.value)}
316
+ placeholder={t("subagents.model_placeholder")}
317
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 font-mono text-sm text-gray-100 outline-none focus:ring-1 focus:ring-blue-500"
318
+ />
319
+ <datalist id="agent-model-options">
320
+ {allModels.map((m) => (
321
+ <option key={`${m.providerId}/${m.id}`} value={`${m.providerId}/${m.id}`}>
322
+ {m.providerName} · {m.name ?? m.id}
323
+ </option>
324
+ ))}
325
+ </datalist>
326
+ </>
327
+ ) : (
328
+ <p className="mt-0.5 text-sm text-gray-200 font-mono">{agent.model || t("subagents.model_default")}</p>
329
+ )}
330
+ </div>
331
+
332
+ {/* Thinking — editable for custom agents */}
333
+ <div>
334
+ <span className="text-xs text-gray-500">{t("subagents.thinking")}</span>
335
+ {editing ? (
336
+ <select
337
+ value={thinking}
338
+ onChange={(e) => setThinking(e.target.value)}
339
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-100 outline-none focus:ring-1 focus:ring-blue-500"
340
+ >
341
+ <option value="">{t("subagents.thinking_default")}</option>
342
+ {["off", "minimal", "low", "medium", "high", "xhigh"].map((lvl) => (
343
+ <option key={lvl} value={lvl}>{lvl}</option>
344
+ ))}
345
+ </select>
346
+ ) : (
347
+ <p className="mt-0.5 text-sm text-gray-200">{agent.thinking || t("subagents.thinking_default")}</p>
348
+ )}
349
+ </div>
350
+
351
+ {editing && (
352
+ <div className="col-span-2 flex items-center gap-3">
353
+ <button
354
+ onClick={handleSave}
355
+ disabled={saving}
356
+ className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-50"
357
+ >
358
+ {saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
359
+ {saving ? t("subagents.saving") : t("subagents.save")}
360
+ </button>
361
+ <button
362
+ onClick={() => { setEditing(false); setModel(agent.model ?? ""); setThinking(agent.thinking ?? ""); }}
363
+ disabled={saving}
364
+ className="flex items-center gap-1.5 rounded-lg border border-gray-700 px-4 py-1.5 text-sm text-gray-300 hover:bg-gray-800"
365
+ >
366
+ <X className="h-3.5 w-3.5" />
367
+ {t("subagents.cancel")}
368
+ </button>
369
+ {msg && (
370
+ <span className={`text-xs ${msg.ok ? "text-emerald-400" : "text-red-400"}`}>{msg.text}</span>
371
+ )}
258
372
  </div>
259
373
  )}
260
- {agent.thinking && (
261
- <div>
262
- <span className="text-xs text-gray-500">{t("subagents.thinking")}</span>
263
- <p className="mt-0.5 text-sm text-gray-200">{agent.thinking}</p>
374
+ {!editing && msg && (
375
+ <div className="col-span-2">
376
+ <span className={`text-xs ${msg.ok ? "text-emerald-400" : "text-red-400"}`}>{msg.text}</span>
264
377
  </div>
265
378
  )}
379
+
266
380
  {agent.tools && (
267
381
  <div className="col-span-2">
268
382
  <span className="text-xs text-gray-500">{t("subagents.tools")}</span>
package/src/index.css CHANGED
@@ -657,3 +657,56 @@ table { border-collapse: collapse; }
657
657
  .official-usage-field-wide { grid-column: auto; }
658
658
  .official-usage-cards { grid-template-columns: 1fr; }
659
659
  }
660
+
661
+ /* ─── Help floating action button ───────────────────────── */
662
+ .help-fab {
663
+ position: fixed;
664
+ right: 22px;
665
+ bottom: 22px;
666
+ z-index: 55;
667
+ display: flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ width: 44px;
671
+ height: 44px;
672
+ border-radius: 50%;
673
+ border: 1px solid color-mix(in srgb, var(--signal-cyan) 40%, var(--card-border));
674
+ background: var(--card-bg-solid);
675
+ color: var(--signal-cyan);
676
+ box-shadow: 0 10px 30px rgba(0,0,0,.28), 0 0 24px color-mix(in srgb, var(--signal-cyan) 14%, transparent);
677
+ cursor: pointer;
678
+ transition: transform 160ms ease, box-shadow 160ms ease, color 160ms ease;
679
+ }
680
+ .help-fab:hover {
681
+ transform: translateY(-2px);
682
+ color: var(--signal-cyan-bright);
683
+ box-shadow: 0 14px 38px rgba(0,0,0,.34), 0 0 34px color-mix(in srgb, var(--signal-cyan) 22%, transparent);
684
+ }
685
+ @media (max-width: 768px) {
686
+ .help-fab { right: 14px; bottom: 14px; width: 40px; height: 40px; }
687
+ }
688
+
689
+ /* ─── Sidebar Telegram join button ──────────────────────── */
690
+ .sidebar-telegram {
691
+ display: flex;
692
+ align-items: center;
693
+ justify-content: center;
694
+ gap: 8px;
695
+ width: 100%;
696
+ margin-bottom: 10px;
697
+ padding: 9px 12px;
698
+ border-radius: 9px;
699
+ border: 1px solid color-mix(in srgb, #229ED9 45%, var(--card-border));
700
+ background: color-mix(in srgb, #229ED9 12%, transparent);
701
+ color: #229ED9;
702
+ font-size: 12px;
703
+ font-weight: 600;
704
+ letter-spacing: .01em;
705
+ cursor: pointer;
706
+ transition: background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
707
+ }
708
+ .sidebar-telegram:hover {
709
+ background: color-mix(in srgb, #229ED9 20%, transparent);
710
+ transform: translateY(-1px);
711
+ box-shadow: 0 8px 22px color-mix(in srgb, #229ED9 22%, transparent);
712
+ }
@@ -140,6 +140,15 @@ const en: Record<string, string> = {
140
140
  "subagents.no_history": "No run history",
141
141
  "subagents.no_history_desc": "Subagent run history will appear here after agents are executed.",
142
142
  "subagents.model": "Model",
143
+ "subagents.edit": "Edit",
144
+ "subagents.save": "Save",
145
+ "subagents.saving": "Saving…",
146
+ "subagents.cancel": "Cancel",
147
+ "subagents.saved": "Saved",
148
+ "subagents.save_failed": "Save failed",
149
+ "subagents.model_placeholder": "provider/model-id (blank = default)",
150
+ "subagents.model_default": "(default)",
151
+ "subagents.thinking_default": "(default)",
143
152
  "subagents.thinking": "Thinking Level",
144
153
  "subagents.tools": "Tools",
145
154
  "subagents.system_prompt_mode": "System Prompt Mode",
@@ -474,6 +483,46 @@ const en: Record<string, string> = {
474
483
  "language.zh-CN": "简体中文",
475
484
  "language.zh-TW": "繁體中文",
476
485
  "language.ja": "日本語",
486
+ "help.button": "User Guide",
487
+ "help.title": "User Guide",
488
+ "help.overview_title": "About this project",
489
+ "help.overview_desc": "pi-web-switch is a visual control panel for the pi coding agent: review sessions, manage long-term memory, configure providers and models, edit subagents, benchmark models, and adjust global settings. All changes write directly to the config files under ~/.pi/agent and take effect immediately.",
490
+ "help.modules_title": "Modules",
491
+ "help.dashboard_title": "01 Dashboard",
492
+ "help.dashboard_desc": "Overview of usage and recent activity.",
493
+ "help.dashboard_s1": "See token usage, cost, and per-model stats.",
494
+ "help.dashboard_s2": "Click \"Refresh now\" to rescan session data.",
495
+ "help.sessions_title": "02 Sessions",
496
+ "help.sessions_desc": "Browse, preview, and manage past sessions.",
497
+ "help.sessions_s1": "Sessions are grouped by project; expand/collapse each.",
498
+ "help.sessions_s2": "Deleting a session moves it to Trash — restore or purge.",
499
+ "help.sessions_s3": "In Trash you can select and permanently delete in bulk.",
500
+ "help.memory_title": "03 Memory",
501
+ "help.memory_desc": "View and optimize pi-hermes-memory long-term memory.",
502
+ "help.memory_s1": "Progress bars show each file’s capacity usage (by characters).",
503
+ "help.memory_s2": "\"Optimize Memory\" runs a model to merge duplicates and drop stale entries.",
504
+ "help.memory_s3": "\"Memory Model\" sets the auto-write model, thinking level, and capacity limits.",
505
+ "help.providers_title": "04 Providers & Models",
506
+ "help.providers_desc": "Manage API providers and their models.",
507
+ "help.providers_s1": "Add a custom provider with its baseUrl and API key.",
508
+ "help.providers_s2": "Use \"Fetch models\" to pull and search available models, then enable them.",
509
+ "help.providers_s3": "Adjust per-model params like maxTokens.",
510
+ "help.subagents_title": "05 Subagents",
511
+ "help.subagents_desc": "View and edit subagents, chains, and run history.",
512
+ "help.subagents_s1": "In \"Agents\", select a custom agent and click \"Edit\" to change its model and thinking level.",
513
+ "help.subagents_s2": "The model field has a dropdown of all configured models.",
514
+ "help.settings_title": "06 Settings",
515
+ "help.settings_desc": "Appearance, model defaults, and advanced options.",
516
+ "help.settings_s1": "Switch theme, font size, UI zoom, and language.",
517
+ "help.settings_s2": "Manage pi packages and check for updates.",
518
+ "help.speedtest_title": "07 Model Speed Test",
519
+ "help.speedtest_desc": "Batch-test custom provider models for availability and latency.",
520
+ "help.speedtest_s1": "Pick a provider and use \"Fetch models\" to load the test set (stored separately).",
521
+ "help.speedtest_s2": "Run the test to see success rate, average latency, and latency range.",
522
+ "help.speedtest_s3": "Enable \"Slow mode\" to reduce 429 rate limits.",
523
+ "help.contact_title": "Need help?",
524
+ "help.contact_desc": "For questions, suggestions, or bug reports, join our Telegram group — click the button below to join.",
525
+ "help.contact_join": "Join Telegram group",
477
526
  };
478
527
 
479
528
  export default en;
@@ -138,6 +138,15 @@ const ja: Record<string, string> = {
138
138
  "subagents.no_history": "実行履歴がありません",
139
139
  "subagents.no_history_desc": "エージェントを実行すると、履歴がここに表示されます。",
140
140
  "subagents.model": "モデル",
141
+ "subagents.edit": "編集",
142
+ "subagents.save": "保存",
143
+ "subagents.saving": "保存中…",
144
+ "subagents.cancel": "キャンセル",
145
+ "subagents.saved": "保存しました",
146
+ "subagents.save_failed": "保存に失敗",
147
+ "subagents.model_placeholder": "provider/model-id(空欄で既定)",
148
+ "subagents.model_default": "(既定)",
149
+ "subagents.thinking_default": "(既定)",
141
150
  "subagents.thinking": "思考レベル",
142
151
  "subagents.tools": "ツール",
143
152
  "subagents.system_prompt_mode": "システムプロンプトモード",
@@ -464,6 +473,46 @@ const ja: Record<string, string> = {
464
473
  "language.zh-CN": "简体中文",
465
474
  "language.zh-TW": "繁體中文",
466
475
  "language.ja": "日本語",
476
+ "help.button": "使い方",
477
+ "help.title": "使い方ガイド",
478
+ "help.overview_title": "このプロジェクトについて",
479
+ "help.overview_desc": "pi-web-switch は pi コーディングエージェントのビジュアル設定パネルです。セッション確認、長期メモリ管理、プロバイダーとモデルの設定、サブエージェント編集、モデル速度計測、全体設定の調整ができます。変更は ~/.pi/agent 配下の設定ファイルに直接書き込まれ、即時反映されます。",
480
+ "help.modules_title": "各モジュール",
481
+ "help.dashboard_title": "01 ダッシュボード",
482
+ "help.dashboard_desc": "使用量と最近の活動の概要。",
483
+ "help.dashboard_s1": "トークン使用量、コスト、モデル別統計を確認。",
484
+ "help.dashboard_s2": "「今すぐ更新」でセッションデータを再スキャン。",
485
+ "help.sessions_title": "02 セッション",
486
+ "help.sessions_desc": "過去のセッションを閲覧・プレビュー・管理。",
487
+ "help.sessions_s1": "セッションはプロジェクト別にグループ化、展開/折りたたみ可能。",
488
+ "help.sessions_s2": "削除はゴミ箱へ移動 — 復元または完全削除。",
489
+ "help.sessions_s3": "ゴミ箱で選択して一括完全削除できます。",
490
+ "help.memory_title": "03 メモリ",
491
+ "help.memory_desc": "pi-hermes-memory の長期メモリを表示・最適化。",
492
+ "help.memory_s1": "プログレスバーが各ファイルの容量(文字数)を表示。",
493
+ "help.memory_s2": "「メモリを最適化」でモデルが重複統合と古い項目の削除を実行。",
494
+ "help.memory_s3": "「メモリモデル設定」で自動書き込みモデル、思考レベル、容量上限を設定。",
495
+ "help.providers_title": "04 プロバイダーとモデル",
496
+ "help.providers_desc": "API プロバイダーとそのモデルを管理。",
497
+ "help.providers_s1": "baseUrl と API キーでカスタムプロバイダーを追加。",
498
+ "help.providers_s2": "「モデル取得」で利用可能なモデルを取得・検索し、有効化。",
499
+ "help.providers_s3": "モデルごとの maxTokens などを調整。",
500
+ "help.subagents_title": "05 サブエージェント",
501
+ "help.subagents_desc": "サブエージェント、チェーン、実行履歴を表示・編集。",
502
+ "help.subagents_s1": "「エージェント」でカスタムエージェントを選び「編集」でモデルと思考レベルを変更。",
503
+ "help.subagents_s2": "モデル入力欄に設定済みモデルのドロップダウン候補付き。",
504
+ "help.settings_title": "06 設定",
505
+ "help.settings_desc": "外観、モデル既定値、詳細オプション。",
506
+ "help.settings_s1": "テーマ、フォントサイズ、UI ズーム、言語を切替。",
507
+ "help.settings_s2": "pi パッケージ管理と更新確認。",
508
+ "help.speedtest_title": "07 モデル速度計測",
509
+ "help.speedtest_desc": "カスタムプロバイダーのモデルの可用性と遅延を一括テスト。",
510
+ "help.speedtest_s1": "プロバイダーを選び「モデル取得」でテスト対象を読み込み(別途保存)。",
511
+ "help.speedtest_s2": "テスト実行で成功率、平均遅延、遅延範囲を確認。",
512
+ "help.speedtest_s3": "レート制限時は「低速モード」で 429 を軽減。",
513
+ "help.contact_title": "お困りですか?",
514
+ "help.contact_desc": "ご質問・ご提案・バグ報告は Telegram グループへ。下のボタンから参加できます。",
515
+ "help.contact_join": "Telegram グループに参加",
467
516
  };
468
517
 
469
518
  export default ja;
@@ -138,6 +138,15 @@ const zhCN: Record<string, string> = {
138
138
  "subagents.no_history": "暂无运行记录",
139
139
  "subagents.no_history_desc": "运行子代理后,运行记录将显示在这里。",
140
140
  "subagents.model": "模型",
141
+ "subagents.edit": "编辑",
142
+ "subagents.save": "保存",
143
+ "subagents.saving": "保存中…",
144
+ "subagents.cancel": "取消",
145
+ "subagents.saved": "已保存",
146
+ "subagents.save_failed": "保存失败",
147
+ "subagents.model_placeholder": "provider/model-id(留空用默认)",
148
+ "subagents.model_default": "(默认)",
149
+ "subagents.thinking_default": "(默认)",
141
150
  "subagents.thinking": "思考级别",
142
151
  "subagents.tools": "工具",
143
152
  "subagents.system_prompt_mode": "系统提示模式",
@@ -464,6 +473,46 @@ const zhCN: Record<string, string> = {
464
473
  "language.zh-CN": "简体中文",
465
474
  "language.zh-TW": "繁體中文",
466
475
  "language.ja": "日本語",
476
+ "help.button": "使用说明",
477
+ "help.title": "使用说明",
478
+ "help.overview_title": "关于本项目",
479
+ "help.overview_desc": "pi-web-switch 是 pi 编码助手的可视化配置面板:查看会话、管理长期记忆、配置提供商与模型、编辑子代理、测速模型,并调整全局设置。所有改动直接写入 ~/.pi/agent 下的配置文件,实时生效。",
480
+ "help.modules_title": "各板块说明",
481
+ "help.dashboard_title": "01 仪表盘",
482
+ "help.dashboard_desc": "总览用量与近期活动。",
483
+ "help.dashboard_s1": "查看 token 用量、成本和各模型统计。",
484
+ "help.dashboard_s2": "点击「立即刷新」重新扫描会话数据。",
485
+ "help.sessions_title": "02 会话",
486
+ "help.sessions_desc": "浏览、预览和管理历史会话。",
487
+ "help.sessions_s1": "按项目分组查看会话,可展开/折叠。",
488
+ "help.sessions_s2": "删除会话会移入回收站,可恢复或永久删除。",
489
+ "help.sessions_s3": "回收站中可勾选批量永久删除。",
490
+ "help.memory_title": "03 记忆",
491
+ "help.memory_desc": "查看和优化 pi-hermes-memory 长期记忆。",
492
+ "help.memory_s1": "进度条显示三个记忆文件的容量占用(按字符)。",
493
+ "help.memory_s2": "「一键优化记忆」调用模型合并重复、清理过时条目。",
494
+ "help.memory_s3": "「记忆模型配置」设置自动写入所用模型、思考级别和容量上限。",
495
+ "help.providers_title": "04 提供商与模型",
496
+ "help.providers_desc": "管理 API 提供商和它们的模型。",
497
+ "help.providers_s1": "添加自定义提供商,填写 baseUrl 和 API Key。",
498
+ "help.providers_s2": "用「获取模型列表」拉取并搜索可用模型,勾选启用。",
499
+ "help.providers_s3": "可调整每个模型的 maxTokens 等参数。",
500
+ "help.subagents_title": "05 子代理",
501
+ "help.subagents_desc": "查看和编辑子代理、链和运行历史。",
502
+ "help.subagents_s1": "在「代理」选中自定义代理,点「编辑」可改模型和思考级别。",
503
+ "help.subagents_s2": "模型输入框带全部已配置模型的下拉候选。",
504
+ "help.settings_title": "06 设置",
505
+ "help.settings_desc": "外观、模型默认值和高级选项。",
506
+ "help.settings_s1": "切换主题、字体大小、界面缩放和语言。",
507
+ "help.settings_s2": "管理 pi 包并检查更新。",
508
+ "help.speedtest_title": "07 模型测速",
509
+ "help.speedtest_desc": "批量测试自定义提供商模型的可用性与延迟。",
510
+ "help.speedtest_s1": "选择提供商,用「获取模型」拉取待测模型(独立存储)。",
511
+ "help.speedtest_s2": "运行测速,查看成功率、平均延迟和延迟区间。",
512
+ "help.speedtest_s3": "遇限流可开「慢速模式」降低 429。",
513
+ "help.contact_title": "遇到问题?",
514
+ "help.contact_desc": "有任何疑问、建议或 bug 反馈,欢迎加入 Telegram 交流群,点击下方按钮即可加入。",
515
+ "help.contact_join": "加入 Telegram 交流群",
467
516
  };
468
517
 
469
518
  export default zhCN;
@@ -137,6 +137,15 @@ const zhTW: Record<string, string> = {
137
137
  "subagents.no_history": "暫無執行記錄",
138
138
  "subagents.no_history_desc": "執行子代理後,執行記錄將顯示在這裡。",
139
139
  "subagents.model": "模型",
140
+ "subagents.edit": "編輯",
141
+ "subagents.save": "儲存",
142
+ "subagents.saving": "儲存中…",
143
+ "subagents.cancel": "取消",
144
+ "subagents.saved": "已儲存",
145
+ "subagents.save_failed": "儲存失敗",
146
+ "subagents.model_placeholder": "provider/model-id(留空用預設)",
147
+ "subagents.model_default": "(預設)",
148
+ "subagents.thinking_default": "(預設)",
140
149
  "subagents.thinking": "思考級別",
141
150
  "subagents.tools": "工具",
142
151
  "subagents.system_prompt_mode": "系統提示模式",
@@ -463,6 +472,46 @@ const zhTW: Record<string, string> = {
463
472
  "language.zh-CN": "简体中文",
464
473
  "language.zh-TW": "繁體中文",
465
474
  "language.ja": "日本語",
475
+ "help.button": "使用說明",
476
+ "help.title": "使用說明",
477
+ "help.overview_title": "關於本專案",
478
+ "help.overview_desc": "pi-web-switch 是 pi 編碼助手的可視化配置面板:查看會話、管理長期記憶、配置提供商與模型、編輯子代理、測速模型,並調整全域設定。所有改動直接寫入 ~/.pi/agent 下的配置檔,即時生效。",
479
+ "help.modules_title": "各板塊說明",
480
+ "help.dashboard_title": "01 儀表板",
481
+ "help.dashboard_desc": "總覽用量與近期活動。",
482
+ "help.dashboard_s1": "查看 token 用量、成本和各模型統計。",
483
+ "help.dashboard_s2": "點擊「立即刷新」重新掃描會話資料。",
484
+ "help.sessions_title": "02 會話",
485
+ "help.sessions_desc": "瀏覽、預覽和管理歷史會話。",
486
+ "help.sessions_s1": "按專案分組查看會話,可展開/摺疊。",
487
+ "help.sessions_s2": "刪除會話會移入回收站,可還原或永久刪除。",
488
+ "help.sessions_s3": "回收站中可勾選批次永久刪除。",
489
+ "help.memory_title": "03 記憶",
490
+ "help.memory_desc": "查看和優化 pi-hermes-memory 長期記憶。",
491
+ "help.memory_s1": "進度條顯示三個記憶檔的容量佔用(按字元)。",
492
+ "help.memory_s2": "「一鍵優化記憶」呼叫模型合併重複、清理過時條目。",
493
+ "help.memory_s3": "「記憶模型設定」設定自動寫入所用模型、思考級別和容量上限。",
494
+ "help.providers_title": "04 提供商與模型",
495
+ "help.providers_desc": "管理 API 提供商和它們的模型。",
496
+ "help.providers_s1": "新增自訂提供商,填寫 baseUrl 和 API Key。",
497
+ "help.providers_s2": "用「獲取模型列表」拉取並搜尋可用模型,勾選啟用。",
498
+ "help.providers_s3": "可調整每個模型的 maxTokens 等參數。",
499
+ "help.subagents_title": "05 子代理",
500
+ "help.subagents_desc": "查看和編輯子代理、鏈和執行歷史。",
501
+ "help.subagents_s1": "在「代理」選中自訂代理,點「編輯」可改模型和思考級別。",
502
+ "help.subagents_s2": "模型輸入框帶全部已配置模型的下拉候選。",
503
+ "help.settings_title": "06 設定",
504
+ "help.settings_desc": "外觀、模型預設值和進階選項。",
505
+ "help.settings_s1": "切換主題、字型大小、介面縮放和語言。",
506
+ "help.settings_s2": "管理 pi 套件並檢查更新。",
507
+ "help.speedtest_title": "07 模型測速",
508
+ "help.speedtest_desc": "批次測試自訂提供商模型的可用性與延遲。",
509
+ "help.speedtest_s1": "選擇提供商,用「獲取模型」拉取待測模型(獨立儲存)。",
510
+ "help.speedtest_s2": "執行測速,查看成功率、平均延遲和延遲區間。",
511
+ "help.speedtest_s3": "遇限流可開「慢速模式」降低 429。",
512
+ "help.contact_title": "遇到問題?",
513
+ "help.contact_desc": "有任何疑問、建議或 bug 回報,歡迎加入 Telegram 交流群,點擊下方按鈕即可加入。",
514
+ "help.contact_join": "加入 Telegram 交流群",
466
515
  };
467
516
 
468
517
  export default zhTW;
package/vite.config.ts CHANGED
@@ -148,6 +148,22 @@ function piApiPlugin(): Plugin {
148
148
  res.setHeader("Content-Type", "application/json");
149
149
  res.end(JSON.stringify(data));
150
150
  },
151
+ "POST /api/pi/subagents/update-agent"(req, res) {
152
+ let body = "";
153
+ req.on("data", (chunk: string) => (body += chunk));
154
+ req.on("end", () => {
155
+ try {
156
+ const { fileName, model, thinking } = JSON.parse(body) as { fileName: string; model?: string; thinking?: string };
157
+ const ok = pi.updateAgentFields(fileName, { model, thinking });
158
+ res.setHeader("Content-Type", "application/json");
159
+ res.end(JSON.stringify({ success: ok }));
160
+ } catch {
161
+ res.statusCode = 400;
162
+ res.setHeader("Content-Type", "application/json");
163
+ res.end(JSON.stringify({ success: false, error: "Invalid request body" }));
164
+ }
165
+ });
166
+ },
151
167
  "GET /api/pi/memory/config"(_, res) {
152
168
  res.setHeader("Content-Type", "application/json");
153
169
  res.end(JSON.stringify(pi.readHermesMemoryConfig() ?? {}));