Skip to content

feat: Give the AI access to sticky notes - #2766

Open
camielvs wants to merge 5 commits into
masterfrom
ai-sticky-notes
Open

camielvs wants to merge 5 commits into
masterfrom
ai-sticky-notes

Conversation

@camielvs

@camielvs camielvs commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Description

The assistant could not see a single word written on a sticky note, and had no way to
leave one. Notes are the one canvas element whose whole purpose is to carry human
intent — "don't touch this branch", "this threshold came from the Q3 eval" — so it
would restructure a canvas around annotation content someone had placed deliberately,
without ever knowing it was there.

Notes now appear in the pipeline state the assistant reads, with their text, colours,
size, position, lock state and author, at the top level and inside every subgraph. It
can also add, edit and delete them.

A note can be placed anywhere, and the prompt treats that as the normal case. Notes
are freeform, and plenty of them are about no single step — a heading over a region, a
caveat about a whole branch, a reminder parked in empty space off to the side. Explicit
coordinates are the general path and work in any graph, including empty canvas far from
every node. Anchoring a note to a task, input or output is offered as a shortcut for the
narrower case where the note really is about that one thing, so the assistant does not
have to work out coordinates for it — not as the default shape of a note.

Its own notes are authored as AI assistant rather than user, which is both
visible in the details panel and the signal the prompts use: anything the user wrote is
theirs, and it is told not to reword, recolour, move or delete it unless asked. Locked
notes are refused outright. Colours are restricted to the values the picker can produce,
so it cannot write a note that renders as nothing.

Two smaller corrections in the same area. A task the assistant added could land on top
of an existing note, because the placement logic only looked at tasks and ports; it now
accounts for notes. And a sticky note mentioned in chat now renders as a working chip
that navigates to it, like every other entity link.

The run view gains the read but not the writes, so the debug assistant can use a note as
context for a failure while the spec stays read-only there.

Related Issue and Pull requests

Bottom of a five-PR stack closing the assistant's canvas-awareness gaps:
this → canvas layout → update input/output → pipeline notes & tags → task colours.

Type of Change

  • New feature

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Test Instructions

  1. Open a pipeline with a hand-placed sticky note. Ask the chat "what do the notes on
    this canvas say?" — it should quote them, not say it cannot see any.
  2. Ask it to add a note next to a named step. It should land just above that step, with
    Author "AI assistant" in the details panel, and be one Cmd/Ctrl+Z away from gone.
  3. Do the same inside a subgraph, then navigate back up — the note must stay in the
    subgraph only, and not appear at the top level.
  4. Ask it to recolour a note to something off-palette ("make it chartreuse") — it should
    explain and offer the swatches rather than writing an invisible note.
  5. Lock a note, then ask it to edit or delete it — it should refuse and say it is locked.
  6. Ask it to delete a note you wrote — per the prompt it should check with you first.
  7. Click a sticky-note chip in one of its replies — the canvas should navigate to and
    select that note (including one inside a subgraph).
  8. Add a note far to the right of the graph, then ask the assistant to add a step — the
    step should not land on top of the note.
  9. Open a run in the run view and ask about the notes — reading works; any edit returns
    the read-only message.

Also in here: get_pipeline_state was failing outright

Found while testing the stack in the app — the assistant reported "the state tool
returned an error"
and then went on to edit the canvas blind, which is the exact
failure this stack exists to remove.

The bridge returns the serialized spec to the agent worker over Comlink, so the payload
has to survive postMessage's structured clone. Several values read straight off the
keystone models are MobX observables, which throw DataCloneError there and take the
whole call down. Three field classes could trigger it, none of them obvious:

  • a dynamicData argument value (secrets / system data) — the deserializer keeps these
    as objects rather than turning them into bindings
  • a structured TypeSpecType on an input, output or component port
  • a string array out of an annotation codec

The first two are pre-existing on master; I confirmed it by running the same check
there. The fix is one conversion at the serializer's boundary, which is where the
module's own contract already claims the result is plain JSON. toJS does not work
here — the returned object is a plain one, and MobX only recurses into observable
containers.

It sits at the bottom of the stack so every branch above inherits it, and it is guarded
by a test that fails without it.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: ai-sticky-notes/e467391

@camielvs
camielvs added this pull request to stack #2771 September 21, 2026 19:09
@camielvs
camielvs force-pushed the ai-sticky-notes branch 2 times, most recently from d94ab24 to 3962370 Compare September 21, 2026 21:38
camielvs and others added 2 commits September 21, 2026 15:21
The assistant could not see a single word written on a sticky note, and had
no way to leave one. Notes are the one canvas element whose whole purpose is
to carry human intent — "don't touch this branch", "this threshold came from
the Q3 eval" — so it would restructure a canvas around annotation content
someone had placed deliberately, without ever knowing it was there.

Notes now appear in the pipeline state the assistant reads, with their text,
colours, size, position, lock state and author, at the top level and inside
every subgraph. It can also add, edit and delete them.

A note can be placed anywhere, and the prompt treats that as the normal case:
notes are freeform, and plenty of them are about no single step — a heading
over a region, a caveat about a whole branch, a reminder parked in empty
space. Explicit coordinates are the general path, and work in any graph
including empty canvas far from every node. Anchoring a note to a task, input
or output is offered as a shortcut for the narrower case where the note
really is about that one thing, so the assistant does not have to work out
coordinates for it.

Its own notes are authored as "AI assistant" rather than "user", which is
both visible in the details panel and the signal the prompts use: anything
the user wrote is theirs, and it is told not to reword, recolour, move or
delete it unless asked. Locked notes are refused outright. Colours are
restricted to the values the picker can produce, so it cannot write a note
that renders as nothing.

Two smaller corrections in the same area. A task the assistant added could
land on top of an existing note, because the placement logic only looked at
tasks and ports; it now accounts for notes. And a sticky note mentioned in
chat now renders as a working chip that navigates to it, like every other
entity link.

The run view gains the read but not the writes, so the debug assistant can
use a note as context for a failure while the spec stays read-only there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bridge returns the serialized spec to the agent worker over Comlink, so
the payload has to survive postMessage's structured clone. Several values
read straight off the keystone models are MobX observables, which throw
DataCloneError there and take the whole get_pipeline_state call down with
them — the agent then reports "the state tool returned an error" and carries
on blind, which is exactly the failure mode this stack is meant to remove.

Three field classes could trigger it, none of them obvious:

  - a dynamicData argument value (secrets / system data), which the
    deserializer keeps as an object rather than turning into a binding
  - a structured TypeSpecType on an input, output or component port
  - an array out of an annotation codec

toJS is not a fix: the returned object is a plain one, and MobX only recurses
into observable containers. A JSON round-trip is lossless for this shape,
since AiSpec is declared plain JSON and pickDefined has already dropped every
undefined.

Guarded with a test that fails without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@camielvs camielvs left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness pass over the branch. tsc --noEmit is clean and all four touched test files pass (112 tests).

The toPlainJson structured-clone fix checks out — no mobx-keystone models reach AiSpec, so the JSON round-trip is lossless as described. Also verified: subgraph note coverage via getSubgraphState reusing the same serializer, EntityChip's "flex" kind against NodeEntityType and selectNode, RunView read-only handlers covering all three new mutations, allTools wiring into architect/repair with the deliberate omission from debugAssistant, the annotation codec always being applied on deserialize (so getFlexNodes never returns a raw string), nested undo.withGroup usage, the locked-note refusal and documented locked: false unlock path, and colour validation on both add and update.

Two findings inline. The first is one instance of a trap that also shows up twice in #2767: the codebase treats (0,0) as "never placed" (buildUtils.resolvePosition substitutes taskDefaultPosition(index)), but Annotations.get("editor.position") returns the codec default {x:0,y:0} rather than undefined. Anything that branches on the raw annotation to decide whether a node has a position gets the wrong answer. Might be worth one shared helper across the stack rather than three separate fixes.

Comment thread src/routes/v2/shared/components/AiChat/toolBridge/utils.ts Outdated
Anchored notes landed at (0,-140) on any pipeline with no saved layout:
`Annotations.get("editor.position")` returns the codec default {x:0,y:0}
rather than undefined, so `resolveNoteAnchor`'s fallback branch was dead
and the anchor read as placed at the origin.

Hoist the canvas's own id -> position map out of ConduitNode into
`resolveEntityPositions`, and read anchors and `computeNextPosition`
through it, so the AI-side paths see the positions the user sees.

Also make `position` actually take precedence over `anchorEntityId` as
the tool schema promises — it previously set the coordinates but left
the note in the anchor's subgraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs marked this pull request as ready for review September 22, 2026 21:14
@camielvs
camielvs requested a review from a team as a code owner September 22, 2026 21:14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/agent/tools/csomTools.ts

@Mbeaulne Mbeaulne left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI comment. beyond that this is good

The canvas clamps user resizing to 50x50, but the AI tools persisted
whatever size they were given, so a note could collapse to a size that
is hard to select or edit.

Share the minimum as MIN_FLEX_NODE_SIZE and refuse an undersized note
in the bridge, matching how colour validation already works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants