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:
@@ -50,7 +50,7 @@ type DraftComposerState = UseAgentFormStateResult & {
|
||||
commandDraftConfig: DraftCommandConfig | undefined;
|
||||
};
|
||||
|
||||
interface AgentInputDraft {
|
||||
export interface AgentInputDraft {
|
||||
text: string;
|
||||
setText: (text: string) => void;
|
||||
attachments: UserComposerAttachment[];
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useRef } from "react";
|
||||
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
|
||||
|
||||
export interface AgentScreenAgent {
|
||||
serverId: string;
|
||||
id: string;
|
||||
status: "initializing" | "idle" | "running" | "error" | "closed";
|
||||
cwd: string;
|
||||
capabilities?: AgentCapabilityFlags;
|
||||
lastError?: string | null;
|
||||
projectPlacement?: {
|
||||
checkout?: {
|
||||
|
||||
@@ -133,6 +133,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
|
||||
id: machine.attempt.clientMessageId,
|
||||
text: machine.attempt.text,
|
||||
timestamp: machine.attempt.timestamp,
|
||||
optimistic: true,
|
||||
...(machine.attempt.images && machine.attempt.images.length > 0
|
||||
? { images: machine.attempt.images }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user