@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
|
@@ -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
|
+
}
|