@raingor/pi-web-switch 0.7.1 → 0.8.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-Bs37wZvZ.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/main-DTnbncES.css">
12
+ <script type="module" crossorigin src="./assets/main-DJ0fZLIg.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/main-K50Fa8uc.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.7.1",
4
+ "version": "0.8.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,50 @@ export function readSubagents(): SubagentsData {
2754
2754
  };
2755
2755
  }
2756
2756
 
2757
+ // ─── Package Search (npm registry) ─────────────────────────
2758
+ // pi packages are npm packages tagged for pi. The pi.dev/packages catalog is
2759
+ // SSR-only (no public JSON API — /api/packages returns 501), so we query the
2760
+ // public npm registry search endpoint directly, which supports fuzzy text and
2761
+ // returns name/description/downloads.
2762
+
2763
+ export interface PackageSearchResult {
2764
+ name: string;
2765
+ description: string;
2766
+ version: string;
2767
+ downloads: number;
2768
+ link: string;
2769
+ }
2770
+
2771
+ export async function searchPackages(query: string): Promise<PackageSearchResult[]> {
2772
+ const q = query.trim();
2773
+ // Bias the search toward pi extensions. When the user types nothing we still
2774
+ // surface the most popular pi packages.
2775
+ const text = q ? `${q} pi` : "pi-extension";
2776
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=40`;
2777
+ try {
2778
+ const res = await fetchExternal(url, { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) });
2779
+ if (!res.ok) return [];
2780
+ const data = (await res.json()) as {
2781
+ objects?: { package?: { name?: string; description?: string; version?: string; links?: { npm?: string } }; searchScore?: number }[];
2782
+ };
2783
+ const rows = (data.objects ?? [])
2784
+ .map((o) => o.package)
2785
+ .filter((p): p is NonNullable<typeof p> => !!p?.name)
2786
+ // Keep pi-related packages only (name or description mentions pi).
2787
+ .filter((p) => /(^|[@/-])pi([-/]|$)|pi coding|pi extension|pi agent/i.test(`${p.name} ${p.description ?? ""}`))
2788
+ .map((p) => ({
2789
+ name: p.name!,
2790
+ description: p.description ?? "",
2791
+ version: p.version ?? "",
2792
+ downloads: 0,
2793
+ link: p.links?.npm ?? `https://www.npmjs.com/package/${p.name}`,
2794
+ }));
2795
+ return rows;
2796
+ } catch {
2797
+ return [];
2798
+ }
2799
+ }
2800
+
2757
2801
  const AGENT_NAME_RE = /^[\w.-]+\.md$/;
2758
2802
 
2759
2803
  /**
@@ -0,0 +1,222 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
+ import { Search, Plus, Check, Loader2, ExternalLink, Sparkles } from "lucide-react";
3
+ import { Modal } from "@/components/ui/Modal";
4
+ import { useTranslation } from "@/lib/i18n";
5
+ import { RECOMMENDED_PACKAGES } from "@/data/recommended-packages";
6
+
7
+ interface PackageSearchResult {
8
+ name: string;
9
+ description: string;
10
+ version: string;
11
+ downloads: number;
12
+ link: string;
13
+ }
14
+
15
+ type PackageFilter = "all" | "installed" | "available";
16
+ type Tab = "recommended" | "search";
17
+
18
+ interface PackageBrowserProps {
19
+ open: boolean;
20
+ onClose: () => void;
21
+ installed: Set<string>; // ids like "npm:pkg-name"
22
+ onInstall: (id: string) => void;
23
+ }
24
+
25
+ /** One package row with an install / installed control. */
26
+ function PackageRow({
27
+ name,
28
+ description,
29
+ link,
30
+ installed,
31
+ onInstall,
32
+ }: {
33
+ name: string;
34
+ description: string;
35
+ link?: string;
36
+ installed: boolean;
37
+ onInstall: () => void;
38
+ }) {
39
+ const { t } = useTranslation();
40
+ return (
41
+ <div
42
+ className="flex items-center gap-3 rounded-lg border px-3 py-2"
43
+ style={{ borderColor: "var(--card-border)", backgroundColor: "var(--card-bg)" }}
44
+ >
45
+ <div className="min-w-0 flex-1">
46
+ <div className="flex items-center gap-2">
47
+ <span className="truncate text-sm font-medium" style={{ color: "var(--page-text)" }}>{name}</span>
48
+ <a
49
+ href={link ?? `https://www.npmjs.com/package/${name}`}
50
+ target="_blank"
51
+ rel="noreferrer"
52
+ className="shrink-0"
53
+ style={{ color: "var(--subtle-text)" }}
54
+ title={t("settings.view_on_npm")}
55
+ >
56
+ <ExternalLink className="h-3 w-3" />
57
+ </a>
58
+ </div>
59
+ {description && (
60
+ <p className="truncate text-xs" style={{ color: "var(--muted-text)" }}>{description}</p>
61
+ )}
62
+ </div>
63
+ {installed ? (
64
+ <span className="flex shrink-0 items-center gap-1 rounded-lg border px-3 py-1.5 text-xs font-medium"
65
+ style={{ borderColor: "rgba(16,185,129,0.4)", color: "#10b981" }}>
66
+ <Check className="h-3.5 w-3.5" />
67
+ {t("settings.installed")}
68
+ </span>
69
+ ) : (
70
+ <button
71
+ onClick={onInstall}
72
+ className="flex shrink-0 items-center gap-1 rounded-lg border border-blue-600/50 bg-blue-600/10 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-600/20"
73
+ >
74
+ <Plus className="h-3.5 w-3.5" />
75
+ {t("settings.install")}
76
+ </button>
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Package browser modal with two tabs:
84
+ * - Recommended: the curated list from src/data/recommended-packages.ts
85
+ * - Search: fuzzy npm-registry search for any pi package
86
+ * Installing writes "npm:<name>" into settings.packages.
87
+ */
88
+ export function PackageBrowser({ open, onClose, installed, onInstall }: PackageBrowserProps) {
89
+ const { t } = useTranslation();
90
+ const [tab, setTab] = useState<Tab>("recommended");
91
+ const [query, setQuery] = useState("");
92
+ const [results, setResults] = useState<PackageSearchResult[]>([]);
93
+ const [loading, setLoading] = useState(false);
94
+ const [justAdded, setJustAdded] = useState<Set<string>>(new Set());
95
+ const [filter, setFilter] = useState<PackageFilter>("all");
96
+ const debounceRef = useRef<number | undefined>(undefined);
97
+
98
+ const runSearch = useCallback((q: string) => {
99
+ setLoading(true);
100
+ fetch(`/api/pi/packages/search?q=${encodeURIComponent(q)}`)
101
+ .then((r) => r.json())
102
+ .then((d: { results?: PackageSearchResult[] }) => setResults(d.results ?? []))
103
+ .catch(() => setResults([]))
104
+ .finally(() => setLoading(false));
105
+ }, []);
106
+
107
+ // Load popular packages the first time the Search tab is opened; debounce typing.
108
+ useEffect(() => {
109
+ if (!open || tab !== "search") return;
110
+ window.clearTimeout(debounceRef.current);
111
+ debounceRef.current = window.setTimeout(() => runSearch(query), 350);
112
+ return () => window.clearTimeout(debounceRef.current);
113
+ }, [query, open, tab, runSearch]);
114
+
115
+ const isInstalledId = (name: string) => {
116
+ const id = `npm:${name}`;
117
+ return installed.has(id) || justAdded.has(id);
118
+ };
119
+
120
+ const handleInstall = (id: string) => {
121
+ onInstall(id);
122
+ setJustAdded((prev) => new Set(prev).add(id));
123
+ };
124
+
125
+ // Search-tab results after the installed/available filter.
126
+ const filtered = results.filter((pkg) => {
127
+ if (filter === "installed") return isInstalledId(pkg.name);
128
+ if (filter === "available") return !isInstalledId(pkg.name);
129
+ return true;
130
+ });
131
+ const installedCount = results.filter((p) => isInstalledId(p.name)).length;
132
+
133
+ return (
134
+ <Modal open={open} onClose={onClose} title={t("settings.browse_packages")} size="lg">
135
+ <div className="space-y-4">
136
+ {/* Tabs */}
137
+ <div className="flex gap-1 rounded-lg p-1" style={{ backgroundColor: "var(--card-bg)" }}>
138
+ {(["recommended", "search"] as Tab[]).map((tKey) => (
139
+ <button
140
+ key={tKey}
141
+ onClick={() => setTab(tKey)}
142
+ className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${tab === tKey ? "bg-blue-600/15 text-blue-400" : "text-gray-400 hover:text-gray-200"}`}
143
+ >
144
+ {tKey === "recommended" ? <Sparkles className="h-3.5 w-3.5" /> : <Search className="h-3.5 w-3.5" />}
145
+ {t(`settings.tab_${tKey}`)}
146
+ </button>
147
+ ))}
148
+ </div>
149
+
150
+ {tab === "recommended" ? (
151
+ <div className="max-h-[55vh] space-y-1.5 overflow-y-auto">
152
+ {RECOMMENDED_PACKAGES.map((pkg) => (
153
+ <PackageRow
154
+ key={pkg.id}
155
+ name={pkg.name}
156
+ description={t(pkg.descKey)}
157
+ installed={installed.has(pkg.id) || justAdded.has(pkg.id)}
158
+ onInstall={() => handleInstall(pkg.id)}
159
+ />
160
+ ))}
161
+ </div>
162
+ ) : (
163
+ <>
164
+ {/* Search box */}
165
+ <div className="relative">
166
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" style={{ color: "var(--subtle-text)" }} />
167
+ <input
168
+ type="text"
169
+ autoFocus
170
+ value={query}
171
+ onChange={(e) => setQuery(e.target.value)}
172
+ placeholder={t("settings.search_packages_placeholder")}
173
+ className="w-full rounded-lg border py-2 pl-9 pr-3 text-sm outline-none focus:ring-1 focus:ring-blue-500"
174
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
175
+ />
176
+ </div>
177
+
178
+ {/* Installed / available filter */}
179
+ <div className="flex gap-1 rounded-lg p-1" style={{ backgroundColor: "var(--card-bg)" }}>
180
+ {(["all", "available", "installed"] as PackageFilter[]).map((f) => {
181
+ const count = f === "all" ? results.length : f === "installed" ? installedCount : results.length - installedCount;
182
+ return (
183
+ <button
184
+ key={f}
185
+ onClick={() => setFilter(f)}
186
+ className={`flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${filter === f ? "bg-blue-600/15 text-blue-400" : "text-gray-400 hover:text-gray-200"}`}
187
+ >
188
+ {t(`settings.filter_${f}`)} ({count})
189
+ </button>
190
+ );
191
+ })}
192
+ </div>
193
+
194
+ {/* Results */}
195
+ <div className="max-h-[42vh] space-y-1.5 overflow-y-auto">
196
+ {loading && results.length === 0 ? (
197
+ <div className="flex items-center justify-center py-10">
198
+ <Loader2 className="h-5 w-5 animate-spin" style={{ color: "var(--muted-text)" }} />
199
+ </div>
200
+ ) : filtered.length === 0 ? (
201
+ <p className="py-10 text-center text-sm" style={{ color: "var(--muted-text)" }}>
202
+ {t("settings.no_packages_found")}
203
+ </p>
204
+ ) : (
205
+ filtered.map((pkg) => (
206
+ <PackageRow
207
+ key={pkg.name}
208
+ name={pkg.name}
209
+ description={pkg.description}
210
+ link={pkg.link}
211
+ installed={isInstalledId(pkg.name)}
212
+ onInstall={() => handleInstall(`npm:${pkg.name}`)}
213
+ />
214
+ ))
215
+ )}
216
+ </div>
217
+ </>
218
+ )}
219
+ </div>
220
+ </Modal>
221
+ );
222
+ }
@@ -11,6 +11,8 @@ import {
11
11
  } from "@/lib/config";
12
12
  import type { PiConfig, UpdateCheckResult } from "@/types";
13
13
  import { cn } from "@/lib/utils";
14
+ import { RECOMMENDED_PACKAGES } from "@/data/recommended-packages";
15
+ import { PackageBrowser } from "./PackageBrowser";
14
16
  import {
15
17
  Download,
16
18
  Upload,
@@ -102,6 +104,7 @@ export function SettingsPage() {
102
104
  const [newPackage, setNewPackage] = useState("");
103
105
  const [importError, setImportError] = useState("");
104
106
  const [showResetConfirm, setShowResetConfirm] = useState(false);
107
+ const [showPackageBrowser, setShowPackageBrowser] = useState(false);
105
108
  const [fontSize, setFontSize] = useState(() => {
106
109
  const saved = Number(localStorage.getItem(FONT_SIZE_KEY));
107
110
  return saved >= 12 && saved <= 24 ? saved : 16;
@@ -569,6 +572,15 @@ export function SettingsPage() {
569
572
  </Card>
570
573
 
571
574
  <Card icon={Package} title={t("settings.packages")}>
575
+ <div className="mb-4">
576
+ <button
577
+ onClick={() => setShowPackageBrowser(true)}
578
+ className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500"
579
+ >
580
+ <Plus className="h-4 w-4" />
581
+ {t("settings.browse_packages")}
582
+ </button>
583
+ </div>
572
584
  {(settings?.packages ?? []).length > 0 && (
573
585
  <div className="mb-4 flex flex-wrap gap-1.5">
574
586
  {(settings?.packages ?? []).map((pkg) => (
@@ -590,33 +602,84 @@ export function SettingsPage() {
590
602
  {(settings?.packages ?? []).length === 0 && (
591
603
  <p className="mb-4 text-sm text-gray-500">{t("settings.no_packages")}</p>
592
604
  )}
593
- <div className="flex max-w-md gap-2">
594
- <input
595
- type="text"
596
- value={newPackage}
597
- onChange={(e) => setNewPackage(e.target.value)}
598
- placeholder={t("settings.package_placeholder")}
599
- className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-500"
600
- onKeyDown={(e) => {
601
- if (e.key === "Enter" && newPackage.trim()) {
602
- addPackage(newPackage.trim());
603
- setNewPackage("");
604
- }
605
- }}
606
- />
607
- <button
608
- onClick={() => {
609
- if (newPackage.trim()) {
610
- addPackage(newPackage.trim());
611
- setNewPackage("");
612
- }
613
- }}
614
- className="flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:bg-gray-700"
615
- >
616
- <Plus className="h-3.5 w-3.5" />
617
- {t("settings.add")}
618
- </button>
619
- </div>
605
+
606
+ {/* Recommended packages — one-click install */}
607
+ {(() => {
608
+ const installed = new Set(settings?.packages ?? []);
609
+ const recommended = RECOMMENDED_PACKAGES.filter((p) => !installed.has(p.id));
610
+ if (recommended.length === 0) return null;
611
+ return (
612
+ <div className="mb-4">
613
+ <p className="mb-2 text-xs font-medium" style={{ color: "var(--muted-text)" }}>
614
+ {t("settings.recommended_packages")}
615
+ </p>
616
+ <div className="space-y-1.5">
617
+ {recommended.map((pkg) => (
618
+ <div
619
+ key={pkg.id}
620
+ className="flex items-center gap-3 rounded-lg border border-gray-700 bg-gray-800/50 px-3 py-2"
621
+ >
622
+ <div className="min-w-0 flex-1">
623
+ <p className="truncate text-sm text-gray-200">{pkg.name}</p>
624
+ <p className="truncate text-xs text-gray-500">{t(pkg.descKey)}</p>
625
+ </div>
626
+ <button
627
+ onClick={() => addPackage(pkg.id)}
628
+ className="flex shrink-0 items-center gap-1 rounded-lg border border-blue-600/50 bg-blue-600/10 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-600/20"
629
+ >
630
+ <Plus className="h-3.5 w-3.5" />
631
+ {t("settings.install")}
632
+ </button>
633
+ </div>
634
+ ))}
635
+ </div>
636
+ <button
637
+ onClick={() => {
638
+ const list = settings?.packages ?? [];
639
+ const toAdd = recommended.map((p) => p.id);
640
+ updateSettings({ packages: [...list, ...toAdd] });
641
+ }}
642
+ className="mt-2 flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700"
643
+ >
644
+ <Plus className="h-3.5 w-3.5" />
645
+ {t("settings.install_all_recommended")}
646
+ </button>
647
+ </div>
648
+ );
649
+ })()}
650
+
651
+ <details className="mb-1">
652
+ <summary className="cursor-pointer text-xs text-gray-500 hover:text-gray-300">
653
+ {t("settings.add_custom_package")}
654
+ </summary>
655
+ <div className="mt-2 flex max-w-md gap-2">
656
+ <input
657
+ type="text"
658
+ value={newPackage}
659
+ onChange={(e) => setNewPackage(e.target.value)}
660
+ placeholder={t("settings.package_placeholder")}
661
+ className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-500"
662
+ onKeyDown={(e) => {
663
+ if (e.key === "Enter" && newPackage.trim()) {
664
+ addPackage(newPackage.trim());
665
+ setNewPackage("");
666
+ }
667
+ }}
668
+ />
669
+ <button
670
+ onClick={() => {
671
+ if (newPackage.trim()) {
672
+ addPackage(newPackage.trim());
673
+ setNewPackage("");
674
+ }
675
+ }}
676
+ className="flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:bg-gray-700"
677
+ >
678
+ <Plus className="h-3.5 w-3.5" />
679
+ {t("settings.add")}
680
+ </button>
681
+ </div>
682
+ </details>
620
683
  </Card>
621
684
 
622
685
  <Card icon={Download} title={t("settings.import_export")}>
@@ -655,6 +718,13 @@ export function SettingsPage() {
655
718
  </div>
656
719
  )}
657
720
 
721
+ <PackageBrowser
722
+ open={showPackageBrowser}
723
+ onClose={() => setShowPackageBrowser(false)}
724
+ installed={new Set(settings?.packages ?? [])}
725
+ onInstall={(id) => addPackage(id)}
726
+ />
727
+
658
728
  {/* Reset Confirm */}
659
729
  <Modal
660
730
  open={showResetConfirm}
@@ -9,6 +9,11 @@ export interface ChangelogEntry {
9
9
  }
10
10
 
11
11
  export const CHANGELOG: ChangelogEntry[] = [
12
+ {
13
+ version: "0.8.0",
14
+ date: "2026-08-27",
15
+ itemKeys: ["changelog.0_8_0_1", "changelog.0_8_0_2", "changelog.0_8_0_3"],
16
+ },
12
17
  {
13
18
  version: "0.7.0",
14
19
  date: "2026-08-27",
@@ -0,0 +1,20 @@
1
+ // Recommended pi extension packages, surfaced in Settings for one-click install.
2
+ // Seeded from the maintainer's own setup. `descKey` points to an i18n string.
3
+ export interface RecommendedPackage {
4
+ id: string; // exact value written to settings.packages, e.g. "npm:pi-hermes-memory"
5
+ name: string; // short display name
6
+ descKey: string;
7
+ }
8
+
9
+ export const RECOMMENDED_PACKAGES: RecommendedPackage[] = [
10
+ { id: "npm:pi-hermes-memory", name: "pi-hermes-memory", descKey: "pkg.hermes_memory" },
11
+ { id: "npm:context-mode", name: "context-mode", descKey: "pkg.context_mode" },
12
+ { id: "npm:pi-subagents", name: "pi-subagents", descKey: "pkg.subagents" },
13
+ { id: "npm:pi-web-access", name: "pi-web-access", descKey: "pkg.web_access" },
14
+ { id: "npm:pi-smart-fetch", name: "pi-smart-fetch", descKey: "pkg.smart_fetch" },
15
+ { id: "npm:pi-rtk-optimizer", name: "pi-rtk-optimizer", descKey: "pkg.rtk_optimizer" },
16
+ { id: "npm:pi-puppeteer", name: "pi-puppeteer", descKey: "pkg.puppeteer" },
17
+ { id: "npm:pi-intercom", name: "pi-intercom", descKey: "pkg.intercom" },
18
+ { id: "npm:pi-prompt-template-model", name: "pi-prompt-template-model", descKey: "pkg.prompt_template" },
19
+ { id: "npm:@pi-unipi/notify", name: "@pi-unipi/notify", descKey: "pkg.notify" },
20
+ ];
@@ -129,6 +129,9 @@ const en: Record<string, string> = {
129
129
  "app.version": "pi-switch v0.7.0",
130
130
  "changelog.button": "Changelog",
131
131
  "changelog.title": "What's New",
132
+ "changelog.0_8_0_1": "Settings: added a package browser dialog with \"Recommended\" and \"Search\" tabs.",
133
+ "changelog.0_8_0_2": "Search tab fuzzy-searches npm for pi packages, filterable by All/Available/Installed.",
134
+ "changelog.0_8_0_3": "One-click install with an installed-state indicator.",
132
135
  "changelog.0_7_0_1": "Added a floating \"User Guide\" button (bottom-right) with a project overview and per-module how-to.",
133
136
  "changelog.0_7_0_2": "Added \"Join Telegram group\" entry points (sidebar + user guide dialog).",
134
137
  "changelog.0_7_0_3": "Added this \"What’s New\" changelog dialog.",
@@ -473,6 +476,30 @@ const en: Record<string, string> = {
473
476
  "settings.packages": "Extensions & Packages",
474
477
  "settings.package_placeholder": "npm:package-name",
475
478
  "settings.add": "Add",
479
+ "settings.install": "Install",
480
+ "settings.browse_packages": "Browse & add packages",
481
+ "settings.tab_recommended": "Recommended",
482
+ "settings.tab_search": "Search",
483
+ "settings.filter_all": "All",
484
+ "settings.filter_available": "Available",
485
+ "settings.filter_installed": "Installed",
486
+ "settings.search_packages_placeholder": "Search packages (fuzzy)…",
487
+ "settings.no_packages_found": "No matching packages found",
488
+ "settings.installed": "Installed",
489
+ "settings.view_on_npm": "View on npm",
490
+ "settings.recommended_packages": "Recommended packages (one-click install)",
491
+ "settings.install_all_recommended": "Install all recommended",
492
+ "settings.add_custom_package": "Add another package manually…",
493
+ "pkg.hermes_memory": "Long-term memory (read/write + consolidation)",
494
+ "pkg.context_mode": "Large-output handling & knowledge base",
495
+ "pkg.subagents": "Subagents & multi-step orchestration",
496
+ "pkg.web_access": "Web fetch & search",
497
+ "pkg.smart_fetch": "Browser-fingerprint web fetch",
498
+ "pkg.rtk_optimizer": "RTK optimization & conventions",
499
+ "pkg.puppeteer": "Browser automation",
500
+ "pkg.intercom": "Cross-session coordination",
501
+ "pkg.prompt_template": "Custom prompt templates & models",
502
+ "pkg.notify": "Cross-platform notifications",
476
503
  "settings.no_packages": "No packages installed",
477
504
  "settings.import_export": "Import & Export",
478
505
  "settings.export": "Export Config",
@@ -127,6 +127,9 @@ const ja: Record<string, string> = {
127
127
  "app.version": "pi-switch v0.7.0",
128
128
  "changelog.button": "変更履歴",
129
129
  "changelog.title": "更新情報",
130
+ "changelog.0_8_0_1": "設定:「おすすめ」と「検索」タブを備えたパッケージ検索ダイアログを追加。",
131
+ "changelog.0_8_0_2": "検索タブは npm の pi パッケージをあいまい検索し、すべて/未インストール/インストール済みで絞り込み可能。",
132
+ "changelog.0_8_0_3": "ワンクリックインストールとインストール済み表示。",
130
133
  "changelog.0_7_0_1": "右下にフローティング「使い方」ボタンを追加。プロジェクト概要と各モジュールの使い方を表示。",
131
134
  "changelog.0_7_0_2": "「Telegram グループに参加」の入口を追加(サイドバー + 使い方ダイアログ)。",
132
135
  "changelog.0_7_0_3": "この「更新情報」ダイアログを追加。",
@@ -465,6 +468,30 @@ const ja: Record<string, string> = {
465
468
  "settings.packages": "拡張機能とパッケージ",
466
469
  "settings.package_placeholder": "npm:package-name",
467
470
  "settings.add": "追加",
471
+ "settings.install": "インストール",
472
+ "settings.browse_packages": "パッケージを検索して追加",
473
+ "settings.tab_recommended": "おすすめ",
474
+ "settings.tab_search": "検索",
475
+ "settings.filter_all": "すべて",
476
+ "settings.filter_available": "未インストール",
477
+ "settings.filter_installed": "インストール済み",
478
+ "settings.search_packages_placeholder": "パッケージを検索(あいまい)…",
479
+ "settings.no_packages_found": "一致するパッケージがありません",
480
+ "settings.installed": "インストール済み",
481
+ "settings.view_on_npm": "npm で見る",
482
+ "settings.recommended_packages": "おすすめパッケージ(ワンクリック)",
483
+ "settings.install_all_recommended": "おすすめを一括インストール",
484
+ "settings.add_custom_package": "他のパッケージを手動追加…",
485
+ "pkg.hermes_memory": "長期メモリ(読み書き・統合)",
486
+ "pkg.context_mode": "大出力処理と知識ベース",
487
+ "pkg.subagents": "サブエージェントと多段編成",
488
+ "pkg.web_access": "Web取得と検索",
489
+ "pkg.smart_fetch": "ブラウザ指紋レベルのWeb取得",
490
+ "pkg.rtk_optimizer": "RTK最適化と規約",
491
+ "pkg.puppeteer": "ブラウザ自動化",
492
+ "pkg.intercom": "セッション間連携",
493
+ "pkg.prompt_template": "カスタムプロンプトとモデル",
494
+ "pkg.notify": "クロスプラットフォーム通知",
468
495
  "settings.no_packages": "パッケージがインストールされていません",
469
496
  "settings.import_export": "インポートとエクスポート",
470
497
  "settings.export": "設定をエクスポート",
@@ -127,6 +127,9 @@ const zhCN: Record<string, string> = {
127
127
  "app.version": "pi-switch v0.7.0",
128
128
  "changelog.button": "更新日志",
129
129
  "changelog.title": "版本更新说明",
130
+ "changelog.0_8_0_1": "设置:新增扩展包浏览弹窗,含「推荐安装」与「搜索」两个标签页。",
131
+ "changelog.0_8_0_2": "搜索标签支持模糊搜索 npm 上的 pi 扩展包,并可按全部/未安装/已安装筛选。",
132
+ "changelog.0_8_0_3": "扩展包一键安装,已安装状态自动标识。",
130
133
  "changelog.0_7_0_1": "新增右下角浮动「使用说明」按钮,弹窗含项目总览与各板块使用指引。",
131
134
  "changelog.0_7_0_2": "新增「加入 Telegram 交流群」入口(侧边栏 + 使用说明弹窗)。",
132
135
  "changelog.0_7_0_3": "新增本「版本更新说明」弹窗。",
@@ -465,6 +468,30 @@ const zhCN: Record<string, string> = {
465
468
  "settings.packages": "扩展与包",
466
469
  "settings.package_placeholder": "npm:package-name",
467
470
  "settings.add": "添加",
471
+ "settings.install": "安装",
472
+ "settings.browse_packages": "浏览并添加扩展包",
473
+ "settings.tab_recommended": "推荐安装",
474
+ "settings.tab_search": "搜索",
475
+ "settings.filter_all": "全部",
476
+ "settings.filter_available": "未安装",
477
+ "settings.filter_installed": "已安装",
478
+ "settings.search_packages_placeholder": "搜索扩展包(模糊匹配)…",
479
+ "settings.no_packages_found": "没有找到匹配的扩展包",
480
+ "settings.installed": "已安装",
481
+ "settings.view_on_npm": "在 npm 查看",
482
+ "settings.recommended_packages": "推荐扩展包(点击安装)",
483
+ "settings.install_all_recommended": "一键安装全部推荐",
484
+ "settings.add_custom_package": "手动添加其他包…",
485
+ "pkg.hermes_memory": "长期记忆系统(记忆读写与整合)",
486
+ "pkg.context_mode": "大输出处理与知识库检索",
487
+ "pkg.subagents": "子代理与多步编排",
488
+ "pkg.web_access": "网页抓取与搜索",
489
+ "pkg.smart_fetch": "浏览器指纹级网页抓取",
490
+ "pkg.rtk_optimizer": "RTK 优化与规范",
491
+ "pkg.puppeteer": "浏览器自动化",
492
+ "pkg.intercom": "多会话协作通信",
493
+ "pkg.prompt_template": "自定义提示词模板与模型",
494
+ "pkg.notify": "跨平台通知",
468
495
  "settings.no_packages": "未安装任何包",
469
496
  "settings.import_export": "导入与导出",
470
497
  "settings.export": "导出配置",
@@ -126,6 +126,9 @@ const zhTW: Record<string, string> = {
126
126
  "app.version": "pi-switch v0.7.0",
127
127
  "changelog.button": "更新日誌",
128
128
  "changelog.title": "版本更新說明",
129
+ "changelog.0_8_0_1": "設定:新增擴充包瀏覽彈窗,含「推薦安裝」與「搜尋」兩個標籤頁。",
130
+ "changelog.0_8_0_2": "搜尋標籤支援模糊搜尋 npm 上的 pi 擴充包,並可按全部/未安裝/已安裝篩選。",
131
+ "changelog.0_8_0_3": "擴充包一鍵安裝,已安裝狀態自動標示。",
129
132
  "changelog.0_7_0_1": "新增右下角浮動「使用說明」按鈕,彈窗含專案總覽與各板塊使用指引。",
130
133
  "changelog.0_7_0_2": "新增「加入 Telegram 交流群」入口(側邊欄 + 使用說明彈窗)。",
131
134
  "changelog.0_7_0_3": "新增本「版本更新說明」彈窗。",
@@ -464,6 +467,30 @@ const zhTW: Record<string, string> = {
464
467
  "settings.packages": "擴充功能與套件",
465
468
  "settings.package_placeholder": "npm:package-name",
466
469
  "settings.add": "新增",
470
+ "settings.install": "安裝",
471
+ "settings.browse_packages": "瀏覽並新增擴充包",
472
+ "settings.tab_recommended": "推薦安裝",
473
+ "settings.tab_search": "搜尋",
474
+ "settings.filter_all": "全部",
475
+ "settings.filter_available": "未安裝",
476
+ "settings.filter_installed": "已安裝",
477
+ "settings.search_packages_placeholder": "搜尋擴充包(模糊匹配)…",
478
+ "settings.no_packages_found": "沒有找到匹配的擴充包",
479
+ "settings.installed": "已安裝",
480
+ "settings.view_on_npm": "在 npm 檢視",
481
+ "settings.recommended_packages": "推薦擴充包(點擊安裝)",
482
+ "settings.install_all_recommended": "一鍵安裝全部推薦",
483
+ "settings.add_custom_package": "手動新增其他包…",
484
+ "pkg.hermes_memory": "長期記憶系統(記憶讀寫與整合)",
485
+ "pkg.context_mode": "大輸出處理與知識庫檢索",
486
+ "pkg.subagents": "子代理與多步編排",
487
+ "pkg.web_access": "網頁抓取與搜尋",
488
+ "pkg.smart_fetch": "瀏覽器指紋級網頁抓取",
489
+ "pkg.rtk_optimizer": "RTK 優化與規範",
490
+ "pkg.puppeteer": "瀏覽器自動化",
491
+ "pkg.intercom": "多會話協作通訊",
492
+ "pkg.prompt_template": "自訂提示詞模板與模型",
493
+ "pkg.notify": "跨平台通知",
467
494
  "settings.no_packages": "未安裝任何套件",
468
495
  "settings.import_export": "匯入與匯出",
469
496
  "settings.export": "匯出設定",
package/vite.config.ts CHANGED
@@ -148,6 +148,17 @@ function piApiPlugin(): Plugin {
148
148
  res.setHeader("Content-Type", "application/json");
149
149
  res.end(JSON.stringify(data));
150
150
  },
151
+ "GET /api/pi/packages/search"(req, res) {
152
+ const parsed = new URL(req.url ?? "", "http://localhost");
153
+ const q = parsed.searchParams.get("q") ?? "";
154
+ pi.searchPackages(q).then((results: unknown) => {
155
+ res.setHeader("Content-Type", "application/json");
156
+ res.end(JSON.stringify({ results }));
157
+ }).catch(() => {
158
+ res.setHeader("Content-Type", "application/json");
159
+ res.end(JSON.stringify({ results: [] }));
160
+ });
161
+ },
151
162
  "POST /api/pi/subagents/update-agent"(req, res) {
152
163
  let body = "";
153
164
  req.on("data", (chunk: string) => (body += chunk));