Repository navigation
AI chat: answers from your data and drafts invoices from one sentence (ai-assistant + ai-chat-v2) - #10
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…the composer move, and startedFrom (T20)
StartPanel becomes SCR-02's centred empty state: the greeting "Hi! What can I
help you with?" (or "Draft an invoice"), a max-2xl Composer with the counter
under it, and the four fixed suggested requests (suggestion 1 only for
start=draft) that only fill and focus the box; at left = 0 the greeting stays
above the used-up AllowanceNotice (hand-made link for start=draft). Composer
gains placeholder, inputRef and autoFocus.
components/ai/start-point.ts maps the /ai address to { start, startedFrom }:
start=draft&from=invoices or no from -> INVOICE_LIST, from=dashboard ->
DASHBOARD_CREATE, an unknown from -> AI_SECTION, never DASHBOARD_PROMPT. The
page passes it to ChatIsland, whose transport adds startedFrom to a new
Conversation's first message only (AI_SECTION omitted; never once the id is
known). On the first send the docked composer FLIPs from the centred
position with a 250 ms translate transition, or jumps under
prefers-reduced-motion (data-composer-move="none"). protectedRoutes.aiStartDraft
is now /ai?start=draft&from=invoices. design-system.md rows for StartPanel,
ChatIsland and Composer updated; component and e2e expectations moved to the
new copy and link.
SDD-Task: T20
SDD-AC: AC-18
SDD-AC: AC-30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…control, and pin the streamed reply (T21) useFollowLatest (components/ai/use-follow-latest.ts) replaces the old scrollIntoView effect: isAtBottom treats an end no more than 48 px out of view as at the bottom; while there, every new message or delta is scrolled into view before paint; further up nothing moves and a floating "Jump to latest" Button appears above the composer (smooth scroll, instant under reduced motion). A send always scrolls to the end. A 429 on the first message now returns to the centred StartPanel with the server text verbatim (SCR-02 "used-up on send"). Component tests pin the word-by-word streaming (first word at once, >= 3 growing steps) and the whole saved reply on reopen; WORD_DELAY_MS is unchanged. T39 R-23 scrollIntoView tests are superseded by tests/component/ai-follow-latest.test.tsx. SDD-Task: T21 SDD-AC: AC-16 SDD-AC: AC-33 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the chat island (T22)
OfferedChoices (components/ai/offered-choices.tsx, SCR-04) renders an
OfferView's stored labels verbatim in a kind-labelled group: skeleton row
while the labels load, pressable only when OPEN and the reply is the latest,
a spinner on the pressed option with the group locked, chosen = secondary +
check + "(chosen)", inactive = disabled with none marked.
ChatIsland keeps `offers` from the page load (/ai/[id] now passes
getConversation's offers) and from loadConversationCards, maps data-offer
parts, and a press sends { conversationId, message: label, choice } with no
origin or startedFrom. 409 CHOICE_UNAVAILABLE drops the optimistic message
and shows the contract sentence in an info Alert; 429 drops it and shows the
used-up notice; a failed press leaves the buttons pressable and Try again
re-sends the same choice. Registered in docs/design-system.md.
SDD-Task: T22
SDD-AC: AC-08
SDD-AC: AC-10
SDD-AC: AC-11
SDD-AC: AC-12
SDD-AC: AC-12b
SDD-AC: AC-13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview button (T23)
InvoiceStateProvider (components/ai/invoice-state-context.tsx, ADR-0008) is
the workspace's one invoice-state store: AiWorkspace holds it and the island
shares it (an inner provider passes through, so the island still works on
its own). useSeedInvoiceStates seeds it from each page load (invoiceLinks +
created cards); createDraftFromCard results are applied by DraftCard;
loadInvoicePreview / issueFromPreview results will be applied by the
preview panel (T24). DraftCard's created states and ChatLink read the store
first, so the card and every link agree after an issue (AC-23) or an
unavailable result (AC-21).
The created card shows "Open in editor →" (renamed from "Open draft →") and
an outline "Preview" button (eye icon) that pushes ?preview={cardId}
(components/ai/preview-address.ts, ADR-0005); aria-expanded + secondary
while open, pressing again closes. The issued state keeps both. Component
and e2e expectations moved to the new link name.
SDD-Task: T23
SDD-AC: AC-21
SDD-AC: AC-23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sktop and phone (T24)
InvoicePreviewPanel (SCR-06) opens from ?preview={cardId}: AiWorkspace
renders it as a grid column from md (md-xl chat | preview with the list
behind the list button, xl list | chat | preview) and as a full-height
bottom Sheet below md. It loads with loadInvoicePreview(cardId) on open,
Try again and after CHANGED_ELSEWHERE, shows the editor's document
(PDFPreviewPanel), and issues through ConfirmationModal + IssueConfirmation
(SCR-07: Customer, total, currency from the loaded preview, the fixed
sentence) -> issueFromPreview(invoiceId, version). Every SCR-06 state is
routed with the contract texts verbatim: issued now (toast), already
issued, reloaded (outdated), blocked (editor message + field errors, Open
in editor primary), issue failed, no longer available, not found, load
failed. Results are written to the invoice-state store, which now also
maps cards to their invoices so a card-scoped UNAVAILABLE reaches the card
and its links. Focus goes to the heading on open and back to the card's
Preview on close. Registered in docs/design-system.md.
SDD-Task: T24
SDD-AC: AC-19
SDD-AC: AC-20
SDD-AC: AC-21
SDD-AC: AC-22
SDD-AC: AC-23
SDD-AC: AC-25
SDD-AC: AC-26
SDD-AC: AC-26b
SDD-AC: AC-27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…group (T25)
The main section's "AI" item is removed. A new "AI" group (type group,
styled like "Management") sits between the main section and Management
with "AI chat" (/ai, sparkles) and "Connect your AI"
(/settings/assistants, new `plug` icon key -> PlugIcon). Existing path
matching marks the item of the current page active (/ai and /ai/{id} ->
AI chat). The old "Sidebar entry" test and the e2e sidebar click move to
"AI chat".
SDD-Task: T25
SDD-AC: AC-28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test Conversations and Create invoice with AI (T26)
DashboardAiBlock (components/dashboard/ai-block.tsx, SCR-09) renders in its
own Suspense after the banners via AiBlockSection -> getDashboardAiBlock
(allowance + listLatestConversations, owner-scoped, read-only). Left: the
prompt box ("Message to AI", "Send to AI", Enter sends, "Type a message
first.", the 2,000-character rule with "{n} / 2,000", the counter), or the
used-up notice with "Create it by hand →". Right: up to three recent
Conversation links or "No conversations yet...", and "All conversations →".
DashboardHeader gains "Create invoice with AI" (/ai?start=draft&from=
dashboard) before "Create Invoice".
A valid prompt is stored under a one-time sessionStorage id
(components/ai/ai-handoff.ts, ADR-0009) and opens /ai?handoff={id}; the
text never enters the address. ChatIsland takes and deletes the entry once
(a ref guards the double effect), drops ?handoff= with replaceState and
sends it with origin DASHBOARD + startedFrom DASHBOARD_PROMPT straight into
the Conversation layout; a missing / taken / blocked entry shows the plain
start screen and sends nothing. The mcp-key e2e now clicks the banner's
exact "Connect your AI →" (the T25 sidebar item also matches the old regex).
SDD-Task: T26
SDD-AC: AC-29
SDD-AC: AC-30
SDD-AC: AC-31
SDD-AC: AC-32
SDD-AC: AC-36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and Offered choices (T27)
getAccountExport now reads each Conversation's startedFrom and, per
message, its origin (null on assistant messages and on rows saved before
the backfill) and its offer: kind, options as { optionId, label }, state,
chosenOptionId and createdAt. The offer's stored draft request and each
option's record ref never leave the database. exportVersion goes 2.2 ->
2.3 (additive); invoices already carry issuedFromChat.
The living UserDataExport schema (security-patch openapi.yaml, extended
in place as ai-assistant did for 2.2) is bumped to 2.3 with the new
fields, so every existing export contract check now validates 2.3.
ChoiceOffer rows cascade with their Conversation, message and user, so
account and Conversation deletion need no new statement; the new
integration test proves 0 offers / messages / Conversations after
deleteAccount and that deleting one Conversation keeps its invoice.
Note: the tracker already showed T27 as done (set by the T26 commit), but
the code had not been written; this commit is the implementation.
SDD-Task: T27
SDD-AC: AC-34
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d shares; terms page checked (T28) The privacy page's "11. AI assistant" section now states, in the page's own list style: what the chat stores (Conversations with messages and draft cards, the options offered as buttons and the one chosen, prompts sent from the dashboard, how each message was sent, where each Conversation was started); retention (90 days after the last message, or until the Conversation or account is deleted); that only the Google Gemini model through the Vercel AI Gateway processes message text, with no new processor; and what an AI client connected with a Personal key can read (sender profile names and defaults; bank name, last four digits and currency, never a full account number; products with prices and Custom prices; Customers found by name). The existing T05 statements stay. Terms page checked: nothing contradicts the chat (sections 1 and 3 make the Freelancer the issuer who verifies all data). Recorded under spec §8 with one pre-existing gap for the owner: the sub-processor table (§10) does not list the model provider. The task is in review until the owner approves the wording before ship. SDD-Task: T28 SDD-AC: AC-35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the own-profile setup and this feature's rows (T29)
dataset.ts: a zod noGuessingItemSchema (strict) checks every row on load;
optional explains "own-sender-profile" and press { label }, kind "issue",
and the own-profile-name setup (Studio USD default, Consulting FOP UAH,
Acme Corp and Acme GmbH).
run.ts: seeds own-profile-name and an Acme draft on base; snapshots the
Freelancer's invoice statuses around every turn (AC-24); records the
reply text, the card's Customer and any Customer offer labels. A press
row replays its history through POST /api/chat (session mocked to the
row's seeded Freelancer via vi.doMock before the route is imported) and
then presses the option found by label with a `choice` - never typed
text, never an origin (the server sets PRESSED). Smoke-tested once with
the scripted model against a throwaway Postgres. ASSISTANT_EVAL=dry
(pnpm eval:assistant:dry) validates the datasets with no model.
score.ts: noGuessingRowFailures with the three-part own-sender-profile
rule (no card; not resolved as the Customer; the reply names Consulting
FOP with "sender profile" / "профіль" and asks for the Customer), the
status-change check and the pressed-record rule. A failing explains row
lowers the question rate.
no-guessing.jsonl: the AC-05b (en, uk, folded), AC-05 (two), AC-24 and
AC-10 press rows. The real-model run is a pre-release step (not run).
SDD-Task: T29
SDD-AC: AC-05
SDD-AC: AC-05b
SDD-AC: AC-10
SDD-AC: AC-24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the full-height AI section Found by the T30 e2e run: since T19 the AI section fills the viewport and docks the composer at the very bottom, where the fixed cookie banner (shown until the cookies are accepted) covered the message box and its Send button. A Freelancer who had not pressed Accept could not click Send (ai-assistant AC-35 hung on it), contradicting AC-15 "the message box stays visible at the bottom". CookieBanner now publishes its measured height (ResizeObserver) as --cookie-banner-height on <html> while it shows and removes it on Accept/unmount; the AI layout subtracts it from its height. Other pages are unchanged. Component test for the variable; the T30 AC-15 e2e checks at four sizes that the box and Send are not covered. SDD-Task: T30 SDD-AC: AC-15 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out, empty start, phone sheet and streaming (T30) tests/e2e/ai-chat-v2-chat.spec.ts against the production build with the scripted stub model: - AC-08: "invoice Acme for 20 hours at $50" with Acme Corp + Acme GmbH shows the "Choose a Customer" group; pressing Acme GmbH sends the label (saved PRESSED) and gives a card whose customerId is Acme GmbH's. - AC-13: a seeded Conversation reopens with three account options and the pressed one "(chosen)"; the latest reply's open button for a since-deleted account is refused with the fixed words, nothing counted. - AC-15 at 360x640, 768x1024, 1280x800, 1440x900 with 40 Conversations / 60 messages: page scrollHeight <= innerHeight, only chat-scroll and the list move, the box and Send stay in view and uncovered. - AC-18: greeting + suggestions, a suggestion only fills the box, nothing saved before the first message, then the box docks below the reply and the Conversation is listed. - AC-19: at 360x640 the list opens as the "Conversations" sheet and closes on a pick. - AC-33: >= 3 growing partial texts before the whole reply; reopened whole at once. - Linux baselines (new): SCR-02 empty state, SCR-03 with SCR-04 pressable and chosen, drawn by the v1.63.0-noble browser and matched in three runs. lib/ai/e2e-stub-model.ts: a USD Acme request that asks back when the resolver reports issues, a short answer after a pressed choice, and a three-burst reply with 400 ms pauses for AC-33. Existing sentences keep their replies. SDD-Task: T30 SDD-AC: AC-08 SDD-AC: AC-13 SDD-AC: AC-15 SDD-AC: AC-18 SDD-AC: AC-19 SDD-AC: AC-33 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings "Connect your AI" link ai-assistant-flows Linux baselines (ai-section-default, ai-draft-card-live, ai-draft-card-created) re-drawn with the v1.63.0-noble browser; every difference is this feature's intended UI: the sidebar "AI" group (T25), the grouped, searchable Conversation list with New chat (T19), the new empty-state start (T20), the card's Preview / "Open in editor →" (T23), and the composer kept above the cookie banner (the T30 fix). Matched in three consecutive runs. The editor-layout baselines still match and are unchanged. mcp-key-flows "settings navigation" now clicks the settings page's own link inside main: the T25 sidebar item has the same name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rsation address
Found by the T31 e2e run (the flagged "pushState vs useSearchParams in
Next 16" risk was real). The App Router patches history.pushState /
replaceState so usePathname and useSearchParams follow an outside write,
but skips that sync for a state carrying its own __NA marker. openPreview,
closePreview and the island's two in-place swaps (/ai -> /ai/{id} on the
first reply, dropping ?handoff=) all passed window.history.state, which
carries it. So:
- pressing Preview pushed ?preview= but the panel never opened (only a
reload showed it);
- the router kept /ai?start=draft as its address and wrote it back after
the next server action (e.g. Create draft): a reload then showed an
empty draft start instead of the Conversation.
preview-address.ts gains replaceAddress(); every write now hands the
router a state without its keys (only our aiPreview mark), so it syncs.
Consequence handled: Back then Forward to a Conversation started in place
restores an entry holding the /ai page's tree, which mounted the empty
start screen under /ai/{id}. ChatIsland now notices it mounted with no
Conversation at a Conversation address, shows a skeleton and refreshes
once, so the server renders the saved Conversation.
Trade-off (for review): because the router now knows /ai/{id}, the first
revalidating server action of a Conversation started in place (normally
Create draft) re-renders it as the /ai/{id} page: one remount with a
brief skeleton, scroll at the end. Before, there was no remount but the
address silently fell back to /ai?start=draft.
Unit test for the history states; island tests for the stale-entry
refresh; two component tests now render the /ai page at /ai.
SDD-Task: T31
SDD-AC: AC-20
SDD-AC: AC-22
SDD-AC: AC-29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the T31 e2e run at 360 x 640: the Sheet's bottom side sets data-[side=bottom]:h-auto, which is more specific than the workspace's plain h-dvh, so the preview sheet grew to its content (774 px) and its header ran 134 px above the screen: the invoice number and "Close preview" could not be reached. The same-variant data-[side=bottom]:h-dvh now wins. The T31 e2e asserts the sheet box is exactly the viewport and its heading and Close are in view. SDD-Task: T31 SDD-AC: AC-19 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…row's date has enough contrast Found by the T31 accessibility pass (axe): the grouped list's ItemGroup is role="list" but its rows were plain divs (critical aria-required- children), and the open row's muted date on the accent background failed colour contrast (serious). Rows now carry role="listitem"; the open row's date uses the accent foreground. Component test for the list semantics. SDD-Task: T31 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ddress, navigation, dashboard, privacy, MCP reads, AC-36 and accessibility (T31) tests/e2e/ai-chat-v2-preview-dashboard.spec.ts (production build, stub model): - AC-20/AC-23: Preview opens beside the messages (<= 1.5 s, measured and annotated); Issue after a dialog naming Acme Corp, 1,000.00 USD and the "can no longer change" line; panel, card and DB agree (PENDING, same number, issuedFromChat); the AI mark leaves the invoices list. - the preview address: ?preview= pushed (Back closes, Forward reopens, reload reopens, Close drops it), and the Conversation address survives the Create action and a reload. - AC-19/SCR-06: a column at 768 px, a full-screen sheet below (exact viewport box, heading and Close in view, same actions). - AC-26 / AC-26b with a second editor tab: already issued -> nothing issued again; changed -> outdated notice, nothing issued, a new confirmation issues it. - AC-28 nav group; AC-29 dashboard prompt (no start screen on the way, DASHBOARD origin, DASHBOARD_PROMPT, counted once, Back/Forward does not resend); AC-30 latest three + All conversations + Create invoice with AI; AC-35 privacy page for a Visitor. - Linux baselines SCR-06 (1440x900; the document body masked, the number normalised) and SCR-09, matched in three runs. mcp-key-flows: AC-06 read-only key calls the three reads through the proxy: ids, last four only, never the full number. route-sweep: AC-36 anonymous /ai, ?preview=, ?handoff=, dashboard go to sign-in; /api/chat and loadInvoicePreview / issueFromPreview are 401. accessibility: AI section empty and with Offered choices, the preview with its issue dialog, the dashboard AI block, desktop and phone; the helper waits for running animations before measuring. ai-draft-card-created baseline re-taken: after Create the Conversation is shown from its own page, scrolled to the end (see the router fix). SDD-Task: T31 SDD-AC: AC-06 SDD-AC: AC-19 SDD-AC: AC-20 SDD-AC: AC-23 SDD-AC: AC-26 SDD-AC: AC-26b SDD-AC: AC-28 SDD-AC: AC-29 SDD-AC: AC-30 SDD-AC: AC-35 SDD-AC: AC-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w sheet height and the history-write rule Registers what the T30/T31 fixes established for AiWorkspace and ConversationList: the AI shell subtracts --cookie-banner-height, the bottom preview Sheet needs the same-variant data-[side=bottom]:h-dvh, history writes go through preview-address.ts without the App Router's keys, and list rows are listitems with a contrast-safe open-row date. No new components in this batch. SDD-Task: T31 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as a symbol or word
The stored offer request keeps the currency as the Freelancer wrote it
("$", "€", "dollars"), but the press check compared it with the record's
code as text, so every bank-account or product press after "at $50" was
refused as unavailable. The check now reads it like the resolver does.
Review 2026-10-13 R1-B1.
SDD-AC: AC-08b
SDD-AC: AC-13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n saved a newer offer A typed turn in another tab could store its own OPEN offer while a press ran; the press's release then tried CHOSEN -> OPEN, hit the one-open-offer index and rolled back, so the slot stayed used and the offer stayed marked. The release now locks the Conversation and, when a newer offer is open, leaves the claimed one CLOSED and unmarked. Review 2026-10-13 R1-B2. SDD-AC: AC-11 SDD-AC: AC-12 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g links to the add page AC-05 / SCR-03 "answered - nothing matches": the add-a-Customer link now comes from code when list_customers or list_products is filtered by a name and matches nothing, not only when the model also calls find_in_app. The app map gains the add-product page (/products/new), and the two links are titled "Add a Customer" and "Add a product" as screens.md names them. Review 2026-10-13 R1-B3, R1-U5. SDD-AC: AC-05 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as TYPED A USER row written before the origin backfill or during the deploy window exported origin null, while the reopen view reads it as TYPED. AC-34 asks the export to say how each message was sent. Review 2026-10-13 R1-S4. SDD-AC: AC-34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oice shows it The dialog read the live Customer record, so after a rename it named a different Customer than the previewed document (which uses the invoice's own customerName copy). Review 2026-10-13 R1-S2. SDD-AC: AC-23 SDD-AC: AC-26b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ger available" checkChoice caught every error as a refusal, so an outage told the Freelancer a valid option was gone (409) and would trip the unavailable presses alert. It now answers failed, and the route returns 503 FAILED. The AC-08b route test also runs with the currency passed as "$". Review 2026-10-13 R1-S3 (and the R1-B1 route test gap). SDD-AC: AC-12b SDD-AC: AC-08b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lace the pinned card or offer The pins covered only the chosen record. A re-call could replace the pinned card's lines and prices, or turn a pinned bank-account offer into a card with an account the model picked itself. In a pressed turn the re-call now gets the pinned result back and changes nothing. The T13 test that expected a model-picked account to win is updated to the new rule. Review 2026-10-13 R1-S1. SDD-AC: AC-08b SDD-AC: AC-10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on, not the faintest Native disabled faded every button through the shared Button's disabled:opacity-50, and secondary equals the reply bubble's colour, so the chosen option read fainter than the unchosen ones. Buttons that can't be pressed are now aria-disabled (Base UI focusableWhenDisabled: still focusable, a click does nothing), only the unchosen ones are muted, and the chosen one keeps full strength with a border edge (SCR-04 chosen). Review 2026-10-13 R1-U1. SDD-AC: AC-12 SDD-AC: AC-13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… /ai
The list only refreshed after a delete, so deleting the open Conversation
(or all of them) from a row or "Delete all" refreshed /ai/{id} into the
not-found page. SCR-01 "deleted" says it navigates to /ai, as the header
delete already did.
Review 2026-10-13 R1-U3.
SDD-AC: AC-17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own-profile notice checked, answered ids tested (T123) SDD-Task: T123 SDD-AC: AC-05b, AC-10, AC-14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ried over, no false 'fills' promise (T124) SDD-Task: T124 SDD-AC: ai-assistant AC-14b, AC-45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own-profile answer carries the exact question (T125) SDD-Task: T125 SDD-AC: AC-05b SDD-AC: AC-37 SDD-AC: AC-46 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ric-looking names are searched, card edits need a card (T126) SDD-Task: T126 SDD-AC: AC-14 SDD-AC: AC-47 SDD-AC: AC-45 SDD-AC: ai-assistant AC-15 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a draft in progress (T127) SDD-Task: T127 SDD-AC: ai-assistant AC-14b SDD-AC: ai-assistant AC-15 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eads the currency like the resolver, logs failures, asks every field (T128) SDD-Task: T128 SDD-AC: AC-46 SDD-AC: AC-37 SDD-AC: AC-09 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDD-Task: T128 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…match error path tested (T129) SDD-Task: T129 SDD-AC: ai-assistant AC-14b SDD-AC: ai-assistant AC-15 SDD-AC: AC-46 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rch before linking a named invoice (T130) SDD-Task: T130 SDD-AC: AC-05b SDD-AC: AC-37 SDD-AC: AC-48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… instruction aligned with the owner decision (T131) SDD-Task: T131 SDD-AC: AC-05b SDD-AC: AC-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts a stray question (T132) SDD-Task: T132 SDD-AC: AC-05b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w recorded as a known limitation (T133) SDD-Task: T133 SDD-AC: AC-14 SDD-AC: AC-05b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fixed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chat contract SDD-Task: T115 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
Feature description
This release adds an AI chat to Invoice Forge.
The chat answers questions about the data of the Freelancer. For example: "who owes me money?"
The chat also prepares a Draft invoice from one sentence. For example: "invoice Acme for 20 hours of consulting at $50".
The chat shows a Draft card first. A Draft invoice exists only after the Freelancer clicks Create.
If a value is missing or not clear, the chat does not guess it. The chat shows one Question form. The options in the form are records of the Freelancer. The server checks each option.
The chat cannot issue, send, change or delete an invoice. Only the Freelancer can do these actions.
The chat reads only the data of the Freelancer who is signed in. The model cannot change this.
The Freelancer can send 30 messages each day and 10 messages each minute.
An external Assistant can also make a Draft invoice through MCP. It must use a Personal key with Draft permission. This permission is off by default.
The chat uses the Claude Haiku 5.5 model through the Vercel AI Gateway. The model does not train on the messages. Anthropic usually deletes the message text in 30 days. The app and the database stay in the EU. The model works outside the EU, mainly in the USA.
The app deletes a Conversation 90 days after its last message.
Summary
Ships the in-app AI chat in two iterations as one release: ai-assistant (chat, Draft cards, MCP Draft permission) and ai-chat-v2 (Question forms, Markdown replies, preview and issue in the chat, full-height workspace), merged in from #11. The chat runs on Claude Haiku 5.5 through the Vercel AI Gateway on paid terms (ai-chat-v2 ADR-0014). Specs: ai-assistant, ai-chat-v2.
Acceptance criteria
ai-assistant (50)
ai-chat-v2 (60)
search_invoicesfilters and sorts by the day created ✓ ("last draft created in January": eval row)Design
docs/features/ai-assistant/spec.md,docs/features/ai-chat-v2/spec.md(each withux-flows.md,screens.md)docs/features/ai-assistant/sad.md,docs/features/ai-chat-v2/sad.mddocs/features/ai-assistant/adr/(ADR-0001 … ADR-0013; ADR-0003 superseded),docs/features/ai-chat-v2/adr/(ADR-0001 … ADR-0014; ADR-0014: Claude Haiku 5.5 through the AI Gateway, Anthropic-only pin, no prompt training, no zero data retention on the Hobby plan)docs/features/ai-assistant/data-model.md,docs/features/ai-chat-v2/data-model.md. 20 migrations,prisma/migrations/20261010100000_create_conversation…20261015100000_add_chat_message_limit_scopedocs/features/ai-assistant/contracts/openapi.yaml,docs/features/ai-chat-v2/contracts/openapi.yaml0.3.0 (POST /api/chat, MCPcreate_draft_invoice,list_sender_profiles,list_products,list_customers), pluscontracts/server-actions.mdin eachdocs/features/ai-assistant/test-plan.md,docs/features/ai-chat-v2/test-plan.mddocs/features/ai-assistant/_review/,docs/features/ai-chat-v2/_review/— the last rounds (review-2026-10-16-*) are PASSdocs/features/ai-assistant/_ship/security-review-2026-10-12.md,docs/features/ai-chat-v2/_ship/security-review-2026-10-13.md,docs/features/ai-chat-v2/_ship/security-review-2026-10-14.md(with the ADR-0014 addenda and resolutions)Tasks (SDD-Task trailers)
188 commits carry
SDD-Tasktrailers (git log --grep SDD-Task origin/main..HEAD).Verification
All runs on macOS with Docker, at 32ae635.
tsc --noEmit)pnpm eval:assistant, Claude Haiku 5.5)AC-14-many-studios: the count is written as a word ("Twelve"). This is accepted by owner decision.AC-05b-own-profile-folded: a known model limitation. Haiku shortens "consulting fop" to "fop", and the own-profile hint stays exact-only by design.disallowPromptTrainingworks on the team's plan, and thatzeroDataRetentionis refused on Hobby (hence T113).Operational notes
prisma migrate deploywith the direct (non-pooler) prod URL, thenpnpm mastra:migrate(creates themastraschema; safe to repeat).20261015100000_add_chat_message_limit_scopeis required by the per-minute limit; without it every chat message answers 503.docs/features/ai-assistant/migrations/anddocs/features/ai-chat-v2/migrations/.AI_GATEWAY_API_KEY, set for Preview and Production.AI_CHAT_MODEL, which acceptsanthropic/*models only.GOOGLE_GENERATIVE_AI_API_KEY; nothing reads it.E2EorAI_CHAT_MODEL=stub:e2e./api/cron/purge-limitsalso purges Conversations (90 days), repeat marks (24 h), traces (30 days), and turn and counter rows (400 days). It is reported to Sentry Crons aspurge-limits.gh workflow run test.yml --ref <branch>). This works once this workflow is onmain.ai,@ai-sdk/gateway,@ai-sdk/react,@mastra/*,react-markdown,remark-gfm, all on exact pins.purge-limitsmonitor after the first night.🤖 Generated with Claude Code