@shendeguize/dsh-agent-sidecar 0.1.0 → 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.
Files changed (58) hide show
  1. package/README.md +78 -11
  2. package/lib/client.js +4708 -2932
  3. package/lib/client.js.map +1 -1
  4. package/package.json +3 -1
  5. package/src/client/analysis/AnalysisPanel.tsx +19 -27
  6. package/src/client/analysis/analysis.module.css +36 -97
  7. package/src/client/board/Board.tsx +255 -48
  8. package/src/client/board/board.module.css +113 -92
  9. package/src/client/board/logic.ts +99 -21
  10. package/src/client/board/project-view-logic.ts +27 -34
  11. package/src/client/board/project-view.module.css +46 -83
  12. package/src/client/board/project-view.tsx +50 -8
  13. package/src/client/board/strings.ts +70 -85
  14. package/src/client/commands.ts +30 -15
  15. package/src/client/controller.ts +27 -7
  16. package/src/client/detail/SessionDetail.tsx +234 -37
  17. package/src/client/detail/detail.module.css +100 -153
  18. package/src/client/detail/logic.ts +220 -29
  19. package/src/client/detail/strings.ts +66 -77
  20. package/src/client/detail-glue.ts +33 -0
  21. package/src/client/detail-view.module.css +43 -35
  22. package/src/client/detail-view.tsx +138 -118
  23. package/src/client/dsh-tools/LineageTree.tsx +22 -13
  24. package/src/client/dsh-tools/SearchPanel.tsx +18 -11
  25. package/src/client/dsh-tools/dsh-tools.module.css +53 -140
  26. package/src/client/dsh-tools/strings.ts +42 -77
  27. package/src/client/index.ts +285 -124
  28. package/src/client/inject/InjectPanel.tsx +81 -61
  29. package/src/client/inject/inject.module.css +97 -197
  30. package/src/client/inject/logic.ts +38 -0
  31. package/src/client/lifecycle/handoff.ts +83 -0
  32. package/src/client/locales/en.ts +91 -4
  33. package/src/client/locales/host.ts +131 -0
  34. package/src/client/locales/index.ts +196 -8
  35. package/src/client/locales/react.ts +10 -0
  36. package/src/client/locales/view.ts +38 -0
  37. package/src/client/locales/zh.ts +200 -125
  38. package/src/client/mount.tsx +75 -41
  39. package/src/client/navigation/CenterOverlay.tsx +40 -0
  40. package/src/client/navigation/center-overlay.module.css +51 -0
  41. package/src/client/navigation/center.ts +45 -0
  42. package/src/client/navigation/modal-isolation.ts +152 -0
  43. package/src/client/navigation/modal-surface-anchor.ts +72 -0
  44. package/src/client/navigation/sidebar-entry.module.css +73 -0
  45. package/src/client/navigation/sidebar-entry.ts +309 -0
  46. package/src/client/primitives/StaticPill.tsx +21 -0
  47. package/src/client/settings-card.module.css +35 -119
  48. package/src/client/settings-card.tsx +31 -153
  49. package/src/client/settings-fields.tsx +131 -0
  50. package/src/client/sidebar/SidebarTab.tsx +156 -0
  51. package/src/client/sidebar/model.ts +65 -0
  52. package/src/client/sidebar/sidebar-tab.module.css +118 -0
  53. package/src/client/sidebar-tab.tsx +46 -320
  54. package/src/client/theme/agsc.module.css +31 -0
  55. package/src/client/theme/parts.ts +56 -0
  56. package/src/client/ui-integration.ts +86 -0
  57. package/src/client/widget.tsx +42 -16
  58. package/src/client/inject/overlay.module.css +0 -22
@@ -1,38 +1,18 @@
1
- /**
2
- * Inject panel (design §5.1 view 3): the two-phase confirmation UI for
3
- * message injection. Fully controlled and presentational — the component
4
- * does NO data fetching; the integration layer supplies `onPrepare` /
5
- * `onExecute` callbacks that speak to `POST <prefix>/action` and resolve
6
- * with either the wire success shape or the data layer's normalized
7
- * ApiError (matched structurally, never imported).
8
- *
9
- * Three zones, driven by the pure reducer in ./logic.ts:
10
- *
11
- * 1. editor — message textarea + live UTF-8 byte counter, queue/steer mode
12
- * radios, target row, cursor-cli process-list warning (§4.d/S7) and the
13
- * audit-fingerprint note (§5.3);
14
- * 2. confirm — the prepare plan (live target-status snapshot + message
15
- * digest), the 60s confirmToken countdown, and the deliberately
16
- * restrained danger button;
17
- * 3. result — delivered / failed (re-prepare offered) / unknown (terminal:
18
- * the reducer itself refuses a reset, so no retry affordance can exist —
19
- * S6 — and the copy points at the session to verify).
20
- *
21
- * `capability.inject === false` disables the whole panel with the
22
- * "enable injection in Settings" note.
23
- */
24
-
1
+ import { Button, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
25
2
  import { useEffect, useId, useReducer, useState } from 'react'
26
3
  import type { ReactNode } from 'react'
27
4
  import { t as defaultT } from '../locales/index.ts'
28
5
  import type { SidecarLocaleKey } from '../locales/index.ts'
6
+ import { surfaceProps } from '../theme/parts.ts'
29
7
  import css from './inject.module.css'
30
8
  import {
31
9
  byteUsage,
32
10
  classifyExecuteResponse,
11
+ classifyPanelKey,
33
12
  classifyPrepareResponse,
34
13
  deriveEditorGate,
35
14
  errorCopy,
15
+ isDeliveredResult,
36
16
  initialPanelState,
37
17
  messageInvalidCopy,
38
18
  MODE_COPY,
@@ -82,6 +62,12 @@ export interface InjectPanelProps {
82
62
  onExecute(req: PanelExecuteRequest): Promise<InjectResultView | ApiErrorLike>
83
63
  /** Close/dismiss the panel (the hosting modal owns the lifecycle). */
84
64
  onClose?(): void
65
+ /**
66
+ * UX-05 observation loop: when given, the delivered result page offers a
67
+ * 「开启监听观察反应」 button (the host typically enables listen mode on
68
+ * the detail timeline and closes the panel).
69
+ */
70
+ onObserve?(): void
85
71
  /** Clock injection for the token countdown; defaults to Date.now. */
86
72
  nowMs?: () => number
87
73
  /** Locale seat override; defaults to the module-local table. */
@@ -95,10 +81,6 @@ function renderCopy(t: InjectTranslate, copy: CopyRef): string {
95
81
  return t(copy.key, copy.params)
96
82
  }
97
83
 
98
- // ---------------------------------------------------------------------------
99
- // Zone building blocks.
100
- // ---------------------------------------------------------------------------
101
-
102
84
  interface WarningsProps {
103
85
  t: InjectTranslate
104
86
  agent: string | null
@@ -133,7 +115,7 @@ function PlanBox(props: PlanBoxProps): ReactNode {
133
115
  <div className={css['planRow']}>
134
116
  <span className={css['planKey']}>{t('inject.planTargetLabel')}</span>
135
117
  <span className={css['planValue']}>
136
- <span className={css['agentTag']}>{plan.target.agent}</span>
118
+ <Pill className={css['agentTag']}>{plan.target.agent}</Pill>
137
119
  <span className={css['planTitle']} title={plan.target.sessionId}>{targetName}</span>
138
120
  </span>
139
121
  </div>
@@ -157,15 +139,6 @@ function PlanBox(props: PlanBoxProps): ReactNode {
157
139
  )
158
140
  }
159
141
 
160
- // ---------------------------------------------------------------------------
161
- // Panel.
162
- // ---------------------------------------------------------------------------
163
-
164
- /**
165
- * Render the two-phase inject panel.
166
- * @param props - capability, target, and the integration callbacks.
167
- * @returns the panel.
168
- */
169
142
  export function InjectPanel(props: InjectPanelProps): ReactNode {
170
143
  const t = props.t ?? defaultT
171
144
  const now = props.nowMs ?? Date.now
@@ -240,18 +213,44 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
240
213
  dispatch(event)
241
214
  }
242
215
 
216
+ // Keyboard affordances (UX-10): Esc closes in every zone; Cmd/Ctrl+Enter
217
+ // triggers PREPARE only (classifyPanelKey refuses everything else, so no
218
+ // keyboard path can shortcut the explicit confirm click).
219
+ const handleKeyDown = (event: {
220
+ key: string
221
+ metaKey: boolean
222
+ ctrlKey: boolean
223
+ stopPropagation(): void
224
+ preventDefault(): void
225
+ }): void => {
226
+ const intent = classifyPanelKey({
227
+ key: event.key,
228
+ metaKey: event.metaKey,
229
+ ctrlKey: event.ctrlKey,
230
+ phase: state.phase,
231
+ canPrepare: gate.canPrepare,
232
+ })
233
+ if (intent === 'close' && props.onClose !== undefined) {
234
+ event.stopPropagation()
235
+ props.onClose()
236
+ } else if (intent === 'prepare') {
237
+ event.preventDefault()
238
+ void handlePrepare()
239
+ }
240
+ }
241
+
242
+ const panelSurface = surfaceProps('inject-panel', css['panel'])
243
243
  const closeButton = props.onClose !== undefined
244
244
  ? (
245
- <button type="button" className={css['btn']} onClick={props.onClose}>
245
+ <Button type="button" size="sm" variant="outline" onClick={props.onClose}>
246
246
  {t('inject.close')}
247
- </button>
247
+ </Button>
248
248
  )
249
249
  : null
250
250
 
251
- // ── whole-panel gate: injection capability off ─────────────────────────
252
251
  if (!props.capability.inject) {
253
252
  return (
254
- <section className={css['panel']} aria-label={t('inject.title')}>
253
+ <section {...panelSurface} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
255
254
  <header className={css['header']}>
256
255
  <h3 className={css['title']}>{t('inject.title')}</h3>
257
256
  </header>
@@ -265,7 +264,6 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
265
264
  )
266
265
  }
267
266
 
268
- // ── zone 3: result ──────────────────────────────────────────────────────
269
267
  if (state.phase === 'result') {
270
268
  const { result } = state
271
269
  const actions = resultActions(result.outcome)
@@ -275,7 +273,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
275
273
  ? css['resultFail']
276
274
  : css['resultUnknown']
277
275
  return (
278
- <section className={css['panel']} aria-label={t('inject.title')}>
276
+ <section {...panelSurface} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
279
277
  <header className={css['header']}>
280
278
  <h3 className={css['title']}>{t('inject.title')}</h3>
281
279
  </header>
@@ -293,26 +291,41 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
293
291
  : null}
294
292
  <div className={css['footer']}>
295
293
  {closeButton}
294
+ {isDeliveredResult(result) && props.onObserve !== undefined
295
+ ? (
296
+ <Button
297
+ type="button"
298
+ size="sm"
299
+ variant="primary"
300
+ onClick={props.onObserve}
301
+ data-testid="agent-sidecar-inject-observe"
302
+ >
303
+ {t('inject.observeListen')}
304
+ </Button>
305
+ )
306
+ : null}
296
307
  {actions.canReprepare
297
308
  ? (
298
- <button
309
+ <Button
299
310
  type="button"
300
- className={css['btnPrimary']}
311
+ size="sm"
312
+ variant="primary"
301
313
  onClick={() => { dispatch({ type: 'RESET' }) }}
302
314
  >
303
315
  {t('inject.reprepare')}
304
- </button>
316
+ </Button>
305
317
  )
306
318
  : null}
307
319
  {result.outcome === 'delivered' && props.onClose === undefined
308
320
  ? (
309
- <button
321
+ <Button
310
322
  type="button"
311
- className={css['btnPrimary']}
323
+ size="sm"
324
+ variant="primary"
312
325
  onClick={() => { dispatch({ type: 'RESET' }) }}
313
326
  >
314
327
  {t('inject.done')}
315
- </button>
328
+ </Button>
316
329
  )
317
330
  : null}
318
331
  </div>
@@ -321,14 +334,17 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
321
334
  )
322
335
  }
323
336
 
324
- // ── zone 2: confirm / executing ─────────────────────────────────────────
325
337
  if (state.phase === 'confirm' || state.phase === 'executing') {
326
338
  const executing = state.phase === 'executing'
327
339
  const countdown = state.phase === 'confirm'
328
340
  ? tokenCountdown(state.expiresAt, clock)
329
341
  : null
330
342
  return (
331
- <section className={css['panel']} aria-label={t('inject.confirmTitle')}>
343
+ <section
344
+ {...panelSurface}
345
+ aria-label={t('inject.confirmTitle')}
346
+ onKeyDown={handleKeyDown}
347
+ >
332
348
  <header className={css['header']}>
333
349
  <h3 className={css['title']}>{t('inject.confirmTitle')}</h3>
334
350
  </header>
@@ -343,14 +359,15 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
343
359
  )
344
360
  : null}
345
361
  <div className={css['footer']}>
346
- <button
362
+ <Button
347
363
  type="button"
348
- className={css['btn']}
364
+ size="sm"
365
+ variant="outline"
349
366
  disabled={executing}
350
367
  onClick={() => { dispatch({ type: 'CANCEL' }) }}
351
368
  >
352
369
  {t('inject.cancel')}
353
- </button>
370
+ </Button>
354
371
  <button
355
372
  type="button"
356
373
  className={css['btnDanger']}
@@ -365,7 +382,6 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
365
382
  )
366
383
  }
367
384
 
368
- // ── zone 1: editor (idle / preparing) ───────────────────────────────────
369
385
  const preparing = state.phase === 'preparing'
370
386
  const canEdit = props.target !== null && !preparing
371
387
  const usage = byteUsage(validation.bytes)
@@ -373,7 +389,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
373
389
  const showInvalid = !validation.ok && validation.code !== 'empty'
374
390
 
375
391
  return (
376
- <section className={css['panel']} aria-label={t('inject.title')}>
392
+ <section {...panelSurface} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
377
393
  <header className={css['header']}>
378
394
  <h3 className={css['title']}>{t('inject.title')}</h3>
379
395
  </header>
@@ -397,7 +413,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
397
413
  {props.target !== null
398
414
  ? (
399
415
  <span className={css['planValue']}>
400
- <span className={css['agentTag']}>{props.target.agent}</span>
416
+ <Pill className={css['agentTag']}>{props.target.agent}</Pill>
401
417
  <span className={css['planTitle']} title={props.target.sessionId}>
402
418
  {props.target.title !== undefined && props.target.title !== ''
403
419
  ? props.target.title
@@ -419,6 +435,9 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
419
435
  placeholder={t('inject.messagePlaceholder')}
420
436
  disabled={!canEdit}
421
437
  rows={5}
438
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the panel is
439
+ // a modal whose single task starts in this field (UX-10).
440
+ autoFocus
422
441
  onChange={(event) => { setDraft(event.target.value) }}
423
442
  />
424
443
  <div className={css['byteRow']}>
@@ -467,14 +486,15 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
467
486
 
468
487
  <div className={css['footer']}>
469
488
  {closeButton}
470
- <button
489
+ <Button
471
490
  type="button"
472
- className={css['btnPrimary']}
491
+ size="sm"
492
+ variant="primary"
473
493
  disabled={!gate.canPrepare}
474
494
  onClick={() => { void handlePrepare() }}
475
495
  >
476
496
  {t(preparing ? 'inject.preparing' : 'inject.prepare')}
477
- </button>
497
+ </Button>
478
498
  </div>
479
499
  </div>
480
500
  </section>