@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.
- package/README.md +78 -11
- package/lib/client.js +8659 -7589
- package/lib/client.js.map +1 -1
- package/package.json +3 -1
- package/src/client/analysis/AnalysisPanel.tsx +19 -27
- package/src/client/analysis/analysis.module.css +36 -97
- package/src/client/board/Board.tsx +115 -61
- package/src/client/board/board.module.css +72 -151
- package/src/client/board/project-view-logic.ts +21 -34
- package/src/client/board/project-view.module.css +41 -96
- package/src/client/board/project-view.tsx +29 -13
- package/src/client/board/strings.ts +68 -107
- package/src/client/commands.ts +30 -15
- package/src/client/detail/SessionDetail.tsx +92 -58
- package/src/client/detail/detail.module.css +62 -218
- package/src/client/detail/strings.ts +64 -88
- package/src/client/detail-view.module.css +30 -55
- package/src/client/detail-view.tsx +80 -108
- package/src/client/dsh-tools/LineageTree.tsx +22 -13
- package/src/client/dsh-tools/SearchPanel.tsx +18 -11
- package/src/client/dsh-tools/dsh-tools.module.css +50 -139
- package/src/client/dsh-tools/strings.ts +42 -77
- package/src/client/index.ts +285 -124
- package/src/client/inject/InjectPanel.tsx +31 -64
- package/src/client/inject/inject.module.css +97 -198
- package/src/client/lifecycle/handoff.ts +83 -0
- package/src/client/locales/en.ts +74 -2
- package/src/client/locales/host.ts +131 -0
- package/src/client/locales/index.ts +196 -8
- package/src/client/locales/react.ts +10 -0
- package/src/client/locales/view.ts +38 -0
- package/src/client/locales/zh.ts +194 -134
- package/src/client/mount.tsx +72 -38
- package/src/client/navigation/CenterOverlay.tsx +40 -0
- package/src/client/navigation/center-overlay.module.css +51 -0
- package/src/client/navigation/center.ts +45 -0
- package/src/client/navigation/modal-isolation.ts +152 -0
- package/src/client/navigation/modal-surface-anchor.ts +72 -0
- package/src/client/navigation/sidebar-entry.module.css +73 -0
- package/src/client/navigation/sidebar-entry.ts +309 -0
- package/src/client/primitives/StaticPill.tsx +21 -0
- package/src/client/settings-card.module.css +35 -119
- package/src/client/settings-card.tsx +31 -153
- package/src/client/settings-fields.tsx +131 -0
- package/src/client/sidebar/SidebarTab.tsx +156 -0
- package/src/client/sidebar/model.ts +65 -0
- package/src/client/sidebar/sidebar-tab.module.css +118 -0
- package/src/client/sidebar-tab.tsx +46 -320
- package/src/client/theme/agsc.module.css +31 -0
- package/src/client/theme/parts.ts +56 -0
- package/src/client/ui-integration.ts +86 -0
- package/src/client/widget.tsx +9 -8
- package/src/client/inject/overlay.module.css +0 -22
|
@@ -1,52 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* better-sidebar docs/external-plugin-guide.md + src/client/service.ts).
|
|
8
|
-
* This module integrates as a SOFT dependency, dsh-sentinel style:
|
|
9
|
-
*
|
|
10
|
-
* - NO value import and NO type import of `dsh-better-sidebar` — the client
|
|
11
|
-
* bundle purity gate stays react-only. The minimal service contract is
|
|
12
|
-
* restated locally ({@link BetterSidebarServiceFace}) and the live service
|
|
13
|
-
* is duck-typed at runtime ({@link probeBetterSidebar}).
|
|
14
|
-
* - Absent → silent skip: {@link mountSidebarTab} parks the registration in
|
|
15
|
-
* `ctx.inject(['betterSidebar'], …)` — a cordis fiber that stays PENDING
|
|
16
|
-
* until some plugin provides the service (zero timers, zero polling, zero
|
|
17
|
-
* resources), plus one debug-level log line at mount time. If
|
|
18
|
-
* better-sidebar is installed later (any load order), the fiber activates
|
|
19
|
-
* and the tab registers; if never, nothing ever runs.
|
|
20
|
-
* - Present → one compact "Sidecar" tab (`agent-sidecar:monitor`): daemon
|
|
21
|
-
* connection dot, working/waiting counts, and the most recently active
|
|
22
|
-
* sessions, all read from the SHARED {@link SidecarController} stores —
|
|
23
|
-
* no second poller, no second SSE.
|
|
24
|
-
* - `visible === false` releases resources: better-sidebar hands tab
|
|
25
|
-
* components a `visible` prop (false while the panel is collapsed or
|
|
26
|
-
* another tab is active). The shared controller has no pause/refcount
|
|
27
|
-
* surface (its stream is plugin-lifetime, `stop()` is terminal), so the
|
|
28
|
-
* gate lives in this view: {@link VisibleGatedStore} unsubscribes from the
|
|
29
|
-
* controller while hidden (re-render churn stops; the view triggers no
|
|
30
|
-
* fetches of its own) and resubscribes + catches up on show.
|
|
2
|
+
* Soft dsh-better-sidebar integration (design §5.2 / ADR-5 option C).
|
|
3
|
+
* The dependency stays duck-typed: absence parks a zero-resource inject
|
|
4
|
+
* fiber, presence registers one compact tab over the shared controller.
|
|
5
|
+
* `visible=false` drops only this view's subscription; the plugin-lifetime
|
|
6
|
+
* controller remains untouched.
|
|
31
7
|
*/
|
|
32
8
|
|
|
33
9
|
import { createElement as h, useEffect, useState, useSyncExternalStore } from 'react'
|
|
34
|
-
import type {
|
|
10
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
35
11
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
36
12
|
import type { SidecarController, SidecarViewState } from './controller.ts'
|
|
37
13
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
14
|
+
acquireWithHandoff,
|
|
15
|
+
isRegistrationCollision,
|
|
16
|
+
} from './lifecycle/handoff.ts'
|
|
17
|
+
import { SidebarTab, SidebarTabIcon } from './sidebar/SidebarTab.tsx'
|
|
18
|
+
import { deriveMiniVM } from './sidebar/model.ts'
|
|
19
|
+
import { t } from './locales/index.ts'
|
|
20
|
+
|
|
21
|
+
// Compatibility exports for consumers of the original integration module.
|
|
22
|
+
export {
|
|
23
|
+
MAX_RECENT_SESSIONS,
|
|
24
|
+
countWaiting,
|
|
25
|
+
deriveMiniVM,
|
|
26
|
+
recentActiveSessions,
|
|
27
|
+
} from './sidebar/model.ts'
|
|
28
|
+
export type { SidebarMiniVM } from './sidebar/model.ts'
|
|
50
29
|
|
|
51
30
|
/** Registered tab type id (design §5.2 names it verbatim). */
|
|
52
31
|
export const SIDEBAR_TAB_ID = 'agent-sidecar:monitor'
|
|
@@ -54,12 +33,7 @@ export const SIDEBAR_TAB_ID = 'agent-sidecar:monitor'
|
|
|
54
33
|
/** `+`-menu order: after the built-ins (explorer=10 … browser=50). */
|
|
55
34
|
export const SIDEBAR_TAB_ORDER = 60
|
|
56
35
|
|
|
57
|
-
/** Compact view caps the session list at this many rows (task spec: 3-5). */
|
|
58
|
-
export const MAX_RECENT_SESSIONS = 5
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
36
|
// Duck-typed better-sidebar contract (locally restated, never imported).
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
37
|
|
|
64
38
|
/** Props better-sidebar hands every tab component; only `visible` is used. */
|
|
65
39
|
export interface SidebarTabComponentProps {
|
|
@@ -98,9 +72,7 @@ export function probeBetterSidebar(candidate: unknown): BetterSidebarServiceFace
|
|
|
98
72
|
: null
|
|
99
73
|
}
|
|
100
74
|
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
75
|
// Visibility gate over the shared controller.
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
76
|
|
|
105
77
|
/** The slice of {@link SidecarController} this view consumes (tests fake it). */
|
|
106
78
|
export type SidecarStateSource = Pick<SidecarController, 'subscribe' | 'getState'>
|
|
@@ -169,214 +141,10 @@ export class VisibleGatedStore {
|
|
|
169
141
|
}
|
|
170
142
|
}
|
|
171
143
|
|
|
172
|
-
//
|
|
173
|
-
// Pure view model (exported for tests).
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
/** Count of sessions currently observed as waiting. */
|
|
177
|
-
export function countWaiting(sessions: ReadonlyArray<{ status: string }>): number {
|
|
178
|
-
let count = 0
|
|
179
|
-
for (const session of sessions) {
|
|
180
|
-
if (normalizeStatus(session.status) === 'waiting') count += 1
|
|
181
|
-
}
|
|
182
|
-
return count
|
|
183
|
-
}
|
|
144
|
+
// React integration adapter.
|
|
184
145
|
|
|
185
146
|
/**
|
|
186
|
-
*
|
|
187
|
-
* at {@link MAX_RECENT_SESSIONS} (ties break by session id for stability).
|
|
188
|
-
*/
|
|
189
|
-
export function recentActiveSessions<T extends SessionCardVM>(
|
|
190
|
-
sessions: readonly T[],
|
|
191
|
-
limit: number = MAX_RECENT_SESSIONS,
|
|
192
|
-
): T[] {
|
|
193
|
-
return sessions
|
|
194
|
-
.filter((session) => normalizeStatus(session.status) !== 'dead')
|
|
195
|
-
.sort((a, b) =>
|
|
196
|
-
b.updatedAtMs !== a.updatedAtMs
|
|
197
|
-
? b.updatedAtMs - a.updatedAtMs
|
|
198
|
-
: a.sessionId.localeCompare(b.sessionId))
|
|
199
|
-
.slice(0, limit)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Everything the mini tab renders. */
|
|
203
|
-
export interface SidebarMiniVM {
|
|
204
|
-
connection: WidgetConnection
|
|
205
|
-
/** Hover text for the header dot (connection + working count). */
|
|
206
|
-
connectionTitle: string
|
|
207
|
-
workingCount: number
|
|
208
|
-
waitingCount: number
|
|
209
|
-
recent: SessionCardVM[]
|
|
210
|
-
hasSnapshot: boolean
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Fold the shared view state into the mini tab's view model (pure). */
|
|
214
|
-
export function deriveMiniVM(state: SidecarViewState): SidebarMiniVM {
|
|
215
|
-
const connection = deriveWidgetConnection(state.daemonState, state.streamHealth)
|
|
216
|
-
const workingCount = countWorking(state.sessions)
|
|
217
|
-
return {
|
|
218
|
-
connection,
|
|
219
|
-
connectionTitle: widgetTitle(connection, workingCount),
|
|
220
|
-
workingCount,
|
|
221
|
-
waitingCount: countWaiting(state.sessions),
|
|
222
|
-
recent: recentActiveSessions(state.sessions),
|
|
223
|
-
hasSnapshot: state.hasSnapshot,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Presentation.
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
const STATUS_LABEL_KEY: Record<SessionStatusToken, SidecarLocaleKey> = {
|
|
232
|
-
working: 'detail.status.working',
|
|
233
|
-
waiting: 'detail.status.waiting',
|
|
234
|
-
idle: 'detail.status.idle',
|
|
235
|
-
dead: 'detail.status.dead',
|
|
236
|
-
unknown: 'detail.status.unknown',
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const DOT_COLORS: Record<WidgetConnection, string> = {
|
|
240
|
-
ok: 'var(--dsw-alias-state-success-primary, #1a7f37)',
|
|
241
|
-
degraded: 'var(--dsw-alias-state-warn-primary, #9a6700)',
|
|
242
|
-
off: 'var(--dsw-alias-label-dimmed, #8c959f)',
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const rootStyle: CSSProperties = {
|
|
246
|
-
display: 'flex',
|
|
247
|
-
flexDirection: 'column',
|
|
248
|
-
gap: 8,
|
|
249
|
-
padding: '10px 12px',
|
|
250
|
-
fontSize: 12,
|
|
251
|
-
color: 'var(--dsw-alias-label-primary, #1f2328)',
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const headerStyle: CSSProperties = {
|
|
255
|
-
display: 'flex',
|
|
256
|
-
alignItems: 'center',
|
|
257
|
-
gap: 6,
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const dotStyle = (connection: WidgetConnection): CSSProperties => ({
|
|
261
|
-
width: 8,
|
|
262
|
-
height: 8,
|
|
263
|
-
borderRadius: '50%',
|
|
264
|
-
flex: 'none',
|
|
265
|
-
background: DOT_COLORS[connection],
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
const countsStyle: CSSProperties = {
|
|
269
|
-
fontVariantNumeric: 'tabular-nums',
|
|
270
|
-
color: 'var(--dsw-alias-label-secondary, #57606a)',
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const sectionTitleStyle: CSSProperties = {
|
|
274
|
-
fontSize: 11,
|
|
275
|
-
fontWeight: 600,
|
|
276
|
-
color: 'var(--dsw-alias-label-secondary, #57606a)',
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const rowStyle: CSSProperties = {
|
|
280
|
-
display: 'flex',
|
|
281
|
-
alignItems: 'center',
|
|
282
|
-
gap: 6,
|
|
283
|
-
width: '100%',
|
|
284
|
-
padding: '4px 6px',
|
|
285
|
-
border: 'none',
|
|
286
|
-
borderRadius: 6,
|
|
287
|
-
background: 'transparent',
|
|
288
|
-
cursor: 'pointer',
|
|
289
|
-
font: 'inherit',
|
|
290
|
-
textAlign: 'left',
|
|
291
|
-
color: 'inherit',
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const rowTitleStyle: CSSProperties = {
|
|
295
|
-
flex: 1,
|
|
296
|
-
minWidth: 0,
|
|
297
|
-
overflow: 'hidden',
|
|
298
|
-
textOverflow: 'ellipsis',
|
|
299
|
-
whiteSpace: 'nowrap',
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const rowMetaStyle: CSSProperties = {
|
|
303
|
-
flex: 'none',
|
|
304
|
-
fontSize: 11,
|
|
305
|
-
color: 'var(--dsw-alias-label-dimmed, #8c959f)',
|
|
306
|
-
whiteSpace: 'nowrap',
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const detailStyle: CSSProperties = {
|
|
310
|
-
margin: '0 6px 4px 20px',
|
|
311
|
-
fontSize: 11,
|
|
312
|
-
lineHeight: '16px',
|
|
313
|
-
color: 'var(--dsw-alias-label-secondary, #57606a)',
|
|
314
|
-
overflowWrap: 'anywhere',
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const mutedStyle: CSSProperties = {
|
|
318
|
-
color: 'var(--dsw-alias-label-dimmed, #8c959f)',
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const hintStyle: CSSProperties = {
|
|
322
|
-
fontSize: 11,
|
|
323
|
-
color: 'var(--dsw-alias-label-dimmed, #8c959f)',
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** One session row + its inline expansion (last event, project, id). */
|
|
327
|
-
function SessionRow(props: {
|
|
328
|
-
session: SessionCardVM
|
|
329
|
-
nowMs: number
|
|
330
|
-
expanded: boolean
|
|
331
|
-
onToggle: () => void
|
|
332
|
-
}): ReactElement {
|
|
333
|
-
const { session, nowMs, expanded } = props
|
|
334
|
-
const status = normalizeStatus(session.status)
|
|
335
|
-
const title = session.title.trim() === '' ? t('sidebar.untitled') : session.title
|
|
336
|
-
const rowChildren: ReactNode[] = [
|
|
337
|
-
h('span', { key: 'glyph', 'aria-hidden': true }, agentGlyph(session.agent)),
|
|
338
|
-
h('span', { key: 'title', style: rowTitleStyle, title: session.sessionId }, title),
|
|
339
|
-
h(
|
|
340
|
-
'span',
|
|
341
|
-
{ key: 'meta', style: rowMetaStyle },
|
|
342
|
-
`${t(STATUS_LABEL_KEY[status])} · ${formatRelativeTime(session.updatedAtMs, nowMs)}`,
|
|
343
|
-
),
|
|
344
|
-
]
|
|
345
|
-
const children: ReactNode[] = [
|
|
346
|
-
h(
|
|
347
|
-
'button',
|
|
348
|
-
{
|
|
349
|
-
key: 'row',
|
|
350
|
-
type: 'button',
|
|
351
|
-
style: rowStyle,
|
|
352
|
-
onClick: props.onToggle,
|
|
353
|
-
'data-testid': 'agent-sidecar-sidebar-session',
|
|
354
|
-
'data-session-id': session.sessionId,
|
|
355
|
-
'data-status': status,
|
|
356
|
-
'aria-expanded': expanded,
|
|
357
|
-
},
|
|
358
|
-
...rowChildren,
|
|
359
|
-
),
|
|
360
|
-
]
|
|
361
|
-
if (expanded) {
|
|
362
|
-
const lastEvent =
|
|
363
|
-
session.lastEvent === null
|
|
364
|
-
? h('span', { style: mutedStyle }, t('sidebar.noEvent'))
|
|
365
|
-
: `${session.lastEvent.kind}: ${session.lastEvent.text}`
|
|
366
|
-
children.push(
|
|
367
|
-
h(
|
|
368
|
-
'div',
|
|
369
|
-
{ key: 'detail', style: detailStyle, 'data-testid': 'agent-sidecar-sidebar-detail' },
|
|
370
|
-
h('div', { key: 'project' }, projectDisplayName(session.project)),
|
|
371
|
-
h('div', { key: 'event' }, lastEvent),
|
|
372
|
-
),
|
|
373
|
-
)
|
|
374
|
-
}
|
|
375
|
-
return h('li', { style: { listStyle: 'none' } }, ...children)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Build the tab component bound to the shared controller. One
|
|
147
|
+
* Bind the presentation-only tab to the shared controller. One
|
|
380
148
|
* {@link VisibleGatedStore} per mounted tab instance: the `visible` prop is
|
|
381
149
|
* synced into it by effect, unmount disposes it.
|
|
382
150
|
*/
|
|
@@ -392,56 +160,11 @@ export function createSidebarTabComponent(
|
|
|
392
160
|
gate.setVisible(visible)
|
|
393
161
|
}, [gate, visible])
|
|
394
162
|
const state = useSyncExternalStore(gate.subscribe, gate.getState, gate.getState)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const vm = deriveMiniVM(state)
|
|
398
|
-
const nowMs = Date.now()
|
|
399
|
-
|
|
400
|
-
let body: ReactNode
|
|
401
|
-
if (!vm.hasSnapshot) {
|
|
402
|
-
body = h('div', { style: mutedStyle }, t('sidebar.connecting'))
|
|
403
|
-
} else if (vm.recent.length === 0) {
|
|
404
|
-
body = h('div', { style: mutedStyle }, t('sidebar.noSessions'))
|
|
405
|
-
} else {
|
|
406
|
-
body = h(
|
|
407
|
-
'ul',
|
|
408
|
-
{ style: { margin: 0, padding: 0 } },
|
|
409
|
-
...vm.recent.map((session) =>
|
|
410
|
-
h(SessionRow, {
|
|
411
|
-
key: session.sessionId,
|
|
412
|
-
session,
|
|
413
|
-
nowMs,
|
|
414
|
-
expanded: expandedId === session.sessionId,
|
|
415
|
-
onToggle: () => {
|
|
416
|
-
setExpandedId((prev) => (prev === session.sessionId ? null : session.sessionId))
|
|
417
|
-
},
|
|
418
|
-
})),
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return h(
|
|
423
|
-
'div',
|
|
424
|
-
{ style: rootStyle, 'data-testid': 'agent-sidecar-sidebar-tab', 'data-visible': visible },
|
|
425
|
-
h(
|
|
426
|
-
'div',
|
|
427
|
-
{ style: headerStyle, title: vm.connectionTitle },
|
|
428
|
-
h('span', { 'aria-hidden': true, style: dotStyle(vm.connection) }),
|
|
429
|
-
h(
|
|
430
|
-
'span',
|
|
431
|
-
{ style: countsStyle, 'data-testid': 'agent-sidecar-sidebar-counts' },
|
|
432
|
-
t('sidebar.countsRow', { working: vm.workingCount, waiting: vm.waitingCount }),
|
|
433
|
-
),
|
|
434
|
-
),
|
|
435
|
-
h('div', { style: sectionTitleStyle }, t('sidebar.recentTitle')),
|
|
436
|
-
body,
|
|
437
|
-
h('div', { style: hintStyle }, t('sidebar.boardHint')),
|
|
438
|
-
)
|
|
163
|
+
return h(SidebarTab, { vm: deriveMiniVM(state), visible })
|
|
439
164
|
}
|
|
440
165
|
}
|
|
441
166
|
|
|
442
|
-
// ---------------------------------------------------------------------------
|
|
443
167
|
// Mount.
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
168
|
|
|
446
169
|
/** Context slice {@link mountSidebarTab} consumes (tests hand in a fake). */
|
|
447
170
|
export interface SidebarMountContext {
|
|
@@ -454,9 +177,9 @@ export interface SidebarMountContext {
|
|
|
454
177
|
* installed → the inject fiber never activates (silent skip, one debug
|
|
455
178
|
* line, zero resources). Installed (before or after this plugin, order
|
|
456
179
|
* does not matter) → duck-type the service and register the mini tab
|
|
457
|
-
* inside `ctx.effect`, so plugin unload / HMR unregisters it.
|
|
458
|
-
*
|
|
459
|
-
*
|
|
180
|
+
* inside `ctx.effect`, so plugin unload / HMR unregisters it. During overlap,
|
|
181
|
+
* the new fiber waits briefly for the old tab to leave, then contributes its
|
|
182
|
+
* own component closure. Foreign collisions time out without eviction.
|
|
460
183
|
*/
|
|
461
184
|
export function mountSidebarTab(ctx: ClientContext, controller: SidecarStateSource): void {
|
|
462
185
|
const mount = ctx as unknown as SidebarMountContext
|
|
@@ -474,21 +197,24 @@ export function mountSidebarTab(ctx: ClientContext, controller: SidecarStateSour
|
|
|
474
197
|
return
|
|
475
198
|
}
|
|
476
199
|
const component = createSidebarTabComponent(controller)
|
|
477
|
-
bctx.effect(() =>
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
200
|
+
bctx.effect(() => acquireWithHandoff(
|
|
201
|
+
() => service.registerTab({
|
|
202
|
+
id: SIDEBAR_TAB_ID,
|
|
203
|
+
title: () => t('sidebar.tabTitle'),
|
|
204
|
+
icon: (size: number) => h(SidebarTabIcon, { size }),
|
|
205
|
+
order: SIDEBAR_TAB_ORDER,
|
|
206
|
+
single: true,
|
|
207
|
+
component,
|
|
208
|
+
}),
|
|
209
|
+
{
|
|
210
|
+
isCollision: isRegistrationCollision,
|
|
211
|
+
onError: (error) => {
|
|
212
|
+
console.error('agent-sidecar: better-sidebar tab registration failed', error)
|
|
213
|
+
},
|
|
214
|
+
onTimeout: () => {
|
|
215
|
+
console.error('agent-sidecar: better-sidebar tab handoff timed out')
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
), 'agent-sidecar: better-sidebar tab')
|
|
493
219
|
})
|
|
494
220
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* L3 of the theming interface (design agent_center_native_ui.md §4).
|
|
3
|
+
* `surfaceProps()` applies this root class together with stable L2 anchors.
|
|
4
|
+
*
|
|
5
|
+
* Every color and shadow delegates to an official --dsw-* token, so the
|
|
6
|
+
* plugin follows the host theme and Skin Center without owning a parallel
|
|
7
|
+
* palette. Users, skins, or other plugins can override one surface:
|
|
8
|
+
*
|
|
9
|
+
* [data-dsh-plugin='agent-sidecar'] { --agsc-accent: var(--skin-accent); }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
.root {
|
|
13
|
+
--agsc-accent: var(--dsw-alias-brand-primary);
|
|
14
|
+
--agsc-bg: var(--dsw-alias-bg-layer-1);
|
|
15
|
+
--agsc-bg-raised: var(--dsw-alias-bg-layer-3);
|
|
16
|
+
--agsc-fg: var(--dsw-alias-label-primary);
|
|
17
|
+
--agsc-fg-secondary: var(--dsw-alias-label-secondary);
|
|
18
|
+
--agsc-fg-dimmed: var(--dsw-alias-label-dimmed);
|
|
19
|
+
--agsc-border: var(--dsw-alias-border-l1);
|
|
20
|
+
--agsc-border-strong: var(--dsw-alias-border-l2);
|
|
21
|
+
--agsc-ok: var(--dsw-alias-state-success-primary);
|
|
22
|
+
--agsc-warn: var(--dsw-alias-state-warn-primary);
|
|
23
|
+
--agsc-err: var(--dsw-alias-state-error-primary);
|
|
24
|
+
--agsc-radius-card: 12px;
|
|
25
|
+
--agsc-radius-control: 8px;
|
|
26
|
+
--agsc-shadow-card: var(--dsw-shadow-lv2);
|
|
27
|
+
--agsc-font-mono: var(--ds-font-family-code);
|
|
28
|
+
|
|
29
|
+
color: var(--agsc-fg);
|
|
30
|
+
font-family: inherit;
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import theme from './agsc.module.css'
|
|
2
|
+
|
|
3
|
+
/** Stable owner value for Agent Sidecar skin anchors. */
|
|
4
|
+
export const PLUGIN_DOM_ID = 'agent-sidecar'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Closed public registry of bare `data-dsh-part` values.
|
|
8
|
+
* Adding an entry changes the theming contract and requires tests + docs.
|
|
9
|
+
*/
|
|
10
|
+
export const SIDECAR_PARTS = Object.freeze([
|
|
11
|
+
'board',
|
|
12
|
+
'board-toolbar',
|
|
13
|
+
'board-card',
|
|
14
|
+
'project-view',
|
|
15
|
+
'detail',
|
|
16
|
+
'timeline',
|
|
17
|
+
'inject-panel',
|
|
18
|
+
'analysis-panel',
|
|
19
|
+
'dsh-tools',
|
|
20
|
+
'footer-widget',
|
|
21
|
+
'sidebar-entry',
|
|
22
|
+
'settings-card',
|
|
23
|
+
'overlay',
|
|
24
|
+
'sidebar-tab',
|
|
25
|
+
] as const)
|
|
26
|
+
|
|
27
|
+
export type SidecarPart = (typeof SIDECAR_PARTS)[number]
|
|
28
|
+
|
|
29
|
+
export interface SurfaceProps {
|
|
30
|
+
readonly className: string
|
|
31
|
+
readonly 'data-dsh-plugin': typeof PLUGIN_DOM_ID
|
|
32
|
+
readonly 'data-dsh-part': SidecarPart
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireThemeClass(name: string): string {
|
|
36
|
+
const className = theme[name]
|
|
37
|
+
if (className === undefined || className.length === 0) {
|
|
38
|
+
throw new Error(`Agent Sidecar theme is missing its ${name} class`)
|
|
39
|
+
}
|
|
40
|
+
return className
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rootClassName = requireThemeClass('root')
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the complete, spreadable contract for one mounted surface.
|
|
47
|
+
* This keeps hashed CSS classes internal while exposing stable DSH anchors.
|
|
48
|
+
*/
|
|
49
|
+
export function surfaceProps(part: SidecarPart, className?: string): SurfaceProps {
|
|
50
|
+
const localClassName = className?.trim()
|
|
51
|
+
return {
|
|
52
|
+
className: localClassName ? `${rootClassName} ${localClassName}` : rootClassName,
|
|
53
|
+
'data-dsh-plugin': PLUGIN_DOM_ID,
|
|
54
|
+
'data-dsh-part': part,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI composition ports for the board and session-detail surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Views depend on these narrow factory seams instead of owning the
|
|
5
|
+
* application assembly contract. The default factory binds the existing
|
|
6
|
+
* stores; callers can provide alternate port implementations in tests or
|
|
7
|
+
* other materializations.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AnalysisStore } from './analysis-glue.ts'
|
|
13
|
+
import { DetailStore, type DetailHeaderHint } from './detail-glue.ts'
|
|
14
|
+
import type { InjectActions } from './inject-glue.ts'
|
|
15
|
+
import type { InjectMode } from './inject/logic.ts'
|
|
16
|
+
import { ProjectsStore } from './project-glue.ts'
|
|
17
|
+
import { SearchStore } from './search-glue.ts'
|
|
18
|
+
|
|
19
|
+
/** Detail-store capabilities consumed by the session-detail surface. */
|
|
20
|
+
export type DetailStorePort = Pick<
|
|
21
|
+
DetailStore,
|
|
22
|
+
| 'subscribe'
|
|
23
|
+
| 'getState'
|
|
24
|
+
| 'open'
|
|
25
|
+
| 'loadMore'
|
|
26
|
+
| 'toggleListen'
|
|
27
|
+
| 'refreshNewest'
|
|
28
|
+
| 'notifySnapshot'
|
|
29
|
+
| 'dispose'
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
/** Search-store capabilities consumed by the detail tools surface. */
|
|
33
|
+
export type SearchStorePort = Pick<
|
|
34
|
+
SearchStore,
|
|
35
|
+
'subscribe' | 'getState' | 'setQuery' | 'submit' | 'dispose'
|
|
36
|
+
>
|
|
37
|
+
|
|
38
|
+
/** Analysis-store capabilities consumed by the analysis panel. */
|
|
39
|
+
export type AnalysisStorePort = Pick<
|
|
40
|
+
AnalysisStore,
|
|
41
|
+
'subscribe' | 'getState' | 'start' | 'followup' | 'stop' | 'dispose'
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
/** Project-store capabilities consumed by the project view. */
|
|
45
|
+
export type ProjectsStorePort = Pick<
|
|
46
|
+
ProjectsStore,
|
|
47
|
+
'subscribe' | 'getState' | 'refresh' | 'notifySnapshot' | 'dispose'
|
|
48
|
+
>
|
|
49
|
+
|
|
50
|
+
/** Optional injection capability consumed by the detail surface. */
|
|
51
|
+
export interface InjectUiPort {
|
|
52
|
+
actions: InjectActions
|
|
53
|
+
/** Read when the panel opens so late settings updates are observed. */
|
|
54
|
+
getDefaultMode: () => InjectMode
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Dependencies required to assemble one session-detail surface. */
|
|
58
|
+
export interface DetailUiPort {
|
|
59
|
+
inject?: InjectUiPort
|
|
60
|
+
/** Live settings-backed analysis gate; defaults remain fail-closed. */
|
|
61
|
+
getAnalysisEnabled: () => boolean
|
|
62
|
+
createDetailStore: (sessionId: string, hint: DetailHeaderHint | null) => DetailStorePort
|
|
63
|
+
createSearchStore: () => SearchStorePort
|
|
64
|
+
createAnalysisStore: () => AnalysisStorePort
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Dependencies required by the board and the detail route it opens. */
|
|
68
|
+
export interface BoardUiPort {
|
|
69
|
+
detail: DetailUiPort
|
|
70
|
+
createProjectsStore: () => ProjectsStorePort
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type DetailIntegrationBase = Pick<DetailUiPort, 'inject' | 'getAnalysisEnabled'>
|
|
74
|
+
|
|
75
|
+
/** Bind the production store implementations at the client composition root. */
|
|
76
|
+
export function createDefaultIntegration(base: DetailIntegrationBase): BoardUiPort {
|
|
77
|
+
return {
|
|
78
|
+
detail: {
|
|
79
|
+
...base,
|
|
80
|
+
createDetailStore: (sessionId, hint) => new DetailStore(sessionId, { hint }),
|
|
81
|
+
createSearchStore: () => new SearchStore(),
|
|
82
|
+
createAnalysisStore: () => new AnalysisStore(),
|
|
83
|
+
},
|
|
84
|
+
createProjectsStore: () => new ProjectsStore(),
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/client/widget.tsx
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { CSSProperties, ReactElement } from 'react'
|
|
15
15
|
import { widgetTitle, type WidgetConnection } from './board/logic.ts'
|
|
16
|
+
import { surfaceProps } from './theme/parts.ts'
|
|
16
17
|
|
|
17
18
|
export interface SidecarWidgetProps {
|
|
18
19
|
connection: WidgetConnection
|
|
@@ -22,9 +23,9 @@ export interface SidecarWidgetProps {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const DOT_COLORS: Record<WidgetConnection, string> = {
|
|
25
|
-
ok: 'var(--
|
|
26
|
-
degraded: 'var(--
|
|
27
|
-
off: 'var(--
|
|
26
|
+
ok: 'var(--agsc-ok)',
|
|
27
|
+
degraded: 'var(--agsc-warn)',
|
|
28
|
+
off: 'var(--agsc-fg-dimmed)',
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const rootStyle: CSSProperties = {
|
|
@@ -36,7 +37,7 @@ const rootStyle: CSSProperties = {
|
|
|
36
37
|
borderRadius: 6,
|
|
37
38
|
background: 'transparent',
|
|
38
39
|
font: 'inherit',
|
|
39
|
-
color: 'var(--
|
|
40
|
+
color: 'var(--agsc-fg-secondary)',
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const countStyle: CSSProperties = {
|
|
@@ -49,10 +50,8 @@ const countStyle: CSSProperties = {
|
|
|
49
50
|
/**
|
|
50
51
|
* Connection dot + working-session counter (e.g. `▸2`) for the footer.
|
|
51
52
|
*
|
|
52
|
-
* Without
|
|
53
|
-
*
|
|
54
|
-
* nothing would be a lie. The button form returns as soon as the
|
|
55
|
-
* integration layer supplies the callback.
|
|
53
|
+
* Without `onOpen` the widget remains an inert status `<span>` so button
|
|
54
|
+
* semantics are only emitted for an actual control.
|
|
56
55
|
*/
|
|
57
56
|
export function SidecarWidget(props: SidecarWidgetProps): ReactElement {
|
|
58
57
|
const title = widgetTitle(props.connection, props.workingCount)
|
|
@@ -78,6 +77,7 @@ export function SidecarWidget(props: SidecarWidgetProps): ReactElement {
|
|
|
78
77
|
if (props.onOpen === undefined) {
|
|
79
78
|
return (
|
|
80
79
|
<span
|
|
80
|
+
{...surfaceProps('footer-widget')}
|
|
81
81
|
style={rootStyle}
|
|
82
82
|
title={title}
|
|
83
83
|
aria-label={title}
|
|
@@ -91,6 +91,7 @@ export function SidecarWidget(props: SidecarWidgetProps): ReactElement {
|
|
|
91
91
|
}
|
|
92
92
|
return (
|
|
93
93
|
<button
|
|
94
|
+
{...surfaceProps('footer-widget')}
|
|
94
95
|
type="button"
|
|
95
96
|
style={{ ...rootStyle, cursor: 'pointer' }}
|
|
96
97
|
title={title}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/* Modal host for the inject panel (S5 wiring): a centered dialog over the
|
|
2
|
-
* board, opened by selecting a session card. Backdrop rides the dsh mask
|
|
3
|
-
* token --dsw-alias-bg-mask-1 (the ui-attachment / ui-settings-general
|
|
4
|
-
* modal precedent) — same no-literal-colors discipline as the panel. */
|
|
5
|
-
|
|
6
|
-
.backdrop {
|
|
7
|
-
position: fixed;
|
|
8
|
-
inset: 0;
|
|
9
|
-
z-index: 1000;
|
|
10
|
-
display: flex;
|
|
11
|
-
align-items: center;
|
|
12
|
-
justify-content: center;
|
|
13
|
-
padding: 24px;
|
|
14
|
-
background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.45));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.dialog {
|
|
18
|
-
width: min(560px, 100%);
|
|
19
|
-
max-height: min(85vh, 720px);
|
|
20
|
-
overflow: auto;
|
|
21
|
-
border-radius: 12px;
|
|
22
|
-
}
|