fix(ui): pause progress cards without active runs (#130271)

This commit is contained in:
Dallin Romney
2026-08-26 13:02:36 -07:00
committed by GitHub
parent 8585e7e04e
commit 0abe8dc4e6
9 changed files with 73 additions and 14 deletions
@@ -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,
);
+28 -12
View File
@@ -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<ProgressCardStep["status"], Parameters<typeof t>[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`<span class="session-run-spinner"></span>`;
case "paused":
case "pending":
return icons.clock;
}
@@ -166,7 +168,7 @@ function renderMarkdown(markdown: string | undefined) {
</div>`;
}
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`<li
class="session-progress-card__step session-progress-card__step--${step.status}"
class="session-progress-card__step session-progress-card__step--${presentedStatus}"
aria-label=${t("sessionProgressCard.stepLabel", { status: statusLabel, step: step.step })}
>
<span
class="session-progress-card__step-marker"
data-status=${step.status}
data-status=${presentedStatus}
data-outcome=${terminalStatusKey ? sessionStatus : nothing}
aria-hidden="true"
>${progressStepMarker(step.status, sessionStatus)}</span
>${progressStepMarker(presentedStatus, sessionStatus)}</span
>
<span class="session-progress-card__step-text">${step.step}</span>
</li>`;
@@ -195,9 +206,9 @@ function renderSteps(card: ProgressCard, sessionStatus?: SessionRunStatus) {
</ol>`;
}
function renderBody(card: ProgressCard, sessionStatus?: SessionRunStatus) {
function renderBody(card: ProgressCard, hasActiveRun: boolean, sessionStatus?: SessionRunStatus) {
return html`<div class="session-progress-card__body">
${renderMarkdown(card.markdown)} ${renderSteps(card, sessionStatus)}
${renderMarkdown(card.markdown)} ${renderSteps(card, hasActiveRun, sessionStatus)}
</div>`;
}
@@ -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`<span class="session-run-spinner"></span>`
? progressStepMarker(presentedCurrentStatus ?? "pending")
: icons.clock;
return html`<details
class="session-progress-card session-progress-card--composer"
@@ -306,7 +322,7 @@ export function renderSessionProgressCard(
effectiveSessionStatus === "done"
? " session-progress-card__summary-indicator--complete"
: ""}"
data-status=${currentStep?.status ?? "pending"}
data-status=${presentedCurrentStatus ?? "pending"}
data-outcome=${effectiveSessionStatus ?? nothing}
aria-hidden="true"
>
@@ -340,7 +356,7 @@ export function renderSessionProgressCard(
>
</summary>
<div class="session-progress-card__body" role="region" aria-label=${composerCountLabel}>
${renderMarkdown(card.markdown)} ${renderSteps(card, effectiveSessionStatus)}
${renderMarkdown(card.markdown)} ${renderSteps(card, hasActiveRun, effectiveSessionStatus)}
</div>
</details>`;
}
@@ -357,6 +373,6 @@ export function renderSessionProgressCard(
>${dismiss}
</span>
</div>
${renderBody(card, effectiveSessionStatus)}
${renderBody(card, hasActiveRun, effectiveSessionStatus)}
</section>`;
}
@@ -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");
+1
View File
@@ -258,6 +258,7 @@ export const en: TranslationMap = {
completed: "completed",
failed: "failed",
inProgress: "in progress",
paused: "paused",
pending: "pending",
stopped: "stopped",
},
+4
View File
@@ -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: () => {
+2
View File
@@ -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,
@@ -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[];
@@ -266,6 +266,7 @@ export function renderChatComposerView(context: ChatComposerViewContext) {
activeSession?.status,
activeSession?.startedAt,
activeSession?.endedAt,
props.progressCardHasActiveRun,
)}
</div>`
: nothing;
+2 -1
View File
@@ -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);
}