From 195897a2e681f75d2904d354dc9dd7f34ef13d0b Mon Sep 17 00:00:00 2001 From: nonoqing Date: Mon, 21 Sep 2026 16:16:30 +0800 Subject: [PATCH] fix(web-ui): show the scheduled job mark on session rows only The scheduled-job count shipped as a pill beside the title, on session rows and workspace rows alike. On a session row it read as a fifth activity badge next to the BTW, dispatch, review, and background-subagent chips, and it drew its clock at 8px, where the shared 1.6 stroke width resolves to about half a pixel, so the glyph became a smudge beside the count. A workspace owns no session, so the count it borrowed from its sessions did not belong on that row at all. Draw the mark as one 12px secondary clock in the session row's trailing cell, the same drawing and slot the session status indicator already uses, and keep the count on the accessible name. A session with a status to report keeps that status, and the cell still yields to the row menu on hover or focus. Workspace rows stop subscribing to the counts store, and the badge stylesheet rule goes with it. Co-authored-by: bitfun-ai <318544290+bitfun-ai@users.noreply.github.com> --- .../sessions/SessionStatusIndicator.tsx | 23 ++++++++++++++-- .../sections/sessions/SessionsSection.scss | 16 ------------ .../sections/sessions/SessionsSection.tsx | 25 +++++++++++------- .../sessions/SessionsSectionLayout.test.ts | 13 +++++----- .../sections/workspaces/WorkspaceItem.tsx | 26 ------------------- .../WorkspaceListSectionLayout.test.ts | 20 +++++++------- 6 files changed, 54 insertions(+), 69 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionStatusIndicator.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionStatusIndicator.tsx index c58e38f284..69968eefc7 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionStatusIndicator.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionStatusIndicator.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, type ReactNode } from 'react'; import { Icon } from '@openbitfun/ui'; import { CircleAlert, CircleCheck, CirclePause, CircleStop, CloudOff, Hand, Loader2, MessageCircleQuestion } from 'lucide-react'; import { useI18n } from '@/infrastructure/i18n'; @@ -17,7 +17,18 @@ const presentation = { syncing: { glyph: CloudOff, tone: 'secondary', label: 'nav.sessions.status.syncing' }, } as const; -export const SessionStatusIndicator = memo(function SessionStatusIndicator({ sessionId }: { sessionId: string }) { +export const SessionStatusIndicator = memo(function SessionStatusIndicator({ + sessionId, + idleFallback, +}: { + sessionId: string; + /** + * Rendered in the same trailing cell while the session has nothing to report, + * so a second row mark can sit on the row's right edge without claiming a + * second cell. The cell still yields to the row menu on hover or focus. + */ + idleFallback?: ReactNode; +}) { const { t } = useI18n('common'); const status = useSessionNavStatus(sessionId); const appearance = status.kind === 'idle' ? null : presentation[status.kind]; @@ -27,6 +38,14 @@ export const SessionStatusIndicator = memo(function SessionStatusIndicator({ ses : t(appearance.label) : ''; + if (!appearance && idleFallback) { + return ( + + {idleFallback} + + ); + } + return appearance ? ( = ({ const backgroundSubagentActivityCount = backgroundSubagentActivity?.totalCount ?? 0; const showBackgroundSubagentActivity = !isChildSession && backgroundSubagentActivityCount > 0; const scheduledJobCount = cronJobCountsBySession.get(session.sessionId) ?? 0; + // Same mark the session status indicator draws, in the same trailing + // cell: one 12px secondary clock on the row's right edge, no count. + // Sessions with a status to report keep that status instead. + const scheduledJobMark = scheduledJobCount > 0 ? ( + + ) : undefined; const parentSessionId = relationship.parentSessionId; const parentSession = parentSessionId ? flowChatState.sessions.get(parentSessionId) : undefined; const parentTitle = parentSession ? resolveSessionTitle(parentSession) : ''; @@ -1724,15 +1735,6 @@ const SessionsSection: React.FC = ({ {dispatchPresentation?.badgeLabel} ) : null} - {scheduledJobCount > 0 ? ( - - - {scheduledJobCount} - - ) : null} {reviewActivityKind ? ( @@ -1770,7 +1772,10 @@ const SessionsSection: React.FC = ({
- +
{ const btwBadgeBlock = extractInlineItemBlock(stylesheet, 'btw-badge'); const reviewBadgeBlock = extractInlineItemBlock(stylesheet, 'review-badge'); const backgroundSubagentBadgeBlock = extractInlineItemBlock(stylesheet, 'background-subagent-badge'); - // Shared by session rows and workspace rows, so it must hold its slot width. - const cronBadgeBlock = extractInlineItemBlock(stylesheet, 'cron-badge'); expect(labelBlock).toContain('flex: 1 1 0;'); expect(labelBlock).toContain('overflow: hidden;'); @@ -191,10 +189,13 @@ describe('SessionsSection layout styles', () => { expect(backgroundSubagentBadgeBlock).toContain('line-height: 0;'); expect(backgroundSubagentBadgeBlock).toContain('width: var(--openbitfun-control-icon-size-md);'); expect(backgroundSubagentBadgeBlock).toContain('height: var(--openbitfun-control-icon-size-md);'); - expect(cronBadgeBlock).toContain('flex: 0 0 auto;'); - expect(cronBadgeBlock).toContain('display: inline-flex;'); - expect(cronBadgeBlock).toContain('align-items: center;'); - expect(cronBadgeBlock).toContain('white-space: nowrap;'); + // The scheduled-job mark is not a chip beside the title: it is one 12px + // secondary clock in the trailing cell, the same drawing and slot the session + // status indicator uses, so no `cron` rule may come back to the stylesheet. + expect(stylesheet).not.toContain('cron'); + expect(readSessionsSectionSource()).toContain('idleFallback={scheduledJobMark}'); + expect(readSessionsSectionSource()).toMatch(/ = ({ }) => { const { t } = useI18n('common'); const { t: tFiles } = useTranslation('panels/files'); - useEffect(() => { ensureCronJobCountsListener(); }, []); - const cronJobCounts = useSyncExternalStore( - subscribeCronJobCounts, - getCronJobCountsSnapshot, - getCronJobCountsSnapshot, - ); - const scheduledJobCount = cronJobCounts.byWorkspaceId.get(workspace.id) ?? 0; - // Rendered through ActionItem's trailing slot so both name rows keep the badge on - // the name line. The rows flatten that label slot with `display: contents`, so a - // badge appended to the label children would silently break if that rule changed. - const scheduledJobBadge = scheduledJobCount > 0 ? ( - - - {scheduledJobCount} - - ) : undefined; const { setActiveWorkspace, closeWorkspaceById, @@ -878,7 +854,6 @@ const WorkspaceItem: React.FC = ({ className="openbitfun-nav-panel__assistant-item-name-action" triggerClassName="openbitfun-nav-panel__assistant-item-name-btn" labelBehavior="static" - metadata={scheduledJobBadge} onClick={e => { e.stopPropagation(); handleCollapseToggle(); }} data-testid="nav-workspace-name-btn" data-workspace-id={workspace.id} @@ -1185,7 +1160,6 @@ const WorkspaceItem: React.FC = ({ className="openbitfun-nav-panel__workspace-item-name-action" triggerClassName="openbitfun-nav-panel__workspace-item-name-btn" labelBehavior="static" - metadata={scheduledJobBadge} onClick={e => { e.stopPropagation(); handleCollapseToggle(); }} data-testid="nav-workspace-name-btn" data-workspace-id={workspace.id} diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSectionLayout.test.ts b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSectionLayout.test.ts index d7d50ce78f..2ecd6a2dd9 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSectionLayout.test.ts +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSectionLayout.test.ts @@ -159,17 +159,19 @@ describe('WorkspaceListSection layout styles', () => { expect(source.match(/data-testid="nav-workspace-new-session-btn"/g)).toHaveLength(2); }); - it('keeps the scheduled-job badge on the trailing slot of both workspace rows', () => { + it('keeps the scheduled-job mark off workspace rows', () => { const source = readWorkspaceItemSource(); - // The workspace row and the assistant-group row both flatten their ActionItem - // label slot with `display: contents`, so the badge must stay one flex sibling of - // the name and be rendered by each variant from a single source. - expect(source).toContain('const scheduledJobBadge = scheduledJobCount > 0 ? ('); - expect(source.match(/metadata=\{scheduledJobBadge\}/g)).toHaveLength(2); - expect(source).not.toContain('{scheduledJobCount > 0 ?'); - expect(source).toContain('className="openbitfun-nav-panel__inline-item-cron-badge"'); - expect(source).toContain("title={t('nav.scheduledJobs.badgeTooltip', { count: scheduledJobCount })}"); + // A workspace row owns no session, so the scheduled-job count it used to + // borrow from its sessions belongs on the session rows only. The workspace + // row must not subscribe to the counts store or claim the name row's + // trailing metadata slot for it. + expect(source).not.toContain('cronJobCountsStore'); + expect(source).not.toContain('scheduledJobCount'); + expect(source).not.toContain('scheduledJobBadge'); + expect(source).not.toContain('inline-item-cron'); + expect(source).not.toContain('nav.scheduledJobs.badgeTooltip'); + expect(source).not.toContain('metadata={scheduledJobBadge}'); }); it('keeps workspace and assistant rows flat on hover', () => {