fix(client): drop the running-sessions badge from the Context Insights entry

The green pill on the sidebar-foot entry read as a notification badge
(#74): a running-task count is not something a footer control should
surface as an attention marker. The entry returns to a plain icon +
label; the count's only consumer was the badge, so the tally helper and
the button's useSessions seat plumbing go with it.

Fixes #74.
This commit is contained in:
bowenliang123 committed 2026-09-17 17:01:01 +08:00
1 parent b6eda0833c
commit ffee2a6652
5 files changed
+5 -67

No files matched your search

+2 -9
View File
@@ -3,9 +3,8 @@
* above Settings (the harness's own foot layout: footer actions, then the
* settings row), mirroring the Settings trigger's geometry in both column
* widths. The button carries the plugin emblem and, on the wide
* column, its label; a badge counts the sessions currently running (the one
* glanceable live fact a footer can carry). Clicking opens the overview
* overlay through the shared module store (overviewStore.ts).
* column, its label. Clicking opens the overview overlay through the shared
* module store (overviewStore.ts).
*
* The per-user `insightsEntry` preference takes the entry down (renders
* null) without unregistering the seat. The subscription fails open: an
@@ -15,7 +14,6 @@
import { useSyncExternalStore, type ReactElement } from 'react'
import { ContextIcon } from '../icon'
import { runningCountOf, sessionsSnapshotOf } from '../overview'
import { overviewStore } from '../overviewStore'
import type { ContextSettings, InsightsEntry } from '../settings'
import type { ViewKit } from '../viewkit'
@@ -23,8 +21,6 @@ import type { ViewKit } from '../viewkit'
export interface OverviewButtonProps {
/** The footer-action owner share: false on the collapsed 56px rail (icon only). */
wide?: boolean
/** The root standard kit's sessions seat (absent on a harness without it). */
useSessions?: unknown
}
/** Stable subscription faces for the settings-less degrade (no re-subscribes). */
@@ -40,8 +36,6 @@ export function makeOverviewButton(kit: ViewKit, settings?: ContextSettings): (p
: (listener: () => void): (() => void) => settings.store.subscribe(listener)
const getEntry = settings === undefined ? entryShown : (): InsightsEntry => settings.insightsEntry()
return function OverviewButton(props: OverviewButtonProps): ReactElement | null {
// The hook-level read (unconditional, guarded inside); the badge hides at 0.
const running = runningCountOf(sessionsSnapshotOf(props))
// The entry toggle: subscribed, so a preference flip takes effect live.
const entry = useSyncExternalStore(subscribeEntry, getEntry)
if (entry === 'hide') return null
@@ -55,7 +49,6 @@ export function makeOverviewButton(kit: ViewKit, settings?: ContextSettings): (p
>
<ContextIcon size={props.wide === true ? 16 : 18} className="lc-ov-entry-icon" />
{props.wide === true && <span className="lc-ov-entry-label">{t('ov.entry')}</span>}
{running > 0 && <span className="lc-ov-badge" aria-hidden="true">{running}</span>}
</button>
)
}
-18
View File
@@ -56,24 +56,6 @@ export function sessionsSnapshotOf(props: { useSessions?: unknown }): unknown {
}
}
/** The running-session tally for the footer entry's badge (0 on any shape failure). */
export function runningCountOf(snapshot: unknown): number {
const state = asRecord(snapshot)
if (state === null) return 0
const byId = asRecord(state.byId)
if (byId === null) return 0
let count = 0
try {
for (const id of Object.keys(byId)) {
const row = asRecord(byId[id])
if (row !== null && row.running === true) count++
}
} catch {
// A hostile row throwing on access stops the tally at what was counted.
}
return count
}
/**
* The `useWorkspaces` standard prop read — same guarded-hook contract as
* {@link sessionsSnapshotOf}. The overview joins its session → workspace
+1 -3
View File
@@ -15,14 +15,12 @@
/* Geometry mirrors the harness's own Settings trigger (ui-settings-general's
.trigger): the 42px row, 12px radius, 14px/22px label, and its shared hover
fill, so the plugin entry reads as a native foot control, not a visitor. */
.lc-ov-entry { position: relative; display: flex; align-items: center; gap: 8px; width: calc(100% + 4px); height: 42px; box-sizing: border-box; margin: 0 -2px; border: 0; padding: 0 10px 0 8px; overflow: hidden; background: transparent; border-radius: 12px; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
.lc-ov-entry { display: flex; align-items: center; gap: 8px; width: calc(100% + 4px); height: 42px; box-sizing: border-box; margin: 0 -2px; border: 0; padding: 0 10px 0 8px; overflow: hidden; background: transparent; border-radius: 12px; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }
.lc-ov-entry:hover { background: var(--dsw-alias-interactive-bg-hover); }
/* The collapsed rail takes the native foot controls' 36x36 circle, icon only. */
.lc-ov-entry-rail { flex: none; justify-content: center; gap: 0; width: 36px; height: 36px; margin: 0; padding: 0; border-radius: 50%; }
.lc-ov-entry-icon { flex: none; }
.lc-ov-entry-label { flex: 1 1 auto; min-width: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The running-sessions badge: the footer's one live fact, pinned to the icon's corner in both column widths. */
.lc-ov-badge { position: absolute; top: 1px; right: 4px; min-width: 15px; height: 15px; box-sizing: border-box; border-radius: 999px; background: var(--color-green-500); color: #fff; font-size: 9px; font-weight: 700; line-height: 1; display: flex; align-items: center; justify-content: center; padding: 0 4px; pointer-events: none; }
/* ---- the overlay chrome ---- */
/* The backdrop carries the `lc-ov` container: it spans the viewport exactly,
+2 -22
View File
@@ -1,6 +1,6 @@
// The Context Dashboard's sidebar-foot entry (src/client/components/
// overviewButton.tsx): wide/rail rendering, the running badge, the store
// flip on click, and the per-user insights-entry toggle.
// overviewButton.tsx): wide/rail rendering, the store flip on click, and the
// per-user insights-entry toggle.
import { createElement as h, act } from 'react'
import assert from 'node:assert/strict'
@@ -17,9 +17,6 @@ afterEach(() => {
overviewStore.set(false)
})
/** A useSessions stand-in over a static snapshot. */
const useSessionsOf = (snapshot: unknown) => (<T>(sel: (s: unknown) => T): T => sel(snapshot))
describe('OverviewButton', () => {
test('the wide column renders icon and label; the rail renders the icon alone', async () => {
const wide = await mount(h(Button, { wide: true }))
@@ -41,23 +38,6 @@ describe('OverviewButton', () => {
await bare.unmount()
})
test('the badge counts running sessions and hides at zero', async () => {
const running = useSessionsOf({ byId: { a: { running: true }, b: { running: false }, c: { running: true } } })
const m = await mount(h(Button, { wide: true, useSessions: running }))
assert.equal(query(m.container, '.lc-ov-badge').textContent, '2')
await m.unmount()
const idle = useSessionsOf({ byId: { a: { running: false } } })
const calm = await mount(h(Button, { wide: true, useSessions: idle }))
assert.equal(queryAll(calm.container, '.lc-ov-badge').length, 0)
await calm.unmount()
// No sessions seat at all: no badge, no throw.
const seatless = await mount(h(Button, { wide: true }))
assert.equal(queryAll(seatless.container, '.lc-ov-badge').length, 0)
await seatless.unmount()
})
test('clicking opens the overview through the shared store', async () => {
assert.equal(overviewStore.getSnapshot(), false)
const m = await mount(h(Button, { wide: true }))
-15
View File
@@ -19,7 +19,6 @@ import {
refreshSessions,
relativeTime,
rowsOfSnapshot,
runningCountOf,
sessionGroupsOf,
sessionsSnapshotOf,
sortRows,
@@ -89,20 +88,6 @@ describe('sessionsSnapshotOf', () => {
})
})
describe('runningCountOf', () => {
test('unusable snapshots tally zero', () => {
assert.equal(runningCountOf(null), 0)
assert.equal(runningCountOf(7), 0)
assert.equal(runningCountOf({ byId: 7 }), 0)
})
test('running rows tally; a hostile row stops the count at what was seen', () => {
const hostile = new Proxy({}, { get: () => { throw new Error('boom') } })
assert.equal(runningCountOf({ byId: { a: { running: true }, b: { running: false }, c: { running: true } } }), 2)
assert.equal(runningCountOf({ byId: { a: { running: true }, x: hostile } }), 1)
})
})
describe('rowsOfSnapshot', () => {
test('unusable snapshots read null (the unavailable note, not an empty list)', () => {
assert.equal(rowsOfSnapshot(null), null)