Skip to content

Agent chat - #52

Draft
paulsonnentag wants to merge 51 commits into
mainfrom
agent-chat
Draft

paulsonnentag wants to merge 51 commits into
mainfrom
agent-chat

Conversation

@paulsonnentag

Copy link
Copy Markdown
Contributor

No description provided.

Provenance generalizes comments: entries stored in a generated doc's
@provenance section link its elements (targets) back to ranges in the
docs they came from (sources), always as automerge urls. The provider
indexes every doc mounted inside it and answers patchwork:provenance
subscriptions scoped by either end, so a source text doc can see inbound
links it doesn't store. codemirror-base renders provenance source ranges
(dotted link-accent tint) and pushes their targets into the shared focus
selection when the cursor lands on them.
Add an unfoldable debug section showing installed/loaded/active llm:skills.
Let the computer activate skills mid-run: a load_skill tool, and
auto-activation when read_doc returns a document matching an inactive
skill's datatypes (instructions delivered inline with the read result).
codemirror-base no longer knows about provenance: `codemirror:extension`
modules may now be factories receiving {handle, element, repo}, and the
corkboard registers one that decorates provenance source ranges and pushes
their linked targets into the shared focus selection.
The model can now mint edit-stable refs — an element by path/{id} matcher,
or a text range by cursor-anchored offsets — so provenance entries can point
at specific passages and elements instead of whole documents. Bumps the
debug panel version to v0.0.2.
…rgets

Lets a selection on a Petrinaut canvas light up the source text range,
not just the other way round.
Provenance entries moved under the document's @Patchwork envelope
(a plain array, no `entries` wrapper) — the provider indexes and
ref-urls them from the new path.
@patchcrow

patchcrow commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

✅ Patchwork glance ready

https://patchwork-preview-52--patchwork-base.netlify.app

Patchwork from inkandswitch/patchwork.inkandswitch.com, packages from this PR at 6694943.

build-info.json

paulsonnentag and others added 18 commits August 26, 2026 17:02
The agent's computer gets a start_scenario tool: called before each
alternative's edits, it forks a fresh draft off main and re-aims the
run's doc resolution at it, so one sequential run can produce several
independent scenarios. Runs that opened scenarios end with a picker
embed instead of the single accept/reject — chips check each branch out
(with diff baselines) for browsing back and forth, accept merges the
selected scenario and unlinks the rest, reject-all discards everything.
On pnpm >= 11 a workspace-less `pnpm -r` recurses into every directory
with a package.json, including the aggregated copies in
static-dist/packages/. Those keep their dev/build scripts but have no
sources or node_modules, so they all fail and their exit code tears
down the whole watch.
- add get datatypes tool and skill
- add create embed of datatype to existing corkboard skill
Record merge provenance on the DraftDoc (mergedFrom per clone, mergedInto)
and walk the change DAG between fork and merge heads to pull a merged
draft's changes out of the time-based grouping into one labelled group.
Scrub boundaries now resolve from the rendered row order instead of raw
timestamps (merged changes interleave in time), and the baseline handle
renders as the boundary below its change so a selected group reads as a
full band. Merging adopts members the target never forked, keeping their
changes scoped to the target draft.
Mirror the drafts sidebar's updated mergeDraft in mergeAgentDraft: adopt
members the merge target never forked, and record mergedFrom per clone and
mergedInto on the draft, so chat-accepted drafts show up as attributed
"Merged" groups in the drafts timeline.
Fill the FIRST unfilled tool-call card — searching from the end paired a
multi-call round's results with the wrong cards. Inactivity timeout up to
5 minutes: a model streaming a huge tool call looks silent to the llm lib.
SkillsDebug reads the shared CHAT_VERSION instead of its own constant.
Change groups split at comment timestamps so each comment (including
replies and resolved threads) gets its own row. Clicking a comment pins
the draft to its state at that time, switches the context sidebar to the
comments tab via a new patchwork:open-context-tool event, and asks the
comments view to select and scroll to the thread through an openThread
field on the shared patchwork:focus doc. Comment writes no longer count
as edits, and the idle scrubber line sits above comments newer than the
latest change.
A thread whose target was not a bare document url was assumed to be a
cursor-anchored range, and dropped when it had no range to measure. That hid
every comment anchored to a whole value — an entity in a list, a field — so
Petrinaut's per-place comments were written, indexed, and never listed.

Threads are now sorted by what their targets resolve to rather than by what
their urls look like: an unranged target is visible while it resolves, and
fails the same way a ranged one does when the thing it names is deleted.
Unranged threads become primary by being clicked, as document-level ones
already did.

The header carries a build number so a stale panel is visible on screen.
It existed to confirm a push reached the browser; it has, so the header goes
back to just the title.
A draft now carries reviews: each person's approve-or-reject verdict, keyed
by their contact, pinned to the member heads they were shown. Merging needs
at least one approval that still covers what is in the draft — changes
landing afterwards make a verdict stale, and a stale approval no longer
opens the gate. Rejection is advisory: recorded and shown, but not a veto.

Whoever's approval authorised a merge is frozen onto the draft at merge time
and carried through to the merged group, so the timeline row in the target
says who signed it off.
The empty state's greeting spoke for the user before they had said
anything; an empty chat now says nothing, with the input as the invitation.

The skills panel was a fold-out for reading how llm:skill activation was
behaving, which it no longer needs to be looked at for. Its two
read-only accessors on llm-skills existed only to feed it and go with it.
Chat derives every surface from the editor tokens but was taking its ink
from the chrome ones. The theme mixes each accent toward the line of the
surface it will be read on, so --studio-primary-text and
--editor-primary-text are different colours the moment a theme gives the
editor its own fill — which is how an accent ends up unreadable on the
background chat actually paints. Accent, link, danger and the three text
weights now come from the editor family, falling back to the chrome one.

--accent-line is ink ON the accent fill, but all but three uses were edges
on the ambient surface, where it draws the background colour. Those move to
a new --accent-border.

--border sat on the same offset as --bg-darkest and --bg-hover, so a border
on either was invisible; it moves a step further out. A rejected draft
review had no colour of its own and fell through to muted, so the two
outcomes didn't read as opposites.
+971 / -155 used --studio-added and --studio-deleted directly. Those are
highlight colours, meant to be painted behind text rather than read as it,
so on a light panel the counts came out as near-invisible neon. Drafts only
ever uses them as ink, so they now go through the theme's own convention for
that — the accent mixed 35% toward the surface's line, which lightens
instead on a dark theme. Added to theme.css as --studio-added-text /
--studio-deleted-text alongside the other inks; spelled out in drafts too,
since the theme document is published from elsewhere and won't carry the
new token for a while.

The secondary ink sat at line-offset-50, three quarters of the way into the
fill, which put the timestamps and most of the timeline on the edge of
legibility. It also left "muted" lighter than the "faint" below it; both
now step in the order their names imply.
Approving a change is the same act as commenting on it — you looked, formed
a view, said so — so the Approve/Reject pair moves to the comments panel,
under the header and above the threads it concludes.

comments-view reaches drafts only through the provider channel (draft:list,
draft:checked-out) and the repo, so the bar simply doesn't render when no
drafts tool is answering, or on main. The DraftDoc shape it writes is
restated locally rather than imported; the packages stay standalone.

The drafts sidebar keeps the half that belongs to it: it still reads the
reviews to decide whether merging is allowed, and still names the approvers
on the merged row. It no longer offers a verdict, so its contact
subscription and the icons that went with the bar are gone.
The card had no hierarchy: muted label, muted buttons, muted everything on
a tinted box. Now the draft name is the title under a small REVIEWING
eyebrow, the two verdicts are outlined in their own colour and fill in once
given (the other stepping back to neutral, so the card shows one decision),
and a standing verdict colours the card's edge. Reviewers are an
overlapping avatar row with the verdict badged on each corner.

Add comment leaves the header for its own dashed row below the card: a
comment is the finer-grained thing, said about part of what the verdict is
about. The panel's muted ink steps in from offset-50 to -40 — at three
quarters into the fill the title and every timestamp were barely there.
Comments start from the thing being commented on — a selection in the
editor, a cell in a notebook — not from the panel. The document-level
thread creator and its button existed to cover tools that couldn't anchor a
comment anywhere; that path goes too, since nothing else called it.
The gate got in the way more than it protected anything. Merging is always
available again; approvals are purely a record — whoever's approval stands
at merge time is still frozen onto the draft and named on the merged row,
and an unapproved merge simply says nothing about approvers.
Approving still happens in the comments panel; the draft card now shows
who has approved or rejected the draft as avatar marks beside its name,
faded when the verdict predates the draft's current contents. Member
heads are watched for every listed draft, not just the selected one, so
the marks are right on unselected cards too. Drops the dead review-strip
CSS left from the card-based UI.
A merge that threw halfway left the draft listed and the view where it
was, indistinguishable from a button that did nothing. Report the error
and log each member as it merges.
A group of changes shows in a draft's timeline only when it names who
made it: an actor that resolves to a contact, or an agent tag. Changes
from a device that never signed in are left out rather than shown under
an anonymous avatar. Merged-draft groups always show, and the empty
state says when there are edits but none from a known account.
A merge group renders as one bordered block holding the merge row and,
unfolded, its contributor runs. The chevron moves into the row as a
1.25rem SVG target instead of a glyph hanging in the gutter strip.
The merge row's chevron no longer carries a z-index, so the pinned
sticker paints over it like over the rest of the row. Rows without a
chevron indent by its width so every avatar lines up.
It holds the approve/reject verdict as well as the comments now; the tab
and header say so. The plugin id stays comments-view, which is what the
drafts sidebar opens it by.
Resting the pointer on a contact avatar — the inline one in history rows,
review marks and comments, or the default-size one — floats a card with
their picture at 64px and their full name. The card lives on <body>,
placed against the viewport so no scrolling ancestor clips it, and takes
no pointer events. The inline avatar's title tooltip goes, since the card
now says the same thing.
A member whose clone or target the repo can't produce — a doc that only
ever lived in another browser's storage, or that the sync server never
saw — is skipped and reported instead of aborting the merge. Members load
16 at a time with a 20s bound each, since a draft can carry well over a
thousand of them. The merge reports how many were left out.
The count goes to the console instead of a dialog: with the dead
infrastructure clones drafts currently accumulate, it would fire on
every merge.
The clone policy turns positive: a doc is draft content when it carries
a @patchwork.type that isn't on the skip-list, which now also names
patchwork:tool-storage. Untyped docs — the per-session focus doc, the
checked-out-draft doc, read positions, raw blobs — are no longer forked,
no longer reported as members, and no longer get a main draft stamped on
them when a provider mounts over one.

Opening a draft prunes its clone map once per session: entries that are
not content under the policy, and clones the connected sync server
reports unavailable (never on a timeout, never offline, never merged
entries). Member resolution for scrubbing and review staleness runs
concurrently with one summary warning instead of one per dead member.
The chat's accept path is its own copy of the sidebar's merge, and it
still stopped at the first clone the sync server couldn't find. It now
loads every clone up front, concurrently, and merges the ones that
loaded, logging one summary line for the rest.

The agent's own resolution (resolveInDraft) follows the same positive
clone policy as the sidebar: only typed docs off the skip-list get a
per-draft clone; untyped session docs and tool-storage pass through.
The actor-attribution entry now stores the contact url pinned to the
contact doc's heads at the moment the actor was first attributed
(automerge:<id>#h1|h2). The sidebar hands that url straight to the
contact-inline view, which renders the doc at those heads: the name and
picture of the time, hover card included. Identity comparisons (group
splitting, avatar dedupe) strip the pin via canonicalUrl so one person
across sessions still reads as one contributor. Bare pre-pinning entries
keep rendering the live contact.
… target

History comments are now identified by thread id + comment id, not by
the member they were read through, so a thread reachable via two members
after a merge renders once.

A thread present at a merge group's mergeHeads and absent at its
baseHeads was born on that branch; the timeline lists such comments
inside the unfolded merge block, interleaved with the contributor runs,
instead of at the top level of the target's history. The branch's own
timeline is unchanged.

Both merge paths (sidebar and chat accept) mark branch-born threads
resolved on the target after target.merge(clone). The clone is untouched,
so the branch checked out again still shows them open.
A user message matching "minimum order(s)" or "partial fulfilment" on a
petrinaut-petrinet document gets a pre-written turn instead of a model
call: a plan sentence, the tool calls (run through runToolByName, so the
edits are real and land in the agent's draft), then a closing summary.
Prose streams a word at a time with a lead-in and sentence pauses.

The two runs edit disjoint fields — run 1 splices the two small-order
arrival kernels, run 2 appends a partial-fulfilment transition; both
only append to parameters — so they can be made on separate drafts and
merged either way round.
The scripted partial-fulfilment run now adds five parameters, a
PartialShipmentCosts place, two transitions (partial fulfilment and
finished-goods consolidation), four metrics, and edits the existing net:
the full-fulfilment transition is renamed, prioritises tagged remainders
and records sales under the plain segment; spot cancellation softens
once part of the order has shipped; the orders-fulfilled and lead-time
metrics count only the closing delivery; policy cost includes the new
place. Still disjoint from the minimum-orders run, which only touches the
two small-order arrival kernels.
A merge forces a full regrouping of the target's timeline so the merged
changes come out of the time-based groups and into one merge block. That
pass resolved every member one at a time with no bound, so a timeline
carrying dead members stalled on them — the old groups stayed on screen
under a permanent "Building history…", and the merge block never came.
The grouper now loads members 16 at a time with a 30 s bound and reports
the unloadable ones in one line.

Both merge paths load a member's target before the parent adopts it, so
an original no peer has any more is skipped instead of being written into
main's clone map as a member. The clone-map prune now also covers main.
The overlay logs each clone it creates — the doc's type, the element
whose handle request caused it (tag, tool-id/doc-url, enclosing
patchwork-view), the draft, and a running tally by type — and the
agent's resolveInDraft logs the same with the calling frames. A merge
logs its members tallied by datatype, so a bloated draft says what it is
made of.
The log you pasted showed ~30 members each carrying the same 13 change
hashes: clones of clones. A clone url handed back to the overlay (or to
the agent's resolveInDraft) as if it were an original isn't found in the
clone map — that is keyed by originals — so it got forked again on every
hop. Both fork sites now look the url up by value and, when it is already
this draft's clone, return it as-is and warn with the requester.

Console prefixes are now `[drafts:fork]`, `[agent:fork]`,
`[drafts:merge]`, `[agent:merge]` so `:fork` / `:merge` filter the
trail. The merge summary counts clone-of-clone members and lists them.
The per-change grouper trace that flooded the console is gone; the other
grouper traces are console.debug (Verbose only).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants