From 571c3c4f008f0c135b66b5790dc51f04f62bc0cc Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:09:10 +0200 Subject: [PATCH] feat(desktop): show compaction progress and outcomes (#48152) --- .../session-timeline-notices.spec.ts | 72 +++++++++++++++++-- .../src/message/message-content.tsx | 43 +++++++---- packages/ui/src/i18n/en.ts | 5 ++ 3 files changed, 101 insertions(+), 19 deletions(-) diff --git a/packages/app/e2e/regression/session-timeline-notices.spec.ts b/packages/app/e2e/regression/session-timeline-notices.spec.ts index f66cd2c3088..0c89d3fe633 100644 --- a/packages/app/e2e/regression/session-timeline-notices.spec.ts +++ b/packages/app/e2e/regression/session-timeline-notices.spec.ts @@ -7,7 +7,6 @@ import { compactionFailed, compactionStarted, directory, - event, session, sessionID, setupTimeline, @@ -87,12 +86,13 @@ test("renders current protocol notices in CLI order", async ({ page }) => { expect(ownerWarnings).toEqual([]) }) -test("renders a compaction summary while it streams and after completion", async ({ page }) => { +test("renders compaction progress, summary, and outcome in order", async ({ page }) => { const timeline = await setupTimeline(page, { settings: { timelineDetail: { ...timelinePresets[2].value, notices: { placement: "separate" } }, }, sessionMessages: [user, assistant(true)], + sessionStatus: { [sessionID]: { type: "busy" } }, }) await timeline.send( @@ -104,7 +104,15 @@ test("renders a compaction summary while it streams and after completion", async ) const compaction = page.locator('[data-component="session-compaction-message"]') - await expect(compaction.getByText("Session compacted", { exact: true })).toBeVisible() + await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible() + await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible() + await expect(compaction.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") + await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0) + await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible() + await expect(page.locator('[data-component="session-working"]')).toHaveCount(0) + + await page.setViewportSize({ width: 480, height: 900 }) + await expect(compaction.getByText("Session compaction started", { exact: true })).toBeInViewport() await timeline.send( compactionDelta({ @@ -114,6 +122,8 @@ test("renders a compaction summary while it streams and after completion", async ) await expect(compaction.getByRole("heading", { name: "Checkpoint" })).toBeVisible() await expect(compaction).toContainText("Streamed implementation details.") + await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible() + await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0) await timeline.send( compactionEnded({ @@ -125,6 +135,18 @@ test("renders a compaction summary while it streams and after completion", async ) await expect(compaction).toContainText("Final implementation details.") await expect(compaction).not.toContainText("Streamed implementation details.") + await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible() + await expect(compaction.getByText("Session compacted", { exact: true })).toBeVisible() + await expect + .poll(async () => { + const summary = await compaction.locator('[data-component="text-part"]').boundingBox() + const completed = await compaction.getByText("Session compacted", { exact: true }).boundingBox() + return !!summary && !!completed && completed.y >= summary.y + summary.height + }) + .toBe(true) + await expect(compaction.getByRole("status")).toHaveCount(0) + await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible() + await expect(page.locator('[data-component="session-working"]')).toBeVisible() }) test("updates running compactions to failed and cancelled boundaries", async ({ page }) => { @@ -146,7 +168,10 @@ test("updates running compactions to failed and cancelled boundaries", async ({ const compactions = page.locator('[data-component="session-compaction-message"]') const failed = compactions.filter({ hasText: "The provider rejected the summary." }) - await expect(failed.getByText("Session compacted", { exact: true })).toBeVisible() + await expect(failed.getByText("Session compaction started", { exact: true })).toBeVisible() + await expect(failed.getByText("Session compaction failed", { exact: true })).toBeVisible() + await expect(failed.getByText("Session compacted", { exact: true })).toHaveCount(0) + await expect(failed.getByRole("status")).toHaveCount(0) await expect(failed.getByText("ProviderError: The provider rejected the summary.", { exact: true })).toBeVisible() await expect(failed).not.toContainText("Partial summary that should be discarded.") @@ -164,11 +189,48 @@ test("updates running compactions to failed and cancelled boundaries", async ({ await expect(compactions).toHaveCount(2) const cancelled = compactions.filter({ hasNotText: "The provider rejected the summary." }) - await expect(cancelled.getByText("Session compacted", { exact: true })).toBeVisible() + await expect(cancelled.getByText("Session compaction started", { exact: true })).toBeVisible() + await expect(cancelled.getByText("Session compaction cancelled", { exact: true })).toBeVisible() + await expect(cancelled.getByText("Session compacted", { exact: true })).toHaveCount(0) + await expect(cancelled.getByRole("status")).toHaveCount(0) await expect(cancelled).not.toContainText("Cancellation detail should stay hidden.") await expect(cancelled).not.toContainText("Summary before cancellation.") }) +test("shows an interrupted outcome when stopping automatic compaction", async ({ page }) => { + const timeline = await setupTimeline(page, { + sessionMessages: [user, assistant(true)], + sessionStatus: { [sessionID]: { type: "busy" } }, + }) + await timeline.send(compactionStarted({ sessionID, reason: "auto", recent: "" })) + await timeline.send(compactionDelta({ sessionID, text: "Partial automatic summary." })) + const compaction = page.locator('[data-component="session-compaction-message"]') + await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible() + await expect(compaction).toContainText("Partial automatic summary.") + + const request = page.waitForRequest( + (request) => + request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${sessionID}/interrupt`, + ) + await page.getByRole("button", { name: "Stop", exact: true }).click() + await request + await timeline.send( + compactionFailed({ + sessionID, + reason: "auto", + error: { type: "compaction.interrupted", message: "Compaction was interrupted" }, + }), + ) + + await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible() + await expect(compaction.getByText("Session compaction interrupted", { exact: true })).toBeVisible() + await expect(compaction.getByText("Session compaction failed", { exact: true })).toHaveCount(0) + await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0) + await expect(compaction.getByRole("status")).toHaveCount(0) + await expect(compaction).not.toContainText("Partial automatic summary.") + await expect(compaction).not.toContainText("Compaction was interrupted") +}) + test("moves blocking work to the background with Ctrl+B", async ({ page }) => { await setupTimeline(page, { settings: { diff --git a/packages/session-ui/src/message/message-content.tsx b/packages/session-ui/src/message/message-content.tsx index 297248e07e3..c4853a31b7d 100644 --- a/packages/session-ui/src/message/message-content.tsx +++ b/packages/session-ui/src/message/message-content.tsx @@ -392,7 +392,8 @@ export function SessionCompactionMessage(props: { message: SessionMessageCompact const i18n = useI18n() const summary = () => (props.message.status === "failed" ? "" : props.message.summary) const error = () => { - if (props.message.status !== "failed" || props.message.error.type === "aborted") return "" + if (props.message.status !== "failed") return "" + if (props.message.error.type === "aborted" || props.message.error.type === "compaction.interrupted") return "" return props.error } const compact = createMemo( @@ -410,24 +411,33 @@ export function SessionCompactionMessage(props: { message: SessionMessageCompact output: compact().format(output), }) } - const label = createMemo(() => - [ - i18n.t( - props.message.status === "completed" && props.message.providerContext - ? "ui.messagePart.providerCompaction" - : "ui.messagePart.compaction", - ), - usage(), - ] - .filter(Boolean) - .join(" · "), - ) + const outcome = () => { + if (props.message.status !== "failed") + return props.message.status === "completed" && props.message.providerContext + ? "ui.messagePart.providerCompaction" + : "ui.messagePart.compaction" + if (props.message.error.type === "aborted") return "ui.messagePart.compaction.cancelled" + if (props.message.error.type === "compaction.interrupted") return "ui.messagePart.compaction.interrupted" + return "ui.messagePart.compaction.failed" + } + const label = createMemo(() => [i18n.t(outcome()), usage()].filter(Boolean).join(" · ")) return (