Rewind chat or files from any user message (#1154)

* Add Pi extension-launch plumbing for in-process tree primitives

* Add Rewind controls for agent sessions

* Preserve SDK checkpoint env var so Claude file rewind works

The SDK injects CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true when file checkpointing is enabled. The earlier parent-env stripping from c4782ec71c was too broad and removed that child flag along with real parent-session markers.

Keep stripping parent markers, but preserve the checkpoint flag so the SDK rewind path can run directly. With that boundary fixed, the JSONL file-rewind fallback is no longer needed.

* Update OpenCode history replay tests for rewind state

* Show Rewind actions in an adaptive sheet

* Fix rewind semantics across providers

* Refactor Pi IDs through extension capture

* Restore user message to composer after rewind

* Fix Claude rewind tool result mapping

* Fix Claude rewind session switch roundtrip

* Fix rewind and optimistic message regressions

* Tighten rewind test discipline

* Fix OpenCode optimistic message reconciliation

* Reconcile optimistic user messages by marker

* Reorder user message actions

* Unify provider user message contract

* Add Claude rewind flow Playwright contract

* Add Codex rewind flow Playwright contract

* Add OpenCode rewind flow Playwright contract

* Add Pi rewind flow Playwright contract

* Remove real-provider rewind Playwright contracts

* Gate real-provider rewind specs from CI Playwright
This commit is contained in:
Mohamed Boudra
2026-05-25 17:11:34 +08:00
committed by GitHub
parent e966f70322
commit 132e572d6e
99 changed files with 7106 additions and 3425 deletions
@@ -99,6 +99,16 @@ function expectSyncErrorSync(state: ReadyState): void {
expect(state.sync.status).toBe("sync_error");
}
function stateMachineLabel(state: AgentScreenViewState): "loading" | "resolving" | Agent["status"] {
if (state.tag === "boot") {
return state.reason;
}
if (state.tag === "ready") {
return state.agent.status;
}
return "error";
}
describe("deriveAgentScreenViewState", () => {
it("returns boot loading before first interactive paint", () => {
const memory = createBaseMemory();
@@ -383,6 +393,39 @@ describe("deriveAgentScreenViewState", () => {
expect(ready.agent.status).toBe("running");
});
it("keeps send lifecycle transitions forward-only across optimistic and authoritative handoff", () => {
let memory = createBaseMemory();
const transitions: Array<"loading" | "resolving" | Agent["status"]> = [];
for (const input of [
createBaseInput(),
{
...createBaseInput(),
agent: createAgentWithStatus({ id: "agent-1", status: "idle" }),
placeholderAgent: createAgent("agent-1"),
shouldUseOptimisticStream: true,
},
{
...createBaseInput(),
agent: createAgentWithStatus({ id: "agent-1", status: "running" }),
placeholderAgent: createAgent("agent-1"),
shouldUseOptimisticStream: true,
},
{
...createBaseInput(),
agent: createAgentWithStatus({ id: "agent-1", status: "idle" }),
},
] satisfies AgentScreenMachineInput[]) {
const result = deriveAgentScreenViewState({ input, memory });
memory = result.memory;
transitions.push(stateMachineLabel(result.state));
}
expect(transitions).toEqual(["loading", "running", "running", "idle"]);
expect(transitions.join(" -> ")).not.toContain("running -> loading");
expect(transitions.join(" -> ")).not.toContain("loading -> idle");
});
it("keeps optimistic running status while authoritative agent is initializing", () => {
const memory = createBaseMemory();
const input: AgentScreenMachineInput = {