Agent: undo for real, past seasons, and tools that correct the record
Six tools the live assistant kept needing and a prompt that knows about them: - undo_change wraps RevertChangeSet(source=agent). A revert is its own change set, so Run reports the last one as the turn's handle when the turn changed nothing else — an undo-only reply keeps its "Undo this", which is now a redo. - describe_garden takes a year: the season view (GardenFull(year)), pulled plops included, with removed/removedAt per group and per plop; list_years says which years have records. Rotation questions finally have data. - update_planting corrects a plop's date, count, label, radius or seed lot in place; remove_planting, remove_plantings and clear_object take a removedAt so a harvest can be backdated. - update_journal_entry / delete_journal_entry correct a note instead of stacking a contradicting one. - update_garden renames/resizes/re-units a garden and rewrites its notes — and the notes now go into the system prompt as the gardener's standing facts, so "remember we're in zone 6a" persists across conversations. describe_garden also reports the garden's notes, version and grid, which the new tools need. Prompt, CLAUDE.md and DESIGN.md updated to match; UI step labels for the new tools. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+314
-26
@@ -3,7 +3,10 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
||||
|
||||
@@ -24,19 +27,35 @@ import (
|
||||
// client, because the server's UTC day is tomorrow by nine in the evening in
|
||||
// Ohio. Empty falls back to the service's UTC today.
|
||||
func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox {
|
||||
box, _ := newToolbox(svc, actorID, today)
|
||||
return box
|
||||
}
|
||||
|
||||
// newToolbox is NewToolbox plus the adapter behind it, which Run keeps hold of:
|
||||
// the adapter remembers what undo_change reverted, and a turn that only undid
|
||||
// something has no other handle to offer as its change.
|
||||
func newToolbox(svc *service.Service, actorID int64, today string) (*llm.Toolbox, *adapter) {
|
||||
a := &adapter{svc: svc, actor: actorID, today: strings.TrimSpace(today)}
|
||||
return llm.NewToolbox("pansy",
|
||||
llm.DefineTool("list_gardens",
|
||||
"List the gardens the user can see (owned and shared), with the user's role on each.",
|
||||
a.listGardens),
|
||||
llm.DefineTool("describe_garden",
|
||||
"Summarize a garden: its dimensions, objects (with sizes/positions/version), and each "+
|
||||
"object's active plantings grouped by plant — how many, roughly where, when they went in, "+
|
||||
"Summarize a garden: its dimensions, notes, version, objects (with sizes/positions/version), "+
|
||||
"and each object's plantings grouped by plant — how many, roughly where, when they went in, "+
|
||||
"and days to maturity when known. A small group lists its plops individually (id + "+
|
||||
"version for move_planting/remove_planting, and xCm/yCm in the object's local frame so a "+
|
||||
"move can keep their layout); a large one (a grid-filled bed) does not — use "+
|
||||
"list_plantings for those, or act on the whole group with remove_plantings.",
|
||||
"version for move_planting/remove_planting/update_planting, and xCm/yCm in the object's "+
|
||||
"local frame so a move can keep their layout); a large one (a grid-filled bed) does not — "+
|
||||
"use list_plantings for those, or act on the whole group with remove_plantings. Without a "+
|
||||
"year it describes what is growing now; with one it is that season's view — every plop "+
|
||||
"whose time in the ground overlapped the year, pulled ones included, each group saying how "+
|
||||
"many were removed and when. That is how to answer \"what was in this bed last year?\" and "+
|
||||
"to check rotation before replanting. list_years says which years have data.",
|
||||
a.describeGarden),
|
||||
llm.DefineTool("list_years",
|
||||
"List the years this garden has planting records for, newest first — the years "+
|
||||
"describe_garden can show as a season view.",
|
||||
a.listYears),
|
||||
llm.DefineTool("list_plantings",
|
||||
"List one object's active plops one by one, each with its id, version, position (xCm/yCm "+
|
||||
"in the object's local frame), location, count and planting date — the detail "+
|
||||
@@ -71,21 +90,30 @@ func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox
|
||||
"is how to relocate plants; removing and re-placing them would lose when they were planted. "+
|
||||
"Needs the plop's id and version (describe_garden or list_plantings).",
|
||||
a.movePlanting),
|
||||
llm.DefineTool("update_planting",
|
||||
"Correct ONE plop's record without moving it: the date it was planted (plantedAt), its "+
|
||||
"plant count (count, or clearCount to go back to deriving it from area and spacing), "+
|
||||
"its label, its radius, or the seed lot it came from. Use for \"those tomatoes actually "+
|
||||
"went in on May 20\" or \"that clump is five plants\". Needs the plop's id and version "+
|
||||
"(describe_garden or list_plantings). Only the fields you pass change.",
|
||||
a.updatePlanting),
|
||||
llm.DefineTool("remove_planting",
|
||||
"Remove ONE plop from a bed, leaving the rest — the single-plant answer to clear_object's "+
|
||||
"all-or-nothing. Soft-removes it (kept for planting history, undoable), like clearing a "+
|
||||
"bed does. Needs the plop's id and version (describe_garden or list_plantings). Use "+
|
||||
"for \"pull the basil out of the corner\".",
|
||||
"for \"pull the basil out of the corner\". Dated today unless removedAt says when it "+
|
||||
"actually came out (\"I harvested the garlic on Aug 1\").",
|
||||
a.removePlanting),
|
||||
llm.DefineTool("remove_plantings",
|
||||
"Remove every plop of ONE plant from an object, leaving the other plants in it — \"take the "+
|
||||
"beets out of the south bed\". Soft-removes them (kept for planting history, undoable as "+
|
||||
"one change). Use this rather than many remove_planting calls.",
|
||||
"one change). Use this rather than many remove_planting calls. Dated today unless "+
|
||||
"removedAt says when they actually came out.",
|
||||
a.removePlantings),
|
||||
llm.DefineTool("clear_object",
|
||||
"Remove all plants from an object. They are soft-removed, so the planting history for past "+
|
||||
"seasons is kept and the change can be undone. Use this before replanting a bed with "+
|
||||
"something else.",
|
||||
"something else. Dated today unless removedAt says when the bed was actually cleared.",
|
||||
a.clearObject),
|
||||
llm.DefineTool("find_plant",
|
||||
"Look up plants in the user's catalog by name or category, to get the plantId that "+
|
||||
@@ -115,16 +143,34 @@ func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox
|
||||
llm.DefineTool("read_journal",
|
||||
"Read back the garden's grow journal — the observations add_journal_entry wrote. "+
|
||||
"Narrow it with objectId (one bed), or a from/to date range (YYYY-MM-DD). Most "+
|
||||
"recently observed first. Use this to answer \"what did I note about the west bed?\" "+
|
||||
"or \"what happened last spring?\".",
|
||||
"recently observed first. Each entry carries the id and version that "+
|
||||
"update_journal_entry and delete_journal_entry need. Use this to answer \"what did I "+
|
||||
"note about the west bed?\" or \"what happened last spring?\".",
|
||||
a.readJournal),
|
||||
llm.DefineTool("update_journal_entry",
|
||||
"Correct a journal entry the user wrote — its text, or the date it describes — instead "+
|
||||
"of adding a second entry that contradicts the first: \"that note was about the "+
|
||||
"cucumbers, not the cantaloupe\". Needs the entry's id and version from read_journal. "+
|
||||
"Only the user's own entries can be edited.",
|
||||
a.updateJournalEntry),
|
||||
llm.DefineTool("delete_journal_entry",
|
||||
"Delete a journal entry, by its id from read_journal. This is permanent — the journal is "+
|
||||
"not in the undo history — so delete only the entry the user pointed at.",
|
||||
a.deleteJournalEntry),
|
||||
llm.DefineTool("read_history",
|
||||
"Read the garden's change history: every change anyone made — by hand in the editor, or "+
|
||||
"in an earlier conversation with you — newest first, with what it changed and whether it "+
|
||||
"was undone. Use it to answer \"what changed this week?\" or \"what did you do last time?\" "+
|
||||
"rather than reciting from memory. You cannot undo from here; the person has an Undo "+
|
||||
"button on each change.",
|
||||
"in an earlier conversation with you — newest first, with its id, what it changed and "+
|
||||
"whether it was undone. Use it to answer \"what changed this week?\" or \"what did you do "+
|
||||
"last time?\" rather than reciting from memory, and to find the id undo_change needs.",
|
||||
a.readHistory),
|
||||
llm.DefineTool("undo_change",
|
||||
"Undo one change from the history by its id (from read_history): it reverts everything "+
|
||||
"that change did, as a new change that can itself be undone. This is how to do \"undo "+
|
||||
"the beets\" or \"put it back the way it was\" — find the change in read_history, then "+
|
||||
"undo it; never claim to have undone something without calling this. A change already "+
|
||||
"marked undone needs no second undo. Anything edited since that change is left alone "+
|
||||
"and reported under conflicts; tell the user about those.",
|
||||
a.undoChange),
|
||||
llm.DefineTool("update_object",
|
||||
"Change an existing object: resize it (widthCm/heightCm), rotate it (rotationDeg), "+
|
||||
"rename it (name), or toggle whether it can hold plants (plantable). Only the fields "+
|
||||
@@ -154,7 +200,17 @@ func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox
|
||||
"(with an em dash) is that garden's plan for the year, and the editor offers it as such. "+
|
||||
"Use it for \"set up next year's plan\"; never use another real garden as a scratch space.",
|
||||
a.copyGarden),
|
||||
)
|
||||
llm.DefineTool("update_garden",
|
||||
"Change a garden the user owns: rename it, resize it (widthCm/heightCm), switch its units "+
|
||||
"(metric|imperial), set its grid (gridSizeCm, snapToGrid), or rewrite its notes. Only the "+
|
||||
"fields you pass change. Needs the garden's current version from describe_garden. The "+
|
||||
"notes are the gardener's standing facts about the place — zone, frost dates, soil, sun, "+
|
||||
"how they like things done — and you are given them at the start of every conversation, "+
|
||||
"so when the user tells you something worth remembering (\"we're in zone 6a\", \"last "+
|
||||
"frost is usually around May 10\"), add it here. notes replaces the WHOLE text: take the "+
|
||||
"current notes from describe_garden, add the new line, and pass all of it.",
|
||||
a.updateGarden),
|
||||
), a
|
||||
}
|
||||
|
||||
// adapter carries the service, the acting user and their local day for the
|
||||
@@ -163,6 +219,25 @@ type adapter struct {
|
||||
svc *service.Service
|
||||
actor int64
|
||||
today string
|
||||
|
||||
mu sync.Mutex
|
||||
// reverts is every change set undo_change produced this turn. A revert is
|
||||
// its own change set (it points back at the one it undid, and the target is
|
||||
// marked undone), so it never joins the turn's scope — which leaves a turn
|
||||
// that only undid something with no change of its own. Run reports the last
|
||||
// revert as that turn's handle, so "Undo this" under the reply can redo it.
|
||||
reverts []int64
|
||||
}
|
||||
|
||||
// lastRevert is the newest change set undo_change produced this turn, if any.
|
||||
func (a *adapter) lastRevert() *int64 {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if len(a.reverts) == 0 {
|
||||
return nil
|
||||
}
|
||||
id := a.reverts[len(a.reverts)-1]
|
||||
return &id
|
||||
}
|
||||
|
||||
// day is the date a tool stamps: the one the model passed, else the gardener's
|
||||
@@ -184,8 +259,45 @@ func (a *adapter) listGardens(ctx context.Context, _ struct{}) (any, error) {
|
||||
|
||||
func (a *adapter) describeGarden(ctx context.Context, args struct {
|
||||
GardenID int64 `json:"gardenId" description:"id of the garden to describe"`
|
||||
Year *int `json:"year" description:"optional: describe that year's season instead of what is growing now — every plop in the ground at any point in the year, pulled ones included"`
|
||||
}) (any, error) {
|
||||
return a.svc.DescribeGarden(ctx, a.actor, args.GardenID)
|
||||
return a.svc.DescribeGarden(ctx, a.actor, args.GardenID, args.Year)
|
||||
}
|
||||
|
||||
func (a *adapter) listYears(ctx context.Context, args struct {
|
||||
GardenID int64 `json:"gardenId" description:"garden whose planting years to list"`
|
||||
}) (any, error) {
|
||||
years, err := a.svc.GardenYears(ctx, a.actor, args.GardenID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The service pads the list with ITS current year (UTC); the gardener's may
|
||||
// differ around New Year. Theirs is the one "this year" means to them.
|
||||
if y, ok := yearOf(a.today); ok {
|
||||
present := false
|
||||
for _, have := range years {
|
||||
if have == y {
|
||||
present = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
years = append([]int{y}, years...)
|
||||
}
|
||||
}
|
||||
return map[string]any{"years": years}, nil
|
||||
}
|
||||
|
||||
// yearOf is the year of a YYYY-MM-DD date, or false for anything else.
|
||||
func yearOf(date string) (int, bool) {
|
||||
if len(date) < 4 {
|
||||
return 0, false
|
||||
}
|
||||
y, err := strconv.Atoi(date[:4])
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return y, true
|
||||
}
|
||||
|
||||
func (a *adapter) listPlantings(ctx context.Context, args struct {
|
||||
@@ -344,9 +456,10 @@ func (a *adapter) addJournalEntry(ctx context.Context, args struct {
|
||||
}
|
||||
|
||||
func (a *adapter) clearObject(ctx context.Context, args struct {
|
||||
ObjectID int64 `json:"objectId" description:"object to remove all plants from"`
|
||||
ObjectID int64 `json:"objectId" description:"object to remove all plants from"`
|
||||
RemovedAt string `json:"removedAt" description:"optional date the plants came out, YYYY-MM-DD; defaults to today"`
|
||||
}) (any, error) {
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID, service.ClearOptions{RemovedAt: a.day("")})
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID, service.ClearOptions{RemovedAt: a.day(args.RemovedAt)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -354,21 +467,75 @@ func (a *adapter) clearObject(ctx context.Context, args struct {
|
||||
}
|
||||
|
||||
func (a *adapter) removePlantings(ctx context.Context, args struct {
|
||||
ObjectID int64 `json:"objectId" description:"object to remove the plant from"`
|
||||
PlantID int64 `json:"plantId" description:"the plant to remove every plop of (from describe_garden)"`
|
||||
ObjectID int64 `json:"objectId" description:"object to remove the plant from"`
|
||||
PlantID int64 `json:"plantId" description:"the plant to remove every plop of (from describe_garden)"`
|
||||
RemovedAt string `json:"removedAt" description:"optional date they came out, YYYY-MM-DD; defaults to today"`
|
||||
}) (any, error) {
|
||||
if args.PlantID == 0 {
|
||||
// Left out, it would "remove" plant 0 — nothing — and report success.
|
||||
return nil, fmt.Errorf("%w: plantId is required — say which plant to remove, or use clear_object for all of them", domain.ErrInvalidInput)
|
||||
}
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID,
|
||||
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: a.day("")})
|
||||
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: a.day(args.RemovedAt)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]int{"removed": n}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) updatePlanting(ctx context.Context, args struct {
|
||||
PlantingID int64 `json:"plantingId" description:"plop to correct (its id from describe_garden or list_plantings)"`
|
||||
Version int64 `json:"version" description:"the plop's current version"`
|
||||
PlantedAt *string `json:"plantedAt" description:"optional corrected planting date, YYYY-MM-DD"`
|
||||
Count *int `json:"count" description:"optional explicit plant count for the plop"`
|
||||
ClearCount bool `json:"clearCount" description:"optional: drop an explicit count and derive it from area and spacing again"`
|
||||
Label *string `json:"label" description:"optional label for the plop; empty clears it"`
|
||||
RadiusCM *float64 `json:"radiusCm" description:"optional new radius in cm"`
|
||||
SeedLotID *int64 `json:"seedLotId" description:"optional seed lot (from list_seed_lots) to attribute it to"`
|
||||
ClearLot bool `json:"clearSeedLot" description:"optional: detach it from its seed lot"`
|
||||
}) (any, error) {
|
||||
patch := service.PlantingPatch{RadiusCM: args.RadiusCM}
|
||||
if args.PlantedAt != nil {
|
||||
if _, err := parseDay(*args.PlantedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch.SetPlantedAt, patch.PlantedAt = true, args.PlantedAt
|
||||
}
|
||||
switch {
|
||||
case args.ClearCount && args.Count != nil:
|
||||
return nil, fmt.Errorf("%w: give a count or clearCount, not both", domain.ErrInvalidInput)
|
||||
case args.ClearCount:
|
||||
patch.SetCount = true
|
||||
case args.Count != nil:
|
||||
patch.SetCount, patch.Count = true, args.Count
|
||||
}
|
||||
if args.Label != nil {
|
||||
patch.SetLabel = true
|
||||
if strings.TrimSpace(*args.Label) != "" {
|
||||
patch.Label = args.Label
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case args.ClearLot && args.SeedLotID != nil:
|
||||
return nil, fmt.Errorf("%w: give a seedLotId or clearSeedLot, not both", domain.ErrInvalidInput)
|
||||
case args.ClearLot:
|
||||
patch.SetSeedLotID = true
|
||||
case args.SeedLotID != nil:
|
||||
patch.SetSeedLotID, patch.SeedLotID = true, args.SeedLotID
|
||||
}
|
||||
return a.svc.UpdatePlanting(ctx, a.actor, args.PlantingID, patch, args.Version)
|
||||
}
|
||||
|
||||
// parseDay checks a date the model typed, so a malformed one fails with a
|
||||
// message about the date rather than as a bare "invalid input" from the store.
|
||||
func parseDay(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if _, err := time.Parse(dateLayout, s); err != nil {
|
||||
return "", fmt.Errorf("%w: %q is not a YYYY-MM-DD date", domain.ErrInvalidInput, s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (a *adapter) readJournal(ctx context.Context, args struct {
|
||||
GardenID int64 `json:"gardenId" description:"garden whose journal to read"`
|
||||
ObjectID *int64 `json:"objectId" description:"optional bed to narrow to; omit for the whole garden"`
|
||||
@@ -391,6 +558,75 @@ func (a *adapter) readJournal(ctx context.Context, args struct {
|
||||
return map[string]any{"entries": entries, "hasMore": hasMore}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) updateJournalEntry(ctx context.Context, args struct {
|
||||
EntryID int64 `json:"entryId" description:"journal entry to correct (its id from read_journal)"`
|
||||
Version int64 `json:"version" description:"the entry's current version (from read_journal)"`
|
||||
Body *string `json:"body" description:"optional corrected text"`
|
||||
ObservedAt *string `json:"observedAt" description:"optional corrected date it happened, YYYY-MM-DD"`
|
||||
}) (any, error) {
|
||||
if args.Body == nil && args.ObservedAt == nil {
|
||||
return nil, fmt.Errorf("%w: say what to change — the body, the date, or both", domain.ErrInvalidInput)
|
||||
}
|
||||
if args.ObservedAt != nil {
|
||||
if _, err := parseDay(*args.ObservedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a.svc.UpdateJournalEntry(ctx, a.actor, args.EntryID,
|
||||
service.JournalPatch{Body: args.Body, ObservedAt: args.ObservedAt}, args.Version)
|
||||
}
|
||||
|
||||
func (a *adapter) deleteJournalEntry(ctx context.Context, args struct {
|
||||
EntryID int64 `json:"entryId" description:"journal entry to delete (its id from read_journal)"`
|
||||
}) (any, error) {
|
||||
if err := a.svc.DeleteJournalEntry(ctx, a.actor, args.EntryID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"deleted": args.EntryID}, nil
|
||||
}
|
||||
|
||||
// undoResult is what undo_change reports: the change it reverted, the new
|
||||
// change set that did so (undoable in turn), and what it had to leave alone.
|
||||
type undoResult struct {
|
||||
UndoneID int64 `json:"undoneId"`
|
||||
// ChangeSet is the revert itself — the history entry that can be undone
|
||||
// to redo — and Summary its row in the history ("Undid: …").
|
||||
ChangeSet *int64 `json:"changeSetId,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Changes string `json:"changes"`
|
||||
Conflicts []domain.RevertConflict `json:"conflicts"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
func (a *adapter) undoChange(ctx context.Context, args struct {
|
||||
ChangeSetID int64 `json:"changeSetId" description:"the change to undo — its id from read_history"`
|
||||
}) (any, error) {
|
||||
if args.ChangeSetID == 0 {
|
||||
return nil, fmt.Errorf("%w: changeSetId is required — find the change in read_history first", domain.ErrInvalidInput)
|
||||
}
|
||||
cs, conflicts, err := a.svc.RevertChangeSet(ctx, a.actor, args.ChangeSetID, domain.SourceAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := undoResult{UndoneID: args.ChangeSetID, Conflicts: conflicts}
|
||||
if cs == nil {
|
||||
// Nothing applied: every revision was a conflict, or the set was empty.
|
||||
res.Changes = "nothing"
|
||||
res.Note = "Nothing was reverted — everything that change touched has been edited since, or there was nothing left to undo."
|
||||
return res, nil
|
||||
}
|
||||
res.ChangeSet = &cs.ID
|
||||
res.Summary = cs.Summary
|
||||
res.Changes = describeCounts(cs.Counts)
|
||||
if len(conflicts) > 0 {
|
||||
res.Note = "Part of the change was left alone because it had been edited since; see conflicts."
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.reverts = append(a.reverts, cs.ID)
|
||||
a.mu.Unlock()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// historyEntry is one change set as read_history reports it: the row a person
|
||||
// would read in the History panel, not the revision snapshots behind it.
|
||||
type historyEntry struct {
|
||||
@@ -471,12 +707,14 @@ func (a *adapter) deleteObject(ctx context.Context, args struct {
|
||||
}
|
||||
|
||||
func (a *adapter) removePlanting(ctx context.Context, args struct {
|
||||
PlantingID int64 `json:"plantingId" description:"plop to remove (its id from describe_garden)"`
|
||||
Version int64 `json:"version" description:"the plop's current version (from describe_garden)"`
|
||||
PlantingID int64 `json:"plantingId" description:"plop to remove (its id from describe_garden)"`
|
||||
Version int64 `json:"version" description:"the plop's current version (from describe_garden)"`
|
||||
RemovedAt string `json:"removedAt" description:"optional date it came out, YYYY-MM-DD; defaults to today"`
|
||||
}) (any, error) {
|
||||
// Soft-remove via the service, dated the gardener's local day like every
|
||||
// other tool here (the service clock's UTC day when that isn't known).
|
||||
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, a.day(""))
|
||||
// Soft-remove via the service, dated the day the gardener said, else their
|
||||
// local day like every other tool here (the service clock's UTC day when
|
||||
// that isn't known).
|
||||
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, a.day(args.RemovedAt))
|
||||
}
|
||||
|
||||
func (a *adapter) listSeedLots(ctx context.Context, args struct {
|
||||
@@ -507,3 +745,53 @@ func (a *adapter) copyGarden(ctx context.Context, args struct {
|
||||
}) (any, error) {
|
||||
return a.svc.CopyGarden(ctx, a.actor, args.GardenID, args.Name)
|
||||
}
|
||||
|
||||
func (a *adapter) updateGarden(ctx context.Context, args struct {
|
||||
GardenID int64 `json:"gardenId" description:"garden to change (the user must own it)"`
|
||||
Version int64 `json:"version" description:"the garden's current version (from describe_garden)"`
|
||||
Name *string `json:"name" description:"optional new name"`
|
||||
WidthCM *float64 `json:"widthCm" description:"optional new width in cm"`
|
||||
HeightCM *float64 `json:"heightCm" description:"optional new height in cm"`
|
||||
UnitPref *string `json:"units" description:"optional: metric | imperial — how the gardener wants lengths shown"`
|
||||
Notes *string `json:"notes" description:"optional replacement for the WHOLE notes text (merge the current notes in yourself); empty clears them"`
|
||||
GridSizeCM *float64 `json:"gridSizeCm" description:"optional grid spacing for the editor, in cm"`
|
||||
SnapToGrid *bool `json:"snapToGrid" description:"optional: whether objects snap to that grid"`
|
||||
}) (any, error) {
|
||||
if args.Name == nil && args.WidthCM == nil && args.HeightCM == nil && args.UnitPref == nil &&
|
||||
args.Notes == nil && args.GridSizeCM == nil && args.SnapToGrid == nil {
|
||||
return nil, fmt.Errorf("%w: say what to change about the garden", domain.ErrInvalidInput)
|
||||
}
|
||||
// UpdateGarden takes the whole row, so start from the current one and
|
||||
// overlay what the model passed — the same merge the editor's settings
|
||||
// dialog does, and the only way a one-field change leaves the rest alone.
|
||||
g, err := a.svc.GetGarden(ctx, a.actor, args.GardenID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in := service.GardenInput{
|
||||
Name: g.Name, WidthCM: g.WidthCM, HeightCM: g.HeightCM, UnitPref: g.UnitPref,
|
||||
Notes: g.Notes, GridSizeCM: g.GridSizeCM, SnapToGrid: g.SnapToGrid,
|
||||
}
|
||||
if args.Name != nil {
|
||||
in.Name = *args.Name
|
||||
}
|
||||
if args.WidthCM != nil {
|
||||
in.WidthCM = *args.WidthCM
|
||||
}
|
||||
if args.HeightCM != nil {
|
||||
in.HeightCM = *args.HeightCM
|
||||
}
|
||||
if args.UnitPref != nil {
|
||||
in.UnitPref = strings.ToLower(strings.TrimSpace(*args.UnitPref))
|
||||
}
|
||||
if args.Notes != nil {
|
||||
in.Notes = *args.Notes
|
||||
}
|
||||
if args.GridSizeCM != nil {
|
||||
in.GridSizeCM = *args.GridSizeCM
|
||||
}
|
||||
if args.SnapToGrid != nil {
|
||||
in.SnapToGrid = *args.SnapToGrid
|
||||
}
|
||||
return a.svc.UpdateGarden(ctx, a.actor, args.GardenID, in, args.Version)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user