Repository navigation
AI: Powered Document (4 modes: article / from list / from article / research URL) #15
Description
Activity
- addedparityWeb/API feature-parity workWeb/API feature-parity workP1Table stakesTable stakesarea:aiArea: aiArea: ai
on Sep 15, 2026 Live-verified corrections from the service layer (#9 / PR #137) — read before building this
Five things contradict what the AI epic and these issues assume.
1.
quotais not the same object on both endpoints.
GET /api/ai/status→{usedToday, dailyLimit, remaining}.POST /api/ai/suggest→{usedToday, dailyLimit}with noremaining(verified twice). SoAiQuota.Remainingisint?, and a post-suggest quota chip cannot read.Remaining— bindRemainingOrComputedinstead. #10's "remaining quota is visible wherever an AI action is offered" has to account for that.2. There is no provider picker to build.
AI is Anthropic-only, app-wide; per-user provider keys were removed server-side 2026-09-05. Theproviderfield on/generateis audit-ledger-only and does not select anything, and theopenai/geminientries indefaultModelsare dormant and unreachable. Don't surface a provider choice.3.
crossPosthas six fields, not two —crossPostToBluesky,selectedMastodonIds,crossPostToLinkedIn,selectedLinkedInTargets,crossPostToTwitter,linkedInLinkAsFirstComment. BothcrossPostandscheduleImmediatelyaremessage_series-only (relevant to #12).4. A sixth
422code exists that the epic didn't list:refused. Treat it likeinvalid_ai_output. Modelled and marked retryable.5. Quota accounting is asymmetric, and it shapes the UI.
Input-validation422s cost nothing (usedTodaystayed 0 across two), but any call that reaches the model costs a unit — including failures. So client-side pre-flight is load-bearing, not politeness: enforce the word caps, the 10-word series gate (#12/#13), a missingpowered_documentsource ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.Also:
context.spacingMinutesis accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.Note on the exception type
The
codeis surfaced on a newAiApiException, not onInterlinedApiException— the latter issealedand was being rewritten in #130. Folding them together is flagged as a follow-up in the code. CatchAiApiExceptionin these UI issues.Unverified
POST /api/ai/generatewas never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the rawcreatedelement for read-after-write. Whichever of #12–#15 lands first should verify it against a real account and tighten the model.Implemented in #177 — work continues in the PR from here.
PoweredDocumentPanelcovers all fourcontext.modevariants behind #10's gate, hosted inDocumentsView.xamlwith a single line and no code-behind edit (#139 is in that file too). All four reference-rejection paths were verified live and cost nothing, so their exact server wording — "List not found.", "A list must be selected.", "Document not found.", "Only http(s) URLs are supported." — is what the user sees, rendered against the source picker rather than in the generic notice slot. Attribution branches on mode plus error code, never on prose: since every client-knowable input problem is rejected locally first, a survivinginvalid_inputin a derived mode can only be the reference being missing or not theirs.Two live findings shaped this. An unrecognized
context.modeis not cheap-rejected — it reaches the model and bills a unit — so mode stays a four-value enum end to end. And the returned draft contained a GFM pipe table, so the hand-rolledFlowDocumentrenderer needed table support; without it those rows would have collapsed into a paragraph of pipes and dashes. The renderer's line-classification predicates were split into a WPF-free file so they could be tested against the real markdown off-Windows — 25 assertions, all passing.POST /api/ai/generatewas not called (shared account), so the confirm path re-fetches withGET /api/documents/{id}; the{documentId}field name and a realresearch_urlfetch are the two things still unverified.
feature: "powered_document"drafts a full markdown document. Four modes, set incontext.mode:context.modearticle(default)inputalonefrom_listcontext.listIdfrom_articlecontext.documentIdresearch_urlcontext.urlYou pass a reference, not source text. Derived modes are IDOR-guarded — a missing or non-owned reference returns
422 invalid_input. Server-fetched context is truncated before it reaches the model./suggest→kind: "document";/generate→{documentId}.Acceptance criteria
Views/DocumentsView.xaml, gated onIsAiAvailable.from_list/from_articlepick their source from the user's own lists/documents;research_urltakes a URL with http/https validation./api/ai/generateand opens the created document from a freshGET.422 invalid_inputon a missing/non-owned reference is reported against the source field, not as a generic failure.