-
Notifications
You must be signed in to change notification settings - Fork 14
feat: collapse compaction surface to a single four-state divider #836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f2d187f
feat(session): keep compaction summary message for UI state machine
Astro-Han 2333993
feat(app): collapse compaction surface to a single four-state divider
Astro-Han c40ff54
test(app): add compaction-divider snap covering four states
Astro-Han 0f5fc4e
test(app): hide toast region before compaction-divider snap
Astro-Han 0e9294d
fix(ui): read compaction error reason from NamedError data.message
Astro-Han aa68301
fix(session): stamp time.completed on compaction terminal placeholders
Astro-Han 146e022
fix(ui): drop trailing colon when compaction error has no reason
Astro-Han d92465f
fix(server): surface compaction failure on summarize route
Astro-Han 17c6428
fix(ui): treat legacy compaction orphans as failed instead of pending
Astro-Han 16b97fb
fix(ui): gate compaction orphan fallback on session work state
Astro-Han 4107a85
fix(server): set session busy before manual compaction marker
Astro-Han f942bf8
fix(session): run compaction marker inside loop runner
Astro-Han 73aab58
fix(session): reject compact-while-busy instead of silent no-op
Astro-Han 0d6be60
fix(app): disable session.compact command while session busy
Astro-Han c2120f7
fix(session): write aborted carrier when cancel races compaction plac…
Astro-Han 41406e9
fix(session): move revert.cleanup into prelude atomic transaction
Astro-Han 3c2e4b1
fix(session): derive compaction agent after revert.cleanup
Astro-Han ba232c6
fix(session): sweep orphan compaction marker on cancel
Astro-Han 629471c
docs(session): pin compaction sweep semantic boundary
Astro-Han 4926dc7
test(session): lock agent derivation order against revert.cleanup
Astro-Han 6d3b237
chore: merge dev for #834 dark + #841 light surface updates
Astro-Han File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import type { Page } from "@playwright/test" | ||
| import { test } from "../fixtures" | ||
| import { composeGrid, snapOutputPath, type Shot } from "./_compose" | ||
|
|
||
| test.use({ viewport: { width: 1100, height: 400 }, deviceScaleFactor: 2 }) | ||
|
|
||
| const SEED_REPLY = "Acknowledged. Seeded turn ready for compaction." | ||
| const SUMMARY_TEXT = [ | ||
| "## Goal", | ||
| "- Validate the compaction divider rendering", | ||
| "", | ||
| "## Progress", | ||
| "### Done", | ||
| "- Seeded one user turn", | ||
| ].join("\n") | ||
|
|
||
| async function seedTurn( | ||
| sdk: ReturnType<typeof import("../utils").createSdk>, | ||
| directory: string, | ||
| sessionID: string, | ||
| prompt: string, | ||
| ) { | ||
| await sdk.session.prompt({ | ||
| sessionID, | ||
| directory, | ||
| parts: [{ type: "text", text: prompt }], | ||
| }) | ||
| } | ||
|
|
||
| async function captureDivider(page: Page, name: string): Promise<Shot> { | ||
| const divider = page.locator('[data-slot="session-turn-compaction"]').last() | ||
| await divider.waitFor({ state: "visible", timeout: 30_000 }) | ||
| // Hide the Solid toast region so the page's "Response ready" notifications | ||
| // do not leak into the divider screenshot. The toasts are position:fixed and | ||
| // would otherwise overlap the divider's bounding box. | ||
| await page.addStyleTag({ | ||
| content: '[data-sonner-toaster], [role="region"][aria-label*="Notifications"] { display: none !important; }', | ||
| }) | ||
| return { name, buf: await divider.screenshot() } | ||
| } | ||
|
|
||
| async function waitForState(page: Page, state: string, timeoutMs: number) { | ||
| await page.waitForFunction( | ||
| (expected) => { | ||
| const part = document.querySelector( | ||
| '[data-slot="session-turn-compaction"] [data-component="compaction-part"]', | ||
| ) | ||
| const current = part?.getAttribute("data-state") | ||
| return current === expected | ||
| }, | ||
| state, | ||
| { timeout: timeoutMs }, | ||
| ) | ||
| } | ||
|
|
||
| // Real production snap for the compaction divider across all four states. | ||
| // Each state runs in its own session so the divider's data-state attribute | ||
| // transitions are isolated from siblings. | ||
| test("compaction-divider", async ({ page, project, assistant }) => { | ||
| test.setTimeout(360_000) | ||
|
|
||
| await project.open() | ||
| const { directory } = project | ||
| const projectSdk = project.sdk | ||
|
|
||
| const shots: Shot[] = [] | ||
|
|
||
| // ── DONE ─────────────────────────────────────────────────────────────────── | ||
| await assistant.reply(SEED_REPLY) | ||
| const doneSession = await projectSdk.session.create({ directory, title: "snap compaction-done" }) | ||
| const doneSessionID = doneSession.data?.id | ||
| if (!doneSessionID) throw new Error("session.create returned no id (done)") | ||
| await seedTurn(projectSdk, directory, doneSessionID, "Seed for done") | ||
| await project.gotoSession(doneSessionID) | ||
| await assistant.reply(SUMMARY_TEXT) | ||
| await projectSdk.session.summarize({ | ||
| sessionID: doneSessionID, | ||
| providerID: "opencode", | ||
| modelID: "big-pickle", | ||
| }) | ||
| await waitForState(page, "done", 45_000) | ||
| shots.push(await captureDivider(page, "done")) | ||
|
|
||
| // ── FAILED ───────────────────────────────────────────────────────────────── | ||
| // HTTP 400 from the LLM endpoint. The OpenAI client wraps it as APIError | ||
| // with isRetryable=false; retry.ts L63 returns undefined immediately, so | ||
| // the schedule yields Cause.done(0) and Effect.catch(halt) writes the | ||
| // error onto the placeholder summary assistant. Divider reads `failed`. | ||
| await assistant.reply(SEED_REPLY) | ||
| const failedSession = await projectSdk.session.create({ directory, title: "snap compaction-failed" }) | ||
| const failedSessionID = failedSession.data?.id | ||
| if (!failedSessionID) throw new Error("session.create returned no id (failed)") | ||
| await seedTurn(projectSdk, directory, failedSessionID, "Seed for failed") | ||
| await project.gotoSession(failedSessionID) | ||
| await assistant.error(400, { error: { type: "BadRequest", message: "Compaction model rejected the request" } }) | ||
| // Summarize must now surface the failure: the route reads the placeholder's | ||
| // `error` field after the loop returns and rethrows as UnknownError, so | ||
| // SDK callers cannot silently see `true` for a visibly failed compaction. | ||
| let summarizeFailureSurfaced = false | ||
| try { | ||
| await projectSdk.session.summarize({ | ||
| sessionID: failedSessionID, | ||
| providerID: "opencode", | ||
| modelID: "big-pickle", | ||
| }) | ||
| } catch { | ||
| summarizeFailureSurfaced = true | ||
| } | ||
| if (!summarizeFailureSurfaced) throw new Error("summarize should reject when compaction fails pre-summary") | ||
| await waitForState(page, "failed", 45_000) | ||
| shots.push(await captureDivider(page, "failed")) | ||
|
|
||
| // ── PENDING + ABORTED ────────────────────────────────────────────────────── | ||
| // hang() returns Stream.never so the compaction streams forever; the | ||
| // placeholder summary assistant stays in pending. After capturing pending | ||
| // we call session.abort which trips Effect.onInterrupt in compaction.ts, | ||
| // writing MessageAbortedError onto the placeholder. | ||
| await assistant.reply(SEED_REPLY) | ||
| const pendingSession = await projectSdk.session.create({ directory, title: "snap compaction-pending" }) | ||
| const pendingSessionID = pendingSession.data?.id | ||
| if (!pendingSessionID) throw new Error("session.create returned no id (pending)") | ||
| await seedTurn(projectSdk, directory, pendingSessionID, "Seed for pending") | ||
| await project.gotoSession(pendingSessionID) | ||
| await assistant.hang() | ||
| // Fire-and-forget: summarize returns once the request is accepted, the | ||
| // actual compaction call hangs on the LLM stream. | ||
| void projectSdk.session.summarize({ | ||
| sessionID: pendingSessionID, | ||
| providerID: "opencode", | ||
| modelID: "big-pickle", | ||
| }) | ||
| await waitForState(page, "pending", 45_000) | ||
| shots.push(await captureDivider(page, "pending")) | ||
|
|
||
| await projectSdk.session.abort({ sessionID: pendingSessionID, directory }) | ||
| await waitForState(page, "aborted", 45_000) | ||
| shots.push(await captureDivider(page, "aborted")) | ||
|
|
||
| const out = snapOutputPath("compaction-divider") | ||
| await composeGrid(shots, out, { cols: 2 }) | ||
| process.stdout.write(`\n[snap] compaction-divider grid -> ${out}\n\n`) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.