@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shendeguize/dsh-agent-sidecar",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Agent Sidecar as a native DSH plugin: cross-agent monitoring, injection and bypass analysis / 跨 agent 监控、注入与旁路分析",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -86,6 +86,8 @@
86
86
  "@deepseek-ai/cordis": "4.0.1",
87
87
  "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
88
88
  "@deepseek-ai/dsh-client-ui-conversation": "0.1.1-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
89
91
  "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.1-rc.2",
90
92
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
91
93
  "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
@@ -1,21 +1,9 @@
1
- /**
2
- * AI bypass-analysis panel (T5.10b, design §4.e.3 / §5.1 view 2): fully
3
- * controlled presentation over an {@link AnalysisGlueState} — the owner
4
- * (detail view) holds the AnalysisStore and passes state + intents down.
5
- *
6
- * Honesty posture (§5.3): the engine disclaimer (fallback copy when a
7
- * settle never happened) renders with every result, truncation is called
8
- * out per exchange, terminal errors carry the vocabulary code, and the
9
- * capability-off state explains where to enable analysis instead of
10
- * hiding the entry. Strings ride the main locale table (`analysis.*`).
11
- *
12
- * @module
13
- */
14
-
1
+ import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
15
2
  import { useState } from 'react'
16
3
  import type { ReactElement } from 'react'
17
4
  import { t } from '../locales/index.ts'
18
5
  import type { AnalysisExchange, AnalysisGlueState } from '../analysis-glue.ts'
6
+ import { surfaceProps } from '../theme/parts.ts'
19
7
  import css from './analysis.module.css'
20
8
 
21
9
  export interface AnalysisPanelProps {
@@ -87,24 +75,25 @@ export function AnalysisPanel(props: AnalysisPanelProps): ReactElement {
87
75
  }
88
76
 
89
77
  return (
90
- <section className={css['panel']} data-testid="agent-sidecar-analysis">
78
+ <section {...surfaceProps('analysis-panel', css['panel'])} data-testid="agent-sidecar-analysis">
91
79
  <div className={css['head']}>
92
80
  <span className={css['title']}>{t('analysis.title')}</span>
93
81
  <span className={css['spacer']} />
94
82
  {conversationLive && (
95
- <button
83
+ <Button
96
84
  type="button"
97
- className={css['textButton']}
85
+ size="sm"
86
+ variant="ghost"
98
87
  disabled={!enabled}
99
88
  onClick={props.onStop}
100
89
  data-testid="agent-sidecar-analysis-stop"
101
90
  >
102
91
  {t('analysis.stop')}
103
- </button>
92
+ </Button>
104
93
  )}
105
- <button type="button" className={css['closeButton']} onClick={props.onClose}>
94
+ <Button type="button" size="sm" variant="ghost" onClick={props.onClose}>
106
95
  {t('analysis.close')}
107
- </button>
96
+ </Button>
108
97
  </div>
109
98
 
110
99
  {!enabled && (
@@ -136,14 +125,16 @@ export function AnalysisPanel(props: AnalysisPanelProps): ReactElement {
136
125
  {state.phase === 'idle' && (
137
126
  <div className={css['mutedLine']}>{t('analysis.idleHint')}</div>
138
127
  )}
139
- <button
128
+ <Button
140
129
  type="button"
141
- className={css['primaryButton']}
130
+ size="sm"
131
+ variant="primary"
132
+ className={css['startButton']}
142
133
  onClick={props.onStart}
143
134
  data-testid="agent-sidecar-analysis-start"
144
135
  >
145
136
  {state.phase === 'idle' ? t('analysis.start') : t('analysis.restart')}
146
- </button>
137
+ </Button>
147
138
  </>
148
139
  )}
149
140
 
@@ -156,7 +147,7 @@ export function AnalysisPanel(props: AnalysisPanelProps): ReactElement {
156
147
 
157
148
  {conversationLive && (
158
149
  <div className={css['followupForm']}>
159
- <input
150
+ <Input
160
151
  className={css['followupInput']}
161
152
  value={question}
162
153
  placeholder={t('analysis.followupPlaceholder')}
@@ -169,15 +160,16 @@ export function AnalysisPanel(props: AnalysisPanelProps): ReactElement {
169
160
  }}
170
161
  data-testid="agent-sidecar-analysis-question"
171
162
  />
172
- <button
163
+ <Button
173
164
  type="button"
174
- className={css['textButton']}
165
+ size="sm"
166
+ variant="ghost"
175
167
  disabled={!enabled || state.phase !== 'ready' || question.trim() === ''}
176
168
  onClick={submitFollowup}
177
169
  data-testid="agent-sidecar-analysis-followup"
178
170
  >
179
171
  {t('analysis.followupSubmit')}
180
- </button>
172
+ </Button>
181
173
  </div>
182
174
  )}
183
175
 
@@ -1,20 +1,13 @@
1
- /*
2
- * AI bypass-analysis panel styles (design §4.e.3 / §5.1 view 2): every
3
- * color rides a --dsw-alias-* design token with a neutral fallback — no
4
- * theme colors are hardcoded as the only value. Same posture as
5
- * dsh-tools.module.css.
6
- */
7
-
8
1
  .panel {
9
2
  display: flex;
10
3
  flex-direction: column;
11
4
  gap: 8px;
12
5
  min-width: 0;
13
6
  padding: 10px 12px;
14
- border-radius: 10px;
15
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
16
- background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
17
- color: var(--dsw-alias-label-primary, #1f2328);
7
+ border: 1px solid var(--agsc-border-strong);
8
+ border-radius: var(--agsc-radius-control);
9
+ background: var(--agsc-bg);
10
+ color: var(--agsc-fg);
18
11
  }
19
12
 
20
13
  .head {
@@ -27,84 +20,43 @@
27
20
  .title {
28
21
  font-size: 13px;
29
22
  font-weight: 600;
30
- color: var(--dsw-alias-label-primary, #1f2328);
23
+ color: var(--agsc-fg);
31
24
  }
32
25
 
33
26
  .spacer {
34
27
  flex: 1;
35
28
  }
36
29
 
37
- .closeButton,
38
- .textButton {
39
- flex: none;
40
- font-size: 12px;
41
- padding: 2px 8px;
42
- border-radius: 6px;
43
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
44
- background: transparent;
45
- color: var(--dsw-alias-label-secondary, #57606a);
46
- cursor: pointer;
47
- }
48
-
49
- .closeButton:hover,
50
- .textButton:hover {
51
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
52
- }
53
-
54
- .primaryButton {
30
+ .startButton {
55
31
  align-self: flex-start;
56
- font-size: 12px;
57
- font-weight: 600;
58
- padding: 4px 12px;
59
- border-radius: 6px;
60
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
61
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
62
- color: var(--dsw-alias-label-primary, #1f2328);
63
- cursor: pointer;
64
- }
65
-
66
- .primaryButton:disabled,
67
- .textButton:disabled {
68
- opacity: 0.5;
69
- cursor: not-allowed;
70
32
  }
71
33
 
72
34
  .mutedLine {
73
35
  font-size: 12px;
74
36
  line-height: 18px;
75
- color: var(--dsw-alias-label-secondary, #57606a);
37
+ color: var(--agsc-fg-secondary);
76
38
  }
77
39
 
78
- /* Capability-off / terminal-error note. */
79
- .noteCard {
40
+ .noteCard,
41
+ .errorCard,
42
+ .noticeBar {
43
+ padding: 8px 10px;
44
+ border: 1px solid var(--agsc-border-strong);
45
+ border-radius: var(--agsc-radius-control);
46
+ background: var(--agsc-bg-raised);
80
47
  font-size: 12px;
81
48
  line-height: 18px;
82
- padding: 8px 10px;
83
- border-radius: 8px;
84
- border: 1px dashed var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
85
- background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
86
- color: var(--dsw-alias-label-secondary, #57606a);
49
+ color: var(--agsc-fg-secondary);
87
50
  }
88
51
 
89
52
  .errorCard {
90
- font-size: 12px;
91
- line-height: 18px;
92
- padding: 8px 10px;
93
- border-radius: 8px;
94
- color: var(--dsw-alias-state-error-primary, #cf222e);
95
- background: var(--dsw-alias-state-error-secondary, rgba(207, 34, 46, 0.08));
96
- border: 1px solid var(--dsw-alias-state-error-secondary, rgba(207, 34, 46, 0.24));
53
+ border-color: var(--agsc-err);
54
+ color: var(--agsc-err);
97
55
  }
98
56
 
99
- /* Non-terminal retryable notice. */
100
57
  .noticeBar {
101
- font-size: 12px;
102
- line-height: 18px;
103
- padding: 6px 10px;
104
- border-radius: 8px;
105
- color: var(--dsw-alias-state-warning-primary, #9a6700);
106
- background: var(--dsw-alias-state-warning-secondary, rgba(154, 103, 0, 0.1));
107
- border: 1px solid var(--dsw-alias-state-warning-secondary, rgba(154, 103, 0, 0.28));
58
+ border-color: var(--agsc-warn);
59
+ color: var(--agsc-warn);
108
60
  }
109
61
 
110
62
  .exchange {
@@ -116,48 +68,47 @@
116
68
  .exchangeLabel {
117
69
  font-size: 11px;
118
70
  font-weight: 600;
119
- color: var(--dsw-alias-label-secondary, #57606a);
71
+ color: var(--agsc-fg-secondary);
120
72
  }
121
73
 
122
74
  .question {
75
+ padding: 6px 10px;
76
+ border-radius: var(--agsc-radius-control);
77
+ background: var(--agsc-bg-raised);
78
+ color: var(--agsc-fg);
123
79
  font-size: 12px;
124
80
  line-height: 18px;
125
- padding: 6px 10px;
126
- border-radius: 8px;
127
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
128
- color: var(--dsw-alias-label-primary, #1f2328);
129
81
  white-space: pre-wrap;
130
82
  word-break: break-word;
131
83
  }
132
84
 
133
85
  .summary {
134
86
  margin: 0;
87
+ max-height: 320px;
88
+ overflow: auto;
89
+ padding: 8px 10px;
90
+ border: 1px solid var(--agsc-border-strong);
91
+ border-radius: var(--agsc-radius-control);
92
+ background: var(--agsc-bg-raised);
93
+ color: var(--agsc-fg);
94
+ font-family: inherit;
135
95
  font-size: 12px;
136
96
  line-height: 18px;
137
- padding: 8px 10px;
138
- border-radius: 8px;
139
- background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
140
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
141
- color: var(--dsw-alias-label-primary, #1f2328);
142
97
  white-space: pre-wrap;
143
98
  word-break: break-word;
144
- font-family: inherit;
145
- max-height: 320px;
146
- overflow: auto;
147
99
  }
148
100
 
149
101
  .truncated {
102
+ color: var(--agsc-warn);
150
103
  font-size: 11px;
151
- color: var(--dsw-alias-state-warning-primary, #9a6700);
152
104
  }
153
105
 
154
- /* The §5.3 honesty line — always visible with results. */
155
106
  .disclaimer {
107
+ padding-top: 6px;
108
+ border-top: 1px solid var(--agsc-border-strong);
109
+ color: var(--agsc-fg-secondary);
156
110
  font-size: 11px;
157
111
  line-height: 16px;
158
- color: var(--dsw-alias-label-secondary, #57606a);
159
- border-top: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08));
160
- padding-top: 6px;
161
112
  }
162
113
 
163
114
  .followupForm {
@@ -168,16 +119,4 @@
168
119
  .followupInput {
169
120
  flex: 1;
170
121
  min-width: 0;
171
- font-size: 12px;
172
- padding: 4px 8px;
173
- border-radius: 6px;
174
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.16));
175
- background: var(--dsw-alias-bg-layer-0, #fff);
176
- color: var(--dsw-alias-label-primary, #1f2328);
177
- }
178
-
179
- .actionsRow {
180
- display: flex;
181
- align-items: center;
182
- gap: 8px;
183
122
  }
@@ -18,8 +18,9 @@
18
18
  * concerns — useState here, reset on tab remount.
19
19
  */
20
20
 
21
- import { useState } from 'react'
22
- import type { MouseEvent, ReactElement } from 'react'
21
+ import { Button, Pill, StateDot, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
22
+ import { useEffect, useRef, useState } from 'react'
23
+ import type { ReactElement } from 'react'
23
24
  import {
24
25
  buildBoardViewModel,
25
26
  formatTemplate,
@@ -35,6 +36,7 @@ import {
35
36
  type StreamHealthToken,
36
37
  } from './logic.ts'
37
38
  import { BOARD_STRINGS } from './strings.ts'
39
+ import { surfaceProps } from '../theme/parts.ts'
38
40
  import styles from './board.module.css'
39
41
 
40
42
  /** Time-window choices offered by the top bar (hours). */
@@ -62,24 +64,47 @@ export interface BoardProps {
62
64
  nowMs?: number
63
65
  }
64
66
 
67
+ function sessionDotState(status: DerivedSessionCardVM['badge']['status']): StateDotState | null {
68
+ if (status === 'working') return 'ongoing'
69
+ if (status === 'waiting') return 'warning'
70
+ return null
71
+ }
72
+
65
73
  function SessionCard(props: {
66
74
  card: DerivedSessionCardVM
67
75
  onSelect: (sessionId: string) => void
68
76
  }): ReactElement {
69
77
  const { card, onSelect } = props
70
78
  const [copied, setCopied] = useState(false)
79
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
80
+ const copyAliveRef = useRef(true)
81
+ const dotState = sessionDotState(card.badge.status)
82
+
83
+ useEffect(() => {
84
+ copyAliveRef.current = true
85
+ return () => {
86
+ copyAliveRef.current = false
87
+ if (copyTimerRef.current !== null) {
88
+ clearTimeout(copyTimerRef.current)
89
+ copyTimerRef.current = null
90
+ }
91
+ }
92
+ }, [])
71
93
 
72
- // UX-17: click the id row to copy the full session id. stopPropagation
73
- // keeps the card's open-detail click intact; the row stays a non-focusable
74
- // span because the card itself is already a <button> (no nested controls).
75
- const onCopyId = (ev: MouseEvent): void => {
76
- ev.stopPropagation()
94
+ // UX-17: opening and copying are sibling buttons inside a semantic card,
95
+ // so both actions are independently keyboard reachable.
96
+ const onCopyId = (): void => {
77
97
  const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard
78
98
  if (clipboard === undefined) return
79
99
  clipboard.writeText(card.sessionId).then(
80
100
  () => {
101
+ if (!copyAliveRef.current) return
102
+ if (copyTimerRef.current !== null) clearTimeout(copyTimerRef.current)
81
103
  setCopied(true)
82
- setTimeout(() => { setCopied(false) }, 2000)
104
+ copyTimerRef.current = setTimeout(() => {
105
+ copyTimerRef.current = null
106
+ setCopied(false)
107
+ }, 2000)
83
108
  },
84
109
  () => {
85
110
  // Clipboard permission denied: the hover title still carries the id.
@@ -88,33 +113,43 @@ function SessionCard(props: {
88
113
  }
89
114
 
90
115
  return (
91
- <button
92
- type="button"
93
- className={styles['card']}
94
- onClick={() => onSelect(card.sessionId)}
116
+ <article
117
+ {...surfaceProps('board-card', styles['card'])}
95
118
  data-testid="agent-sidecar-card"
96
119
  >
97
- <div className={styles['cardHead']}>
98
- <span className={styles['agent']}>
99
- <span className={styles['glyph']} aria-hidden>
100
- {card.glyph}
120
+ <button
121
+ type="button"
122
+ className={styles['cardOpen']}
123
+ onClick={() => onSelect(card.sessionId)}
124
+ >
125
+ <span className={styles['cardHead']}>
126
+ <span className={styles['agent']}>
127
+ <span className={styles['glyph']} aria-hidden>
128
+ {card.glyph}
129
+ </span>
130
+ {card.agent}
131
+ </span>
132
+ <span title={card.hoverTitle}>
133
+ <Pill className={styles['statusPill']}>
134
+ {dotState === null
135
+ ? <span className={styles['dot']} data-tone={card.badge.tone} />
136
+ : <StateDot state={dotState} size={8} />}
137
+ {card.badge.label}
138
+ {card.badge.attention !== null && (
139
+ <span className={styles['attention']} data-kind={card.badge.attention}>
140
+ {card.badge.attentionLabel}
141
+ </span>
142
+ )}
143
+ </Pill>
101
144
  </span>
102
- {card.agent}
103
145
  </span>
104
- <span className={styles['badge']} data-tone={card.badge.tone} title={card.hoverTitle}>
105
- <span className={styles['dot']} data-tone={card.badge.tone} />
106
- {card.badge.label}
107
- {card.badge.attention !== null && (
108
- <span className={styles['attention']} data-kind={card.badge.attention}>
109
- {card.badge.attentionLabel}
110
- </span>
111
- )}
146
+ <span className={styles['cardTitle']} title={card.title}>
147
+ {card.title.trim() === '' ? BOARD_STRINGS.card.untitled : card.title}
112
148
  </span>
113
- </div>
114
- <div className={styles['cardTitle']} title={card.title}>
115
- {card.title.trim() === '' ? BOARD_STRINGS.card.untitled : card.title}
116
- </div>
117
- <div
149
+ </button>
150
+ <Button
151
+ size="sm"
152
+ variant="ghost"
118
153
  className={styles['cardId']}
119
154
  title={`${card.sessionId}\n${BOARD_STRINGS.card.copyId}`}
120
155
  onClick={onCopyId}
@@ -122,14 +157,14 @@ function SessionCard(props: {
122
157
  >
123
158
  {card.shortId}
124
159
  {copied && <span className={styles['copied']} role="status">{BOARD_STRINGS.card.copied}</span>}
125
- </div>
160
+ </Button>
126
161
  <div className={styles['cardEvent']}>
127
162
  {card.lastEvent === null
128
163
  ? BOARD_STRINGS.card.noEvent
129
164
  : `${card.lastEvent.kind} · ${card.lastEvent.text}`}
130
165
  </div>
131
166
  <div className={styles['cardTime']}>{card.relativeTime}</div>
132
- </button>
167
+ </article>
133
168
  )
134
169
  }
135
170
 
@@ -180,22 +215,24 @@ function ProjectGroup(props: {
180
215
  ))}
181
216
  </div>
182
217
  {hiddenCount > 0 && (
183
- <button
184
- type="button"
218
+ <Button
219
+ size="sm"
220
+ variant="outline"
185
221
  className={styles['showMore']}
186
222
  onClick={() => { setExpanded(true) }}
187
223
  >
188
224
  {formatTemplate(BOARD_STRINGS.group.showAll, { n: group.cards.length })}
189
- </button>
225
+ </Button>
190
226
  )}
191
227
  {expanded && group.cards.length > GROUP_CARD_LIMIT && (
192
- <button
193
- type="button"
228
+ <Button
229
+ size="sm"
230
+ variant="outline"
194
231
  className={styles['showMore']}
195
232
  onClick={() => { setExpanded(false) }}
196
233
  >
197
234
  {formatTemplate(BOARD_STRINGS.group.showLess, { n: GROUP_CARD_LIMIT })}
198
- </button>
235
+ </Button>
199
236
  )}
200
237
  </>
201
238
  )}
@@ -249,41 +286,57 @@ export function Board(props: BoardProps): ReactElement {
249
286
  : formatTemplate(BOARD_STRINGS.topbar.filterByStatusTitle, {
250
287
  label: BOARD_STRINGS.status[status],
251
288
  })
289
+ const daemonDotState: StateDotState =
290
+ props.daemonState === 'failed'
291
+ ? 'error'
292
+ : props.daemonState === 'defer' || props.daemonState === 'backoff'
293
+ ? 'warning'
294
+ : props.daemonState === 'adopted' || props.daemonState === 'hosted'
295
+ ? 'done'
296
+ : 'ongoing'
297
+ const streamDotState: StateDotState =
298
+ props.streamHealth === 'ok' ? 'done' : props.streamHealth === 'degraded' ? 'warning' : 'ongoing'
252
299
 
253
300
  return (
254
- <div className={styles['root']} data-testid="agent-sidecar-board">
255
- <header className={styles['topbar']}>
301
+ <div {...surfaceProps('board', styles['root'])} data-testid="agent-sidecar-board">
302
+ <header {...surfaceProps('board-toolbar', styles['topbar'])}>
256
303
  <span className={styles['title']}>{BOARD_STRINGS.topbar.title}</span>
257
- <span className={styles['badge']} data-tone={vm.daemonBadge.tone} title={props.daemonDetail}>
258
- <span className={styles['dot']} data-tone={vm.daemonBadge.tone} />
259
- {vm.daemonBadge.label}
304
+ <span title={props.daemonDetail}>
305
+ <Pill>
306
+ <StateDot state={daemonDotState} size={8} />
307
+ {vm.daemonBadge.label}
308
+ </Pill>
260
309
  </span>
261
- <span className={styles['badge']} data-tone={vm.streamTone}>
262
- <span className={styles['dot']} data-tone={vm.streamTone} />
310
+ <Pill>
311
+ <StateDot state={streamDotState} size={8} />
263
312
  {vm.streamLabel}
264
- </span>
265
- <button
266
- type="button"
313
+ </Pill>
314
+ <Pill
267
315
  className={styles['countBadge']}
316
+ active={props.filters.statusFilter === 'working'}
268
317
  aria-pressed={props.filters.statusFilter === 'working'}
269
318
  title={statusBadgeTitle('working')}
270
319
  onClick={() => { toggleStatusFilter('working') }}
271
320
  data-testid="agent-sidecar-count-working"
272
321
  >
273
- <span className={styles['dot']} data-tone={vm.workingCount > 0 ? 'success' : 'neutral'} />
322
+ {vm.workingCount > 0
323
+ ? <StateDot state="ongoing" size={8} />
324
+ : <span className={styles['dot']} data-tone="neutral" />}
274
325
  {formatTemplate(BOARD_STRINGS.topbar.countWorking, { n: vm.workingCount })}
275
- </button>
276
- <button
277
- type="button"
326
+ </Pill>
327
+ <Pill
278
328
  className={styles['countBadge']}
329
+ active={props.filters.statusFilter === 'waiting'}
279
330
  aria-pressed={props.filters.statusFilter === 'waiting'}
280
331
  title={statusBadgeTitle('waiting')}
281
332
  onClick={() => { toggleStatusFilter('waiting') }}
282
333
  data-testid="agent-sidecar-count-waiting"
283
334
  >
284
- <span className={styles['dot']} data-tone={vm.waitingCount > 0 ? 'warn' : 'neutral'} />
335
+ {vm.waitingCount > 0
336
+ ? <StateDot state="warning" size={8} />
337
+ : <span className={styles['dot']} data-tone="neutral" />}
285
338
  {formatTemplate(BOARD_STRINGS.topbar.countWaiting, { n: vm.waitingCount })}
286
- </button>
339
+ </Pill>
287
340
  <span className={styles['countTotal']} data-testid="agent-sidecar-count-total">
288
341
  {formatTemplate(BOARD_STRINGS.topbar.countTotal, { n: vm.totalCount })}
289
342
  </span>
@@ -318,27 +371,28 @@ export function Board(props: BoardProps): ReactElement {
318
371
  />
319
372
  {BOARD_STRINGS.topbar.showDead}
320
373
  </label>
321
- <button
322
- type="button"
323
- className={styles['refresh']}
374
+ <Button
375
+ size="sm"
376
+ variant="toolbar"
324
377
  title={BOARD_STRINGS.topbar.refreshTitle}
325
378
  disabled={refreshing}
326
379
  onClick={onRefreshClick}
327
380
  >
328
381
  {refreshing ? BOARD_STRINGS.topbar.refreshing : BOARD_STRINGS.topbar.refresh}
329
- </button>
382
+ </Button>
330
383
  </header>
331
384
 
332
385
  {refreshFailed && (
333
386
  <div className={styles['banner']} data-tone="warn" role="status">
334
387
  {BOARD_STRINGS.topbar.refreshFailed}
335
- <button
336
- type="button"
388
+ <Button
389
+ size="sm"
390
+ variant="ghost"
337
391
  className={styles['bannerDismiss']}
338
392
  onClick={() => { setRefreshFailed(false) }}
339
393
  >
340
394
  {BOARD_STRINGS.topbar.dismiss}
341
- </button>
395
+ </Button>
342
396
  </div>
343
397
  )}
344
398