@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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. 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
+ }