@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.
Files changed (53) hide show
  1. package/README.md +78 -11
  2. package/lib/client.js +8659 -7589
  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 +115 -61
  8. package/src/client/board/board.module.css +72 -151
  9. package/src/client/board/project-view-logic.ts +21 -34
  10. package/src/client/board/project-view.module.css +41 -96
  11. package/src/client/board/project-view.tsx +29 -13
  12. package/src/client/board/strings.ts +68 -107
  13. package/src/client/commands.ts +30 -15
  14. package/src/client/detail/SessionDetail.tsx +92 -58
  15. package/src/client/detail/detail.module.css +62 -218
  16. package/src/client/detail/strings.ts +64 -88
  17. package/src/client/detail-view.module.css +30 -55
  18. package/src/client/detail-view.tsx +80 -108
  19. package/src/client/dsh-tools/LineageTree.tsx +22 -13
  20. package/src/client/dsh-tools/SearchPanel.tsx +18 -11
  21. package/src/client/dsh-tools/dsh-tools.module.css +50 -139
  22. package/src/client/dsh-tools/strings.ts +42 -77
  23. package/src/client/index.ts +285 -124
  24. package/src/client/inject/InjectPanel.tsx +31 -64
  25. package/src/client/inject/inject.module.css +97 -198
  26. package/src/client/lifecycle/handoff.ts +83 -0
  27. package/src/client/locales/en.ts +74 -2
  28. package/src/client/locales/host.ts +131 -0
  29. package/src/client/locales/index.ts +196 -8
  30. package/src/client/locales/react.ts +10 -0
  31. package/src/client/locales/view.ts +38 -0
  32. package/src/client/locales/zh.ts +194 -134
  33. package/src/client/mount.tsx +72 -38
  34. package/src/client/navigation/CenterOverlay.tsx +40 -0
  35. package/src/client/navigation/center-overlay.module.css +51 -0
  36. package/src/client/navigation/center.ts +45 -0
  37. package/src/client/navigation/modal-isolation.ts +152 -0
  38. package/src/client/navigation/modal-surface-anchor.ts +72 -0
  39. package/src/client/navigation/sidebar-entry.module.css +73 -0
  40. package/src/client/navigation/sidebar-entry.ts +309 -0
  41. package/src/client/primitives/StaticPill.tsx +21 -0
  42. package/src/client/settings-card.module.css +35 -119
  43. package/src/client/settings-card.tsx +31 -153
  44. package/src/client/settings-fields.tsx +131 -0
  45. package/src/client/sidebar/SidebarTab.tsx +156 -0
  46. package/src/client/sidebar/model.ts +65 -0
  47. package/src/client/sidebar/sidebar-tab.module.css +118 -0
  48. package/src/client/sidebar-tab.tsx +46 -320
  49. package/src/client/theme/agsc.module.css +31 -0
  50. package/src/client/theme/parts.ts +56 -0
  51. package/src/client/ui-integration.ts +86 -0
  52. package/src/client/widget.tsx +9 -8
  53. package/src/client/inject/overlay.module.css +0 -22
@@ -1,11 +1,3 @@
1
- /*
2
- * Detail-view container + board-tab view-switcher chrome (T5.10b, design
3
- * §5.1): every color rides a --dsw-alias-* design token with a neutral
4
- * fallback. Same posture as board.module.css / dsh-tools.module.css.
5
- */
6
-
7
- /* ------------------------------------------------ board-tab view switcher */
8
-
9
1
  .switcherBar {
10
2
  display: flex;
11
3
  align-items: center;
@@ -19,31 +11,26 @@
19
11
  border-radius: 999px;
20
12
  border: 1px solid transparent;
21
13
  background: transparent;
22
- color: var(--dsw-alias-label-secondary, #57606a);
14
+ color: var(--dsw-alias-label-secondary);
23
15
  cursor: pointer;
24
16
  }
25
17
 
26
18
  .switcherButton:hover {
27
- background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
19
+ background: var(--dsw-alias-bg-layer-1);
28
20
  }
29
21
 
30
22
  .switcherButton[data-active='true'] {
31
23
  font-weight: 600;
32
- color: var(--dsw-alias-label-primary, #1f2328);
33
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
34
- border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
24
+ color: var(--dsw-alias-label-primary);
25
+ background: var(--dsw-alias-bg-layer-2);
26
+ border-color: var(--dsw-alias-border-l2);
35
27
  }
36
28
 
37
- /* --------------------------------------------------- detail-view chrome */
38
-
39
29
  .detailRoot {
40
30
  display: flex;
41
31
  flex-direction: column;
42
32
  gap: 12px;
43
33
  min-width: 0;
44
- /* Own the scroll like the board/project roots do (UX-12): without a
45
- * bounded height the inner timeline's overflow-y can never engage, and
46
- * the UX-04 landing / listen-mode tail pinning would silently no-op. */
47
34
  height: 100%;
48
35
  overflow-y: auto;
49
36
  box-sizing: border-box;
@@ -54,59 +41,47 @@
54
41
  display: flex;
55
42
  align-items: center;
56
43
  gap: 8px;
44
+ flex-wrap: wrap;
57
45
  }
58
46
 
59
- .actionButton {
60
- font-size: 12px;
61
- padding: 3px 12px;
62
- border-radius: 6px;
63
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
64
- background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
65
- color: var(--dsw-alias-label-primary, #1f2328);
66
- cursor: pointer;
47
+ .analysisDisabledReason {
48
+ max-width: 360px;
49
+ font-size: 11px;
50
+ line-height: 16px;
51
+ color: var(--agsc-fg-secondary);
67
52
  }
68
53
 
69
- .actionButton:hover {
70
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
71
- }
72
-
73
- .actionButton:disabled {
74
- opacity: 0.5;
75
- cursor: not-allowed;
76
- }
77
-
78
- /* dsh deep-query tools: collapsible segment between the actions row and
79
- * the timeline (UX-09 — previously buried below a possibly very long
80
- * list, outside the viewport on long sessions). */
81
54
  .toolsSection {
82
55
  display: flex;
83
56
  flex-direction: column;
84
57
  gap: 16px;
85
- border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
86
- border-radius: 8px;
58
+ border: 1px solid var(--agsc-border);
59
+ border-radius: var(--agsc-radius-control);
87
60
  padding: 8px 12px;
88
61
  }
89
62
 
90
63
  .toolsToggle {
91
64
  align-self: flex-start;
92
- display: inline-flex;
93
- align-items: center;
94
- gap: 6px;
95
- font-size: 12px;
96
- font-family: inherit;
97
- color: var(--dsw-alias-label-secondary, #57606a);
98
- background: transparent;
99
- border: none;
100
- padding: 0;
101
- cursor: pointer;
102
- }
103
-
104
- .toolsToggle:hover {
105
- color: var(--dsw-alias-label-primary, #1f2328);
106
65
  }
107
66
 
108
67
  .toolsToggleGlyph {
109
68
  flex: none;
110
69
  font-size: 10px;
111
- color: var(--dsw-alias-label-tertiary, #6e7781);
70
+ color: var(--agsc-fg-dimmed);
71
+ }
72
+
73
+ .injectDialog {
74
+ gap: 0;
75
+ width: min(560px, 100%);
76
+ max-height: min(85vh, 720px);
77
+ padding: 0;
78
+ overflow: auto;
79
+ }
80
+
81
+ .injectDialog > [data-dsh-part='inject-panel'] {
82
+ width: 100%;
83
+ max-width: none;
84
+ border: 0;
85
+ border-radius: inherit;
86
+ background: transparent;
112
87
  }
@@ -1,27 +1,16 @@
1
1
  /**
2
- * Session-detail container (T5.10b, design §5.1 view 2): the full-tab
3
- * detail surface opened by clicking a session card on the board or in the
4
- * project view. Composes the M3 presentational components over their glue
5
- * stores:
2
+ * Session-detail React container. It composes the timeline, lineage,
3
+ * search, analysis, and optional injection surfaces over stores supplied
4
+ * by {@link DetailUiPort}.
6
5
  *
7
- * - SessionDetail (timeline) DetailStore (fetchSessionDetail +
8
- * fetchTimelinePage pagination + SSE-triggered listen refetch);
9
- * - action row: 注入 (M2 InjectPanel as a modal, reused verbatim) and
10
- * AI 分析 (AnalysisPanel over AnalysisStore; disabled with an honest
11
- * hint while `analysis.enabled` is off);
12
- * - dsh 会话专属区: LineageTree ← the DetailStore lineage slice (non-dsh
13
- * sessions degrade client-side, no dialing) and SearchPanel ←
14
- * SearchStore (full-text or filter-only degradation; a result click
15
- * navigates the detail view to that session).
16
- *
17
- * The stores live in component state (one set per opened session id — the
18
- * owner keys this component by session id) and are disposed on unmount.
19
- * SSE coupling: the controller's subscribe seam notifies the DetailStore
20
- * on every state frame (header refresh + listen-mode refetch trigger).
6
+ * Stores are scoped to one opened session and disposed on unmount. Each
7
+ * controller state frame refreshes the header hint and drives the
8
+ * detail store's bounded listen-mode refetch.
21
9
  *
22
10
  * @module
23
11
  */
24
12
 
13
+ import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
25
14
  import { useEffect, useState, useSyncExternalStore } from 'react'
26
15
  import type { ReactElement } from 'react'
27
16
  import { SessionDetail } from './detail/SessionDetail.tsx'
@@ -29,58 +18,28 @@ import { LineageTree } from './dsh-tools/LineageTree.tsx'
29
18
  import { SearchPanel } from './dsh-tools/SearchPanel.tsx'
30
19
  import { AnalysisPanel } from './analysis/AnalysisPanel.tsx'
31
20
  import { InjectPanel } from './inject/InjectPanel.tsx'
32
- import { DetailStore, findCardHint, type DetailHeaderHint } from './detail-glue.ts'
33
- import { SearchStore } from './search-glue.ts'
34
- import { AnalysisStore } from './analysis-glue.ts'
35
- import { ProjectsStore } from './project-glue.ts'
21
+ import { findCardHint, type DetailHeaderHint } from './detail-glue.ts'
36
22
  import type { SidecarController } from './controller.ts'
37
- import { isDeliveredResult, type InjectMode } from './inject/logic.ts'
23
+ import { isDeliveredResult } from './inject/logic.ts'
38
24
  import type { InjectActions } from './inject-glue.ts'
25
+ import type {
26
+ AnalysisStorePort,
27
+ DetailStorePort,
28
+ DetailUiPort,
29
+ SearchStorePort,
30
+ } from './ui-integration.ts'
39
31
  import { t } from './locales/index.ts'
32
+ import { surfaceProps } from './theme/parts.ts'
40
33
  import css from './detail-view.module.css'
41
- import overlay from './inject/overlay.module.css'
42
-
43
- /**
44
- * Everything the integrated M3 surfaces need from the entry (index.ts
45
- * builds one; mount.tsx and this container share it). Store factories are
46
- * seams so tests/materialization can substitute injected transports.
47
- */
48
- export interface SidecarUiIntegration {
49
- /** M2 injection wiring; absent = injection not assembled (entry hidden). */
50
- inject?: {
51
- actions: InjectActions
52
- getDefaultMode: () => InjectMode
53
- }
54
- /** Live `analysis.enabled` reader (settings scope box; default false). */
55
- getAnalysisEnabled: () => boolean
56
- createDetailStore: (sessionId: string, hint: DetailHeaderHint | null) => DetailStore
57
- createSearchStore: () => SearchStore
58
- createAnalysisStore: () => AnalysisStore
59
- createProjectsStore: () => ProjectsStore
60
- }
61
34
 
62
- /** Default integration over the real transports (analysis off until read). */
63
- export function createDefaultIntegration(
64
- base: Omit<
65
- SidecarUiIntegration,
66
- 'createDetailStore' | 'createSearchStore' | 'createAnalysisStore' | 'createProjectsStore'
67
- >,
68
- ): SidecarUiIntegration {
69
- return {
70
- ...base,
71
- createDetailStore: (sessionId, hint) => new DetailStore(sessionId, { hint }),
72
- createSearchStore: () => new SearchStore(),
73
- createAnalysisStore: () => new AnalysisStore(),
74
- createProjectsStore: () => new ProjectsStore(),
75
- }
76
- }
35
+ const ANALYSIS_DISABLED_REASON_ID = 'agent-sidecar-analysis-disabled-reason'
77
36
 
78
37
  export interface SidecarDetailViewProps {
79
38
  sessionId: string
80
39
  /** Header seed from the opening surface (board card / project row). */
81
40
  hint: DetailHeaderHint | null
82
41
  controller: SidecarController
83
- integration: SidecarUiIntegration
42
+ integration: DetailUiPort
84
43
  onClose: () => void
85
44
  /** Provenance/search jump: navigate the detail view to another session. */
86
45
  onSelectSession: (sessionId: string) => void
@@ -92,9 +51,11 @@ export interface SidecarDetailViewProps {
92
51
  */
93
52
  export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
94
53
  const { controller, integration, sessionId } = props
95
- const [detailStore] = useState(() => integration.createDetailStore(sessionId, props.hint))
96
- const [searchStore] = useState(() => integration.createSearchStore())
97
- const [analysisStore] = useState(() => integration.createAnalysisStore())
54
+ const [detailStore] = useState<DetailStorePort>(
55
+ () => integration.createDetailStore(sessionId, props.hint),
56
+ )
57
+ const [searchStore] = useState<SearchStorePort>(() => integration.createSearchStore())
58
+ const [analysisStore] = useState<AnalysisStorePort>(() => integration.createAnalysisStore())
98
59
  const [injectOpen, setInjectOpen] = useState(false)
99
60
  const [analysisOpen, setAnalysisOpen] = useState(false)
100
61
  const [toolsOpen, setToolsOpen] = useState(false)
@@ -127,15 +88,14 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
127
88
  )
128
89
 
129
90
  const analysisEnabled = integration.getAnalysisEnabled()
91
+ const analysisDisabledHint =
92
+ analysisEnabled ? undefined : t('detail.actions.analyzeDisabledHint')
130
93
  const injectIntegration = props.integration.inject
131
94
  const closeInject = (): void => { setInjectOpen(false) }
132
95
  const title = detail.header.title.trim()
133
96
 
134
- // UX-05 observation loop, part 1: a delivered execute refetches the
135
- // newest timeline window at once, so closing the panel never lands on a
136
- // stale pre-injection timeline. Wraps (not replaces) the integration
137
- // callback — the two-phase flow and the owner's onDelivered hook (board
138
- // snapshot refresh) stay untouched.
97
+ // A delivered execute refreshes the newest timeline while preserving
98
+ // the integration-owned two-phase flow and delivery callback.
139
99
  const injectActions: InjectActions | undefined =
140
100
  injectIntegration === undefined
141
101
  ? undefined
@@ -148,43 +108,55 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
148
108
  },
149
109
  }
150
110
 
151
- // UX-05 part 2: the delivered result page offers「开启监听观察反应」—
152
- // flip listen mode on (if off) and hand the view back to the timeline.
153
111
  const observeReaction = (): void => {
154
112
  if (!detailStore.getState().listening) detailStore.toggleListen()
155
113
  closeInject()
156
114
  }
157
115
 
158
116
  return (
159
- <div className={css['detailRoot']} data-testid="agent-sidecar-detail-view">
117
+ <div
118
+ {...surfaceProps('detail', css['detailRoot'])}
119
+ data-testid="agent-sidecar-detail-view"
120
+ >
160
121
  <div className={css['actionsRow']}>
161
122
  {injectIntegration !== undefined && (
162
- <button
163
- type="button"
164
- className={css['actionButton']}
123
+ <Button
124
+ size="sm"
125
+ variant="outline"
165
126
  onClick={() => { setInjectOpen(true) }}
166
127
  data-testid="agent-sidecar-detail-inject"
167
128
  >
168
129
  {t('detail.actions.inject')}
169
- </button>
130
+ </Button>
170
131
  )}
171
- <button
172
- type="button"
173
- className={css['actionButton']}
132
+ <Button
133
+ size="sm"
134
+ variant="outline"
174
135
  disabled={!analysisEnabled}
175
- title={analysisEnabled ? undefined : t('detail.actions.analyzeDisabledHint')}
136
+ title={analysisDisabledHint}
137
+ aria-describedby={analysisEnabled ? undefined : ANALYSIS_DISABLED_REASON_ID}
176
138
  onClick={() => { setAnalysisOpen(true) }}
177
139
  data-testid="agent-sidecar-detail-analyze"
178
140
  >
179
141
  {t('detail.actions.analyze')}
180
- </button>
142
+ </Button>
143
+ {!analysisEnabled && (
144
+ <span
145
+ id={ANALYSIS_DISABLED_REASON_ID}
146
+ className={css['analysisDisabledReason']}
147
+ >
148
+ {analysisDisabledHint}
149
+ </span>
150
+ )}
181
151
  </div>
182
152
 
183
- {/* Collapsible dsh deep-query tools ABOVE the timeline (UX-09):
184
- discoverable without scrolling past a long event list. */}
185
- <div className={css['toolsSection']} data-testid="agent-sidecar-detail-tools">
186
- <button
187
- type="button"
153
+ <div
154
+ {...surfaceProps('dsh-tools', css['toolsSection'])}
155
+ data-testid="agent-sidecar-detail-tools"
156
+ >
157
+ <Button
158
+ size="sm"
159
+ variant="ghost"
188
160
  className={css['toolsToggle']}
189
161
  aria-expanded={toolsOpen}
190
162
  onClick={() => { setToolsOpen((open) => !open) }}
@@ -196,7 +168,7 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
196
168
  <span className={css['toolsToggleGlyph']}>
197
169
  {toolsOpen ? t('detail.tools.hide') : t('detail.tools.show')}
198
170
  </span>
199
- </button>
171
+ </Button>
200
172
  {toolsOpen && (
201
173
  <>
202
174
  <LineageTree
@@ -252,29 +224,29 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
252
224
  />
253
225
  )}
254
226
 
255
- {injectIntegration !== undefined && injectActions !== undefined && injectOpen && (
256
- <div className={overlay['backdrop']} role="presentation" onClick={closeInject}>
257
- <div
258
- className={overlay['dialog']}
259
- role="dialog"
260
- aria-modal="true"
261
- onClick={(event) => { event.stopPropagation() }}
262
- >
263
- <InjectPanel
264
- capability={{ inject: view.injectCapability }}
265
- target={{
266
- agent: detail.header.agent,
267
- sessionId,
268
- ...(title !== '' ? { title } : {}),
269
- }}
270
- defaultMode={injectIntegration.getDefaultMode()}
271
- onPrepare={injectActions.onPrepare}
272
- onExecute={injectActions.onExecute}
273
- onClose={closeInject}
274
- onObserve={observeReaction}
275
- />
276
- </div>
277
- </div>
227
+ {injectIntegration !== undefined && injectActions !== undefined && (
228
+ <Modal
229
+ open={injectOpen}
230
+ onClose={closeInject}
231
+ title={t('inject.title')}
232
+ closeLabel={t('inject.close')}
233
+ className={css['injectDialog']}
234
+ headless
235
+ >
236
+ <InjectPanel
237
+ capability={{ inject: view.injectCapability }}
238
+ target={{
239
+ agent: detail.header.agent,
240
+ sessionId,
241
+ ...(title !== '' ? { title } : {}),
242
+ }}
243
+ defaultMode={injectIntegration.getDefaultMode()}
244
+ onPrepare={injectActions.onPrepare}
245
+ onExecute={injectActions.onExecute}
246
+ onClose={closeInject}
247
+ onObserve={observeReaction}
248
+ />
249
+ </Modal>
278
250
  )}
279
251
  </div>
280
252
  )
@@ -13,8 +13,11 @@
13
13
  * ./logic.ts and is unit-tested there.
14
14
  */
15
15
 
16
+ import { Button, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
16
17
  import { useState } from 'react'
17
18
  import type { ReactElement } from 'react'
19
+ import { StaticPill } from '../primitives/StaticPill.tsx'
20
+ import { surfaceProps } from '../theme/parts.ts'
18
21
  import css from './dsh-tools.module.css'
19
22
  import {
20
23
  deriveLineageView,
@@ -64,20 +67,24 @@ function TreeRow(props: {
64
67
  data-role={node.role}
65
68
  >
66
69
  {node.hasChildren ? (
67
- <button
70
+ <Button
68
71
  type="button"
72
+ size="sm"
73
+ variant="ghost"
69
74
  className={css['toggle']}
70
75
  aria-expanded={!collapsed}
71
76
  aria-label={collapsed ? S.expand : S.collapse}
72
77
  onClick={() => props.onToggle(node.id)}
73
78
  >
74
79
  {collapsed ? '▸' : '▾'}
75
- </button>
80
+ </Button>
76
81
  ) : (
77
82
  <span className={css['toggleSpacer']} aria-hidden />
78
83
  )}
79
- <button
84
+ <Button
80
85
  type="button"
86
+ size="sm"
87
+ variant="ghost"
81
88
  className={css['node']}
82
89
  data-current={node.isCurrent ? 'true' : 'false'}
83
90
  aria-current={node.isCurrent ? 'true' : undefined}
@@ -91,17 +98,17 @@ function TreeRow(props: {
91
98
  {node.shortId}
92
99
  </span>
93
100
  {node.isCurrent && (
94
- <span className={css['nodeBadge']} data-kind="current">
101
+ <StaticPill className={css['nodeBadge']} data-kind="current">
95
102
  {S.currentBadge}
96
- </span>
103
+ </StaticPill>
97
104
  )}
98
105
  {node.live && (
99
- <span className={css['nodeBadge']} data-kind="live">
106
+ <StaticPill className={css['nodeBadge']} data-kind="live">
100
107
  {S.liveBadge}
101
- </span>
108
+ </StaticPill>
102
109
  )}
103
- {!node.persisted && <span className={css['nodeBadge']}>{S.notPersistedBadge}</span>}
104
- </button>
110
+ {!node.persisted && <Pill className={css['nodeBadge']}>{S.notPersistedBadge}</Pill>}
111
+ </Button>
105
112
  </div>
106
113
  )
107
114
  }
@@ -163,17 +170,19 @@ export function LineageTree(props: LineageTreeProps): ReactElement {
163
170
  })
164
171
 
165
172
  return (
166
- <section className={css['panel']} data-testid="agent-sidecar-lineage">
173
+ <section {...surfaceProps('dsh-tools', css['panel'])} data-testid="agent-sidecar-lineage">
167
174
  <div className={css['panelHead']}>
168
175
  <span className={css['panelTitle']}>{S.title}</span>
169
176
  {view.kind === 'tree' && (
170
- <span className={css['panelCount']}>
177
+ <Pill className={css['panelCount']}>
171
178
  {formatTemplate(S.nodeCount, { n: view.tree.nodeCount })}
172
- </span>
179
+ </Pill>
173
180
  )}
174
181
  </div>
175
182
 
176
- {view.kind === 'loading' && <div className={css['mutedLine']}>{view.text}</div>}
183
+ {view.kind === 'loading' && (
184
+ <div className={css['mutedLine']} role="status">{view.text}</div>
185
+ )}
177
186
 
178
187
  {view.kind === 'error' && (
179
188
  <div className={css['errorCard']} role="alert">
@@ -14,7 +14,10 @@
14
14
  * injection); full-text hits carry them, filter hits render without.
15
15
  */
16
16
 
17
+ import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
17
18
  import type { ReactElement } from 'react'
19
+ import { StaticPill } from '../primitives/StaticPill.tsx'
20
+ import { surfaceProps } from '../theme/parts.ts'
18
21
  import css from './dsh-tools.module.css'
19
22
  import {
20
23
  deriveSearchView,
@@ -50,8 +53,10 @@ function ResultItem(props: {
50
53
  }): ReactElement {
51
54
  const { item } = props
52
55
  return (
53
- <button
56
+ <Button
54
57
  type="button"
58
+ size="sm"
59
+ variant="ghost"
55
60
  className={css['resultItem']}
56
61
  onClick={() => props.onSelectSession(item.sessionId)}
57
62
  data-testid="agent-sidecar-search-item"
@@ -61,9 +66,9 @@ function ResultItem(props: {
61
66
  <span className={css['resultTitle']} title={item.title}>
62
67
  {item.titleLabel}
63
68
  </span>
64
- <span className={css['matchTag']} data-kind={item.matchedBy}>
69
+ <StaticPill className={css['matchTag']} data-kind={item.matchedBy}>
65
70
  {item.matchedByLabel}
66
- </span>
71
+ </StaticPill>
67
72
  </span>
68
73
  <span className={css['resultMeta']}>
69
74
  <span className={css['resultProject']} title={item.project}>
@@ -86,7 +91,7 @@ function ResultItem(props: {
86
91
  )}
87
92
  </span>
88
93
  )}
89
- </button>
94
+ </Button>
90
95
  )
91
96
  }
92
97
 
@@ -102,7 +107,7 @@ export function SearchPanel(props: SearchPanelProps): ReactElement {
102
107
  const project = props.project ?? null
103
108
 
104
109
  return (
105
- <section className={css['panel']} data-testid="agent-sidecar-search">
110
+ <section {...surfaceProps('dsh-tools', css['panel'])} data-testid="agent-sidecar-search">
106
111
  <div className={css['panelHead']}>
107
112
  <span className={css['panelTitle']}>{S.title}</span>
108
113
  </div>
@@ -114,22 +119,22 @@ export function SearchPanel(props: SearchPanelProps): ReactElement {
114
119
  props.onSubmit()
115
120
  }}
116
121
  >
117
- <input
122
+ <Input
118
123
  type="search"
119
124
  className={css['searchInput']}
120
125
  value={props.query}
121
126
  placeholder={S.placeholder}
122
127
  onChange={(ev) => props.onQueryChange(ev.target.value)}
123
128
  />
124
- <button type="submit" className={css['searchSubmit']}>
129
+ <Button type="submit" size="sm" variant="outline">
125
130
  {S.submit}
126
- </button>
131
+ </Button>
127
132
  </form>
128
133
 
129
134
  {project !== null && project !== '' && (
130
- <span className={css['projectChip']} title={project}>
135
+ <StaticPill className={css['projectChip']} title={project}>
131
136
  {formatTemplate(S.projectFilter, { project })}
132
- </span>
137
+ </StaticPill>
133
138
  )}
134
139
 
135
140
  {view.notice !== null && (
@@ -142,7 +147,9 @@ export function SearchPanel(props: SearchPanelProps): ReactElement {
142
147
  </div>
143
148
  )}
144
149
 
145
- {view.body === 'loading' && <div className={css['mutedLine']}>{view.text}</div>}
150
+ {view.body === 'loading' && (
151
+ <div className={css['mutedLine']} role="status">{view.text}</div>
152
+ )}
146
153
 
147
154
  {view.body === 'error' && (
148
155
  <div className={css['errorCard']} role="alert">