@shendeguize/dsh-agent-sidecar 0.1.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +8062 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.d.ts +396 -0
  7. package/lib/index.js +4166 -0
  8. package/package.json +101 -0
  9. package/src/analysis.ts +782 -0
  10. package/src/bridge.ts +841 -0
  11. package/src/client/analysis/AnalysisPanel.tsx +191 -0
  12. package/src/client/analysis/analysis.module.css +183 -0
  13. package/src/client/analysis-glue.ts +331 -0
  14. package/src/client/api.ts +380 -0
  15. package/src/client/board/Board.tsx +214 -0
  16. package/src/client/board/board.module.css +302 -0
  17. package/src/client/board/logic.ts +556 -0
  18. package/src/client/board/project-view-logic.ts +361 -0
  19. package/src/client/board/project-view.module.css +307 -0
  20. package/src/client/board/project-view.tsx +189 -0
  21. package/src/client/board/strings.ts +112 -0
  22. package/src/client/commands.ts +484 -0
  23. package/src/client/controller.ts +360 -0
  24. package/src/client/css-modules.d.ts +11 -0
  25. package/src/client/detail/SessionDetail.tsx +270 -0
  26. package/src/client/detail/detail.module.css +433 -0
  27. package/src/client/detail/logic.ts +779 -0
  28. package/src/client/detail/strings.ts +98 -0
  29. package/src/client/detail/transport.ts +175 -0
  30. package/src/client/detail-glue.ts +397 -0
  31. package/src/client/detail-view.module.css +79 -0
  32. package/src/client/detail-view.tsx +233 -0
  33. package/src/client/dsh-tools/LineageTree.tsx +210 -0
  34. package/src/client/dsh-tools/SearchPanel.tsx +169 -0
  35. package/src/client/dsh-tools/dsh-tools.module.css +374 -0
  36. package/src/client/dsh-tools/logic.ts +596 -0
  37. package/src/client/dsh-tools/strings.ts +90 -0
  38. package/src/client/index.ts +315 -0
  39. package/src/client/inject/InjectPanel.tsx +482 -0
  40. package/src/client/inject/inject.module.css +446 -0
  41. package/src/client/inject/logic.ts +516 -0
  42. package/src/client/inject/overlay.module.css +22 -0
  43. package/src/client/inject-glue.ts +171 -0
  44. package/src/client/locales/command.ts +48 -0
  45. package/src/client/locales/en.ts +385 -0
  46. package/src/client/locales/index.ts +123 -0
  47. package/src/client/locales/zh.ts +402 -0
  48. package/src/client/m3-transport.ts +151 -0
  49. package/src/client/mount.tsx +307 -0
  50. package/src/client/project-glue.ts +134 -0
  51. package/src/client/search-glue.ts +143 -0
  52. package/src/client/settings-card.module.css +359 -0
  53. package/src/client/settings-card.tsx +565 -0
  54. package/src/client/settings-glue.ts +130 -0
  55. package/src/client/sidebar-tab.tsx +494 -0
  56. package/src/client/sse.ts +366 -0
  57. package/src/client/widget.tsx +80 -0
  58. package/src/config.ts +193 -0
  59. package/src/dsh-inject.ts +240 -0
  60. package/src/fusion.ts +988 -0
  61. package/src/guard.ts +274 -0
  62. package/src/index.ts +950 -0
  63. package/src/inject-gateway.ts +574 -0
  64. package/src/routes.ts +1133 -0
  65. package/src/send-cli.ts +340 -0
  66. package/src/session-store.ts +184 -0
  67. package/src/skills-provider.ts +293 -0
  68. package/src/supervisor.ts +463 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Pure view-model logic for the cross-agent project correlation view
3
+ * (design §4.e.2 / §5.1 M3, T5.5). No React, no I/O, no data-layer
4
+ * imports — plain values in, plain values out, unit-testable in a bare
5
+ * node environment (same discipline as `logic.ts`).
6
+ *
7
+ * Wire contract (hand-written mirror, per module-ownership rules): the
8
+ * input types below mirror the host's `GET <prefix>/projects` response —
9
+ * `{groups: ProjectGroup[]}` where `ProjectGroup` is fusion.ts
10
+ * `getProjectGroups()` output: `project` (normalized path, '' when
11
+ * unknown), `agents` (distinct, sorted), `sessions` (UnifiedSession[],
12
+ * camelCase, **epoch milliseconds** `lastActivityAt`, most recent first)
13
+ * and the group-level `lastActivityAt`. Only the fields this view renders
14
+ * are mirrored; extra wire fields are structurally ignored.
15
+ *
16
+ * Reuse contract (T2.2, read-only): tone/glyph/relative-time/label tools
17
+ * are imported from `./logic.ts` without touching its exports, so both
18
+ * board views speak the same visual language. Strings NEW to this view
19
+ * live in the module-local {@link PROJECT_VIEW_STRINGS} table (S7 unifies
20
+ * locales later); strings emitted by reused board tools (status labels,
21
+ * relative time, the unknown-project label) intentionally stay with the
22
+ * board table so the two views can never drift apart.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ import {
28
+ abbreviateSessionId,
29
+ agentGlyph,
30
+ deriveBadge,
31
+ formatRelativeTime,
32
+ formatTemplate,
33
+ normalizeStatus,
34
+ projectDisplayName,
35
+ statusRank,
36
+ type StatusBadgeVM,
37
+ } from './logic.ts'
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Module-local strings (project-view-only vocabulary; Chinese primary).
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const PROJECT_VIEW_STRINGS = {
44
+ /** View heading. */
45
+ title: '项目关联',
46
+ /** Header count summary. */
47
+ summary: '{projects} 个项目 · {sessions} 个会话',
48
+ /** Cross-agent badge, shown when a project hosts 2+ agent kinds. */
49
+ crossAgent: '{n} 种 agent',
50
+ /** Group header session count. */
51
+ sessionCount: '{n} 个会话',
52
+ /** Group header recency line. */
53
+ lastActive: '最近活跃 {time}',
54
+ /** Marker on sessions live in this dsh process (UnifiedSession.live). */
55
+ liveChip: '实时',
56
+ /** Untitled-session fallback. */
57
+ untitled: '(无标题)',
58
+ /** Empty state (no groups at all). */
59
+ empty: {
60
+ title: '暂无项目关联',
61
+ hint: '时间窗内没有跨 agent 的项目活动;agent 在某个项目目录下开始工作后会出现在这里。',
62
+ },
63
+ /** Loading placeholder (first fetch, nothing to show yet). */
64
+ loading: '正在加载项目关联…',
65
+ /** Error banner prefix; the raw error detail is appended by the view. */
66
+ errorTitle: '项目关联加载失败',
67
+ } as const
68
+
69
+ export type ProjectViewStrings = typeof PROJECT_VIEW_STRINGS
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Input view models (hand-written wire mirror, see module doc).
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /** One session row of a project group (mirror of `UnifiedSession` subset). */
76
+ export interface ProjectSessionVM {
77
+ agent: string
78
+ sessionId: string
79
+ /** Raw observed status (open vocabulary; sidecar-inferred). */
80
+ status: string
81
+ title: string
82
+ /** Unix epoch MILLISECONDS (fusion already normalized the sidecar seconds). */
83
+ lastActivityAt: number
84
+ /** True when the session is live in this dsh process. */
85
+ live?: boolean
86
+ /** Sidecar-observed seq discontinuity flag. */
87
+ gap?: boolean
88
+ }
89
+
90
+ /** One wire project group (mirror of fusion.ts `ProjectGroup`). */
91
+ export interface ProjectGroupVM {
92
+ /** Project path; '' for the unknown-project bucket. */
93
+ project: string
94
+ /** Distinct agents as reported by the host (re-derived defensively here). */
95
+ agents: string[]
96
+ sessions: ProjectSessionVM[]
97
+ /** Unix epoch MILLISECONDS of the freshest member session. */
98
+ lastActivityAt: number
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Derived view models (what project-view.tsx renders).
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /** A session row with every render-ready derivation attached. */
106
+ export interface DerivedProjectSessionVM {
107
+ agent: string
108
+ sessionId: string
109
+ status: string
110
+ title: string
111
+ lastActivityAt: number
112
+ live: boolean
113
+ gap: boolean
114
+ badge: StatusBadgeVM
115
+ glyph: string
116
+ shortId: string
117
+ relativeTime: string
118
+ /** Title with the untitled fallback already resolved. */
119
+ displayTitle: string
120
+ }
121
+
122
+ /** One per-agent column/section inside a project group. */
123
+ export interface AgentLaneVM {
124
+ agent: string
125
+ glyph: string
126
+ sessions: DerivedProjectSessionVM[]
127
+ }
128
+
129
+ /** One agent participation marker of the group header. */
130
+ export interface AgentBadgeVM {
131
+ agent: string
132
+ glyph: string
133
+ }
134
+
135
+ /** One fully derived project section. */
136
+ export interface DerivedProjectGroupVM {
137
+ /** Normalized project path; '' for the unknown-project bucket. */
138
+ key: string
139
+ /** Display name (path basename; board's 未知项目 label for ''). */
140
+ label: string
141
+ /** Full path for the hover title; '' when unknown. */
142
+ fullPath: string
143
+ /** Distinct participating agents, sorted (derived from sessions). */
144
+ agentBadges: AgentBadgeVM[]
145
+ /** Cross-agent marker text; null when a single agent kind participates. */
146
+ crossAgentLabel: string | null
147
+ sessionCount: number
148
+ sessionCountLabel: string
149
+ /** Unix epoch ms of the freshest member session. */
150
+ lastActivityAt: number
151
+ /** '最近活跃 {relative time}'; empty when the group has no finite time. */
152
+ lastActiveLabel: string
153
+ /** Per-agent lanes, sorted by agent name (same order as agentBadges). */
154
+ lanes: AgentLaneVM[]
155
+ }
156
+
157
+ /** Full render model for the project view. */
158
+ export interface ProjectViewModel {
159
+ groups: DerivedProjectGroupVM[]
160
+ /** Non-null exactly when there is no group to render. */
161
+ emptyState: { title: string; hint: string } | null
162
+ projectCount: number
163
+ sessionCount: number
164
+ /** Header summary line ('{projects} 个项目 · {sessions} 个会话'). */
165
+ summaryLabel: string
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Normalization helpers.
170
+ // ---------------------------------------------------------------------------
171
+
172
+ const KEY_SEP = '\u0000'
173
+
174
+ /**
175
+ * Group-key normalization, aligned with fusion's correlation key: trim,
176
+ * strip trailing slashes (keeping root '/'), whitespace-only → '' (the
177
+ * unknown bucket). The host already normalizes; this re-run makes the
178
+ * view robust to hand-fed or merged inputs.
179
+ */
180
+ export function normalizeProjectKey(project: string): string {
181
+ const trimmed = project.trim()
182
+ if (trimmed === '') return ''
183
+ if (trimmed.length > 1 && trimmed.endsWith('/')) {
184
+ const stripped = trimmed.replace(/\/+$/, '')
185
+ return stripped === '' ? '/' : stripped
186
+ }
187
+ return trimmed
188
+ }
189
+
190
+ /**
191
+ * Distinct participating agents of a session list, sorted by name. This
192
+ * is derived from the sessions themselves (not the wire `agents` field)
193
+ * so the badge row can never disagree with the lanes actually rendered —
194
+ * notably after two wire groups merge under one normalized key.
195
+ */
196
+ export function deriveAgentBadges(
197
+ sessions: ReadonlyArray<{ agent: string }>,
198
+ ): AgentBadgeVM[] {
199
+ const names = new Set<string>()
200
+ for (const session of sessions) names.add(session.agent)
201
+ return [...names].sort().map((agent) => ({ agent, glyph: agentGlyph(agent) }))
202
+ }
203
+
204
+ /**
205
+ * Session ordering inside a lane, mirroring the board's card order so
206
+ * both views read the same way: status rank (working first, dead last),
207
+ * then recency, then id as the deterministic tiebreak.
208
+ */
209
+ export function compareProjectSessions(a: ProjectSessionVM, b: ProjectSessionVM): number {
210
+ const rankDelta = statusRank(normalizeStatus(a.status)) - statusRank(normalizeStatus(b.status))
211
+ if (rankDelta !== 0) return rankDelta
212
+ if (a.lastActivityAt !== b.lastActivityAt) return b.lastActivityAt - a.lastActivityAt
213
+ return a.sessionId.localeCompare(b.sessionId)
214
+ }
215
+
216
+ function deriveSession(session: ProjectSessionVM, nowMs: number): DerivedProjectSessionVM {
217
+ const live = session.live === true
218
+ const gap = session.gap === true
219
+ return {
220
+ agent: session.agent,
221
+ sessionId: session.sessionId,
222
+ status: session.status,
223
+ title: session.title,
224
+ lastActivityAt: session.lastActivityAt,
225
+ live,
226
+ gap,
227
+ // 'ok' stream health: the project view has no global stream signal,
228
+ // so badge attention reflects only the per-session gap marker.
229
+ badge: deriveBadge(session.status, gap, 'ok'),
230
+ glyph: agentGlyph(session.agent),
231
+ shortId: abbreviateSessionId(session.sessionId),
232
+ relativeTime: formatRelativeTime(session.lastActivityAt, nowMs),
233
+ displayTitle: session.title.trim() === '' ? PROJECT_VIEW_STRINGS.untitled : session.title,
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Split a group's sessions into per-agent lanes. Lanes are sorted by
239
+ * agent name (matching the header badge order); sessions inside a lane
240
+ * follow {@link compareProjectSessions}.
241
+ */
242
+ export function buildAgentLanes(
243
+ sessions: readonly ProjectSessionVM[],
244
+ nowMs: number,
245
+ ): AgentLaneVM[] {
246
+ const byAgent = new Map<string, ProjectSessionVM[]>()
247
+ for (const session of sessions) {
248
+ const lane = byAgent.get(session.agent)
249
+ if (lane === undefined) byAgent.set(session.agent, [session])
250
+ else lane.push(session)
251
+ }
252
+ return [...byAgent.keys()].sort().map((agent) => {
253
+ const members = byAgent.get(agent) ?? []
254
+ members.sort(compareProjectSessions)
255
+ return {
256
+ agent,
257
+ glyph: agentGlyph(agent),
258
+ sessions: members.map((session) => deriveSession(session, nowMs)),
259
+ }
260
+ })
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Full pipeline.
265
+ // ---------------------------------------------------------------------------
266
+
267
+ export interface ProjectViewComputeInput {
268
+ groups: readonly ProjectGroupVM[]
269
+ /** Clock injection (epoch ms) for deterministic derivation. */
270
+ nowMs: number
271
+ }
272
+
273
+ /**
274
+ * normalize/merge → derive → sort; the one call project-view.tsx renders
275
+ * from.
276
+ *
277
+ * Normalization: groups collapsing onto the same normalized key are
278
+ * merged; duplicate sessions (same agent + sessionId) within a merged
279
+ * group keep the freshest copy. Group order is `lastActivityAt`
280
+ * descending (recomputed from member sessions, so a stale wire value
281
+ * cannot misplace a group), with the unknown-project bucket ('') always
282
+ * last — same rule as the board.
283
+ */
284
+ export function buildProjectViewModel(input: ProjectViewComputeInput): ProjectViewModel {
285
+ const { groups, nowMs } = input
286
+
287
+ interface Bucket {
288
+ sessions: Map<string, ProjectSessionVM>
289
+ wireLastActivityAt: number
290
+ }
291
+ const buckets = new Map<string, Bucket>()
292
+ for (const group of groups) {
293
+ const key = normalizeProjectKey(group.project)
294
+ let bucket = buckets.get(key)
295
+ if (bucket === undefined) {
296
+ bucket = { sessions: new Map(), wireLastActivityAt: Number.NEGATIVE_INFINITY }
297
+ buckets.set(key, bucket)
298
+ }
299
+ if (Number.isFinite(group.lastActivityAt)) {
300
+ bucket.wireLastActivityAt = Math.max(bucket.wireLastActivityAt, group.lastActivityAt)
301
+ }
302
+ for (const session of group.sessions) {
303
+ const id = `${session.agent}${KEY_SEP}${session.sessionId}`
304
+ const existing = bucket.sessions.get(id)
305
+ if (existing === undefined || session.lastActivityAt > existing.lastActivityAt) {
306
+ bucket.sessions.set(id, session)
307
+ }
308
+ }
309
+ }
310
+
311
+ const derived: DerivedProjectGroupVM[] = []
312
+ let sessionCount = 0
313
+ for (const [key, bucket] of buckets) {
314
+ const sessions = [...bucket.sessions.values()]
315
+ sessionCount += sessions.length
316
+ const memberMax = sessions.reduce(
317
+ (max, s) => (Number.isFinite(s.lastActivityAt) ? Math.max(max, s.lastActivityAt) : max),
318
+ Number.NEGATIVE_INFINITY,
319
+ )
320
+ const lastActivityAt = Math.max(memberMax, bucket.wireLastActivityAt)
321
+ const agentBadges = deriveAgentBadges(sessions)
322
+ derived.push({
323
+ key,
324
+ label: projectDisplayName(key),
325
+ fullPath: key,
326
+ agentBadges,
327
+ crossAgentLabel:
328
+ agentBadges.length > 1
329
+ ? formatTemplate(PROJECT_VIEW_STRINGS.crossAgent, { n: agentBadges.length })
330
+ : null,
331
+ sessionCount: sessions.length,
332
+ sessionCountLabel: formatTemplate(PROJECT_VIEW_STRINGS.sessionCount, {
333
+ n: sessions.length,
334
+ }),
335
+ lastActivityAt: Number.isFinite(lastActivityAt) ? lastActivityAt : 0,
336
+ lastActiveLabel: Number.isFinite(lastActivityAt)
337
+ ? formatTemplate(PROJECT_VIEW_STRINGS.lastActive, {
338
+ time: formatRelativeTime(lastActivityAt, nowMs),
339
+ })
340
+ : '',
341
+ lanes: buildAgentLanes(sessions, nowMs),
342
+ })
343
+ }
344
+
345
+ derived.sort((a, b) => {
346
+ if (a.key === '') return b.key === '' ? 0 : 1
347
+ if (b.key === '') return -1
348
+ return b.lastActivityAt - a.lastActivityAt || a.key.localeCompare(b.key)
349
+ })
350
+
351
+ return {
352
+ groups: derived,
353
+ emptyState: derived.length === 0 ? { ...PROJECT_VIEW_STRINGS.empty } : null,
354
+ projectCount: derived.length,
355
+ sessionCount,
356
+ summaryLabel: formatTemplate(PROJECT_VIEW_STRINGS.summary, {
357
+ projects: derived.length,
358
+ sessions: sessionCount,
359
+ }),
360
+ }
361
+ }
@@ -0,0 +1,307 @@
1
+ /*
2
+ * Project correlation view styles (T5.5): same visual language as
3
+ * board.module.css — every color rides a --dsw-alias-* design token with
4
+ * a neutral fallback, flex top bar, grouped sections. Sessions render as
5
+ * compact rows inside per-agent lanes instead of the board's card grid.
6
+ */
7
+
8
+ .root {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 12px;
12
+ height: 100%;
13
+ min-height: 320px;
14
+ padding: 16px 20px;
15
+ box-sizing: border-box;
16
+ overflow-y: auto;
17
+ color: var(--dsw-alias-label-primary, #1f2328);
18
+ background: var(--dsw-alias-bg-base, transparent);
19
+ }
20
+
21
+ /* ------------------------------------------------------------- top bar */
22
+
23
+ .topbar {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 10px;
27
+ flex-wrap: wrap;
28
+ }
29
+
30
+ .title {
31
+ font-size: 15px;
32
+ font-weight: 650;
33
+ color: var(--dsw-alias-label-primary, #1f2328);
34
+ }
35
+
36
+ .summary {
37
+ font-size: 12px;
38
+ color: var(--dsw-alias-label-secondary, #57606a);
39
+ }
40
+
41
+ .loadingChip {
42
+ font-size: 11px;
43
+ line-height: 18px;
44
+ padding: 0 8px;
45
+ border-radius: 999px;
46
+ color: var(--dsw-alias-label-secondary, #57606a);
47
+ background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
48
+ }
49
+
50
+ /* -------------------------------------------------------------- banner */
51
+
52
+ .banner {
53
+ font-size: 12px;
54
+ line-height: 18px;
55
+ padding: 6px 10px;
56
+ border-radius: 8px;
57
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
58
+ }
59
+
60
+ .banner[data-tone='danger'] {
61
+ color: var(--dsw-alias-state-error-primary, #cf222e);
62
+ background: var(--dsw-alias-state-error-secondary, rgba(207, 34, 46, 0.08));
63
+ border-color: var(--dsw-alias-state-error-secondary, rgba(207, 34, 46, 0.24));
64
+ }
65
+
66
+ /* -------------------------------------------------------- empty states */
67
+
68
+ .empty {
69
+ display: flex;
70
+ flex-direction: column;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 6px;
74
+ flex: 1;
75
+ min-height: 160px;
76
+ text-align: center;
77
+ padding: 24px;
78
+ }
79
+
80
+ .emptyTitle {
81
+ font-size: 14px;
82
+ font-weight: 600;
83
+ color: var(--dsw-alias-label-primary, #1f2328);
84
+ }
85
+
86
+ .emptyHint {
87
+ font-size: 12px;
88
+ max-width: 420px;
89
+ line-height: 20px;
90
+ color: var(--dsw-alias-label-secondary, #57606a);
91
+ }
92
+
93
+ /* -------------------------------------------------------- group blocks */
94
+
95
+ .group {
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 8px;
99
+ padding: 10px 12px;
100
+ border-radius: 10px;
101
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
102
+ background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
103
+ }
104
+
105
+ .groupHead {
106
+ display: flex;
107
+ align-items: baseline;
108
+ gap: 8px;
109
+ flex-wrap: wrap;
110
+ min-width: 0;
111
+ }
112
+
113
+ .groupName {
114
+ font-size: 13px;
115
+ font-weight: 600;
116
+ color: var(--dsw-alias-label-primary, #1f2328);
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ white-space: nowrap;
120
+ }
121
+
122
+ .agentBadges {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: 4px;
126
+ flex-wrap: wrap;
127
+ }
128
+
129
+ .agentBadge {
130
+ display: inline-flex;
131
+ align-items: center;
132
+ gap: 4px;
133
+ font-size: 11px;
134
+ line-height: 18px;
135
+ padding: 0 7px;
136
+ border-radius: 999px;
137
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
138
+ background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
139
+ color: var(--dsw-alias-label-secondary, #57606a);
140
+ white-space: nowrap;
141
+ }
142
+
143
+ .crossBadge {
144
+ font-size: 11px;
145
+ line-height: 18px;
146
+ padding: 0 7px;
147
+ border-radius: 999px;
148
+ color: var(--dsw-alias-static-white, #fff);
149
+ background: var(--dsw-alias-brand-primary, #4d6bfe);
150
+ white-space: nowrap;
151
+ }
152
+
153
+ .spacer {
154
+ flex: 1;
155
+ }
156
+
157
+ .groupMeta {
158
+ font-size: 11px;
159
+ flex: none;
160
+ color: var(--dsw-alias-label-tertiary, #6e7781);
161
+ white-space: nowrap;
162
+ }
163
+
164
+ /* --------------------------------------------------------- agent lanes */
165
+
166
+ .lanes {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 6px;
170
+ }
171
+
172
+ .lane {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 4px;
176
+ }
177
+
178
+ .laneHead {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 5px;
182
+ font-size: 12px;
183
+ font-weight: 600;
184
+ color: var(--dsw-alias-label-primary, #1f2328);
185
+ }
186
+
187
+ .glyph {
188
+ flex: none;
189
+ color: var(--dsw-alias-brand-primary, #4d6bfe);
190
+ }
191
+
192
+ .laneSessions {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 4px;
196
+ }
197
+
198
+ /* -------------------------------------------------------- session rows */
199
+
200
+ .session {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ min-width: 0;
205
+ text-align: left;
206
+ font-family: inherit;
207
+ font-size: 12px;
208
+ padding: 5px 8px;
209
+ border-radius: 7px;
210
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
211
+ background: var(--dsw-alias-bg-base, transparent);
212
+ color: var(--dsw-alias-label-primary, #1f2328);
213
+ cursor: pointer;
214
+ }
215
+
216
+ .session:hover {
217
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
218
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));
219
+ }
220
+
221
+ .session:focus-visible {
222
+ outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe);
223
+ outline-offset: 1px;
224
+ }
225
+
226
+ .badge {
227
+ display: inline-flex;
228
+ align-items: center;
229
+ gap: 5px;
230
+ flex: none;
231
+ font-size: 11px;
232
+ line-height: 18px;
233
+ padding: 0 7px;
234
+ border-radius: 999px;
235
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
236
+ background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.03));
237
+ color: var(--dsw-alias-label-secondary, #57606a);
238
+ white-space: nowrap;
239
+ }
240
+
241
+ .dot {
242
+ width: 7px;
243
+ height: 7px;
244
+ border-radius: 50%;
245
+ flex: none;
246
+ background: var(--dsw-alias-label-tertiary, #6e7781);
247
+ }
248
+
249
+ .dot[data-tone='success'] {
250
+ background: var(--dsw-alias-state-success-primary, #1a7f37);
251
+ }
252
+
253
+ .dot[data-tone='warn'] {
254
+ background: var(--dsw-alias-state-warn-primary, #9a6700);
255
+ }
256
+
257
+ .dot[data-tone='danger'] {
258
+ background: var(--dsw-alias-state-error-primary, #cf222e);
259
+ }
260
+
261
+ .dot[data-tone='neutral'] {
262
+ background: var(--dsw-alias-label-tertiary, #6e7781);
263
+ }
264
+
265
+ .dot[data-tone='muted'] {
266
+ background: var(--dsw-alias-label-dimmed, #8c959f);
267
+ }
268
+
269
+ .attention {
270
+ font-size: 11px;
271
+ color: var(--dsw-alias-state-warn-primary, #9a6700);
272
+ }
273
+
274
+ .attention[data-kind='gap'] {
275
+ color: var(--dsw-alias-state-error-primary, #cf222e);
276
+ }
277
+
278
+ .sessionTitle {
279
+ min-width: 0;
280
+ overflow: hidden;
281
+ text-overflow: ellipsis;
282
+ white-space: nowrap;
283
+ }
284
+
285
+ .liveChip {
286
+ flex: none;
287
+ font-size: 10px;
288
+ line-height: 16px;
289
+ padding: 0 6px;
290
+ border-radius: 999px;
291
+ color: var(--dsw-alias-state-success-primary, #1a7f37);
292
+ border: 1px solid var(--dsw-alias-state-success-primary, rgba(26, 127, 55, 0.4));
293
+ }
294
+
295
+ .sessionId {
296
+ flex: none;
297
+ font-size: 11px;
298
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
299
+ color: var(--dsw-alias-label-tertiary, #6e7781);
300
+ }
301
+
302
+ .sessionTime {
303
+ flex: none;
304
+ margin-left: auto;
305
+ font-size: 11px;
306
+ color: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary, #6e7781));
307
+ }