mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
// Collapsed roster header counts/phrases only; grouping and states belong to background-task-roster.ts.
|
|
|
|
import type {
|
|
AgentSessionBackgroundTask,
|
|
AgentSessionBackgroundTaskRunState
|
|
} from './agent-session-background-task-wire'
|
|
import type { BackgroundTaskSay } from './background-task-copy'
|
|
import {
|
|
backgroundTaskCountedState,
|
|
backgroundTaskElapsedLabel,
|
|
backgroundTaskStateReason,
|
|
backgroundTaskStateWord,
|
|
type BackgroundTaskGroup
|
|
} from './background-task-roster'
|
|
|
|
type TaskKind = AgentSessionBackgroundTask['kind']
|
|
type RunState = AgentSessionBackgroundTaskRunState
|
|
|
|
/** Below this strip width in rem (16px fallback), replace multi-kind breakdowns with totals. Single-kind state forms still fit; measure the strip so split panes behave like narrow windows. */
|
|
export const NARROW_BACKGROUND_TASKS_STRIP_REM = 24
|
|
|
|
function kindCountLabel(kind: TaskKind, count: number, say: BackgroundTaskSay): string {
|
|
switch (kind) {
|
|
case 'agent':
|
|
return say('countAgents', { count })
|
|
case 'command':
|
|
return say('countShell', { count })
|
|
case 'monitor':
|
|
return say('countMonitors', { count })
|
|
case 'workflow':
|
|
return say('countWorkflows', { count })
|
|
case 'unknown':
|
|
return say('countTasks', { count })
|
|
}
|
|
}
|
|
|
|
/** Replace oversized kind breakdowns entirely; never enumerate a misleading subset. */
|
|
const HEADER_SEGMENT_CAP = 3
|
|
|
|
/** Include done last: settled rows count in the headline and must also count in the breakdown. */
|
|
const HEADER_STATE_ORDER: readonly RunState[] = [
|
|
'working',
|
|
'monitoring',
|
|
'waiting',
|
|
'blocked',
|
|
'unverifiable',
|
|
'idle',
|
|
'done'
|
|
]
|
|
|
|
const ATTENTION_STATES: ReadonlySet<RunState> = new Set(['waiting', 'unverifiable', 'blocked'])
|
|
|
|
export type BackgroundTasksHeaderSegment = {
|
|
text: string
|
|
/** Icon kind, or null for multi-kind totals no single icon can represent. */
|
|
kind: TaskKind | null
|
|
}
|
|
|
|
export type BackgroundTasksHeaderContent = {
|
|
/** Emphasised segments, joined with a muted separator by the renderer. */
|
|
segments: BackgroundTasksHeaderSegment[]
|
|
/** Muted " — …" tail; null when the segments say everything. */
|
|
detail: string | null
|
|
}
|
|
|
|
/** Narrow replaces only multi-kind breakdowns. Any lossy total/fallback stays expandable, regardless of count, so detail remains reachable. */
|
|
export function backgroundTasksHeaderContent(
|
|
groups: readonly BackgroundTaskGroup[],
|
|
options: { narrow: boolean; now: number },
|
|
say: BackgroundTaskSay
|
|
): BackgroundTasksHeaderContent {
|
|
const all = groups.flatMap((group) => group.tasks)
|
|
if (all.length === 0) {
|
|
return { segments: [], detail: say('monitoring') }
|
|
}
|
|
if (groups.length > HEADER_SEGMENT_CAP || (options.narrow && groups.length > 1)) {
|
|
return {
|
|
segments: [{ text: say('headerTotal', { count: all.length }), kind: null }],
|
|
detail: null
|
|
}
|
|
}
|
|
if (groups.length > 1) {
|
|
return {
|
|
segments: groups.map((group) => ({
|
|
text: kindCountLabel(group.kind, group.tasks.length, say),
|
|
kind: group.kind
|
|
})),
|
|
detail: null
|
|
}
|
|
}
|
|
const group = groups[0]
|
|
const count = group.tasks.length
|
|
const uniformState = group.tasks.every((entry) => entry.state === group.tasks[0].state)
|
|
? group.tasks[0].state
|
|
: null
|
|
if (uniformState && ATTENTION_STATES.has(uniformState)) {
|
|
return {
|
|
segments: [
|
|
{
|
|
text: backgroundTaskCountedState(
|
|
count,
|
|
uniformState,
|
|
say,
|
|
kindCountLabel(group.kind, count, say)
|
|
),
|
|
kind: group.kind
|
|
}
|
|
],
|
|
detail: backgroundTaskStateReason(uniformState, say)
|
|
}
|
|
}
|
|
if (count === 1) {
|
|
const entry = group.tasks[0]
|
|
const subject =
|
|
group.kind === 'command' ? say('countShellCommandOne') : kindCountLabel(group.kind, 1, say)
|
|
// A still-growing clock on finished work would lie, exactly as on the row.
|
|
const elapsed =
|
|
group.kind === 'command' && !entry.row.settled
|
|
? backgroundTaskElapsedLabel(entry.row.firstObservedAt, options.now)
|
|
: null
|
|
return {
|
|
segments: [{ text: subject, kind: group.kind }],
|
|
detail: elapsed ?? backgroundTaskStateWord(entry.state, say)
|
|
}
|
|
}
|
|
const stateCounts = HEADER_STATE_ORDER.map((state) => ({
|
|
state,
|
|
count: group.tasks.filter((entry) => entry.state === state).length
|
|
})).filter((entry) => entry.count > 0)
|
|
return {
|
|
segments: [{ text: kindCountLabel(group.kind, count, say), kind: group.kind }],
|
|
// Count done in muted detail, never an emphasized segment: finished siblings claim no composer color.
|
|
detail:
|
|
stateCounts.length > 0
|
|
? stateCounts
|
|
.map((entry) => backgroundTaskCountedState(entry.count, entry.state, say))
|
|
.join(', ')
|
|
: null
|
|
}
|
|
}
|
|
|
|
/** The header as one line, for an accessibility label. */
|
|
export function backgroundTasksHeaderText(header: BackgroundTasksHeaderContent): string {
|
|
const segments = header.segments.map((segment) => segment.text).join(' · ')
|
|
return `${segments}${header.detail ? `${header.segments.length > 0 ? ' — ' : ''}${header.detail}` : ''}`
|
|
}
|