Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,8 @@ pub fn closeDocumentResources(app: *App, doc: sdk.DocHandle) void {
if (app.tracks) |*t| t.closed(doc.id);
// Whoever held it is holding nothing now (`sdk.Claim`).
app.host.releaseDocumentClaims(doc.id);
// Nor are its steps anyone's now (`Host.stepAuthor`).
app.host.forgetDocumentAuthors(doc.id);
_ = doc.owner.closeDocument(doc);
doc.owner.unregisterDocument(doc.id);
}
Expand Down
15 changes: 12 additions & 3 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,16 +667,25 @@ gets the same, and `DropZones.wheel`/`at`/`draw` give a plugin the same drop for
document open just is not snapshotted, and seeks through it replay from the keyframe. `text`
implements all three.
- **Steps** (optional) — what a document's history records and replays (`sdk.Step`,
`plans/RECORDING_PLAN.md`). While `host.recording(doc.id)` says so, the owner reports each
change as it happens with `host.recordStep(doc.id, step)`: a new history entry (`added`), more of
`plans/RECORDING_PLAN.md`). The owner reports each change as it happens with
`host.recordStep(doc.id, step)`, and encodes its `effect` only while `host.recording(doc.id)`
says so: the host keeps who made every step (below), recorded or not. Each is a new history entry (`added`), more of
the entry on top (`joined`, a run of typing), an entry taken back (`undone`, its effect the
reverse) or put back (`redone`), and the history starting over (`restarted`, a reload). Every
step's `effect` applies **forward**, in the owner's own encoding, numbered by the plugin's
`step_format` (bump it when an older build would misread a step); `motion` is how it was made,
when the owner knows. `applyStep(doc, effect, motion, ms)` lands one as a new history entry,
shown being made over `ms` (0: at once), and refuses one that does not fit what the document
holds, leaving it as it was. Nobody recording, nothing is encoded. `text` implements both:
each step is a transaction's edits, removed and inserted bytes, and the selection after.
each step is a transaction's edits, removed and inserted bytes, and the selection after. Fill
`top` (the entry on top once the step is done) so Undo can say whose step it takes back.
- **Authors** — who made each step (`sdk.Author`: the person, an agent, a tape;
`plans/SHARED_HISTORY_PLAN.md`). The host keeps it by step id; owners never store one. A plugin
changing a document on someone's behalf acts as them around the commands it runs
(`const outer = host.act(author); defer host.endAct(outer);`), and work it puts off stores
`host.acted()` and runs inside `host.actAgain(stored)`. An owner whose edits join a run (typing)
asks `host.mayMerge(doc.id, top_step)` and starts a new step when it says no, so one hand's
edits never fold into another's. `host.stepAuthor` and `host.topStep` say who made a step.
- **Rendering** — `drawDocument(doc)` (the document's content, called through the document's
surface wherever a region accepting `document` shows it), `infobarEntries(active_doc)` (icon
+ text chips Fizzy draws in the infobar; plugins do not draw into the bar).
Expand Down
38 changes: 38 additions & 0 deletions sdk/src/Author.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Who made a change to a document: the person, an agent, a tape, or nobody known
//! (`plans/SHARED_HISTORY_PLAN.md`). The host keeps a step's author by the step's id
//! (`Host.recordStep`); owners never store one.
const std = @import("std");
const Author = @This();

kind: Kind,
/// Which one: the plugin acting, and its own id for the session or connection (the agent
/// plugin's connection, chat's session). Two agents both called "Claude" are two authors.
plugin: []const u8 = "",
session: u64 = 0,
/// As the person reads it: "Claude", "the tour". Display only, never identity.
label: []const u8 = "",

pub const Kind = enum {
/// Whoever is at the keyboard and pointer. Their steps are never stored: a step with no
/// author kept is theirs.
person,
agent,
tape,
/// A step recorded where nobody acts: a missed site, never quietly the person's.
unattributed,
};

pub const person: Author = .{ .kind = .person };
pub const unattributed: Author = .{ .kind = .unattributed };

/// The same author: kind, plugin and session. The label is not identity.
pub fn same(a: Author, b: Author) bool {
return a.kind == b.kind and a.session == b.session and std.mem.eql(u8, a.plugin, b.plugin);
}

test "an author is its kind, plugin and session, not its label" {
const a: Author = .{ .kind = .agent, .plugin = "agent", .session = 1, .label = "Claude" };
try std.testing.expect(a.same(.{ .kind = .agent, .plugin = "agent", .session = 1, .label = "An agent" }));
try std.testing.expect(!a.same(.{ .kind = .agent, .plugin = "agent", .session = 2, .label = "Claude" }));
try std.testing.expect(!a.same(person));
}
Loading
Loading