feat(apes-sidebar): Phase 2 — badge 计数 + auto-collapse(visible 门控暂停)
- apes:tasks badge = running 会话数;apes:agents badge = subagent 数 (纯 (ctx)=>number|null 函数,0→null 隐藏,host 封顶 99+) - useSessionList 接受 visible:非活动 tab 切 no-op subscribe, 停止 live session-list churn 重渲染隐藏面板(better-sidebar 'live views pause otherwise' 契约 = auto-collapse) - tests/tabs.spec.ts:新增 5 个 badge 契约测试(6→11),mock sessions feed 直测 badge 纯函数 - AGENTS.md:Phase 2 标 partial(per-tab state / scratch 冒烟 / apps·messages·profile feed 延后,better-sidebar 0.18.0 无相应 API/数据源) 门禁:typecheck rc=0 · build rc=0(client.js 15.74→20K)· test 25/25
This commit is contained in:
+56
-7
@@ -49,10 +49,43 @@ const TABS = {
|
||||
*/
|
||||
type ApesClientContext = Context
|
||||
|
||||
/** Read one client session list snapshot uSES-safely. */
|
||||
function useSessionList(ctx: Context) {
|
||||
/**
|
||||
* Read the client session list uSES-safely, with `visible`-gated pausing
|
||||
* (Phase 2 auto-collapse). `useSyncExternalStore` re-subscribes whenever the
|
||||
* `subscribe` identity changes, so passing a no-op subscribe while the tab is
|
||||
* not the active-open one stops live session-list churn from re-rendering a
|
||||
* hidden panel — exactly better-sidebar's "live views pause otherwise"
|
||||
* contract. The snapshot itself is always read (cheap), so a hidden tab still
|
||||
* shows its last-known data when re-shown.
|
||||
*/
|
||||
const NOOP_SUBSCRIBE = () => () => {}
|
||||
function useSessionList(ctx: Context, visible: boolean) {
|
||||
const list = ctx.sessions.list
|
||||
return useSyncExternalStore(list.subscribe, list.getSnapshot, list.getSnapshot)
|
||||
return useSyncExternalStore(
|
||||
visible ? list.subscribe : NOOP_SUBSCRIBE,
|
||||
list.getSnapshot,
|
||||
list.getSnapshot,
|
||||
)
|
||||
}
|
||||
|
||||
/** Count of sessions whose agent is currently running (badge source). */
|
||||
function runningCount(ctx: Context): number {
|
||||
const { byId } = ctx.sessions.list.getSnapshot()
|
||||
let n = 0
|
||||
for (const id in byId) {
|
||||
if (byId[id]?.running) n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/** Count of subagent-origin sessions (badge source for apes:agents). */
|
||||
function subagentCount(ctx: Context): number {
|
||||
const { byId } = ctx.sessions.list.getSnapshot()
|
||||
let n = 0
|
||||
for (const id in byId) {
|
||||
if (byId[id]?.origin === 'subagent') n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
/** A row in a list view: id, title, and optional flag chips. */
|
||||
@@ -132,7 +165,7 @@ function Empty({ key: key }: { key: string }): ReactElement {
|
||||
*/
|
||||
function TasksView(props: TabComponentProps): ReactElement {
|
||||
useApesLocale()
|
||||
const list = useSessionList(props.ctx)
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const rows = Object.values(list.byId)
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
@@ -165,7 +198,7 @@ function TasksView(props: TabComponentProps): ReactElement {
|
||||
*/
|
||||
function AgentsView(props: TabComponentProps): ReactElement {
|
||||
useApesLocale()
|
||||
const list = useSessionList(props.ctx)
|
||||
const list = useSessionList(props.ctx, props.visible)
|
||||
const children = Object.values(list.byId).filter((s) => s.origin === 'subagent')
|
||||
if (children.length === 0) {
|
||||
return (
|
||||
@@ -256,8 +289,14 @@ function ProfileView(): ReactElement {
|
||||
/**
|
||||
* Build the five tab descriptors. Each `component` receives the real
|
||||
* better-sidebar `TabComponentProps` (ctx / store / scope / tab / visible).
|
||||
* `visible` is deliberately unused in Phase 1 (no polling yet); Phase 2
|
||||
* gates live feeds on it the way better-sidebar's subagent view does.
|
||||
*
|
||||
* Phase 2 additions:
|
||||
* - `badge` on tasks/agents: a cheap count pill (running sessions / subagent
|
||||
* count). Called on every tab-bar render, so it reads the live sessions
|
||||
* snapshot synchronously and returns `null` when the count is 0 (hidden).
|
||||
* - `visible` is now honored by the live views: `useSessionList` swaps to a
|
||||
* no-op subscribe when the tab is not the active-open one (auto-collapse —
|
||||
* hidden panels stop re-rendering on session-list churn).
|
||||
*/
|
||||
function buildDescriptors(): TabDescriptor[] {
|
||||
return [
|
||||
@@ -266,6 +305,11 @@ function buildDescriptors(): TabDescriptor[] {
|
||||
title: () => t('tab.tasks.title'),
|
||||
order: 90,
|
||||
single: true,
|
||||
// Running-session count pill (99+ capped by the host; 0 → hidden).
|
||||
badge: (ctx) => {
|
||||
const n = runningCount(ctx)
|
||||
return n === 0 ? null : n
|
||||
},
|
||||
component: (props) => <TasksView {...props} />,
|
||||
},
|
||||
{
|
||||
@@ -273,6 +317,11 @@ function buildDescriptors(): TabDescriptor[] {
|
||||
title: () => t('tab.agents.title'),
|
||||
order: 91,
|
||||
single: true,
|
||||
// Active-subagent count pill (0 → hidden).
|
||||
badge: (ctx) => {
|
||||
const n = subagentCount(ctx)
|
||||
return n === 0 ? null : n
|
||||
},
|
||||
component: (props) => <AgentsView {...props} />,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user