opencode-plugin-hub 0.1.0 → 0.1.2
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 +45 -7
- package/dist/gui-experimental.cjs +59 -37
- package/package.json +1 -1
- package/src/contract.ts +2 -0
- package/src/gui/catalog.ts +25 -7
- package/src/gui/page.tsx +109 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-plugin-hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Plugin manager for OpenCode Desktop: browse community plugins, search npm, inspect README and downloads, install and update without editing JSON.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/contract.ts
CHANGED
|
@@ -131,6 +131,8 @@ export const CatalogResult = Schema.Struct({
|
|
|
131
131
|
entries: Schema.Array(CatalogEntry),
|
|
132
132
|
/** `network`, `cache` or `builtin`. */
|
|
133
133
|
source: Schema.String,
|
|
134
|
+
/** The registry URL the entries came from. Optional for older cached results. */
|
|
135
|
+
registry: Schema.optional(Schema.String),
|
|
134
136
|
fetchedAt: Schema.Number,
|
|
135
137
|
error: Schema.String,
|
|
136
138
|
})
|
package/src/gui/catalog.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface CatalogResultValue {
|
|
|
27
27
|
entries: CatalogEntryValue[]
|
|
28
28
|
/** Where the list came from: `network`, `cache` or `builtin`. */
|
|
29
29
|
source: string
|
|
30
|
+
/** The registry URL the entries came from. */
|
|
31
|
+
registry: string
|
|
30
32
|
/** Epoch milliseconds of the data, 0 for the built-in snapshot. */
|
|
31
33
|
fetchedAt: number
|
|
32
34
|
error: string
|
|
@@ -129,7 +131,7 @@ function toEntries(raw: RawEntry[], installed: Set<string>): CatalogEntryValue[]
|
|
|
129
131
|
return out
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
async function fetchRemote(): Promise<RawEntry[]> {
|
|
134
|
+
async function fetchRemote(): Promise<{ entries: RawEntry[]; url: string }> {
|
|
133
135
|
let lastError = ""
|
|
134
136
|
for (const url of SOURCES) {
|
|
135
137
|
try {
|
|
@@ -143,7 +145,7 @@ async function fetchRemote(): Promise<RawEntry[]> {
|
|
|
143
145
|
lastError = "registry 格式不是数组"
|
|
144
146
|
continue
|
|
145
147
|
}
|
|
146
|
-
return body as RawEntry[]
|
|
148
|
+
return { entries: body as RawEntry[], url }
|
|
147
149
|
} catch (error) {
|
|
148
150
|
lastError = describe(error)
|
|
149
151
|
}
|
|
@@ -153,6 +155,8 @@ async function fetchRemote(): Promise<RawEntry[]> {
|
|
|
153
155
|
|
|
154
156
|
interface CacheFile {
|
|
155
157
|
fetchedAt: number
|
|
158
|
+
/** The source URL that produced this cache. Older caches do not have it. */
|
|
159
|
+
url?: string
|
|
156
160
|
entries: RawEntry[]
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -167,11 +171,11 @@ async function readCache(): Promise<CacheFile | undefined> {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
async function writeCache(entries: RawEntry[]): Promise<number> {
|
|
174
|
+
async function writeCache(entries: RawEntry[], url: string): Promise<number> {
|
|
171
175
|
const fetchedAt = Date.now()
|
|
172
176
|
try {
|
|
173
177
|
await mkdir(cacheDir(), { recursive: true })
|
|
174
|
-
await writeFile(cacheFile(), JSON.stringify({ fetchedAt, entries }), "utf8")
|
|
178
|
+
await writeFile(cacheFile(), JSON.stringify({ fetchedAt, url, entries }), "utf8")
|
|
175
179
|
} catch {
|
|
176
180
|
// A failed cache write still leaves the in-memory result usable.
|
|
177
181
|
}
|
|
@@ -186,17 +190,30 @@ export async function browse(installed: Set<string>, force: boolean): Promise<Ca
|
|
|
186
190
|
const cache = await readCache()
|
|
187
191
|
const fresh = cache !== undefined && Date.now() - cache.fetchedAt < TTL
|
|
188
192
|
if (cache && fresh && !force) {
|
|
189
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
entries: toEntries(cache.entries, installed),
|
|
195
|
+
source: "cache",
|
|
196
|
+
registry: cache.url ?? SOURCES[0]!,
|
|
197
|
+
fetchedAt: cache.fetchedAt,
|
|
198
|
+
error: "",
|
|
199
|
+
}
|
|
190
200
|
}
|
|
191
201
|
try {
|
|
192
202
|
const remote = await fetchRemote()
|
|
193
|
-
const fetchedAt = await writeCache(remote)
|
|
194
|
-
return {
|
|
203
|
+
const fetchedAt = await writeCache(remote.entries, remote.url)
|
|
204
|
+
return {
|
|
205
|
+
entries: toEntries(remote.entries, installed),
|
|
206
|
+
source: "network",
|
|
207
|
+
registry: remote.url,
|
|
208
|
+
fetchedAt,
|
|
209
|
+
error: "",
|
|
210
|
+
}
|
|
195
211
|
} catch (error) {
|
|
196
212
|
if (cache) {
|
|
197
213
|
return {
|
|
198
214
|
entries: toEntries(cache.entries, installed),
|
|
199
215
|
source: "cache",
|
|
216
|
+
registry: cache.url ?? SOURCES[0]!,
|
|
200
217
|
fetchedAt: cache.fetchedAt,
|
|
201
218
|
error: describe(error),
|
|
202
219
|
}
|
|
@@ -204,6 +221,7 @@ export async function browse(installed: Set<string>, force: boolean): Promise<Ca
|
|
|
204
221
|
return {
|
|
205
222
|
entries: toEntries(SNAPSHOT.map(toRaw), installed),
|
|
206
223
|
source: "builtin",
|
|
224
|
+
registry: SOURCES[0]!,
|
|
207
225
|
fetchedAt: 0,
|
|
208
226
|
error: describe(error),
|
|
209
227
|
}
|
package/src/gui/page.tsx
CHANGED
|
@@ -17,11 +17,8 @@ type Note = { text: string; tone: Tone }
|
|
|
17
17
|
|
|
18
18
|
const PAGE = 24
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
cache: "社区目录 · 本地缓存",
|
|
23
|
-
builtin: "社区目录 · 内置快照",
|
|
24
|
-
}
|
|
20
|
+
const CATALOG_SOURCE_FALLBACK = "https://github.com/awesome-opencode/awesome-opencode"
|
|
21
|
+
const CATALOG_SOURCE_TITLE = "精选目录来自 awesome-opencode 社区维护的 registry.json,不是 OpenCode 官方推荐"
|
|
25
22
|
|
|
26
23
|
const TYPE_LABEL: Record<string, string> = {
|
|
27
24
|
plugins: "插件",
|
|
@@ -72,16 +69,6 @@ function relativeDate(value: string): string {
|
|
|
72
69
|
return `${Math.floor(days / 365)} 年前`
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
function formatDate(epoch: number): string {
|
|
76
|
-
if (!epoch) return ""
|
|
77
|
-
return new Date(epoch).toLocaleString("zh-CN", {
|
|
78
|
-
month: "2-digit",
|
|
79
|
-
day: "2-digit",
|
|
80
|
-
hour: "2-digit",
|
|
81
|
-
minute: "2-digit",
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
72
|
function initial(name: string): string {
|
|
86
73
|
const base = name.replace(/^@/, "").split("/").pop() ?? name
|
|
87
74
|
const cleaned = base.replace(/^opencode[-_]?/i, "")
|
|
@@ -115,7 +102,7 @@ function noteIcon(tone: Tone): IconName {
|
|
|
115
102
|
function readableError(error: unknown): string {
|
|
116
103
|
const text = error instanceof Error ? error.message : String(error)
|
|
117
104
|
if (/SchemaError|Missing key at/i.test(text)) {
|
|
118
|
-
return "
|
|
105
|
+
return "插件窗口与后台服务版本不一致。点上方工具栏的「彻底重启」让两边换成同一版本。"
|
|
119
106
|
}
|
|
120
107
|
if (/unknown method|not implemented|no such method|method.*not.*found/i.test(text)) {
|
|
121
108
|
return "后台服务还是旧版本,暂时没有「彻底重启」能力。请先手动完全退出 OpenCode(并结束 serve --service)再打开一次。"
|
|
@@ -173,7 +160,7 @@ const CSS = `
|
|
|
173
160
|
radial-gradient(120% 160% at 0% 0%, color-mix(in srgb, var(--ph-accent) 13%, transparent) 0%, transparent 52%),
|
|
174
161
|
var(--ph-layer);
|
|
175
162
|
}
|
|
176
|
-
.ph-hero-copy { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
163
|
+
.ph-hero-copy { display: flex; flex: 1; flex-direction: column; gap: 6px; min-width: 0; }
|
|
177
164
|
.ph-kicker {
|
|
178
165
|
display: flex;
|
|
179
166
|
align-items: center;
|
|
@@ -195,7 +182,22 @@ const CSS = `
|
|
|
195
182
|
font-size: 12px;
|
|
196
183
|
line-height: 1.6;
|
|
197
184
|
color: var(--ph-muted);
|
|
198
|
-
max-width:
|
|
185
|
+
max-width: 72ch;
|
|
186
|
+
}
|
|
187
|
+
.ph-inline-source {
|
|
188
|
+
padding: 0;
|
|
189
|
+
border: 0;
|
|
190
|
+
background: none;
|
|
191
|
+
color: var(--ph-accent);
|
|
192
|
+
font: inherit;
|
|
193
|
+
line-height: inherit;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
.ph-inline-source:hover { text-decoration: underline; }
|
|
197
|
+
.ph-inline-source:focus-visible {
|
|
198
|
+
outline: 2px solid color-mix(in srgb, var(--ph-accent) 55%, transparent);
|
|
199
|
+
outline-offset: 2px;
|
|
200
|
+
border-radius: 2px;
|
|
199
201
|
}
|
|
200
202
|
.ph-path {
|
|
201
203
|
overflow: hidden;
|
|
@@ -209,23 +211,6 @@ const CSS = `
|
|
|
209
211
|
border-radius: 5px;
|
|
210
212
|
background: var(--ph-layer-2);
|
|
211
213
|
}
|
|
212
|
-
.ph-hero-actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
|
213
|
-
.ph-stats {
|
|
214
|
-
display: grid;
|
|
215
|
-
grid-template-columns: repeat(2, minmax(92px, 1fr));
|
|
216
|
-
gap: 8px;
|
|
217
|
-
align-content: center;
|
|
218
|
-
}
|
|
219
|
-
.ph-stat {
|
|
220
|
-
min-width: 92px;
|
|
221
|
-
padding: 10px 12px;
|
|
222
|
-
border: 1px solid var(--ph-border);
|
|
223
|
-
border-radius: 10px;
|
|
224
|
-
background: var(--ph-layer-2);
|
|
225
|
-
}
|
|
226
|
-
.ph-stat b { display: block; font-size: 18px; font-weight: 650; line-height: 1.2; color: var(--ph-strong); }
|
|
227
|
-
.ph-stat span { font-size: 10.5px; color: var(--ph-muted); }
|
|
228
|
-
.ph-stat[data-tone="accent"] b { color: var(--ph-accent); }
|
|
229
214
|
|
|
230
215
|
/* notice */
|
|
231
216
|
.ph-notice {
|
|
@@ -300,6 +285,11 @@ const CSS = `
|
|
|
300
285
|
font-size: 9.5px;
|
|
301
286
|
font-weight: 600;
|
|
302
287
|
}
|
|
288
|
+
.ph-count-alert {
|
|
289
|
+
background: color-mix(in srgb, var(--ph-accent) 14%, transparent);
|
|
290
|
+
color: var(--ph-accent);
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
303
293
|
.ph-tab[data-active="true"] .ph-count {
|
|
304
294
|
background: color-mix(in srgb, var(--ph-accent) 16%, transparent);
|
|
305
295
|
color: var(--ph-accent);
|
|
@@ -607,6 +597,26 @@ button.ph-chip:focus-visible,
|
|
|
607
597
|
max-width: none;
|
|
608
598
|
padding: 0 0 8px;
|
|
609
599
|
}
|
|
600
|
+
/* The host caps "x-large" at 980x600. Give the standalone hub more room and
|
|
601
|
+
let the dialog be resized from its bottom-right corner, within the window. */
|
|
602
|
+
[data-component="dialog-v2"][data-size="x-large"]:has(.ph-root[data-embedded="true"]) [data-slot="dialog-container"] {
|
|
603
|
+
width: min(100vw - 48px, 1180px);
|
|
604
|
+
height: min(100vh - 88px, 780px);
|
|
605
|
+
min-width: min(100vw - 48px, 720px);
|
|
606
|
+
min-height: min(100vh - 88px, 480px);
|
|
607
|
+
max-width: calc(100vw - 24px);
|
|
608
|
+
max-height: calc(100vh - 24px);
|
|
609
|
+
resize: both;
|
|
610
|
+
}
|
|
611
|
+
[data-component="dialog-v2"][data-size="x-large"]:has(.ph-detail) [data-slot="dialog-container"] {
|
|
612
|
+
width: min(100vw - 48px, 1180px);
|
|
613
|
+
height: min(100vh - 88px, 780px);
|
|
614
|
+
min-width: min(100vw - 48px, 720px);
|
|
615
|
+
min-height: min(100vh - 88px, 480px);
|
|
616
|
+
max-width: calc(100vw - 24px);
|
|
617
|
+
max-height: calc(100vh - 24px);
|
|
618
|
+
resize: both;
|
|
619
|
+
}
|
|
610
620
|
.ph-root[data-embedded="true"] .ph-toolbar {
|
|
611
621
|
position: sticky;
|
|
612
622
|
top: 0;
|
|
@@ -629,7 +639,6 @@ button.ph-chip:focus-visible,
|
|
|
629
639
|
|
|
630
640
|
@media (max-width: 760px) {
|
|
631
641
|
.ph-hero { flex-direction: column; }
|
|
632
|
-
.ph-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
633
642
|
.ph-card { flex-wrap: wrap; }
|
|
634
643
|
.ph-card-body { flex-basis: calc(100% - 50px); }
|
|
635
644
|
.ph-actions { width: 100%; justify-content: flex-start; padding-left: 50px; }
|
|
@@ -1000,7 +1009,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1000
1009
|
notify(result.message, "error")
|
|
1001
1010
|
return
|
|
1002
1011
|
}
|
|
1003
|
-
notify(`${result.message}
|
|
1012
|
+
notify(`${result.message}。点上方工具栏的「彻底重启」后生效`, "success")
|
|
1004
1013
|
const latest = version || hits().find((hit) => hit.name === name)?.version || ""
|
|
1005
1014
|
setCatalog((prev) => prev.map((entry) => (entry.pkg === name ? { ...entry, installed: true } : entry)))
|
|
1006
1015
|
setHits((prev) =>
|
|
@@ -1031,7 +1040,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1031
1040
|
notify(result.message, "error")
|
|
1032
1041
|
return
|
|
1033
1042
|
}
|
|
1034
|
-
notify(`${result.message}
|
|
1043
|
+
notify(`${result.message}。点上方工具栏的「彻底重启」后生效`, "success")
|
|
1035
1044
|
setCatalog((prev) =>
|
|
1036
1045
|
prev.map((item) => (item.pkg === entry.name ? { ...item, installed: false } : item)),
|
|
1037
1046
|
)
|
|
@@ -1158,7 +1167,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1158
1167
|
return list
|
|
1159
1168
|
})
|
|
1160
1169
|
|
|
1161
|
-
const
|
|
1170
|
+
const registryUrl = () => meta()?.registry || CATALOG_SOURCE_FALLBACK
|
|
1162
1171
|
const outdated = createMemo(() => [...updates().values()].filter((item) => item.hasUpdate).length)
|
|
1163
1172
|
const bySpec = (spec: string) => updates().get(spec)
|
|
1164
1173
|
const byPackage = createMemo(() => {
|
|
@@ -1179,13 +1188,18 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1179
1188
|
void checkUpdates(true)
|
|
1180
1189
|
})
|
|
1181
1190
|
|
|
1182
|
-
const tabButton = (id: Tab, label: string, icon: IconName, count?: number | string) => (
|
|
1191
|
+
const tabButton = (id: Tab, label: string, icon: IconName, count?: number | string, alert?: number) => (
|
|
1183
1192
|
<button class="ph-tab" data-active={tab() === id} onClick={() => switchTab(id)}>
|
|
1184
1193
|
<ui.Icon name={icon} size="small" />
|
|
1185
1194
|
{label}
|
|
1186
1195
|
<Show when={count !== undefined && count !== ""}>
|
|
1187
1196
|
<span class="ph-count">{count}</span>
|
|
1188
1197
|
</Show>
|
|
1198
|
+
<Show when={alert !== undefined && alert > 0}>
|
|
1199
|
+
<span class="ph-count ph-count-alert" title={`${alert} 个可更新`}>
|
|
1200
|
+
{alert} 可更新
|
|
1201
|
+
</span>
|
|
1202
|
+
</Show>
|
|
1189
1203
|
</button>
|
|
1190
1204
|
)
|
|
1191
1205
|
|
|
@@ -1504,58 +1518,20 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1504
1518
|
<h2 class="ph-title">插件中心</h2>
|
|
1505
1519
|
</Show>
|
|
1506
1520
|
<p class="ph-sub">
|
|
1507
|
-
|
|
1521
|
+
浏览{" "}
|
|
1522
|
+
<button
|
|
1523
|
+
type="button"
|
|
1524
|
+
class="ph-inline-source"
|
|
1525
|
+
title={CATALOG_SOURCE_TITLE}
|
|
1526
|
+
onClick={() => void openUrl(registryUrl())}
|
|
1527
|
+
>
|
|
1528
|
+
awesome-opencode
|
|
1529
|
+
</button>{" "}
|
|
1530
|
+
社区维护的精选目录、搜索 npm 上的 OpenCode 插件。安装前先看下载量、README 和主页,确认它到底能做什么。
|
|
1508
1531
|
</p>
|
|
1509
1532
|
<div class="ph-path" title={state()?.configPath}>
|
|
1510
1533
|
配置:<code>{state()?.configPath ?? "读取中…"}</code> · <code>{state()?.configKey ?? "plugins"}[]</code>
|
|
1511
1534
|
</div>
|
|
1512
|
-
<div class="ph-hero-actions">
|
|
1513
|
-
<ui.Button
|
|
1514
|
-
variant="neutral"
|
|
1515
|
-
size="small"
|
|
1516
|
-
icon="open-file"
|
|
1517
|
-
busy={isBusy("config")}
|
|
1518
|
-
onClick={() => void openConfig()}
|
|
1519
|
-
>
|
|
1520
|
-
打开配置
|
|
1521
|
-
</ui.Button>
|
|
1522
|
-
<ui.Button
|
|
1523
|
-
variant="ghost"
|
|
1524
|
-
size="small"
|
|
1525
|
-
icon="refresh"
|
|
1526
|
-
busy={isBusy("reload")}
|
|
1527
|
-
onClick={() => void reload()}
|
|
1528
|
-
>
|
|
1529
|
-
重载配置
|
|
1530
|
-
</ui.Button>
|
|
1531
|
-
<ui.Button
|
|
1532
|
-
variant="ghost"
|
|
1533
|
-
size="small"
|
|
1534
|
-
icon="outline-rotate-clockwise"
|
|
1535
|
-
busy={isBusy("restart")}
|
|
1536
|
-
onClick={() => void restartApp()}
|
|
1537
|
-
>
|
|
1538
|
-
彻底重启
|
|
1539
|
-
</ui.Button>
|
|
1540
|
-
</div>
|
|
1541
|
-
</div>
|
|
1542
|
-
<div class="ph-stats">
|
|
1543
|
-
<div class="ph-stat">
|
|
1544
|
-
<b>{state()?.plugins.length ?? "—"}</b>
|
|
1545
|
-
<span>已安装</span>
|
|
1546
|
-
</div>
|
|
1547
|
-
<div class="ph-stat" data-tone={outdated() > 0 ? "accent" : ""}>
|
|
1548
|
-
<b>{checked() ? outdated() : "—"}</b>
|
|
1549
|
-
<span>可更新</span>
|
|
1550
|
-
</div>
|
|
1551
|
-
<div class="ph-stat">
|
|
1552
|
-
<b>{meta() ? catalog().length : "—"}</b>
|
|
1553
|
-
<span>精选条目</span>
|
|
1554
|
-
</div>
|
|
1555
|
-
<div class="ph-stat">
|
|
1556
|
-
<b>{meta() ? catalog().filter((entry) => entry.pkg).length : "—"}</b>
|
|
1557
|
-
<span>可直接安装</span>
|
|
1558
|
-
</div>
|
|
1559
1535
|
</div>
|
|
1560
1536
|
</section>
|
|
1561
1537
|
|
|
@@ -1577,20 +1553,36 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1577
1553
|
<div class="ph-tabs" role="tablist">
|
|
1578
1554
|
{tabButton("catalog", "精选目录", "extensions", meta() ? catalog().length : undefined)}
|
|
1579
1555
|
{tabButton("search", "npm 搜索", "magnifying-glass", searched() ? hits().length : undefined)}
|
|
1580
|
-
{tabButton("installed", "已安装", "checklist", state()?.plugins.length ?? 0)}
|
|
1556
|
+
{tabButton("installed", "已安装", "checklist", state()?.plugins.length ?? 0, outdated())}
|
|
1581
1557
|
</div>
|
|
1582
1558
|
<span class="ph-spacer" />
|
|
1583
|
-
<
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1559
|
+
<ui.Button
|
|
1560
|
+
variant="ghost"
|
|
1561
|
+
size="small"
|
|
1562
|
+
icon="open-file"
|
|
1563
|
+
busy={isBusy("config")}
|
|
1564
|
+
onClick={() => void openConfig()}
|
|
1565
|
+
>
|
|
1566
|
+
打开配置
|
|
1567
|
+
</ui.Button>
|
|
1568
|
+
<ui.Button
|
|
1569
|
+
variant="ghost"
|
|
1570
|
+
size="small"
|
|
1571
|
+
icon="refresh"
|
|
1572
|
+
busy={isBusy("reload")}
|
|
1573
|
+
onClick={() => void reload()}
|
|
1574
|
+
>
|
|
1575
|
+
重载配置
|
|
1576
|
+
</ui.Button>
|
|
1577
|
+
<ui.Button
|
|
1578
|
+
variant="neutral"
|
|
1579
|
+
size="small"
|
|
1580
|
+
icon="outline-rotate-clockwise"
|
|
1581
|
+
busy={isBusy("restart")}
|
|
1582
|
+
onClick={() => void restartApp()}
|
|
1583
|
+
>
|
|
1584
|
+
彻底重启
|
|
1585
|
+
</ui.Button>
|
|
1594
1586
|
<Show when={tab() === "catalog"}>
|
|
1595
1587
|
<ui.Button
|
|
1596
1588
|
variant="ghost"
|
|
@@ -1639,7 +1631,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1639
1631
|
<div class="ph-filter-row">
|
|
1640
1632
|
<div class="ph-grow">
|
|
1641
1633
|
<ui.TextInput
|
|
1642
|
-
label="
|
|
1634
|
+
label="筛选 awesome-opencode 精选插件"
|
|
1643
1635
|
placeholder="按名称、描述或标签筛选,例如 memory / auth / token"
|
|
1644
1636
|
type="search"
|
|
1645
1637
|
value={filter()}
|
|
@@ -1667,17 +1659,6 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1667
1659
|
</For>
|
|
1668
1660
|
</div>
|
|
1669
1661
|
</div>
|
|
1670
|
-
<div class="ph-meta">
|
|
1671
|
-
<span>{SOURCE_LABEL[meta()?.source ?? ""] ?? "加载中…"}</span>
|
|
1672
|
-
<span class="ph-dot" />
|
|
1673
|
-
<span>
|
|
1674
|
-
{catalog().length} 个条目 · {installable()} 个可安装
|
|
1675
|
-
</span>
|
|
1676
|
-
<Show when={meta()?.fetchedAt}>
|
|
1677
|
-
<span class="ph-dot" />
|
|
1678
|
-
<span>更新于 {formatDate(meta()!.fetchedAt)}</span>
|
|
1679
|
-
</Show>
|
|
1680
|
-
</div>
|
|
1681
1662
|
</div>
|
|
1682
1663
|
|
|
1683
1664
|
<Show when={isBusy("catalog") && catalog().length === 0}>
|
|
@@ -1824,7 +1805,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1824
1805
|
<div class="ph-empty">
|
|
1825
1806
|
<ui.Icon name="extensions" size="large" />
|
|
1826
1807
|
<strong>还没有安装插件</strong>
|
|
1827
|
-
<span
|
|
1808
|
+
<span>去「精选目录」看看 awesome-opencode 的推荐,或者在「npm 搜索」里找长尾插件。</span>
|
|
1828
1809
|
</div>
|
|
1829
1810
|
}
|
|
1830
1811
|
>
|
|
@@ -1838,7 +1819,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1838
1819
|
|
|
1839
1820
|
<footer class="ph-footer">
|
|
1840
1821
|
<div class="ph-footer-note">
|
|
1841
|
-
|
|
1822
|
+
安装和移除只改配置,不会立即生效:插件与模型目录都是启动时快照的。改完点上方工具栏的「彻底重启」,应用会自动退出并重新打开。
|
|
1842
1823
|
</div>
|
|
1843
1824
|
</footer>
|
|
1844
1825
|
</div>
|
|
@@ -1848,11 +1829,24 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1848
1829
|
/** The standalone x-large dialog opened from the titlebar pill and the ⌘⌥P shortcut. */
|
|
1849
1830
|
export function PluginHubDialog(props: { ctx: Context; admin: AdminClient; onClose: () => void }) {
|
|
1850
1831
|
const ui = props.ctx.ui
|
|
1832
|
+
const openSettings = () => {
|
|
1833
|
+
props.onClose()
|
|
1834
|
+
// Let the dialog unmount before the host swaps in the Settings view.
|
|
1835
|
+
setTimeout(() => props.ctx.layout.settings("hub"), 0)
|
|
1836
|
+
}
|
|
1851
1837
|
return (
|
|
1852
1838
|
<ui.Dialog
|
|
1853
1839
|
title="插件中心"
|
|
1854
1840
|
size="x-large"
|
|
1855
|
-
footer={
|
|
1841
|
+
footer={
|
|
1842
|
+
<div class="ph-dialog-footer">
|
|
1843
|
+
<span class="ph-footer-note">拖动右下角可调整大小</span>
|
|
1844
|
+
<ui.Button size="small" variant="ghost" icon="settings-gear" onClick={openSettings}>
|
|
1845
|
+
在设置中打开
|
|
1846
|
+
</ui.Button>
|
|
1847
|
+
<ui.Button size="small" onClick={props.onClose}>关闭</ui.Button>
|
|
1848
|
+
</div>
|
|
1849
|
+
}
|
|
1856
1850
|
>
|
|
1857
1851
|
<PluginHubPage ctx={props.ctx} admin={props.admin} embedded />
|
|
1858
1852
|
</ui.Dialog>
|