@shendeguize/dsh-agent-sidecar 0.1.1 → 0.2.0
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 +78 -11
- package/lib/client.js +8659 -7589
- package/lib/client.js.map +1 -1
- package/package.json +3 -1
- package/src/client/analysis/AnalysisPanel.tsx +19 -27
- package/src/client/analysis/analysis.module.css +36 -97
- package/src/client/board/Board.tsx +115 -61
- package/src/client/board/board.module.css +72 -151
- package/src/client/board/project-view-logic.ts +21 -34
- package/src/client/board/project-view.module.css +41 -96
- package/src/client/board/project-view.tsx +29 -13
- package/src/client/board/strings.ts +68 -107
- package/src/client/commands.ts +30 -15
- package/src/client/detail/SessionDetail.tsx +92 -58
- package/src/client/detail/detail.module.css +62 -218
- package/src/client/detail/strings.ts +64 -88
- package/src/client/detail-view.module.css +30 -55
- package/src/client/detail-view.tsx +80 -108
- package/src/client/dsh-tools/LineageTree.tsx +22 -13
- package/src/client/dsh-tools/SearchPanel.tsx +18 -11
- package/src/client/dsh-tools/dsh-tools.module.css +50 -139
- package/src/client/dsh-tools/strings.ts +42 -77
- package/src/client/index.ts +285 -124
- package/src/client/inject/InjectPanel.tsx +31 -64
- package/src/client/inject/inject.module.css +97 -198
- package/src/client/lifecycle/handoff.ts +83 -0
- package/src/client/locales/en.ts +74 -2
- package/src/client/locales/host.ts +131 -0
- package/src/client/locales/index.ts +196 -8
- package/src/client/locales/react.ts +10 -0
- package/src/client/locales/view.ts +38 -0
- package/src/client/locales/zh.ts +194 -134
- package/src/client/mount.tsx +72 -38
- package/src/client/navigation/CenterOverlay.tsx +40 -0
- package/src/client/navigation/center-overlay.module.css +51 -0
- package/src/client/navigation/center.ts +45 -0
- package/src/client/navigation/modal-isolation.ts +152 -0
- package/src/client/navigation/modal-surface-anchor.ts +72 -0
- package/src/client/navigation/sidebar-entry.module.css +73 -0
- package/src/client/navigation/sidebar-entry.ts +309 -0
- package/src/client/primitives/StaticPill.tsx +21 -0
- package/src/client/settings-card.module.css +35 -119
- package/src/client/settings-card.tsx +31 -153
- package/src/client/settings-fields.tsx +131 -0
- package/src/client/sidebar/SidebarTab.tsx +156 -0
- package/src/client/sidebar/model.ts +65 -0
- package/src/client/sidebar/sidebar-tab.module.css +118 -0
- package/src/client/sidebar-tab.tsx +46 -320
- package/src/client/theme/agsc.module.css +31 -0
- package/src/client/theme/parts.ts +56 -0
- package/src/client/ui-integration.ts +86 -0
- package/src/client/widget.tsx +9 -8
- package/src/client/inject/overlay.module.css +0 -22
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
import type { HTMLAttributes, ReactElement } from 'react'
|
|
3
|
+
|
|
4
|
+
export type StaticPillProps = HTMLAttributes<HTMLSpanElement>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Preserve native span attributes around the rc.2 Pill static branch, which
|
|
8
|
+
* currently omits its rest props. The inner primitive remains the sole owner
|
|
9
|
+
* of the DSH pill visuals.
|
|
10
|
+
*/
|
|
11
|
+
export function StaticPill({
|
|
12
|
+
className,
|
|
13
|
+
children,
|
|
14
|
+
...rest
|
|
15
|
+
}: StaticPillProps): ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<span className={className} {...rest}>
|
|
18
|
+
<Pill>{children}</Pill>
|
|
19
|
+
</span>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
/* Agent Sidecar settings
|
|
2
|
-
*
|
|
3
|
-
* sectioned form body); every color rides a dsh --dsw-alias-* design token,
|
|
4
|
-
* no literal colors. */
|
|
1
|
+
/* Layout specific to the Agent Sidecar settings composition. Control chrome
|
|
2
|
+
* comes from dsh primitives; native select/checkbox styles stay token-backed. */
|
|
5
3
|
|
|
6
4
|
.card {
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 760px;
|
|
7
|
+
box-sizing: border-box;
|
|
7
8
|
list-style: none;
|
|
8
9
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
9
|
-
border-radius:
|
|
10
|
+
border-radius: var(--agsc-radius-card);
|
|
10
11
|
background: var(--dsw-alias-bg-layer-3);
|
|
11
12
|
transition: border-color .16s, background .16s;
|
|
12
13
|
}
|
|
@@ -20,8 +21,6 @@
|
|
|
20
21
|
border-color: var(--dsw-alias-label-dimmed);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/* ── disclosure header ─────────────────────────────────────────────────── */
|
|
24
|
-
|
|
25
24
|
.header {
|
|
26
25
|
width: 100%;
|
|
27
26
|
appearance: none;
|
|
@@ -35,7 +34,7 @@
|
|
|
35
34
|
align-items: center;
|
|
36
35
|
gap: 12px;
|
|
37
36
|
padding: 14px 16px;
|
|
38
|
-
border-radius:
|
|
37
|
+
border-radius: var(--agsc-radius-card);
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
.header:focus-visible {
|
|
@@ -64,37 +63,21 @@
|
|
|
64
63
|
color: var(--dsw-alias-label-tertiary);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/* Carried on the header so a collapsed card still says it holds edits. */
|
|
68
66
|
.pending {
|
|
69
67
|
flex: none;
|
|
70
|
-
border-radius: 999px;
|
|
71
|
-
padding: 1px 8px;
|
|
72
|
-
font-size: 11px;
|
|
73
|
-
line-height: 17px;
|
|
74
|
-
font-weight: 500;
|
|
75
68
|
white-space: nowrap;
|
|
76
|
-
background: var(--dsw-alias-bg-module-platform);
|
|
77
|
-
color: var(--dsw-alias-label-secondary);
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
/* CSS-drawn caret (ui-primitives icons are not on this package's dependency
|
|
81
|
-
* surface, so the chevron is a rotated border square). */
|
|
82
71
|
.chevron {
|
|
83
72
|
flex: none;
|
|
84
|
-
|
|
85
|
-
height: 8px;
|
|
86
|
-
border-right: 1.5px solid var(--dsw-alias-label-tertiary);
|
|
87
|
-
border-bottom: 1.5px solid var(--dsw-alias-label-tertiary);
|
|
88
|
-
transform: rotate(45deg) translateY(-2px);
|
|
73
|
+
color: var(--dsw-alias-label-tertiary);
|
|
89
74
|
transition: transform .16s;
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
.chevronOpen {
|
|
93
|
-
transform: rotate(
|
|
78
|
+
transform: rotate(180deg);
|
|
94
79
|
}
|
|
95
80
|
|
|
96
|
-
/* ── body ──────────────────────────────────────────────────────────────── */
|
|
97
|
-
|
|
98
81
|
.body {
|
|
99
82
|
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
100
83
|
margin: 0 16px;
|
|
@@ -124,8 +107,6 @@
|
|
|
124
107
|
color: var(--dsw-alias-label-primary);
|
|
125
108
|
}
|
|
126
109
|
|
|
127
|
-
/* ── fields ────────────────────────────────────────────────────────────── */
|
|
128
|
-
|
|
129
110
|
.field {
|
|
130
111
|
padding: 8px 0;
|
|
131
112
|
display: flex;
|
|
@@ -150,46 +131,47 @@
|
|
|
150
131
|
margin: 0;
|
|
151
132
|
font-size: 12px;
|
|
152
133
|
line-height: 1.5;
|
|
153
|
-
color: var(--dsw-alias-
|
|
134
|
+
color: var(--dsw-alias-state-error-primary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.input {
|
|
138
|
+
width: 100%;
|
|
139
|
+
max-width: 420px;
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.inputInvalid {
|
|
144
|
+
border-color: var(--dsw-alias-state-error-primary);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.input:has(input:disabled) {
|
|
148
|
+
opacity: .5;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
|
-
.input,
|
|
157
151
|
.select {
|
|
158
|
-
|
|
152
|
+
width: 100%;
|
|
159
153
|
max-width: 420px;
|
|
154
|
+
height: 32px;
|
|
155
|
+
box-sizing: border-box;
|
|
160
156
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
161
|
-
border-radius:
|
|
162
|
-
background: var(--dsw-alias-bg-layer-
|
|
163
|
-
padding:
|
|
157
|
+
border-radius: var(--agsc-radius-control);
|
|
158
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
159
|
+
padding: 0 8px;
|
|
164
160
|
font: inherit;
|
|
165
161
|
font-size: 13px;
|
|
166
162
|
line-height: 1.5;
|
|
167
163
|
color: var(--dsw-alias-label-primary);
|
|
168
164
|
}
|
|
169
165
|
|
|
170
|
-
.input:focus-visible,
|
|
171
166
|
.select:focus-visible {
|
|
172
167
|
outline: none;
|
|
173
168
|
border-color: var(--dsw-alias-brand-primary);
|
|
174
169
|
}
|
|
175
170
|
|
|
176
|
-
.input::placeholder {
|
|
177
|
-
color: var(--dsw-alias-label-tertiary);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.input:disabled,
|
|
181
171
|
.select:disabled {
|
|
182
172
|
opacity: 0.5;
|
|
183
173
|
cursor: default;
|
|
184
174
|
}
|
|
185
|
-
|
|
186
|
-
/* Modifier applied ALONGSIDE .input (no `composes`: the tsdown class-map
|
|
187
|
-
* emitter keeps only the local hashed name and would drop composed bases). */
|
|
188
|
-
.inputInvalid {
|
|
189
|
-
border-color: var(--dsw-alias-label-error);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/* Toggle row: label text left, native checkbox right-aligned with it. */
|
|
193
175
|
.toggleRow {
|
|
194
176
|
display: flex;
|
|
195
177
|
align-items: center;
|
|
@@ -209,8 +191,6 @@
|
|
|
209
191
|
cursor: default;
|
|
210
192
|
}
|
|
211
193
|
|
|
212
|
-
/* ── notes (safety / daemon guidance) ──────────────────────────────────── */
|
|
213
|
-
|
|
214
194
|
.note {
|
|
215
195
|
margin: 6px 0 2px;
|
|
216
196
|
border-radius: 8px;
|
|
@@ -221,8 +201,6 @@
|
|
|
221
201
|
color: var(--dsw-alias-label-secondary);
|
|
222
202
|
}
|
|
223
203
|
|
|
224
|
-
/* ── daemon status row ─────────────────────────────────────────────────── */
|
|
225
|
-
|
|
226
204
|
.statusRow {
|
|
227
205
|
display: flex;
|
|
228
206
|
align-items: center;
|
|
@@ -235,16 +213,15 @@
|
|
|
235
213
|
width: 8px;
|
|
236
214
|
height: 8px;
|
|
237
215
|
border-radius: 50%;
|
|
238
|
-
background: var(--
|
|
216
|
+
background: var(--agsc-fg-dimmed);
|
|
239
217
|
}
|
|
240
218
|
|
|
241
|
-
/* Modifiers applied alongside .statusDot (same no-`composes` rationale). */
|
|
242
219
|
.statusOk {
|
|
243
|
-
background: var(--
|
|
220
|
+
background: var(--agsc-ok);
|
|
244
221
|
}
|
|
245
222
|
|
|
246
223
|
.statusError {
|
|
247
|
-
background: var(--
|
|
224
|
+
background: var(--agsc-err);
|
|
248
225
|
}
|
|
249
226
|
|
|
250
227
|
.statusText {
|
|
@@ -260,30 +237,9 @@
|
|
|
260
237
|
}
|
|
261
238
|
|
|
262
239
|
.retry {
|
|
263
|
-
|
|
264
|
-
border: 1px solid var(--dsw-alias-border-l2);
|
|
265
|
-
border-radius: 8px;
|
|
266
|
-
background: none;
|
|
267
|
-
padding: 3px 12px;
|
|
268
|
-
font: inherit;
|
|
269
|
-
font-size: 12px;
|
|
270
|
-
line-height: 1.5;
|
|
271
|
-
color: var(--dsw-alias-label-secondary);
|
|
272
|
-
cursor: pointer;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.retry:hover:not(:disabled) {
|
|
276
|
-
color: var(--dsw-alias-label-primary);
|
|
277
|
-
border-color: var(--dsw-alias-label-dimmed);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.retry:focus-visible {
|
|
281
|
-
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
282
|
-
outline-offset: 1px;
|
|
240
|
+
margin-left: auto;
|
|
283
241
|
}
|
|
284
242
|
|
|
285
|
-
/* ── footer ────────────────────────────────────────────────────────────── */
|
|
286
|
-
|
|
287
243
|
.footer {
|
|
288
244
|
display: flex;
|
|
289
245
|
align-items: center;
|
|
@@ -311,49 +267,9 @@
|
|
|
311
267
|
text-align: right;
|
|
312
268
|
font-size: 12px;
|
|
313
269
|
line-height: 1.5;
|
|
314
|
-
color: var(--dsw-alias-
|
|
270
|
+
color: var(--dsw-alias-state-error-primary);
|
|
315
271
|
}
|
|
316
272
|
|
|
317
273
|
.spacer {
|
|
318
274
|
flex: 1;
|
|
319
275
|
}
|
|
320
|
-
|
|
321
|
-
.discard,
|
|
322
|
-
.save {
|
|
323
|
-
appearance: none;
|
|
324
|
-
border: 1px solid transparent;
|
|
325
|
-
border-radius: 8px;
|
|
326
|
-
padding: 5px 14px;
|
|
327
|
-
font: inherit;
|
|
328
|
-
font-size: 13px;
|
|
329
|
-
line-height: 1.5;
|
|
330
|
-
cursor: pointer;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.discard {
|
|
334
|
-
border-color: var(--dsw-alias-border-l2);
|
|
335
|
-
background: none;
|
|
336
|
-
color: var(--dsw-alias-label-secondary);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
.discard:hover:not(:disabled) {
|
|
340
|
-
color: var(--dsw-alias-label-primary);
|
|
341
|
-
border-color: var(--dsw-alias-label-dimmed);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
.save {
|
|
345
|
-
background: var(--dsw-alias-label-primary);
|
|
346
|
-
color: var(--dsw-alias-bg-layer-3);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.discard:disabled,
|
|
350
|
-
.save:disabled {
|
|
351
|
-
opacity: 0.4;
|
|
352
|
-
cursor: default;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.discard:focus-visible,
|
|
356
|
-
.save:focus-visible {
|
|
357
|
-
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
358
|
-
outline-offset: 1px;
|
|
359
|
-
}
|
|
@@ -30,10 +30,22 @@
|
|
|
30
30
|
* the sidecar card reads native next to first-party ones.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Button,
|
|
35
|
+
IconChevronDownOutline14,
|
|
36
|
+
Pill,
|
|
37
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
38
|
+
import { useState } from 'react'
|
|
34
39
|
import type { ReactNode } from 'react'
|
|
35
40
|
import { t as defaultT } from './locales/index.ts'
|
|
36
41
|
import type { SidecarLocaleKey } from './locales/index.ts'
|
|
42
|
+
import {
|
|
43
|
+
NumberField,
|
|
44
|
+
SelectField,
|
|
45
|
+
TextField,
|
|
46
|
+
ToggleField,
|
|
47
|
+
} from './settings-fields.tsx'
|
|
48
|
+
import { surfaceProps } from './theme/parts.ts'
|
|
37
49
|
import css from './settings-card.module.css'
|
|
38
50
|
|
|
39
51
|
/**
|
|
@@ -176,147 +188,6 @@ function Section(props: SectionProps): ReactNode {
|
|
|
176
188
|
)
|
|
177
189
|
}
|
|
178
190
|
|
|
179
|
-
interface SelectFieldProps {
|
|
180
|
-
label: string
|
|
181
|
-
hint: string
|
|
182
|
-
value: string
|
|
183
|
-
options: readonly { value: string, label: string }[]
|
|
184
|
-
disabled: boolean
|
|
185
|
-
onCommit: (value: string) => void
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function SelectField(props: SelectFieldProps): ReactNode {
|
|
189
|
-
const id = useId()
|
|
190
|
-
return (
|
|
191
|
-
<div className={css['field']}>
|
|
192
|
-
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
193
|
-
<select
|
|
194
|
-
id={id}
|
|
195
|
-
className={css['select']}
|
|
196
|
-
value={props.value}
|
|
197
|
-
disabled={props.disabled}
|
|
198
|
-
onChange={(event) => { props.onCommit(event.target.value) }}
|
|
199
|
-
>
|
|
200
|
-
{props.options.map(option => (
|
|
201
|
-
<option key={option.value} value={option.value}>{option.label}</option>
|
|
202
|
-
))}
|
|
203
|
-
</select>
|
|
204
|
-
<p className={css['hint']}>{props.hint}</p>
|
|
205
|
-
</div>
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
interface ToggleFieldProps {
|
|
210
|
-
label: string
|
|
211
|
-
hint: string
|
|
212
|
-
checked: boolean
|
|
213
|
-
disabled: boolean
|
|
214
|
-
onCommit: (checked: boolean) => void
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function ToggleField(props: ToggleFieldProps): ReactNode {
|
|
218
|
-
return (
|
|
219
|
-
<div className={css['field']}>
|
|
220
|
-
<label className={css['toggleRow']}>
|
|
221
|
-
<input
|
|
222
|
-
type="checkbox"
|
|
223
|
-
checked={props.checked}
|
|
224
|
-
disabled={props.disabled}
|
|
225
|
-
onChange={(event) => { props.onCommit(event.target.checked) }}
|
|
226
|
-
/>
|
|
227
|
-
<span className={css['label']}>{props.label}</span>
|
|
228
|
-
</label>
|
|
229
|
-
<p className={css['hint']}>{props.hint}</p>
|
|
230
|
-
</div>
|
|
231
|
-
)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
interface TextFieldProps {
|
|
235
|
-
label: string
|
|
236
|
-
hint: string
|
|
237
|
-
value: string
|
|
238
|
-
placeholder?: string
|
|
239
|
-
disabled: boolean
|
|
240
|
-
onCommit: (value: string) => void
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Text field with a local draft so typing survives a non-echoing beat. */
|
|
244
|
-
function TextField(props: TextFieldProps): ReactNode {
|
|
245
|
-
const id = useId()
|
|
246
|
-
const [draft, setDraft] = useState(props.value)
|
|
247
|
-
useEffect(() => { setDraft(props.value) }, [props.value])
|
|
248
|
-
return (
|
|
249
|
-
<div className={css['field']}>
|
|
250
|
-
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
251
|
-
<input
|
|
252
|
-
id={id}
|
|
253
|
-
className={css['input']}
|
|
254
|
-
type="text"
|
|
255
|
-
value={draft}
|
|
256
|
-
placeholder={props.placeholder ?? ''}
|
|
257
|
-
disabled={props.disabled}
|
|
258
|
-
onChange={(event) => {
|
|
259
|
-
setDraft(event.target.value)
|
|
260
|
-
props.onCommit(event.target.value)
|
|
261
|
-
}}
|
|
262
|
-
/>
|
|
263
|
-
<p className={css['hint']}>{props.hint}</p>
|
|
264
|
-
</div>
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
interface NumberFieldProps {
|
|
269
|
-
label: string
|
|
270
|
-
hint: string
|
|
271
|
-
/** Copy shown while the draft is not an acceptable integer. */
|
|
272
|
-
invalidHint: string
|
|
273
|
-
/** Schema lower bound (host Config schemastery `.min()`). */
|
|
274
|
-
min: number
|
|
275
|
-
value: number
|
|
276
|
-
disabled: boolean
|
|
277
|
-
onCommit: (value: number) => void
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Integer field with a local draft: invalid intermediate text (empty,
|
|
282
|
-
* non-numeric, below the schema minimum) shows the invalid hint and commits
|
|
283
|
-
* nothing, so the staged value can never leave the schema's domain.
|
|
284
|
-
*/
|
|
285
|
-
function NumberField(props: NumberFieldProps): ReactNode {
|
|
286
|
-
const id = useId()
|
|
287
|
-
const [draft, setDraft] = useState(String(props.value))
|
|
288
|
-
const [invalid, setInvalid] = useState(false)
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
setDraft(String(props.value))
|
|
291
|
-
setInvalid(false)
|
|
292
|
-
}, [props.value])
|
|
293
|
-
return (
|
|
294
|
-
<div className={css['field']}>
|
|
295
|
-
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
296
|
-
<input
|
|
297
|
-
id={id}
|
|
298
|
-
className={invalid ? `${css['input']} ${css['inputInvalid']}` : css['input']}
|
|
299
|
-
type="text"
|
|
300
|
-
inputMode="numeric"
|
|
301
|
-
value={draft}
|
|
302
|
-
disabled={props.disabled}
|
|
303
|
-
{...invalid ? { 'aria-invalid': true } : {}}
|
|
304
|
-
onChange={(event) => {
|
|
305
|
-
const text = event.target.value
|
|
306
|
-
setDraft(text)
|
|
307
|
-
const parsed = Number(text)
|
|
308
|
-
const acceptable = text.trim() !== '' && Number.isInteger(parsed) && parsed >= props.min
|
|
309
|
-
setInvalid(!acceptable)
|
|
310
|
-
if (acceptable && parsed !== props.value) props.onCommit(parsed)
|
|
311
|
-
}}
|
|
312
|
-
/>
|
|
313
|
-
{invalid
|
|
314
|
-
? <p className={css['invalidHint']} role="alert">{props.invalidHint}</p>
|
|
315
|
-
: <p className={css['hint']}>{props.hint}</p>}
|
|
316
|
-
</div>
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
191
|
/**
|
|
321
192
|
* Render the Agent Sidecar settings card.
|
|
322
193
|
* @param props - staged values, form state, and the wiring callbacks.
|
|
@@ -334,9 +205,10 @@ export function SettingsCard(props: SettingsCardProps): ReactNode {
|
|
|
334
205
|
: props.daemon?.state === 'failed'
|
|
335
206
|
? t('settings.daemonFailedNote')
|
|
336
207
|
: undefined
|
|
208
|
+
const cardClassName = `${css['card']} ${open ? css['cardOpen'] : ''}`
|
|
337
209
|
|
|
338
210
|
return (
|
|
339
|
-
<li
|
|
211
|
+
<li {...surfaceProps('settings-card', cardClassName)}>
|
|
340
212
|
<button
|
|
341
213
|
type="button"
|
|
342
214
|
className={css['header']}
|
|
@@ -348,8 +220,10 @@ export function SettingsCard(props: SettingsCardProps): ReactNode {
|
|
|
348
220
|
<span className={css['name']}>{title}</span>
|
|
349
221
|
<span className={css['description']}>{t('settings.cardDescription')}</span>
|
|
350
222
|
</span>
|
|
351
|
-
{props.dirty ? <
|
|
352
|
-
<
|
|
223
|
+
{props.dirty ? <Pill className={css['pending']}>{t('settings.unsaved')}</Pill> : null}
|
|
224
|
+
<IconChevronDownOutline14
|
|
225
|
+
className={`${css['chevron']} ${open ? css['chevronOpen'] : ''}`}
|
|
226
|
+
/>
|
|
353
227
|
</button>
|
|
354
228
|
{open
|
|
355
229
|
? (
|
|
@@ -380,13 +254,15 @@ export function SettingsCard(props: SettingsCardProps): ReactNode {
|
|
|
380
254
|
: null}
|
|
381
255
|
{props.daemon.state === 'failed' && props.onDaemonRetry !== undefined
|
|
382
256
|
? (
|
|
383
|
-
<
|
|
257
|
+
<Button
|
|
384
258
|
type="button"
|
|
259
|
+
size="sm"
|
|
260
|
+
variant="outline"
|
|
385
261
|
className={css['retry']}
|
|
386
262
|
onClick={props.onDaemonRetry}
|
|
387
263
|
>
|
|
388
264
|
{t('settings.daemonRetry')}
|
|
389
|
-
</
|
|
265
|
+
</Button>
|
|
390
266
|
)
|
|
391
267
|
: null}
|
|
392
268
|
</div>
|
|
@@ -540,22 +416,24 @@ export function SettingsCard(props: SettingsCardProps): ReactNode {
|
|
|
540
416
|
{props.saveFailed === true
|
|
541
417
|
? <p className={css['failed']} role="status">{t('settings.saveFailed')}</p>
|
|
542
418
|
: <span className={css['spacer']} />}
|
|
543
|
-
<
|
|
419
|
+
<Button
|
|
544
420
|
type="button"
|
|
545
|
-
|
|
421
|
+
size="sm"
|
|
422
|
+
variant="outline"
|
|
546
423
|
disabled={!props.dirty || props.saving}
|
|
547
424
|
onClick={props.onDiscard}
|
|
548
425
|
>
|
|
549
426
|
{t('settings.discard')}
|
|
550
|
-
</
|
|
551
|
-
<
|
|
427
|
+
</Button>
|
|
428
|
+
<Button
|
|
552
429
|
type="button"
|
|
553
|
-
|
|
430
|
+
size="sm"
|
|
431
|
+
variant="primary"
|
|
554
432
|
disabled={!props.dirty || props.saving || !props.writable}
|
|
555
433
|
onClick={props.onSave}
|
|
556
434
|
>
|
|
557
435
|
{t(props.saving ? 'settings.saving' : 'settings.save')}
|
|
558
|
-
</
|
|
436
|
+
</Button>
|
|
559
437
|
</div>
|
|
560
438
|
</div>
|
|
561
439
|
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
import { useEffect, useId, useState } from 'react'
|
|
3
|
+
import type { ReactElement } from 'react'
|
|
4
|
+
import css from './settings-card.module.css'
|
|
5
|
+
|
|
6
|
+
interface FieldCopy {
|
|
7
|
+
label: string
|
|
8
|
+
hint: string
|
|
9
|
+
disabled: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SelectFieldProps extends FieldCopy {
|
|
13
|
+
value: string
|
|
14
|
+
options: readonly { value: string, label: string }[]
|
|
15
|
+
onCommit: (value: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SelectField(props: SelectFieldProps): ReactElement {
|
|
19
|
+
const id = useId()
|
|
20
|
+
return (
|
|
21
|
+
<div className={css['field']}>
|
|
22
|
+
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
23
|
+
<select
|
|
24
|
+
id={id}
|
|
25
|
+
className={css['select']}
|
|
26
|
+
value={props.value}
|
|
27
|
+
disabled={props.disabled}
|
|
28
|
+
onChange={(event) => { props.onCommit(event.target.value) }}
|
|
29
|
+
>
|
|
30
|
+
{props.options.map(option => (
|
|
31
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
32
|
+
))}
|
|
33
|
+
</select>
|
|
34
|
+
<p className={css['hint']}>{props.hint}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ToggleFieldProps extends FieldCopy {
|
|
40
|
+
checked: boolean
|
|
41
|
+
onCommit: (checked: boolean) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ToggleField(props: ToggleFieldProps): ReactElement {
|
|
45
|
+
return (
|
|
46
|
+
<div className={css['field']}>
|
|
47
|
+
<label className={css['toggleRow']}>
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
checked={props.checked}
|
|
51
|
+
disabled={props.disabled}
|
|
52
|
+
onChange={(event) => { props.onCommit(event.target.checked) }}
|
|
53
|
+
/>
|
|
54
|
+
<span className={css['label']}>{props.label}</span>
|
|
55
|
+
</label>
|
|
56
|
+
<p className={css['hint']}>{props.hint}</p>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface TextFieldProps extends FieldCopy {
|
|
62
|
+
value: string
|
|
63
|
+
placeholder?: string
|
|
64
|
+
onCommit: (value: string) => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function TextField(props: TextFieldProps): ReactElement {
|
|
68
|
+
const id = useId()
|
|
69
|
+
const [draft, setDraft] = useState(props.value)
|
|
70
|
+
useEffect(() => { setDraft(props.value) }, [props.value])
|
|
71
|
+
return (
|
|
72
|
+
<div className={css['field']}>
|
|
73
|
+
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
74
|
+
<Input
|
|
75
|
+
id={id}
|
|
76
|
+
className={css['input']}
|
|
77
|
+
type="text"
|
|
78
|
+
value={draft}
|
|
79
|
+
placeholder={props.placeholder ?? ''}
|
|
80
|
+
disabled={props.disabled}
|
|
81
|
+
onChange={(event) => {
|
|
82
|
+
setDraft(event.target.value)
|
|
83
|
+
props.onCommit(event.target.value)
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
<p className={css['hint']}>{props.hint}</p>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface NumberFieldProps extends FieldCopy {
|
|
92
|
+
invalidHint: string
|
|
93
|
+
min: number
|
|
94
|
+
value: number
|
|
95
|
+
onCommit: (value: number) => void
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function NumberField(props: NumberFieldProps): ReactElement {
|
|
99
|
+
const id = useId()
|
|
100
|
+
const [draft, setDraft] = useState(String(props.value))
|
|
101
|
+
const [invalid, setInvalid] = useState(false)
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
setDraft(String(props.value))
|
|
104
|
+
setInvalid(false)
|
|
105
|
+
}, [props.value])
|
|
106
|
+
return (
|
|
107
|
+
<div className={css['field']}>
|
|
108
|
+
<label className={css['label']} htmlFor={id}>{props.label}</label>
|
|
109
|
+
<Input
|
|
110
|
+
id={id}
|
|
111
|
+
className={`${css['input']} ${invalid ? css['inputInvalid'] : ''}`}
|
|
112
|
+
type="text"
|
|
113
|
+
inputMode="numeric"
|
|
114
|
+
value={draft}
|
|
115
|
+
disabled={props.disabled}
|
|
116
|
+
{...invalid ? { 'aria-invalid': true } : {}}
|
|
117
|
+
onChange={(event) => {
|
|
118
|
+
const text = event.target.value
|
|
119
|
+
const parsed = Number(text)
|
|
120
|
+
const acceptable = text.trim() !== '' && Number.isInteger(parsed) && parsed >= props.min
|
|
121
|
+
setDraft(text)
|
|
122
|
+
setInvalid(!acceptable)
|
|
123
|
+
if (acceptable && parsed !== props.value) props.onCommit(parsed)
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
<p className={invalid ? css['invalidHint'] : css['hint']} {...invalid ? { role: 'alert' } : {}}>
|
|
127
|
+
{invalid ? props.invalidHint : props.hint}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|