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:
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+378
-563
File diff suppressed because it is too large
Load Diff
@@ -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 },
|
||||
|
||||
+20
-31
@@ -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('智能体')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')),
|
||||
|
||||
Reference in New Issue
Block a user