mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
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:
committed by
GitHub
parent
89a7e4e965
commit
2a469c1cb2
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user