diff --git a/ui/src/components/session-progress-card.test.ts b/ui/src/components/session-progress-card.test.ts index fe23844a0cbb..010e9dad950c 100644 --- a/ui/src/components/session-progress-card.test.ts +++ b/ui/src/components/session-progress-card.test.ts @@ -194,6 +194,30 @@ describe("renderSessionProgressCard", () => { ).not.toBeNull(); }); + it("presents durable in-progress work as paused without an active run", () => { + const container = document.createElement("div"); + render( + renderSessionProgressCard( + progressCard, + "composer", + undefined, + undefined, + undefined, + undefined, + false, + ), + container, + ); + + expect(container.querySelector(".session-run-spinner")).toBeNull(); + expect( + container.querySelector('.session-progress-card__current-marker[data-status="paused"]'), + ).not.toBeNull(); + const pausedStep = container.querySelector(".session-progress-card__step--paused"); + expect(pausedStep?.getAttribute("aria-label")).toBe("Wire the checklist, paused"); + expect(pausedStep?.querySelector("polyline")).not.toBeNull(); + }); + it("keeps a disclosure affordance beside a completed dismissible composer card", () => { const container = document.createElement("div"); const completed = { @@ -282,7 +306,7 @@ describe("renderSessionProgressCard", () => { ).toBe(expected); }); - it("uses a terminal circle-x instead of a spinner after the run stops", () => { + it("uses a terminal circle-x instead of pausing after the run stops", () => { const container = document.createElement("div"); render( renderSessionProgressCard( @@ -292,6 +316,7 @@ describe("renderSessionProgressCard", () => { "killed", RUN_STARTED_MS, RUN_ENDED_MS, + false, ), container, ); diff --git a/ui/src/components/session-progress-card.ts b/ui/src/components/session-progress-card.ts index 1393fcfd3607..6c044d89c322 100644 --- a/ui/src/components/session-progress-card.ts +++ b/ui/src/components/session-progress-card.ts @@ -11,6 +11,7 @@ import { icons } from "./icons.ts"; import { toSanitizedMarkdownHtml } from "./markdown.ts"; type SessionProgressCardPlacement = "board" | "composer" | "hovercard"; +type PresentedProgressStepStatus = ProgressCardStep["status"] | "paused"; const STATUS_LABEL_KEYS: Record[0]> = { completed: "sessionProgressCard.status.completed", @@ -130,7 +131,7 @@ function currentProgressStep(steps: readonly ProgressCardStep[]): ProgressCardSt ); } -function progressStepMarker(status: ProgressCardStep["status"], sessionStatus?: SessionRunStatus) { +function progressStepMarker(status: PresentedProgressStepStatus, sessionStatus?: SessionRunStatus) { if (status === "in_progress" && sessionStatus === "done") { return icons.check; } @@ -145,6 +146,7 @@ function progressStepMarker(status: ProgressCardStep["status"], sessionStatus?: return icons.check; case "in_progress": return html``; + case "paused": case "pending": return icons.clock; } @@ -166,7 +168,7 @@ function renderMarkdown(markdown: string | undefined) { `; } -function renderSteps(card: ProgressCard, sessionStatus?: SessionRunStatus) { +function renderSteps(card: ProgressCard, hasActiveRun: boolean, sessionStatus?: SessionRunStatus) { const steps = card.steps; if (!steps?.length) { return nothing; @@ -177,17 +179,26 @@ function renderSteps(card: ProgressCard, sessionStatus?: SessionRunStatus) { step.status === "in_progress" && sessionStatus ? TERMINAL_STEP_STATUS_LABEL_KEYS[sessionStatus] : undefined; - const statusLabel = t(terminalStatusKey ?? STATUS_LABEL_KEYS[step.status]); + const presentedStatus = + step.status === "in_progress" && !hasActiveRun && !terminalStatusKey + ? "paused" + : step.status; + const statusLabel = t( + terminalStatusKey ?? + (presentedStatus === "paused" + ? "sessionProgressCard.status.paused" + : STATUS_LABEL_KEYS[presentedStatus]), + ); return html`
  • ${progressStepMarker(presentedStatus, sessionStatus)} ${step.step}
  • `; @@ -195,9 +206,9 @@ function renderSteps(card: ProgressCard, sessionStatus?: SessionRunStatus) { `; } -function renderBody(card: ProgressCard, sessionStatus?: SessionRunStatus) { +function renderBody(card: ProgressCard, hasActiveRun: boolean, sessionStatus?: SessionRunStatus) { return html`
    - ${renderMarkdown(card.markdown)} ${renderSteps(card, sessionStatus)} + ${renderMarkdown(card.markdown)} ${renderSteps(card, hasActiveRun, sessionStatus)}
    `; } @@ -208,6 +219,7 @@ export function renderSessionProgressCard( sessionStatus?: SessionRunStatus, startedAt?: number, endedAt?: number, + hasActiveRun = true, ) { if (!card) { return nothing; @@ -282,6 +294,10 @@ export function renderSessionProgressCard( total: String(counts.total), }) : nothing; + const presentedCurrentStatus = + currentStep?.status === "in_progress" && !hasActiveRun && !terminalOutcomeKey + ? "paused" + : currentStep?.status; const summaryIndicator = effectiveSessionStatus === "done" ? icons.check @@ -292,7 +308,7 @@ export function renderSessionProgressCard( : complete ? icons.check : currentStep?.status === "in_progress" - ? html`` + ? progressStepMarker(presentedCurrentStatus ?? "pending") : icons.clock; return html``; } @@ -357,6 +373,6 @@ export function renderSessionProgressCard( >${dismiss} - ${renderBody(card, effectiveSessionStatus)} + ${renderBody(card, hasActiveRun, effectiveSessionStatus)} `; } diff --git a/ui/src/e2e/session-progress-live-placement.e2e.test.ts b/ui/src/e2e/session-progress-live-placement.e2e.test.ts index 2f7cc2169f9a..75d72612996b 100644 --- a/ui/src/e2e/session-progress-live-placement.e2e.test.ts +++ b/ui/src/e2e/session-progress-live-placement.e2e.test.ts @@ -89,6 +89,9 @@ suite.define(() => { key: sessionKey, kind: "direct", label: "Progress placement", + hasActiveRun: true, + activeRunIds: ["stale-run"], + status: "completed", updatedAt, }, ]), @@ -126,6 +129,11 @@ suite.define(() => { await expect .poll(() => visiblePane.locator('[data-progress-card-placement="composer"]').count()) .toBe(1); + const pausedStep = visiblePane.locator(".session-progress-card__step--paused"); + await expect.poll(() => pausedStep.getAttribute("aria-label")).toBe("Implement, paused"); + await expect + .poll(() => visiblePane.locator(".session-progress-card .session-run-spinner").count()) + .toBe(0); await expectVisibleLastActivity("composer"); await captureProof(page, "composer-attached-wide.png"); diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 2dba87a7adf3..42b24eb841f8 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -258,6 +258,7 @@ export const en: TranslationMap = { completed: "completed", failed: "failed", inProgress: "in progress", + paused: "paused", pending: "pending", stopped: "stopped", }, diff --git a/ui/src/pages/chat/chat-pane-render.ts b/ui/src/pages/chat/chat-pane-render.ts index 7f4ba480e6e0..4abd655a24ce 100644 --- a/ui/src/pages/chat/chat-pane-render.ts +++ b/ui/src/pages/chat/chat-pane-render.ts @@ -21,6 +21,7 @@ import { resolveChatPaneObserverRunId, } from "../../lib/observer-digest.ts"; import { hasSessionPresenceViewers } from "../../lib/presence-users.ts"; +import { isSessionRunActive } from "../../lib/session-run-state.ts"; import { buildAgentMainSessionKey } from "../../lib/sessions/session-key.ts"; import { showToast } from "../../lib/toast.ts"; import { generateUUID } from "../../lib/uuid.ts"; @@ -315,6 +316,9 @@ export class ChatPane extends ChatPaneLayoutRender { compactionStatus: state.compactionStatus, fallbackStatus: state.fallbackStatus, progressCard: this.progressCard.card, + progressCardHasActiveRun: Boolean( + state.chatRunId || (selectedSession && isSessionRunActive(selectedSession)), + ), onDismissProgressCard, gatewayQuestionPrompts: catalogKey || sessionParticipationBlocked ? [] : this.questionPrompts, onGatewayQuestionChange: () => { diff --git a/ui/src/pages/chat/chat-view.ts b/ui/src/pages/chat/chat-view.ts index 0eb76be43a50..c5d332b4858a 100644 --- a/ui/src/pages/chat/chat-view.ts +++ b/ui/src/pages/chat/chat-view.ts @@ -109,6 +109,7 @@ export type ChatProps = ChatTaskSuggestionTrayProps & compactionStatus?: CompactionStatus | null; fallbackStatus?: FallbackStatus | null; progressCard?: ProgressCard | null; + progressCardHasActiveRun?: boolean; onDismissProgressCard?: (card: ProgressCard) => void; gatewayQuestionPrompts?: readonly QuestionPrompt[]; onGatewayQuestionChange?: () => void; @@ -429,6 +430,7 @@ export function renderChat(props: ChatProps) { compactionStatus: props.compactionStatus, fallbackStatus: props.fallbackStatus, progressCard: props.progressCard, + progressCardHasActiveRun: props.progressCardHasActiveRun, onDismissProgressCard: props.onDismissProgressCard, gatewayQuestionPrompts: props.gatewayQuestionPrompts, messages: props.messages, diff --git a/ui/src/pages/chat/components/chat-composer-types.ts b/ui/src/pages/chat/components/chat-composer-types.ts index 067c9a88de93..8760473d3f75 100644 --- a/ui/src/pages/chat/components/chat-composer-types.ts +++ b/ui/src/pages/chat/components/chat-composer-types.ts @@ -74,6 +74,7 @@ export type ChatComposerProps = ChatAttachmentControlsProps & { compactionStatus?: CompactionStatus | null; fallbackStatus?: FallbackStatus | null; progressCard?: ProgressCard | null; + progressCardHasActiveRun?: boolean; onDismissProgressCard?: (card: ProgressCard) => void; gatewayQuestionPrompts?: readonly QuestionPrompt[]; messages: unknown[]; diff --git a/ui/src/pages/chat/components/chat-composer-view.ts b/ui/src/pages/chat/components/chat-composer-view.ts index f9de9a47beda..65fb35c4dd77 100644 --- a/ui/src/pages/chat/components/chat-composer-view.ts +++ b/ui/src/pages/chat/components/chat-composer-view.ts @@ -266,6 +266,7 @@ export function renderChatComposerView(context: ChatComposerViewContext) { activeSession?.status, activeSession?.startedAt, activeSession?.endedAt, + props.progressCardHasActiveRun, )} ` : nothing; diff --git a/ui/src/styles/chat/progress-card.css b/ui/src/styles/chat/progress-card.css index 146231f27c2b..01b470086112 100644 --- a/ui/src/styles/chat/progress-card.css +++ b/ui/src/styles/chat/progress-card.css @@ -611,7 +611,8 @@ color: var(--session-progress-neutral); } -.session-progress-card__step--pending .session-progress-card__step-marker { +.session-progress-card__step--pending .session-progress-card__step-marker, +.session-progress-card__step--paused .session-progress-card__step-marker { color: var(--session-progress-neutral); }