mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): pause progress cards without active runs (#130271)
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ export const en: TranslationMap = {
|
||||
completed: "completed",
|
||||
failed: "failed",
|
||||
inProgress: "in progress",
|
||||
paused: "paused",
|
||||
pending: "pending",
|
||||
stopped: "stopped",
|
||||
},
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user