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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user