Skip to content

feat: visual feedback — annotate the canvas, hand notes to a coding agent - #46

Open
pat-mw wants to merge 8 commits into
flowtricks:mainfrom
pat-mw:feat/visual-feedback
Open

pat-mw wants to merge 8 commits into
flowtricks:mainfrom
pat-mw:feat/visual-feedback

Conversation

@pat-mw

@pat-mw pat-mw commented Oct 20, 2026

Copy link
Copy Markdown

Summary

A new Feedback tab (M) - for annotations on the canvas, and a quick handoff to local coding agents.
While it is open, clicking the canvas leaves a note on the element instead of selecting it, even inside components. Notes show as numbered pins and are saved to .stacki/feedback.json, which any local agent can read and resolve.

Demo video:
stacki-annotations-demo-ezgif com-gif-maker

Demo Screenshots (Expand) Access annotation mode via the Feedback tab (M) image

Annotate freely on any element in the canvas, from any page
image
image
image

Jump to annotations from the sidebar:
image

Copy prompt for local agent:
image

Prompt looks something like this:

Address the open items in /[project_folder]/.stacki/feedback.json (1 open). For each item, open every file:line in its trail — the last entry is the element, the earlier ones are the page and the component instances it sits in — make the change, then set that item's "done" to true in the file.

1. src/layouts/BaseLayout.astro:33 › src/components/wrapper/Section.astro:98 › src/components/wrapper/ContentWrapper.astro:141-143 › src/pages/404.astro:18-20 › src/components/typography/Heading.astro:48-58 (h1.heading): This page left the universe

Clear resolved annotations
image

Breakdown

sequenceDiagram
    participant Canvas as Canvas frame
    participant App as Renderer
    participant Main
    participant Agent
    Canvas->>App: avb:annotate-node (element + chain of files)
    App->>Main: feedback:write
    Main->>Main: resolve file:line trails, write .stacki/feedback.json, note in CLAUDE.md / AGENTS.md
    App-->>Agent: Copy for agent (paste)
    Agent->>Main: edits project, sets "done": true in the file
    App->>Main: feedback:read (tab open, window focus, before each write)
Loading
 electron/
+├── handlers/feedbackHandlers.ts   # read/write the file, resolve trails, agent note
 ├── preload/frameTargets.ts        # + annotate mode: hit-test every file, report the chain
 shared/ipc/
+├── feedback.ts                    # item + file parsers
 src/features/
+├── feedback/                      # panel, state, bridge
 ├── preview/PreviewOverlays.tsx    # + pins, note cards, note box

Evidence

  • Before: clicking an element inside a component selects its instance and moves to the Navigator, and nothing can hold a note.
    After: in the Feedback tab the click opens a note box on that element. outlines.test.js checks that a click in the second Card reports [page#0.6.1, Card.astro#0.0.0.1], copy 1, and selects nothing.
  • feedbackHandlers.test.ts: a live key resolves to index.astro:1-3 and a dead key to the bare file; CLAUDE.md gets the note once; an AGENTS.md that CLAUDE.md imports gets it instead.
  • feedback.test.js: pins only for open notes on this page, and the handoff names the file by absolute path.
  • npm run check:changed passes. Full gate passes except the 10 Electron browser-probe suites, which won't launch as root in the container it ran in.

Merge Danger

Door: two-way - easily reversible

The only things written into a user's project are .stacki/feedback.json (git-excluded) and one appended section in CLAUDE.md / AGENTS.md, and only once a note is saved.

Blast Radius: canvas

Annotate mode changes frame hit-testing only while the Feedback tab is open; when it isn't, the frame behaves as before.

@cusable
cusable requested a review from flowtricks October 20, 2026 14:10
Notes pin to the current canvas selection and live in one file,
.stacki/feedback.json, inside the open project (git-excluded with the rest
of .stacki/). Claude Code reads that file with its own Read tool, edits the
project, and flips each item's "done"; Stacki re-reads it on window focus,
when the tab opens, and before every write. No server, port or watcher:
the file is the interface, because the agent can already read the project.

- shared/ipc/feedback.ts: the item and file parsers (version 1, bounded,
  ids unique). Agent-appended items may omit createdAt and use keys: [].
- feedback:read / feedback:write: one channel pair through the usual six
  touch points. Main resolves each item's file:line trail from its
  selection keys with selectionTrail on every read and write, and writes
  through writeProjectText so the write is atomic like every project write.
- Every renderer change reads the file fresh, applies itself, then writes,
  so an agent's done flips made in the meantime are not overwritten.
- Feedback rail tab (M), with Copy for agent rendering the handoff text.
- Numbered pins on the canvas for open notes at the current drill-in level;
  their paths join the frame's tracked set so it measures them.

shortcut: pins are index paths, not handle-carried spans; a structural edit
above a pinned node moves the pin to its neighbour. Upgrade by storing a
byte span mapped with shared/engine/mapSpan.ts if notes start living across
editing sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
With the Feedback tab open, a click on the canvas annotates the element
under the pointer instead of selecting it: a note box opens under the
element, and saved notes show as numbered bubbles pinned to their copy of
the element. A bubble opens a card with the note, Done and Delete. The tab
no longer gives way to the Navigator on a canvas click, because the click
never reaches the selection.

Components are unwrapped: the frame hit-tests every file's markers while
annotating, so an element inside a component is clickable without drilling
in. The click reports the chain of files the element renders through (the
page's instance, each nested instance, then the element), ordered by how
far up the document each file's deepest node reaches, so content a page
slots into its layout still ends on the page. The chain becomes the note's
selection keys, so main resolves the same file:line trail as before, and
the element's marker path and copy index become the note's `marker`, where
its bubble draws.

The agent finds the notes without being told:
- Copy for agent names the file by absolute path, so the paste works from
  any directory.
- The first write adds a short "Stacki feedback" section to the project's
  CLAUDE.md (created if missing) and to AGENTS.md if the project keeps one;
  a CLAUDE.md that imports AGENTS.md is left alone.

Feedback now loads only while its tab is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
They had no CSS, so the note box rendered unpositioned at the top of the
canvas, inside an overlay layer that ignores the pointer (and children
inherit that), so it could not be clicked or typed into. Position both
absolutely, give them a surface, and take pointer events back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
The trail's file:line sat beside the label as one unbreakable monospace
run, so it ran under the done and remove buttons. Give it its own line,
cut with an ellipsis, with the whole trail in its title; let long labels
wrap, and keep the buttons from shrinking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
The app's button:hover background replaced the pin's amber, so a hovered
pin all but disappeared. A more specific hover keeps it: a lighter amber,
a slight grow and a shadow, eased over 120ms. The note box's primary
button keeps its blue the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
Copy for agent works with whatever agent runs in the project folder, so
the toast no longer names one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAFvSVVtdvBZHDz2thvb12
@pat-mw
pat-mw force-pushed the feat/visual-feedback branch from ed4cbbe to 040b67a Compare October 20, 2026 19:06
The project's .stacki/ folder is now tracked so shared settings reach Git
and Studio; only generated folders stay in the local exclude. Feedback
notes are the editor's working state, not site source, so the file joins
.stacki/preview/ and .stacki/canvas/ in .git/info/exclude. The handler
test asks Git directly whether it ignores the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pat-mw
pat-mw force-pushed the feat/visual-feedback branch from 6d2ca1e to 47fc612 Compare October 20, 2026 23:23
A click on a row now always brings its note on screen. A note on another
page opens that page first; a note out of view scrolls the canvas to it.
Before, a row did anything only when its pin was already drawn on the
open page.

The row asks useFeedback to reveal the item. feedbackRevealPlan finds the
note's page among its selection keys: they are not in page-first order
(content a page slots into its layout lists the layout first), so the
open page is matched by the same any-key rule its pins use, and otherwise
the first key naming one of the project's pages is opened through the
page switcher's own selectPage. A note whose page is gone says so.

The canvas half waits in useFeedbackReveal: once the pin exists and the
frame showing its page has measured it, it posts the same avb:scroll-to
the navigator uses, once. Rects held when the frame changed pages are
refused, because index paths repeat across pages and a match among the
previous page's would scroll to the wrong element.

Done notes and an agent's notes without a canvas place have no pin, so
their rows still only show the note in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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