@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,365 @@
1
+ /**
2
+ * The activity entry and its panel: every install, update and uninstall the
3
+ * user started, with the action each outcome calls for.
4
+ *
5
+ * The entry lives in the tab row rather than above the plugin grid, so
6
+ * paginating, searching or switching tab cannot take a record — or a pending
7
+ * decision — off screen. It reports the batch as one aggregate ("installing
8
+ * 3 / 7") instead of one line per plugin.
9
+ */
10
+
11
+ import {
12
+ Button,
13
+ IconCheckOutline16,
14
+ IconLoadingOutline16,
15
+ IconWarningOutline16,
16
+ IconChevronDownOutline14,
17
+ IconChevronUpOutline14,
18
+ } from '@deepseek-ai/dsh-client-ui-primitives'
19
+ import { useEffect, useRef, useState } from 'react'
20
+ import type { ReactNode } from 'react'
21
+ import css from './Market.module.css'
22
+ import type { Translate } from './market-data.ts'
23
+ import type { ConflictGroup, OperationRecord } from './operations.ts'
24
+ import { bucketOf, isSettled, needsUser, queuePosition, sortForPanel, summarize } from './operations.ts'
25
+
26
+ /** What the two clash outcomes are, in the order they are offered. */
27
+ const CHOICES = [
28
+ { id: 'keep' as const, label: 'conflictKeep', note: 'conflictKeepNote' },
29
+ { id: 'swap' as const, label: 'conflictSwap', note: 'conflictSwapNote' },
30
+ ]
31
+
32
+ /**
33
+ * How the panel renders a plugin it only knows by package name: the catalog
34
+ * (or the installed list) supplies the author and avatar a card would show.
35
+ */
36
+ export type DescribePlugin = (name: string) => {
37
+ title: string
38
+ author?: string | undefined
39
+ avatar?: ReactNode
40
+ }
41
+
42
+ export interface OperationsPanelProps {
43
+ t: Translate
44
+ /** Resolves a package name to the identity a card would show for it. */
45
+ describe: DescribePlugin
46
+ records: readonly OperationRecord[]
47
+ open: boolean
48
+ /** Owned by the parent: a card's "cannot install" marker raises the panel. */
49
+ onOpenChange: (open: boolean) => void
50
+ /** True while a swap is running, which disables both clash outcomes. */
51
+ replacing: boolean
52
+ /** Blocks the destructive outcome when pnpm is not usable yet. */
53
+ envReady: boolean
54
+ onClearSettled: () => void
55
+ onCancel: (record: OperationRecord) => void
56
+ onDismiss: (record: OperationRecord) => void
57
+ onRefresh: () => void
58
+ /** Resolve a clash: keep what is installed, or uninstall it and retry. */
59
+ onResolveConflict: (record: OperationRecord, choice: 'keep' | 'swap') => void
60
+ /** Retry an operation the host refused for a fixable reason. */
61
+ onRetry?: ((record: OperationRecord) => void) | undefined
62
+ /** Approve the build scripts pnpm blocked, then retry — replaces the plain
63
+ * retry on a record that failed for that reason, so the fix sits next to
64
+ * the sentence describing the problem. */
65
+ onApproveBuilds?: ((record: OperationRecord) => void) | undefined
66
+ }
67
+
68
+ /**
69
+ * What each plugin ends up as under the selected outcome.
70
+ *
71
+ * The consequence is drawn ON the plugins rather than described beside them:
72
+ * pick "keep what I have" and the installed rows tick while the candidate
73
+ * crosses out; pick the other and they swap. The candidate is in this list
74
+ * for the same reason — a decision about which plugin survives has to show
75
+ * what happens to the one being installed, not only to the others.
76
+ */
77
+ function OutcomePreview(props: {
78
+ t: Translate
79
+ record: OperationRecord
80
+ choice: 'keep' | 'swap'
81
+ describe: DescribePlugin
82
+ }) {
83
+ const { t, record, choice } = props
84
+ const swap = choice === 'swap'
85
+ const candidate = props.describe(record.name)
86
+ const row = (key: string, info: ReturnType<DescribePlugin>, kept: boolean, tag: string) => (
87
+ <div key={key} className={kept ? css.rosterRow : `${css.rosterRow} ${css.rosterRowOut}`}>
88
+ {info.avatar}
89
+ <span className={css.rosterMain}>
90
+ <span className={css.rosterName} title={key}>{info.title}</span>
91
+ {info.author !== undefined && <span className={css.rosterAuthor}>{info.author}</span>}
92
+ </span>
93
+ <span className={kept ? `${css.rosterTag} ${css.rosterTagKeep}` : `${css.rosterTag} ${css.rosterTagDrop}`}>
94
+ {kept ? '✓' : '✕'} {tag}
95
+ </span>
96
+ </div>
97
+ )
98
+ return (
99
+ <div className={css.roster}>
100
+ {row(record.name, candidate, swap, t(swap ? 'conflictOutcomeInstall' : 'conflictOutcomeSkip'))}
101
+ <div className={css.rosterSplit} />
102
+ {(record.conflicts ?? []).map(group =>
103
+ row(group.owner, props.describe(group.owner), !swap, t(swap ? 'conflictOutcomeRemove' : 'conflictOutcomeKeep')))}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ /**
109
+ * The decision attached to a clash. Two outcomes rather than an error plus a
110
+ * destructive button: the default changes nothing, and selecting the other
111
+ * one is itself the consent step, so its cost is stated here.
112
+ */
113
+ function ConflictChoice(props: {
114
+ t: Translate
115
+ record: OperationRecord
116
+ replacing: boolean
117
+ envReady: boolean
118
+ describe: DescribePlugin
119
+ onResolve: (choice: 'keep' | 'swap') => void
120
+ }) {
121
+ const { t, record, replacing, envReady } = props
122
+ const [choice, setChoice] = useState<'keep' | 'swap'>('keep')
123
+ const [whyOpen, setWhyOpen] = useState(false)
124
+ return (
125
+ <div className={css.opDecision}>
126
+ <p className={css.conflictBody}>{t('conflictBody')}</p>
127
+ <OutcomePreview t={t} record={record} choice={choice} describe={props.describe} />
128
+ {/* Native radios: the platform already draws the control, groups it by
129
+ `name`, and gives arrow-key navigation. A hand-rolled ring gets the
130
+ look wrong and the keyboard behaviour with it. */}
131
+ <div className={css.choices}>
132
+ {CHOICES.map(({ id, label, note }) => (
133
+ <label
134
+ key={id}
135
+ className={choice === id ? `${css.choice} ${css.choiceOn}` : css.choice}
136
+ >
137
+ <input
138
+ type="radio"
139
+ className={css.choiceRadio}
140
+ name={`dshm-conflict-${record.id}`}
141
+ checked={choice === id}
142
+ disabled={replacing}
143
+ onChange={() => setChoice(id)}
144
+ />
145
+ <span className={css.choiceMain}>
146
+ <span className={css.choiceTitle}>{t(label)}</span>
147
+ {t(note) !== '' && <span className={css.choiceNote}>{t(note)}</span>}
148
+ </span>
149
+ </label>
150
+ ))}
151
+ </div>
152
+ {/* Both ways out of the decision share one row: the quiet one that
153
+ reveals evidence, and the one that commits. */}
154
+ <div className={css.opDecisionFoot}>
155
+ <button
156
+ type="button"
157
+ className={css.conflictDetailsToggle}
158
+ aria-expanded={whyOpen}
159
+ onClick={() => setWhyOpen(open => !open)}
160
+ >
161
+ {t('conflictDetails')}
162
+ {whyOpen ? <IconChevronUpOutline14 size={12} /> : <IconChevronDownOutline14 size={12} />}
163
+ </button>
164
+ <span className={css.grow} />
165
+ <Button
166
+ variant={choice === 'swap' ? 'outline' : 'primary'}
167
+ size="sm"
168
+ className={choice === 'swap' ? css.dangerBtn : undefined}
169
+ disabled={replacing || (choice === 'swap' && !envReady)}
170
+ onClick={() => props.onResolve(choice)}
171
+ >{replacing ? t('conflictReplacing') : t('confirm')}</Button>
172
+ </div>
173
+ {whyOpen && (
174
+ <div className={css.conflictWhy}>
175
+ {(record.conflicts ?? []).map(group => (
176
+ <div key={group.owner}>{group.owner}: {group.ids.join(', ')}</div>
177
+ ))}
178
+ <div className={css.conflictWhyText}>{t('conflictWhy')}</div>
179
+ </div>
180
+ )}
181
+ </div>
182
+ )
183
+ }
184
+
185
+ /** Icon for a record's visual bucket — three, not one per state. */
186
+ function BucketIcon(props: { record: OperationRecord }) {
187
+ const bucket = bucketOf(props.record.state)
188
+ if (bucket === 'busy') {
189
+ return props.record.state === 'running'
190
+ ? <span className={css.spin}><IconLoadingOutline16 size={13} /></span>
191
+ : <span className={css.opQueuedIcon}>⋯</span>
192
+ }
193
+ if (bucket === 'ok') return <IconCheckOutline16 size={13} className={css.reassureOk} />
194
+ return <IconWarningOutline16 size={14} className={css.conflictIcon} />
195
+ }
196
+
197
+ /** The one-line status under a record's name; the bucket carries the rest. */
198
+ function statusLine(t: Translate, record: OperationRecord, ahead: number | null): string {
199
+ switch (record.state) {
200
+ case 'queued':
201
+ return ahead === null || ahead === 0 ? t('opQueued') : `${t('opQueued')} · ${t('opQueuedAhead')} ${String(ahead)}`
202
+ case 'running':
203
+ return record.detail ?? t('opRunning')
204
+ case 'input':
205
+ return t('opNeedsChoice')
206
+ case 'failed':
207
+ return record.reason ?? t('installFail')
208
+ case 'warned':
209
+ return record.reason ?? t('opDone')
210
+ case 'done':
211
+ return record.needsRefresh === true ? t('opDoneRefresh') : t('opDone')
212
+ }
213
+ }
214
+
215
+ export function OperationsPanel(props: OperationsPanelProps) {
216
+ const { t, records, open } = props
217
+ const setOpen = props.onOpenChange
218
+ const wrapRef = useRef<HTMLDivElement | null>(null)
219
+ const summary = summarize(records)
220
+ const busy = summary.running + summary.queued > 0
221
+
222
+ // Dismissing a popover by pressing the control that opened it is the one
223
+ // route nobody looks for. Escape and an outside click are; the header also
224
+ // carries an explicit collapse for anyone who wants a target to aim at.
225
+ // The listener covers the whole wrapper, button included, so the button's
226
+ // own toggle is not undone by this closing first.
227
+ useEffect(() => {
228
+ if (!open) return undefined
229
+ const onKey = (event: KeyboardEvent) => { if (event.key === 'Escape') setOpen(false) }
230
+ const onPointer = (event: MouseEvent) => {
231
+ const wrap = wrapRef.current
232
+ if (wrap !== null && !wrap.contains(event.target as Node)) setOpen(false)
233
+ }
234
+ document.addEventListener('keydown', onKey)
235
+ document.addEventListener('mousedown', onPointer)
236
+ return () => {
237
+ document.removeEventListener('keydown', onKey)
238
+ document.removeEventListener('mousedown', onPointer)
239
+ }
240
+ }, [open, setOpen])
241
+
242
+ // The entry label is the batch, not a verb with no object: a bare "3" says
243
+ // nothing about what is happening to the profile.
244
+ const label = busy
245
+ ? `${t('opInstalling')} ${String(summary.progressed)}/${String(summary.total)}`
246
+ : summary.attention > 0
247
+ ? `${String(summary.attention)} ${t('opNeedsYou')}`
248
+ : t('opTitle')
249
+
250
+ // Always render inside .opWrap so the tab-row bottom margin is stable —
251
+ // the old idle-only <button> skipped the wrapper and jumped on first open.
252
+ const quiet = records.length === 0 && !open
253
+
254
+ return (
255
+ <div className={css.opWrap} ref={wrapRef}>
256
+ <button
257
+ type="button"
258
+ className={quiet
259
+ ? `${css.opEntry} ${css.opEntryQuiet}`
260
+ : summary.attention > 0
261
+ ? `${css.opEntry} ${css.opEntryAlert}`
262
+ : css.opEntry}
263
+ aria-expanded={open}
264
+ onClick={() => setOpen(quiet ? true : !open)}
265
+ >
266
+ {!quiet && busy && <span className={css.spin}><IconLoadingOutline16 size={12} /></span>}
267
+ {quiet ? t('opTitle') : label}
268
+ {!quiet && summary.attention > 0 && <span className={css.opDot} />}
269
+ </button>
270
+ {open && (
271
+ <div className={css.opPanel}>
272
+ <div className={css.opHead}>
273
+ <span className={css.opPanelTitle}>{t('opTitle')}</span>
274
+ <span className={css.grow} />
275
+ {summary.settled > 0 && (
276
+ <Button variant="ghost" size="sm" onClick={props.onClearSettled}>{t('opClear')}</Button>
277
+ )}
278
+ <Button
279
+ variant="ghost"
280
+ size="sm"
281
+ aria-label={t('opClose')}
282
+ title={t('opClose')}
283
+ className={css.opCloseBtn}
284
+ onClick={() => setOpen(false)}
285
+ ><IconChevronUpOutline14 size={14} /></Button>
286
+ </div>
287
+ {busy && (
288
+ <div className={css.opAggregate}>
289
+ <div className={css.opAggregateTop}>
290
+ <span>{t('opInstalling')} {summary.progressed}/{summary.total}</span>
291
+ </div>
292
+ <div className={css.bar}>
293
+ <div
294
+ className={css.barFill}
295
+ style={{ width: `${String(Math.round(summary.progressed / Math.max(1, summary.total) * 100))}%` }}
296
+ />
297
+ </div>
298
+ {/* Long installs are the norm; saying so is what makes leaving
299
+ the page an option rather than a gamble. */}
300
+ <div className={css.opAggregateHint}>{t('opLeaveHint')}</div>
301
+ </div>
302
+ )}
303
+ {records.length === 0 && (
304
+ <div className={css.opEmpty}>
305
+ {t('opEmpty')}
306
+ <div className={css.opEmptyHint}>{t('opEmptyHint')}</div>
307
+ </div>
308
+ )}
309
+ {sortForPanel(records).map((record) => {
310
+ const ahead = queuePosition(records, record.id)
311
+ return (
312
+ <div key={record.id} className={needsUser(record) ? `${css.opRow} ${css.opRowAlert}` : css.opRow}>
313
+ <span className={css.opIcon}><BucketIcon record={record} /></span>
314
+ <div className={css.opMain}>
315
+ <div className={css.opTop}>
316
+ <span className={css.opVerb}>{t(`opKind_${record.kind}`)}</span>
317
+ <span className={css.opName} title={record.name}>{record.name}</span>
318
+ </div>
319
+ {record.state === 'running' && typeof record.percent === 'number' && (
320
+ <div className={css.bar}>
321
+ <div className={css.barFill} style={{ width: `${String(record.percent)}%` }} />
322
+ </div>
323
+ )}
324
+ <div className={bucketOf(record.state) === 'attention' ? `${css.opStatus} ${css.opStatusBad}` : css.opStatus}>
325
+ {statusLine(t, record, ahead)}
326
+ </div>
327
+ {needsUser(record) && (
328
+ <ConflictChoice
329
+ t={t}
330
+ record={record}
331
+ replacing={props.replacing}
332
+ envReady={props.envReady}
333
+ describe={props.describe}
334
+ onResolve={choice => props.onResolveConflict(record, choice)}
335
+ />
336
+ )}
337
+ </div>
338
+ <div className={css.opActions}>
339
+ {record.state === 'running' && (
340
+ <Button variant="outline" size="sm" onClick={() => props.onCancel(record)}>{t('cancelOp')}</Button>
341
+ )}
342
+ {record.state === 'queued' && (
343
+ <Button variant="ghost" size="sm" onClick={() => props.onDismiss(record)}>{t('opDequeue')}</Button>
344
+ )}
345
+ {record.state === 'done' && record.needsRefresh === true && (
346
+ <Button variant="primary" size="sm" onClick={props.onRefresh}>{t('refresh')}</Button>
347
+ )}
348
+ {record.state === 'failed' && (record.blockedBuilds ?? []).length > 0 && props.onApproveBuilds !== undefined && (
349
+ <Button variant="primary" size="sm" onClick={() => props.onApproveBuilds?.(record)}>{t('approveBuilds')}</Button>
350
+ )}
351
+ {record.state === 'failed' && (record.blockedBuilds ?? []).length === 0 && props.onRetry !== undefined && (
352
+ <Button variant="outline" size="sm" onClick={() => props.onRetry?.(record)}>{t('opRetry')}</Button>
353
+ )}
354
+ {isSettled(record) && (
355
+ <Button variant="ghost" size="sm" onClick={() => props.onDismiss(record)}>{t('dismissNotice')}</Button>
356
+ )}
357
+ </div>
358
+ </div>
359
+ )
360
+ })}
361
+ </div>
362
+ )}
363
+ </div>
364
+ )
365
+ }