fix(ui): dismiss completed progress cards (#126102)

* test(ui): cover completed progress card dismissal

* test(ui): capture both completed card themes

* fix(ui): dismiss completed progress cards

* style: format progress card dismissal

* test(ui): assert visible dismissal lifecycle

* test(ui): wait for reloaded composer proof

* refactor(ui): reuse progress card action styles

* chore(protocol): refresh Swift progress card params

* style(ui): format progress card test

* fix(ui): hide progress dismissal from viewers

* test(ui): use Vitest viewer assertion
This commit is contained in:
Vyctor H. Brzezowski
2026-08-18 23:39:31 -03:00
committed by GitHub
parent 89a7e4e965
commit 2a469c1cb2
20 changed files with 350 additions and 19 deletions
@@ -1257,21 +1257,25 @@ public struct ProgressCardPutParams: Codable, Sendable {
public let sessionkey: String
public let markdown: String?
public let plan: [ProgressCardStep]?
public let expectedrevision: Int?
public init(
sessionkey: String,
markdown: String? = nil,
plan: [ProgressCardStep]? = nil)
plan: [ProgressCardStep]? = nil,
expectedrevision: Int? = nil)
{
self.sessionkey = sessionkey
self.markdown = markdown
self.plan = plan
self.expectedrevision = expectedrevision
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case markdown
case plan
case expectedrevision = "expectedRevision"
}
}
@@ -43,6 +43,7 @@ export const ProgressCardPutParamsSchema = closedObject({
sessionKey: NonEmptyString,
markdown: Type.Optional(Type.String()),
plan: Type.Optional(Type.Array(ProgressCardStepSchema, { maxItems: PROGRESS_CARD_MAX_STEPS })),
expectedRevision: Type.Optional(Type.Integer({ minimum: 1 })),
});
export type ProgressCardPutParams = Static<typeof ProgressCardPutParamsSchema>;
+1 -1
View File
@@ -14,7 +14,7 @@ export type ProgressCardStore = {
get(sessionKey: string): ProgressCard | null;
put(
sessionKey: string,
input: { markdown?: string; steps?: ProgressCardStep[] },
input: { markdown?: string; steps?: ProgressCardStep[]; expectedRevision?: number },
): { card: ProgressCard | null };
};
@@ -18,7 +18,11 @@ function createHarness() {
const store: ProgressCardStore = {
get: (sessionKey) => cards.get(sessionKey) ?? null,
put: (sessionKey, input) => {
const current = cards.get(sessionKey);
if (!input.markdown && !input.steps?.length) {
if (input.expectedRevision !== undefined && current?.revision !== input.expectedRevision) {
return { card: current ?? null };
}
cards.delete(sessionKey);
return { card: null };
}
@@ -157,4 +161,34 @@ describe("progress card gateway methods", () => {
revision: null,
});
});
it("dismisses only the matching completed revision", async () => {
const { broadcast, invoke } = createHarness();
await invoke("progressCard.put", {
sessionKey: "agent:main:main",
plan: [{ step: "Done", status: "completed" }],
});
broadcast.mockClear();
const stale = await invoke("progressCard.put", {
sessionKey: "agent:main:main",
expectedRevision: 2,
});
const dismissed = await invoke("progressCard.put", {
sessionKey: "agent:main:main",
expectedRevision: 1,
});
expect(stale).toHaveBeenCalledWith(
true,
{ card: expect.objectContaining({ revision: 1 }) },
undefined,
);
expect(dismissed).toHaveBeenCalledWith(true, { card: null }, undefined);
expect(broadcast).toHaveBeenCalledOnce();
expect(broadcast).toHaveBeenCalledWith("progressCard.changed", {
sessionKey: "agent:main:main",
revision: null,
});
});
});
+20 -4
View File
@@ -71,16 +71,32 @@ export function createProgressCardHandlers(
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, error.message));
return;
}
if (params.expectedRevision !== undefined && (input.markdown || input.steps?.length)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"expectedRevision is only valid when clearing a card",
),
);
return;
}
const sessionKey = resolveProgressCardSessionKey(params.sessionKey, context, respond);
if (!sessionKey) {
return;
}
try {
const result = store.put(sessionKey, input);
context.broadcast("progressCard.changed", {
sessionKey,
revision: result.card?.revision ?? null,
const result = store.put(sessionKey, {
...input,
expectedRevision: params.expectedRevision,
});
if (params.expectedRevision === undefined || result.card === null) {
context.broadcast("progressCard.changed", {
sessionKey,
revision: result.card?.revision ?? null,
});
}
respond(true, result, undefined);
} catch (error) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(error)));
@@ -88,4 +88,42 @@ describe("session progress card store", () => {
expect(readSessionProgressCard(db, SESSION_KEY)).toBeNull();
});
it("dismisses only a completed card at the expected revision", () => {
writeSessionProgressCard(db, SESSION_KEY, {
steps: [{ step: "Done", status: "completed" }],
});
expect(writeSessionProgressCard(db, SESSION_KEY, { expectedRevision: 2 })).toEqual({
card: expect.objectContaining({ revision: 1 }),
});
expect(readSessionProgressCard(db, SESSION_KEY)).not.toBeNull();
expect(writeSessionProgressCard(db, SESSION_KEY, { expectedRevision: 1 })).toEqual({
cleared: true,
});
expect(readSessionProgressCard(db, SESSION_KEY)).toBeNull();
expect(
writeSessionProgressCard(db, SESSION_KEY, {
steps: [{ step: "New work", status: "in_progress" }],
}),
).toEqual({
card: expect.objectContaining({ revision: 3 }),
});
expect(writeSessionProgressCard(db, SESSION_KEY, { expectedRevision: 1 })).toEqual({
card: expect.objectContaining({ revision: 3 }),
});
});
it("does not dismiss an active or note-only card", () => {
writeSessionProgressCard(db, SESSION_KEY, { steps: STEPS });
expect(writeSessionProgressCard(db, SESSION_KEY, { expectedRevision: 1 })).toEqual({
card: expect.objectContaining({ revision: 1 }),
});
writeSessionProgressCard(db, SESSION_KEY, { markdown: "Still relevant" });
expect(writeSessionProgressCard(db, SESSION_KEY, { expectedRevision: 2 })).toEqual({
card: expect.objectContaining({ revision: 2 }),
});
});
});
+33 -8
View File
@@ -61,8 +61,11 @@ function selectProgressCard(db: DatabaseSync, sessionKey: string): StoredProgres
);
}
function rowToProgressCard(row: StoredProgressCardRow): ProgressCard {
function rowToProgressCard(row: StoredProgressCardRow): ProgressCard | null {
const steps = row.steps_json ? readStoredSteps(row.steps_json) : undefined;
if (!row.markdown && !steps?.length) {
return null;
}
return {
sessionKey: row.session_key,
revision: row.revision,
@@ -112,19 +115,41 @@ export function readSessionProgressCard(
export function writeSessionProgressCard(
dbPathOrDb: ProgressCardDatabaseInput,
sessionKey: string,
input: { markdown?: string; steps?: ProgressCardStep[] },
): { card: ProgressCard } | { cleared: true } {
input: { markdown?: string; steps?: ProgressCardStep[]; expectedRevision?: number },
): { card: ProgressCard | null } | { cleared: true } {
return withProgressCardDatabase(dbPathOrDb, false, (db, label) => {
const write = (): { card: ProgressCard } | { cleared: true } => {
const write = (): { card: ProgressCard | null } | { cleared: true } => {
ensureOpenClawAgentProgressCardSchemaInTransaction(db);
const kysely = getNodeSqliteKysely<ProgressCardDatabase>(db);
const markdown = input.markdown?.trim() ? input.markdown : undefined;
const steps = input.steps && input.steps.length > 0 ? input.steps : undefined;
if (!markdown && !steps) {
executeSqliteQuerySync(
db,
kysely.deleteFrom("session_progress_cards").where("session_key", "=", sessionKey),
);
const previous = selectProgressCard(db, sessionKey);
if (input.expectedRevision !== undefined) {
const current = previous ? rowToProgressCard(previous) : null;
if (
!previous ||
previous.revision !== input.expectedRevision ||
!current?.steps?.length ||
current.steps.some((step) => step.status !== "completed")
) {
return { card: current };
}
}
if (previous) {
executeSqliteQuerySync(
db,
kysely
.updateTable("session_progress_cards")
.set({
markdown: null,
steps_json: null,
revision: previous.revision + 1,
updated_at: Date.now(),
})
.where("session_key", "=", sessionKey),
);
}
return { cleared: true };
}
const previous = selectProgressCard(db, sessionKey);
@@ -28,6 +28,9 @@ export class SessionProgressCardController implements ReactiveController {
return this.store?.get(this.sessionKey) ?? null;
}
dismiss = (card: ProgressCard): Promise<boolean> =>
this.store?.dismiss(card) ?? Promise.resolve(false);
hostUpdate(): void {
this.synchronize();
}
+25 -1
View File
@@ -2,6 +2,7 @@ import type { ProgressCard, ProgressCardStep } from "@openclaw/gateway-protocol"
import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { t } from "../i18n/index.ts";
import { icons } from "./icons.ts";
import { toSanitizedMarkdownHtml } from "./markdown.ts";
type SessionProgressCardPlacement = "board" | "composer" | "hovercard" | "rail";
@@ -69,6 +70,7 @@ function renderBody(card: ProgressCard) {
export function renderSessionProgressCard(
card: ProgressCard | null | undefined,
placement: SessionProgressCardPlacement,
onDismiss?: (card: ProgressCard) => void,
) {
if (!card) {
return nothing;
@@ -80,6 +82,24 @@ export function renderSessionProgressCard(
total: String(counts.total),
})
: t("sessionProgressCard.noteLabel");
const dismissible = Boolean(
onDismiss && card.steps?.length && card.steps.every((step) => step.status === "completed"),
);
const dismiss = dismissible
? html`<button
class="rail-header__action session-progress-card__dismiss"
type="button"
aria-label=${t("sessionProgressCard.dismiss")}
title=${t("sessionProgressCard.dismiss")}
@click=${(event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onDismiss?.(card);
}}
>
${icons.x}
</button>`
: nothing;
if (placement === "composer") {
const current = currentProgressStep(card.steps ?? []);
return html`<details
@@ -96,6 +116,7 @@ export function renderSessionProgressCard(
>${counts.completed}/${counts.total}</span
>`
: nothing}
${dismiss}
</summary>
${renderBody(card)}
</details>`;
@@ -108,7 +129,10 @@ export function renderSessionProgressCard(
${counts
? html`<div class="session-progress-card__heading">
<span>${t("sessionProgressCard.title")}</span>
<span>${counts.completed}/${counts.total}</span>
<span class="session-progress-card__heading-actions">
<span>${counts.completed}/${counts.total}</span>
${dismiss}
</span>
</div>`
: nothing}
${renderBody(card)}
@@ -137,4 +137,121 @@ suite.define(() => {
},
);
});
it("dismisses a completed card across rerender and reload", async () => {
const sessionKey = "agent:main:progress-complete";
const plan = [
{ step: "Inspected owner", status: "completed" },
{ step: "Implemented fix", status: "completed" },
{ step: "Filed issue", status: "completed" },
];
for (const colorScheme of ["light", "dark"] as const) {
await suite.withPage(
{
colorScheme,
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 560 },
},
async ({ page }) => {
const gateway = await installMockGateway(page, {
featureMethods: [
"chat.metadata",
"chat.startup",
"progressCard.get",
"progressCard.put",
],
methodResponses: {
"progressCard.get": {
card: {
revision: 3,
sessionKey,
steps: plan,
updatedAt: 3,
},
},
"progressCard.put": { card: null },
"sessions.list": chatSessionListResponse([
{
key: sessionKey,
kind: "direct",
label: "Completed progress",
updatedAt: 3,
},
]),
},
sessionKey,
});
await page.goto(controlUiSessionUrl(suite.server.baseUrl, sessionKey));
await expect.poll(() => gateway.getRequests("progressCard.get")).toHaveLength(1);
const card = page.locator('[data-progress-card-placement="composer"]');
await expect.poll(() => card.isVisible()).toBe(true);
await card.locator("summary").click();
await captureProof(page, `completed-${colorScheme}-before.png`);
await card.getByRole("button", { name: "Dismiss progress card" }).click();
const dismissRequest = await gateway.waitForRequest("progressCard.put");
expect(dismissRequest.params).toEqual({ sessionKey, expectedRevision: 3 });
await expect.poll(() => card.count()).toBe(0);
await page.locator("textarea").fill("rerender");
await expect.poll(() => card.count()).toBe(0);
await gateway.setMethodResponse("progressCard.get", { card: null });
await page.reload();
await page.locator("textarea").waitFor({ state: "visible" });
await expect.poll(() => card.count()).toBe(0);
expect(await gateway.getRequests("chat.send")).toHaveLength(0);
await captureProof(page, `completed-${colorScheme}-after.png`);
},
);
}
});
it("keeps dismissal unavailable to a restricted session viewer", async () => {
const sessionKey = "agent:main:progress-viewer";
await suite.withPage(
{
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 560 },
},
async ({ page }) => {
const gateway = await installMockGateway(page, {
featureMethods: ["chat.metadata", "chat.startup", "progressCard.get", "progressCard.put"],
hasMultipleSessionSharingIdentities: true,
methodResponses: {
"progressCard.get": {
card: {
revision: 1,
sessionKey,
steps: [{ step: "Completed work", status: "completed" }],
updatedAt: 1,
},
},
"sessions.list": chatSessionListResponse([
{
key: sessionKey,
kind: "direct",
label: "Restricted progress",
sharingRole: "viewer",
updatedAt: 1,
visibility: "suggest",
},
]),
},
sessionKey,
});
await page.goto(controlUiSessionUrl(suite.server.baseUrl, sessionKey));
const card = page.locator('[data-progress-card-placement="composer"]');
await expect.poll(() => card.isVisible()).toBe(true);
await expect
.poll(() => card.getByRole("button", { name: "Dismiss progress card" }).count())
.toBe(0);
expect(await gateway.getRequests("progressCard.put")).toHaveLength(0);
},
);
});
});
+2
View File
@@ -189,6 +189,8 @@ export const en: TranslationMap = {
ariaLabel: "Session progress",
title: "Progress",
noteLabel: "Progress note",
dismiss: "Dismiss progress card",
dismissFailed: "Could not dismiss the progress card. Try again.",
widgetLabel: "Session progress",
widgetLoading: "Loading session progress…",
widgetEmpty: "No progress card yet",
+22
View File
@@ -1,6 +1,7 @@
import type {
ProgressCard,
ProgressCardGetResult,
ProgressCardPutResult,
ProgressCardStep,
} from "@openclaw/gateway-protocol";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
@@ -9,6 +10,7 @@ import type { ApplicationGateway } from "../app/gateway.ts";
import { isGatewayMethodAdvertised } from "./gateway-methods.ts";
const PROGRESS_CARD_GET_METHOD = "progressCard.get";
const PROGRESS_CARD_PUT_METHOD = "progressCard.put";
const PROGRESS_CARD_CHANGED_EVENT = "progressCard.changed";
const CACHE_LIMIT = 100;
@@ -23,6 +25,7 @@ export type SessionProgressCardStore = {
watch: (owner: object, sessionKeys: readonly string[]) => void;
unwatch: (owner: object) => void;
load: (sessionKey: string) => Promise<ProgressCard | null>;
dismiss: (card: ProgressCard) => Promise<boolean>;
get: (sessionKey: string) => ProgressCard | null | undefined;
getError: (sessionKey: string) => SessionProgressCardLoadError | undefined;
subscribe: (listener: () => void) => () => void;
@@ -289,6 +292,25 @@ function createStore(gateway: ApplicationGateway): SessionProgressCardStore {
watch,
unwatch: (owner) => watch(owner, []),
load,
dismiss: async (card) => {
const client = gateway.snapshot.client;
if (!client) {
return false;
}
const result = await client.request<ProgressCardPutResult>(PROGRESS_CARD_PUT_METHOD, {
sessionKey: card.sessionKey,
expectedRevision: card.revision,
});
const dismissed = result.card === null;
if (dismissed && cache.get(card.sessionKey)?.revision === card.revision) {
remember(card.sessionKey, { card: null, revision: null });
notify();
} else if (result.card) {
remember(card.sessionKey, { card: result.card, revision: result.card.revision });
notify();
}
return dismissed;
},
get: (sessionKey) => cache.get(sessionKey)?.card,
getError: (sessionKey) => errors.get(sessionKey),
subscribe: (listener) => {
@@ -33,6 +33,7 @@ type SidebarPanelDefinitionParams = {
lastReadAt: number | undefined;
pullRequests: ControlUiSessionPullRequest[];
progressCard: ProgressCard | null;
onDismissProgressCard?: (card: ProgressCard) => void;
companion: ChatSessionCompanionThread;
onCompanionSubmit: (question: string) => void;
onCompanionDraftChange: (draft: string) => void;
@@ -116,6 +117,7 @@ export function sidebarPanelDefinitions(
.startedAt=${params.startedAt}
.lastReadAt=${params.lastReadAt}
.progressCard=${params.progressCard}
.onDismissProgressCard=${params.onDismissProgressCard}
.pullRequests=${params.pullRequests}
.companion=${params.companion}
.connected=${state?.connected === true}
+7 -1
View File
@@ -1,5 +1,8 @@
import { html, nothing } from "lit";
import type { SessionObserverDigest } from "../../../../packages/gateway-protocol/src/schema/sessions.js";
import type {
ProgressCard,
SessionObserverDigest,
} from "../../../../packages/gateway-protocol/src/index.js";
import type { GatewaySessionRow } from "../../api/types.ts";
import { isDesktopPanelAvailable } from "../../app/app-shell-chrome.ts";
import { ChatPaneBrowserAnnotationRender } from "./chat-pane-browser-annotation-render.ts";
@@ -36,6 +39,7 @@ type ChatPaneLayoutRenderParams = {
board: ResolvedBoardView;
sidebarLayout: SidebarLayout;
progressCardInRail: boolean;
onDismissProgressCard?: (card: ProgressCard) => void;
sessionWorkspace: SessionWorkspaceProps;
backgroundTasks: BackgroundTasksProps;
chatProps: ChatProps;
@@ -58,6 +62,7 @@ export abstract class ChatPaneLayoutRender extends ChatPaneBrowserAnnotationRend
board,
sidebarLayout,
progressCardInRail,
onDismissProgressCard,
sessionWorkspace,
backgroundTasks,
chatProps,
@@ -118,6 +123,7 @@ export abstract class ChatPaneLayoutRender extends ChatPaneBrowserAnnotationRend
lastReadAt: selectedSession?.lastReadAt,
pullRequests: this.sessionPullRequests,
progressCard: progressCardInRail ? this.progressCard.card : null,
onDismissProgressCard,
companion: companionThread,
onCompanionSubmit: (question) => void this.submitSessionCompanionQuestion(question),
onCompanionDraftChange: (draft) =>
+15
View File
@@ -18,6 +18,7 @@ import {
} from "../../lib/observer-digest.ts";
import { hasSessionPresenceViewers } from "../../lib/presence-users.ts";
import { buildAgentMainSessionKey } from "../../lib/sessions/session-key.ts";
import { showToast } from "../../lib/toast.ts";
import { clearChatHistory } from "./chat-history.ts";
import { resolveChatMessageAccess } from "./chat-message-access.ts";
import { requiresChatModelSetup } from "./chat-model-setup.ts";
@@ -158,6 +159,18 @@ export class ChatPane extends ChatPaneLayoutRender {
session: selectedSession,
});
const gatewaySnapshot = this.context.gateway.snapshot;
const canDismissProgressCard =
state.connected &&
!sessionParticipationBlocked &&
hasOperatorWriteAccess(gatewaySnapshot.hello?.auth ?? null) &&
isGatewayMethodAdvertised(gatewaySnapshot, "progressCard.put") === true;
const onDismissProgressCard = canDismissProgressCard
? (card: NonNullable<ChatProps["progressCard"]>) => {
void this.progressCard
.dismiss(card)
.catch(() => showToast({ message: t("sessionProgressCard.dismissFailed") }));
}
: undefined;
const restartRecoveryTombstoned = selectedSession?.restartRecoveryStatus === "tombstoned";
const multiIdentity = this.hasMultipleIdentities();
const suggestionViewer =
@@ -285,6 +298,7 @@ export class ChatPane extends ChatPaneLayoutRender {
compactionStatus: state.compactionStatus,
fallbackStatus: state.fallbackStatus,
progressCard: progressCardInRail ? null : this.progressCard.card,
onDismissProgressCard,
gatewayQuestionPrompts: catalogKey || sessionParticipationBlocked ? [] : this.questionPrompts,
onGatewayQuestionChange: () => {
this.questionPrompts = [...this.questionPrompts];
@@ -547,6 +561,7 @@ export class ChatPane extends ChatPaneLayoutRender {
board,
sidebarLayout,
progressCardInRail,
onDismissProgressCard,
sessionWorkspace,
backgroundTasks,
chatProps: props,
+2
View File
@@ -101,6 +101,7 @@ export type ChatProps = ChatTaskSuggestionTrayProps &
compactionStatus?: CompactionStatus | null;
fallbackStatus?: FallbackStatus | null;
progressCard?: ProgressCard | null;
onDismissProgressCard?: (card: ProgressCard) => void;
gatewayQuestionPrompts?: readonly QuestionPrompt[];
onGatewayQuestionChange?: () => void;
onGatewayQuestionSubmit?: (
@@ -392,6 +393,7 @@ export function renderChat(props: ChatProps) {
compactionStatus: props.compactionStatus,
fallbackStatus: props.fallbackStatus,
progressCard: props.progressCard,
onDismissProgressCard: props.onDismissProgressCard,
gatewayQuestionPrompts: props.gatewayQuestionPrompts,
messages: props.messages,
stream: props.stream,
@@ -86,6 +86,7 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
compactionStatus?: CompactionStatus | null;
fallbackStatus?: FallbackStatus | null;
progressCard?: ProgressCard | null;
onDismissProgressCard?: (card: ProgressCard) => void;
gatewayQuestionPrompts?: readonly QuestionPrompt[];
messages: unknown[];
stream: string | null;
@@ -266,7 +266,11 @@ export function renderChatComposerView(context: ChatComposerViewContext) {
</div>
`
: nothing}
${renderSessionProgressCard(props.progressCard, "composer")}
${renderSessionProgressCard(
props.progressCard,
"composer",
props.onDismissProgressCard,
)}
${renderFallbackIndicator(props.fallbackStatus)}
${renderCompactionIndicator(props.compactionStatus)}
${renderChatGoal(state, activeSession?.goal, {
@@ -220,6 +220,7 @@ export class ChatSessionRailElement extends OpenClawLightDomElement {
@property({ attribute: false }) startedAt?: number;
@property({ attribute: false }) lastReadAt?: number;
@property({ attribute: false }) progressCard: ProgressCard | null = null;
@property({ attribute: false }) onDismissProgressCard?: (card: ProgressCard) => void;
@property({ attribute: false }) pullRequests: ControlUiSessionPullRequest[] = [];
@property({ attribute: false }) companion: ChatSessionCompanionThread = {
exchanges: [],
@@ -626,7 +627,8 @@ export class ChatSessionRailElement extends OpenClawLightDomElement {
${digest
? html`<div class="chat-session-rail__digest">${this.renderDigestDetails(digest)}</div>`
: nothing}
${renderSessionProgressCard(this.progressCard, "rail")} ${this.renderThread()}
${renderSessionProgressCard(this.progressCard, "rail", this.onDismissProgressCard)}
${this.renderThread()}
${this.companion.exchanges.length === 0 && !this.companion.pendingQuestion
? this.renderStarters()
: nothing}
+14 -1
View File
@@ -179,9 +179,15 @@
text-transform: uppercase;
}
.session-progress-card__heading-actions {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.session-progress-card__summary {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: 7px;
min-width: 0;
@@ -236,6 +242,13 @@
font-variant-numeric: tabular-nums;
}
.session-progress-card__dismiss {
width: 22px;
min-width: 22px;
height: 22px;
min-height: 22px;
}
.session-progress-card--composer .session-progress-card__body {
padding: 7px 9px 8px;
border-top: 1px solid color-mix(in srgb, var(--accent) 16%, var(--border));