Compare commits

...
3 changed files with 101 additions and 19 deletions
@@ -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()}
+5
View File
@@ -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",