feat(apes): revive the task/message tabs in the info layer
C6 removed apes:tasks as a duplicate of the host session panel; in practice the two answer different questions (host panel = switch workspaces, plugin tab = who is running right now), so the tab is restored and the info layer goes back to five types. tasks: live session list (current / running / subagent markers) + showInactive toggle via pluginSettings['apes:tasks']; messages: placeholder that states the host has no message feed yet; both get badge counters, and the whole layer now honours the visible gate (auto-collapse) so hidden tabs stop waking on session events. zh/en/ja dictionaries in sync (AGENTS 5); wiki 02/03/07 reverted from three to five tabs; decision record in docs/plans/2026-10-11-apes-tab-revival.md.
This commit is contained in:
@@ -48,3 +48,23 @@ export function IconUserOutline16({ size = 16, className }: { size?: number; cla
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** Tasks glyph: a rounded card carrying a check mark. */
|
||||
export function IconTasksOutline16({ size = 16, className }: { size?: number; className?: string }): ReactNode {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<rect x="1.5" y="1.5" width="13" height="13" rx="3.6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M4.7 8.2l2.2 2.2 4.4-4.8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** Messages glyph: a speech bubble with a lower-left tail. */
|
||||
export function IconMessagesOutline16({ size = 16, className }: { size?: number; className?: string }): ReactNode {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M1.5 5.1c0-2 1.6-3.6 3.6-3.6h5.8c2 0 3.6 1.6 3.6 3.6v3.1c0 2-1.6 3.6-3.6 3.6H6.6L2.9 14.5V11.5h-.2c-.7-.6-1.2-1.6-1.2-2.6z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
<path d="M5.4 7.4h5.2M5.4 4.9h3.1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/**
|
||||
* apes:* information-layer tabs — the fork's own contribution on top of the
|
||||
* 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).
|
||||
* upstream dsh-better-sidebar. Five tabs ship (tasks / agents / messages /
|
||||
* apps / profile).
|
||||
*
|
||||
* History (C7): `apes:tasks` was removed under the old C6 decision (it looked
|
||||
* redundant next to the official DSH session tree). The `v2-revive` line
|
||||
* restores it — the native tree is a *navigator* (one focused lineage), this
|
||||
* panel is a *list* (every session, with an idle filter and a running badge),
|
||||
* so the two answer different questions. `messages` is still an honest
|
||||
* placeholder: no DSH message-stream feed exists yet.
|
||||
*
|
||||
* Isolated under `src/client/apes/` so the upstream `builtins/` tree stays
|
||||
* untouched: cherry-picking upstream releases only has to keep the single
|
||||
@@ -20,7 +25,7 @@ import { apesTabs } from './tabs.tsx'
|
||||
|
||||
// Re-exported so index.tsx can register the apes dictionaries + attach the
|
||||
// locale service in one import (keeps the apes module self-contained).
|
||||
export { APES_LOCALE_NS, apesZh, apesEn, attachApesLocale } from './locales.ts'
|
||||
export { APES_LOCALE_NS, apesZh, apesEn, apesJa, attachApesLocale } from './locales.ts'
|
||||
|
||||
/**
|
||||
* Register the apes:* tabs with the sidebar service. Returns a disposer that
|
||||
|
||||
@@ -17,9 +17,19 @@ export const APES_LOCALE_NS = 'apes'
|
||||
/** The zh dictionary (source of truth). */
|
||||
export const apesZh = {
|
||||
// View titles
|
||||
apesTasksTitle: '任务',
|
||||
apesMessagesTitle: '消息',
|
||||
apesAgentsTitle: '智能体',
|
||||
apesAppsTitle: '应用',
|
||||
apesProfileTitle: '画像',
|
||||
// Tasks view
|
||||
apesTasksShowInactive: '显示空闲会话',
|
||||
apesTasksEmpty: '当前没有会话',
|
||||
apesTasksCurrent: '当前',
|
||||
apesTasksRunning: '运行中',
|
||||
apesTasksSubagent: '子智能体',
|
||||
// Messages view
|
||||
apesMessagesNoFeed: '当前 DSH 版本未提供消息流数据源',
|
||||
// Agents view
|
||||
apesAgentsEmpty: '当前会话没有子智能体',
|
||||
apesAgentsRoot: '主智能体',
|
||||
@@ -40,9 +50,17 @@ export const apesZh = {
|
||||
|
||||
/** The en dictionary (checked against zh — every zh key must appear here). */
|
||||
export const apesEn: Record<keyof typeof apesZh, string> = {
|
||||
apesTasksTitle: 'Tasks',
|
||||
apesMessagesTitle: 'Messages',
|
||||
apesAgentsTitle: 'Agents',
|
||||
apesAppsTitle: 'Apps',
|
||||
apesProfileTitle: 'Profile',
|
||||
apesTasksShowInactive: 'Show idle sessions',
|
||||
apesTasksEmpty: 'No sessions right now',
|
||||
apesTasksCurrent: 'current',
|
||||
apesTasksRunning: 'running',
|
||||
apesTasksSubagent: 'subagent',
|
||||
apesMessagesNoFeed: 'This DSH version exposes no message stream feed',
|
||||
apesAgentsEmpty: 'No subagents in this session',
|
||||
apesAgentsRoot: 'Main agent',
|
||||
apesAgentRunning: 'running',
|
||||
@@ -60,7 +78,39 @@ export const apesEn: Record<keyof typeof apesZh, string> = {
|
||||
|
||||
export type ApesCopyKey = keyof typeof apesZh
|
||||
|
||||
/** Resolve the currently active DSH locale ('zh' | 'en' today). */
|
||||
/**
|
||||
* The ja dictionary (AGENTS §5: a new zh key must land in ja in the same
|
||||
* change, otherwise ja readers silently get the en text). Japanese is not a
|
||||
* separate source of truth — it mirrors the en wording, never the zh one.
|
||||
*/
|
||||
export const apesJa: Record<keyof typeof apesZh, string> = {
|
||||
apesTasksTitle: 'タスク',
|
||||
apesMessagesTitle: 'メッセージ',
|
||||
apesAgentsTitle: 'エージェント',
|
||||
apesAppsTitle: 'アプリ',
|
||||
apesProfileTitle: 'プロフィール',
|
||||
apesTasksShowInactive: 'アイドル状態のセッションを表示',
|
||||
apesTasksEmpty: 'セッションがありません',
|
||||
apesTasksCurrent: '現在',
|
||||
apesTasksRunning: '実行中',
|
||||
apesTasksSubagent: 'サブエージェント',
|
||||
apesMessagesNoFeed: 'この DSH バージョンはメッセージストリームの供給元を提供していません',
|
||||
apesAgentsEmpty: 'このセッションにサブエージェントはいません',
|
||||
apesAgentsRoot: 'メインエージェント',
|
||||
apesAgentRunning: '実行中',
|
||||
apesAgentInactive: 'アイドル',
|
||||
apesAgentOneShot: '単発',
|
||||
apesAgentContinuable: '継続可',
|
||||
apesProfileCurrent: '現在のセッション',
|
||||
apesProfileCwd: '作業ディレクトリ',
|
||||
apesProfileNoCwd: '不明',
|
||||
apesProfileRunning: '実行中',
|
||||
apesProfileIdle: 'アイドル',
|
||||
apesAppsNoFeed: 'この DSH バージョンはワークスペースアプリのデータ源を提供していません',
|
||||
apesAppsWorkspace: 'ワークスペース',
|
||||
}
|
||||
|
||||
/** Resolve the currently active DSH locale ('zh' | 'ja' | 'en' today). */
|
||||
export function attachApesLocale(service: { getSnapshot(): { active: string } } | undefined): void {
|
||||
apesLocaleService = service
|
||||
}
|
||||
@@ -69,13 +119,14 @@ let apesLocaleService: { getSnapshot(): { active: string } } | undefined
|
||||
|
||||
/**
|
||||
* Translate an apes key. Resolution order: active-locale chain (zh dict when
|
||||
* the DSH preference is zh, en dict otherwise) → key fallback (shows the key
|
||||
* itself rather than `undefined`). Mirrors the upstream t() minus the
|
||||
* better-locale override (Phase 2).
|
||||
* the DSH preference is zh*, ja dict when it is ja*, en dict otherwise) → key
|
||||
* fallback (shows the key itself rather than `undefined`). Mirrors the
|
||||
* upstream t() minus the better-locale override (Phase 2).
|
||||
*/
|
||||
export function t(key: ApesCopyKey, params?: Record<string, string | number>): string {
|
||||
const active = apesLocaleService?.getSnapshot().active ?? 'zh'
|
||||
const dict = active.toLowerCase().startsWith('zh') ? apesZh : apesEn
|
||||
const lang = active.toLowerCase()
|
||||
const dict = lang.startsWith('zh') ? apesZh : lang.startsWith('ja') ? apesJa : apesEn
|
||||
let text: string | undefined = dict[key]
|
||||
if (text === undefined) text = key
|
||||
if (params !== undefined) {
|
||||
|
||||
+215
-25
@@ -1,17 +1,27 @@
|
||||
/**
|
||||
* 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 — these are framework-free and
|
||||
* reuses the fork's PURE lineage 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).
|
||||
* Five information-layer tabs (C7 reversal: `apes:tasks` is back). The reason
|
||||
* the earlier C6 removal no longer holds: the native DSH session tree renders
|
||||
* ONE focused lineage, while this panel is a *flat, whole-workspace* list with
|
||||
* a running badge and an idle filter — complementary, not redundant.
|
||||
* - apes:tasks → every session (running / current / subagent marks) with
|
||||
* an idle filter; a running count badges the tab.
|
||||
* - apes:agents → the subagent topology rooted at the main agent of the
|
||||
* current session (rootAncestor + per-parent catalogs).
|
||||
* - apes:messages → honest placeholder: DSH exposes no message-stream feed.
|
||||
* - 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).
|
||||
*
|
||||
* Live-data discipline (C4): list-reading views subscribe only while the tab
|
||||
* is on screen (`visible ? subscribe : NOOP_SUBSCRIBE`), so a collapsed panel
|
||||
* stops re-rendering; tab-strip counts ride the cheap `badge` contract instead
|
||||
* of a live subscription.
|
||||
*
|
||||
* Visual unity (C7): colors use the DSW design-token aliases
|
||||
* (`--dsw-alias-*`) so the apes panels match the native DSH sidebar
|
||||
@@ -24,14 +34,19 @@
|
||||
* `subscribe(cb)` is uSES-safe.
|
||||
* - `scope.sessionId` = the currently-open session.
|
||||
*/
|
||||
import { useCallback, useMemo, useState, type CSSProperties } from 'react'
|
||||
import { useCallback, useMemo, type CSSProperties } from 'react'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { IconAgentOutline16, IconAppsOutline16, IconUserOutline16 } from './icons.tsx'
|
||||
import {
|
||||
IconAgentOutline16,
|
||||
IconAppsOutline16,
|
||||
IconMessagesOutline16,
|
||||
IconTasksOutline16,
|
||||
IconUserOutline16,
|
||||
} from './icons.tsx'
|
||||
import type { TabComponentProps, TabDescriptor } from '../service.ts'
|
||||
import type {
|
||||
SidebarSessionList,
|
||||
SidebarSessionSummary,
|
||||
SidebarSubagentCatalog,
|
||||
SidebarSubagentChildEntry,
|
||||
} from '../../context-types.ts'
|
||||
import { rootAncestor, isSideThreadSummary } from '../subagent-lineage.ts'
|
||||
@@ -66,8 +81,16 @@ const S: Record<string, CSSProperties> = {
|
||||
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 },
|
||||
chip: { fontSize: 10, lineHeight: '15px', padding: '0 6px', borderRadius: 999, border: `1px solid ${DSW.borderL2}`, color: DSW.labelSecondary, flexShrink: 0, whiteSpace: 'nowrap' },
|
||||
taskRow: { appearance: 'none', textAlign: 'left', width: '100%', cursor: 'pointer', font: 'inherit', color: 'inherit' },
|
||||
}
|
||||
|
||||
/** apes:tasks descriptor id (the plugin-toggle blob lives under this key). */
|
||||
export const TASKS_TAB_ID = 'apes:tasks'
|
||||
|
||||
/** The plugin-local switch persisted in `pluginSettings['apes:tasks']`. */
|
||||
export const TASKS_SHOW_INACTIVE_KEY = 'showInactive'
|
||||
|
||||
/** A small colored status dot (self-contained, not the builtin StateDot). */
|
||||
function Dot(props: { color: string }) {
|
||||
const style: CSSProperties = {
|
||||
@@ -76,15 +99,91 @@ function Dot(props: { color: string }) {
|
||||
return <span aria-hidden style={style} />
|
||||
}
|
||||
|
||||
/** Subscribe to the sessions list feed (uSES-safe). Returns the live snapshot. */
|
||||
function useSessionList(ctx: TabComponentProps['ctx']): SidebarSessionList {
|
||||
const sessions = ctx.sessions
|
||||
/** A tiny status pill (current / running / subagent marks). */
|
||||
function Chip(props: { label: string; color?: string }) {
|
||||
return <span style={props.color === undefined ? S.chip : { ...S.chip, color: props.color, borderColor: props.color }}>{props.label}</span>
|
||||
}
|
||||
|
||||
/** Stable no-op subscription: `visible === false` pauses the feed. */
|
||||
export const NOOP_SUBSCRIBE = (): (() => void) => () => {}
|
||||
|
||||
/**
|
||||
* The subscription a view hands to uSES (C4 pause contract). Exported so the
|
||||
* "hidden tab stops waking on session events" rule is unit-testable without a
|
||||
* DOM: with `visible = false` the feed is never touched.
|
||||
*/
|
||||
export function feedSubscribe(
|
||||
ctx: TabComponentProps['ctx'],
|
||||
visible: boolean,
|
||||
): (callback: () => void) => () => void {
|
||||
if (!visible) return NOOP_SUBSCRIBE
|
||||
return callback => ctx.sessions.list.subscribe(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the sessions list feed (uSES-safe). Returns the live snapshot.
|
||||
* Pass `visible = false` to detach — a hidden tab keeps rendering from the
|
||||
* last snapshot without waking on every session event.
|
||||
*/
|
||||
function useSessionList(ctx: TabComponentProps['ctx'], visible = true): SidebarSessionList {
|
||||
const subscribe = useMemo(() => feedSubscribe(ctx, visible), [ctx, visible])
|
||||
return useSyncExternalStore(
|
||||
useMemo(() => (callback: () => void) => sessions.list.subscribe(callback), [sessions]),
|
||||
useCallback(() => sessions.list.getSnapshot(), [sessions]),
|
||||
subscribe,
|
||||
useCallback(() => ctx.sessions.list.getSnapshot(), [ctx]),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `showInactive` filter state. The declarative settings row writes it into
|
||||
* the sidebar prefs document under `pluginSettings['apes:tasks']` (service
|
||||
* contract, `SidebarSettingsDeclaration.pluginToggles`), which is NOT part of
|
||||
* the public `SidebarPrefs` type — hence the narrow local cast. Default is
|
||||
* true: the panel is a task list, so showing idle sessions is the useful
|
||||
* default and the switch lets the reader trim it down.
|
||||
*/
|
||||
export function tasksShowInactive(store: TabComponentProps['store']): boolean {
|
||||
try {
|
||||
const prefs = store.getPrefs() as unknown as { pluginSettings?: Record<string, Record<string, unknown>> }
|
||||
const blob = prefs.pluginSettings?.[TASKS_TAB_ID]
|
||||
const value = blob?.[TASKS_SHOW_INACTIVE_KEY]
|
||||
return typeof value === 'boolean' ? value : true
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apes:tasks rows: every non-side-chat session, running ones first (then the
|
||||
* current one), idle ones only when `showInactive`. PURE — unit-tested.
|
||||
*/
|
||||
export function tasksRows(list: SidebarSessionList, showInactive: boolean): SidebarSessionSummary[] {
|
||||
const rows = Object.values(list.byId).filter(s => !isSideThreadSummary(s) && (showInactive || s.running === true))
|
||||
const rank = (s: SidebarSessionSummary): number => (s.id === list.current ? 0 : s.running === true ? 1 : 2)
|
||||
return rows.sort(
|
||||
(a, b) => rank(a) - rank(b) || (a.displayTitle ?? a.id).localeCompare(b.displayTitle ?? b.id),
|
||||
)
|
||||
}
|
||||
|
||||
/** Running sessions (the tasks tab badge). Cheap: one pass, no allocation. */
|
||||
export function runningSessionCount(list: SidebarSessionList): number {
|
||||
let count = 0
|
||||
for (const s of Object.values(list.byId)) {
|
||||
if (s.running === true && !isSideThreadSummary(s)) count += 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/** Subagents in the given session's tree (the agents tab badge). */
|
||||
export function subagentCount(list: SidebarSessionList, sessionId: string): number {
|
||||
const root = rootAncestor(list.byId, sessionId)
|
||||
let count = 0
|
||||
for (const s of Object.values(list.byId)) {
|
||||
if (s.origin !== 'subagent' || isSideThreadSummary(s)) continue
|
||||
if (rootAncestor(list.byId, s.id) === root) count += 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/** One level of the subagent topology (rooted at the main agent). */
|
||||
function agentChildren(
|
||||
byId: Readonly<Record<string, SidebarSessionSummary>>,
|
||||
@@ -95,9 +194,69 @@ function agentChildren(
|
||||
)
|
||||
}
|
||||
|
||||
/** apes:tasks — the flat, whole-workspace session list. */
|
||||
export function ApesTasksView(props: TabComponentProps) {
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const store = props.store
|
||||
const showInactive = useSyncExternalStore(
|
||||
useMemo(() => (callback: () => void) => store.subscribe(callback), [store]),
|
||||
useCallback(() => tasksShowInactive(store), [store]),
|
||||
)
|
||||
const rows = useMemo(() => tasksRows(list, showInactive), [list, showInactive])
|
||||
const open = props.ctx.sessions.open
|
||||
|
||||
return (
|
||||
<div style={S.body}>
|
||||
<h2 style={S.title}>{t('apesTasksTitle')}</h2>
|
||||
{rows.length === 0
|
||||
? <p style={S.hint}>{t('apesTasksEmpty')}</p>
|
||||
: (
|
||||
<div style={S.list}>
|
||||
{rows.map(row => {
|
||||
const running = row.running === true
|
||||
return (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
aria-current={row.id === list.current ? 'true' : undefined}
|
||||
title={row.displayTitle}
|
||||
onClick={() => { open?.(row.id) }}
|
||||
style={{ ...S.card, ...S.taskRow }}
|
||||
>
|
||||
<div style={S.cardRow}>
|
||||
<Dot color={running ? DSW.success : DSW.labelTertiary} />
|
||||
<span style={S.cardLabel}>{row.displayTitle}</span>
|
||||
{row.id === list.current && <Chip label={t('apesTasksCurrent')} color={DSW.labelPrimary} />}
|
||||
{running && <Chip label={t('apesTasksRunning')} color={DSW.success} />}
|
||||
{row.origin === 'subagent' && <Chip label={t('apesTasksSubagent')} />}
|
||||
</div>
|
||||
{row.cwd !== undefined && (
|
||||
<div style={S.cardRow}>
|
||||
<span style={S.cardSecondary}>{row.cwd}</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** apes:messages — DSH exposes no message-stream feed; honest placeholder. */
|
||||
export function ApesMessagesView(_props: TabComponentProps) {
|
||||
return (
|
||||
<div style={S.body}>
|
||||
<h2 style={S.title}>{t('apesMessagesTitle')}</h2>
|
||||
<p style={S.hint}>{t('apesMessagesNoFeed')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** apes:agents — the subagent topology of the current session's tree. */
|
||||
function ApesAgentsView(props: TabComponentProps) {
|
||||
const list = useSessionList(props.ctx)
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const { byId, subagentsByParent } = list
|
||||
const rootId = useMemo(() => rootAncestor(byId, props.scope.sessionId), [byId, props.scope.sessionId])
|
||||
const rootSummary = rootId === undefined ? undefined : byId[rootId]
|
||||
@@ -165,7 +324,7 @@ function ApesAgentsView(props: TabComponentProps) {
|
||||
|
||||
/** apes:profile — the current session's identity (title, cwd, running state). */
|
||||
export function ApesProfileView(props: TabComponentProps) {
|
||||
const list = useSessionList(props.ctx)
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const current = list.current
|
||||
const summary = current === undefined ? undefined : list.byId[current]
|
||||
const cwd = summary?.cwd ?? props.scope.cwd
|
||||
@@ -202,7 +361,7 @@ export function ApesProfileView(props: TabComponentProps) {
|
||||
|
||||
/** apes:apps — no DSH host feed today; shows the current workspace (cwd). */
|
||||
function ApesAppsView(props: TabComponentProps) {
|
||||
const list = useSessionList(props.ctx)
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const current = list.current
|
||||
const summary = current === undefined ? undefined : list.byId[current]
|
||||
const cwd = summary?.cwd ?? props.scope.cwd
|
||||
@@ -224,14 +383,45 @@ function ApesAppsView(props: TabComponentProps) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The five apes:* tab descriptors (C7: `apes:tasks` restored — the native
|
||||
* session tree focuses one lineage, this list covers the whole workspace).
|
||||
* Order 55-70 keeps them after the built-ins (editor 10 … browser 50) and in
|
||||
* reading order: tasks / agents / apps / messages / 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:agents', title: () => t('apesAgentsTitle'), icon: (size: number) => <IconAgentOutline16 size={size} />, order: 60, single: true, component: ApesAgentsView },
|
||||
{
|
||||
id: TASKS_TAB_ID,
|
||||
title: () => t('apesTasksTitle'),
|
||||
icon: (size: number) => <IconTasksOutline16 size={size} />,
|
||||
order: 55,
|
||||
single: true,
|
||||
component: ApesTasksView,
|
||||
badge: ctx => {
|
||||
const count = runningSessionCount(ctx.sessions.list.getSnapshot())
|
||||
return count > 0 ? count : null
|
||||
},
|
||||
settings: {
|
||||
pluginToggles: [
|
||||
{ key: TASKS_SHOW_INACTIVE_KEY, title: () => t('apesTasksShowInactive'), type: 'switch' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'apes:agents',
|
||||
title: () => t('apesAgentsTitle'),
|
||||
icon: (size: number) => <IconAgentOutline16 size={size} />,
|
||||
order: 60,
|
||||
single: true,
|
||||
component: ApesAgentsView,
|
||||
badge: (ctx, scope) => {
|
||||
const count = subagentCount(ctx.sessions.list.getSnapshot(), scope.sessionId)
|
||||
return count > 0 ? count : null
|
||||
},
|
||||
},
|
||||
{ id: 'apes:apps', title: () => t('apesAppsTitle'), icon: (size: number) => <IconAppsOutline16 size={size} />, order: 65, single: true, component: ApesAppsView },
|
||||
{ id: 'apes:messages', title: () => t('apesMessagesTitle'), icon: (size: number) => <IconMessagesOutline16 size={size} />, order: 67, single: true, component: ApesMessagesView },
|
||||
{ id: 'apes:profile', title: () => t('apesProfileTitle'), icon: (size: number) => <IconUserOutline16 size={size} />, order: 70, single: true, component: ApesProfileView },
|
||||
]
|
||||
|
||||
@@ -15,7 +15,7 @@ import { allLeaves, createSidebarStore, isAgentTabId } from './state.ts'
|
||||
import { createBetterSidebarService, matchUrlTarget } from './service.ts'
|
||||
import { revalidateChunksOnReactivate, setChunkModuleSystem } from './chunk-loader.ts'
|
||||
import { registerBuiltins } from './builtins/index.ts'
|
||||
import { registerApesTabs, APES_LOCALE_NS, apesZh, apesEn, attachApesLocale } from './apes/index.ts'
|
||||
import { registerApesTabs, APES_LOCALE_NS, apesZh, apesEn, apesJa, attachApesLocale } from './apes/index.ts'
|
||||
import { Sidebar } from './Sidebar.tsx'
|
||||
import { RenderBoundary } from './RenderBoundary.tsx'
|
||||
import { registerTurnTailInterception } from './intercept.tsx'
|
||||
@@ -65,14 +65,16 @@ export function apply(ctx: Context): void {
|
||||
attachLocale(ctx.locale)
|
||||
// The apes module has its own locale namespace (isolated from the upstream
|
||||
// betterSidebar dicts): attach it so apes t() tracks the DSH active-locale
|
||||
// preference, and register its zh/en dicts under `apes`.
|
||||
// preference, and register its zh/ja/en dicts under `apes` (AGENTS §5: a ja
|
||||
// dict must ship with every new zh key, else ja readers fall back to en).
|
||||
attachApesLocale(ctx.locale)
|
||||
ctx.effect(() => {
|
||||
const offZh = ctx.locale.register(LOCALE_NS, 'zh', zh)
|
||||
const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
|
||||
const offApesZh = ctx.locale.register(APES_LOCALE_NS, 'zh', apesZh as Record<string, string>)
|
||||
const offApesJa = ctx.locale.register(APES_LOCALE_NS, 'ja', apesJa as Record<string, string>)
|
||||
const offApesEn = ctx.locale.register(APES_LOCALE_NS, 'en', apesEn as Record<string, string>)
|
||||
return () => { offZh(); offEn(); offApesZh(); offApesEn() }
|
||||
return () => { offZh(); offEn(); offApesZh(); offApesJa(); offApesEn() }
|
||||
}, 'dsh-apes-sidebar: dictionaries')
|
||||
|
||||
// Opt-in third-language support through @huanlin/dsh-plugin-better-locale.
|
||||
|
||||
Reference in New Issue
Block a user