Twenty-one prompts against the live assistant found one fabricated success,
a model that believed it was 2025, and a describe_garden that was ~450 plop
entries per turn. This is the set of fixes, each traceable to a finding:
- The gardener's LOCAL day travels with the turn (`today` on POST /agent/chat,
sent by the UI like plantedAt) into the system prompt and every dated tool
default. Left to guess, the model dated journal entries a year back; left to
the server, a 9 pm fill landed on UTC's tomorrow.
- describe_garden groups plops by plant — count, where, planted date, days to
maturity — and lists ids only for groups of ≤ 8; list_plantings spells a big
group out on demand and remove_plantings acts on one plant in a bed ("take
the beets out, leave the garlic"), which used to mean 116 single removals.
- New tools: move_planting (keeps the planting date; across beds via the new
MovePlanting, which is why the store's UPDATE now writes object_id),
update_plant, read_history, copy_garden (the "<garden> — <year>" plan
convention). fill_region takes an explicit local rectangle and a seedLotId;
place_planting's radius defaults to one plant (spacing/2) instead of a guess.
- The system prompt states the date and the gardener's units, forbids claiming
a change no tool made, says it cannot undo and points at the Undo button,
asks before clearing beds on an ambiguous sentence, and stops narrating its
own plantings into the journal.
- A mutation aimed at ANOTHER garden inside a turn is recorded under that
garden as its own change set, not filed into the open scope.
- UI: the thread scrolls inside the Assistant panel so the composer stays
put; every tool has a step label; wide tables stay inside the bubble.
Co-Authored-By: Claude Fable 5 <[email protected]>
The why-a-controller-can't-reach-the-socket story was told in full in
deadlines.go, agent.go, the test, and CLAUDE.md. It lives in deadlines.go
now; the others say what they need to and point there.
Co-Authored-By: Claude Fable 5 <[email protected]>
Long agent turns were cut at exactly 30s on the live instance with "The
connection dropped partway through." — the #78 failure, which its tests
said was fixed. The tests host openEventStream on a bare gin.New(); in
production, slog-gin replaces c.Writer with a wrapper that embeds the
gin.ResponseWriter interface, which has no Unwrap, so the ResponseController
built from the handler's writer can't reach the connection and every
SetWriteDeadline returns ErrNotSupported. The stream fell back to the
server's absolute WriteTimeout; the first write past it failed, cancelled
the request context, and closed the socket under the client mid-frame.
The scan upload's read/write extensions failed the same way, with the
errors discarded.
captureController now runs first on the engine and stashes a controller
built before anything wraps the writer; openEventStream and scanSeedPacket
take it from responseController(c). The regression tests run the stream
through New() — the real stack, in the real order — and check from the
client side; the scan path logs once instead of swallowing the error.
Co-Authored-By: Claude Fable 5 <[email protected]>
- monogramInk is memoized by color string; the canvas asks for every
visible plop on every frame of a pan (Gadfly, 2/4 models).
- FALLBACK_PLANT_COLOR lives in lib/plants and is used by the canvas, the
inspector and the garden thumbnail instead of three raw '#97a97c's.
- CopyDialog keeps its proposed "<base> — <year>" in step with the gardens
list until the person edits the name, so a list that loads after the
dialog opens can't leave a taken year in the field.
- GardenCard: reflowed the summary comment; no dead fallback on a plan
name that's already known to parse.
- today() has one import path (lib/dates); the journal re-export is gone.
- CLAUDE.md says what the inspector actually does (a text-compare guard)
rather than claiming it uses LengthField.
Co-Authored-By: Claude Fable 5 <[email protected]>
A Save that changed nothing still sent a PATCH, which bumped the row's
version and landed an "Edited garden settings" step in History that undid
nothing — the drift is gone since the last commit, but the write was still
there. Both dialogs now close without a request when every field matches
the loaded row.
In Settings, a rejected model spec's reason stayed under the field after
the field was blanked back to the saved value; committing an unchanged
value now clears it.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Garden and plant dialogs keep centimeters as the source of truth
(LengthField in lib/units.ts): a no-change Save no longer rewrites
900 cm as 899.922 or a 45 cm spacing as 44.958, bumping versions and
writing bogus history entries on the way.
- The UI stamps every date with the browser's local day (lib/dates.ts).
Journal notes already did; plop placement, fill and removal now do too,
so a 9 pm placement isn't "planted tomorrow". The fill endpoint gained an
optional plantedAt; API and agent callers still default to UTC today.
- Removing an object that holds plants asks first and says how many go
with it. An empty one still goes straight away (one Undo restores it).
- The expanded plant card's action row wraps instead of clipping "Delete".
- Monogram lettering switches to a dark ink on pale marker colors (garlic,
cabbage, marigold) instead of near-white on near-white.
- Copy-as-plan proposes the next free year and warns when the typed name
already exists, so two gardens can't both read as "the 2027 plan".
- Plan cards show the base name with a "2027 plan" tag, so the year — the
point of the name — survives truncation.
- A rejected model spec now says which model and why: a wrapped
ErrInvalidInput's reason reaches the client as the 400's message, and the
Settings field shows it inline instead of toasting "invalid input".
Also defuses a clock bomb in TestRemainingReturnsWhenAPlantingIsRemoved,
which only passed while the real date was before 2026-08-01.
Co-Authored-By: Claude Fable 5 <[email protected]>
Gadfly (error-handling lens): react-query keeps the stale pages in `data`
when a refetch fails, so `useUndoLast` would fall through and revert the step
BEFORE the one just made — the exact outcome the refetch exists to prevent.
Bail out with a toast unless the refetch succeeded.
Co-Authored-By: Claude Fable 5 <[email protected]>
The pinned gadfly commit (c9dab69) hard-coded the reviewer image
gadfly:sha-b37cd09, which has since been pruned from the registry, so every
review on a new PR failed in one second at "manifest unknown" (#124's did).
gadfly's current main (8adeeea) runs the reviewer as a job container whose tag
resolves at run time — reviewer_tag input → GADFLY_REVIEWER_TAG var → a baked
fallback (sha-b850e35, verified present) — so a retired tag can't strand the
consumer stubs again. Inputs and secrets are a superset of what this stub
forwards.
Co-Authored-By: Claude Fable 5 <[email protected]>
The frontend is rebuilt screen by screen from the handoff: warm cream ground,
terracotta + sage accents, Caprasimo over Figtree, every control a pill. Same
React/Vite/TanStack stack and the same lib/ data layer; the presentation is new.
- Tokens: web/src/styles/index.css declares the handoff's styles.css variables
through Tailwind's @theme under the same names; dark mode is those variables
overridden on <html> by the handoff's pansy-theme.js, inlined in index.html
so it runs before first paint. Lucide glyphs at stroke 2.75; a small pill kit
(Button, Dialog, Field, Seg, Toggle, Tag, toast).
- Login / Register: the centered column over soft accent circles; OIDC button
and signup footer still follow /auth/providers.
- Gardens: cards with a real SVG plot thumbnail (objects + plant-colored dots
from /full), a `plan` tag for "<name> — <year>" copies, shares line, Open +
share/copy/edit/delete; New garden / Share / Plan-a-season dialogs.
- Plants: monogram markers derived from the name (collision-resolved across the
catalog — replaces emoji icons), category chips, expandable lot cards, the
scan-packet flow as a two-step dialog that never auto-creates.
- Settings: Appearance (theme seg), Who gets in (read-only sign-in config),
Garden assistant (self-saving toggle + chat/vision model fields), You.
- Editor: a new canvas with the prototype's pointer model (wheel-to-cursor,
pinch about the centroid, 3″ snap, one PATCH per drop, semantic-zoom
monograms/labels), plus corner resize handles; desktop three-card workspace
(toolkit | plan | rail with Plot/Journal/History/Assistant) and, below 760px
of container width, the phone chrome (header, peek panel, tool strip, mode
bar). Seasons as a segmented control over the years with data plus plan
copies; Undo re-reads history before reverting the newest step.
- Public read-only view and the register page restyled to match.
- GET /settings gains a read-only `auth` view (registration mode, local auth,
OIDC issuer) so the Settings page can show what's in force.
- README / DESIGN.md / CLAUDE.md updated; @use-gesture/react dropped.
Co-Authored-By: Claude Fable 5 <[email protected]>
- The "add note" affordance no longer hides from viewers (it claimed
parity with the bed inspector but gated on !readOnly). A viewer now sees
"📓 Notes about this plant" and can open the plop's journal to read it;
the composer stays edit-gated, so they can't write. Real parity now.
- onScopePlantingChange is required, matching onScopeChange (its bed twin),
so a caller can't pass a plop scope with no way to clear it. Dropped the
now-dead guard on the "Show all" button.
- Pulled the plop-over-bed scope-label priority into one `scopeLabel`,
shared by the filter's sibling logic and the composer, so they can't
drift; trimmed the invariant comment that was restated a third time.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
- read_journal now takes an offset, so the hasMore it returns is
actionable — an agent can page a journal longer than 50 entries.
- remove_planting goes through a new service RemovePlanting that stamps
removed_at from s.now() (the injectable clock ClearObject and the fill
path use), instead of the adapter computing the date off the wall clock.
It delegates to UpdatePlanting, so the role check, version guard and
history record are unchanged. Drops the now-unused time import.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
- Fold the account menu into the editor's mobile strip. Hiding the global
header removed the only sign-out on mobile in the editor; the strip now
carries it, so the space win stays but sign-out is one tap away.
- EditorRail peek cap vh → dvh, matching the dvh-bounded editor column, so
it can't overrun the visible viewport and push the mode bar off-screen.
- Mobile editor height 4rem → 3rem: with the header hidden, only <main>'s
py-6 (3rem) is outside the editor, so 4rem left ~16px dead. Comment
corrected.
- Trim two comments that duplicated nearby docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
journal_entries.planting_id was modelled, accepted by the API, and the
JournalPanel already rendered a "planting" badge for such entries — but
nothing in the UI ever created one. A badge for a state the UI couldn't
produce.
Give the plop the same "add note" affordance the bed inspector has:
- PlopInspector gains an onAddNote button ("📓 Add a note about this
plant"), shown only to an editor (a viewer can't write notes).
- The editor store gains a journalPlantingId scope beside journalObjectId.
The two are mutually exclusive — each setter clears the other — so the
journal filter is never double-scoped.
- JournalPanel filters by plantingId when that scope is set, shows a
"Notes about one planting · Show all" banner, and its composer attaches
new notes to the plop. Empty-state copy updated to match.
Two taps from a selected plant to typing, mirroring the bed flow. No
backend change — the API already accepted plantingId.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
The toolbox could create and move but not delete or resize; write the
journal but not read it; clear a whole bed but not pull one plant; report
seed remaining but not record a purchase. Close those gaps with thin
adapters over the SAME service methods the REST API uses, so they inherit
the permission checks unchanged:
read_journal → ListJournal (the write/read asymmetry, most visible)
update_object → UpdateObject (resize / rotate / rename / plantable)
delete_object → DeleteObject (counterpart to create_object)
remove_planting → UpdatePlanting (soft-remove ONE plop, like clear does)
list_seed_lots → ListSeedLots
record_seed_lot → CreateSeedLot (record a purchase; "I bought 2 packets")
To address a single plop the agent needs its id + version, so
DescribePlanting now carries both — the same way DescribeObject.Version
already lets it edit an object. remove_planting soft-removes (removed_at =
today), mirroring clear_object, so the plant stays in planting history and
the change is undoable.
Deferred deliberately: an undo/revert tool needs a way to list recent
change sets to get a changeSetId, which is a larger addition; noted on the
issue for a follow-up.
Tested through the tool layer (TestCorrectiveTools): resize, single-plop
removal, journal read-back, seed-lot record+list, and delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Two mobile complaints, both in the editor/assistant context:
- The global top bar (brand + account) sat above the editor's own
garden-name strip — a whole banner of pure chrome over a full-screen
canvas. Hide it on mobile in the editor (the same rationale that hides
the bottom nav there) and fold a leaf/back affordance into the garden
strip so there's still a way out. Desktop keeps the header. The editor's
height band shrinks 8rem → 4rem on mobile to hand that space to the
canvas; desktop stays 8rem since the header is still there.
- The assistant (and journal/history) rendered inside the rail peek
capped at max-h-[50vh]; after the tab bar, header and input, messages
got ~200px. That cap is right for the inspector (read alongside the
canvas) but not for a mode where reading/typing is the task. Panel
modes now take a taller slice (78vh) via a `tall` prop; the inspector
keeps the 50vh peek. Chat message spacing loosened gap-2 → gap-3.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Surfaces the seed-packet scan (shipped in #102) inside the editor's Plants mode — a capability-gated "📷 Scan packet" entry in the shared PlantPlacementTools cluster (desktop focus toolbar + mobile strip) and in the mobile pre-focus hint — so a variety can be added mid-planting without leaving the garden. Follow-up to #102 per Steve's deferred design call.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Adds the mobile-first UI for the seed-packet capture backend (live since #94): a capability-gated "Scan a packet" entry in the Plants catalog opens a camera/upload → editable proposal → confirm flow that creates a plant (new or matched) + a seed lot. Frontend only. Closes#102 — the last open child of epic #96.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Render assistant chat output as GFM Markdown (tables, lists, code, headings), lazy-loaded so the ~150 KB renderer only ships when an assistant message shows. Hardened per review: no <img> (exfiltration-beacon guard), no raw HTML, error-boundary + stale-chunk recovery around the lazy chunk, GFM column alignment, and memoized parsing.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Gadfly on #104:
- The handles keyed off `pointer: coarse` but the NudgePad off `md:hidden`
(viewport), so a large touchscreen or a narrow mouse window got them
disagreeing. Extracted one `isCoarsePointer` in shared.ts that both use —
the pad now shows on a coarse pointer, same as the bigger handles.
- Wrapped commitLater/nudgeSelected in useCallback([]) — stable identity, so
NudgePad doesn't re-render each parent render, and the mount-once keydown
effect capturing nudgeSelected is now explicitly safe (a comment spells out
the refs-only invariant that makes the empty-deps capture correct).
- isCoarsePointer's optional-chained matchMedia keeps it false (mouse
defaults) under test/SSR, addressing the constants-file testability note.
tsc + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Two touch gaps the audit named:
- Resize/rotate handles were a fixed 12px — fine for a mouse, hard for a
fingertip. HANDLE_PX is now 22px on a coarse pointer (touch), 12px
otherwise. Read once at load.
- Fine positioning was keyboard-only (arrow-nudge), which a phone can't
reach and a drag can't do at single-cm precision. Added an on-screen
NudgePad — a ↑←→↓ d-pad (~40px targets) shown while something's selected
on a touch layout (md:hidden).
To share behaviour without duplicating the intricate part, extracted
nudgeSelected(dx, dy) from the keyboard handler — the live-geometry update
+ one debounced PATCH (so a burst of nudges from either surface commits
once) + the plop-bounds clamp. The keyboard handler and the pad both call
it. Verified live: the pad moves a selected bed 1cm/tap on mobile, and the
keyboard arrows still nudge on desktop after the refactor.
(The rail-vs-toast layering the issue also lists was resolved by #101 —
the rail is now an in-flow peek, not a fixed sheet the toast could cover.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #107:
- ClearBed reported a failure twice — ConfirmModal's inline Alert AND
useClearObject's own onError toast. Dropped the toast from useClearObject
(its only caller is that modal now), so the failure shows once, inline in
the dialog where the action is.
- LeaveGarden's onConfirm silently resolved (closing the dialog as if it
worked) if me.data was missing, relying on confirmDisabled to prevent it.
Throw instead, so a drift in that guard surfaces an error rather than a
fake success.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Three tidy-ups from the audit's deferred list:
- Extracted a ConfirmModal primitive (message + Cancel/Confirm, owning the
busy lock + inline error) and folded the five hand-rolled confirm dialogs
onto it: DeleteGarden, LeaveGarden, DeletePlant, DeleteSeedLot, ClearBed.
Each is now just its message + mutation + labels. Bonus: ClearBed now
shows a failure inline instead of swallowing it. (CopyGarden stays on
Modal — it has a name field, not a plain confirm.)
- Removed components/PageStub.tsx — dead scaffolding, imported nowhere.
- The garden editor was squeezed into the max-w-5xl reading measure the
other pages use; the canvas routes (editor + public garden) now go
edge-to-edge on desktop, and the top bar matches so the brand aligns with
the editor's left edge. Mobile was already full-width, so it's unchanged.
Verified live: the Delete-garden confirm renders/cancels; the desktop editor
now uses the full viewport width. tsc + vitest + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #105: cardActionClass now bakes in `inline-flex items-center`, so
the PlantCard seed-lot toggle's own `flex items-center` conflicted (two
display utilities in one plain-string className). Drop them — keep just
`mr-auto gap-1.5`. Also made cardActions.ts use one consistent concatenation
style instead of mixing concat + template literals.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #101:
- EditorRail's module doc still described a "fixed 20rem column / bottom
sheet"; updated to the in-flow peek (desktop column, phone ≤50vh peek
between canvas and mode bar).
- (correctness) A canvas-mode tap only deselected when railTab was
'inspector', so a selection made, then routed through Journal/Assistant,
survived a return to Fixtures/Plants — the canvas kept highlighting it
with no inspector. Canvas modes now clear the selection unconditionally.
- Dedup: the "closing the inspector deselects" pair lived in both selectMode
and the rail's onClose; extracted a clearSelection() helper (also used by
exitFocus) and fixed the now-stale "leave an inspector alone" comment.
tsc + vitest + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
The Share/Copy/Edit/Delete (gardens) and Duplicate/Edit/Delete (plants)
footer actions were a row of ~28px text links — easy to mis-tap on a phone,
the "cramped link row" the issue calls out. Both card types share
cardActionClass/cardDangerClass, so one change fixes both: a ~40px-tall tap
target (min-h + py-2) that reads as a button, not a link. min-h guarantees
the target even for a short label.
Verified at 390px on the gardens list; desktop unaffected (just a slightly
taller footer). tsc + vitest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Two things at once, because they're one layout: on a phone, selecting a bed
opened the rail as a bottom sheet that COVERED the whole garden, and opening
Journal/Assistant hid the mode bar until you closed it. You couldn't see what
you were editing, or switch modes without backing out.
Now the rail is an in-flow PEEK. Instead of `fixed bottom-0 max-h-70vh`
overlaying everything, EditorRail on mobile is a ≤50vh flex child the editor
places BETWEEN the canvas and the mode bar:
canvas (flex-1, shrinks) → rail peek (≤50vh) → mode bar (always shown)
So the garden stays visible in the top band, the mode bar stays reachable
below, and you can tap another mode straight from an open panel. The
contextual tool strip yields to the peek (gated on !railTab), and tapping a
canvas mode closes the panel (deselecting if it was the inspector).
Desktop is untouched — the same EditorRail is the right-hand column there
(`md:` styles), the mode bar stays `md:hidden`.
Answers Steve's two calls from the end-of-run questions: always-visible mode
bar + non-occluding inspector. Verified live at 390px (inspector + journal
peeks keep the garden and mode bar visible; mode switching works) and 1280px
(desktop unchanged). tsc + vitest + 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 #106:
- (2 models) React.lazy memoizes a REJECTED import, so after a deploy (every
main push) a still-open app references chunk hashes the server just
replaced — the import 404s and RouteError's "Try again" can never recover.
New lazyPage() helper reloads once on a chunk-load failure to fetch fresh
hashes (session-flag guarded against a reload loop; cleared on success).
- (perf) @use-gesture is editor-only, but the single vendor chunk pulled it
into the eager first paint. Exclude it from vendor so it rides with the
lazy editor chunk (vendor 421→392 KB; the gesture code moved into the
editor's own chunk). react/react-dom/tanstack still share one vendor chunk
— splitting react-dom out is what broke React 19 at load.
- (nits) lazyPage also unwraps the named export, so the five route lazies are
uniform one-liners; moved the lazy block below the import group.
Re-verified live: app mounts, /gardens/1 lazy-loads + renders the editor,
console clean. tsc + build green, no >500 KB warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
The whole app shipped in one 578 KB chunk, so a phone on cell data
downloaded and parsed everything — the canvas editor, gestures, geometry,
every page — before the login screen could paint.
- Lazy-load the heavy/deep routes via React.lazy: the editor (its
GardenCanvas + use-gesture + geometry are the biggest surface), the
public garden view, plants, settings, register. Login and the gardens
list stay eager (entry points — no fallback flash on landing). AppShell
wraps <Outlet> in a Suspense boundary.
- One `vendor` manualChunk for all node_modules so the rarely-changing
libraries cache across app deploys while the tiny app chunk churns.
Kept as a SINGLE chunk deliberately: splitting react-dom/scheduler into
their own chunk reorders module init across chunk boundaries and breaks
React 19 at load ("Cannot set 'Activity' of undefined") — verified that
failure and backed it out.
Result: app entry chunk 578 KB → 39 KB; vendor 421 KB (cached); the editor
(43 KB) + canvas (17 KB) only download when you open a garden. No more
>500 KB chunk warning.
Verified live against the embedded binary: /gardens loads with only
index+vendor; opening a garden lazy-fetches the editor chunk and renders;
console clean; the embed serves the hashed split chunks + SPA fallback fine.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #100:
- Extracted the tap-to-arm chip (icon + name + armed state) into a shared
PlantChip, used by both RecentPlants and SeedTray, so the two quick-pick
surfaces can't drift. SeedTray composes it (rounded={false}) with its
remove button into one seamless pill.
- Named the recent-strip cap: RECENT_PLANTS_MAX = 8, was a bare slice(0, 8).
Verified live: recent chips and the tray render identically post-refactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
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 #103:
- (4 models) applyOrientation allocated a WxH RGBA then threw it away for a
quarter-turn to allocate HxW. Compute the output dims once, allocate one
buffer.
- (perf) The At/Set loop boxed a color.Color per pixel — millions of heap
allocs on a full-res rotation. Convert to *image.RGBA once (draw.Draw)
then copy 4 bytes per pixel by offset. No boxing.
- (2 findings) exifOrientation didn't skip 0xFF fill bytes before a marker,
so a spec-valid padded APP1 would be misread. Skip them.
- (2 findings) orientationFromApp1 read the tag's inline value without
checking its type/count — a mistyped LONG/offset would be read as a
bogus SHORT. Require type=SHORT, count=1; also validate the TIFF 0x2A
magic agrees with the byte order.
- Tests now cover all 8 orientations (added the mirror/transpose cases
2/4/5/7, the most error-prone switch arms) as t.Run subtests.
All imagenorm tests green; gofmt clean; still CGO_ENABLED=0.
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
imagenorm decoded and re-encoded to JPEG but ignored the EXIF Orientation
tag. Phone cameras store the sensor pixels one way and set an EXIF flag to
rotate on display, so a "portrait" JPEG is really a landscape bitmap tagged
"rotate 90°" — and our re-encode strips EXIF, so without baking the rotation
in, a packet photographed in portrait reaches the vision model sideways
(bad OCR) and any future thumbnail is wrong.
- exifOrientation: a small pure-Go parser that walks the JPEG APP1/Exif
segment for tag 0x0112, returning 1 (normal) for non-JPEG or unparseable
input — never guess a rotation onto a correct image. No cgo, no new dep.
- applyOrientation: bakes in all 8 orientations (the 4 rotations + mirrors)
after downscale (cheaper to rotate the small image; a 90° turn swaps the
sides but not the longest edge, so the downscale bound still holds).
- Non-JPEG paths (HEIC/webp/png) are untouched — their decoders own
orientation and they carry no JPEG EXIF.
Tests build oriented JPEGs (a corner marker + a spliced Exif APP1) and
assert the marker lands where each orientation says, dims swapping for the
quarter-turns; plus parser defaults for non-JPEG / no-EXIF / garbage.
Stays CGO_ENABLED=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ