Chat panel in the garden editor (#57) (#71)
Build image / build-and-push (push) Successful in 6s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #71.
This commit is contained in:
2026-07-21 06:43:22 +00:00
committed by steve
parent 3a3ce16fce
commit 8dbbc5439d
10 changed files with 604 additions and 10 deletions
+23 -6
View File
@@ -113,9 +113,10 @@ export function useRevertChangeSet(gardenId: number) {
})
}
/** How many rows a change set touched, for "3 changes" in the list. */
export function totalChanges(cs: ChangeSet): number {
return cs.counts.reduce((sum, c) => sum + c.n, 0)
/** How many rows a change set touched, for "3 changes" in the list. Undefined
* counts total zero, which callers read as "no denominator to quote". */
export function totalChanges(cs: { counts?: ChangeCount[] }): number {
return (cs.counts ?? []).reduce((sum, c) => sum + c.n, 0)
}
const ENTITY_NOUNS: Record<ChangeCount['entityType'], [string, string]> = {
@@ -171,7 +172,7 @@ export function useUndo(gardenId: number) {
const revert = useRevertChangeSet(gardenId)
const [outcomes, setOutcomes] = useState<Record<number, UndoOutcome>>({})
const undo = (cs: ChangeSet) => {
const undo = (cs: UndoTarget) => {
setOutcomes((prev) => ({ ...prev, [cs.id]: { tone: 'pending', message: 'Undoing…' } }))
revert.mutate(cs.id, {
onSuccess: (result) => {
@@ -197,6 +198,19 @@ export function useUndo(gardenId: number) {
}
}
/**
* What undo needs to know about a change set.
*
* `counts` is optional because the chat panel offers Undo on a turn knowing only
* its change set id — the agent's reply carries the id, not the tally. Fabricating
* counts to satisfy a type would produce a confidently wrong "1 of 1 changes
* undone"; leaving them out lets describeUndo say what it actually knows.
*/
export interface UndoTarget {
id: number
counts?: ChangeCount[]
}
export interface UndoOutcome {
tone: 'pending' | 'ok' | 'partial' | 'error'
message: string
@@ -208,7 +222,7 @@ export interface UndoOutcome {
* useful thing to report: a bare failure would be a lie about the two that did
* apply, and a bare success would hide the one that didn't.
*/
export function describeUndo(target: ChangeSet, result: RevertResult): UndoOutcome {
export function describeUndo(target: UndoTarget, result: RevertResult): UndoOutcome {
const skipped = result.conflicts.map(describeConflict).join('; ')
const applied = result.changeSet ? totalChanges(result.changeSet) : 0
if (result.conflicts.length === 0) {
@@ -221,6 +235,9 @@ export function describeUndo(target: ChangeSet, result: RevertResult): UndoOutco
if (applied === 0) {
return { tone: 'error', message: `Nothing was undone — ${skipped}.` }
}
// Only claim a denominator when we have one. "2 of 3" from a caller that
// never knew the total would be a number invented to fill a sentence.
const total = totalChanges(target)
return { tone: 'partial', message: `${applied} of ${total} changes undone${skipped}.` }
const scale = total > 0 ? `${applied} of ${total} changes undone` : 'Partly undone'
return { tone: 'partial', message: `${scale}${skipped}.` }
}