reasonix 0.26.1 → 0.27.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/README.md CHANGED
@@ -118,6 +118,14 @@ Scoped starter tickets — each with background, code pointers, acceptance crite
118
118
  - [#21 · Dashboard design](https://github.com/esengine/reasonix/discussions/21) — react against the [proposed mockup](https://esengine.github.io/reasonix/design/agent-dashboard.html)
119
119
  - [#22 · Future feature wishlist](https://github.com/esengine/reasonix/discussions/22) — what would you build into Reasonix next?
120
120
 
121
+ **Already using Reasonix and willing to help others discover it?** Publish blog posts, articles, screenshots, talks, or videos to [**Show and tell**](https://github.com/esengine/reasonix/discussions/categories/show-and-tell). The project has no marketing budget — community word of mouth is how new users find it. Sustained advocates earn the badge below, displayed next to the contributors wall once awarded:
122
+
123
+ <p align="center">
124
+ <a href="https://github.com/esengine/reasonix/discussions/categories/show-and-tell">
125
+ <img src="https://img.shields.io/badge/REASONIX-📣%20ADVOCATE-c4b5fd?style=for-the-badge&labelColor=0d1117" alt="Reasonix Advocate badge — earned by sustained advocates"/>
126
+ </a>
127
+ </p>
128
+
121
129
  **Before your first PR**: read [`CONTRIBUTING.md`](./CONTRIBUTING.md) — short, strict rules (comments, errors, libraries-over-hand-rolled). `tests/comment-policy.test.ts` enforces the comment ones; `npm run verify` is the pre-push gate. By participating you agree to the [Code of Conduct](./CODE_OF_CONDUCT.md). Security issues → [SECURITY.md](./SECURITY.md).
122
130
 
123
131
  <p align="center">
package/README.zh-CN.md CHANGED
@@ -118,6 +118,14 @@ npx reasonix code # 首次运行粘贴 DeepSeek API Key,之后会记住
118
118
  - [#21 · Dashboard 设计](https://github.com/esengine/reasonix/discussions/21) —— 对着[设计稿](https://esengine.github.io/reasonix/design/agent-dashboard.html)拍砖
119
119
  - [#22 · 未来功能愿望单](https://github.com/esengine/reasonix/discussions/22) —— 你希望 Reasonix 长出什么功能?
120
120
 
121
+ **正在使用 Reasonix,愿意让更多人了解它?** 欢迎将相关博客、文章、截图、演讲或视频发布到 [**Show and tell**](https://github.com/esengine/reasonix/discussions/categories/show-and-tell)。项目没有营销预算,新用户主要通过社区口碑找到这里。持续参与传播的用户将获得下方这枚徽章,颁发后会展示在贡献者墙旁:
122
+
123
+ <p align="center">
124
+ <a href="https://github.com/esengine/reasonix/discussions/categories/show-and-tell">
125
+ <img src="https://img.shields.io/badge/REASONIX-📣%20ADVOCATE-c4b5fd?style=for-the-badge&labelColor=0d1117" alt="Reasonix Advocate 徽章 —— 授予持续参与传播的用户"/>
126
+ </a>
127
+ </p>
128
+
121
129
  **第一次提 PR 之前**:先读 [`CONTRIBUTING.md`](./CONTRIBUTING.md) —— 短小、严格的项目规则(注释、错误处理、用现成库不手写)。`tests/comment-policy.test.ts` 静态强制执行注释那部分,`npm run verify` 是 push 前的闸。参与本项目即同意 [行为准则](./CODE_OF_CONDUCT.md)。安全相关问题请走 [SECURITY.md](./SECURITY.md)。
122
130
 
123
131
  <p align="center">
package/dashboard/app.css CHANGED
@@ -690,8 +690,19 @@ main { padding: 32px 40px 60px 32px; min-width: 0; }
690
690
  .crumbs .sep { color: var(--fg-4); }
691
691
 
692
692
  /* ── Sessions panel ──────────────────────────────────────────────────── */
693
- .sessions-grid { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 14px; min-height: 540px; }
694
- .sessions-list { background: var(--bg-elev); border: 1px solid var(--bd); border-radius: var(--r); display: flex; flex-direction: column; overflow: hidden; }
693
+ .sessions-grid {
694
+ display: grid;
695
+ grid-template-columns: 320px minmax(0, 1fr);
696
+ /* `minmax(0, 1fr)` on the row + `min-height: 0` on the children is the
697
+ standard recipe for "let the inner overflow:auto take effect" — without
698
+ it the grid items default to min-height: auto (= content size) and
699
+ grow past the parent's max-height, dragging .app-body along. */
700
+ grid-template-rows: minmax(0, 1fr);
701
+ gap: 14px;
702
+ min-height: 540px;
703
+ max-height: calc(100vh - 140px);
704
+ }
705
+ .sessions-list { background: var(--bg-elev); border: 1px solid var(--bd); border-radius: var(--r); display: flex; flex-direction: column; overflow: hidden; min-height: 0; min-width: 0; }
695
706
  .sessions-list .ssl-h { padding: 10px 12px; border-bottom: 1px solid var(--bd); display: flex; align-items: center; gap: 8px; }
696
707
  .sessions-list .ssl-h input {
697
708
  flex: 1; background: var(--bg-input); border: 1px solid var(--bd); border-radius: var(--r);
@@ -710,7 +721,7 @@ main { padding: 32px 40px 60px 32px; min-width: 0; }
710
721
  .ssl-row .meta { display: flex; gap: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--fg-4); margin-top: 2px; }
711
722
  .ssl-row .meta .v { color: var(--fg-2); }
712
723
 
713
- .sessions-detail { background: var(--bg-elev); border: 1px solid var(--bd); border-radius: var(--r); padding: 14px 16px; overflow: auto; }
724
+ .sessions-detail { background: var(--bg-elev); border: 1px solid var(--bd); border-radius: var(--r); padding: 14px 16px; overflow: auto; min-height: 0; min-width: 0; }
714
725
  .sessions-detail-h { display: flex; align-items: baseline; gap: 12px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--bd); }
715
726
  .sessions-detail-h .name { font-family: var(--font-mono); font-size: 14px; color: var(--fg-0); font-weight: 600; }
716
727
  .sessions-detail-h .ws { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); }
@@ -19013,7 +19013,33 @@ var en = {
19013
19013
  promptsTitle: "Prompts \xB7 {count}",
19014
19014
  colName: "name",
19015
19015
  colDesc: "description",
19016
- colUri: "uri"
19016
+ colUri: "uri",
19017
+ marketplace: "marketplace",
19018
+ marketplaceSearch: "search the registry\u2026",
19019
+ marketplaceLoading: "loading registry\u2026",
19020
+ marketplaceMore: "load 5 more pages",
19021
+ marketplaceMoreLabel: "load 50 more \xB7 showing {shown} / {total}",
19022
+ marketplaceMoreHint: "fetches more pages from the registry",
19023
+ marketplaceMoreCachedHint: "more entries already cached locally",
19024
+ marketplaceExhausted: "all pages loaded",
19025
+ marketplaceExhaustedFull: "showing all {total} entries \u2014 registry exhausted",
19026
+ marketplaceCount: "{loaded} loaded \xB7 {matched} match \xB7 source: {source}{cached}",
19027
+ marketplaceCachedSuffix: " \xB7 cached",
19028
+ marketplaceNoMatches: "No matches. Try different terms or load more pages.",
19029
+ marketplaceInstall: "Install",
19030
+ marketplacePickHint: "Pick a server on the left, then Install.",
19031
+ marketplaceInstalled: "installed \u2192 {spec}",
19032
+ marketplaceInstalledBridged: "installed + bridged \u2192 {spec}",
19033
+ marketplaceAlready: "already installed",
19034
+ marketplaceNeedsEnv: "needs env: {names}",
19035
+ marketplaceSourceTag: "[{source}]",
19036
+ marketplaceNoInstall: "smithery listing \u2014 install metadata not exposed; use `npx -y @smithery/cli install {name}` directly",
19037
+ marketplaceFetchOnInstall: "Smithery listing \u2014 install detail fetched on Install. http servers map to streamable-http remotes; stdio servers run via @smithery/cli.",
19038
+ marketplaceInstalledBadge: "installed",
19039
+ marketplaceUninstall: "Uninstall",
19040
+ marketplaceEnvTitle: "Required environment variables",
19041
+ marketplaceEnvHint: "Set these in your shell before next `reasonix code` so the bridged server can authenticate.",
19042
+ marketplaceRestartHint: "Spec written to ~/.reasonix/config.json. Restart `reasonix code` to bridge the server (live hot-reload is on the roadmap)."
19017
19043
  },
19018
19044
  memory: {
19019
19045
  loading: "loading memory\u2026",
@@ -19500,7 +19526,33 @@ var zhCN = {
19500
19526
  promptsTitle: "\u63D0\u793A \xB7 {count}",
19501
19527
  colName: "\u540D\u79F0",
19502
19528
  colDesc: "\u63CF\u8FF0",
19503
- colUri: "URI"
19529
+ colUri: "URI",
19530
+ marketplace: "\u5E02\u573A",
19531
+ marketplaceSearch: "\u641C\u7D22\u6CE8\u518C\u8868\u2026",
19532
+ marketplaceLoading: "\u52A0\u8F7D\u6CE8\u518C\u8868\u2026",
19533
+ marketplaceMore: "\u518D\u52A0\u8F7D 5 \u9875",
19534
+ marketplaceMoreLabel: "\u518D\u52A0\u8F7D 50 \u6761 \xB7 \u5F53\u524D {shown} / {total}",
19535
+ marketplaceMoreHint: "\u9700\u8981\u4ECE\u8FDC\u7AEF\u6CE8\u518C\u8868\u518D\u62C9\u51E0\u9875",
19536
+ marketplaceMoreCachedHint: "\u672C\u5730\u7F13\u5B58\u5DF2\u6709\u66F4\u591A\u6761\u76EE",
19537
+ marketplaceExhausted: "\u5DF2\u52A0\u8F7D\u5168\u90E8\u9875",
19538
+ marketplaceExhaustedFull: "\u5DF2\u5C55\u793A\u5168\u90E8 {total} \u6761 \u2014 \u6CE8\u518C\u8868\u8017\u5C3D",
19539
+ marketplaceCount: "\u5DF2\u8F7D\u5165 {loaded} \xB7 \u5339\u914D {matched} \xB7 \u6765\u6E90\uFF1A{source}{cached}",
19540
+ marketplaceCachedSuffix: " \xB7 \u7F13\u5B58\u4E2D",
19541
+ marketplaceNoMatches: "\u65E0\u5339\u914D\u7ED3\u679C\u3002\u6362\u5173\u952E\u8BCD\u6216\u52A0\u8F7D\u66F4\u591A\u9875\u3002",
19542
+ marketplaceInstall: "\u5B89\u88C5",
19543
+ marketplacePickHint: "\u5728\u5DE6\u4FA7\u9009\u62E9\u670D\u52A1\u5668\uFF0C\u7136\u540E\u70B9\u5B89\u88C5\u3002",
19544
+ marketplaceInstalled: "\u5DF2\u5B89\u88C5 \u2192 {spec}",
19545
+ marketplaceInstalledBridged: "\u5DF2\u5B89\u88C5\u5E76\u6865\u63A5 \u2192 {spec}",
19546
+ marketplaceAlready: "\u5DF2\u5B89\u88C5\u8FC7",
19547
+ marketplaceNeedsEnv: "\u9700\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\uFF1A{names}",
19548
+ marketplaceSourceTag: "[{source}]",
19549
+ marketplaceNoInstall: "Smithery \u5217\u8868\u9879 \u2014 \u4E0D\u66B4\u9732\u5B89\u88C5\u5143\u6570\u636E\uFF1B\u8BF7\u76F4\u63A5 `npx -y @smithery/cli install {name}`",
19550
+ marketplaceFetchOnInstall: "Smithery \u5217\u8868 \u2014 \u5B89\u88C5\u65F6\u518D\u62C9\u8BE6\u60C5\u3002HTTP \u670D\u52A1\u6620\u5C04\u4E3A streamable-http \u8FDC\u7AEF\uFF1Bstdio \u670D\u52A1\u901A\u8FC7 @smithery/cli \u8FD0\u884C\u3002",
19551
+ marketplaceInstalledBadge: "\u5DF2\u5B89\u88C5",
19552
+ marketplaceUninstall: "\u5378\u8F7D",
19553
+ marketplaceEnvTitle: "\u5FC5\u9700\u7684\u73AF\u5883\u53D8\u91CF",
19554
+ marketplaceEnvHint: "\u4E0B\u6B21\u542F\u52A8 `reasonix code` \u4E4B\u524D\u5728 shell \u91CC\u8BBE\u597D\uFF0C\u6865\u63A5\u7684\u670D\u52A1\u5668\u624D\u80FD\u6B63\u5E38\u9274\u6743\u3002",
19555
+ marketplaceRestartHint: "\u5DF2\u5199\u5165 ~/.reasonix/config.json\u3002\u91CD\u542F `reasonix code` \u540E\u670D\u52A1\u5668\u624D\u4F1A\u771F\u6B63\u6865\u63A5\uFF08\u70ED\u91CD\u8F7D\u5728\u8DEF\u7EBF\u56FE\u4E0A\uFF09\u3002"
19504
19556
  },
19505
19557
  memory: {
19506
19558
  loading: "\u52A0\u8F7D\u8BB0\u5FC6\u2026",
@@ -23432,6 +23484,24 @@ function HooksPanel() {
23432
23484
  }
23433
23485
 
23434
23486
  // dashboard/src/panels/mcp.ts
23487
+ function specForEntry(e3) {
23488
+ if (!e3.install) return null;
23489
+ const localName = e3.name.split("/").pop() ?? e3.name;
23490
+ const safe = localName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "mcp";
23491
+ const trail = e3.install.extraArgs?.length ? ` ${e3.install.extraArgs.join(" ")}` : "";
23492
+ if (e3.install.runtime === "remote" && e3.install.url) {
23493
+ if (e3.install.transport === "streamable-http") return `${safe}=streamable+${e3.install.url}`;
23494
+ return `${safe}=${e3.install.url}`;
23495
+ }
23496
+ if (e3.install.runtime === "npm" && e3.install.packageId) {
23497
+ const pinned = e3.install.version ? `${e3.install.packageId}@${e3.install.version}` : e3.install.packageId;
23498
+ return `${safe}=npx -y ${pinned}${trail}`;
23499
+ }
23500
+ if (e3.install.runtime === "pypi" && e3.install.packageId) {
23501
+ return `${safe}=uvx ${e3.install.packageId}${trail}`;
23502
+ }
23503
+ return null;
23504
+ }
23435
23505
  function specLabel(spec) {
23436
23506
  const eq = spec.indexOf("=");
23437
23507
  return eq > 0 ? spec.slice(0, eq) : spec;
@@ -23451,6 +23521,57 @@ function McpPanel() {
23451
23521
  const [open, setOpen] = p2(null);
23452
23522
  const [openUnbridged, setOpenUnbridged] = p2(null);
23453
23523
  const [filter, setFilter] = p2("all");
23524
+ const [registry, setRegistry] = p2(null);
23525
+ const [registryQuery, setRegistryQuery] = p2("");
23526
+ const [registryLoading, setRegistryLoading] = p2(false);
23527
+ const [openRegistry, setOpenRegistry] = p2(null);
23528
+ const [displayLimit, setDisplayLimit] = p2(50);
23529
+ const loadRegistry = x2(async (q3, pages, limit) => {
23530
+ setRegistryLoading(true);
23531
+ try {
23532
+ const params = new URLSearchParams();
23533
+ if (q3.trim()) params.set("q", q3.trim());
23534
+ params.set("pages", String(pages));
23535
+ params.set("maxPages", String(Math.max(20, pages)));
23536
+ params.set("limit", String(limit));
23537
+ const r3 = await api(`/mcp/registry?${params.toString()}`);
23538
+ setRegistry(r3);
23539
+ } catch (err) {
23540
+ setError(err.message);
23541
+ } finally {
23542
+ setRegistryLoading(false);
23543
+ }
23544
+ }, []);
23545
+ _2(() => {
23546
+ if (filter !== "marketplace") return;
23547
+ if (registry) return;
23548
+ void loadRegistry("", 1, displayLimit);
23549
+ }, [filter, registry, loadRegistry, displayLimit]);
23550
+ _2(() => {
23551
+ if (filter !== "marketplace") return;
23552
+ setDisplayLimit(50);
23553
+ const handle = setTimeout(() => void loadRegistry(registryQuery, 1, 50), 300);
23554
+ return () => clearTimeout(handle);
23555
+ }, [registryQuery, filter, loadRegistry]);
23556
+ const installFromRegistry = x2(async (entry) => {
23557
+ setBusy(true);
23558
+ try {
23559
+ const r3 = await api("/mcp/registry/install", { method: "POST", body: { name: entry.name } });
23560
+ if (r3.alreadyPresent) {
23561
+ setInfo(t4("mcp.marketplaceAlready"));
23562
+ } else if (r3.bridged) {
23563
+ setInfo(t4("mcp.marketplaceInstalledBridged", { spec: r3.spec }));
23564
+ } else {
23565
+ setInfo(t4("mcp.marketplaceInstalled", { spec: r3.spec }));
23566
+ }
23567
+ setTimeout(() => setInfo(null), 5e3);
23568
+ await load();
23569
+ } catch (err) {
23570
+ setError(err.message);
23571
+ } finally {
23572
+ setBusy(false);
23573
+ }
23574
+ }, []);
23454
23575
  const load = x2(async () => {
23455
23576
  try {
23456
23577
  setData(await api("/mcp"));
@@ -23470,9 +23591,7 @@ function McpPanel() {
23470
23591
  method: "POST",
23471
23592
  body: { spec: newSpec.trim() }
23472
23593
  });
23473
- setInfo(
23474
- r3.requiresRestart ? t4("mcp.savedRestart") : t4("mcp.saved")
23475
- );
23594
+ setInfo(r3.requiresRestart ? t4("mcp.savedRestart") : t4("mcp.saved"));
23476
23595
  setTimeout(() => setInfo(null), 4e3);
23477
23596
  setNewSpec("");
23478
23597
  await load();
@@ -23499,14 +23618,16 @@ function McpPanel() {
23499
23618
  },
23500
23619
  [load]
23501
23620
  );
23502
- if (!data && !error) return html4`<div class="card" style="color:var(--fg-3)">${t4("mcp.loading")}</div>`;
23621
+ if (!data && !error)
23622
+ return html4`<div class="card" style="color:var(--fg-3)">${t4("mcp.loading")}</div>`;
23503
23623
  if (error && !data) return html4`<div class="card accent-err">${error}</div>`;
23504
23624
  if (!data) return null;
23505
23625
  const liveCount = data.servers.length;
23506
23626
  const unbridgedSpecs = (specs ?? []).filter((spec) => !data.servers.some((s3) => s3.spec === spec));
23507
23627
  const unbridgedCount = unbridgedSpecs.length;
23508
- const showLive = filter !== "unbridged";
23509
- const showUnbridged = filter !== "live";
23628
+ const showLive = filter === "all" || filter === "live";
23629
+ const showUnbridged = filter === "all" || filter === "unbridged";
23630
+ const showMarketplace = filter === "marketplace";
23510
23631
  return html4`
23511
23632
  <div class="sessions-grid">
23512
23633
  <div class="sessions-list">
@@ -23518,25 +23639,63 @@ function McpPanel() {
23518
23639
  <span class=${`chip-f ${filter === "all" ? "active" : ""}`} onClick=${() => setFilter("all")}>${t4("mcp.all")} <span class="ct">${liveCount + unbridgedCount}</span></span>
23519
23640
  <span class=${`chip-f ${filter === "live" ? "active" : ""}`} onClick=${() => setFilter("live")}>${t4("mcp.live")} <span class="ct">${liveCount}</span></span>
23520
23641
  <span class=${`chip-f ${filter === "unbridged" ? "active" : ""}`} onClick=${() => setFilter("unbridged")}>${t4("mcp.unbridged")} <span class="ct">${unbridgedCount}</span></span>
23642
+ <span class=${`chip-f ${filter === "marketplace" ? "active" : ""}`} onClick=${() => setFilter("marketplace")}>${t4("mcp.marketplace")}</span>
23521
23643
  </div>
23522
23644
  </div>
23523
- <div style="padding:8px 12px;display:flex;gap:6px">
23524
- <input
23525
- type="text"
23526
- placeholder=${t4("mcp.specPlaceholder")}
23527
- value=${newSpec}
23528
- onInput=${(e3) => setNewSpec(e3.target.value)}
23529
- style="flex:1;font-size:11px"
23530
- />
23531
- <button class="btn primary" disabled=${busy || !newSpec.trim()} onClick=${addSpec}>+</button>
23532
- </div>
23645
+ ${showMarketplace ? html4`
23646
+ <div style="padding:8px 12px;display:flex;gap:6px">
23647
+ <input
23648
+ type="text"
23649
+ placeholder=${t4("mcp.marketplaceSearch")}
23650
+ value=${registryQuery}
23651
+ onInput=${(e3) => setRegistryQuery(e3.target.value)}
23652
+ style="flex:1;font-size:11px"
23653
+ />
23654
+ </div>
23655
+ ${registry ? html4`<div style="padding:0 12px 6px;font-size:11px;color:var(--fg-3)">
23656
+ ${t4("mcp.marketplaceCount", {
23657
+ loaded: registry.loaded,
23658
+ matched: registry.matched,
23659
+ source: registry.source,
23660
+ cached: registry.fromCache ? t4("mcp.marketplaceCachedSuffix") : ""
23661
+ })}
23662
+ </div>` : null}
23663
+ ` : html4`
23664
+ <div style="padding:8px 12px;display:flex;gap:6px">
23665
+ <input
23666
+ type="text"
23667
+ placeholder=${t4("mcp.specPlaceholder")}
23668
+ value=${newSpec}
23669
+ onInput=${(e3) => setNewSpec(e3.target.value)}
23670
+ style="flex:1;font-size:11px"
23671
+ />
23672
+ <button class="btn primary" disabled=${busy || !newSpec.trim()} onClick=${addSpec}>+</button>
23673
+ </div>
23674
+ `}
23533
23675
  ${info ? html4`<div style="padding:0 12px 8px"><span class="pill ok">${info}</span></div>` : null}
23534
23676
  ${error ? html4`<div class="card accent-err" style="margin:0 12px 8px">${error}</div>` : null}
23535
23677
 
23536
23678
  <div class="ssl-rows">
23537
- ${liveCount === 0 && unbridgedCount === 0 ? html4`<div style="color:var(--fg-3);padding:14px;font-size:12px">
23679
+ ${!showMarketplace && liveCount === 0 && unbridgedCount === 0 ? html4`<div style="color:var(--fg-3);padding:14px;font-size:12px">
23538
23680
  ${t4("mcp.noServers")}
23539
23681
  </div>` : null}
23682
+ ${showMarketplace ? renderMarketplaceRows({
23683
+ registry,
23684
+ registryLoading,
23685
+ openRegistry,
23686
+ setOpenRegistry: (e3) => {
23687
+ setOpenRegistry(e3);
23688
+ setOpen(null);
23689
+ setOpenUnbridged(null);
23690
+ },
23691
+ loadMore: () => {
23692
+ const nextLimit = displayLimit + 50;
23693
+ setDisplayLimit(nextLimit);
23694
+ const pagesNeeded = Math.ceil(nextLimit / 30) + 3;
23695
+ void loadRegistry(registryQuery, pagesNeeded, nextLimit);
23696
+ },
23697
+ installedSpecs: new Set(specs ?? [])
23698
+ }) : null}
23540
23699
  ${showLive ? data.servers.map(
23541
23700
  (s3) => html4`
23542
23701
  <div
@@ -23571,7 +23730,17 @@ function McpPanel() {
23571
23730
  </div>
23572
23731
 
23573
23732
  <div class="sessions-detail">
23574
- ${openUnbridged != null ? html4`
23733
+ ${openRegistry != null ? renderRegistryDetail({
23734
+ entry: openRegistry,
23735
+ busy,
23736
+ installedSpec: (() => {
23737
+ const spec = specForEntry(openRegistry);
23738
+ return spec && (specs ?? []).includes(spec) ? spec : null;
23739
+ })(),
23740
+ onInstall: () => installFromRegistry(openRegistry),
23741
+ onUninstall: (spec) => removeSpec(spec),
23742
+ onClose: () => setOpenRegistry(null)
23743
+ }) : openUnbridged != null ? html4`
23575
23744
  <div class="sessions-detail-h">
23576
23745
  <span class="name">${specLabel(openUnbridged)}</span>
23577
23746
  <span class="ws"><span class="pill">${t4("mcp.unbridgedTitle")}</span></span>
@@ -23595,7 +23764,7 @@ function McpPanel() {
23595
23764
  </div>
23596
23765
  </div>
23597
23766
  ` : open == null ? html4`<div style="color:var(--fg-3);font-size:13px;text-align:center;padding:60px 20px">
23598
- ${t4("mcp.pickHint")}
23767
+ ${showMarketplace ? t4("mcp.marketplacePickHint") : t4("mcp.pickHint")}
23599
23768
  </div>` : html4`
23600
23769
  <div class="sessions-detail-h">
23601
23770
  <span class="name">${open.label}</span>
@@ -23664,6 +23833,131 @@ function McpPanel() {
23664
23833
  </div>
23665
23834
  `;
23666
23835
  }
23836
+ function renderLoadMoreFooter({
23837
+ registry,
23838
+ registryLoading,
23839
+ loadMore
23840
+ }) {
23841
+ if (!registry) return null;
23842
+ const shown = registry.entries.length;
23843
+ const total = registry.matched;
23844
+ const moreCached = total > shown;
23845
+ const moreOnNetwork = registry.hasMore;
23846
+ const canDoSomething = moreCached || moreOnNetwork;
23847
+ if (canDoSomething) {
23848
+ const label = registryLoading ? t4("mcp.marketplaceLoading") : t4("mcp.marketplaceMoreLabel", {
23849
+ shown,
23850
+ total: moreOnNetwork ? `${total}+` : `${total}`
23851
+ });
23852
+ return html4`<div style="padding:10px 12px;display:flex;align-items:center;gap:10px">
23853
+ <button class="btn primary" disabled=${registryLoading} onClick=${loadMore}>${label}</button>
23854
+ <span style="font-size:11px;color:var(--fg-3)">
23855
+ ${moreOnNetwork ? t4("mcp.marketplaceMoreHint") : t4("mcp.marketplaceMoreCachedHint")}
23856
+ </span>
23857
+ </div>`;
23858
+ }
23859
+ return html4`<div style="padding:12px;background:var(--bg-elev-2,rgba(36,143,242,0.07));border-top:1px solid var(--bd);display:flex;align-items:center;gap:8px;font-size:12px;color:var(--fg-2)">
23860
+ <span style="color:var(--c-ok)">✓</span>
23861
+ <span>${t4("mcp.marketplaceExhaustedFull", { total })}</span>
23862
+ </div>`;
23863
+ }
23864
+ function renderMarketplaceRows({
23865
+ registry,
23866
+ registryLoading,
23867
+ openRegistry,
23868
+ setOpenRegistry,
23869
+ loadMore,
23870
+ installedSpecs
23871
+ }) {
23872
+ if (!registry && registryLoading) {
23873
+ return html4`<div style="color:var(--fg-3);padding:14px;font-size:12px">${t4("mcp.marketplaceLoading")}</div>`;
23874
+ }
23875
+ if (!registry || registry.entries.length === 0) {
23876
+ return html4`<div style="color:var(--fg-3);padding:14px;font-size:12px">${t4("mcp.marketplaceNoMatches")}</div>`;
23877
+ }
23878
+ return html4`
23879
+ ${registry.entries.map((e3) => {
23880
+ const sel = openRegistry?.name === e3.name;
23881
+ const tag2 = t4("mcp.marketplaceSourceTag", { source: e3.source });
23882
+ const spec = specForEntry(e3);
23883
+ const installed = spec !== null && installedSpecs.has(spec);
23884
+ const pop = e3.popularity !== void 0 ? html4` <span class="dim">· ${fmtNum(e3.popularity)}</span>` : null;
23885
+ const icon = e3.iconUrl ? html4`<img src=${e3.iconUrl} alt="" style="width:16px;height:16px;border-radius:3px;margin-right:6px;vertical-align:middle;object-fit:cover" loading="lazy" referrerpolicy="no-referrer" onError=${(ev) => ev.target.style.display = "none"} />` : null;
23886
+ return html4`
23887
+ <div class=${`ssl-row ${sel ? "sel" : ""}`} onClick=${() => setOpenRegistry(e3)}>
23888
+ <span class="name">${icon}${e3.name} <span class="pill">${tag2}</span>${installed ? html4` <span class="pill ok">${t4("mcp.marketplaceInstalledBadge")}</span>` : null}</span>
23889
+ <span class="preview">${e3.description}</span>
23890
+ <span class="meta">${pop}</span>
23891
+ </div>
23892
+ `;
23893
+ })}
23894
+ ${renderLoadMoreFooter({ registry, registryLoading, loadMore })}
23895
+ `;
23896
+ }
23897
+ function renderRegistryDetail({
23898
+ entry,
23899
+ busy,
23900
+ installedSpec,
23901
+ onInstall,
23902
+ onUninstall,
23903
+ onClose
23904
+ }) {
23905
+ const installable = !!entry.install || entry.source === "smithery";
23906
+ const installed = installedSpec !== null;
23907
+ const specPreview = entry.install ? `${entry.install.runtime} \xB7 ${entry.install.transport}${entry.install.packageId ? ` \xB7 ${entry.install.packageId}` : entry.install.url ? ` \xB7 ${entry.install.url}` : ""}${entry.install.version ? `@${entry.install.version}` : ""}` : "";
23908
+ const icon = entry.iconUrl ? html4`<img src=${entry.iconUrl} alt="" style="width:24px;height:24px;border-radius:4px;margin-right:8px;vertical-align:middle;object-fit:cover" loading="lazy" referrerpolicy="no-referrer" onError=${(ev) => ev.target.style.display = "none"} />` : null;
23909
+ return html4`
23910
+ <div class="sessions-detail-h">
23911
+ <span class="name">${icon}${entry.name}${installed ? html4` <span class="pill ok">${t4("mcp.marketplaceInstalledBadge")}</span>` : null}</span>
23912
+ <span class="ws">${t4("mcp.marketplaceSourceTag", { source: entry.source })}${entry.popularity !== void 0 ? ` \xB7 ${fmtNum(entry.popularity)} uses` : ""}${entry.homepage ? html4` · <a href=${entry.homepage} target="_blank" rel="noopener noreferrer">homepage</a>` : ""}</span>
23913
+ <span class="actions">
23914
+ ${installed ? html4`<button
23915
+ class="btn"
23916
+ disabled=${busy}
23917
+ onClick=${() => onUninstall(installedSpec)}
23918
+ style="border-color:var(--c-err);color:var(--c-err)"
23919
+ >${t4("mcp.marketplaceUninstall")}</button>` : html4`<button class="btn primary" disabled=${busy || !installable} onClick=${onInstall}>${t4("mcp.marketplaceInstall")}</button>`}
23920
+ <button class="btn ghost" onClick=${onClose}>${t4("common.back")}</button>
23921
+ </span>
23922
+ </div>
23923
+
23924
+ <div class="card" style="margin-bottom:12px">
23925
+ <div class="card-b" style="font-size:13px;line-height:1.6">${entry.description || "\u2014"}</div>
23926
+ </div>
23927
+
23928
+ ${entry.install ? html4`<div class="card" style="margin-bottom:12px">
23929
+ <div class="card-h"><span class="title">${t4("mcp.spec")}</span></div>
23930
+ <div class="card-b">
23931
+ <code class="mono" style="font-size:11.5px;color:var(--fg-2);word-break:break-all;display:block">${specPreview}</code>
23932
+ ${installedSpec ? html4`<div style="margin-top:8px;font-size:11px;color:var(--fg-3)">
23933
+ <span class="dim">on disk:</span> <code class="mono">${installedSpec}</code>
23934
+ </div>` : null}
23935
+ </div>
23936
+ </div>` : entry.source === "smithery" ? html4`<div class="card" style="margin-bottom:12px">
23937
+ <div class="card-b" style="font-size:13px;line-height:1.6;color:var(--fg-3)">
23938
+ ${t4("mcp.marketplaceFetchOnInstall")}
23939
+ </div>
23940
+ </div>` : null}
23941
+
23942
+ ${entry.install?.requiredEnv?.length ? html4`<div class="card accent-brand" style="margin-bottom:12px">
23943
+ <div class="card-h"><span class="title">${t4("mcp.marketplaceEnvTitle")}</span></div>
23944
+ <div class="card-b" style="font-size:13px">
23945
+ ${entry.install.requiredEnv.map(
23946
+ (name) => html4`<div><code class="mono" style="color:var(--c-warn)">${name}</code></div>`
23947
+ )}
23948
+ <div style="margin-top:6px;color:var(--fg-3);font-size:12px">
23949
+ ${t4("mcp.marketplaceEnvHint")}
23950
+ </div>
23951
+ </div>
23952
+ </div>` : null}
23953
+
23954
+ ${installed ? html4`<div class="card accent-warn">
23955
+ <div class="card-b" style="font-size:12.5px;line-height:1.6">
23956
+ ${t4("mcp.marketplaceRestartHint")}
23957
+ </div>
23958
+ </div>` : null}
23959
+ `;
23960
+ }
23667
23961
 
23668
23962
  // dashboard/src/panels/memory.ts
23669
23963
  function MemoryPanel() {