@chatcode/cco-market 1.45.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operations model behind the market's activity panel: one record per
|
|
3
|
+
* mutating operation (install, update, uninstall), from the moment it is
|
|
4
|
+
* queued until the user clears it.
|
|
5
|
+
*
|
|
6
|
+
* It exists because operation state used to live on the card that started it,
|
|
7
|
+
* which disappears the moment the user paginates, searches, or switches tab —
|
|
8
|
+
* taking any decision attached to it, such as a loader-id clash, along with
|
|
9
|
+
* it. Records live here instead, keyed by their own id, so the card only has
|
|
10
|
+
* to ask "is anything running for this url".
|
|
11
|
+
*
|
|
12
|
+
* Every function is pure so the queue, the aggregate counts, and the
|
|
13
|
+
* three-way visual grouping can be tested without a host or a DOM.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** What a record does to the profile. */
|
|
17
|
+
export type OperationKind = 'install' | 'update' | 'uninstall'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lifecycle of one operation.
|
|
21
|
+
*
|
|
22
|
+
* `input` is not a failure: the host already reverted the change (a clash is
|
|
23
|
+
* detected after pnpm resolves, and the package is removed on the spot), so
|
|
24
|
+
* the profile is intact and the queue moves on. It means the operation cannot
|
|
25
|
+
* finish until the user picks an outcome.
|
|
26
|
+
*/
|
|
27
|
+
export type OperationState = 'queued' | 'running' | 'input' | 'done' | 'warned' | 'failed'
|
|
28
|
+
|
|
29
|
+
/** The installed plugins one candidate clashed with, one entry per owner. */
|
|
30
|
+
export interface ConflictGroup {
|
|
31
|
+
owner: string
|
|
32
|
+
ids: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OperationRecord {
|
|
36
|
+
/** Stable across re-renders; how actions address a record. */
|
|
37
|
+
id: string
|
|
38
|
+
kind: OperationKind
|
|
39
|
+
/** Package name, or the catalog name for an install. */
|
|
40
|
+
name: string
|
|
41
|
+
/** Catalog url, present for installs — what a card is keyed by. */
|
|
42
|
+
url?: string
|
|
43
|
+
state: OperationState
|
|
44
|
+
/** 0-100 while running; null when the host reports no total to divide by. */
|
|
45
|
+
percent?: number | null
|
|
46
|
+
/** The host's latest progress line, shown verbatim under the bar. */
|
|
47
|
+
detail?: string
|
|
48
|
+
/** Set with `input`: the installed plugins that hold the same entry ids. */
|
|
49
|
+
conflicts?: ConflictGroup[]
|
|
50
|
+
/** Set with `warned` or `failed`: one sentence naming what went wrong. */
|
|
51
|
+
reason?: string
|
|
52
|
+
/**
|
|
53
|
+
* Set with `failed` when pnpm refused to run a dependency's build scripts.
|
|
54
|
+
* The way out is one click, so it belongs on the record that reports the
|
|
55
|
+
* failure — the approval banner lives elsewhere on the page, and a reader
|
|
56
|
+
* looking at this row was told to press a button they could not see (#314).
|
|
57
|
+
*/
|
|
58
|
+
blockedBuilds?: string[]
|
|
59
|
+
/** Set with `done` when the change needs a page refresh to be visible. */
|
|
60
|
+
needsRefresh?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Visual grouping. Six states are what the code has to distinguish; three are
|
|
65
|
+
* what the panel shows, because a reader scanning icons and colors cannot
|
|
66
|
+
* hold six apart. The distinction inside a bucket is carried by the record's
|
|
67
|
+
* own status line, not by another color.
|
|
68
|
+
*/
|
|
69
|
+
export type OperationBucket = 'busy' | 'ok' | 'attention'
|
|
70
|
+
|
|
71
|
+
const BUCKETS: Record<OperationState, OperationBucket> = {
|
|
72
|
+
queued: 'busy',
|
|
73
|
+
running: 'busy',
|
|
74
|
+
input: 'attention',
|
|
75
|
+
failed: 'attention',
|
|
76
|
+
done: 'ok',
|
|
77
|
+
warned: 'ok',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Which of the three visual groups a state belongs to. */
|
|
81
|
+
export function bucketOf(state: OperationState): OperationBucket {
|
|
82
|
+
return BUCKETS[state]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Whether a record has stopped moving on its own. */
|
|
86
|
+
export function isSettled(record: OperationRecord): boolean {
|
|
87
|
+
return record.state === 'done' || record.state === 'warned' || record.state === 'failed'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether a record is waiting on the user rather than on the host. */
|
|
91
|
+
export function needsUser(record: OperationRecord): boolean {
|
|
92
|
+
return record.state === 'input'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Append a record. Ids are supplied by the caller so tests stay deterministic. */
|
|
96
|
+
export function enqueue(list: readonly OperationRecord[], record: OperationRecord): OperationRecord[] {
|
|
97
|
+
return [...list, record]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Apply changes to one record.
|
|
102
|
+
* @returns a new list; unchanged (same contents) when no record matches.
|
|
103
|
+
*/
|
|
104
|
+
export function patch(
|
|
105
|
+
list: readonly OperationRecord[], id: string, changes: Partial<Omit<OperationRecord, 'id'>>,
|
|
106
|
+
): OperationRecord[] {
|
|
107
|
+
return list.map(record => (record.id === id ? { ...record, ...changes } : record))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Drop one record outright — the panel's per-row dismiss. */
|
|
111
|
+
export function drop(list: readonly OperationRecord[], id: string): OperationRecord[] {
|
|
112
|
+
return list.filter(record => record.id !== id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear finished records, keeping anything still moving or still waiting on
|
|
117
|
+
* the user. A record in `input` survives: the user has not answered it yet,
|
|
118
|
+
* and clearing it would delete the only route back to that decision.
|
|
119
|
+
*/
|
|
120
|
+
export function clearSettled(list: readonly OperationRecord[]): OperationRecord[] {
|
|
121
|
+
return list.filter(record => !isSettled(record))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How many queued records sit ahead of this one. The panel shows it because
|
|
126
|
+
* "queued" without a position reads as stuck; the host runs one mutation at a
|
|
127
|
+
* time, so the order shown is the order that will run.
|
|
128
|
+
* @returns the count ahead, or null when the record is not queued.
|
|
129
|
+
*/
|
|
130
|
+
export function queuePosition(list: readonly OperationRecord[], id: string): number | null {
|
|
131
|
+
const queued = list.filter(record => record.state === 'queued')
|
|
132
|
+
const index = queued.findIndex(record => record.id === id)
|
|
133
|
+
return index < 0 ? null : index
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface OperationSummary {
|
|
137
|
+
running: number
|
|
138
|
+
queued: number
|
|
139
|
+
/** Records waiting on a decision. */
|
|
140
|
+
attention: number
|
|
141
|
+
/** Records that finished, however they finished. */
|
|
142
|
+
settled: number
|
|
143
|
+
/** running + queued + settled: the denominator of "3 / 7". */
|
|
144
|
+
total: number
|
|
145
|
+
/** settled + running, the numerator — a running item is the one in flight. */
|
|
146
|
+
progressed: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Counts for the panel entry. The entry reports the batch, never one row:
|
|
151
|
+
* one aggregate line beats seven notifications racing each other.
|
|
152
|
+
*/
|
|
153
|
+
export function summarize(list: readonly OperationRecord[]): OperationSummary {
|
|
154
|
+
let running = 0
|
|
155
|
+
let queued = 0
|
|
156
|
+
let attention = 0
|
|
157
|
+
let settled = 0
|
|
158
|
+
for (const record of list) {
|
|
159
|
+
if (record.state === 'running') running += 1
|
|
160
|
+
else if (record.state === 'queued') queued += 1
|
|
161
|
+
else if (needsUser(record)) attention += 1
|
|
162
|
+
else settled += 1
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
running,
|
|
166
|
+
queued,
|
|
167
|
+
attention,
|
|
168
|
+
settled,
|
|
169
|
+
total: running + queued + settled,
|
|
170
|
+
progressed: settled + running,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Panel order: what needs the user first, then what is still moving, then
|
|
176
|
+
* what is finished. Within a group the original order is kept so a queue
|
|
177
|
+
* reads top-to-bottom in the order it will run.
|
|
178
|
+
*/
|
|
179
|
+
export function sortForPanel(list: readonly OperationRecord[]): OperationRecord[] {
|
|
180
|
+
const rank = (record: OperationRecord): number =>
|
|
181
|
+
needsUser(record) ? 0 : isSettled(record) ? 2 : 1
|
|
182
|
+
return [...list]
|
|
183
|
+
.map((record, index) => ({ record, index }))
|
|
184
|
+
.sort((a, b) => rank(a.record) - rank(b.record) || a.index - b.index)
|
|
185
|
+
.map(entry => entry.record)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The record a plugin card should reflect, newest first. A card shows the
|
|
190
|
+
* button state for an operation in flight and a terminal marker for one that
|
|
191
|
+
* ended without installing — without that marker a rejected install leaves
|
|
192
|
+
* the card looking untouched, and the obvious next move is to press Install
|
|
193
|
+
* again.
|
|
194
|
+
*/
|
|
195
|
+
export function recordForUrl(list: readonly OperationRecord[], url: string): OperationRecord | null {
|
|
196
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
197
|
+
const record = list[index]
|
|
198
|
+
if (record.url === url) return record
|
|
199
|
+
}
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics sub-panel — issue #98 phase 3 (plugin presets).
|
|
3
|
+
*
|
|
4
|
+
* Rendered at the bottom of the Diagnostics tab inside a collapsible section
|
|
5
|
+
* (the parent keeps it mounted and passes `open`, so it lazy-loads on first
|
|
6
|
+
* expand and keeps its state across collapses). Lists named presets
|
|
7
|
+
* (GET /dsh-market/presets), saves the current community bundle order as a
|
|
8
|
+
* new preset (POST /dsh-market/presets {action:'save', name, bundleOrder,
|
|
9
|
+
* disabled}), applies one (action:'apply' — the host trial-validates and
|
|
10
|
+
* auto-snapshots before writing; on success the parent refreshes the check
|
|
11
|
+
* report) and deletes one after an inline double confirmation
|
|
12
|
+
* (action:'delete').
|
|
13
|
+
*
|
|
14
|
+
* The list payload is read defensively (bare array or {presets: [...]}).
|
|
15
|
+
*/
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
17
|
+
import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
18
|
+
import css from './Market.module.css'
|
|
19
|
+
import { api } from './market-data.ts'
|
|
20
|
+
import type { Translate } from './market-data.ts'
|
|
21
|
+
|
|
22
|
+
/** One preset from GET /dsh-market/presets. */
|
|
23
|
+
export interface Preset {
|
|
24
|
+
name: string
|
|
25
|
+
bundleOrder: string[]
|
|
26
|
+
disabled: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PresetPanelProps {
|
|
30
|
+
t: Translate
|
|
31
|
+
/** True while the parent collapsible section is expanded (lazy-load gate). */
|
|
32
|
+
open: boolean
|
|
33
|
+
/** Current community bundle order (the Diagnostics ordering DRAFT — what the
|
|
34
|
+
* user is editing, not necessarily the last-applied order); saved verbatim. */
|
|
35
|
+
bundleOrder: string[]
|
|
36
|
+
/** Re-fetch the check report after a successful preset apply. */
|
|
37
|
+
onRefresh: () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface JsonBody {
|
|
41
|
+
ok?: unknown
|
|
42
|
+
error?: unknown
|
|
43
|
+
changes?: unknown
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function postJson(url: string, body: unknown): Promise<{ status: number; body: JsonBody | null }> {
|
|
47
|
+
let res: Response
|
|
48
|
+
try {
|
|
49
|
+
res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
50
|
+
} catch {
|
|
51
|
+
return { status: 0, body: null }
|
|
52
|
+
}
|
|
53
|
+
let payload: JsonBody | null = null
|
|
54
|
+
try { payload = (await res.json()) as JsonBody } catch { /* non-JSON body */ }
|
|
55
|
+
return { status: res.status, body: payload }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const errorText = (status: number, body: JsonBody | null, fallback: string): string =>
|
|
59
|
+
body !== null && typeof body.error === 'string'
|
|
60
|
+
? body.error
|
|
61
|
+
: status === 0 ? fallback + 'network error' : fallback + `HTTP ${String(status)}`
|
|
62
|
+
|
|
63
|
+
export function PresetPanel(props: PresetPanelProps) {
|
|
64
|
+
const { t, open, bundleOrder, onRefresh } = props
|
|
65
|
+
const [presets, setPresets] = useState<Preset[] | null>(null)
|
|
66
|
+
const [error, setError] = useState<string | null>(null)
|
|
67
|
+
const [msg, setMsg] = useState<string | null>(null)
|
|
68
|
+
const [busy, setBusy] = useState<string | null>(null)
|
|
69
|
+
const [name, setName] = useState('')
|
|
70
|
+
/** Preset name awaiting the second confirm click, or null. */
|
|
71
|
+
const [confirmDelete, setConfirmDelete] = useState<string | null>(null)
|
|
72
|
+
/** Hidden <input type="file"> for the import action. */
|
|
73
|
+
const loaded = useRef(false)
|
|
74
|
+
|
|
75
|
+
const load = useCallback(() => {
|
|
76
|
+
fetch(api('/dsh-market/presets'), { cache: 'no-store' })
|
|
77
|
+
.then(res => res.json())
|
|
78
|
+
.then(body => {
|
|
79
|
+
const list: unknown[] = Array.isArray(body) ? body : Array.isArray(body.presets) ? body.presets : []
|
|
80
|
+
setPresets(list.map((item: unknown) => {
|
|
81
|
+
const preset = (item ?? {}) as Preset
|
|
82
|
+
return {
|
|
83
|
+
name: String(preset.name ?? ''),
|
|
84
|
+
bundleOrder: Array.isArray(preset.bundleOrder) ? preset.bundleOrder.map(String) : [],
|
|
85
|
+
disabled: Array.isArray(preset.disabled) ? preset.disabled.map(String) : [],
|
|
86
|
+
}
|
|
87
|
+
}))
|
|
88
|
+
setError(null)
|
|
89
|
+
// Mark loaded only on success so a transient failure retries on the
|
|
90
|
+
// next expand (review M3).
|
|
91
|
+
loaded.current = true
|
|
92
|
+
})
|
|
93
|
+
.catch(() => setError(t('presetListFail') + 'network'))
|
|
94
|
+
}, [t])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (open && !loaded.current) {
|
|
98
|
+
load()
|
|
99
|
+
}
|
|
100
|
+
}, [open, load])
|
|
101
|
+
|
|
102
|
+
const save = useCallback(() => {
|
|
103
|
+
const presetName = name.trim()
|
|
104
|
+
if (presetName === '') {
|
|
105
|
+
setError(t('presetNameEmpty'))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (busy !== null) return
|
|
109
|
+
setBusy('save')
|
|
110
|
+
setMsg(null)
|
|
111
|
+
setError(null)
|
|
112
|
+
// Save the CURRENT disable list too — applying a preset restores it, so
|
|
113
|
+
// saving an empty list would silently re-enable every disabled plugin.
|
|
114
|
+
fetch(api('/dsh-market/installed'), { cache: 'no-store' })
|
|
115
|
+
.then(res => res.json())
|
|
116
|
+
.then((installed: { disabled?: unknown }) => postJson(api('/dsh-market/presets'), {
|
|
117
|
+
action: 'save',
|
|
118
|
+
name: presetName,
|
|
119
|
+
bundleOrder,
|
|
120
|
+
disabled: Array.isArray(installed.disabled) ? installed.disabled.map(String) : [],
|
|
121
|
+
}))
|
|
122
|
+
.then(({ status, body }) => {
|
|
123
|
+
if (status >= 200 && status < 300 && body?.ok === true) {
|
|
124
|
+
setName('')
|
|
125
|
+
setMsg(t('presetSaved'))
|
|
126
|
+
load()
|
|
127
|
+
} else {
|
|
128
|
+
setError(errorText(status, body, t('presetFail')))
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
.catch(() => setError(t('presetFail') + 'network'))
|
|
132
|
+
.finally(() => setBusy(null))
|
|
133
|
+
}, [busy, bundleOrder, load, name, t])
|
|
134
|
+
|
|
135
|
+
const apply = useCallback((presetName: string) => {
|
|
136
|
+
if (busy !== null) return
|
|
137
|
+
setBusy('apply')
|
|
138
|
+
setMsg(null)
|
|
139
|
+
setError(null)
|
|
140
|
+
postJson(api('/dsh-market/presets'), { action: 'apply', name: presetName })
|
|
141
|
+
.then(({ status, body }) => {
|
|
142
|
+
if (status >= 200 && status < 300 && body?.ok === true) {
|
|
143
|
+
setMsg(t('presetApplied'))
|
|
144
|
+
onRefresh()
|
|
145
|
+
} else {
|
|
146
|
+
setError(errorText(status, body, t('presetFail')))
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.catch(() => setError(t('presetFail') + 'network'))
|
|
150
|
+
.finally(() => setBusy(null))
|
|
151
|
+
}, [busy, onRefresh, t])
|
|
152
|
+
|
|
153
|
+
const remove = useCallback((presetName: string) => {
|
|
154
|
+
if (busy !== null) return
|
|
155
|
+
setBusy('delete')
|
|
156
|
+
setMsg(null)
|
|
157
|
+
setError(null)
|
|
158
|
+
postJson(api('/dsh-market/presets'), { action: 'delete', name: presetName })
|
|
159
|
+
.then(({ status, body }) => {
|
|
160
|
+
if (status >= 200 && status < 300 && body?.ok === true) {
|
|
161
|
+
setConfirmDelete(null)
|
|
162
|
+
setMsg(t('presetDeleted'))
|
|
163
|
+
load()
|
|
164
|
+
} else {
|
|
165
|
+
setError(errorText(status, body, t('presetFail')))
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.catch(() => setError(t('presetFail') + 'network'))
|
|
169
|
+
.finally(() => setBusy(null))
|
|
170
|
+
}, [busy, load, t])
|
|
171
|
+
|
|
172
|
+
/** Change preview of one preset: {reordered[], enabled[], disabled[], noop}. */
|
|
173
|
+
const [previewed, setPreviewed] = useState<{ name: string; changes: { reordered: string[]; enabled: string[]; disabled: string[]; noop: boolean } } | null>(null)
|
|
174
|
+
const preview = useCallback((presetName: string) => {
|
|
175
|
+
if (busy !== null) return
|
|
176
|
+
setBusy('preview')
|
|
177
|
+
setMsg(null)
|
|
178
|
+
setError(null)
|
|
179
|
+
if (previewed?.name === presetName) {
|
|
180
|
+
setPreviewed(null)
|
|
181
|
+
setBusy(null)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
postJson(api('/dsh-market/presets'), { action: 'preview', name: presetName })
|
|
185
|
+
.then(({ status, body }) => {
|
|
186
|
+
if (status >= 200 && status < 300 && body?.ok === true && body.changes !== null && typeof body.changes === 'object') {
|
|
187
|
+
const changes = body.changes as { reordered?: unknown; enabled?: unknown; disabled?: unknown; noop?: unknown }
|
|
188
|
+
setPreviewed({
|
|
189
|
+
name: presetName,
|
|
190
|
+
changes: {
|
|
191
|
+
reordered: Array.isArray(changes.reordered) ? changes.reordered.map(String) : [],
|
|
192
|
+
enabled: Array.isArray(changes.enabled) ? changes.enabled.map(String) : [],
|
|
193
|
+
disabled: Array.isArray(changes.disabled) ? changes.disabled.map(String) : [],
|
|
194
|
+
noop: changes.noop === true,
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
setError(errorText(status, body, t('presetPreviewFail')))
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
.catch(() => setError(t('presetPreviewFail') + 'network'))
|
|
202
|
+
.finally(() => setBusy(null))
|
|
203
|
+
}, [busy, previewed, t])
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className={css.orderPanel}>
|
|
207
|
+
<p className={css.panelNote}>{t('presetHint')}</p>
|
|
208
|
+
|
|
209
|
+
{error !== null && <div className={css.err}>{error}</div>}
|
|
210
|
+
|
|
211
|
+
{presets === null || presets.length === 0
|
|
212
|
+
? <div className={css.diagEmpty}>{t('presetEmpty')}</div>
|
|
213
|
+
: (
|
|
214
|
+
<div className={css.presetList}>
|
|
215
|
+
{presets.map(preset => (
|
|
216
|
+
<div key={preset.name} className={css.presetRow}>
|
|
217
|
+
<div className={css.confirmRow}>
|
|
218
|
+
<span className={css.presetName}>{preset.name}</span>
|
|
219
|
+
{preset.bundleOrder.length > 0 && (
|
|
220
|
+
<span className={css.spec}>{t('presetBundleCount').replace('{0}', String(preset.bundleOrder.length))}</span>
|
|
221
|
+
)}
|
|
222
|
+
<span className={css.grow} />
|
|
223
|
+
{confirmDelete === preset.name ? (
|
|
224
|
+
<span className={css.confirmRow}>
|
|
225
|
+
<Button variant="primary" size="sm" disabled={busy !== null} onClick={() => remove(preset.name)}>{t('presetDelete')}</Button>
|
|
226
|
+
<Button variant="ghost" size="sm" disabled={busy !== null} onClick={() => setConfirmDelete(null)}>{t('cancel')}</Button>
|
|
227
|
+
</span>
|
|
228
|
+
) : (
|
|
229
|
+
<span className={css.confirmRow}>
|
|
230
|
+
<Button variant="outline" size="sm" disabled={busy !== null} onClick={() => apply(preset.name)}>{t('presetApply')}</Button>
|
|
231
|
+
<Button variant="ghost" size="sm" disabled={busy !== null} onClick={() => preview(preset.name)}>
|
|
232
|
+
{previewed?.name === preset.name ? t('cancel') : t('presetPreview')}
|
|
233
|
+
</Button>
|
|
234
|
+
<Button variant="ghost" size="sm" disabled={busy !== null} onClick={() => setConfirmDelete(preset.name)}>{t('presetDelete')}</Button>
|
|
235
|
+
</span>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
{previewed?.name === preset.name && (
|
|
239
|
+
<div className={css.diagList}>
|
|
240
|
+
<span className={css.diagKey}>{t('presetPreviewTitle')}</span>
|
|
241
|
+
{previewed.changes.noop
|
|
242
|
+
? <div className={css.spec}>{t('presetNoop')}</div>
|
|
243
|
+
: (
|
|
244
|
+
<div className={css.spec}>
|
|
245
|
+
{previewed.changes.reordered.length > 0
|
|
246
|
+
&& <div className={css.warnLine}>{t('presetReorder').replace('{0}', String(previewed.changes.reordered.length))}: {previewed.changes.reordered.join(', ')}</div>}
|
|
247
|
+
{previewed.changes.enabled.length > 0
|
|
248
|
+
&& <div className={css.warnLine}>{t('presetEnable').replace('{0}', String(previewed.changes.enabled.length))}: {previewed.changes.enabled.join(', ')}</div>}
|
|
249
|
+
{previewed.changes.disabled.length > 0
|
|
250
|
+
&& <div className={css.warnLine}>{t('presetDisable').replace('{0}', String(previewed.changes.disabled.length))}: {previewed.changes.disabled.join(', ')}</div>}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{msg !== null && <div className={css.okState}>{msg}</div>}
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient types for @deepseek-ai/dsh-client-ui-primitives — provided at
|
|
3
|
+
* runtime by the host's frozen platform module table
|
|
4
|
+
* (packages/client/web/src/platform.ts). The published npm package is only a
|
|
5
|
+
* dev-time mirror used by the jsdom test lane, so the browser bundle stays
|
|
6
|
+
* external (see tsdown.config.ts CLIENT_EXTERNALS); keep these signatures in
|
|
7
|
+
* sync with deepseek-harness/packages/client/ui-primitives/src/ @0.1.0-rc.6.
|
|
8
|
+
* Only the members this plugin uses are declared.
|
|
9
|
+
*/
|
|
10
|
+
declare module '@deepseek-ai/dsh-client-ui-primitives' {
|
|
11
|
+
import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactElement, ReactNode } from 'react'
|
|
12
|
+
|
|
13
|
+
export type ButtonVariant = 'primary' | 'ghost' | 'outline' | 'toolbar'
|
|
14
|
+
export function Button(props: {
|
|
15
|
+
variant?: ButtonVariant
|
|
16
|
+
size?: 'md' | 'sm'
|
|
17
|
+
icon?: ReactNode
|
|
18
|
+
className?: string | undefined
|
|
19
|
+
children?: ReactNode
|
|
20
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>): ReactElement
|
|
21
|
+
|
|
22
|
+
export function Pill(props: {
|
|
23
|
+
active?: boolean
|
|
24
|
+
className?: string | undefined
|
|
25
|
+
children?: ReactNode
|
|
26
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>): ReactElement
|
|
27
|
+
|
|
28
|
+
export function Input(props: {
|
|
29
|
+
icon?: ReactNode
|
|
30
|
+
className?: string
|
|
31
|
+
} & InputHTMLAttributes<HTMLInputElement>): ReactElement
|
|
32
|
+
|
|
33
|
+
export function Modal(props: {
|
|
34
|
+
open: boolean
|
|
35
|
+
onClose: () => void
|
|
36
|
+
title: string
|
|
37
|
+
closeLabel?: string
|
|
38
|
+
description?: string
|
|
39
|
+
children?: ReactNode
|
|
40
|
+
footer?: ReactNode
|
|
41
|
+
className?: string
|
|
42
|
+
contentClassName?: string
|
|
43
|
+
headless?: boolean
|
|
44
|
+
}): ReactElement | null
|
|
45
|
+
|
|
46
|
+
export function Toast(props: {
|
|
47
|
+
text: string
|
|
48
|
+
icon?: ReactNode
|
|
49
|
+
anchor?: HTMLElement | null
|
|
50
|
+
onDone: () => void
|
|
51
|
+
}): ReactElement
|
|
52
|
+
|
|
53
|
+
/** One primary-menu entry: a row, a separator, or a heading label. */
|
|
54
|
+
export interface MenuItem {
|
|
55
|
+
id: string
|
|
56
|
+
label: ReactNode
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
icon?: ReactNode
|
|
59
|
+
danger?: boolean
|
|
60
|
+
submenu?: readonly MenuItem[]
|
|
61
|
+
}
|
|
62
|
+
export interface MenuSeparator { type: 'separator'; id: string }
|
|
63
|
+
export interface MenuLabel { type: 'label'; id: string; text: string }
|
|
64
|
+
export type MenuEntry = MenuItem | MenuSeparator | MenuLabel
|
|
65
|
+
|
|
66
|
+
export function Menu(props: {
|
|
67
|
+
open: boolean
|
|
68
|
+
anchor: ReactNode
|
|
69
|
+
items: readonly MenuEntry[]
|
|
70
|
+
footer?: readonly MenuEntry[]
|
|
71
|
+
selectedId?: string | undefined
|
|
72
|
+
selectedIds?: readonly string[] | undefined
|
|
73
|
+
onSelect: (id: string) => void
|
|
74
|
+
onClose: () => void
|
|
75
|
+
align?: 'start' | 'end'
|
|
76
|
+
side?: 'bottom' | 'top' | 'right'
|
|
77
|
+
portal?: boolean
|
|
78
|
+
closeOnPointerLeave?: boolean
|
|
79
|
+
dense?: boolean
|
|
80
|
+
compact?: boolean
|
|
81
|
+
className?: string
|
|
82
|
+
}): ReactElement
|
|
83
|
+
|
|
84
|
+
export interface DisclosureRowProps {
|
|
85
|
+
icon: ReactNode
|
|
86
|
+
title: string
|
|
87
|
+
open: boolean
|
|
88
|
+
expandable: boolean
|
|
89
|
+
onToggle: () => void
|
|
90
|
+
expandOnRowClick?: boolean | undefined
|
|
91
|
+
previewChevron?: boolean | undefined
|
|
92
|
+
keepContentWhenOpen?: boolean | undefined
|
|
93
|
+
collapsedContent?: ReactNode
|
|
94
|
+
children?: ReactNode
|
|
95
|
+
className?: string | undefined
|
|
96
|
+
rowClassName?: string | undefined
|
|
97
|
+
leadingClassName?: string | undefined
|
|
98
|
+
chevronClassName?: string | undefined
|
|
99
|
+
titleClassName?: string | undefined
|
|
100
|
+
}
|
|
101
|
+
export function DisclosureRow(props: DisclosureRowProps): ReactElement
|
|
102
|
+
|
|
103
|
+
export function Tooltip(props: {
|
|
104
|
+
label: string | (() => string)
|
|
105
|
+
side?: 'right' | 'bottom' | 'top'
|
|
106
|
+
delayMs?: number
|
|
107
|
+
disabled?: boolean
|
|
108
|
+
maxWidth?: number
|
|
109
|
+
children: ReactElement
|
|
110
|
+
}): ReactElement
|
|
111
|
+
|
|
112
|
+
export interface IconProps {
|
|
113
|
+
size?: number
|
|
114
|
+
className?: string
|
|
115
|
+
}
|
|
116
|
+
export function IconChevronDownOutline14(props: IconProps): ReactElement
|
|
117
|
+
export function IconChevronUpOutline14(props: IconProps): ReactElement
|
|
118
|
+
export function IconCheckOutline16(props: IconProps): ReactElement
|
|
119
|
+
export function IconChevronLeftOutline14(props: IconProps): ReactElement
|
|
120
|
+
export function IconChevronRightOutline14(props: IconProps): ReactElement
|
|
121
|
+
export function IconSearchOutline16(props: IconProps): ReactElement
|
|
122
|
+
export function IconRefreshOutline14(props: IconProps): ReactElement
|
|
123
|
+
export function IconWarningOutline16(props: IconProps): ReactElement
|
|
124
|
+
export function IconQuestionOutline14(props: IconProps): ReactElement
|
|
125
|
+
export function IconSparkle16(props: IconProps): ReactElement
|
|
126
|
+
export function IconCodeOutline16(props: IconProps): ReactElement
|
|
127
|
+
export function IconCordisPluginOutline14(props: IconProps): ReactElement
|
|
128
|
+
export function IconLoadingOutline16(props: IconProps): ReactElement
|
|
129
|
+
export function IconLinkOutline14(props: IconProps): ReactElement
|
|
130
|
+
export function IconDownloadOutline16(props: IconProps): ReactElement
|
|
131
|
+
export function IconFullscreenOutline16(props: IconProps): ReactElement
|
|
132
|
+
export function IconFolderOpen16(props: IconProps): ReactElement
|
|
133
|
+
|
|
134
|
+
export type StateDotState = 'done' | 'warning' | 'ongoing' | 'error'
|
|
135
|
+
export function StateDot(props: {
|
|
136
|
+
state: StateDotState
|
|
137
|
+
size?: number | undefined
|
|
138
|
+
className?: string | undefined
|
|
139
|
+
}): ReactElement
|
|
140
|
+
}
|