opencode-plugin-hub 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dist/gui-experimental.cjs +125 -33
- package/package.json +1 -1
- package/src/contract.ts +2 -0
- package/src/gui/catalog.ts +25 -7
- package/src/gui/page.tsx +348 -202
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)再打开一次。"
|
|
@@ -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;
|
|
@@ -346,12 +348,18 @@ const CSS = `
|
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
/* cards */
|
|
349
|
-
.ph-list {
|
|
351
|
+
.ph-list {
|
|
352
|
+
display: grid;
|
|
353
|
+
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
|
354
|
+
gap: 10px;
|
|
355
|
+
align-items: stretch;
|
|
356
|
+
}
|
|
350
357
|
.ph-card {
|
|
358
|
+
position: relative;
|
|
351
359
|
display: flex;
|
|
352
|
-
|
|
353
|
-
gap:
|
|
354
|
-
padding:
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
gap: 9px;
|
|
362
|
+
padding: 12px;
|
|
355
363
|
border: 1px solid var(--ph-border);
|
|
356
364
|
border-radius: 12px;
|
|
357
365
|
background: var(--ph-layer);
|
|
@@ -362,22 +370,59 @@ const CSS = `
|
|
|
362
370
|
background: var(--ph-layer-2);
|
|
363
371
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.05);
|
|
364
372
|
}
|
|
373
|
+
/* Highlight cards whose plugin has a pending update. */
|
|
374
|
+
.ph-card[data-stale="true"] {
|
|
375
|
+
border-color: color-mix(in srgb, var(--ph-accent) 36%, transparent);
|
|
376
|
+
background: color-mix(in srgb, var(--ph-accent) 5%, transparent);
|
|
377
|
+
}
|
|
378
|
+
/* While an install or update runs, pulse and sweep the card. */
|
|
379
|
+
.ph-card[data-busy="true"] {
|
|
380
|
+
border-color: color-mix(in srgb, var(--ph-accent) 45%, transparent);
|
|
381
|
+
animation: ph-busy-pulse 1.1s ease-in-out infinite;
|
|
382
|
+
}
|
|
383
|
+
.ph-card[data-busy="true"]::after {
|
|
384
|
+
content: "";
|
|
385
|
+
position: absolute;
|
|
386
|
+
inset: 0;
|
|
387
|
+
border-radius: inherit;
|
|
388
|
+
background: linear-gradient(
|
|
389
|
+
100deg,
|
|
390
|
+
transparent 32%,
|
|
391
|
+
color-mix(in srgb, var(--ph-accent) 12%, transparent) 50%,
|
|
392
|
+
transparent 68%
|
|
393
|
+
);
|
|
394
|
+
background-size: 220% 100%;
|
|
395
|
+
animation: ph-shimmer 1.1s linear infinite;
|
|
396
|
+
pointer-events: none;
|
|
397
|
+
}
|
|
398
|
+
@keyframes ph-busy-pulse {
|
|
399
|
+
0%,
|
|
400
|
+
100% {
|
|
401
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--ph-accent) 20%, transparent);
|
|
402
|
+
}
|
|
403
|
+
50% {
|
|
404
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--ph-accent) 8%, transparent);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
.ph-card-main { display: flex; flex: 1; flex-direction: column; gap: 8px; min-width: 0; }
|
|
408
|
+
.ph-card-body { display: flex; flex: 1; flex-direction: column; gap: 5px; min-width: 0; }
|
|
409
|
+
.ph-card-body[role="button"] { cursor: pointer; border-radius: 6px; }
|
|
410
|
+
.ph-card-head { display: flex; align-items: flex-start; gap: 9px; }
|
|
411
|
+
.ph-card-titles { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; }
|
|
365
412
|
.ph-tile {
|
|
366
413
|
flex: 0 0 auto;
|
|
367
414
|
display: grid;
|
|
368
415
|
place-items: center;
|
|
369
|
-
width:
|
|
370
|
-
height:
|
|
416
|
+
width: 32px;
|
|
417
|
+
height: 32px;
|
|
371
418
|
border: 1px solid color-mix(in srgb, var(--ph-accent) 20%, transparent);
|
|
372
|
-
border-radius:
|
|
419
|
+
border-radius: 9px;
|
|
373
420
|
background: color-mix(in srgb, var(--ph-accent) 10%, transparent);
|
|
374
421
|
color: var(--ph-accent);
|
|
375
|
-
font-size:
|
|
422
|
+
font-size: 13px;
|
|
376
423
|
font-weight: 700;
|
|
377
424
|
overflow: hidden;
|
|
378
425
|
}
|
|
379
|
-
.ph-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
380
|
-
.ph-card-body[role="button"] { cursor: pointer; border-radius: 6px; }
|
|
381
426
|
.ph-name-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
|
382
427
|
.ph-name {
|
|
383
428
|
font-size: 13.5px;
|
|
@@ -407,7 +452,16 @@ const CSS = `
|
|
|
407
452
|
text-transform: uppercase;
|
|
408
453
|
color: var(--ph-faint);
|
|
409
454
|
}
|
|
410
|
-
.ph-actions {
|
|
455
|
+
.ph-actions {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
justify-content: flex-end;
|
|
459
|
+
gap: 4px;
|
|
460
|
+
flex-wrap: wrap;
|
|
461
|
+
margin-top: auto;
|
|
462
|
+
padding-top: 8px;
|
|
463
|
+
border-top: 1px solid var(--ph-border);
|
|
464
|
+
}
|
|
411
465
|
|
|
412
466
|
/* badges and chips */
|
|
413
467
|
.ph-badge {
|
|
@@ -469,6 +523,7 @@ button.ph-chip[data-active="true"] {
|
|
|
469
523
|
border-color: color-mix(in srgb, var(--ph-accent) 32%, transparent);
|
|
470
524
|
background: color-mix(in srgb, var(--ph-accent) 10%, transparent);
|
|
471
525
|
}
|
|
526
|
+
.ph-chip[data-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
472
527
|
.ph-chip-count { font-size: 9px; opacity: 0.7; }
|
|
473
528
|
|
|
474
529
|
/* empty and loading */
|
|
@@ -595,10 +650,36 @@ button.ph-chip:focus-visible,
|
|
|
595
650
|
max-width: none;
|
|
596
651
|
padding: 0 0 8px;
|
|
597
652
|
}
|
|
598
|
-
|
|
653
|
+
/* The host caps "x-large" at 980x600. Give the standalone hub more room and
|
|
654
|
+
let the dialog be resized from its bottom-right corner, within the window. */
|
|
655
|
+
[data-component="dialog-v2"][data-size="x-large"]:has(.ph-root[data-embedded="true"]) [data-slot="dialog-container"] {
|
|
656
|
+
width: min(100vw - 48px, 1180px);
|
|
657
|
+
height: min(100vh - 88px, 780px);
|
|
658
|
+
min-width: min(100vw - 48px, 720px);
|
|
659
|
+
min-height: min(100vh - 88px, 480px);
|
|
660
|
+
max-width: calc(100vw - 24px);
|
|
661
|
+
max-height: calc(100vh - 24px);
|
|
662
|
+
resize: both;
|
|
663
|
+
}
|
|
664
|
+
[data-component="dialog-v2"][data-size="x-large"]:has(.ph-detail) [data-slot="dialog-container"] {
|
|
665
|
+
width: min(100vw - 48px, 1180px);
|
|
666
|
+
height: min(100vh - 88px, 780px);
|
|
667
|
+
min-width: min(100vw - 48px, 720px);
|
|
668
|
+
min-height: min(100vh - 88px, 480px);
|
|
669
|
+
max-width: calc(100vw - 24px);
|
|
670
|
+
max-height: calc(100vh - 24px);
|
|
671
|
+
resize: both;
|
|
672
|
+
}
|
|
673
|
+
/* Notices and the toolbar share one sticky head, so a note raised after an
|
|
674
|
+
action (e.g. updating a plugin near the bottom of the list) is always on
|
|
675
|
+
screen instead of appearing far above the current scroll position. */
|
|
676
|
+
.ph-head {
|
|
599
677
|
position: sticky;
|
|
600
678
|
top: 0;
|
|
601
679
|
z-index: 3;
|
|
680
|
+
display: flex;
|
|
681
|
+
flex-direction: column;
|
|
682
|
+
gap: 14px;
|
|
602
683
|
padding: 6px 0;
|
|
603
684
|
margin: -6px 0;
|
|
604
685
|
background: color-mix(in srgb, var(--color-v2-background-bg-base, Canvas) 92%, transparent);
|
|
@@ -617,9 +698,7 @@ button.ph-chip:focus-visible,
|
|
|
617
698
|
|
|
618
699
|
@media (max-width: 760px) {
|
|
619
700
|
.ph-hero { flex-direction: column; }
|
|
620
|
-
.ph-
|
|
621
|
-
.ph-card-body { flex-basis: calc(100% - 50px); }
|
|
622
|
-
.ph-actions { width: 100%; justify-content: flex-start; padding-left: 50px; }
|
|
701
|
+
.ph-list { grid-template-columns: 1fr; }
|
|
623
702
|
.ph-detail-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
624
703
|
}
|
|
625
704
|
`
|
|
@@ -885,6 +964,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
885
964
|
|
|
886
965
|
const [updates, setUpdates] = createSignal<Map<string, UpdateInfo>>(new Map())
|
|
887
966
|
const [checked, setChecked] = createSignal(false)
|
|
967
|
+
const [staleOnly, setStaleOnly] = createSignal(false)
|
|
888
968
|
|
|
889
969
|
const [busyKeys, setBusyKeys] = createSignal<ReadonlySet<string>>(new Set())
|
|
890
970
|
const [note, setNote] = createSignal<Note>()
|
|
@@ -987,7 +1067,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
987
1067
|
notify(result.message, "error")
|
|
988
1068
|
return
|
|
989
1069
|
}
|
|
990
|
-
notify(`${result.message}
|
|
1070
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
991
1071
|
const latest = version || hits().find((hit) => hit.name === name)?.version || ""
|
|
992
1072
|
setCatalog((prev) => prev.map((entry) => (entry.pkg === name ? { ...entry, installed: true } : entry)))
|
|
993
1073
|
setHits((prev) =>
|
|
@@ -1018,7 +1098,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1018
1098
|
notify(result.message, "error")
|
|
1019
1099
|
return
|
|
1020
1100
|
}
|
|
1021
|
-
notify(`${result.message}
|
|
1101
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
1022
1102
|
setCatalog((prev) =>
|
|
1023
1103
|
prev.map((item) => (item.pkg === entry.name ? { ...item, installed: false } : item)),
|
|
1024
1104
|
)
|
|
@@ -1145,8 +1225,13 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1145
1225
|
return list
|
|
1146
1226
|
})
|
|
1147
1227
|
|
|
1148
|
-
const
|
|
1228
|
+
const registryUrl = () => meta()?.registry || CATALOG_SOURCE_FALLBACK
|
|
1149
1229
|
const outdated = createMemo(() => [...updates().values()].filter((item) => item.hasUpdate).length)
|
|
1230
|
+
const visiblePlugins = createMemo(() => {
|
|
1231
|
+
const plugins = state()?.plugins ?? []
|
|
1232
|
+
if (!staleOnly()) return plugins
|
|
1233
|
+
return plugins.filter((entry) => updates().get(entry.spec)?.hasUpdate === true)
|
|
1234
|
+
})
|
|
1150
1235
|
const bySpec = (spec: string) => updates().get(spec)
|
|
1151
1236
|
const byPackage = createMemo(() => {
|
|
1152
1237
|
const map = new Map<string, UpdateInfo>()
|
|
@@ -1156,6 +1241,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1156
1241
|
|
|
1157
1242
|
const switchTab = (next: Tab) => {
|
|
1158
1243
|
setTab(next)
|
|
1244
|
+
// Leaving the installed view resets its "updatable only" filter.
|
|
1245
|
+
if (next !== "installed") setStaleOnly(false)
|
|
1159
1246
|
// The first look at the installed list also checks the registry, once.
|
|
1160
1247
|
if (next !== "catalog" && !checked() && !isBusy("updates")) void checkUpdates(true)
|
|
1161
1248
|
}
|
|
@@ -1193,6 +1280,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1193
1280
|
const catalogRow = (entry: CatalogEntry) => {
|
|
1194
1281
|
const info = () => byPackage().get(entry.pkg)
|
|
1195
1282
|
const stale = () => info()?.hasUpdate === true
|
|
1283
|
+
const busy = () => isBusy(`install:${entry.pkg}`)
|
|
1196
1284
|
const detail = () =>
|
|
1197
1285
|
openDetail(entry.pkg, {
|
|
1198
1286
|
version: entry.version,
|
|
@@ -1200,54 +1288,62 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1200
1288
|
repo: entry.repo,
|
|
1201
1289
|
})
|
|
1202
1290
|
return (
|
|
1203
|
-
<article class="ph-card">
|
|
1204
|
-
<div class="ph-
|
|
1205
|
-
<
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
<
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
</Show>
|
|
1230
|
-
<Show when={stale()}>
|
|
1231
|
-
<span class="ph-badge" data-tone="accent">
|
|
1232
|
-
可更新 → v{info()!.latest}
|
|
1233
|
-
</span>
|
|
1234
|
-
</Show>
|
|
1291
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1292
|
+
<div class="ph-card-main">
|
|
1293
|
+
<div class="ph-card-head">
|
|
1294
|
+
<div class="ph-tile">
|
|
1295
|
+
<ui.Icon name={typeIcon(entry.type)} size="normal" />
|
|
1296
|
+
</div>
|
|
1297
|
+
<div class="ph-card-titles">
|
|
1298
|
+
<div class="ph-name-row">
|
|
1299
|
+
<span class="ph-name">{entry.name}</span>
|
|
1300
|
+
</div>
|
|
1301
|
+
<Show when={entry.pkg}>
|
|
1302
|
+
<span class="ph-pkg">{entry.pkg}</span>
|
|
1303
|
+
</Show>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div class="ph-chips">
|
|
1306
|
+
<Show when={entry.installed}>
|
|
1307
|
+
<span class="ph-badge" data-tone="success">
|
|
1308
|
+
已安装
|
|
1309
|
+
</span>
|
|
1310
|
+
</Show>
|
|
1311
|
+
<Show when={stale()}>
|
|
1312
|
+
<span class="ph-badge" data-tone="accent">
|
|
1313
|
+
可更新 → v{info()!.latest}
|
|
1314
|
+
</span>
|
|
1315
|
+
</Show>
|
|
1316
|
+
</div>
|
|
1235
1317
|
</div>
|
|
1236
|
-
<div
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1318
|
+
<div
|
|
1319
|
+
class="ph-card-body"
|
|
1320
|
+
role="button"
|
|
1321
|
+
tabindex="0"
|
|
1322
|
+
onClick={() => (entry.pkg ? detail() : void openUrl(entry.repo))}
|
|
1323
|
+
onKeyDown={(event) => {
|
|
1324
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1325
|
+
event.preventDefault()
|
|
1326
|
+
if (entry.pkg) detail()
|
|
1327
|
+
else void openUrl(entry.repo)
|
|
1328
|
+
}
|
|
1329
|
+
}}
|
|
1330
|
+
>
|
|
1331
|
+
<div class="ph-desc ph-clamp">{entry.desc || entry.long || "—"}</div>
|
|
1332
|
+
<div class="ph-meta">
|
|
1333
|
+
<span class="ph-type">{TYPE_LABEL[entry.type] ?? entry.type}</span>
|
|
1334
|
+
<Show when={entry.version}>
|
|
1335
|
+
<span class="ph-dot" />
|
|
1336
|
+
<span>v{entry.version}</span>
|
|
1337
|
+
</Show>
|
|
1338
|
+
<Show when={!entry.pkg}>
|
|
1339
|
+
<span class="ph-dot" />
|
|
1340
|
+
<span>仅 GitHub 发布</span>
|
|
1341
|
+
</Show>
|
|
1342
|
+
<Show when={entry.tags.length > 0}>
|
|
1343
|
+
<span class="ph-dot" />
|
|
1344
|
+
<span>{entry.tags.slice(0, 4).join(" / ")}</span>
|
|
1345
|
+
</Show>
|
|
1346
|
+
</div>
|
|
1251
1347
|
</div>
|
|
1252
1348
|
</div>
|
|
1253
1349
|
<div class="ph-actions">
|
|
@@ -1269,7 +1365,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1269
1365
|
variant="ghost"
|
|
1270
1366
|
size="small"
|
|
1271
1367
|
icon="download"
|
|
1272
|
-
busy={
|
|
1368
|
+
busy={busy()}
|
|
1273
1369
|
onClick={() => void install(entry.pkg, info()!.latest)}
|
|
1274
1370
|
>
|
|
1275
1371
|
更新
|
|
@@ -1280,7 +1376,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1280
1376
|
variant="neutral"
|
|
1281
1377
|
size="small"
|
|
1282
1378
|
icon="download"
|
|
1283
|
-
busy={
|
|
1379
|
+
busy={busy()}
|
|
1284
1380
|
onClick={() => void install(entry.pkg)}
|
|
1285
1381
|
>
|
|
1286
1382
|
安装
|
|
@@ -1304,71 +1400,70 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1304
1400
|
npm: hit.npm,
|
|
1305
1401
|
})
|
|
1306
1402
|
return (
|
|
1307
|
-
<article class="ph-card">
|
|
1308
|
-
<div class="ph-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
</
|
|
1328
|
-
</
|
|
1329
|
-
<Show when={hit.hasUpdate}>
|
|
1330
|
-
<span class="ph-badge" data-tone="accent">
|
|
1331
|
-
可更新 → v{hit.version}
|
|
1332
|
-
</span>
|
|
1333
|
-
</Show>
|
|
1403
|
+
<article class="ph-card" data-busy={isBusy(`install:${hit.name}`) ? "true" : undefined}>
|
|
1404
|
+
<div class="ph-card-main">
|
|
1405
|
+
<div class="ph-card-head">
|
|
1406
|
+
<div class="ph-tile">{initial(hit.name)}</div>
|
|
1407
|
+
<div class="ph-card-titles">
|
|
1408
|
+
<div class="ph-name-row">
|
|
1409
|
+
<span class="ph-name">{hit.name}</span>
|
|
1410
|
+
</div>
|
|
1411
|
+
<span class="ph-pkg">v{hit.version}</span>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div class="ph-chips">
|
|
1414
|
+
<Show when={hit.installed}>
|
|
1415
|
+
<span class="ph-badge" data-tone="success">
|
|
1416
|
+
已安装{hit.current ? ` v${hit.current}` : ""}
|
|
1417
|
+
</span>
|
|
1418
|
+
</Show>
|
|
1419
|
+
<Show when={hit.hasUpdate}>
|
|
1420
|
+
<span class="ph-badge" data-tone="accent">
|
|
1421
|
+
可更新 → v{hit.version}
|
|
1422
|
+
</span>
|
|
1423
|
+
</Show>
|
|
1424
|
+
</div>
|
|
1334
1425
|
</div>
|
|
1335
|
-
<div
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
<
|
|
1348
|
-
|
|
1349
|
-
<span
|
|
1350
|
-
|
|
1351
|
-
|
|
1426
|
+
<div
|
|
1427
|
+
class="ph-card-body"
|
|
1428
|
+
role="button"
|
|
1429
|
+
tabindex="0"
|
|
1430
|
+
onClick={detail}
|
|
1431
|
+
onKeyDown={(event) => {
|
|
1432
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1433
|
+
event.preventDefault()
|
|
1434
|
+
detail()
|
|
1435
|
+
}
|
|
1436
|
+
}}
|
|
1437
|
+
>
|
|
1438
|
+
<div class="ph-desc ph-clamp">{hit.description || "—"}</div>
|
|
1439
|
+
<div class="ph-meta">
|
|
1440
|
+
<span title={`周下载 ${hit.weekly.toLocaleString()} 次`}>
|
|
1441
|
+
<ui.Icon name="download" size="small" />
|
|
1442
|
+
{formatCount(hit.weekly)}/周
|
|
1443
|
+
</span>
|
|
1352
1444
|
<span class="ph-dot" />
|
|
1353
|
-
<span>{
|
|
1445
|
+
<span title={`月下载 ${hit.monthly.toLocaleString()} 次`}>{formatCount(hit.monthly)}/月</span>
|
|
1446
|
+
<Show when={hit.publisher}>
|
|
1447
|
+
<span class="ph-dot" />
|
|
1448
|
+
<span>by {hit.publisher}</span>
|
|
1449
|
+
</Show>
|
|
1450
|
+
<Show when={hit.published}>
|
|
1451
|
+
<span class="ph-dot" />
|
|
1452
|
+
<span>{relativeDate(hit.published)}更新</span>
|
|
1453
|
+
</Show>
|
|
1454
|
+
</div>
|
|
1455
|
+
<Show when={hit.keywords.length > 0}>
|
|
1456
|
+
<div class="ph-chips">
|
|
1457
|
+
<For each={hit.keywords.slice(0, 3)}>{(keyword) => <span class="ph-chip">{keyword}</span>}</For>
|
|
1458
|
+
</div>
|
|
1354
1459
|
</Show>
|
|
1355
1460
|
</div>
|
|
1356
|
-
<Show when={hit.keywords.length > 0}>
|
|
1357
|
-
<div class="ph-chips">
|
|
1358
|
-
<For each={hit.keywords}>{(keyword) => <span class="ph-chip">{keyword}</span>}</For>
|
|
1359
|
-
</div>
|
|
1360
|
-
</Show>
|
|
1361
1461
|
</div>
|
|
1362
1462
|
<div class="ph-actions">
|
|
1363
1463
|
<Show when={hit.homepage}>
|
|
1364
|
-
<ui.
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
icon="globe"
|
|
1368
|
-
onClick={() => void openUrl(hit.homepage)}
|
|
1369
|
-
>
|
|
1370
|
-
主页
|
|
1371
|
-
</ui.Button>
|
|
1464
|
+
<ui.Tooltip value="主页">
|
|
1465
|
+
<ui.IconButton icon="globe" size="small" label="主页" onClick={() => void openUrl(hit.homepage)} />
|
|
1466
|
+
</ui.Tooltip>
|
|
1372
1467
|
</Show>
|
|
1373
1468
|
<Show when={hit.repo}>
|
|
1374
1469
|
<ui.Tooltip value="源码仓库">
|
|
@@ -1413,35 +1508,44 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1413
1508
|
const installedRow = (entry: PluginEntry) => {
|
|
1414
1509
|
const info = () => bySpec(entry.spec)
|
|
1415
1510
|
const stale = () => info()?.hasUpdate === true
|
|
1511
|
+
const busy = () => isBusy(`install:${entry.name}`)
|
|
1416
1512
|
const current = () => info()?.current || entry.version
|
|
1417
1513
|
const unchecked = () => (info()?.error ?? "") !== ""
|
|
1418
1514
|
return (
|
|
1419
|
-
<article class="ph-card">
|
|
1420
|
-
<div class="ph-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
<
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1515
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1516
|
+
<div class="ph-card-main">
|
|
1517
|
+
<div class="ph-card-head">
|
|
1518
|
+
<div class="ph-tile">{initial(entry.name)}</div>
|
|
1519
|
+
<div class="ph-card-titles">
|
|
1520
|
+
<div class="ph-name-row">
|
|
1521
|
+
<span class="ph-name">{entry.name}</span>
|
|
1522
|
+
</div>
|
|
1523
|
+
<span class="ph-pkg">{entry.spec}</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
<div class="ph-chips">
|
|
1526
|
+
<Show when={stale()}>
|
|
1527
|
+
<span class="ph-badge" data-tone="accent">
|
|
1528
|
+
可更新 → v{info()!.latest}
|
|
1529
|
+
</span>
|
|
1530
|
+
</Show>
|
|
1531
|
+
<Show when={unchecked()}>
|
|
1532
|
+
<span class="ph-badge" data-tone="warning">
|
|
1533
|
+
未能检查
|
|
1534
|
+
</span>
|
|
1535
|
+
</Show>
|
|
1536
|
+
<Show when={entry.kind === "path"}>
|
|
1537
|
+
<span class="ph-badge">本地目录</span>
|
|
1538
|
+
</Show>
|
|
1539
|
+
</div>
|
|
1438
1540
|
</div>
|
|
1439
|
-
<div class="ph-
|
|
1440
|
-
<
|
|
1441
|
-
|
|
1442
|
-
<
|
|
1443
|
-
|
|
1444
|
-
|
|
1541
|
+
<div class="ph-card-body">
|
|
1542
|
+
<div class="ph-meta">
|
|
1543
|
+
{versionLabel(current(), info()?.latest ?? "", stale())}
|
|
1544
|
+
<Show when={stale() && entry.kind === "path"}>
|
|
1545
|
+
<span class="ph-dot" />
|
|
1546
|
+
<span>更新会覆盖目录内容,原文件自动备份</span>
|
|
1547
|
+
</Show>
|
|
1548
|
+
</div>
|
|
1445
1549
|
</div>
|
|
1446
1550
|
</div>
|
|
1447
1551
|
<div class="ph-actions">
|
|
@@ -1460,7 +1564,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1460
1564
|
variant="ghost"
|
|
1461
1565
|
size="small"
|
|
1462
1566
|
icon="download"
|
|
1463
|
-
busy={
|
|
1567
|
+
busy={busy()}
|
|
1464
1568
|
onClick={() => void install(info()!.name, info()!.latest)}
|
|
1465
1569
|
>
|
|
1466
1570
|
更新
|
|
@@ -1471,7 +1575,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1471
1575
|
variant="ghost-muted"
|
|
1472
1576
|
size="small"
|
|
1473
1577
|
icon="trash"
|
|
1474
|
-
busy={
|
|
1578
|
+
busy={busy()}
|
|
1475
1579
|
onClick={() => void uninstall(entry)}
|
|
1476
1580
|
>
|
|
1477
1581
|
移除
|
|
@@ -1496,7 +1600,16 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1496
1600
|
<h2 class="ph-title">插件中心</h2>
|
|
1497
1601
|
</Show>
|
|
1498
1602
|
<p class="ph-sub">
|
|
1499
|
-
|
|
1603
|
+
浏览{" "}
|
|
1604
|
+
<button
|
|
1605
|
+
type="button"
|
|
1606
|
+
class="ph-inline-source"
|
|
1607
|
+
title={CATALOG_SOURCE_TITLE}
|
|
1608
|
+
onClick={() => void openUrl(registryUrl())}
|
|
1609
|
+
>
|
|
1610
|
+
awesome-opencode
|
|
1611
|
+
</button>{" "}
|
|
1612
|
+
社区维护的精选目录、搜索 npm 上的 OpenCode 插件。安装前先看下载量、README 和主页,确认它到底能做什么。
|
|
1500
1613
|
</p>
|
|
1501
1614
|
<div class="ph-path" title={state()?.configPath}>
|
|
1502
1615
|
配置:<code>{state()?.configPath ?? "读取中…"}</code> · <code>{state()?.configKey ?? "plugins"}[]</code>
|
|
@@ -1504,21 +1617,22 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1504
1617
|
</div>
|
|
1505
1618
|
</section>
|
|
1506
1619
|
|
|
1507
|
-
<
|
|
1508
|
-
<
|
|
1509
|
-
<
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1620
|
+
<div class="ph-head">
|
|
1621
|
+
<Show when={note()}>
|
|
1622
|
+
<div class="ph-notice" data-tone={note()!.tone}>
|
|
1623
|
+
<ui.Icon name={noteIcon(note()!.tone)} size="small" />
|
|
1624
|
+
<span>{note()!.text}</span>
|
|
1625
|
+
</div>
|
|
1626
|
+
</Show>
|
|
1513
1627
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1628
|
+
<Show when={state()?.error}>
|
|
1629
|
+
<div class="ph-notice" data-tone="error">
|
|
1630
|
+
<ui.Icon name="circle-exclamation" size="small" />
|
|
1631
|
+
<span>{state()!.error}</span>
|
|
1632
|
+
</div>
|
|
1633
|
+
</Show>
|
|
1520
1634
|
|
|
1521
|
-
|
|
1635
|
+
<div class="ph-toolbar">
|
|
1522
1636
|
<div class="ph-tabs" role="tablist">
|
|
1523
1637
|
{tabButton("catalog", "精选目录", "extensions", meta() ? catalog().length : undefined)}
|
|
1524
1638
|
{tabButton("search", "npm 搜索", "magnifying-glass", searched() ? hits().length : undefined)}
|
|
@@ -1590,7 +1704,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1590
1704
|
>
|
|
1591
1705
|
检查更新
|
|
1592
1706
|
</ui.Button>
|
|
1593
|
-
|
|
1707
|
+
</Show>
|
|
1708
|
+
</div>
|
|
1594
1709
|
</div>
|
|
1595
1710
|
|
|
1596
1711
|
<Switch>
|
|
@@ -1600,7 +1715,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1600
1715
|
<div class="ph-filter-row">
|
|
1601
1716
|
<div class="ph-grow">
|
|
1602
1717
|
<ui.TextInput
|
|
1603
|
-
label="
|
|
1718
|
+
label="筛选 awesome-opencode 精选插件"
|
|
1604
1719
|
placeholder="按名称、描述或标签筛选,例如 memory / auth / token"
|
|
1605
1720
|
type="search"
|
|
1606
1721
|
value={filter()}
|
|
@@ -1628,17 +1743,6 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1628
1743
|
</For>
|
|
1629
1744
|
</div>
|
|
1630
1745
|
</div>
|
|
1631
|
-
<div class="ph-meta">
|
|
1632
|
-
<span>{SOURCE_LABEL[meta()?.source ?? ""] ?? "加载中…"}</span>
|
|
1633
|
-
<span class="ph-dot" />
|
|
1634
|
-
<span>
|
|
1635
|
-
{catalog().length} 个条目 · {installable()} 个可安装
|
|
1636
|
-
</span>
|
|
1637
|
-
<Show when={meta()?.fetchedAt}>
|
|
1638
|
-
<span class="ph-dot" />
|
|
1639
|
-
<span>更新于 {formatDate(meta()!.fetchedAt)}</span>
|
|
1640
|
-
</Show>
|
|
1641
|
-
</div>
|
|
1642
1746
|
</div>
|
|
1643
1747
|
|
|
1644
1748
|
<Show when={isBusy("catalog") && catalog().length === 0}>
|
|
@@ -1773,6 +1877,21 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1773
1877
|
<h3 class="ph-h3">
|
|
1774
1878
|
已安装 <span class="ph-count">{state()?.plugins.length ?? 0}</span>
|
|
1775
1879
|
</h3>
|
|
1880
|
+
<div class="ph-chips">
|
|
1881
|
+
<button
|
|
1882
|
+
class="ph-chip"
|
|
1883
|
+
data-active={staleOnly()}
|
|
1884
|
+
data-disabled={outdated() === 0 ? "true" : undefined}
|
|
1885
|
+
title={outdated() === 0 ? "检查更新后再来看可更新的插件" : "只显示有新版本的插件"}
|
|
1886
|
+
onClick={() => {
|
|
1887
|
+
if (outdated() > 0) setStaleOnly(!staleOnly())
|
|
1888
|
+
}}
|
|
1889
|
+
>
|
|
1890
|
+
可更新
|
|
1891
|
+
<span class="ph-chip-count">{outdated()}</span>
|
|
1892
|
+
</button>
|
|
1893
|
+
</div>
|
|
1894
|
+
<span class="ph-spacer" />
|
|
1776
1895
|
<Show when={checked()}>
|
|
1777
1896
|
<span class="ph-meta">
|
|
1778
1897
|
检查过 npm registry{outdated() > 0 ? ` · ${outdated()} 个可更新` : " · 全部已是最新"}
|
|
@@ -1785,13 +1904,27 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1785
1904
|
<div class="ph-empty">
|
|
1786
1905
|
<ui.Icon name="extensions" size="large" />
|
|
1787
1906
|
<strong>还没有安装插件</strong>
|
|
1788
|
-
<span
|
|
1907
|
+
<span>去「精选目录」看看 awesome-opencode 的推荐,或者在「npm 搜索」里找长尾插件。</span>
|
|
1789
1908
|
</div>
|
|
1790
1909
|
}
|
|
1791
1910
|
>
|
|
1792
|
-
<
|
|
1793
|
-
|
|
1794
|
-
|
|
1911
|
+
<Show
|
|
1912
|
+
when={visiblePlugins().length > 0}
|
|
1913
|
+
fallback={
|
|
1914
|
+
<div class="ph-empty">
|
|
1915
|
+
<ui.Icon name="checklist" size="large" />
|
|
1916
|
+
<strong>没有可更新的插件</strong>
|
|
1917
|
+
<span>已安装的插件都已是最新版本。点工具栏的「检查更新」可以再查一次。</span>
|
|
1918
|
+
<ui.Button variant="ghost" size="small" onClick={() => setStaleOnly(false)}>
|
|
1919
|
+
显示全部
|
|
1920
|
+
</ui.Button>
|
|
1921
|
+
</div>
|
|
1922
|
+
}
|
|
1923
|
+
>
|
|
1924
|
+
<div class="ph-list">
|
|
1925
|
+
<For each={visiblePlugins()}>{(entry) => installedRow(entry)}</For>
|
|
1926
|
+
</div>
|
|
1927
|
+
</Show>
|
|
1795
1928
|
</Show>
|
|
1796
1929
|
</section>
|
|
1797
1930
|
</Match>
|
|
@@ -1799,7 +1932,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1799
1932
|
|
|
1800
1933
|
<footer class="ph-footer">
|
|
1801
1934
|
<div class="ph-footer-note">
|
|
1802
|
-
|
|
1935
|
+
安装和移除只改配置,不会立即生效:插件与模型目录都是启动时快照的。改完点工具栏的「彻底重启」,应用会自动退出并重新打开。
|
|
1803
1936
|
</div>
|
|
1804
1937
|
</footer>
|
|
1805
1938
|
</div>
|
|
@@ -1809,11 +1942,24 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1809
1942
|
/** The standalone x-large dialog opened from the titlebar pill and the ⌘⌥P shortcut. */
|
|
1810
1943
|
export function PluginHubDialog(props: { ctx: Context; admin: AdminClient; onClose: () => void }) {
|
|
1811
1944
|
const ui = props.ctx.ui
|
|
1945
|
+
const openSettings = () => {
|
|
1946
|
+
props.onClose()
|
|
1947
|
+
// Let the dialog unmount before the host swaps in the Settings view.
|
|
1948
|
+
setTimeout(() => props.ctx.layout.settings("hub"), 0)
|
|
1949
|
+
}
|
|
1812
1950
|
return (
|
|
1813
1951
|
<ui.Dialog
|
|
1814
1952
|
title="插件中心"
|
|
1815
1953
|
size="x-large"
|
|
1816
|
-
footer={
|
|
1954
|
+
footer={
|
|
1955
|
+
<div class="ph-dialog-footer">
|
|
1956
|
+
<span class="ph-footer-note">拖动右下角可调整大小</span>
|
|
1957
|
+
<ui.Button size="small" variant="ghost" icon="settings-gear" onClick={openSettings}>
|
|
1958
|
+
在设置中打开
|
|
1959
|
+
</ui.Button>
|
|
1960
|
+
<ui.Button size="small" onClick={props.onClose}>关闭</ui.Button>
|
|
1961
|
+
</div>
|
|
1962
|
+
}
|
|
1817
1963
|
>
|
|
1818
1964
|
<PluginHubPage ctx={props.ctx} admin={props.admin} embedded />
|
|
1819
1965
|
</ui.Dialog>
|