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
@@ -276,8 +276,17 @@ describe("processTimelineResponse", () => {
});
it("sets cursor to null when reset=true but no cursors in payload", () => {
const existingTail: StreamItem[] = [
{
kind: "user_message",
id: "only-user",
text: "first turn",
timestamp: new Date(500),
},
];
const result = processTimelineResponse({
...baseTimelineInput,
currentTail: existingTail,
currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 5 },
payload: {
...baseTimelineInput.payload,
@@ -288,6 +297,8 @@ describe("processTimelineResponse", () => {
expect(result.cursor).toBe(null);
expect(result.cursorChanged).toBe(true);
expect(result.tail).toEqual([]);
expect(getUserTexts(result.tail)).toHaveLength(0);
});
it("performs bootstrap tail init with catch-up side effect", () => {
@@ -933,6 +944,34 @@ describe("processAgentStreamEvent", () => {
expect(result.sideEffects).toEqual([]);
});
it("resets visible timeline when a new epoch starts at seq 1", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 1,
endSeq: 5,
};
const currentTail = [makeAssistantItem("old timeline")];
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("rewound start", "user_message"),
seq: 1,
epoch: "epoch-2",
currentCursor: existingCursor,
currentTail,
});
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
epoch: "epoch-2",
startSeq: 1,
endSeq: 1,
});
expect(getAssistantTexts(result.tail)).toEqual([]);
expect(getUserTexts(result.tail)).toEqual(["rewound start"]);
expect(result.sideEffects).toEqual([]);
});
it("initializes cursor when none exists", () => {
const result = processAgentStreamEvent({
...baseStreamInput,