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
+23 -1
View File
@@ -263,7 +263,29 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
## 🆕 最近更新
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.0 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.0 正式版):0.1.5-rc.2+" src="https://img.shields.io/badge/DSH-0.1.5--rc.2%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
### v1.1.0(fork · DSH 2.0.10 / 0.1.5-rc.2 适配 + 精简 + 风格对齐)
> 📌 **DSH 2.0.10 适配**:跟随 DSH 2.0.10 内发的 `@deepseek-ai/dsh-*@0.1.5-rc.2`,peer + devDeps 全量对齐。
**🗑 精简 tab(去 `apes:tasks`)**
- 移除 `apes:tasks` —— 与 DSH 原生侧栏的官方会话树功能重叠,保留 **3 个** `apes:*` 信息层 tab:`apes:agents` / `apes:apps` / `apes:profile`
- 同步清理死代码:`ApesTasksView` + `collectTreeJobs`/`orderJobs`/`isJobLive`/`jobDotState` import + apes i18n 死 key(tasks/status/duration)
**🎨 风格对齐 DSW 设计令牌**
- apes 视图内联样式的颜色全部改用 DSW alias 令牌(`--dsw-alias-bg-*` / `--dsw-alias-border-*` / `--dsw-alias-label-*` / `--dsw-alias-state-success-*` / `--dsw-alias-interactive-*`),与 DSH 原生侧栏视觉统一;`var(...)` 带字面 fallback 保证非 DSH 环境(单测 harness)仍可渲染
- 字号 / 间距 / 圆角保持内联值(DSH 未对插件面板公开 `--ds-*` 几何令牌,其自身 vendor bundle 亦内联)
**🔧 0.1.5-rc.2 适配修复**
- primitives npm 发布缺陷(15 个运行时依赖被误列为 devDeps)→ 补进本地 `devDependencies` 供 vitest 内联解析
- sidechat seed 校验契约升级:`dsh-session@0.1.5` 的 `assertAssistantSettlementShape` 要求 assistant 消息的 `data` 顶层带 `stream: Array` → 测试 seed helper 对齐
- 完整 gate:typecheck ✓ / build ✓ / test 119 files · 1249 passed ✓
---
### v1.0.0(fork · apes 信息层)
+51 -36
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-apes-sidebar",
"version": "1.0.0",
"description": "APE fork of dsh-better-sidebar v0.18.0: VSCode-like right sidebar (explorer / editor / terminal / git / browser) + apes:* information-layer tabs (tasks / agents / apps / messages / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
"version": "1.1.0",
"description": "APE fork of dsh-better-sidebar v0.18.0: VSCode-like right sidebar (explorer / editor / terminal / git / browser) + apes:* information-layer tabs (agents / apps / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
"type": "module",
"repository": {
"type": "git",
@@ -98,19 +98,19 @@
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.2",
"@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
"@deepseek-ai/dsh-session": "^0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -154,26 +154,26 @@
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
"@deepseek-ai/dsh-code-runtime": "0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
"@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
"@deepseek-ai/dsh-util-time": "0.1.2-rc.1",
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
"@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
"@deepseek-ai/dsh-scope": "0.1.5-rc.2",
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
"@deepseek-ai/dsh-session-projection": "0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
"@deepseek-ai/dsh-user-approval": "0.1.5-rc.2",
"@deepseek-ai/dsh-util-time": "0.1.5-rc.2",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1",
"@types/node": "^24.0.0",
@@ -190,6 +190,21 @@
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.2.39",
"vitest": "^4.1.8"
"vitest": "^4.1.8",
"anser": "^2.3.5",
"shiki": "^4.3.1",
"@shikijs/langs": "^4.3.1",
"katex": "^0.16.47",
"mdast-util-from-markdown": "^2.0.3",
"mdast-util-gfm": "^3.1.0",
"mdast-util-math": "^3.0.0",
"micromark-core-commonmark": "^2.0.3",
"micromark-extension-gfm": "^3.0.0",
"micromark-extension-math": "^3.1.0",
"micromark-factory-space": "^2.0.1",
"micromark-util-character": "^2.1.1",
"micromark-util-classify-character": "^2.0.1",
"micromark-util-sanitize-uri": "^2.0.1",
"micromark-util-symbol": "^2.0.1"
}
}
+378 -563
View File
File diff suppressed because it is too large Load Diff
+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 },
+20 -31
View File
@@ -1,10 +1,12 @@
/**
* apes:* tab registration tests (Phase 1/1b): the fork registers its four
* information-layer tabs (tasks / agents / apps / profile) through the same
* BetterSidebarService the upstream builtins use (dogfood). The spec pins
* the registration contract (id / single / order / component presence) and
* the isolated i18n contract (the apes namespace resolves zh vs en through
* the attached locale service, and every apesZh key has an apesEn entry).
* apes:* tab registration tests (Phase 1/1b, C6: 3 tabs). The fork registers
* its three information-layer tabs (agents / apps / profile) through the same
* BetterSidebarService the upstream builtins use (dogfood). `apes:tasks` was
* removed in C6 — it overlaps the official DSH session tree in the native
* sidebar. The spec pins the registration contract (id / single / order /
* component presence) and the isolated i18n contract (the apes namespace
* resolves zh vs en through the attached locale service, and every apesZh
* key has an apesEn entry).
*
* Runs in the node vitest environment like builtins.spec.ts — no DOM, no
* real DSH host (a live cordis scratch mount is Phase 2+).
@@ -31,11 +33,11 @@ function setup(): {
}
describe('apes:* tab registrations', () => {
it('registers the 4 apes tabs with the apes: namespace', () => {
it('registers the 3 apes tabs with the apes: namespace', () => {
const { service, dispose } = setup()
try {
expect(service.getTabs().map(t => t.id).sort()).toEqual(
['apes:agents', 'apes:apps', 'apes:profile', 'apes:tasks'],
['apes:agents', 'apes:apps', 'apes:profile'],
)
} finally {
dispose()
@@ -45,7 +47,7 @@ describe('apes:* tab registrations', () => {
it('every apes tab is single-instance, visible, and component-carrying', () => {
const { service, dispose } = setup()
try {
for (const id of ['apes:tasks', 'apes:agents', 'apes:apps', 'apes:profile']) {
for (const id of ['apes:agents', 'apes:apps', 'apes:profile']) {
const tab = service.getTab(id)
expect(tab, `${id} registered`).toBeDefined()
expect(tab?.single, `${id} single`).toBe(true)
@@ -62,17 +64,14 @@ describe('apes:* tab registrations', () => {
const { service, dispose } = setup()
try {
const orders = {
tasks: service.getTab('apes:tasks')?.order,
agents: service.getTab('apes:agents')?.order,
apps: service.getTab('apes:apps')?.order,
profile: service.getTab('apes:profile')?.order,
}
expect(orders.tasks).toBeGreaterThanOrEqual(900)
expect(orders.agents).toBeGreaterThanOrEqual(900)
expect(orders.apps).toBeGreaterThanOrEqual(900)
expect(orders.profile).toBeGreaterThanOrEqual(900)
// Strictly ordered: tasks < agents < apps < profile.
expect(orders.tasks!).toBeLessThan(orders.agents!)
// Strictly ordered: agents < apps < profile.
expect(orders.agents!).toBeLessThan(orders.apps!)
expect(orders.apps!).toBeLessThan(orders.profile!)
} finally {
@@ -84,7 +83,7 @@ describe('apes:* tab registrations', () => {
const store = createSidebarStore()
const service = createBetterSidebarService(store)
const dispose = registerApesTabs({} as Context, service)
expect(service.getTabs()).toHaveLength(4)
expect(service.getTabs()).toHaveLength(3)
dispose()
expect(service.getTabs()).toHaveLength(0)
})
@@ -102,30 +101,20 @@ describe('apes i18n contract', () => {
it('resolves zh when the DSH preference is zh', () => {
attachApesLocale({ getSnapshot: () => ({ active: 'zh-CN' }) })
expect(t('apesTasks')).toBe('任务')
expect(t('apesAgents')).toBe('智能体')
expect(t('apesApps')).toBe('应用')
expect(t('apesProfile')).toBe('画像')
expect(t('apesAgentsTitle')).toBe('智能体')
expect(t('apesAppsTitle')).toBe('应用')
expect(t('apesProfileTitle')).toBe('画像')
})
it('resolves en when the DSH preference is en', () => {
attachApesLocale({ getSnapshot: () => ({ active: 'en' }) })
expect(t('apesTasks')).toBe('Tasks')
expect(t('apesAgents')).toBe('Agents')
expect(t('apesApps')).toBe('Apps')
expect(t('apesProfile')).toBe('Profile')
expect(t('apesAgentsTitle')).toBe('Agents')
expect(t('apesAppsTitle')).toBe('Apps')
expect(t('apesProfileTitle')).toBe('Profile')
})
it('falls back to zh when no locale service is attached', () => {
attachApesLocale(undefined)
expect(t('apesTasks')).toBe('任务')
})
it('supports {param} interpolation', () => {
attachApesLocale({ getSnapshot: () => ({ active: 'zh' }) })
// The duration strings carry {seconds} params — assert the mechanism via
// a real key (the t() contract, not a synthetic one).
expect(t('apesDurationSeconds', { seconds: 5 })).toBe('5 秒')
expect(t('apesDurationMinutes', { minutes: 2, seconds: 7 })).toBe('2 分 7 秒')
expect(t('apesAgentsTitle')).toBe('智能体')
})
})
+1 -1
View File
@@ -56,7 +56,7 @@ function parentLog(): SidebarSessionEvent[] {
ev('user/message', 0, userMessage('first question')),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/message', 3, { turn: 1, step: 1, message: assistantMessage('first answer') }),
ev('assistant/message', 3, { turn: 1, step: 1, stream: [], message: assistantMessage('first answer') }),
ev('step/end', 4, { turn: 1, step: 1 }),
ev('turn/end', 5, { turn: 1, reason: { kind: 'completed' } }),
ev('user/message', 6, userMessage('pending question')),