@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
@@ -0,0 +1,156 @@
1
+ /** Presentation-only compact view for the optional better-sidebar surface.
2
+ * Integration owns discovery and view-model derivation; this root subscribes
3
+ * once to locale changes for its complete presentation subtree. */
4
+
5
+ import { Button, StateDot, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
6
+ import { useState } from 'react'
7
+ import type { ReactElement } from 'react'
8
+ import {
9
+ agentGlyph,
10
+ formatRelativeTime,
11
+ normalizeStatus,
12
+ projectDisplayName,
13
+ type SessionCardVM,
14
+ type SessionStatusToken,
15
+ } from '../board/logic.ts'
16
+ import { t, type SidecarLocaleKey } from '../locales/index.ts'
17
+ import { useActiveLocale } from '../locales/react.ts'
18
+ import { StaticPill } from '../primitives/StaticPill.tsx'
19
+ import { surfaceProps } from '../theme/parts.ts'
20
+ import type { SidebarMiniVM } from './model.ts'
21
+ import css from './sidebar-tab.module.css'
22
+
23
+ const STATUS_LABEL_KEY: Record<SessionStatusToken, SidecarLocaleKey> = {
24
+ working: 'detail.status.working',
25
+ waiting: 'detail.status.waiting',
26
+ idle: 'detail.status.idle',
27
+ dead: 'detail.status.dead',
28
+ unknown: 'detail.status.unknown',
29
+ }
30
+
31
+ const CONNECTION_DOT_STATE: Record<SidebarMiniVM['connection'], StateDotState> = {
32
+ ok: 'done',
33
+ degraded: 'warning',
34
+ off: 'error',
35
+ }
36
+
37
+ export interface SidebarTabProps {
38
+ vm: SidebarMiniVM
39
+ visible: boolean
40
+ /** Clock injection keeps SSR and focused view tests deterministic. */
41
+ nowMs?: number
42
+ }
43
+
44
+ /** Icon renderer kept beside the view while preserving the descriptor callback API. */
45
+ export function SidebarTabIcon({ size }: { size: number }): ReactElement {
46
+ return (
47
+ <svg className={css['icon']} width={size} height={size} viewBox="0 0 24 24" aria-hidden>
48
+ <path d="M12 2 22 12 12 22 2 12 12 2Zm0 3.4L5.4 12l6.6 6.6 6.6-6.6L12 5.4Z" />
49
+ <circle cx="12" cy="12" r="2.2" />
50
+ </svg>
51
+ )
52
+ }
53
+
54
+ function SessionRow(props: {
55
+ session: SessionCardVM
56
+ nowMs: number
57
+ expanded: boolean
58
+ detailId: string
59
+ onToggle: () => void
60
+ }): ReactElement {
61
+ const { session, nowMs, expanded, detailId } = props
62
+ const status = normalizeStatus(session.status)
63
+ const title = session.title.trim() === '' ? t('sidebar.untitled') : session.title
64
+ const lastEvent =
65
+ session.lastEvent === null
66
+ ? <span className={css['muted']}>{t('sidebar.noEvent')}</span>
67
+ : `${session.lastEvent.kind}: ${session.lastEvent.text}`
68
+
69
+ return (
70
+ <li className={css['sessionItem']}>
71
+ <Button
72
+ type="button"
73
+ size="sm"
74
+ variant="ghost"
75
+ className={css['sessionButton']}
76
+ onClick={props.onToggle}
77
+ data-testid="agent-sidecar-sidebar-session"
78
+ data-session-id={session.sessionId}
79
+ data-status={status}
80
+ aria-expanded={expanded}
81
+ aria-controls={detailId}
82
+ >
83
+ <span className={css['glyph']} aria-hidden>{agentGlyph(session.agent)}</span>
84
+ <span className={css['sessionTitle']} title={session.sessionId}>
85
+ {title}
86
+ </span>
87
+ <span className={css['sessionMeta']}>
88
+ {t(STATUS_LABEL_KEY[status])} · {formatRelativeTime(session.updatedAtMs, nowMs)}
89
+ </span>
90
+ </Button>
91
+ {expanded && (
92
+ <div
93
+ id={detailId}
94
+ className={css['detail']}
95
+ role="region"
96
+ aria-label={title}
97
+ data-testid="agent-sidecar-sidebar-detail"
98
+ >
99
+ <div>{projectDisplayName(session.project)}</div>
100
+ <div>{lastEvent}</div>
101
+ </div>
102
+ )}
103
+ </li>
104
+ )
105
+ }
106
+
107
+ /** Compact better-sidebar body. No controller, service, or transport imports. */
108
+ export function SidebarTab({ vm, visible, nowMs = Date.now() }: SidebarTabProps): ReactElement {
109
+ useActiveLocale()
110
+ const [expandedId, setExpandedId] = useState<string | null>(null)
111
+ const recentTitleId = 'agent-sidecar-sidebar-recent-title'
112
+
113
+ let body: ReactElement
114
+ if (!vm.hasSnapshot) {
115
+ body = <p className={css['muted']}>{t('sidebar.connecting')}</p>
116
+ } else if (vm.recent.length === 0) {
117
+ body = <p className={css['muted']}>{t('sidebar.noSessions')}</p>
118
+ } else {
119
+ body = (
120
+ <ul className={css['sessionList']} aria-labelledby={recentTitleId}>
121
+ {vm.recent.map((session, index) => (
122
+ <SessionRow
123
+ key={session.sessionId}
124
+ session={session}
125
+ nowMs={nowMs}
126
+ expanded={expandedId === session.sessionId}
127
+ detailId={`agent-sidecar-sidebar-detail-${index}`}
128
+ onToggle={() => {
129
+ setExpandedId((previous) => previous === session.sessionId ? null : session.sessionId)
130
+ }}
131
+ />
132
+ ))}
133
+ </ul>
134
+ )
135
+ }
136
+
137
+ return (
138
+ <section
139
+ {...surfaceProps('sidebar-tab', css['root'])}
140
+ data-testid="agent-sidecar-sidebar-tab"
141
+ data-visible={visible}
142
+ aria-label={t('sidebar.tabTitle')}
143
+ aria-hidden={!visible}
144
+ >
145
+ <header className={css['header']} title={vm.connectionTitle}>
146
+ <StaticPill className={css['counts']} data-testid="agent-sidecar-sidebar-counts">
147
+ <StateDot state={CONNECTION_DOT_STATE[vm.connection]} size={8} />
148
+ {t('sidebar.countsRow', { working: vm.workingCount, waiting: vm.waitingCount })}
149
+ </StaticPill>
150
+ </header>
151
+ <h2 id={recentTitleId} className={css['sectionTitle']}>{t('sidebar.recentTitle')}</h2>
152
+ {body}
153
+ <p className={css['hint']}>{t('sidebar.boardHint')}</p>
154
+ </section>
155
+ )
156
+ }
@@ -0,0 +1,65 @@
1
+ /** Pure view-model derivation for the optional compact sidebar. */
2
+
3
+ import type { SidecarViewState } from '../controller.ts'
4
+ import {
5
+ countWorking,
6
+ deriveWidgetConnection,
7
+ normalizeStatus,
8
+ type SessionCardVM,
9
+ type WidgetConnection,
10
+ widgetTitle,
11
+ } from '../board/logic.ts'
12
+
13
+ /** Compact view caps the session list at this many rows. */
14
+ export const MAX_RECENT_SESSIONS = 5
15
+
16
+ /** Count of sessions currently observed as waiting. */
17
+ export function countWaiting(sessions: ReadonlyArray<{ status: string }>): number {
18
+ let count = 0
19
+ for (const session of sessions) {
20
+ if (normalizeStatus(session.status) === 'waiting') count += 1
21
+ }
22
+ return count
23
+ }
24
+
25
+ /**
26
+ * Return non-dead sessions by descending update time, capped for the compact
27
+ * view. Ties break by session id for stability.
28
+ */
29
+ export function recentActiveSessions<T extends SessionCardVM>(
30
+ sessions: readonly T[],
31
+ limit: number = MAX_RECENT_SESSIONS,
32
+ ): T[] {
33
+ return sessions
34
+ .filter((session) => normalizeStatus(session.status) !== 'dead')
35
+ .sort((a, b) =>
36
+ b.updatedAtMs !== a.updatedAtMs
37
+ ? b.updatedAtMs - a.updatedAtMs
38
+ : a.sessionId.localeCompare(b.sessionId))
39
+ .slice(0, limit)
40
+ }
41
+
42
+ /** Everything the mini tab renders. */
43
+ export interface SidebarMiniVM {
44
+ connection: WidgetConnection
45
+ /** Hover text for the header dot (connection + working count). */
46
+ connectionTitle: string
47
+ workingCount: number
48
+ waitingCount: number
49
+ recent: SessionCardVM[]
50
+ hasSnapshot: boolean
51
+ }
52
+
53
+ /** Fold the shared view state into the mini tab's view model. */
54
+ export function deriveMiniVM(state: SidecarViewState): SidebarMiniVM {
55
+ const connection = deriveWidgetConnection(state.daemonState, state.streamHealth)
56
+ const workingCount = countWorking(state.sessions)
57
+ return {
58
+ connection,
59
+ connectionTitle: widgetTitle(connection, workingCount),
60
+ workingCount,
61
+ waitingCount: countWaiting(state.sessions),
62
+ recent: recentActiveSessions(state.sessions),
63
+ hasSnapshot: state.hasSnapshot,
64
+ }
65
+ }
@@ -0,0 +1,118 @@
1
+ /* Compact native sidebar layout over the shared Agent Sidecar token surface. */
2
+
3
+ .root {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 8px;
7
+ min-width: 0;
8
+ padding: 10px 12px;
9
+ box-sizing: border-box;
10
+ overflow-y: auto;
11
+ font-size: 12px;
12
+ color: var(--agsc-fg);
13
+ background: var(--agsc-bg);
14
+ }
15
+
16
+ .header {
17
+ display: flex;
18
+ align-items: center;
19
+ min-width: 0;
20
+ }
21
+
22
+ .counts {
23
+ max-width: 100%;
24
+ font-variant-numeric: tabular-nums;
25
+ color: var(--agsc-fg-secondary);
26
+ }
27
+
28
+ .counts > span {
29
+ color: inherit;
30
+ }
31
+
32
+ .sectionTitle {
33
+ margin: 0;
34
+ font-size: 11px;
35
+ font-weight: 600;
36
+ line-height: 16px;
37
+ color: var(--agsc-fg-secondary);
38
+ }
39
+
40
+ .sessionList {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 2px;
44
+ min-width: 0;
45
+ margin: 0;
46
+ padding: 0;
47
+ list-style: none;
48
+ }
49
+
50
+ .sessionItem {
51
+ min-width: 0;
52
+ list-style: none;
53
+ }
54
+
55
+ .sessionButton {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: flex-start;
59
+ gap: 6px;
60
+ width: 100%;
61
+ height: auto;
62
+ min-width: 0;
63
+ padding: 4px 6px;
64
+ color: var(--agsc-fg);
65
+ text-align: left;
66
+ }
67
+
68
+ .glyph {
69
+ flex: none;
70
+ color: var(--agsc-accent);
71
+ }
72
+
73
+ .sessionTitle {
74
+ flex: 1;
75
+ min-width: 0;
76
+ overflow: hidden;
77
+ color: var(--agsc-fg);
78
+ text-overflow: ellipsis;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .sessionMeta {
83
+ flex: none;
84
+ font-size: 11px;
85
+ color: var(--agsc-fg-dimmed);
86
+ white-space: nowrap;
87
+ }
88
+
89
+ .detail {
90
+ margin: 0 6px 4px 20px;
91
+ overflow-wrap: anywhere;
92
+ font-size: 11px;
93
+ line-height: 16px;
94
+ color: var(--agsc-fg-secondary);
95
+ }
96
+
97
+ .muted {
98
+ margin: 0;
99
+ color: var(--agsc-fg-dimmed);
100
+ }
101
+
102
+ .hint {
103
+ margin: 0;
104
+ font-size: 11px;
105
+ line-height: 16px;
106
+ color: var(--agsc-fg-dimmed);
107
+ }
108
+
109
+ .icon {
110
+ display: block;
111
+ flex: none;
112
+ color: var(--agsc-fg-secondary);
113
+ }
114
+
115
+ .icon path,
116
+ .icon circle {
117
+ fill: currentColor;
118
+ }