Skip to content

Changesets ​

A changeset is a named, individually subscribable view of file changes associated with a session or one of its chats. Changesets generalise the v0.1.0 SessionSummary.diffs field: a session can expose any number of changesets — uncommitted working-tree edits, the diff between two turns, the cumulative changes for the whole session, the staged index, etc. — each with its own URI, lifecycle, and update stream. A chat can additionally advertise its own Branch and Uncommitted Changes views, scoped to that chat's effective working directories.

Concepts ​

Changeset Catalogue ​

Each session's SessionState can advertise session-level changesets, and each subscribed ChatState can advertise changesets for that chat. The catalogue entry is intentionally lightweight — just enough to render a chip or list row without subscribing — and references a full subscribable ChangesetState by URI. ChatSummary does not carry this catalogue; clients that need per-chat changesets subscribe to that chat.

typescript
SessionState {
  // ...existing fields...
  changesets?: Changeset[]
}

ChatState {
  // ...existing fields...
  changesets?: Changeset[]
}

Changeset {
  /** Human-readable label, e.g. `"Uncommitted Changes"`. */
  label: string
  /** RFC 6570 URI template; expand to obtain a subscribable URI. */
  uriTemplate: string
  description?: string
  /**
   * Advisory hint: one of `'session'`, `'branch'`, `'uncommitted'`,
   * `'turn'`, or `'compare-turns'`. Other values allowed.
   */
  changeKind: string
  /** Optional capability declarations (presence-flag objects). */
  capabilities?: {
    /** Present ⇒ this changeset supports the per-file review workflow. */
    review?: {}
  }
}

URI Templates and Variables ​

uriTemplate is an RFC 6570 URI template. Clients expand it with concrete values to obtain a subscribable changeset URI. Only the following variable names are defined by this protocol; clients SHOULD ignore templates containing unknown variables.

Variables in templateMeaning
(none)A static changeset scoped to the advertising session or chat. The template is itself a subscribable URI.
{turnId}Per-turn slice. Expand with a Turn.id from the advertising chat or session.
{originalTurnId} and {modifiedTurnId}Diff between two turns. Both must be present.

Multiroot Sessions ​

A changeset is not scoped to a single working directory — a per-turn or session-wide changeset naturally spans every directory the agent touched. A client that wants to present changes grouped by directory does so itself, by matching each file's URI against the session's workingDirectories (a list the client already has); a client that does not care simply renders one tree.

A host MAY also advertise dedicated per-directory changesets — one catalogue entry per working directory — for clients that prefer server-scoped views. This needs no extra field: the changesets catalogue is already a list, so a host lists one entry per directory alongside the spanning ones.

For a chat catalogue, "session-wide" above means the chat's effective working directory set: ChatState.workingDirectories when present, otherwise the owning session's full workingDirectories. A host SHOULD scope each advertised chat changeset to that set. This allows chats backed by different repositories or worktrees to expose independent Branch and Uncommitted Changes entries while reusing the same changeset state and action contract.

Changeset State ​

Each concrete (expanded) changeset URI is its own subscribable resource.

typescript
ChangesetState {
  status: 'computing' | 'recomputing' | 'ready' | 'error'
  error?: ErrorInfo
  files: ChangesetFile[]
  operations?: ChangesetOperation[]
}

ChangesetFile {
  id: string                               // typically `after.uri` (or `before.uri` for deletions)
  edit: FileEdit                           // reuses the existing FileEdit shape
  reviewed?: boolean                       // GitHub-style "Viewed" flag; absent ⇒ not reviewed
  _meta?: Record<string, unknown>
}

computing means the host is producing the first result and no completed result is available yet. recomputing means the host is refreshing an existing result; while it does so, files remains the previous completed result, including when that result is an empty array. This lets clients distinguish an initial empty placeholder from a cached empty result without inspecting the array.

Updates flow through changeset-scoped actions, broadcast to subscribers of the changeset URI:

TypeClient-dispatchable?When
changeset/statusChangedNostatus transitioned (e.g. computing → ready or ready → recomputing).
changeset/fileSetNoUpsert a ChangesetFile (new or replacing existing by id).
changeset/fileRemovedNoA file is no longer in the changeset.
changeset/filesReviewChangedYesA reviewer toggled the reviewed flag on one or more files.
changeset/contentChangedNoFull replacement of files, optionally with operations.
changeset/operationsChangedNoThe set of available operations changed.
changeset/operationStatusChangedNoA single operation's status transitioned (e.g. idle → running → error).
changeset/clearedNoAll files dropped (e.g. branch switched, or the owning session ended).

Typed file-edit models ​

The client API uses three shared model types:

TypePurpose
FileEditSideA file URI and its required ContentRef.
FileEditDiffStatsOptional added and removed item counts.
FileEditCollectionThe preview wrapper with required items.

FileEdit.before and after use the same side type. ToolResultFileEditContent exposes the same fields. The ready action and pending-confirmation state both expose previews through edits.items.

When migrating from a client with raw-JSON file-edit properties, replace JSON lookups and construction with these typed properties and constructors. This changes the affected SDK APIs, not the JSON structure. Kotlin consumers must also rebuild dependent binaries.

Sides remain optional. In Kotlin, a present side requires its file URI and content reference. The existing serializer reports missing required fields and incompatible values. A file that causes a serialization error fails the containing payload. Callers must handle that error rather than apply a partial snapshot.

Each SDK keeps its native validation rules. Go can use zero values for missing fields. TypeScript types do not perform runtime validation.

Runtime model decoders accept unknown keys but do not retain them on recognized objects. Keep the original raw payload separately if forwarding must be lossless. Intentional extension fields and unknown variants keep their existing raw-data behavior.

The statistics count items, such as text lines or notebook cells. They are not patch data. ContentRef retains its existing URI, size hint, content type, and nonce. This API change does not add diff computation or a new resource protocol.

File Review ​

Review is a capability of the changeset. A changeset advertises support for the review workflow on its catalogue Changeset entry via capabilities.review (a presence-flag object). Clients see this up-front on the session's changeset list, so they can decide whether to surface review UI without first subscribing. When the capability is absent, the changeset is not reviewable.

For a reviewable changeset, each ChangesetFile carries an optional reviewed flag — the equivalent of GitHub's per-file "Viewed" checkbox. A missing value is treated as not reviewed.

Unlike the rest of the changeset/* family, the changeset/filesReviewChanged action is client-dispatchable: a reviewer toggles files' review state directly, applying it optimistically through the write-ahead reducer and letting the server echo it back on the normal action envelope stream. The server MAY also originate it (e.g. an agent marking its own output reviewed). The action is batched — it carries a list of file ids that all move to the same reviewed value.

typescript
// dispatched by a client (or the server)
{
  type: 'changeset/filesReviewChanged'
  files: string[]       // ChangesetFile.id values
  reviewed: boolean     // true marks the files reviewed, false clears them
}

The reducer sets reviewed on every listed file that is present in the changeset, leaving each file's edit and _meta untouched. Ids that don't match a current file are ignored; the action is a no-op when none match.

Reset on edit. The protocol has no per-file content version, so review is not reset automatically when a file's contents change under a stable id. The server, which is the authority on what changed, resets review explicitly — either by re-emitting the file (via changeset/fileSet or changeset/contentChanged) without reviewed: true, or by dispatching changeset/filesReviewChanged with reviewed: false.

Changeset Operations ​

A changeset operation is a server-declared invokable verb the client can run against a changeset, a file, or a range — "revert", and similar file-level actions. Richer SCM workflows such as staging changes or creating pull requests are better expressed as dedicated commands or skill buttons rather than changeset operations.

typescript
ChangesetOperation {
  id: string
  label: string
  description?: string
  scopes: ChangesetOperationScope[]   // 'changeset' | 'resource' | 'range'
  /**
   * When set, the client should prompt the user for confirmation before
   * invoking the operation, using this text as the prompt body.
   */
  confirmation?: StringOrMarkdown
  icon?: string
  /**
   * Execution status of the operation. The server sets `'running'` while
   * an invocation is in flight, `'error'` (with `error`) when the most
   * recent invocation failed, `'disabled'` when the operation cannot
   * currently be invoked, and `'idle'` otherwise.
   */
  status: 'idle' | 'running' | 'error' | 'disabled'
  /** Present iff `status === 'error'`. */
  error?: ErrorInfo
}

Because invokeChangesetOperation is a request/response command, an operation's progress and outcome are reflected back into changeset state via the changeset/operationStatusChanged action so that every subscriber observes a consistent view (e.g. a spinner on a "Create Pull Request" button, or an inline error after a failed "revert"). The action targets a single operation by operationId and is a no-op if no operation with that id is currently present.

Operations are invoked via the invokeChangesetOperation JSON-RPC command (not via dispatched actions, because they return data and may fail per-call). State changes resulting from the operation flow back through the normal changeset/* action stream.

typescript
invokeChangesetOperation(params: {
  channel: URI
  operationId: string
  target?:
    | { kind: ChangesetOperationTargetKind.Resource; resource: URI; side?: 'before' | 'after' }
    | { kind: ChangesetOperationTargetKind.Range; resource: URI; side?: 'before' | 'after'; range: TextRange }
}) → {
  message?: StringOrMarkdown
  followUp?: {
    content: ContentRef
    /** When true, open in an external handler (e.g. browser) rather than inline. */
    external?: boolean
  }
}

The server validates that operationId exists in the changeset's current operations list and that the requested target's kind is contained in the operation's scopes. Invalid combinations result in a JSON-RPC error.

Lifecycle ​

  1. The server publishes a catalogue on SessionState.changesets and/or ChatState.changesets. Updates ride on session/changesetsChanged or chat/changesetsChanged, respectively.
  2. The client picks catalogue entries whose template variables it can satisfy and subscribes to the resulting URIs.
  3. The server returns a ChangesetState snapshot (status: 'computing' is allowed if the initial scan is async). For a later refresh, the server transitions to recomputing and keeps the previous completed files until the replacement is available. It can push changeset/contentChanged for a batched file snapshot, optionally including operations or error details, followed by narrower changeset/* actions as files or operations change.
  4. The user invokes a ChangesetOperation. The client calls invokeChangesetOperation. The server applies the operation and emits any resulting changeset updates.
  5. When a chat ends, its chat-scoped changesets implicitly become un-subscribable. When a session ends, all remaining changesets implicitly become un-subscribable. Existing subscriptions receive changeset/cleared and the server unsubscribes them.

Migration from v0.1.0 ​

The summary.diffs field and the session/diffsChanged action were removed in v0.2.0. Servers that previously populated summary.diffs should expose an equivalent server-side changeset with a static uriTemplate ending in /changeset/session and surface its aggregate counts on the new summary.changes field. Clients that want a single "session-wide" diff view subscribe to that one changeset URI.

Released under the MIT License.