diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..04911f6bb3 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,280 @@ +(eval):5: parse error near `end' +# Codex Review Experience + +Language for features that help people understand, navigate, and evaluate code changes in Codex. + +## Language + +**Review Story**: +A structured explanation of a change that organizes the diff into a small number of ordered, cohesive steps for reviewer understanding. A **Review Story** is a navigation and explanation artifact, not a finding engine or correctness verdict. +_Avoid_: Review module, PR story, change story + +**Story Source**: +The change set that a **Review Story** explains. A **Story Source** may be a branch comparison, uncommitted changes, or a single commit; it is not limited to a hosted pull request. +_Avoid_: Pull request, GitHub PR + +**Concrete Story Source**: +A **Story Source** that resolves to a deterministic diff and **Source Fingerprint**, such as a branch comparison, uncommitted changes, or a single commit. V1 **Review Stories** require a **Concrete Story Source**. +_Avoid_: Custom instructions + +**Source Fingerprint**: +A deterministic identity for a **Story Source** used to decide whether a **Story Snapshot** still matches the underlying change. The fingerprint is derived from resolved refs, SHAs, and diff content rather than model output. +_Avoid_: Cache key, model context hash + +**Stale Story**: +A **Story Snapshot** whose saved **Source Fingerprint** no longer matches the current **Story Source**. A **Stale Story** remains readable, but reviewers should refresh it before relying on its anchors or ordering. +_Avoid_: Invalid story, expired cache + +**Story Step**: +One ordered unit inside a **Review Story** that explains a cohesive part of the change. A **Story Step** has a goal, a summary, and references to the specific changed files or ranges that support that explanation. +_Avoid_: Cohort, layer, phase + +**Step Goal**: +The reason a **Story Step** exists in the **Review Story**. A **Step Goal** explains the role of the step in the change, while the step summary explains what changed. +_Avoid_: Step title, implementation detail + +**Review Focus**: +Non-verdict guidance on what a reviewer should pay attention to while reading a **Story Step**. **Review Focus** can highlight assumptions, dependencies, or areas worth checking, but it is not a finding. +_Avoid_: Finding, issue, warning + +**Change Anchor**: +An exact reference from a **Story Step** to evidence in the **Story Source**, such as a changed file, hunk, or line range. A **Change Anchor** must point to an actual changed region, so the reviewer can verify the story against the diff. +_Avoid_: File mention, related file + +**Anchor Id**: +A stable identifier assigned by Codex to a **Change Anchor** before model story generation begins. Models choose from **Anchor Ids** instead of inventing file paths or line ranges. +_Avoid_: Model anchor, generated location + +**Evidence Graph**: +The system-derived structure of the **Story Source**, including changed files, hunks, commits, renames, and cheap dependency signals. The **Evidence Graph** constrains the model-authored **Review Story** but is not itself the story shown to reviewers. +_Avoid_: Dependency map + +**Evidence Signal**: +A cheap, local fact included in the **Evidence Graph**, such as commit order, changed file status, hunk ranges, rename metadata, path relationships, or obvious test/source pairing. **Evidence Signals** are intentionally lighter than full semantic indexing. +_Avoid_: Semantic index, full dependency graph + +**Story Review**: +A review-adjacent workflow that generates a **Review Story** for understanding and navigation before findings are produced. A **Story Review** is distinct from the findings-oriented review workflow. +_Avoid_: Review mode, code review + +**Story Snapshot**: +A persisted version of a **Review Story** tied to the identity of its **Story Source**. A **Story Snapshot** gives reviewers stable step numbers and lets Codex detect when the story is stale because the underlying change has moved. +_Avoid_: Cached story, generated output + +**Snapshot Lineage**: +The relationship between a refreshed **Story Snapshot** and the older snapshot it replaces for the same **Story Source**. **Snapshot Lineage** preserves what reviewers saw before refresh while letting surfaces default to the newest story. +_Avoid_: In-place refresh + +**Snapshot Status**: +The lifecycle state of a **Story Snapshot**, such as outline generation, outline ready, enrichment, completion, failure, or staleness. **Snapshot Status** lets **Story Surfaces** show progress without guessing from individual fields. +_Avoid_: Generation state + +**Snapshot Update**: +A full replacement notification for the latest version of a **Story Snapshot**. V1 **Story Surfaces** apply **Snapshot Updates** by replacing their local snapshot instead of merging granular patches. +_Avoid_: Step patch, delta event + +**Story Store**: +The structured persistence location for **Story Snapshots**, keyed by thread and **Story Source**. The **Story Store** holds the evolving snapshot data while thread history records lifecycle events that point to snapshot ids. +_Avoid_: Thread history, transcript + +**Story Database**: +The SQLite database managed by the shared state layer that backs the **Story Store**. The **Story Database** is separate from thread metadata storage so story-specific migrations, cleanup, and larger structured payloads remain isolated. +_Avoid_: Thread database, metadata database + +**Story Record**: +The persisted database representation of a **Story Snapshot**. A **Story Record** stores the canonical snapshot as structured JSON plus indexed fields for lookup, status, timestamps, source identity, and step readiness. +_Avoid_: Normalized story schema, raw blob + +**Progressive Story Snapshot**: +A **Story Snapshot** that becomes useful before every **Story Step** is fully enriched. A **Progressive Story Snapshot** first exposes a validated outline, then updates individual steps as their descriptions and rationale become ready. +_Avoid_: Streaming story, partial JSON + +**Step Enrichment**: +The background work that fills in detailed descriptions, rationale, and summaries for **Story Steps** after the validated outline exists. **Step Enrichment** may run in small batches so reviewers can begin navigating before every step is complete. +_Avoid_: Step generation, eager loading + +**Story Schema**: +The strict structured output contract used by model calls that create a **Review Story**. A **Story Schema** lets Codex validate step order, **Anchor Ids**, and enriched fields before updating a **Story Snapshot**. +_Avoid_: Markdown format, prose contract + +**Enrichment Context**: +The bounded source context given to **Step Enrichment**, including the selected **Change Anchors**, nearby file context, and cheap dependency neighbors from the **Evidence Graph**. **Enrichment Context** supports explanation but does not replace the anchored evidence. +_Avoid_: Full repo context, diff-only context + +**Step Readiness**: +The per-step state that tells a **Story Surface** whether a **Story Step** has only outline data or also has completed **Step Enrichment**. **Step Readiness** lets surfaces render useful pending states without assuming steps finish in story order. +_Avoid_: Loading flag + +**Partial Story**: +A **Story Snapshot** whose outline is usable even though one or more **Story Steps** have not completed **Step Enrichment**. A **Partial Story** may include failed steps, but it still preserves navigation through validated anchors. +_Avoid_: Failed story + +**Story Surface**: +A product surface that presents a **Story Snapshot** to a reviewer. The TUI and App UI are separate **Story Surfaces** that should read the same underlying **Review Story** data. +_Avoid_: UI implementation, frontend + +**Story Overlay**: +The TUI **Story Surface** for navigating a **Story Snapshot**. The **Story Overlay** presents ordered steps, anchored diffs, and step explanation outside the normal transcript flow. +_Avoid_: Markdown story, transcript summary + +**Read-Only Story Overlay**: +The v1 **Story Overlay** mode that lets reviewers navigate, inspect, copy, and refresh **Story Snapshots** without leaving comments or submitting hosted reviews. +_Avoid_: Review submission, comment mode + +**/story**: +The TUI slash command that starts or opens a **Story Review**. The existing review picker should also expose **Review Story** creation for discoverability. +_Avoid_: /review-story + +**Story API**: +The app-server v2 contract used by **Story Surfaces** to create, read, and refresh **Story Snapshots**. The **Story API** is the shared product boundary for the TUI and App UI. +_Avoid_: TUI API, local story service + +**reviewStory**: +The app-server v2 API namespace for **Story API** methods. **reviewStory** is separate from the findings-oriented `review` namespace. +_Avoid_: story, review + +**Story Turn**: +A thread-scoped model run that generates or refreshes a **Story Snapshot** from a **Story Source**. A **Story Turn** uses Codex's normal execution lifecycle while producing a persisted story artifact. +_Avoid_: Background job, standalone task + +## Example Dialogue + +Dev: "Can the reviewer find bugs from the Review Story?" + +Domain expert: "Findings may later attach to Review Story steps, but the Review Story itself exists to explain the change in a useful review order." + +Dev: "Is every Review Story about a GitHub pull request?" + +Domain expert: "No. A hosted pull request is one possible Story Source, but local branch comparisons and commits should use the same language." + +Dev: "Can arbitrary custom review instructions create a Review Story in v1?" + +Domain expert: "No. V1 requires a Concrete Story Source so every Story Step can be anchored to a deterministic diff." + +Dev: "How does Codex know a Story Snapshot is stale?" + +Domain expert: "Codex compares the current Story Source to the Source Fingerprint saved with the Story Snapshot." + +Dev: "Should Codex automatically rewrite a stale story?" + +Domain expert: "No. V1 should mark a Stale Story visibly and let the reviewer choose when to refresh it." + +Dev: "Should the story have cohorts and layers?" + +Domain expert: "Not initially. A Review Story is a single ordered list of Story Steps unless we later introduce a separate concept for independent sub-stories." + +Dev: "What is the difference between a Story Step goal and summary?" + +Domain expert: "The Step Goal explains why this step belongs in the story; the summary explains what changed in the anchored code." + +Dev: "Can a Story Step tell the reviewer what to inspect?" + +Domain expert: "Yes, as Review Focus. It should guide attention without claiming the code is wrong." + +Dev: "Can a Story Step just mention the files it talks about?" + +Domain expert: "No. Each Story Step should carry Change Anchors to the changed ranges that support the explanation." + +Dev: "Can the model write paths and line numbers for step anchors?" + +Domain expert: "No. Codex assigns Anchor Ids first, and the model may only reference those ids." + +Dev: "Does the model decide what changed?" + +Domain expert: "The model explains and orders the change, but the Evidence Graph defines the changed evidence it is allowed to reference." + +Dev: "Does v1 need a full semantic dependency graph?" + +Domain expert: "No. V1 should use cheap local Evidence Signals and leave deeper indexing for later." + +Dev: "Should the Review Story be part of the bug-finding review?" + +Domain expert: "No. A Story Review helps the reviewer understand the change; a code review looks for issues." + +Dev: "Can the story be regenerated whenever the reviewer opens it?" + +Domain expert: "No. A Review Story should be saved as a Story Snapshot so reviewers can rely on stable steps and know when the source changed." + +Dev: "Does refreshing a stale story overwrite the old snapshot?" + +Domain expert: "No. Refresh creates a new Story Snapshot linked to the previous one through Snapshot Lineage." + +Dev: "How does a Story Surface know whether the story is ready?" + +Domain expert: "It reads the Snapshot Status and Step Readiness rather than inferring readiness from missing prose." + +Dev: "Should progressive story updates be sent as small patches?" + +Domain expert: "Not in v1. Story Surfaces should receive Snapshot Updates and replace their local snapshot with the newest version." + +Dev: "Should the whole Story Snapshot live inside chat history?" + +Domain expert: "No. Thread history should record story lifecycle events, while the Story Store holds structured snapshot data." + +Dev: "Should Story Snapshots be stored in the thread metadata database?" + +Domain expert: "No. Story Snapshots belong in a separate Story Database managed by the state layer." + +Dev: "Should the Story Database fully normalize every step and anchor?" + +Domain expert: "Not in v1. A Story Record should keep a canonical snapshot JSON while indexing the fields needed for lookup and progress." + +Dev: "Does the reviewer have to wait until every step is fully written?" + +Domain expert: "No. A Progressive Story Snapshot can expose a validated outline first, then fill in step details while the reviewer navigates." + +Dev: "Should Codex enrich every Story Step in a separate model call?" + +Domain expert: "Not by default. Step Enrichment should use small batches with bounded parallelism so navigation feels fast without creating excessive model work." + +Dev: "Can story generation return Markdown?" + +Domain expert: "No. Story generation should return data that matches the Story Schema so Codex can validate and render it across surfaces." + +Dev: "Can Step Enrichment inspect context outside the changed ranges?" + +Domain expert: "Yes, but only as Enrichment Context. The Story Step still has to explain the anchored changed evidence." + +Dev: "Should navigating to a step reprioritize enrichment in v1?" + +Domain expert: "Not initially. V1 should enrich the first step, nearby steps, and then the remaining steps in order, while Step Readiness keeps the UI ready for out-of-order completion later." + +Dev: "If one Step Enrichment batch fails, is the whole story failed?" + +Domain expert: "No. Once the outline exists, Codex should keep a Partial Story usable and mark only the affected steps as failed." + +Dev: "Should Review Stories include diagrams in v1?" + +Domain expert: "No. V1 should focus on trustworthy step ordering, anchors, progressive readiness, and diff navigation." + +Dev: "Can a Review Story include code-review findings?" + +Domain expert: "No. Findings belong to the findings-oriented review workflow, though a future surface may attach findings to Story Steps." + +Dev: "Will the TUI story and App UI story be different concepts?" + +Domain expert: "No. They are different Story Surfaces over the same Review Story data." + +Dev: "Should the TUI print the Review Story as a normal assistant message?" + +Domain expert: "No. The TUI should use a Story Overlay so reviewers can move between steps and anchored diffs." + +Dev: "Can reviewers submit comments from the Story Overlay in v1?" + +Domain expert: "No. V1 is a Read-Only Story Overlay; comments can attach to Change Anchors in a later version." + +Dev: "How does a TUI user start a Story Review?" + +Domain expert: "Use /story directly, or choose the Review Story option from the review picker." + +Dev: "Can the TUI define its own story shape first?" + +Domain expert: "No. The Story API defines the shared shape, even if the TUI is the first Story Surface to render it." + +Dev: "Should story APIs live under review/start?" + +Domain expert: "No. Story APIs use the reviewStory namespace because they support review understanding, not findings review." + +Dev: "Is story generation a separate background job?" + +Domain expert: "No. Story generation should be a Story Turn so progress, cancellation, and replay follow the rest of Codex." diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7595527c21..ab9f624d57 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2671,6 +2671,61 @@ ], "type": "object" }, + "ReviewStoryListParams": { + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ReviewStoryReadParams": { + "properties": { + "storySnapshotId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "storySnapshotId", + "threadId" + ], + "type": "object" + }, + "ReviewStoryStartParams": { + "properties": { + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "target", + "threadId" + ], + "type": "object" + }, "ReviewTarget": { "oneOf": [ { @@ -5425,6 +5480,78 @@ "title": "Review/startRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/start" + ], + "title": "ReviewStory/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/read" + ], + "title": "ReviewStory/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/list" + ], + "title": "ReviewStory/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/listRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 90899cb152..4a57728dcd 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2946,6 +2946,294 @@ }, "type": "object" }, + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStorySnapshotUpdatedNotification": { + "properties": { + "snapshot": { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "snapshot", + "threadId" + ], + "type": "object" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + }, "SandboxPolicy": { "oneOf": [ { @@ -5557,6 +5845,26 @@ "title": "Turn/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "reviewStory/snapshot/updated" + ], + "title": "ReviewStory/snapshot/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStorySnapshotUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ReviewStory/snapshot/updatedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index a738405036..66bc64f10f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1429,6 +1429,78 @@ "title": "Review/startRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "reviewStory/start" + ], + "title": "ReviewStory/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ReviewStoryStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "reviewStory/read" + ], + "title": "ReviewStory/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ReviewStoryReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "reviewStory/list" + ], + "title": "ReviewStory/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ReviewStoryListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -4227,6 +4299,26 @@ "title": "Turn/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "reviewStory/snapshot/updated" + ], + "title": "ReviewStory/snapshot/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ReviewStorySnapshotUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ReviewStory/snapshot/updatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -14411,6 +14503,379 @@ "title": "ReviewStartResponse", "type": "object" }, + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/v2/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStoryListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ReviewStoryListParams", + "type": "object" + }, + "ReviewStoryListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/ReviewStorySnapshotSummary" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ReviewStoryListResponse", + "type": "object" + }, + "ReviewStoryReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "storySnapshotId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "storySnapshotId", + "threadId" + ], + "title": "ReviewStoryReadParams", + "type": "object" + }, + "ReviewStoryReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReviewStorySnapshot" + }, + { + "type": "null" + } + ] + } + }, + "title": "ReviewStoryReadResponse", + "type": "object" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/v2/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/v2/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/v2/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/v2/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStorySnapshotSummary": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/ReviewStorySnapshotStatus" + }, + "stepCount": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/v2/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "sourceFingerprint", + "status", + "stepCount", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "$ref": "#/definitions/v2/ReviewStorySnapshot" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "snapshot", + "threadId" + ], + "title": "ReviewStorySnapshotUpdatedNotification", + "type": "object" + }, + "ReviewStoryStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "target": { + "$ref": "#/definitions/v2/ReviewTarget" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "target", + "threadId" + ], + "title": "ReviewStoryStartParams", + "type": "object" + }, + "ReviewStoryStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "$ref": "#/definitions/v2/ReviewStorySnapshot" + }, + "storySnapshotId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/v2/Turn" + } + }, + "required": [ + "snapshot", + "storySnapshotId", + "turn" + ], + "title": "ReviewStoryStartResponse", + "type": "object" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/v2/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, "ReviewTarget": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ac565328f9..5e318dcabe 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2155,6 +2155,78 @@ "title": "Review/startRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/start" + ], + "title": "ReviewStory/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/read" + ], + "title": "ReviewStory/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "reviewStory/list" + ], + "title": "ReviewStory/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStoryListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ReviewStory/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -10940,6 +11012,379 @@ "title": "ReviewStartResponse", "type": "object" }, + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStoryListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ReviewStoryListParams", + "type": "object" + }, + "ReviewStoryListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ReviewStorySnapshotSummary" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ReviewStoryListResponse", + "type": "object" + }, + "ReviewStoryReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "storySnapshotId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "storySnapshotId", + "threadId" + ], + "title": "ReviewStoryReadParams", + "type": "object" + }, + "ReviewStoryReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "anyOf": [ + { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + { + "type": "null" + } + ] + } + }, + "title": "ReviewStoryReadResponse", + "type": "object" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStorySnapshotSummary": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "stepCount": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "sourceFingerprint", + "status", + "stepCount", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "snapshot", + "threadId" + ], + "title": "ReviewStorySnapshotUpdatedNotification", + "type": "object" + }, + "ReviewStoryStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "target", + "threadId" + ], + "title": "ReviewStoryStartParams", + "type": "object" + }, + "ReviewStoryStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "snapshot": { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + "storySnapshotId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "snapshot", + "storySnapshotId", + "turn" + ], + "title": "ReviewStoryStartResponse", + "type": "object" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, "ReviewTarget": { "oneOf": [ { @@ -11493,6 +11938,26 @@ "title": "Turn/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "reviewStory/snapshot/updated" + ], + "title": "ReviewStory/snapshot/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStorySnapshotUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ReviewStory/snapshot/updatedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListParams.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListParams.json new file mode 100644 index 0000000000..4101a6aa55 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListParams.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ReviewStoryListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListResponse.json new file mode 100644 index 0000000000..5626807079 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryListResponse.json @@ -0,0 +1,177 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStorySnapshotSummary": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "stepCount": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "sourceFingerprint", + "status", + "stepCount", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + } + }, + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ReviewStorySnapshotSummary" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ReviewStoryListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadParams.json new file mode 100644 index 0000000000..32ed641596 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "storySnapshotId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "storySnapshotId", + "threadId" + ], + "title": "ReviewStoryReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadResponse.json new file mode 100644 index 0000000000..9d62de2c7b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryReadResponse.json @@ -0,0 +1,292 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + } + }, + "properties": { + "snapshot": { + "anyOf": [ + { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + { + "type": "null" + } + ] + } + }, + "title": "ReviewStoryReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStorySnapshotUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStorySnapshotUpdatedNotification.json new file mode 100644 index 0000000000..1bc24665aa --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStorySnapshotUpdatedNotification.json @@ -0,0 +1,292 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + } + }, + "properties": { + "snapshot": { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "snapshot", + "threadId" + ], + "title": "ReviewStorySnapshotUpdatedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartParams.json new file mode 100644 index 0000000000..5ae8b4fd56 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartParams.json @@ -0,0 +1,110 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + } + }, + "properties": { + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "target", + "threadId" + ], + "title": "ReviewStoryStartParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartResponse.json new file mode 100644 index 0000000000..db65c6961c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStoryStartResponse.json @@ -0,0 +1,1971 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "end", + "start" + ], + "type": "object" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "httpConnectionFailed" + ], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamConnectionFailed" + ], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamDisconnected" + ], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseTooManyFailedAttempts" + ], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": [ + "turnKind" + ], + "type": "object" + } + }, + "required": [ + "activeTurnNotSteerable" + ], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CollabAgentState": { + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "read" + ], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "name", + "path", + "type" + ], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "listFiles" + ], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionSource": { + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": [ + "diff", + "kind", + "path" + ], + "type": "object" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "hookRunId", + "text" + ], + "type": "object" + }, + "ImageDetail": { + "enum": [ + "high", + "original" + ], + "type": "string" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "type": "object" + }, + "McpToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "threadIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": [ + "commentary" + ], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": [ + "final_answer" + ], + "type": "string" + } + ] + }, + "NonSteerableTurnKind": { + "enum": [ + "review", + "compact" + ], + "type": "string" + }, + "PatchApplyStatus": { + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "ReasoningEffort": { + "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "type": "string" + }, + "ReviewStoryAnchor": { + "properties": { + "anchorId": { + "type": "string" + }, + "changeKind": { + "$ref": "#/definitions/ReviewStoryAnchorKind" + }, + "diff": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "anchorId", + "changeKind", + "diff", + "filePath", + "summary" + ], + "type": "object" + }, + "ReviewStoryAnchorKind": { + "enum": [ + "added", + "modified", + "deleted", + "renamed", + "copied", + "unknown" + ], + "type": "string" + }, + "ReviewStorySnapshot": { + "properties": { + "anchors": { + "items": { + "$ref": "#/definitions/ReviewStoryAnchor" + }, + "type": "array" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "overview": { + "type": "string" + }, + "previousStorySnapshotId": { + "type": [ + "string", + "null" + ] + }, + "sourceFingerprint": { + "type": "string" + }, + "stale": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/ReviewStorySnapshotStatus" + }, + "steps": { + "items": { + "$ref": "#/definitions/ReviewStoryStep" + }, + "type": "array" + }, + "storySnapshotId": { + "type": "string" + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "anchors", + "createdAt", + "overview", + "sourceFingerprint", + "stale", + "status", + "steps", + "storySnapshotId", + "target", + "threadId", + "title", + "updatedAt" + ], + "type": "object" + }, + "ReviewStorySnapshotStatus": { + "enum": [ + "building", + "ready", + "partial", + "failed" + ], + "type": "string" + }, + "ReviewStoryStep": { + "properties": { + "anchorIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "dependencyRationale": { + "type": "string" + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "goal": { + "type": "string" + }, + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "readiness": { + "$ref": "#/definitions/ReviewStoryStepReadiness" + }, + "reviewFocus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "stepId": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "anchorIds", + "dependencyRationale", + "goal", + "index", + "readiness", + "reviewFocus", + "stepId", + "summary", + "title" + ], + "type": "object" + }, + "ReviewStoryStepReadiness": { + "enum": [ + "outline", + "enriching", + "ready", + "failed" + ], + "type": "string" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "byteRange" + ], + "type": "object" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": [ + "userMessage" + ], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": [ + "content", + "id", + "type" + ], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": [ + "hookPrompt" + ], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": [ + "fragments", + "id", + "type" + ], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": [ + "agentMessage" + ], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "text", + "type" + ], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": [ + "plan" + ], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "text", + "type" + ], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": [ + "string", + "null" + ] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": [ + "commandExecution" + ], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": [ + "fileChange" + ], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": [ + "changes", + "id", + "status", + "type" + ], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "type": [ + "string", + "null" + ] + }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcpToolCall" + ], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": [ + "string", + "null" + ] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": [ + "collabAgentToolCall" + ], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "type": { + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "query", + "type" + ], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "path", + "type" + ], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "type": { + "enum": [ + "imageGeneration" + ], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "result", + "status", + "type" + ], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": [ + "enteredReviewMode" + ], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "review", + "type" + ], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": [ + "exitedReviewMode" + ], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "review", + "type" + ], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": [ + "contextCompaction" + ], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": [ + "id", + "items", + "status" + ], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, + "TurnStatus": { + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ], + "type": "string" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": [ + "text" + ], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": [ + "image" + ], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "url" + ], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "skill" + ], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "mention" + ], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "openPage" + ], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "findInPage" + ], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + } + }, + "properties": { + "snapshot": { + "$ref": "#/definitions/ReviewStorySnapshot" + }, + "storySnapshotId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "snapshot", + "storySnapshotId", + "turn" + ], + "title": "ReviewStoryStartResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 86ba2cd171..f8eed8949c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -55,6 +55,9 @@ import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTarge import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams"; import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; +import type { ReviewStoryListParams } from "./v2/ReviewStoryListParams"; +import type { ReviewStoryReadParams } from "./v2/ReviewStoryReadParams"; +import type { ReviewStoryStartParams } from "./v2/ReviewStoryStartParams"; import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; @@ -85,4 +88,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "reviewStory/start", id: RequestId, params: ReviewStoryStartParams, } | { "method": "reviewStory/read", id: RequestId, params: ReviewStoryReadParams, } | { "method": "reviewStory/list", id: RequestId, params: ReviewStoryListParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 3ed710efc0..f934287252 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -38,6 +38,7 @@ import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummar import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; +import type { ReviewStorySnapshotUpdatedNotification } from "./v2/ReviewStorySnapshotUpdatedNotification"; import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; @@ -70,4 +71,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "reviewStory/snapshot/updated", "params": ReviewStorySnapshotUpdatedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchor.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchor.ts new file mode 100644 index 0000000000..47bcd42223 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchor.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStoryAnchorKind } from "./ReviewStoryAnchorKind"; + +export type ReviewStoryAnchor = { anchorId: string, filePath: string, changeKind: ReviewStoryAnchorKind, summary: string, diff: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchorKind.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchorKind.ts new file mode 100644 index 0000000000..a969c090ec --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryAnchorKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewStoryAnchorKind = "added" | "modified" | "deleted" | "renamed" | "copied" | "unknown"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListParams.ts new file mode 100644 index 0000000000..115e6c6bb0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewStoryListParams = { threadId: string, cursor?: string | null, limit?: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListResponse.ts new file mode 100644 index 0000000000..c1e0dc935f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryListResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStorySnapshotSummary } from "./ReviewStorySnapshotSummary"; + +export type ReviewStoryListResponse = { data: Array, nextCursor: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadParams.ts new file mode 100644 index 0000000000..75b545e144 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewStoryReadParams = { threadId: string, storySnapshotId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadResponse.ts new file mode 100644 index 0000000000..f704306610 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStorySnapshot } from "./ReviewStorySnapshot"; + +export type ReviewStoryReadResponse = { snapshot: ReviewStorySnapshot | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshot.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshot.ts new file mode 100644 index 0000000000..9f5b5d2a6e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshot.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStoryAnchor } from "./ReviewStoryAnchor"; +import type { ReviewStorySnapshotStatus } from "./ReviewStorySnapshotStatus"; +import type { ReviewStoryStep } from "./ReviewStoryStep"; +import type { ReviewTarget } from "./ReviewTarget"; + +export type ReviewStorySnapshot = { storySnapshotId: string, threadId: string, title: string, overview: string, target: ReviewTarget, sourceFingerprint: string, status: ReviewStorySnapshotStatus, createdAt: bigint, updatedAt: bigint, previousStorySnapshotId: string | null, stale: boolean, steps: Array, anchors: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotStatus.ts new file mode 100644 index 0000000000..8aa4ae7d38 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewStorySnapshotStatus = "building" | "ready" | "partial" | "failed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotSummary.ts new file mode 100644 index 0000000000..7770dd0ea8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotSummary.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStorySnapshotStatus } from "./ReviewStorySnapshotStatus"; +import type { ReviewTarget } from "./ReviewTarget"; + +export type ReviewStorySnapshotSummary = { storySnapshotId: string, threadId: string, title: string, target: ReviewTarget, sourceFingerprint: string, status: ReviewStorySnapshotStatus, createdAt: bigint, updatedAt: bigint, previousStorySnapshotId: string | null, stepCount: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotUpdatedNotification.ts new file mode 100644 index 0000000000..1da349b76f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStorySnapshotUpdatedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStorySnapshot } from "./ReviewStorySnapshot"; + +export type ReviewStorySnapshotUpdatedNotification = { threadId: string, snapshot: ReviewStorySnapshot, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartParams.ts new file mode 100644 index 0000000000..9b4aa47799 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewTarget } from "./ReviewTarget"; + +export type ReviewStoryStartParams = { threadId: string, target: ReviewTarget, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartResponse.ts new file mode 100644 index 0000000000..7056f23a42 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStartResponse.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStorySnapshot } from "./ReviewStorySnapshot"; +import type { Turn } from "./Turn"; + +export type ReviewStoryStartResponse = { turn: Turn, storySnapshotId: string, snapshot: ReviewStorySnapshot, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStep.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStep.ts new file mode 100644 index 0000000000..be7ab0658c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStep.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReviewStoryStepReadiness } from "./ReviewStoryStepReadiness"; + +export type ReviewStoryStep = { stepId: string, index: number, title: string, goal: string, summary: string, dependencyRationale: string, anchorIds: Array, reviewFocus: Array, readiness: ReviewStoryStepReadiness, error: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStepReadiness.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStepReadiness.ts new file mode 100644 index 0000000000..3fc54f6312 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ReviewStoryStepReadiness.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReviewStoryStepReadiness = "outline" | "enriching" | "ready" | "failed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 5b4f2ed283..8d9053a259 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -323,6 +323,20 @@ export type { ResidencyRequirement } from "./ResidencyRequirement"; export type { ReviewDelivery } from "./ReviewDelivery"; export type { ReviewStartParams } from "./ReviewStartParams"; export type { ReviewStartResponse } from "./ReviewStartResponse"; +export type { ReviewStoryAnchor } from "./ReviewStoryAnchor"; +export type { ReviewStoryAnchorKind } from "./ReviewStoryAnchorKind"; +export type { ReviewStoryListParams } from "./ReviewStoryListParams"; +export type { ReviewStoryListResponse } from "./ReviewStoryListResponse"; +export type { ReviewStoryReadParams } from "./ReviewStoryReadParams"; +export type { ReviewStoryReadResponse } from "./ReviewStoryReadResponse"; +export type { ReviewStorySnapshot } from "./ReviewStorySnapshot"; +export type { ReviewStorySnapshotStatus } from "./ReviewStorySnapshotStatus"; +export type { ReviewStorySnapshotSummary } from "./ReviewStorySnapshotSummary"; +export type { ReviewStorySnapshotUpdatedNotification } from "./ReviewStorySnapshotUpdatedNotification"; +export type { ReviewStoryStartParams } from "./ReviewStoryStartParams"; +export type { ReviewStoryStartResponse } from "./ReviewStoryStartResponse"; +export type { ReviewStoryStep } from "./ReviewStoryStep"; +export type { ReviewStoryStepReadiness } from "./ReviewStoryStepReadiness"; export type { ReviewTarget } from "./ReviewTarget"; export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a8bec6b402..d1f04a3dae 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -794,6 +794,21 @@ client_request_definitions! { serialization: thread_id(params.thread_id), response: v2::ReviewStartResponse, }, + ReviewStoryStart => "reviewStory/start" { + params: v2::ReviewStoryStartParams, + serialization: thread_id(params.thread_id), + response: v2::ReviewStoryStartResponse, + }, + ReviewStoryRead => "reviewStory/read" { + params: v2::ReviewStoryReadParams, + serialization: thread_id(params.thread_id), + response: v2::ReviewStoryReadResponse, + }, + ReviewStoryList => "reviewStory/list" { + params: v2::ReviewStoryListParams, + serialization: thread_id(params.thread_id), + response: v2::ReviewStoryListResponse, + }, ModelList => "model/list" { params: v2::ModelListParams, @@ -1484,6 +1499,7 @@ server_notification_definitions! { TurnStarted => "turn/started" (v2::TurnStartedNotification), HookStarted => "hook/started" (v2::HookStartedNotification), TurnCompleted => "turn/completed" (v2::TurnCompletedNotification), + ReviewStorySnapshotUpdated => "reviewStory/snapshot/updated" (v2::ReviewStorySnapshotUpdatedNotification), HookCompleted => "hook/completed" (v2::HookCompletedNotification), TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification), TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs index b5fa9fdc65..3fbfb9e446 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -21,6 +21,7 @@ mod process; mod realtime; mod remote_control; mod review; +mod review_story; mod thread; mod thread_data; mod turn; @@ -47,6 +48,7 @@ pub use process::*; pub use realtime::*; pub use remote_control::*; pub use review::*; +pub use review_story::*; pub use shared::*; pub use thread::*; pub use thread_data::*; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/review_story.rs b/codex-rs/app-server-protocol/src/protocol/v2/review_story.rs new file mode 100644 index 0000000000..f263ef4c42 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/review_story.rs @@ -0,0 +1,161 @@ +use super::ReviewTarget; +use super::Turn; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryStartParams { + pub thread_id: String, + pub target: ReviewTarget, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryStartResponse { + pub turn: Turn, + pub story_snapshot_id: String, + pub snapshot: ReviewStorySnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryReadParams { + pub thread_id: String, + pub story_snapshot_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryReadResponse { + pub snapshot: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryListParams { + pub thread_id: String, + + #[ts(optional = nullable)] + pub cursor: Option, + + #[ts(optional = nullable)] + pub limit: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryListResponse { + pub data: Vec, + pub next_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStorySnapshotUpdatedNotification { + pub thread_id: String, + pub snapshot: ReviewStorySnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStorySnapshotSummary { + pub story_snapshot_id: String, + pub thread_id: String, + pub title: String, + pub target: ReviewTarget, + pub source_fingerprint: String, + pub status: ReviewStorySnapshotStatus, + pub created_at: i64, + pub updated_at: i64, + pub previous_story_snapshot_id: Option, + pub step_count: u32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStorySnapshot { + pub story_snapshot_id: String, + pub thread_id: String, + pub title: String, + pub overview: String, + pub target: ReviewTarget, + pub source_fingerprint: String, + pub status: ReviewStorySnapshotStatus, + pub created_at: i64, + pub updated_at: i64, + pub previous_story_snapshot_id: Option, + pub stale: bool, + pub steps: Vec, + pub anchors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ReviewStorySnapshotStatus { + Building, + Ready, + Partial, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryStep { + pub step_id: String, + pub index: u32, + pub title: String, + pub goal: String, + pub summary: String, + pub dependency_rationale: String, + pub anchor_ids: Vec, + pub review_focus: Vec, + pub readiness: ReviewStoryStepReadiness, + pub error: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ReviewStoryStepReadiness { + Outline, + Enriching, + Ready, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStoryAnchor { + pub anchor_id: String, + pub file_path: String, + pub change_kind: ReviewStoryAnchorKind, + pub summary: String, + pub diff: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ReviewStoryAnchorKind { + Added, + Modified, + Deleted, + Renamed, + Copied, + Unknown, +} diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 95baac4e9e..fdba32aa06 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -76,6 +76,7 @@ clap = { workspace = true, features = ["derive"] } futures = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +sha2 = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } time = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index dcc2953073..9e711193d8 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -167,6 +167,7 @@ Example with notification opt-out: - `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`. - `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. +- `reviewStory/start` — build a persisted, read-only review story for a concrete source (`uncommittedChanges`, `baseBranch`, or `commit`). Codex collects stable change anchors from the diff, asks a constrained model task to group those anchors into cohesive story steps, validates that every model-referenced anchor exists, and falls back to file-level steps if model generation is unavailable. The response includes the initial `storySnapshot`, and the server emits `reviewStory/snapshot/updated` whenever a snapshot is written. Use `reviewStory/read` to fetch a snapshot by id and `reviewStory/list` to page saved stories for a thread. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `command/exec/write` — write base64-decoded stdin bytes to a running `command/exec` session or close stdin; returns `{}`. - `command/exec/resize` — resize a running PTY-backed `command/exec` session by `processId`; returns `{}`. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 0e1f019de9..c5dc70e4c0 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -31,6 +31,7 @@ use crate::request_processors::McpRequestProcessor; use crate::request_processors::PluginRequestProcessor; use crate::request_processors::ProcessExecRequestProcessor; use crate::request_processors::RemoteControlRequestProcessor; +use crate::request_processors::ReviewStoryRequestProcessor; use crate::request_processors::SearchRequestProcessor; use crate::request_processors::ThreadGoalRequestProcessor; use crate::request_processors::ThreadRequestProcessor; @@ -177,6 +178,7 @@ pub(crate) struct MessageProcessor { mcp_processor: McpRequestProcessor, plugin_processor: PluginRequestProcessor, remote_control_processor: RemoteControlRequestProcessor, + review_story_processor: ReviewStoryRequestProcessor, search_processor: SearchRequestProcessor, thread_goal_processor: ThreadGoalRequestProcessor, thread_processor: ThreadRequestProcessor, @@ -402,6 +404,11 @@ impl MessageProcessor { workspace_settings_cache, ); let remote_control_processor = RemoteControlRequestProcessor::new(remote_control_handle); + let review_story_processor = ReviewStoryRequestProcessor::new( + Arc::clone(&thread_manager), + outgoing.clone(), + state_db.clone(), + ); let search_processor = SearchRequestProcessor::new(outgoing.clone()); let thread_goal_processor = ThreadGoalRequestProcessor::new( Arc::clone(&thread_manager), @@ -498,6 +505,7 @@ impl MessageProcessor { mcp_processor, plugin_processor, remote_control_processor, + review_story_processor, search_processor, thread_goal_processor, thread_processor, @@ -1222,6 +1230,15 @@ impl MessageProcessor { ClientRequest::ReviewStart { params, .. } => { self.turn_processor.review_start(&request_id, params).await } + ClientRequest::ReviewStoryStart { params, .. } => { + self.review_story_processor.start(&request_id, params).await + } + ClientRequest::ReviewStoryRead { params, .. } => { + self.review_story_processor.read(params).await + } + ClientRequest::ReviewStoryList { params, .. } => { + self.review_story_processor.list(params).await + } ClientRequest::McpServerOauthLogin { params, .. } => { self.mcp_processor.mcp_server_oauth_login(params).await } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index d0958e80ab..777d677e57 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -466,6 +466,7 @@ mod mcp_processor; mod plugins; mod process_exec_processor; mod remote_control_processor; +mod review_story_processor; mod search; mod thread_processor; mod token_usage_replay; @@ -488,6 +489,7 @@ pub(crate) use mcp_processor::McpRequestProcessor; pub(crate) use plugins::PluginRequestProcessor; pub(crate) use process_exec_processor::ProcessExecRequestProcessor; pub(crate) use remote_control_processor::RemoteControlRequestProcessor; +pub(crate) use review_story_processor::ReviewStoryRequestProcessor; pub(crate) use search::SearchRequestProcessor; pub(crate) use thread_goal_processor::ThreadGoalRequestProcessor; pub(crate) use thread_processor::ThreadRequestProcessor; diff --git a/codex-rs/app-server/src/request_processors/review_story_processor.rs b/codex-rs/app-server/src/request_processors/review_story_processor.rs new file mode 100644 index 0000000000..d77fb0dc14 --- /dev/null +++ b/codex-rs/app-server/src/request_processors/review_story_processor.rs @@ -0,0 +1,1023 @@ +use std::path::Path; +use std::sync::Arc; + +use codex_app_server_protocol::ClientResponsePayload; +use codex_app_server_protocol::ReviewStoryAnchor; +use codex_app_server_protocol::ReviewStoryAnchorKind; +use codex_app_server_protocol::ReviewStoryListParams; +use codex_app_server_protocol::ReviewStoryListResponse; +use codex_app_server_protocol::ReviewStoryReadParams; +use codex_app_server_protocol::ReviewStoryReadResponse; +use codex_app_server_protocol::ReviewStorySnapshot; +use codex_app_server_protocol::ReviewStorySnapshotStatus; +use codex_app_server_protocol::ReviewStorySnapshotSummary; +use codex_app_server_protocol::ReviewStorySnapshotUpdatedNotification; +use codex_app_server_protocol::ReviewStoryStartParams; +use codex_app_server_protocol::ReviewStoryStartResponse; +use codex_app_server_protocol::ReviewStoryStep; +use codex_app_server_protocol::ReviewStoryStepReadiness; +use codex_app_server_protocol::ReviewTarget; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::Turn; +use codex_app_server_protocol::TurnItemsView; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput as V2UserInput; +use codex_core::ThreadManager; +use codex_protocol::ThreadId; +use codex_protocol::protocol::SubAgentSource; +use codex_rollout::state_db::StateDbHandle; +use codex_state::ReviewStoryRecord; +use codex_state::ReviewStorySummaryRecord; +use serde::Deserialize; +use serde_json::json; +use sha2::Digest; +use sha2::Sha256; +use std::collections::HashMap; +use std::collections::HashSet; +use tokio::process::Command; +use uuid::Uuid; + +use crate::error_code::internal_error; +use crate::error_code::invalid_request; +use crate::outgoing_message::ConnectionRequestId; +use crate::outgoing_message::OutgoingMessageSender; + +#[derive(Clone)] +pub(crate) struct ReviewStoryRequestProcessor { + thread_manager: Arc, + outgoing: Arc, + state_db: Option, +} + +impl ReviewStoryRequestProcessor { + pub(crate) fn new( + thread_manager: Arc, + outgoing: Arc, + state_db: Option, + ) -> Self { + Self { + thread_manager, + outgoing, + state_db, + } + } + + pub(crate) async fn start( + &self, + _request_id: &ConnectionRequestId, + params: ReviewStoryStartParams, + ) -> Result, codex_app_server_protocol::JSONRPCErrorError> { + let ReviewStoryStartParams { thread_id, target } = params; + let (thread_uuid, thread) = self.load_thread(&thread_id).await?; + let state_db = self.require_state_db()?; + let cwd = thread.config_snapshot().await.cwd; + let evidence = collect_evidence(cwd.as_path(), &target).await?; + let model_story = generate_model_story(&thread, &target, &evidence).await; + let story_snapshot_id = Uuid::now_v7().to_string(); + let now = chrono::Utc::now().timestamp(); + let snapshot = build_snapshot( + story_snapshot_id.clone(), + thread_id.clone(), + target, + evidence, + model_story, + now, + /*previous_story_snapshot_id*/ None, + ); + let record = snapshot_record(&snapshot)?; + state_db + .review_stories() + .upsert_snapshot(record) + .await + .map_err(|err| internal_error(format!("failed to store review story: {err}")))?; + + self.outgoing + .send_server_notification(ServerNotification::ReviewStorySnapshotUpdated( + ReviewStorySnapshotUpdatedNotification { + thread_id: thread_uuid.to_string(), + snapshot: snapshot.clone(), + }, + )) + .await; + + let turn = build_story_turn( + format!("review-story-{}", snapshot.story_snapshot_id), + "Create a review story for the selected changes.", + ); + Ok(Some( + ReviewStoryStartResponse { + turn, + story_snapshot_id, + snapshot, + } + .into(), + )) + } + + pub(crate) async fn read( + &self, + params: ReviewStoryReadParams, + ) -> Result, codex_app_server_protocol::JSONRPCErrorError> { + let ReviewStoryReadParams { + thread_id, + story_snapshot_id, + } = params; + let thread_id = parse_thread_id(&thread_id)?; + let state_db = self.require_state_db()?; + let record = state_db + .review_stories() + .get_snapshot(thread_id, &story_snapshot_id) + .await + .map_err(|err| internal_error(format!("failed to read review story: {err}")))?; + let snapshot = record + .map(|record| serde_json::from_value(record.snapshot_json)) + .transpose() + .map_err(|err| internal_error(format!("failed to decode review story: {err}")))?; + Ok(Some(ReviewStoryReadResponse { snapshot }.into())) + } + + pub(crate) async fn list( + &self, + params: ReviewStoryListParams, + ) -> Result, codex_app_server_protocol::JSONRPCErrorError> { + let ReviewStoryListParams { + thread_id, + cursor, + limit, + } = params; + let thread_id = parse_thread_id(&thread_id)?; + let state_db = self.require_state_db()?; + let (records, next_cursor) = state_db + .review_stories() + .list_snapshots(thread_id, cursor, limit) + .await + .map_err(|err| internal_error(format!("failed to list review stories: {err}")))?; + let data = records + .into_iter() + .map(summary_from_record) + .collect::, _>>()?; + Ok(Some(ReviewStoryListResponse { data, next_cursor }.into())) + } + + async fn load_thread( + &self, + thread_id: &str, + ) -> Result< + (ThreadId, Arc), + codex_app_server_protocol::JSONRPCErrorError, + > { + let thread_id = parse_thread_id(thread_id)?; + let thread = self + .thread_manager + .get_thread(thread_id) + .await + .map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?; + Ok((thread_id, thread)) + } + + fn require_state_db( + &self, + ) -> Result<&StateDbHandle, codex_app_server_protocol::JSONRPCErrorError> { + self.state_db + .as_ref() + .ok_or_else(|| internal_error("review story storage is unavailable")) + } +} + +struct StoryEvidence { + source_fingerprint: String, + anchors: Vec, +} + +struct ModelStoryResult { + output: Option, + error: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ModelReviewStoryOutput { + title: String, + overview: String, + steps: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ModelReviewStoryStep { + title: String, + goal: String, + summary: String, + dependency_rationale: String, + anchor_ids: Vec, + review_focus: Vec, +} + +async fn collect_evidence( + cwd: &Path, + target: &ReviewTarget, +) -> Result { + let diff = match target { + ReviewTarget::UncommittedChanges => { + git_output( + cwd, + &["diff", "--no-ext-diff", "--find-renames", "HEAD", "--"], + ) + .await? + } + ReviewTarget::BaseBranch { branch } => { + let range = format!("{branch}...HEAD"); + git_output( + cwd, + &[ + "diff", + "--no-ext-diff", + "--find-renames", + range.as_str(), + "--", + ], + ) + .await? + } + ReviewTarget::Commit { sha, .. } => { + git_output( + cwd, + &[ + "show", + "--format=", + "--no-ext-diff", + "--find-renames", + sha, + "--", + ], + ) + .await? + } + ReviewTarget::Custom { .. } => { + return Err(invalid_request( + "reviewStory/start requires a concrete source: uncommitted changes, base branch, or commit", + )); + } + }; + let source_fingerprint = source_fingerprint(target, &diff)?; + let anchors = anchors_from_diff(&diff); + Ok(StoryEvidence { + source_fingerprint, + anchors, + }) +} + +async fn generate_model_story( + thread: &codex_core::CodexThread, + target: &ReviewTarget, + evidence: &StoryEvidence, +) -> ModelStoryResult { + if evidence.anchors.is_empty() { + return ModelStoryResult { + output: None, + error: None, + }; + } + let prompt = review_story_prompt(target, evidence); + let schema = review_story_output_schema(); + match thread + .run_structured_model_task( + prompt, + REVIEW_STORY_MODEL_INSTRUCTIONS.to_string(), + schema, + SubAgentSource::Other("review_story".to_string()), + ) + .await + { + Ok(Some(output_text)) => match parse_model_review_story_output(&output_text) { + Ok(output) => ModelStoryResult { + output: Some(output), + error: None, + }, + Err(err) => ModelStoryResult { + output: None, + error: Some(format!("model returned an invalid review story: {err}")), + }, + }, + Ok(None) => ModelStoryResult { + output: None, + error: Some("model did not return a review story".to_string()), + }, + Err(err) => ModelStoryResult { + output: None, + error: Some(format!("failed to generate review story: {err}")), + }, + } +} + +const REVIEW_STORY_MODEL_INSTRUCTIONS: &str = r#"You create review stories for code changes. + +Group the supplied change anchors into a small sequence of cohesive logic steps. Each step should help a reviewer understand why the change exists, what changed, and how that step depends on earlier steps. Use only the provided anchor ids. Do not invent files, commits, or anchor ids. Prefer semantic dependency order over file order when the evidence supports it."#; + +fn review_story_prompt(target: &ReviewTarget, evidence: &StoryEvidence) -> String { + let mut prompt = String::new(); + prompt.push_str("Create a dependency-aware review story for these changes.\n\n"); + prompt.push_str("Target:\n"); + prompt.push_str(&target_description(target)); + prompt.push_str("\n\nSource fingerprint:\n"); + prompt.push_str(&evidence.source_fingerprint); + prompt.push_str("\n\nAnchors:\n"); + for anchor in &evidence.anchors { + prompt.push_str("\n---\n"); + prompt.push_str("anchorId: "); + prompt.push_str(&anchor.anchor_id); + prompt.push_str("\nfilePath: "); + prompt.push_str(&anchor.file_path); + prompt.push_str("\nchangeKind: "); + prompt.push_str(anchor_kind_as_str(anchor.change_kind)); + prompt.push_str("\nsummary: "); + prompt.push_str(&anchor.summary); + prompt.push_str("\ndiff:\n"); + prompt.push_str(&truncate_for_prompt( + &anchor.diff, + /*max_chars*/ 12_000, + )); + prompt.push('\n'); + } + prompt.push_str( + "\nReturn JSON only. Every anchor id above should appear in exactly one step unless the diff is empty. Keep steps reviewer-sized and cohesive.", + ); + truncate_for_prompt(&prompt, /*max_chars*/ 120_000) +} + +fn target_description(target: &ReviewTarget) -> String { + match target { + ReviewTarget::UncommittedChanges => "uncommitted changes".to_string(), + ReviewTarget::BaseBranch { branch } => format!("diff from base branch {branch}"), + ReviewTarget::Commit { sha, title } => { + let title = title.as_deref().unwrap_or("untitled commit"); + format!("commit {sha}: {title}") + } + ReviewTarget::Custom { instructions } => { + format!("custom review target: {instructions}") + } + } +} + +fn anchor_kind_as_str(kind: ReviewStoryAnchorKind) -> &'static str { + match kind { + ReviewStoryAnchorKind::Added => "added", + ReviewStoryAnchorKind::Modified => "modified", + ReviewStoryAnchorKind::Deleted => "deleted", + ReviewStoryAnchorKind::Renamed => "renamed", + ReviewStoryAnchorKind::Copied => "copied", + ReviewStoryAnchorKind::Unknown => "unknown", + } +} + +fn truncate_for_prompt(text: &str, max_chars: usize) -> String { + if text.chars().count() <= max_chars { + return text.to_string(); + } + let mut truncated = text.chars().take(max_chars).collect::(); + truncated.push_str("\n[truncated]"); + truncated +} + +fn review_story_output_schema() -> serde_json::Value { + json!({ + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string" + }, + "overview": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string" + }, + "goal": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "dependencyRationale": { + "type": "string" + }, + "anchorIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "reviewFocus": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title", + "goal", + "summary", + "dependencyRationale", + "anchorIds", + "reviewFocus" + ] + } + } + }, + "required": [ + "title", + "overview", + "steps" + ] + }) +} + +fn parse_model_review_story_output( + text: &str, +) -> Result { + if let Ok(output) = serde_json::from_str::(text) { + return Ok(output); + } + if let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) + && start < end + && let Some(slice) = text.get(start..=end) + { + return serde_json::from_str::(slice); + } + serde_json::from_str::(text) +} + +async fn git_output( + cwd: &Path, + args: &[&str], +) -> Result { + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .output() + .await + .map_err(|err| internal_error(format!("failed to run git: {err}")))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(invalid_request(format!("failed to read changes: {stderr}"))); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +fn source_fingerprint( + target: &ReviewTarget, + diff: &str, +) -> Result { + let mut hasher = Sha256::new(); + let target_json = serde_json::to_vec(target) + .map_err(|err| internal_error(format!("failed to encode review story target: {err}")))?; + hasher.update(target_json); + const FINGERPRINT_SEPARATOR: [u8; 1] = [0]; + hasher.update(FINGERPRINT_SEPARATOR); + hasher.update(diff.as_bytes()); + Ok(format!("sha256:{:x}", hasher.finalize())) +} + +fn anchors_from_diff(diff: &str) -> Vec { + let mut anchors = Vec::new(); + let mut current_header: Option = None; + let mut current_diff = String::new(); + for line in diff.lines() { + if line.starts_with("diff --git ") { + if let Some(header) = current_header.take() { + anchors.push(anchor_from_file_diff(anchors.len(), &header, ¤t_diff)); + current_diff.clear(); + } + current_header = Some(line.to_string()); + } + if current_header.is_some() { + current_diff.push_str(line); + current_diff.push('\n'); + } + } + if let Some(header) = current_header { + anchors.push(anchor_from_file_diff(anchors.len(), &header, ¤t_diff)); + } + anchors +} + +fn anchor_from_file_diff(index: usize, header: &str, diff: &str) -> ReviewStoryAnchor { + let file_path = file_path_from_diff_header(header).unwrap_or_else(|| "unknown".to_string()); + let change_kind = change_kind_from_file_diff(diff); + let summary = match change_kind { + ReviewStoryAnchorKind::Added => format!("Added {file_path}"), + ReviewStoryAnchorKind::Deleted => format!("Deleted {file_path}"), + ReviewStoryAnchorKind::Renamed => format!("Renamed {file_path}"), + ReviewStoryAnchorKind::Copied => format!("Copied {file_path}"), + ReviewStoryAnchorKind::Modified => format!("Modified {file_path}"), + ReviewStoryAnchorKind::Unknown => format!("Changed {file_path}"), + }; + ReviewStoryAnchor { + anchor_id: format!("anchor-{}", index + 1), + file_path, + change_kind, + summary, + diff: diff.to_string(), + } +} + +fn file_path_from_diff_header(header: &str) -> Option { + let mut parts = header.split_whitespace(); + let _diff = parts.next()?; + let _git = parts.next()?; + let _old = parts.next()?; + let new = parts.next()?; + Some(new.strip_prefix("b/").unwrap_or(new).to_string()) +} + +fn change_kind_from_file_diff(diff: &str) -> ReviewStoryAnchorKind { + if diff.contains("\nnew file mode ") { + ReviewStoryAnchorKind::Added + } else if diff.contains("\ndeleted file mode ") { + ReviewStoryAnchorKind::Deleted + } else if diff.contains("\nrename from ") { + ReviewStoryAnchorKind::Renamed + } else if diff.contains("\ncopy from ") { + ReviewStoryAnchorKind::Copied + } else if diff.is_empty() { + ReviewStoryAnchorKind::Unknown + } else { + ReviewStoryAnchorKind::Modified + } +} + +fn build_snapshot( + story_snapshot_id: String, + thread_id: String, + target: ReviewTarget, + evidence: StoryEvidence, + model_story: ModelStoryResult, + created_at: i64, + previous_story_snapshot_id: Option, +) -> ReviewStorySnapshot { + let fallback_title = title_for_target(&target); + let (title, overview, steps, status) = if evidence.anchors.is_empty() { + ( + fallback_title, + "No changes were detected for this review story source.".to_string(), + steps_from_anchors(&evidence.anchors), + ReviewStorySnapshotStatus::Ready, + ) + } else if let Some(output) = model_story.output { + let (steps, validation_error) = steps_from_model_output(output.steps, &evidence.anchors); + let status = if validation_error.is_some() { + ReviewStorySnapshotStatus::Partial + } else { + ReviewStorySnapshotStatus::Ready + }; + let overview = if let Some(error) = validation_error { + format!("{}\n\n{error}", output.overview) + } else { + output.overview + }; + (output.title, overview, steps, status) + } else { + let overview = model_story.error.map_or_else( + || { + format!( + "This story groups {} changed file(s) into review steps based on the local change map.", + evidence.anchors.len() + ) + }, + |error| format!("Model story generation did not complete. Falling back to file-level steps. {error}"), + ); + ( + fallback_title, + overview, + steps_from_anchors(&evidence.anchors), + ReviewStorySnapshotStatus::Partial, + ) + }; + ReviewStorySnapshot { + story_snapshot_id, + thread_id, + title, + overview, + target, + source_fingerprint: evidence.source_fingerprint, + status, + created_at, + updated_at: created_at, + previous_story_snapshot_id, + stale: false, + steps, + anchors: evidence.anchors, + } +} + +fn title_for_target(target: &ReviewTarget) -> String { + match target { + ReviewTarget::UncommittedChanges => "Review story for uncommitted changes".to_string(), + ReviewTarget::BaseBranch { branch } => format!("Review story against {branch}"), + ReviewTarget::Commit { title, sha } => title + .clone() + .unwrap_or_else(|| format!("Review story for commit {sha}")), + ReviewTarget::Custom { .. } => "Review story".to_string(), + } +} + +fn steps_from_anchors(anchors: &[ReviewStoryAnchor]) -> Vec { + if anchors.is_empty() { + return vec![ReviewStoryStep { + step_id: "step-1".to_string(), + index: 1, + title: "No changes".to_string(), + goal: "Confirm that the selected source has no diff to review.".to_string(), + summary: "The source currently produces an empty diff.".to_string(), + dependency_rationale: "There are no file-level dependencies to order.".to_string(), + anchor_ids: Vec::new(), + review_focus: Vec::new(), + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }]; + } + + anchors + .iter() + .enumerate() + .map(|(index, anchor)| ReviewStoryStep { + step_id: format!("step-{}", index + 1), + index: (index + 1) as u32, + title: anchor.summary.clone(), + goal: format!("Understand the purpose of changes in {}.", anchor.file_path), + summary: summarize_anchor(anchor), + dependency_rationale: "Placed according to the file-level evidence order from the diff; enrichment can refine this with semantic dependencies.".to_string(), + anchor_ids: vec![anchor.anchor_id.clone()], + review_focus: vec![format!("Check how {} fits the overall change story.", anchor.file_path)], + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }) + .collect() +} + +fn steps_from_model_output( + model_steps: Vec, + anchors: &[ReviewStoryAnchor], +) -> (Vec, Option) { + let anchors_by_id = anchors + .iter() + .map(|anchor| (anchor.anchor_id.as_str(), anchor)) + .collect::>(); + let mut seen_anchor_ids = HashSet::new(); + let mut invalid_anchor_ids = Vec::new(); + let mut steps = Vec::new(); + for model_step in model_steps { + let mut anchor_ids = Vec::new(); + for anchor_id in model_step.anchor_ids { + if anchors_by_id.contains_key(anchor_id.as_str()) + && seen_anchor_ids.insert(anchor_id.clone()) + { + anchor_ids.push(anchor_id); + } else if !anchors_by_id.contains_key(anchor_id.as_str()) { + invalid_anchor_ids.push(anchor_id); + } + } + if anchor_ids.is_empty() { + continue; + } + let index = steps.len() + 1; + steps.push(ReviewStoryStep { + step_id: format!("step-{index}"), + index: index as u32, + title: model_step.title, + goal: model_step.goal, + summary: model_step.summary, + dependency_rationale: model_step.dependency_rationale, + anchor_ids, + review_focus: model_step.review_focus, + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }); + } + + let missing_anchors = anchors + .iter() + .filter(|anchor| !seen_anchor_ids.contains(&anchor.anchor_id)) + .cloned() + .collect::>(); + if !missing_anchors.is_empty() { + let index = steps.len() + 1; + steps.push(ReviewStoryStep { + step_id: format!("step-{index}"), + index: index as u32, + title: "Remaining changes".to_string(), + goal: "Review changes the model did not place into a story step.".to_string(), + summary: format!( + "Covers {} anchor(s) that were omitted from the model-authored story.", + missing_anchors.len() + ), + dependency_rationale: + "Appended after model-authored steps to preserve full diff coverage.".to_string(), + anchor_ids: missing_anchors + .iter() + .map(|anchor| anchor.anchor_id.clone()) + .collect(), + review_focus: missing_anchors + .iter() + .map(|anchor| format!("Check {} independently.", anchor.file_path)) + .collect(), + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }); + } + + if steps.is_empty() { + return ( + steps_from_anchors(anchors), + Some("The model did not return any usable review story steps.".to_string()), + ); + } + + let mut validation_notes = Vec::new(); + if !missing_anchors.is_empty() { + validation_notes.push(format!( + "The model omitted {} anchor(s); Codex appended a coverage step.", + missing_anchors.len() + )); + } + if !invalid_anchor_ids.is_empty() { + validation_notes.push(format!( + "The model referenced unknown anchor id(s): {}.", + invalid_anchor_ids.join(", ") + )); + } + if validation_notes.is_empty() { + (steps, None) + } else { + (steps, Some(validation_notes.join(" "))) + } +} + +fn summarize_anchor(anchor: &ReviewStoryAnchor) -> String { + let additions = anchor + .diff + .lines() + .filter(|line| line.starts_with('+') && !line.starts_with("+++")) + .count(); + let deletions = anchor + .diff + .lines() + .filter(|line| line.starts_with('-') && !line.starts_with("---")) + .count(); + format!( + "{} with {additions} added line(s) and {deletions} removed line(s).", + anchor.summary + ) +} + +fn build_story_turn(turn_id: String, display_text: &str) -> Turn { + Turn { + id: turn_id.clone(), + items: vec![ThreadItem::UserMessage { + id: turn_id, + content: vec![V2UserInput::Text { + text: display_text.to_string(), + text_elements: Vec::new(), + }], + }], + items_view: TurnItemsView::NotLoaded, + error: None, + status: TurnStatus::Completed, + started_at: None, + completed_at: None, + duration_ms: None, + } +} + +fn snapshot_record( + snapshot: &ReviewStorySnapshot, +) -> Result { + let target_json = serde_json::to_value(&snapshot.target) + .map_err(|err| internal_error(format!("failed to encode review story target: {err}")))?; + let snapshot_json = serde_json::to_value(snapshot) + .map_err(|err| internal_error(format!("failed to encode review story snapshot: {err}")))?; + Ok(ReviewStoryRecord { + story_snapshot_id: snapshot.story_snapshot_id.clone(), + thread_id: snapshot.thread_id.clone(), + source_fingerprint: snapshot.source_fingerprint.clone(), + status: status_as_str(snapshot.status).to_string(), + title: snapshot.title.clone(), + step_count: snapshot.steps.len() as i64, + target_json, + snapshot_json, + previous_story_snapshot_id: snapshot.previous_story_snapshot_id.clone(), + created_at_ms: snapshot.created_at * 1000, + updated_at_ms: snapshot.updated_at * 1000, + }) +} + +fn summary_from_record( + record: ReviewStorySummaryRecord, +) -> Result { + let target = serde_json::from_value(record.target_json) + .map_err(|err| internal_error(format!("failed to decode review story target: {err}")))?; + Ok(ReviewStorySnapshotSummary { + story_snapshot_id: record.story_snapshot_id, + thread_id: record.thread_id, + title: record.title, + target, + source_fingerprint: record.source_fingerprint, + status: status_from_str(record.status.as_str()), + created_at: record.created_at_ms / 1000, + updated_at: record.updated_at_ms / 1000, + previous_story_snapshot_id: record.previous_story_snapshot_id, + step_count: record.step_count.max(/*other*/ 0) as u32, + }) +} + +fn status_as_str(status: ReviewStorySnapshotStatus) -> &'static str { + match status { + ReviewStorySnapshotStatus::Building => "building", + ReviewStorySnapshotStatus::Ready => "ready", + ReviewStorySnapshotStatus::Partial => "partial", + ReviewStorySnapshotStatus::Failed => "failed", + } +} + +fn status_from_str(status: &str) -> ReviewStorySnapshotStatus { + match status { + "building" => ReviewStorySnapshotStatus::Building, + "partial" => ReviewStorySnapshotStatus::Partial, + "failed" => ReviewStorySnapshotStatus::Failed, + "ready" => ReviewStorySnapshotStatus::Ready, + _ => ReviewStorySnapshotStatus::Failed, + } +} + +fn parse_thread_id( + thread_id: &str, +) -> Result { + ThreadId::from_string(thread_id) + .map_err(|err| invalid_request(format!("invalid thread id: {err}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn anchors_split_diff_by_file() { + let diff = "\ +diff --git a/src/lib.rs b/src/lib.rs +index 111..222 100644 +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -1 +1 @@ +-old ++new +diff --git a/src/main.rs b/src/main.rs +new file mode 100644 +--- /dev/null ++++ b/src/main.rs +@@ -0,0 +1 @@ ++fn main() {} +"; + + let anchors = anchors_from_diff(diff); + + assert_eq!( + anchors, + vec![ + ReviewStoryAnchor { + anchor_id: "anchor-1".to_string(), + file_path: "src/lib.rs".to_string(), + change_kind: ReviewStoryAnchorKind::Modified, + summary: "Modified src/lib.rs".to_string(), + diff: "diff --git a/src/lib.rs b/src/lib.rs\nindex 111..222 100644\n--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1 +1 @@\n-old\n+new\n".to_string(), + }, + ReviewStoryAnchor { + anchor_id: "anchor-2".to_string(), + file_path: "src/main.rs".to_string(), + change_kind: ReviewStoryAnchorKind::Added, + summary: "Added src/main.rs".to_string(), + diff: "diff --git a/src/main.rs b/src/main.rs\nnew file mode 100644\n--- /dev/null\n+++ b/src/main.rs\n@@ -0,0 +1 @@\n+fn main() {}\n".to_string(), + }, + ] + ); + } + + #[test] + fn model_output_groups_anchors_into_cohesive_steps() { + let anchors = sample_anchors(); + let model_steps = vec![ModelReviewStoryStep { + title: "Introduce review story state".to_string(), + goal: "Create the storage and protocol foundation.".to_string(), + summary: "Adds reusable state and API shapes for review stories.".to_string(), + dependency_rationale: "The UI depends on the state and protocol contract.".to_string(), + anchor_ids: vec!["anchor-1".to_string(), "anchor-2".to_string()], + review_focus: vec!["Confirm the persisted snapshot is reusable.".to_string()], + }]; + + let (steps, validation_error) = steps_from_model_output(model_steps, &anchors); + + assert_eq!(validation_error, None); + assert_eq!( + steps, + vec![ReviewStoryStep { + step_id: "step-1".to_string(), + index: 1, + title: "Introduce review story state".to_string(), + goal: "Create the storage and protocol foundation.".to_string(), + summary: "Adds reusable state and API shapes for review stories.".to_string(), + dependency_rationale: "The UI depends on the state and protocol contract." + .to_string(), + anchor_ids: vec!["anchor-1".to_string(), "anchor-2".to_string()], + review_focus: vec!["Confirm the persisted snapshot is reusable.".to_string()], + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }] + ); + } + + #[test] + fn model_output_appends_missing_anchors_for_full_coverage() { + let anchors = sample_anchors(); + let model_steps = vec![ModelReviewStoryStep { + title: "Add protocol".to_string(), + goal: "Define the API contract.".to_string(), + summary: "Adds the protocol file.".to_string(), + dependency_rationale: "Protocol comes first.".to_string(), + anchor_ids: vec!["anchor-1".to_string(), "missing-anchor".to_string()], + review_focus: vec!["Check wire shape.".to_string()], + }]; + + let (steps, validation_error) = steps_from_model_output(model_steps, &anchors); + + assert_eq!( + validation_error, + Some( + "The model omitted 1 anchor(s); Codex appended a coverage step. The model referenced unknown anchor id(s): missing-anchor." + .to_string() + ) + ); + assert_eq!( + steps, + vec![ + ReviewStoryStep { + step_id: "step-1".to_string(), + index: 1, + title: "Add protocol".to_string(), + goal: "Define the API contract.".to_string(), + summary: "Adds the protocol file.".to_string(), + dependency_rationale: "Protocol comes first.".to_string(), + anchor_ids: vec!["anchor-1".to_string()], + review_focus: vec!["Check wire shape.".to_string()], + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }, + ReviewStoryStep { + step_id: "step-2".to_string(), + index: 2, + title: "Remaining changes".to_string(), + goal: "Review changes the model did not place into a story step.".to_string(), + summary: "Covers 1 anchor(s) that were omitted from the model-authored story." + .to_string(), + dependency_rationale: + "Appended after model-authored steps to preserve full diff coverage." + .to_string(), + anchor_ids: vec!["anchor-2".to_string()], + review_focus: vec![ + "Check codex-rs/tui/src/story.rs independently.".to_string() + ], + readiness: ReviewStoryStepReadiness::Ready, + error: None, + }, + ] + ); + } + + fn sample_anchors() -> Vec { + vec![ + ReviewStoryAnchor { + anchor_id: "anchor-1".to_string(), + file_path: "codex-rs/app-server-protocol/src/protocol/v2/review_story.rs" + .to_string(), + change_kind: ReviewStoryAnchorKind::Added, + summary: "Added protocol".to_string(), + diff: "+protocol".to_string(), + }, + ReviewStoryAnchor { + anchor_id: "anchor-2".to_string(), + file_path: "codex-rs/tui/src/story.rs".to_string(), + change_kind: ReviewStoryAnchorKind::Added, + summary: "Added TUI".to_string(), + diff: "+tui".to_string(), + }, + ] + } +} diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 1b40387c3f..a7269d30e2 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -1,4 +1,6 @@ use crate::agent::AgentStatus; +use crate::codex_delegate::run_codex_thread_one_shot; +use crate::config::Constrained; use crate::config::ConstraintResult; use crate::goals::ExternalGoalSet; use crate::goals::GoalRuntimeEvent; @@ -11,6 +13,7 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; @@ -23,10 +26,12 @@ use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; +use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::Submission; use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::ThreadSource; @@ -41,11 +46,13 @@ use codex_thread_store::ThreadStoreError; use codex_thread_store::ThreadStoreResult; use codex_utils_absolute_path::AbsolutePathBuf; use rmcp::model::ReadResourceRequestParams; +use serde_json::Value; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use tokio::sync::watch; +use tokio_util::sync::CancellationToken; use codex_rollout::state_db::StateDbHandle; @@ -502,6 +509,68 @@ impl CodexThread { self.codex.session.get_config().await } + /// Run a short-lived model task that produces structured JSON for a caller-owned workflow. + /// + /// This keeps app-server features from needing to know how to spawn sub-agents while still + /// reusing the same constrained delegate machinery as review mode. + pub async fn run_structured_model_task( + &self, + prompt: String, + base_instructions: String, + final_output_json_schema: Value, + subagent_source: SubAgentSource, + ) -> CodexResult> { + let ctx = self + .codex + .session + .new_default_turn_with_sub_id(format!("structured-task-{}", uuid::Uuid::now_v7())) + .await; + let mut sub_agent_config = ctx.config.as_ref().clone(); + if let Err(err) = sub_agent_config + .web_search_mode + .set(WebSearchMode::Disabled) + { + panic!( + "by construction Constrained must always support Disabled: {err}" + ); + } + let _ = sub_agent_config.features.disable(Feature::SpawnCsv); + let _ = sub_agent_config.features.disable(Feature::Collab); + let _ = sub_agent_config.features.disable(Feature::MultiAgentV2); + sub_agent_config.permissions.approval_policy = + Constrained::allow_only(AskForApproval::Never); + sub_agent_config.base_instructions = Some(base_instructions); + let input = vec![UserInput::Text { + text: prompt, + text_elements: Vec::new(), + }]; + let io = run_codex_thread_one_shot( + sub_agent_config, + Arc::clone(&self.codex.session.services.auth_manager), + Arc::clone(&self.codex.session.services.models_manager), + input, + Arc::clone(&self.codex.session), + ctx, + CancellationToken::new(), + subagent_source, + Some(final_output_json_schema), + /*initial_history*/ None, + ) + .await?; + while let Ok(event) = io.next_event().await { + match event.msg { + EventMsg::TurnComplete(turn_complete) => { + return Ok(turn_complete.last_agent_message); + } + EventMsg::TurnAborted(_) => { + return Ok(None); + } + _ => {} + } + } + Ok(None) + } + /// Refresh the thread's layer-backed user config state from a caller-supplied /// config snapshot. Thread-scoped layers and session-static settings remain /// unchanged. diff --git a/codex-rs/state/BUILD.bazel b/codex-rs/state/BUILD.bazel index b3f0fecab7..dca74541f5 100644 --- a/codex-rs/state/BUILD.bazel +++ b/codex-rs/state/BUILD.bazel @@ -3,5 +3,5 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "state", crate_name = "codex_state", - compile_data = glob(["goals_migrations/**", "logs_migrations/**", "migrations/**"]), + compile_data = glob(["goals_migrations/**", "logs_migrations/**", "migrations/**", "review_story_migrations/**"]), ) diff --git a/codex-rs/state/review_story_migrations/0001_review_stories.sql b/codex-rs/state/review_story_migrations/0001_review_stories.sql new file mode 100644 index 0000000000..62f09234b9 --- /dev/null +++ b/codex-rs/state/review_story_migrations/0001_review_stories.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS review_story_snapshots ( + story_snapshot_id TEXT PRIMARY KEY NOT NULL, + thread_id TEXT NOT NULL, + source_fingerprint TEXT NOT NULL, + status TEXT NOT NULL, + title TEXT NOT NULL, + step_count INTEGER NOT NULL, + target_json TEXT NOT NULL, + snapshot_json TEXT NOT NULL, + previous_story_snapshot_id TEXT, + created_at_ms INTEGER NOT NULL, + updated_at_ms INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS review_story_snapshots_thread_updated_idx + ON review_story_snapshots(thread_id, updated_at_ms DESC, story_snapshot_id DESC); + +CREATE INDEX IF NOT EXISTS review_story_snapshots_thread_fingerprint_idx + ON review_story_snapshots(thread_id, source_fingerprint, updated_at_ms DESC); diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index f35b3420f6..0e8341a4d2 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -53,12 +53,17 @@ pub use runtime::GoalAccountingOutcome; pub use runtime::GoalStore; pub use runtime::GoalUpdate; pub use runtime::RemoteControlEnrollmentRecord; +pub use runtime::ReviewStoryRecord; +pub use runtime::ReviewStoryStore; +pub use runtime::ReviewStorySummaryRecord; pub use runtime::RuntimeDbPath; pub use runtime::ThreadFilterOptions; pub use runtime::goals_db_filename; pub use runtime::goals_db_path; pub use runtime::logs_db_filename; pub use runtime::logs_db_path; +pub use runtime::review_story_db_filename; +pub use runtime::review_story_db_path; pub use runtime::runtime_db_paths; pub use runtime::sqlite_integrity_check; pub use runtime::state_db_filename; @@ -74,6 +79,7 @@ pub const SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME"; pub const LOGS_DB_FILENAME: &str = "logs_2.sqlite"; pub const GOALS_DB_FILENAME: &str = "goals_1.sqlite"; +pub const REVIEW_STORY_DB_FILENAME: &str = "review_stories_1.sqlite"; pub const STATE_DB_FILENAME: &str = "state_5.sqlite"; /// Errors encountered during DB operations. Tags: [stage] diff --git a/codex-rs/state/src/migrations.rs b/codex-rs/state/src/migrations.rs index 526e958b5a..e6d34898f6 100644 --- a/codex-rs/state/src/migrations.rs +++ b/codex-rs/state/src/migrations.rs @@ -5,6 +5,7 @@ use sqlx::migrate::Migrator; pub(crate) static STATE_MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) static LOGS_MIGRATOR: Migrator = sqlx::migrate!("./logs_migrations"); pub(crate) static GOALS_MIGRATOR: Migrator = sqlx::migrate!("./goals_migrations"); +pub(crate) static REVIEW_STORY_MIGRATOR: Migrator = sqlx::migrate!("./review_story_migrations"); /// Allow an older Codex binary to open a database that has already been /// migrated by a newer binary running in parallel. @@ -32,3 +33,7 @@ pub(crate) fn runtime_logs_migrator() -> Migrator { pub(crate) fn runtime_goals_migrator() -> Migrator { runtime_migrator(&GOALS_MIGRATOR) } + +pub(crate) fn runtime_review_story_migrator() -> Migrator { + runtime_migrator(&REVIEW_STORY_MIGRATOR) +} diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 8c9c6d0267..4bb30a935c 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -10,6 +10,7 @@ use crate::LOGS_DB_FILENAME; use crate::LogEntry; use crate::LogQuery; use crate::LogRow; +use crate::REVIEW_STORY_DB_FILENAME; use crate::STATE_DB_FILENAME; use crate::SortKey; use crate::ThreadMetadata; @@ -18,6 +19,7 @@ use crate::ThreadsPage; use crate::apply_rollout_item; use crate::migrations::runtime_goals_migrator; use crate::migrations::runtime_logs_migrator; +use crate::migrations::runtime_review_story_migrator; use crate::migrations::runtime_state_migrator; use crate::model::AgentJobRow; use crate::model::ThreadRow; @@ -62,6 +64,7 @@ mod goals; mod logs; mod memories; mod remote_control; +mod review_stories; #[cfg(test)] mod test_support; mod threads; @@ -71,6 +74,9 @@ pub use goals::GoalAccountingOutcome; pub use goals::GoalStore; pub use goals::GoalUpdate; pub use remote_control::RemoteControlEnrollmentRecord; +pub use review_stories::ReviewStoryRecord; +pub use review_stories::ReviewStoryStore; +pub use review_stories::ReviewStorySummaryRecord; pub use threads::ThreadFilterOptions; // "Partition" is the retained-log-content bucket we cap at 10 MiB: @@ -121,7 +127,15 @@ const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec { migrate_phase: "migrate_goals", }; -const RUNTIME_DBS: [RuntimeDbSpec; 3] = [STATE_DB, LOGS_DB, GOALS_DB]; +const REVIEW_STORY_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "review story DB", + filename: REVIEW_STORY_DB_FILENAME, + kind: DbKind::ReviewStories, + open_phase: "open_review_stories", + migrate_phase: "migrate_review_stories", +}; + +const RUNTIME_DBS: [RuntimeDbSpec; 4] = [STATE_DB, LOGS_DB, GOALS_DB, REVIEW_STORY_DB]; #[derive(Clone, Debug, Eq, PartialEq)] pub struct RuntimeDbPath { @@ -136,6 +150,7 @@ pub struct StateRuntime { pool: Arc, logs_pool: Arc, thread_goals: GoalStore, + review_stories: ReviewStoryStore, thread_updated_at_millis: Arc, } @@ -172,9 +187,11 @@ impl StateRuntime { let state_migrator = runtime_state_migrator(); let logs_migrator = runtime_logs_migrator(); let goals_migrator = runtime_goals_migrator(); + let review_story_migrator = runtime_review_story_migrator(); let state_path = STATE_DB.path(codex_home.as_path()); let logs_path = LOGS_DB.path(codex_home.as_path()); let goals_path = GOALS_DB.path(codex_home.as_path()); + let review_story_path = REVIEW_STORY_DB.path(codex_home.as_path()); let pool = match open_state_sqlite(&state_path, &state_migrator, telemetry_override).await { Ok(db) => Arc::new(db), Err(err) => { @@ -198,6 +215,22 @@ impl StateRuntime { return Err(err); } }; + let review_story_pool = match open_review_story_sqlite( + &review_story_path, + &review_story_migrator, + telemetry_override, + ) + .await + { + Ok(db) => Arc::new(db), + Err(err) => { + warn!( + "failed to open review story db at {}: {err}", + review_story_path.display() + ); + return Err(err); + } + }; let started = Instant::now(); let backfill_state_result = ensure_backfill_state_row_in_pool(pool.as_ref()).await; crate::telemetry::record_init_result( @@ -225,6 +258,7 @@ impl StateRuntime { let thread_updated_at_millis = thread_updated_at_millis.unwrap_or(0); let runtime = Arc::new(Self { thread_goals: GoalStore::new(Arc::clone(&goals_pool)), + review_stories: ReviewStoryStore::new(review_story_pool), pool, logs_pool, codex_home, @@ -248,6 +282,10 @@ impl StateRuntime { pub fn thread_goals(&self) -> &GoalStore { &self.thread_goals } + + pub fn review_stories(&self) -> &ReviewStoryStore { + &self.review_stories + } } fn base_sqlite_options(path: &Path) -> SqliteConnectOptions { @@ -287,6 +325,14 @@ async fn open_goals_sqlite( open_sqlite(path, migrator, GOALS_DB, telemetry_override).await } +async fn open_review_story_sqlite( + path: &Path, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, +) -> anyhow::Result { + open_sqlite(path, migrator, REVIEW_STORY_DB, telemetry_override).await +} + async fn open_sqlite( path: &Path, migrator: &Migrator, @@ -363,6 +409,14 @@ pub fn goals_db_path(codex_home: &Path) -> PathBuf { GOALS_DB.path(codex_home) } +pub fn review_story_db_filename() -> String { + REVIEW_STORY_DB.filename.to_string() +} + +pub fn review_story_db_path(codex_home: &Path) -> PathBuf { + REVIEW_STORY_DB.path(codex_home) +} + pub fn runtime_db_paths(codex_home: &Path) -> Vec { RUNTIME_DBS .iter() @@ -579,6 +633,8 @@ mod tests { "migrate_logs", "open_goals", "migrate_goals", + "open_review_stories", + "migrate_review_stories", "ensure_backfill_state", "post_init_query", ] diff --git a/codex-rs/state/src/runtime/review_stories.rs b/codex-rs/state/src/runtime/review_stories.rs new file mode 100644 index 0000000000..8d34ac70a2 --- /dev/null +++ b/codex-rs/state/src/runtime/review_stories.rs @@ -0,0 +1,220 @@ +use super::*; + +#[derive(Clone)] +pub struct ReviewStoryStore { + pool: Arc, +} + +impl ReviewStoryStore { + pub(crate) fn new(pool: Arc) -> Self { + Self { pool } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReviewStoryRecord { + pub story_snapshot_id: String, + pub thread_id: String, + pub source_fingerprint: String, + pub status: String, + pub title: String, + pub step_count: i64, + pub target_json: Value, + pub snapshot_json: Value, + pub previous_story_snapshot_id: Option, + pub created_at_ms: i64, + pub updated_at_ms: i64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReviewStorySummaryRecord { + pub story_snapshot_id: String, + pub thread_id: String, + pub source_fingerprint: String, + pub status: String, + pub title: String, + pub step_count: i64, + pub target_json: Value, + pub previous_story_snapshot_id: Option, + pub created_at_ms: i64, + pub updated_at_ms: i64, +} + +impl ReviewStoryStore { + pub async fn upsert_snapshot(&self, record: ReviewStoryRecord) -> anyhow::Result<()> { + let ReviewStoryRecord { + story_snapshot_id, + thread_id, + source_fingerprint, + status, + title, + step_count, + target_json, + snapshot_json, + previous_story_snapshot_id, + created_at_ms, + updated_at_ms, + } = record; + let target_json = serde_json::to_string(&target_json)?; + let snapshot_json = serde_json::to_string(&snapshot_json)?; + sqlx::query( + r#" +INSERT INTO review_story_snapshots ( + story_snapshot_id, + thread_id, + source_fingerprint, + status, + title, + step_count, + target_json, + snapshot_json, + previous_story_snapshot_id, + created_at_ms, + updated_at_ms +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(story_snapshot_id) DO UPDATE SET + thread_id = excluded.thread_id, + source_fingerprint = excluded.source_fingerprint, + status = excluded.status, + title = excluded.title, + step_count = excluded.step_count, + target_json = excluded.target_json, + snapshot_json = excluded.snapshot_json, + previous_story_snapshot_id = excluded.previous_story_snapshot_id, + created_at_ms = excluded.created_at_ms, + updated_at_ms = excluded.updated_at_ms + "#, + ) + .bind(story_snapshot_id) + .bind(thread_id) + .bind(source_fingerprint) + .bind(status) + .bind(title) + .bind(step_count) + .bind(target_json) + .bind(snapshot_json) + .bind(previous_story_snapshot_id) + .bind(created_at_ms) + .bind(updated_at_ms) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + pub async fn get_snapshot( + &self, + thread_id: ThreadId, + story_snapshot_id: &str, + ) -> anyhow::Result> { + let row = sqlx::query( + r#" +SELECT + story_snapshot_id, + thread_id, + source_fingerprint, + status, + title, + step_count, + target_json, + snapshot_json, + previous_story_snapshot_id, + created_at_ms, + updated_at_ms +FROM review_story_snapshots +WHERE thread_id = ? AND story_snapshot_id = ? + "#, + ) + .bind(thread_id.to_string()) + .bind(story_snapshot_id) + .fetch_optional(self.pool.as_ref()) + .await?; + + row.map(|row| review_story_record_from_row(&row)) + .transpose() + } + + pub async fn list_snapshots( + &self, + thread_id: ThreadId, + cursor: Option, + limit: Option, + ) -> anyhow::Result<(Vec, Option)> { + let limit = limit + .unwrap_or(/*default*/ 50) + .clamp(/*min*/ 1, /*max*/ 100); + let offset = cursor + .as_deref() + .and_then(|cursor| cursor.parse::().ok()) + .unwrap_or(/*default*/ 0); + let rows = sqlx::query( + r#" +SELECT + story_snapshot_id, + thread_id, + source_fingerprint, + status, + title, + step_count, + target_json, + previous_story_snapshot_id, + created_at_ms, + updated_at_ms +FROM review_story_snapshots +WHERE thread_id = ? +ORDER BY updated_at_ms DESC, story_snapshot_id DESC +LIMIT ? OFFSET ? + "#, + ) + .bind(thread_id.to_string()) + .bind(i64::from(limit) + 1) + .bind(i64::from(offset)) + .fetch_all(self.pool.as_ref()) + .await?; + + let has_next = rows.len() > limit as usize; + let rows = rows.into_iter().take(limit as usize); + let records = rows + .map(|row| review_story_summary_record_from_row(&row)) + .collect::>>()?; + let next_cursor = has_next.then(|| (offset + limit).to_string()); + Ok((records, next_cursor)) + } +} + +fn review_story_record_from_row( + row: &sqlx::sqlite::SqliteRow, +) -> anyhow::Result { + let target_json: String = row.try_get("target_json")?; + let snapshot_json: String = row.try_get("snapshot_json")?; + Ok(ReviewStoryRecord { + story_snapshot_id: row.try_get("story_snapshot_id")?, + thread_id: row.try_get("thread_id")?, + source_fingerprint: row.try_get("source_fingerprint")?, + status: row.try_get("status")?, + title: row.try_get("title")?, + step_count: row.try_get("step_count")?, + target_json: serde_json::from_str(&target_json)?, + snapshot_json: serde_json::from_str(&snapshot_json)?, + previous_story_snapshot_id: row.try_get("previous_story_snapshot_id")?, + created_at_ms: row.try_get("created_at_ms")?, + updated_at_ms: row.try_get("updated_at_ms")?, + }) +} + +fn review_story_summary_record_from_row( + row: &sqlx::sqlite::SqliteRow, +) -> anyhow::Result { + let target_json: String = row.try_get("target_json")?; + Ok(ReviewStorySummaryRecord { + story_snapshot_id: row.try_get("story_snapshot_id")?, + thread_id: row.try_get("thread_id")?, + source_fingerprint: row.try_get("source_fingerprint")?, + status: row.try_get("status")?, + title: row.try_get("title")?, + step_count: row.try_get("step_count")?, + target_json: serde_json::from_str(&target_json)?, + previous_story_snapshot_id: row.try_get("previous_story_snapshot_id")?, + created_at_ms: row.try_get("created_at_ms")?, + updated_at_ms: row.try_get("updated_at_ms")?, + }) +} diff --git a/codex-rs/state/src/telemetry.rs b/codex-rs/state/src/telemetry.rs index da2b7de7a8..19a8b3e1d3 100644 --- a/codex-rs/state/src/telemetry.rs +++ b/codex-rs/state/src/telemetry.rs @@ -40,6 +40,7 @@ pub(crate) enum DbKind { State, Logs, Goals, + ReviewStories, } impl DbKind { @@ -48,6 +49,7 @@ impl DbKind { Self::State => "state", Self::Logs => "logs", Self::Goals => "goals", + Self::ReviewStories => "review_stories", } } } diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 0b2143b429..27465af71e 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -68,6 +68,9 @@ pub(super) fn server_notification_thread_target( ServerNotification::TurnStarted(notification) => Some(notification.thread_id.as_str()), ServerNotification::HookStarted(notification) => Some(notification.thread_id.as_str()), ServerNotification::TurnCompleted(notification) => Some(notification.thread_id.as_str()), + ServerNotification::ReviewStorySnapshotUpdated(notification) => { + Some(notification.thread_id.as_str()) + } ServerNotification::HookCompleted(notification) => Some(notification.thread_id.as_str()), ServerNotification::TurnDiffUpdated(notification) => Some(notification.thread_id.as_str()), ServerNotification::TurnPlanUpdated(notification) => Some(notification.thread_id.as_str()), diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index e150dbccd2..68b210b8d4 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1835,12 +1835,29 @@ impl App { AppEvent::OpenReviewBranchPicker(cwd) => { self.chat_widget.show_review_branch_picker(&cwd).await; } + AppEvent::OpenReviewStoryPopup => { + self.chat_widget.open_review_story_popup(); + } + AppEvent::OpenReviewStoryBranchPicker(cwd) => { + self.chat_widget.show_review_story_branch_picker(&cwd).await; + } AppEvent::OpenReviewCommitPicker(cwd) => { self.chat_widget.show_review_commit_picker(&cwd).await; } + AppEvent::OpenReviewStoryCommitPicker(cwd) => { + self.chat_widget.show_review_story_commit_picker(&cwd).await; + } AppEvent::OpenReviewCustomPrompt => { self.chat_widget.show_review_custom_prompt(); } + AppEvent::ReviewStoryReady(snapshot) => { + let _ = tui.enter_alt_screen(); + self.overlay = Some(Overlay::new_static_with_lines( + review_story_overlay_lines(snapshot), + "R E V I E W S T O R Y".to_string(), + self.keymap.pager.clone(), + )); + } AppEvent::SubmitUserMessageWithMode { text, collaboration_mode, @@ -2212,3 +2229,68 @@ impl App { } } } + +fn review_story_overlay_lines( + snapshot: codex_app_server_protocol::ReviewStorySnapshot, +) -> Vec> { + let mut lines = Vec::new(); + lines.push(snapshot.title.bold().into()); + lines.push(Line::from("")); + push_plain(&mut lines, "Overview", snapshot.overview.as_str()); + lines.push(Line::from(vec![ + "Source fingerprint: ".dim(), + snapshot.source_fingerprint.dim(), + ])); + lines.push(Line::from("")); + + for step in snapshot.steps { + lines.push(Line::from(vec![ + format!("{}. ", step.index).cyan().bold(), + step.title.bold(), + ])); + push_plain(&mut lines, "Goal", step.goal.as_str()); + push_plain(&mut lines, "Summary", step.summary.as_str()); + if !step.dependency_rationale.is_empty() { + push_plain(&mut lines, "Order", step.dependency_rationale.as_str()); + } + if !step.review_focus.is_empty() { + lines.push("Focus".bold().into()); + for focus in step.review_focus { + lines.push(Line::from(vec![" - ".dim(), focus.into()])); + } + } + for anchor_id in step.anchor_ids { + if let Some(anchor) = snapshot + .anchors + .iter() + .find(|candidate| candidate.anchor_id == anchor_id) + { + lines.push(Line::from(vec![ + "Change ".bold(), + anchor.anchor_id.clone().cyan(), + ": ".into(), + anchor.file_path.clone().into(), + ])); + if !anchor.summary.is_empty() { + lines.push(Line::from(vec![" ".into(), anchor.summary.clone().into()])); + } + for diff_line in anchor.diff.lines() { + lines.push(diff_line.to_string().dim().into()); + } + } + } + lines.push(Line::from("")); + } + + lines +} + +fn push_plain(lines: &mut Vec>, label: &str, text: &str) { + if text.trim().is_empty() { + return; + } + lines.push(label.to_string().bold().into()); + for wrapped in textwrap::wrap(text, /*width*/ 100) { + lines.push(Line::from(wrapped.into_owned())); + } +} diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 8f15aed79a..d57c26b97e 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -654,6 +654,14 @@ impl App { app_server.review_start(thread_id, target.clone()).await?; Ok(true) } + AppCommand::ReviewStory { target } => { + let response = app_server + .review_story_start(thread_id, target.clone()) + .await?; + self.app_event_tx + .send(AppEvent::ReviewStoryReady(response.snapshot)); + Ok(true) + } AppCommand::CleanBackgroundTerminals => { app_server .thread_background_terminals_clean(thread_id) diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 2ecf17f5a3..50c2d4fb35 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -105,6 +105,9 @@ pub(crate) enum AppCommand { Review { target: ReviewTarget, }, + ReviewStory { + target: ReviewTarget, + }, ApproveGuardianDeniedAction { event: GuardianAssessmentEvent, }, @@ -272,6 +275,10 @@ impl AppCommand { Self::Review { target } } + pub(crate) fn review_story(target: ReviewTarget) -> Self { + Self::ReviewStory { target } + } + pub(crate) fn approve_guardian_denied_action(event: GuardianAssessmentEvent) -> Self { Self::ApproveGuardianDeniedAction { event } } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 17b749971a..d115810aa6 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -24,6 +24,7 @@ use codex_app_server_protocol::PluginReadParams; use codex_app_server_protocol::PluginReadResponse; use codex_app_server_protocol::PluginUninstallResponse; use codex_app_server_protocol::RateLimitSnapshot; +use codex_app_server_protocol::ReviewStorySnapshot; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::ThreadGoalStatus; use codex_file_search::FileMatch; @@ -885,13 +886,19 @@ pub(crate) enum AppEvent { /// Open the branch picker option from the review popup. OpenReviewBranchPicker(PathBuf), + OpenReviewStoryPopup, + OpenReviewStoryBranchPicker(PathBuf), /// Open the commit picker option from the review popup. OpenReviewCommitPicker(PathBuf), + OpenReviewStoryCommitPicker(PathBuf), /// Open the custom prompt option from the review popup. OpenReviewCustomPrompt, + /// Open the review story overlay for a freshly generated snapshot. + ReviewStoryReady(ReviewStorySnapshot), + /// Submit a user message with an explicit collaboration mask. SubmitUserMessageWithMode { text: String, diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs index 5b8da9ec09..d26dd34454 100644 --- a/codex-rs/tui/src/app_event_sender.rs +++ b/codex-rs/tui/src/app_event_sender.rs @@ -59,6 +59,10 @@ impl AppEventSender { self.send(AppEvent::CodexOp(AppCommand::review(target))); } + pub(crate) fn review_story(&self, target: ReviewTarget) { + self.send(AppEvent::CodexOp(AppCommand::review_story(target))); + } + pub(crate) fn list_skills(&self, cwds: Vec, force_reload: bool) { self.send(AppEvent::CodexOp(AppCommand::list_skills( cwds, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 59d56ca56e..bb3f400ad8 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -40,6 +40,8 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewDelivery; use codex_app_server_protocol::ReviewStartParams; use codex_app_server_protocol::ReviewStartResponse; +use codex_app_server_protocol::ReviewStoryStartParams; +use codex_app_server_protocol::ReviewStoryStartResponse; use codex_app_server_protocol::ReviewTarget; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; @@ -978,6 +980,24 @@ impl AppServerSession { .wrap_err("review/start failed in TUI") } + pub(crate) async fn review_story_start( + &mut self, + thread_id: ThreadId, + target: ReviewTarget, + ) -> Result { + let request_id = self.next_request_id(); + self.client + .request_typed(ClientRequest::ReviewStoryStart { + request_id, + params: ReviewStoryStartParams { + thread_id: thread_id.to_string(), + target, + }, + }) + .await + .wrap_err("reviewStory/start failed in TUI") + } + pub(crate) async fn skills_list( &mut self, params: SkillsListParams, diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index b3b58cbc82..20984dece9 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -219,6 +219,7 @@ impl ChatWidget { | ServerNotification::AccountUpdated(_) | ServerNotification::AccountRateLimitsUpdated(_) | ServerNotification::ThreadStarted(_) + | ServerNotification::ReviewStorySnapshotUpdated(_) | ServerNotification::ThreadStatusChanged(_) | ServerNotification::ThreadArchived(_) | ServerNotification::ThreadUnarchived(_) diff --git a/codex-rs/tui/src/chatwidget/review_popups.rs b/codex-rs/tui/src/chatwidget/review_popups.rs index b069649ef3..b894bb483b 100644 --- a/codex-rs/tui/src/chatwidget/review_popups.rs +++ b/codex-rs/tui/src/chatwidget/review_popups.rs @@ -6,6 +6,17 @@ impl ChatWidget { pub(crate) fn open_review_popup(&mut self) { let mut items: Vec = Vec::new(); + items.push(SelectionItem { + name: "Create review story".to_string(), + description: Some("Step-by-step walkthrough".into()), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::OpenReviewStoryPopup); + })], + dismiss_on_select: false, + dismiss_parent_on_child_accept: true, + ..Default::default() + }); + items.push(SelectionItem { name: "Review against a base branch".to_string(), description: Some("(PR Style)".into()), @@ -60,6 +71,53 @@ impl ChatWidget { }); } + pub(crate) fn open_review_story_popup(&mut self) { + let mut items: Vec = Vec::new(); + + items.push(SelectionItem { + name: "Story against a base branch".to_string(), + description: Some("(PR Style)".into()), + actions: vec![Box::new({ + let cwd = self.config.cwd.to_path_buf(); + move |tx| { + tx.send(AppEvent::OpenReviewStoryBranchPicker(cwd.clone())); + } + })], + dismiss_on_select: false, + dismiss_parent_on_child_accept: true, + ..Default::default() + }); + + items.push(SelectionItem { + name: "Story for uncommitted changes".to_string(), + actions: vec![Box::new(move |tx: &AppEventSender| { + tx.review_story(ReviewTarget::UncommittedChanges); + })], + dismiss_on_select: true, + ..Default::default() + }); + + items.push(SelectionItem { + name: "Story for a commit".to_string(), + actions: vec![Box::new({ + let cwd = self.config.cwd.to_path_buf(); + move |tx| { + tx.send(AppEvent::OpenReviewStoryCommitPicker(cwd.clone())); + } + })], + dismiss_on_select: false, + dismiss_parent_on_child_accept: true, + ..Default::default() + }); + + self.bottom_pane.show_selection_view(SelectionViewParams { + title: Some("Select a review story source".into()), + footer_hint: Some(standard_popup_hint_line()), + items, + ..Default::default() + }); + } + pub(crate) async fn show_review_branch_picker(&mut self, cwd: &Path) { let branches = local_git_branches(cwd).await; let current_branch = current_branch_name(cwd) @@ -92,6 +150,38 @@ impl ChatWidget { }); } + pub(crate) async fn show_review_story_branch_picker(&mut self, cwd: &Path) { + let branches = local_git_branches(cwd).await; + let current_branch = current_branch_name(cwd) + .await + .unwrap_or_else(|| "(detached HEAD)".to_string()); + let mut items: Vec = Vec::with_capacity(branches.len()); + + for option in branches { + let branch = option.clone(); + items.push(SelectionItem { + name: format!("{current_branch} -> {branch}"), + actions: vec![Box::new(move |tx3: &AppEventSender| { + tx3.review_story(ReviewTarget::BaseBranch { + branch: branch.clone(), + }); + })], + dismiss_on_select: true, + search_value: Some(option), + ..Default::default() + }); + } + + self.bottom_pane.show_selection_view(SelectionViewParams { + title: Some("Select a base branch".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items, + is_searchable: true, + search_placeholder: Some("Type to search branches".to_string()), + ..Default::default() + }); + } + pub(crate) async fn show_review_commit_picker(&mut self, cwd: &Path) { let commits = recent_commits(cwd, /*limit*/ 100).await; @@ -125,6 +215,39 @@ impl ChatWidget { }); } + pub(crate) async fn show_review_story_commit_picker(&mut self, cwd: &Path) { + let commits = recent_commits(cwd, /*limit*/ 100).await; + + let mut items: Vec = Vec::with_capacity(commits.len()); + for entry in commits { + let subject = entry.subject.clone(); + let sha = entry.sha.clone(); + let search_val = format!("{subject} {sha}"); + + items.push(SelectionItem { + name: subject.clone(), + actions: vec![Box::new(move |tx3: &AppEventSender| { + tx3.review_story(ReviewTarget::Commit { + sha: sha.clone(), + title: Some(subject.clone()), + }); + })], + dismiss_on_select: true, + search_value: Some(search_val), + ..Default::default() + }); + } + + self.bottom_pane.show_selection_view(SelectionViewParams { + title: Some("Select a commit for the story".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items, + is_searchable: true, + search_placeholder: Some("Type to search commits".to_string()), + ..Default::default() + }); + } + pub(crate) fn show_review_custom_prompt(&mut self) { let tx = self.app_event_tx.clone(); let view = CustomPromptView::new( diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index b479718304..c6161827b7 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -195,6 +195,9 @@ impl ChatWidget { SlashCommand::Review => { self.open_review_popup(); } + SlashCommand::Story => { + self.open_review_story_popup(); + } SlashCommand::Rename => { self.session_telemetry .counter("codex.thread.rename", /*inc*/ 1, &[]); @@ -955,6 +958,7 @@ impl ChatWidget { | SlashCommand::Init | SlashCommand::Compact | SlashCommand::Review + | SlashCommand::Story | SlashCommand::Model | SlashCommand::Realtime | SlashCommand::Settings diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index 48bc341f6b..0117349026 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -1038,7 +1038,8 @@ async fn review_popup_custom_prompt_action_sends_event() { // Open the preset selection popup chat.open_review_popup(); - // Move selection down to the fourth item: "Custom review instructions" + // Move selection down to the fifth item: "Custom review instructions" + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 4a43409296..edd7c2d358 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -28,6 +28,7 @@ pub enum SlashCommand { Skills, Hooks, Review, + Story, Rename, New, Resume, @@ -84,6 +85,7 @@ impl SlashCommand { SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", SlashCommand::Review => "review my current changes and find issues", + SlashCommand::Story => "explain changes as a step-by-step review story", SlashCommand::Rename => "rename the current thread", SlashCommand::Resume => "resume a saved chat", SlashCommand::Clear => "clear the terminal and start a new chat", @@ -148,6 +150,7 @@ impl SlashCommand { matches!( self, SlashCommand::Review + | SlashCommand::Story | SlashCommand::Rename | SlashCommand::Plan | SlashCommand::Goal @@ -194,6 +197,7 @@ impl SlashCommand { | SlashCommand::Experimental | SlashCommand::Memories | SlashCommand::Review + | SlashCommand::Story | SlashCommand::Plan | SlashCommand::Clear | SlashCommand::Logout diff --git a/docs/adr/0001-review-story-v1.md b/docs/adr/0001-review-story-v1.md new file mode 100644 index 0000000000..c5a66972a9 --- /dev/null +++ b/docs/adr/0001-review-story-v1.md @@ -0,0 +1,42 @@ +(eval):5: parse error near `end' +# Review Story v1 + +Status: accepted + +Codex will introduce a Review Story workflow that explains a concrete code change as an ordered, anchored story for reviewers. The story is a navigation and understanding artifact, distinct from findings-oriented code review, and v1 will expose it through app-server v2 plus a TUI surface. + +## Context + +Large PRs are hard to review when the reviewer only sees an alphabetical file list or a flat diff. Recent review tools, especially CodeRabbit Change Stack and PR Walkthroughs, show that reviewers benefit from a logical reading order, grouped explanations, and range-specific summaries. Codex already has `/review`, but that flow is optimized for finding issues and producing verdicts, not for helping a human understand the shape of a change step by step. + +## Decision + +Review Story v1 will use a shared app-server v2 API namespace, `reviewStory/*`, backed by a reusable Rust implementation and a separate SQLite story database managed by the state layer. The TUI will be the first Story Surface and will provide a read-only Story Overlay; the App UI can later consume the same API and snapshot model. + +A Review Story is generated from a Concrete Story Source: branch comparison, uncommitted changes, or a single commit. Custom review instructions are out of scope for v1 unless they resolve to a concrete source. Each Story Source receives a deterministic Source Fingerprint so saved Story Snapshots can be marked stale instead of silently rewritten. + +Story generation is progressive. Codex first builds a deterministic Evidence Graph with stable Anchor Ids for files and hunks. A model then creates a strict JSON outline that orders Story Steps using only those Anchor Ids. After the outline is validated, Codex exposes a Progressive Story Snapshot and enriches steps in small batches with bounded parallelism. Story Surfaces receive full Snapshot Updates and replace their local snapshot with the newest version. + +Story Snapshots are persisted in a Story Store, not embedded wholesale in thread history. Thread history records lifecycle events that point to snapshot ids. Refreshing a stale story creates a new snapshot linked to the previous one, preserving Snapshot Lineage. + +## Considered Options + +- Reuse `/review`: rejected because findings and correctness verdicts should remain separate from explanatory navigation. +- Store snapshots only in thread history: rejected because snapshots are structured, mutable during generation, and shared by multiple surfaces. +- Generate the whole story in one model call: rejected because users should be able to navigate once a validated outline exists. +- Let the model invent file paths and line ranges: rejected because Change Anchors must be system-validated. +- Fully normalize the story database schema in v1: rejected because the API usually reads full snapshots, while indexed lookup/status fields are enough initially. + +## V1 Shape + +The initial `StorySnapshot` should expose snapshot identity, thread id, source, source fingerprint, status, timestamps, optional previous snapshot id, title, overview, steps, anchors, and staleness. + +Each `StoryStep` should expose a stable id, display index, title, goal, summary, dependency rationale, anchor ids, review focus, readiness, and optional error. Step readiness can be `outline`, `enriching`, `ready`, or `failed`. + +The Story Overlay should show an ordered step list, a diff view filtered to the selected step's Change Anchors, and the selected step's goal, summary, dependency rationale, and Review Focus. V1 is read-only: no GitHub comments or review submission from the overlay. + +## Consequences + +This design gives reviewers fast, trustworthy navigation without turning Review Story into another finding engine. It also creates durable product boundaries: app-server v2 owns the contract, the Story Database owns structured persistence, and Story Surfaces render snapshots without re-deriving the diff story themselves. + +The trade-off is more infrastructure in v1: a story database, source fingerprints, snapshot lifecycle notifications, and structured model schemas are required before the feature feels complete. The payoff is that TUI and App UI can share one artifact, and future features such as comments, diagrams, snapshot comparison, or findings attached to Story Steps can be added without replacing the core model.