@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,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 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
34
 
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
-
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,11 +51,14 @@ 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)
61
+ const [toolsOpen, setToolsOpen] = useState(false)
100
62
 
101
63
  useEffect(() => {
102
64
  void detailStore.open()
@@ -126,33 +88,112 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
126
88
  )
127
89
 
128
90
  const analysisEnabled = integration.getAnalysisEnabled()
91
+ const analysisDisabledHint =
92
+ analysisEnabled ? undefined : t('detail.actions.analyzeDisabledHint')
129
93
  const injectIntegration = props.integration.inject
130
94
  const closeInject = (): void => { setInjectOpen(false) }
131
95
  const title = detail.header.title.trim()
132
96
 
97
+ // A delivered execute refreshes the newest timeline while preserving
98
+ // the integration-owned two-phase flow and delivery callback.
99
+ const injectActions: InjectActions | undefined =
100
+ injectIntegration === undefined
101
+ ? undefined
102
+ : {
103
+ onPrepare: injectIntegration.actions.onPrepare,
104
+ onExecute: async (req) => {
105
+ const result = await injectIntegration.actions.onExecute(req)
106
+ if (isDeliveredResult(result)) void detailStore.refreshNewest()
107
+ return result
108
+ },
109
+ }
110
+
111
+ const observeReaction = (): void => {
112
+ if (!detailStore.getState().listening) detailStore.toggleListen()
113
+ closeInject()
114
+ }
115
+
133
116
  return (
134
- <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
+ >
135
121
  <div className={css['actionsRow']}>
136
122
  {injectIntegration !== undefined && (
137
- <button
138
- type="button"
139
- className={css['actionButton']}
123
+ <Button
124
+ size="sm"
125
+ variant="outline"
140
126
  onClick={() => { setInjectOpen(true) }}
141
127
  data-testid="agent-sidecar-detail-inject"
142
128
  >
143
129
  {t('detail.actions.inject')}
144
- </button>
130
+ </Button>
145
131
  )}
146
- <button
147
- type="button"
148
- className={css['actionButton']}
132
+ <Button
133
+ size="sm"
134
+ variant="outline"
149
135
  disabled={!analysisEnabled}
150
- title={analysisEnabled ? undefined : t('detail.actions.analyzeDisabledHint')}
136
+ title={analysisDisabledHint}
137
+ aria-describedby={analysisEnabled ? undefined : ANALYSIS_DISABLED_REASON_ID}
151
138
  onClick={() => { setAnalysisOpen(true) }}
152
139
  data-testid="agent-sidecar-detail-analyze"
153
140
  >
154
141
  {t('detail.actions.analyze')}
155
- </button>
142
+ </Button>
143
+ {!analysisEnabled && (
144
+ <span
145
+ id={ANALYSIS_DISABLED_REASON_ID}
146
+ className={css['analysisDisabledReason']}
147
+ >
148
+ {analysisDisabledHint}
149
+ </span>
150
+ )}
151
+ </div>
152
+
153
+ <div
154
+ {...surfaceProps('dsh-tools', css['toolsSection'])}
155
+ data-testid="agent-sidecar-detail-tools"
156
+ >
157
+ <Button
158
+ size="sm"
159
+ variant="ghost"
160
+ className={css['toolsToggle']}
161
+ aria-expanded={toolsOpen}
162
+ onClick={() => { setToolsOpen((open) => !open) }}
163
+ >
164
+ <span className={css['toolsToggleGlyph']} aria-hidden>
165
+ {toolsOpen ? '▾' : '▸'}
166
+ </span>
167
+ {t('detail.tools.title')}
168
+ <span className={css['toolsToggleGlyph']}>
169
+ {toolsOpen ? t('detail.tools.hide') : t('detail.tools.show')}
170
+ </span>
171
+ </Button>
172
+ {toolsOpen && (
173
+ <>
174
+ <LineageTree
175
+ trace={detail.lineage.trace}
176
+ available={detail.lineage.available}
177
+ reason={detail.lineage.reason}
178
+ detail={detail.lineage.detail}
179
+ currentSessionId={sessionId}
180
+ onSelectSession={props.onSelectSession}
181
+ loading={detail.lineage.loading}
182
+ error={detail.lineage.error}
183
+ />
184
+ <SearchPanel
185
+ query={search.query}
186
+ project={search.project}
187
+ mode={search.mode}
188
+ items={search.items}
189
+ loading={search.loading}
190
+ error={search.error}
191
+ onQueryChange={(query) => { searchStore.setQuery(query) }}
192
+ onSubmit={() => { void searchStore.submit() }}
193
+ onSelectSession={props.onSelectSession}
194
+ />
195
+ </>
196
+ )}
156
197
  </div>
157
198
 
158
199
  <SessionDetail
@@ -163,8 +204,10 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
163
204
  error={detail.error}
164
205
  hasMore={detail.hasMore}
165
206
  listening={detail.listening}
207
+ refreshing={detail.refreshing}
166
208
  onLoadMore={() => { void detailStore.loadMore() }}
167
209
  onToggleListen={() => { detailStore.toggleListen() }}
210
+ onRefresh={() => { void detailStore.refreshNewest() }}
168
211
  onClose={props.onClose}
169
212
  />
170
213
 
@@ -181,52 +224,29 @@ export function SidecarDetailView(props: SidecarDetailViewProps): ReactElement {
181
224
  />
182
225
  )}
183
226
 
184
- <div className={css['toolsSection']} data-testid="agent-sidecar-detail-tools">
185
- <LineageTree
186
- trace={detail.lineage.trace}
187
- available={detail.lineage.available}
188
- reason={detail.lineage.reason}
189
- detail={detail.lineage.detail}
190
- currentSessionId={sessionId}
191
- onSelectSession={props.onSelectSession}
192
- loading={detail.lineage.loading}
193
- error={detail.lineage.error}
194
- />
195
- <SearchPanel
196
- query={search.query}
197
- project={search.project}
198
- mode={search.mode}
199
- items={search.items}
200
- loading={search.loading}
201
- error={search.error}
202
- onQueryChange={(query) => { searchStore.setQuery(query) }}
203
- onSubmit={() => { void searchStore.submit() }}
204
- onSelectSession={props.onSelectSession}
205
- />
206
- </div>
207
-
208
- {injectIntegration !== undefined && injectOpen && (
209
- <div className={overlay['backdrop']} role="presentation" onClick={closeInject}>
210
- <div
211
- className={overlay['dialog']}
212
- role="dialog"
213
- aria-modal="true"
214
- onClick={(event) => { event.stopPropagation() }}
215
- >
216
- <InjectPanel
217
- capability={{ inject: view.injectCapability }}
218
- target={{
219
- agent: detail.header.agent,
220
- sessionId,
221
- ...(title !== '' ? { title } : {}),
222
- }}
223
- defaultMode={injectIntegration.getDefaultMode()}
224
- onPrepare={injectIntegration.actions.onPrepare}
225
- onExecute={injectIntegration.actions.onExecute}
226
- onClose={closeInject}
227
- />
228
- </div>
229
- </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>
230
250
  )}
231
251
  </div>
232
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">