AI: Powered Templates — generate a list from a description - #174
Merged
Merged
Conversation
Describe a list in plain language, get a proposed schema plus starter rows, edit both, then confirm it into a real list. The web app's Powered Templates tab, as a self-contained card in the new-list flow. Views/ListsView.xaml is contended by #161/#163/#164, so this uses the pattern that worked on #55/#56: everything lives in PoweredTemplatePanel and its own ViewModel, and the host contributes exactly one line — <local:PoweredTemplatePanel OpenListCommand="{Binding SelectListCommand}" RefreshCommand="{Binding LoadListsCommand}"/> — which is the whole diff to ListsView.xaml. No code-behind edit, no new resource, no visibility rule: the control collapses itself when #10's IsAiAvailable gate is closed, so a free account sees nothing at all. ## The DSL, live-verified for the first time The list artifact was contract-transcribed only until now — #137's own doc comment says the shape was never exercised. One /suggest call settled it: dsl: { name, description, fields: [ { key, type, label, required, displayOrder, options?, defaultValue?, visibility? } ] } rows: [ { <fieldKey>: value } ] // flat maps, null is a legal cell value Nothing in this repo had the dsl wrapper's name/description members, nor the per-field displayOrder, options, defaultValue or visibility.condition — all of which turned up unannounced on the very first call. That is the whole argument for AiListDsl being a deliberately *partial* projection: it parses the five members the column editor needs and round-trips every other member byte-for- byte out of a retained raw element. Renaming a label must not silently drop a conditional-visibility rule the model wrote, and /generate re-validates the DSL server-side, so sending back exactly what came out — minus the deliberate edits — is the only safe default. That round-trip is verified, not assumed: a scratch harness replays the real /suggest body through AiListDsl + AiListArtifactPayload, drops a column, renames a label and flips a required, and asserts 20 properties of the result — including that visibility.condition, options[] and defaultValue all survive the edit, that displayOrder is re-numbered without gaps, and that untouched cells keep their original JSON kind (5 stays a number, null stays null) rather than being re-typed from their string rendering. AiListArtifactPayload exists alongside AiListArtifact because they are opposite directions: the existing type is the read side, so its cells are already parsed JsonElements, whereas cells a user just typed are strings the client types per column. Forcing those back through JsonElement would mean serializing and re-parsing each cell individually just to satisfy the read type. ## Quota discipline /suggest and /generate are two units, so both are pre-flighted. Suggest checks the 300-word powered_template cap (mirrored, with a live word counter that turns amber before the button is pressed). Confirm checks a non-empty title, at least one column, the documented 20-field ceiling, unique keys, and the documented ^[a-z][a-z0-9_-]*$ key rule — user-added columns are slugified into that shape locally, because learning a key was illegal from a failed /generate costs a unit. Field keys are intentionally not editable: rows address cells by key, so retyping one would orphan a column's data silently. Column type pickers offer only the types this client has seen the server emit (text, number, select) plus whatever types the artifact in hand already uses. Inventing a DSL type would be a guess the server bills a unit to refuse. Confirm treats /generate's {listId} as a hint, not a fact — that envelope is still contract-transcribed — and re-fetches with GET /api/lists/{id} before handing the host anything. If no id comes back, it refreshes the host's browser and says plainly that a retry would duplicate the list and spend another credit, rather than claiming either outcome. /suggest calls made: 1 (powered_template, "a reading list with title, author, status, and rating"). POST /api/ai/generate was NOT called — it persists to a shared account — so the confirm path's response handling is the one part of this that is reasoned rather than observed. Closes #14 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 2026
Conflict in ListsView.xaml: #174's PoweredTemplatePanel and #161's column form builder both insert at the same point in the right pane. Two independent panels, both wanted — kept both, AI first. That order is deliberate rather than incidental: the Powered Template panel is a compact entry point that hides itself when AI is unavailable, while the column builder is a large editor shown only when opened. AI above the form also matches the web, where Powered Templates is a tab over the new-list form. Debug and Release both build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adron
added a commit
that referenced
this pull request
Sep 24, 2026
ListsView.xaml conflict: GitHubListHeader (#74) and the PoweredTemplatePanel + column form builder (#174/#161) all insert at the top of the right pane. All additive — kept all three. Order is deliberate: GitHubListHeader goes FIRST because it identifies the selected list (owner/repo link, private-repo tag, Refresh from GitHub), so it belongs directly under the title rather than below a large editor panel. Debug and Release both build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Stack
Fourth of five stacked PRs on the AI epic. Based on #173, not
main:issue-9-ai-servicemainissue-10-ai-gatingissue-9-ai-serviceissue-14-powered-templatesissue-10-ai-gatingissue-15-powered-documentissue-14-powered-templatesMerge in that order. Set
--baseaccordingly when retargeting.Hosting: one line, zero code-behind
Views/ListsView.xamlis contended by #161/#163/#164, so this follows the #55/#56 pattern. The entire diff to the contended file is:dropped as the first child of the right pane's
ScrollViewerStackPanel. No code-behind edit, no new resource, no visibility rule — the control collapses itself when #10'sIsAiAvailablegate is closed, so a free account sees nothing at all, and it reads the two host commands through dependency properties. If that one line conflicts, drop it anywhere inside aStackPanelwhoseDataContextisListsViewModel; nothing else about the control cares where it sits.The
listartifact, live-verified for the first time#137's own doc comment records the list shape as contract-transcribed and never exercised. One
/suggestcall settled it:POST /api/ai/suggest·{"feature":"powered_template","input":"a reading list with title, author, status, and rating"}→ 200{ "ok": true, "feature": "powered_template", "artifact": { "kind": "list", "title": "Reading List", "description": "Track books to read, currently reading, and finished, with ratings.", "dsl": { "name": "Reading List", "description": "A personal list to track books, their authors, reading status, and your rating.", "fields": [ { "key":"title", "type":"text", "label":"Title", "required":true, "displayOrder":0 }, { "key":"author", "type":"text", "label":"Author", "required":true, "displayOrder":1 }, { "key":"status", "type":"select", "label":"Status", "required":true, "displayOrder":2, "options":["To Read","Reading","Finished","Abandoned"], "defaultValue":"To Read" }, { "key":"rating", "type":"number", "label":"Rating (1-5)", "required":false, "displayOrder":3, "visibility":{ "condition":{ "field":"status","operator":"equals","value":"Finished" } } } ] }, "rows": [ { "title":"The Hobbit", "author":"J.R.R. Tolkien", "status":"Finished", "rating":5 }, { "title":"Project Hail Mary", "author":"Andy Weir", "status":"Reading", "rating":null }, { "title":"Atomic Habits", "author":"James Clear", "status":"To Read", "rating":null }, { "title":"Dune", "author":"Frank Herbert", "status":"Abandoned", "rating":2 } ] }, "usage": { "inputTokens":545, "outputTokens":713, "model":"claude-sonnet-5" }, "quota": { "usedToday":4, "dailyLimit":50 } }Things that were on record nowhere in this repo before this call: the
dslwrapper's ownname/description, and the per-fielddisplayOrder,options,defaultValueandvisibility.condition. Four unannounced members on the very first call is the whole argument forAiListDslbeing a deliberately partial projection — it parses the five members the column editor needs and round-trips every other member byte-for-byte out of a retained raw element. Renaming a label must not silently drop a conditional-visibility rule the model wrote, and/generatere-validates the DSL server-side, so returning exactly what came out (minus the deliberate edits) is the only safe default. Note also thatrowsare flat maps keyed by field key, andnullis a legal cell value.The round-trip is verified, not assumed
A scratch harness replays the real
/suggestbody throughAiListDsl+AiListArtifactPayload, drops a column, renames a label, flips arequired, and asserts 20 properties of the output. All pass, including the ones that would otherwise be silent data loss:That last group matters because the preview must show values as text (a
TextBoxholds a string), and re-serializing"5"is a guess about whether it was5,5.0or"5". So a cell the user didn't touch returns its originalJsonElementverbatim, and only an edited cell is re-typed from its column type — falling back to a string rather than discarding input it can't parse as a number.AiListArtifactPayloadexists alongside the read-sideAiListArtifactbecause they face opposite directions: that type's cells are already-parsedJsonElements from a response, whereas cells a user just typed are strings the client types per column. Forcing those back throughJsonElementwould mean serializing and re-parsing each cell individually purely to satisfy the read type.Quota discipline
/suggestand/generateare two units, so both are pre-flighted:powered_templatecap, mirrored, with a live word counter that goes amber before the button is pressed.^[a-z][a-z0-9_-]*$key rule. User-added columns are slugified into that shape locally — learning a key was illegal from a failed/generatecosts a unit.text,number,select) plus whatever types the artifact in hand already uses. Inventing a DSL type is a guess the server bills a unit to refuse.RunAiAsyncfunnel.Confirm treats
/generate's{listId}as a hint, not a fact, and re-fetches withGET /api/lists/{id}before handing the host anything. If no id comes back it refreshes the host's browser and says plainly that a retry would duplicate the list and spend another credit, rather than asserting either outcome.Quota spent and what's unverified
/suggestcalls: 1. Units spent by this PR: 1 (usedToday3 → 4).POST /api/ai/generatewas NOT called — it persists to a shared account. So the confirm path's response handling is reasoned from AI: add InterlinedApiClient.Ai.cs service + artifact models #137's contract transcription, not observed. It is written defensively for exactly that reason (read-after-write, and an explicit "don't retry, you'd duplicate" message when the id is absent). Whoever first runs this against a real account should confirm the{listId}field name and tightenAiCreatedResources.context.templateKeyis not surfaced. It's modelled in AI: add InterlinedApiClient.Ai.cs service + artifact models #137 (AiContext.PoweredTemplate) but "which base template to personalize" needs a list of valid keys, and no endpoint enumerating them was found. Plain descriptions work, so that's the supported path.selectcolumn options editor.options/defaultValueround-trip untouched and the column row displays the choices, but editing them would mean committing to the parts of the DSL that are still guesswork. Flagged rather than guessed.Builds clean in both
-c Debugand-c Release.Closes #14
🤖 Generated with Claude Code