feat(v1.1.0): adapt to DSH 2.0.10 (0.1.5-rc.2), drop apes:tasks, DSW token styles

C1-C5 env repair:
- align all @deepseek-ai/dsh-* peer+devDeps to 0.1.5-rc.2 (DSH 2.0.10 ships it)
- fix 0.1.5-rc.2 npm packaging defect: 15 primitives runtime deps (anser,
  shiki, katex, marked, ... ) were mis-declared as devDeps in the published
  package -> add to local devDependencies so vitest can resolve them
- fix sidechat-seed contract: dsh-session@0.1.5 assertAssistantSettlementShape
  now requires a top-level data.stream: Array on assistant messages -> seed
  helper emits stream: []

C6 slim tabs:
- remove apes:tasks (overlaps the official DSH session tree in the native
  sidebar); keep apes:agents / apes:apps / apes:profile
- clean dead code: ApesTasksView + job helpers import + apes i18n dead keys
  (tasks/status/duration) and the unused menu-title keys

C7 style align:
- apes view inline colors -> DSW alias tokens (--dsw-alias-bg/border/label/
  state-success/interactive) with literal fallbacks; geometry (font/spacing/
  radius) stays inline (DSH exposes no --ds-* geometry tokens to plugin panels)

gate: typecheck ok / build ok / test 119 files · 1249 passed
This commit is contained in:
figmar
2026-09-16 01:57:46 +08:00
parent 34e86141bd
commit aa28c1fbc7
8 changed files with 526 additions and 792 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
/**
* apes:* information-layer tabs — the fork's own contribution on top of the
* upstream dsh-better-sidebar. Three tabs ship in the first round (tasks /
* agents / profile); apps lands in Phase 1b, messages is deferred (no DSH
* upstream dsh-better-sidebar. Three tabs ship (agents / apps / profile);
* `apes:tasks` was removed (C6) because it overlaps the official DSH session
* tree in the native sidebar, and `messages` is deferred (no DSH
* message-stream API yet — tracked separately).
*
* Isolated under `src/client/apes/` so the upstream `builtins/` tree stays
-38
View File
@@ -16,31 +16,10 @@ export const APES_LOCALE_NS = 'apes'
/** The zh dictionary (source of truth). */
export const apesZh = {
// + menu tab titles
apesTasks: '任务',
apesAgents: '智能体',
apesApps: '应用',
apesProfile: '画像',
// View titles
apesTasksTitle: '任务',
apesAgentsTitle: '智能体',
apesAppsTitle: '应用',
apesProfileTitle: '画像',
// Tasks view
apesTasksEmpty: '暂无后台任务',
apesTasksLiveCount: '{count} 个运行中',
apesTasksOwner: '来自 {owner}',
apesTaskKind: '{kind}',
// Job statuses
apesStatusRunning: '运行中',
apesStatusStopping: '停止中',
apesStatusCompleted: '已完成',
apesStatusKilled: '已终止',
apesStatusFailed: '失败',
// Duration (at most two adjacent units)
apesDurationHours: '{hours} 小时 {minutes} 分',
apesDurationMinutes: '{minutes} 分 {seconds} 秒',
apesDurationSeconds: '{seconds} 秒',
// Agents view
apesAgentsEmpty: '当前会话没有子智能体',
apesAgentsRoot: '主智能体',
@@ -61,26 +40,9 @@ export const apesZh = {
/** The en dictionary (checked against zh — every zh key must appear here). */
export const apesEn: Record<keyof typeof apesZh, string> = {
apesTasks: 'Tasks',
apesAgents: 'Agents',
apesApps: 'Apps',
apesProfile: 'Profile',
apesTasksTitle: 'Tasks',
apesAgentsTitle: 'Agents',
apesAppsTitle: 'Apps',
apesProfileTitle: 'Profile',
apesTasksEmpty: 'No background jobs',
apesTasksLiveCount: '{count} running',
apesTasksOwner: 'from {owner}',
apesTaskKind: '{kind}',
apesStatusRunning: 'running',
apesStatusStopping: 'stopping',
apesStatusCompleted: 'completed',
apesStatusKilled: 'killed',
apesStatusFailed: 'failed',
apesDurationHours: '{hours}h {minutes}m',
apesDurationMinutes: '{minutes}m {seconds}s',
apesDurationSeconds: '{seconds}s',
apesAgentsEmpty: 'No subagents in this session',
apesAgentsRoot: 'Main agent',
apesAgentRunning: 'running',
+50 -120
View File
@@ -1,91 +1,71 @@
/**
* apes:* tab views (D2 = data wiring). Each tab reads live data from the
* sessions list feed (`ctx.sessions.list`) via `useSyncExternalStore` and
* reuses the fork's PURE lineage/job helpers (rootAncestor, treeSessionIds,
* collectTreeJobs, orderJobs, isJobLive, jobDotState) — these are framework-free
* and live outside `builtins/`, so importing them does not couple the apes
* module to upstream UI and survives cherry-picks. UI is inline-styled and
* self-contained (no CSS modules, no `@deepseek-ai/dsh-client-ui-primitives`)
* so the module stays isolated under `src/client/apes/`.
* reuses the fork's PURE lineage/job helpers — these are framework-free and
* live outside `builtins/`, so importing them does not couple the apes module
* to upstream UI and survives cherry-picks.
*
* Three information-layer tabs (C6: `apes:tasks` removed — it overlaps the
* official DSH session tree in the native sidebar):
* - apes:agents → the subagent topology rooted at the main agent of the
* current session (rootAncestor + per-parent catalogs).
* - apes:apps → no DSH host feed today: shows the workspace (cwd) of the
* current session, otherwise an honest "no feed" note.
* - apes:profile → the current session's summary row (title, cwd, running).
*
* Visual unity (C7): colors use the DSW design-token aliases
* (`--dsw-alias-*`) so the apes panels match the native DSH sidebar
* surface; the runtime defines these on :root. Font sizes / spacing /
* radius stay inline (DSH exposes no public `--ds-*` geometry alias for
* plugin panels — its own vendor bundles inline those too).
*
* Data sources (structural mirror — runtime provided by the DSH host):
* - `ctx.sessions.list.getSnapshot()` → { current, byId, subagentsByParent?,
* jobsBySession? }; `subscribe(cb)` is uSES-safe.
* - `ctx.sessions.list.getSnapshot()` → { current, byId, subagentsByParent? };
* `subscribe(cb)` is uSES-safe.
* - `scope.sessionId` = the currently-open session.
*
* apes:tasks → jobsBySession across the current tree (owner-labeled,
* live-first order) + a running-session count.
* apes:agents → the subagent topology rooted at the main agent of the
* current session (rootAncestor + per-parent catalogs).
* apes:profile → the current session's summary row (title, cwd, running).
* apes:apps → no DSH host feed today: shows the workspace (cwd) of the
* current session, otherwise an honest "no feed" note.
*/
import { useCallback, useMemo, useState, type CSSProperties } from 'react'
import { useSyncExternalStore } from 'react'
import { FiList, FiCpu, FiGrid, FiUser } from 'react-icons/fi'
import { FiCpu, FiGrid, FiUser } from 'react-icons/fi'
import type { TabComponentProps, TabDescriptor } from '../service.ts'
import type {
SidebarSessionList,
SidebarSessionSummary,
SidebarJobView,
SidebarJobStatus,
SidebarSubagentCatalog,
SidebarSubagentChildEntry,
} from '../../context-types.ts'
import { rootAncestor, isSideThreadSummary } from '../subagent-lineage.ts'
import { collectTreeJobs, orderJobs, isJobLive, jobDotState } from '../subagent-jobs.ts'
import { t } from './locales.ts'
/** Map a job wire status to its apes i18n key. */
function statusKey(status: SidebarJobStatus): 'apesStatusRunning' | 'apesStatusStopping' | 'apesStatusCompleted' | 'apesStatusKilled' | 'apesStatusFailed' {
switch (status) {
case 'running': return 'apesStatusRunning'
case 'stopping': return 'apesStatusStopping'
case 'completed': return 'apesStatusCompleted'
case 'killed': return 'apesStatusKilled'
case 'failed': return 'apesStatusFailed'
}
}
/**
* DSW design-token color aliases. The DSH runtime defines these on :root
* (same set the native dsh-client-ui-* vendor bundles consume). We reference
* them by name via `var(...)` with a literal fallback so the panels still
* render in a non-DSH context (unit harness) with a sane dark palette.
*/
const DSW = {
bgBase: 'var(--dsw-alias-bg-base, #1a1a1a)',
bgLayer1: 'var(--dsw-alias-bg-layer-1, #202020)',
borderL1: 'var(--dsw-alias-border-l1, #2a2a2a)',
borderL2: 'var(--dsw-alias-border-l2, #303030)',
labelPrimary: 'var(--dsw-alias-label-primary, #e8e8e8)',
labelSecondary: 'var(--dsw-alias-label-secondary, #a0a0a0)',
labelTertiary: 'var(--dsw-alias-label-tertiary, #7a7a7a)',
success: 'var(--dsw-alias-state-success-primary, #52c41a)',
interactiveHover: 'var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))',
} as const
/** Dot color for a job status (reuses the fork's jobDotState mapping). */
function dotColor(state: 'ongoing' | 'warning' | 'done' | 'error'): string {
switch (state) {
case 'ongoing': return '#52c41a'
case 'warning': return '#faad14'
case 'done': return '#8c8c8c'
case 'error': return '#ff4d4f'
}
}
/** Elapsed ms of a job (running → now, settled → finishedAt). */
function jobElapsedMs(job: SidebarJobView, now: number): number {
const end = job.finishedAt ?? (isJobLive(job) ? now : job.startedAt)
return Math.max(0, end - job.startedAt)
}
/** Elapsed time in at most two adjacent units (apes-local, no builtin i18n). */
function duration(elapsedMs: number): string {
const total = Math.max(0, Math.floor(elapsedMs / 1_000))
const seconds = total % 60
const minutes = Math.floor(total / 60) % 60
const hours = Math.floor(total / 3_600)
if (hours > 0) return t('apesDurationHours', { hours, minutes })
if (minutes > 0) return t('apesDurationMinutes', { minutes, seconds })
return t('apesDurationSeconds', { seconds })
}
/** Shared inline styles (KISS: one object, no CSS module). */
/** Shared inline styles (KISS: one object, no CSS module). Colors via DSW. */
const S: Record<string, CSSProperties> = {
body: { padding: 12, display: 'flex', flexDirection: 'column', gap: 12 },
title: { margin: 0, fontSize: 14, fontWeight: 600 },
hint: { margin: 0, fontSize: 12, color: '#8c8c8c' },
title: { margin: 0, fontSize: 14, fontWeight: 600, color: DSW.labelPrimary },
hint: { margin: 0, fontSize: 12, color: DSW.labelTertiary },
list: { display: 'flex', flexDirection: 'column', gap: 8 },
card: { border: '1px solid #303030', borderRadius: 8, padding: 10, display: 'flex', flexDirection: 'column', gap: 6 },
card: { border: `1px solid ${DSW.borderL1}`, borderRadius: 8, padding: 10, display: 'flex', flexDirection: 'column', gap: 6, background: DSW.bgLayer1 },
cardRow: { display: 'flex', alignItems: 'center', gap: 8 },
cardLabel: { fontSize: 13, fontWeight: 500, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
cardSecondary: { fontSize: 11, color: '#8c8c8c', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
badge: { fontSize: 11, color: '#8c8c8c', flexShrink: 0 },
cardLabel: { fontSize: 13, fontWeight: 500, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: DSW.labelPrimary },
cardSecondary: { fontSize: 11, color: DSW.labelSecondary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
badge: { fontSize: 11, color: DSW.labelTertiary, flexShrink: 0 },
}
/** A small colored status dot (self-contained, not the builtin StateDot). */
@@ -105,56 +85,6 @@ function useSessionList(ctx: TabComponentProps['ctx']): SidebarSessionList {
)
}
/** A 1-second ticker that only runs while `active` (for live durations). */
function useNow(active: boolean): number {
const [now, setNow] = useState(() => Date.now())
useMemo(() => {
if (!active) return
const id = window.setInterval(() => { setNow(Date.now()) }, 1_000)
return () => { window.clearInterval(id) }
}, [active])
return now
}
/** apes:tasks — the current tree's background jobs (live-first, owner-labeled). */
function ApesTasksView(props: TabComponentProps) {
const list = useSessionList(props.ctx)
const { byId, jobsBySession } = list
const rootId = useMemo(() => rootAncestor(byId, props.scope.sessionId), [byId, props.scope.sessionId])
const rows = useMemo(() => orderJobs(collectTreeJobs(byId, jobsBySession, rootId)), [byId, jobsBySession, rootId])
const liveCount = useMemo(() => rows.reduce((n, row) => n + (isJobLive(row.job) ? 1 : 0), 0), [rows])
const now = useNow(liveCount > 0)
return (
<div style={S.body}>
<h2 style={S.title}>{t('apesTasksTitle')}</h2>
{rows.length === 0
? <p style={S.hint}>{t('apesTasksEmpty')}</p>
: (
<>
<p style={S.hint}>{t('apesTasksLiveCount', { count: liveCount })}</p>
<div style={S.list}>
{rows.map(row => (
<div key={row.job.id} style={S.card}>
<div style={S.cardRow}>
<Dot color={dotColor(jobDotState(row.job.status))} />
<span style={S.cardLabel} title={row.job.label}>{row.job.label}</span>
<span style={S.badge}>{t('apesTaskKind', { kind: row.job.kind })}</span>
</div>
<div style={S.cardRow}>
<span style={S.cardSecondary}>{t('apesTasksOwner', { owner: row.ownerTitle })}</span>
<span style={S.badge}>{duration(jobElapsedMs(row.job, now))}</span>
<span style={S.badge}>{t(statusKey(row.job.status))}</span>
</div>
</div>
))}
</div>
</>
)}
</div>
)
}
/** One level of the subagent topology (rooted at the main agent). */
function agentChildren(
byId: Readonly<Record<string, SidebarSessionSummary>>,
@@ -198,7 +128,7 @@ function ApesAgentsView(props: TabComponentProps) {
{rootSummary !== undefined && (
<div style={S.card}>
<div style={S.cardRow}>
<Dot color={rootSummary.running === true ? '#52c41a' : '#8c8c8c'} />
<Dot color={rootSummary.running === true ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={rootSummary.displayTitle}>{t('apesAgentsRoot')}: {rootSummary.displayTitle}</span>
</div>
<div style={S.cardRow}>
@@ -212,9 +142,9 @@ function ApesAgentsView(props: TabComponentProps) {
const running = entry.activity === 'running'
const label = entry.label ?? summary?.displayTitle ?? entry.id
return (
<div key={entry.id} style={{ ...S.card, marginLeft: 14, borderLeft: '2px solid #303030' }}>
<div key={entry.id} style={{ ...S.card, marginLeft: 14, borderLeft: `2px solid ${DSW.borderL2}` }}>
<div style={S.cardRow}>
<Dot color={running ? '#52c41a' : '#8c8c8c'} />
<Dot color={running ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={label}>{label}</span>
</div>
<div style={S.cardRow}>
@@ -249,7 +179,7 @@ function ApesProfileView(props: TabComponentProps) {
<div style={S.list}>
<div style={S.card}>
<div style={S.cardRow}>
<Dot color={summary.running === true ? '#52c41a' : '#8c8c8c'} />
<Dot color={summary.running === true ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={summary.displayTitle}>{t('apesProfileCurrent')}</span>
</div>
<div style={S.cardRow}>
@@ -294,13 +224,13 @@ function ApesAppsView(props: TabComponentProps) {
}
/**
* The four apes:* tab descriptors (order 900-903: tasks/agents/apps/profile),
* The three apes:* tab descriptors (C6: `apes:tasks` removed — overlaps the
* official DSH session tree). Order 901-903: agents/apps/profile. Each is
* already shaped as `TabDescriptor` (component + single-instance + icon
* element + i18n title function) so `registerApesTabs` can hand them straight
* to `service.registerTab`. Information-layer tabs are single-instance.
*/
export const apesTabs: TabDescriptor[] = [
{ id: 'apes:tasks', title: () => t('apesTasksTitle'), icon: <FiList size={14} />, order: 900, single: true, component: ApesTasksView },
{ id: 'apes:agents', title: () => t('apesAgentsTitle'), icon: <FiCpu size={14} />, order: 901, single: true, component: ApesAgentsView },
{ id: 'apes:apps', title: () => t('apesAppsTitle'), icon: <FiGrid size={14} />, order: 902, single: true, component: ApesAppsView },
{ id: 'apes:profile', title: () => t('apesProfileTitle'), icon: <FiUser size={14} />, order: 903, single: true, component: ApesProfileView },