You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AI: Powered Document (4 modes: article / from list / from article / research URL) #15
feature: "powered_document" drafts a full markdown document. Four modes, set in context.mode:
context.mode
Also requires
Source
article (default)
—
drafts from the topic in input alone
from_list
context.listId
loads your list's schema + up to 50 rows server-side
from_article
context.documentId
loads your document's title + body server-side
research_url
context.url
fetches the page (http/https, SSRF-guarded), extracts title/description/text, cites the URL
You 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.
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. quota is not the same object on both endpoints. GET /api/ai/status → {usedToday, dailyLimit, remaining}. POST /api/ai/suggest → {usedToday, dailyLimit} with no remaining (verified twice). So AiQuota.Remaining is int?, and a post-suggest quota chip cannot read .Remaining — bind RemainingOrComputed instead. #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. The provider field on /generate is audit-ledger-only and does not select anything, and the openai/gemini entries in defaultModels are dormant and unreachable. Don't surface a provider choice.
3. crossPost has six fields, not two — crossPostToBluesky, selectedMastodonIds, crossPostToLinkedIn, selectedLinkedInTargets, crossPostToTwitter, linkedInLinkAsFirstComment. Both crossPost and scheduleImmediately are message_series-only (relevant to #12).
4. A sixth 422 code exists that the epic didn't list: refused. Treat it like invalid_ai_output. Modelled and marked retryable.
5. Quota accounting is asymmetric, and it shapes the UI.
Input-validation 422s cost nothing (usedToday stayed 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 missing powered_document source ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.
Also: context.spacingMinutes is accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.
Note on the exception type
The code is surfaced on a new AiApiException, not on InterlinedApiException — the latter is sealed and was being rewritten in #130. Folding them together is flagged as a follow-up in the code. Catch AiApiException in these UI issues.
Unverified
POST /api/ai/generate was never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the raw created element 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.
PoweredDocumentPanel covers all four context.mode variants behind #10's gate, hosted in DocumentsView.xaml with 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 surviving invalid_input in a derived mode can only be the reference being missing or not theirs.
Two live findings shaped this. An unrecognized context.mode is 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-rolled FlowDocument renderer 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/generate was not called (shared account), so the confirm path re-fetches with GET /api/documents/{id}; the {documentId} field name and a real research_url fetch are the two things still unverified.
Activity
Adron commentedon Sep 16, 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. The
providerfield 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-validation
422s 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.Adron commentedon Sep 16, 2026
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.