Approach C: headless view rendering + moi scratch lint feedback loop#29
Open
molefrog wants to merge 1 commit into
Open
Conversation
The agent no longer draws blind. Two feedback channels close the loop: - server/scratchpad-render.ts: our own SVG emitter for the primitive shape set (geo rect/ellipse, note, text, arrow with bindings/elbow/arc, draw, image), rasterized by @resvg/resvg-js with the real canvas font (Shantell Sans woff2 decompressed to TTF via wawoff2, cached in moi's cache dir). `moi scratch view` now always works: a live tab still gives exact pixels; with no tab the server renders an approximation, marked `headless` in the result and noted on stderr. `--headless` skips the relay outright. - server/scratchpad-lint.ts: read-only geometry checks off the disk snapshot — text-overflow (measured with the actual font), overlap (containment = grouping, skipped), near-misalign (≤10px), uneven-gaps in rows/columns — each with a ready-to-run fix command. Advisory: `moi scratch lint` always exits 0; `--json` for structured output. - `moi scratch resize <id> --size w,h` (geo rects and images only) makes overflow findings actionable; lint's fix is the exact fitRectToLabel size. Supporting changes: richer snapshot access in server/scratchpad.ts (readScratchpadRecords, scratchShapeBounds, exported extractShapeText with paragraph-aware newlines), the palette hex table moved to lib/scratch-palette.ts (shared by CLI snapping and the renderer), the relay's no-live-canvas failure exported as a constant so the view fallback catches precisely that case, and the skill/docs teach the loop: draw → lint → view → adjust. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4d4g5CV89L1AanPrEYp7p
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bet
Feedback dominates tooling: an agent that can always see the canvas and machine-check "looks off" converges on human-looking output no matter how it draws.
Stacked on #26 (text-metrics foundation). Sibling approaches: A (ELK auto-layout), B (relative tools).
What it does
Two feedback channels, plus the mutation needed to act on them:
moi scratch viewalways works now. When no browser tab shows the canvas (previously a hard "No live canvas" failure), the server renders it: our own SVG emitter for the shape set, rasterized by@resvg/resvg-jswith the real Shantell Sans (woff2 → TTF viawawoff2, cached underenvPaths('moi').cache). Covers rects (all four fills incl. hatch), ellipses, notes, text, arrows (binding resolution → border-clipped heads, elbow + arc routing, halo'd labels), freehand strokes, images (webp→png transcode); grey dashed placeholder for anything else.--headlessskips the relay for determinism; headless results are marked as approximations.moi scratch lint— geometry checks off the disk snapshot, each finding with a copy-pasteable fix:text-overflow(error) — measured with the real font; fix is an exactresizefromfitRectToLabel(never suggests narrowing; applying it provably clears the finding)overlap(error) — skips ≥90% containment (intentional grouping); fix is a minimal concrete pushnear-misalign(warn) — edges/centers ≤10px apart; fix is the exact aligning moveuneven-gaps(warn) — row/column runs with >12px gap spread; fix uses the median gap--jsonfor machine use; always exit 0.moi scratch resize <id> --size w,h— small new op (geo/image) so overflow findings are actionable.Skill/doc update teaches the loop: draw →
lint(fix errors, judge warns) →view→ adjust.Verified end-to-end
Recreated the motivating screenshot's mess (3 overflowing labels, title crossing a box, 2px misalignment): lint reported all five with exact fixes; applying them plus one follow-up gap fix reached zero findings, and the headless render visually confirms the before/after. Relay fallback,
--headless, and CLI output verified against a live server.Tests
13 new tests (render: PNG output, webp transcode, unsupported-type safety; lint: each finding type, containment exclusion, clean-canvas zero, resize clears overflow). Full suite 332 green; lint/format clean.
Trade-offs
Iteration costs turns vs. approach A's one-shot correctness — but this is the only approach that also audits what's already on the canvas (including user drawings) and it composes with both siblings.
🤖 Generated with Claude Code
https://claude.ai/code/session_01A4d4g5CV89L1AanPrEYp7p
Generated by Claude Code