mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 12:10:45 +03:00
feat(desktop): show compaction progress and outcomes (#48152)
This commit is contained in:
committed by
GitHub
Unverified
parent
50ed7c41ef
commit
571c3c4f00
@@ -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: {
|
||||
|
||||
@@ -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 (
|
||||
<div data-component="session-compaction-message">
|
||||
<div class="py-2">
|
||||
<TimelineSeparator label={label()} />
|
||||
<TimelineSeparator label={i18n.t("ui.messagePart.compaction.started")} />
|
||||
</div>
|
||||
<Show when={props.message.status === "running"}>
|
||||
<div role="status" class="py-2">
|
||||
<BasicTool
|
||||
icon="archive"
|
||||
trigger={{ title: i18n.t("ui.messagePart.compaction.running") }}
|
||||
status="running"
|
||||
locked
|
||||
hideDetails
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={summary().trim()}>
|
||||
<div data-component="text-part" data-timeline-part-id={props.message.id}>
|
||||
<div data-slot="text-part-body">
|
||||
@@ -439,6 +449,11 @@ export function SessionCompactionMessage(props: { message: SessionMessageCompact
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.message.status !== "running"}>
|
||||
<div class="py-2">
|
||||
<TimelineSeparator label={label()} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={error()}>
|
||||
<Card variant="error" class="error-card">
|
||||
{error()}
|
||||
|
||||
@@ -104,6 +104,11 @@ const source = {
|
||||
"ui.messagePart.review.title": "Review your answers",
|
||||
"ui.messagePart.questions.dismissed": "Questions dismissed",
|
||||
"ui.messagePart.compaction": "Session compacted",
|
||||
"ui.messagePart.compaction.started": "Session compaction started",
|
||||
"ui.messagePart.compaction.running": "Compacting",
|
||||
"ui.messagePart.compaction.failed": "Session compaction failed",
|
||||
"ui.messagePart.compaction.cancelled": "Session compaction cancelled",
|
||||
"ui.messagePart.compaction.interrupted": "Session compaction interrupted",
|
||||
"ui.messagePart.providerCompaction": "Session compacted by provider",
|
||||
"ui.messagePart.compaction.usage": "{{input}} in · {{output}} out",
|
||||
"ui.messagePart.context.details": "Details",
|
||||
|
||||
Reference in New Issue
Block a user