Two things Steve asked for, both in the #99 Plants mode:
- A "Recent" strip of the plants most recently planted IN THIS GARDEN
(recentlyPlantedIds — derived from actual plantings, newest first, unique;
NOT the manual localStorage tray), as tap-to-arm chips. So re-planting
"more of the same" is one tap, no picker. Hidden until something's planted.
- A clump/rows fill control (FillControl), shown once a plant is armed:
pick a layout, "Fill bed", and it runs POST /objects/:id/fill region=all
with the chosen layout — the #77 grid/clump fill the UI could NOT reach
before (it was agent/REST only). Defaults to rows (a real planting).
useFillObject mirrors useClearObject: one request, invalidate /full.
Both live in the shared PlantPlacementTools, so desktop's focus toolbar and
the mobile Plants strip get them identically. Fill is capped/validated
server-side (#95) and covered spots are skipped, so re-filling is safe.
Verified live at 390px: the Recent strip shows this garden's tomato/lettuce/
garlic; arming a chip reveals Clump|Rows + Fill bed; fill fires cleanly.
tsc + vitest (95, incl. recentlyPlantedIds) + build green. DESIGN updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #99, the real ones — all in the mode/focus/rail interplay:
- (3 models) Closing a journal/assistant rail while a bed was focused
hard-set mode='fixtures', docking the OBJECT palette inside the focused
bed with the seed tray unreachable. Derive it: closing a panel returns
to Plants if still focused, else Fixtures. The canvas-mode effect now
also follows UN-focus (plants→fixtures) and won't override a panel mode.
- (2 models) Plants mode on a focused non-plantable object (reachable via
a ?focus= deep link) showed the misleading "tap a bed" hint and no way
out on mobile. Now it says what's wrong and offers Done (exit focus).
- Selecting an object leaves a panel mode, so closing the inspector can't
strand the bar on Journal/Assistant with nothing open.
- Tapping Fixtures steps out of a focused bed (you're arranging again).
- Viewer mode bar drops Fixtures/Plants (a viewer can't place anything),
leaving Journal + Assistant.
- Safety: if the assistant capability flips off live while it's the active
mode, fall back to a canvas mode and close the orphaned chat rail.
Maintainability: extracted the shared seed-tray + Done + Clear cluster into
PlantPlacementTools (was duplicated between the desktop focus toolbar and
the mobile Plants strip); DEFAULT_MODE const replaces the twice-hardcoded
'fixtures'; selectMode reads the reactive railTab, not getState().
Verified live at 390px: focus a bed → Plants; open journal → close → back
to Plants (tray), not the Fixtures palette; tap Fixtures → exits focus.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
On a phone the editor stacked a control column (title, share, season, a
7-chip palette wrapping to 3 rows, journal/history/assistant) ABOVE the
canvas, crushing the garden — the point of the app — into a short strip.
There was no single "mode" switch: fixtures lived in the palette, plants
in a floating focus toolbar, and journal/assistant in the rail.
Mobile-first now (#99): the canvas is the whole screen, and a bottom mode
bar switches the tools docked beneath it —
- Fixtures → the object palette (arm → tap to place)
- Plants → the seed tray, once a bed is focused; focusing a bed enters
this mode. No bed focused → a "tap a bed, then Plant here"
hint. Done planting / Clear ride along.
- Journal → opens the journal rail sheet
- Assistant → opens the chat rail sheet (hidden with no model configured)
A slim mobile top strip keeps the garden name / season / share that lived
in the desktop column. Closing a panel rail returns to a canvas mode so
the bar reappears; History stays a rail sub-tab (not a fifth mode).
Desktop is untouched: the mode bar is md:hidden and the side-column layout
stands; `mode` is an inert hint there. Store gains `mode`/`setMode`
(reset with the rest of transient state on garden switch).
Verified live at 390px (each mode + the select-bed → Plant here → seed-tray
flow, canvas now dominant) and 1280px (desktop unchanged). tsc + vitest
(92) + build green. DESIGN.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly (2 models, correctness): the redirect effect checked live.isError
but the rendered "taking you to your gardens…" message read full.error —
which is the SEASON query when viewing a past year. A season-view 404 with
a healthy live garden would then show a redirect promise the effect never
fires, stranding the user.
Derive gardenGone once from the LIVE query (garden existence doesn't depend
on the season viewed) and use it for BOTH the bounce effect and the render
message, so the promise and the redirect can't disagree. Also removes the
duplicated not-found reasoning the maintainability finding flagged.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
`/` always dumped you on the gardens list; a phone user who lives in one
garden had to open the list and tap in every time. Now the device
remembers the garden it was last in and `/` resumes there.
- lib/lastGarden.ts: per-device localStorage (pansy:last-garden), same
swallow-failures rationale as the seed tray / recents. getLastGardenId
guards against a non-positive/garbage stored value.
- The `/` route redirects to the stored garden when present, else /gardens.
- The editor records the garden on successful load, and — if it 404s
(deleted or access revoked) — forgets it (only if it's the stored one,
so a bad direct link can't wipe a good resume target) and bounces to the
list, so a stale id can't trap the user on an error screen. Transient
errors still show the retryable message.
- ApiError.isNotFound getter (mirrors isConflict/isUnauthorized).
Verified live at 390px: resume into the last garden; a stale id bounces to
/gardens and clears itself. tsc + vitest (incl. new lastGarden test) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
The safe, self-contained wins from the #85 bundle:
- **Mobile viewport.** The editor used `100vh`, which on mobile Safari/Chrome is
the LARGEST viewport (URL bar hidden), so with the bar visible the canvas
bottom and Fit button were pushed under the browser chrome. `100dvh` fixes it.
- **Session expiry mid-chat.** A 401 during a chat turn was mapped to "the
assistant is not available right now" — sending the user to debug a config
problem that isn't there. Now it says the session expired and redirects to
/login, preserving the path, like the rest of the app treats 401.
- **Error toasts persist.** Error toasts were the primary report that a mutation
failed, yet auto-dismissed at 4s with no way to retrieve them — look away and
it's gone. Errors now stay until dismissed; info toasts still time out; both
get a close button.
- **Journal date filter.** `from`/`to` existed in the API and JournalFilter but
had no UI, so "show me last spring" was unreachable. Added two date inputs
(with a Clear) that feed the existing filter. No backend change.
Deferred to their own follow-ups (too big for this bundle, or need a decision):
revision-history retention/pruning, the agent toolbox's missing corrective tools,
plop-level journal entries from the UI, and the editor's max-width cap (a layout
call worth confirming rather than changing blind). The DESIGN.md API-listing
drift the issue mentioned was already fixed in #89.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
FillRegion and ClearObject were reachable only through the agent toolbox, so
on an instance with no model configured the most valuable bulk operation in a
garden planner — and the one carrying the most carefully reasoned geometry in
the codebase — did not exist at all. internal/service/ops.go said these lived
on *Service so "any future REST surface" would inherit the ACL checks; this is
that surface.
POST /objects/:id/fill takes a region EITHER by compass name ("all", "ne",
"south half") or as an explicit rect in the object's local frame, and refuses
both-or-neither: silently preferring one would make a client bug look like a
geometry bug. It answers 200 rather than 201 because a fill can legitimately
create nothing (the region is already planted) and there is no single resource
to point a Location at.
POST /objects/:id/clear replaces a client-side loop of PATCHes. That loop
wrote one change set per plop, so clearing a 40-plop bed took 40 presses of
Undo to put back — while the agent's clear_object, for the identical
user-facing action, undid in one click. CLAUDE.md states the rule it violated:
multi-row operations record together so they undo as one unit. Moving it
server-side also removes the partial-failure case the loop had to reconcile.
TestClearObjectIsOneChangeSetAPI pins that: fill a bed, count change sets,
clear it, assert the count rose by exactly one.
DESIGN.md's API block gains the two new routes, and the four it was already
missing: GET/POST/DELETE /gardens/:id/share-link and the unauthenticated
GET /public/gardens/:token. An unauthenticated surface absent from the
architecture doc is the one worth fixing on sight.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Closes#44. Two independent grids (garden + per-bed), each with a size and a snap toggle; snapping defaults off. See PR #45 for details.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Use the full Plant the picker returns (fixes the silent placement failure) and add a per-garden Seed Tray for quick repeat placing. Review fixes: disarm on tray-remove, cap load, consolidate toolbar guards, rename interface, tidy.
Closes#39.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
The editor's technically-riskiest slice, built against mock data so #11
only adds interactions on top.
- lib/geometry.ts (+ vitest): world<->screen and object-local<->world
transforms, clampScale, zoomViewportAt (the wheel/pinch "keep the point
under the cursor stationary" math), zoomToFitRect. 11 geometry tests
(round-trips, rotation, cursor-anchored zoom, fit) + 6 units tests.
- editor/store.ts: Zustand store for ephemeral editor state — viewport,
selection, focusedObjectId (selection interactions grow in #11).
- editor/useViewport.ts: @use-gesture wiring — wheel + pinch zoom toward
the pointer, drag-pan, scale clamped to [0.05, 20] px/cm, and an animated
fitToRect (eased rAF tween). touch-action:none so the browser doesn't
fight gestures.
- editor/ObjectShape.tsx: rect/circle centered + rotated, kind default
colors, name label; non-scaling strokes so outlines stay 1px at any zoom.
- editor/GardenCanvas.tsx: full-size svg, single world <g>, fading 1m grid,
garden boundary, z-ordered objects; ResizeObserver-driven auto-fit, a
zoom readout, and a Fit control. Deps: zustand, @use-gesture/react,
vitest (+ test scripts).
- GardenEditorPage renders the canvas with a mock scene (#11 swaps in
/full).
Verified in a real browser: wheel zoom keeps the world point under the
cursor stationary (sub-cm drift on a 12m garden), a real drag pans, and
Fit animates to frame the garden. tsc + vitest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
Implements the Pansy v1 scaffold (epic #20, phase 0).
#1 Backend: Go module (pure-Go, builds with CGO_ENABLED=0), env config,
gin server with slog + recovery + /api/v1/healthz, modernc SQLite store
with WAL/busy_timeout/foreign_keys pragmas, embedded numbered-migration
runner, full initial schema (users, sessions, gardens, garden_shares,
garden_objects, plants, plantings), domain structs + sentinel errors,
graceful shutdown.
#2 Frontend: Vite + React 19 + TS (strict) + Tailwind 4 + TanStack
Router/Query, typed /api/v1 fetch wrapper (ApiError carries status +
body so later issues can read 409 conflict rows), dev proxy /api -> :8080,
responsive app shell with stub pages for all five routes.
#3 Single binary: //go:embed of the web build with a committed placeholder,
SPA fallback (deep links, immutable asset caching, JSON 404 for unmatched
/api and missing assets), Makefile (web/build/dev/test), README quickstart
+ env var table.
Verified: go build/vet/test clean (CGO off); binary migrates idempotently and
serves healthz; web tsc + build clean; integrated binary serves the SPA, deep
links, and correct cache headers; app mounts and navigates all five routes at
desktop and 375px widths.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi