Skip to content

AI chat: answers from your data and drafts invoices from one sentence (ai-assistant + ai-chat-v2) - #10

Merged
GopkoDev merged 403 commits into
mainfrom
feat/ai-assistant
Oct 16, 2026
Merged

GopkoDev merged 403 commits into
mainfrom
feat/ai-assistant

Conversation

@GopkoDev

@GopkoDev GopkoDev commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Feature description

Written in ASD-STE100 Simplified Technical English. Freelancer, Customer, Assistant, Draft card, Question form, Create and Personal key are technical names.

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)

  • AC-01 — the AI section opens ready to use, with examples, messages left and a link to connect your own AI client ✓
  • AC-02 — Debtors and overdue totals per currency equal the dashboard ✓
  • AC-03 — one invoice by number, with a link that opens it ✓
  • AC-04 — a partial list is called partial, with the total count; no total from a partial list ✓
  • AC-05 — the same overdue rule and the same "today" as the dashboard (Kyiv) ✓
  • AC-06 — an off-topic question gets a one-sentence refusal and counts ✓
  • AC-06b — a mixed message gets the draft card plus a one-sentence refusal, counted once, not as off-topic ✓
  • AC-07 — "where do I change the logo" links to the sender profiles; only real pages are linked ✓
  • AC-08 — "how much did Acme pay this year" is answered from a read question, never added up by the model ✓
  • AC-09 — 18 used + 1 → 11 left ✓
  • AC-10 — a used-up allowance refuses the message and names the renewal time in the Freelancer's zone ✓
  • AC-10b — two tabs sending the last message at once: exactly one is processed ✓
  • AC-11 — a system-side failure says so, invites a retry, and does not count ✓
  • AC-11b — a question past the 8-step cap gets a partial answer, says so, and counts ✓
  • AC-11c — Create works with no messages left ✓
  • AC-11d — a reply is never empty: with no words and no card, the chat asks to rephrase in one sentence (added by fix 2026-10-12) ✓
  • AC-12 — the draft card: Customer, 20 × 50.00 USD, named defaults, today + 30 days, no tax/discount/shipping, number on creation ✓
  • AC-13 — Create makes exactly the card's Draft invoice and links to it; nothing is issued or sent ✓
  • AC-14 — a second Create, or a reconnect repeat, creates nothing and links to the first draft ✓
  • AC-14b — a replaced card cannot create and points to the newer card ✓
  • AC-15 — text stored in the Freelancer's data never produces a card ✓
  • AC-16 — no price named → the Customer's custom price, named as the source ✓
  • AC-16b — a named price wins (catalogue price shown beside it); a different currency gives a free-text line and a question ✓
  • AC-17 — no sender profile or bank account → explained with a link, no card; MCP is refused the same way ✓
  • AC-18 — an outdated card is rebuilt and must be confirmed again ✓
  • AC-19 — "600 EUR" uses the EUR bank account and names quantity 1 and unit "item" as defaults ✓
  • AC-19b — a named bank account in another currency is never used; the chat says why and links to adding an account (added by fix 2026-10-12) ✓
  • AC-20 — two matching Customers → the chat asks which, naming both ✓
  • AC-21 — no matching Customer → a link to add one; no Customer is created ✓
  • AC-22 — a line without an amount → the chat asks for quantity and price ✓
  • AC-23 — Assistant drafts are marked (chat or key name at creation); the mark goes away on issue ✓
  • AC-24 — "send it" / "issue it" → only the Freelancer can, with a link; the draft stays a draft ✓
  • AC-25 — a new key can be created with Draft permission and is listed as able to create drafts ✓
  • AC-26 — an existing key can never gain Draft permission; the page explains how to get one ✓
  • AC-27 — the 51st draft of the day is refused with how to lift the limit; reading keeps working ✓
  • AC-28 — the Freelancer can lift a reached limit by 50 for today, repeatedly ✓
  • AC-29 — a read-only key is refused and nothing is created ✓
  • AC-30 — a Draft-permission key creates the draft with the editor's defaults, listing each one ✓
  • AC-31 — ambiguous or missing values are refused, naming what to ask; nothing is created ✓
  • AC-32 — a repeat within 24 h returns the first draft; the same mark with other content is refused ✓
  • AC-33 — "Create with AI" on the invoice list opens a Conversation ready to draft ✓
  • AC-34 — with no messages left, "Create with AI" says so up front and links to the editor ✓
  • AC-35 — Conversations are listed newest first, reopen in full and can be continued ✓
  • AC-36 — a reopened Conversation shows the draft's current state (issued or gone) ✓
  • AC-37 — deleting one or all Conversations leaves every draft as it was ✓
  • AC-38 — Conversations are deleted 90 days after their last message, and the list says so ✓
  • AC-39 — the export includes Conversations; account deletion removes them ✓
  • AC-40 — another Freelancer's Customer is answered as if it did not exist ✓
  • AC-41 — another Freelancer's Conversation link is "can't be found", like a missing one ✓
  • AC-42 — a Visitor is sent to sign in; nothing is processed or counted ✓

ai-chat-v2 (60)

  • AC-01 — the chat lists sender profiles and their accounts, with the defaults marked and no full account number ✓
  • AC-02 — a product's standard prices and a Customer's custom price, exactly as stored ✓
  • AC-03 — a Customer saved with a Cyrillic letter and two spaces is found as "oohn doe" ✓
  • AC-04 — a draft under another sender profile and its UAH account, both named as taken from the request ✓
  • AC-05 — no matching Customer or product: said plainly, with a link to add one ✓
  • AC-05b — a sender profile's name is never taken for a Customer; the chat explains the difference and asks ✓
  • AC-06 — a read-only Personal key gets the same reads with ids and no full account number ✓
  • AC-07 — the reads hold only the Freelancer's own records ✓
  • AC-08 — two matching Customers become a form question with both and "Other…"; Send posts "Customer: Acme GmbH" and the card is for Acme GmbH ✓
  • AC-08b — the Customer and the bank account in one form; Send waits for both; one Send gives the card and counts once ✓
  • AC-09 — options only from records the server checked, whatever the model writes or names by id ✓
  • AC-10 — look-alike Customer options show their emails; the picked one is used exactly ✓
  • AC-11 — one Send counts once; with the allowance used up, a Send is refused like a typed message and marks nothing ✓
  • AC-12 — older forms are read-only and show the answers that were sent ✓
  • AC-12b — a forged, outdated or foreign form Send never reaches the model, is not counted, and gets the same words ✓
  • AC-13 — a reopened Conversation shows its sent form read-only; the latest unsent form turns down a deleted account without counting ✓
  • AC-14 — 12 matches: an input-only question with the count and a request for a more exact name ✓
  • AC-15 — only the panes scroll; the page never does; the message box stays visible ✓
  • AC-16 — follow new text within 48 px; "Jump to latest" after scrolling up ✓
  • AC-17 — Today / Yesterday / Previous 7 days / Older, a look-alike-aware search and New chat ✓
  • AC-18 — the greeting start, the composer move (none under reduced motion), suggestions only fill, empty Conversations are not saved ✓
  • AC-19 — the phone layout: list sheet, full-screen preview sheet ✓
  • AC-20 — Preview opens the editor's preview with "Open in editor" and Issue ✓
  • AC-21 — a deleted draft's preview says it is no longer available, with no Issue ✓
  • AC-22 — another Freelancer's preview is "can't be found" ✓
  • AC-23 — issue after one confirmation through the editor's path; the number is kept; card, preview and links show issued ✓
  • AC-24 — "issue it" in words still only points to the preview or editor ✓
  • AC-25 — an invoice-integrity rule blocks with the editor's message; "Open in editor" ✓
  • AC-26 — already issued in another tab: nothing issued again ✓
  • AC-26b — changed in another tab: the preview reloads and must be confirmed again ✓
  • AC-27 — issuing works with the allowance used up ✓
  • AC-28 — the "AI" navigation group with "AI chat" and "Connect your AI" ✓
  • AC-29 — removed by the owner 2026-10-14 (dashboard AI block)
  • AC-30 — the 3 latest Conversations, a link to all, and "Create invoice with AI" ✓
  • AC-31 — removed by the owner 2026-10-14
  • AC-32 — removed by the owner 2026-10-14
  • AC-54 — record questions are filled from the Freelancer's own records ✓ (fix 2026-10-14)
  • AC-55 — one question at a time above the composer ✓ (fix 2026-10-14)
  • AC-56 — no field headings, English chrome, app date/amount formats ✓ (fix 2026-10-14)
  • AC-33 — replies appear word by word; reopened replies are whole ✓
  • AC-34 — the export includes forms with their answers, choices, origins and start points; account deletion removes them ✓
  • AC-35 — the privacy page's AI section (implemented; wording awaits owner approval — see above)
  • AC-36 — a Visitor is sent to sign in; nothing is processed or counted ✓
  • AC-37 — a list of Customers plus one form (Customer, Line 1 · Price), no questions in prose; one Send gives the card ✓ (real-model behaviour: eval rows)
  • AC-38 — an "Other…" name is matched as words; "forty" or "-5" is stopped in the form ✓
  • AC-39 — "Yesterday · 12 Oct 2026" and "14 days after the issue date" give 12 and 26 October ✓
  • AC-40 — a typed message instead of Send closes the form with no answers saved ✓
  • AC-41 — a card turn has no form; never two forms in a reply ✓
  • AC-42 — lists, tables and bold render, streamed and reopened alike; an invoice link opens the editor ✓
  • AC-43 — HTML shows as text; an outside link is plain text ✓
  • AC-44 — an unclosed mark streams as typed and becomes bold; never an empty element ✓
  • AC-45 — the turn tells the model today, the weekday and the time zone ✓ ("dated yesterday" end to end: eval row)
  • AC-46 — a create request with missing values gets a card or a form, never prose ✓ (eval rows)
  • AC-47 — the chat searches before it says a record is missing ✓ (eval row)
  • AC-48 — search rows carry the in-app path; the chat links a draft ✓ (eval row)
  • AC-49 — search_invoices filters and sorts by the day created ✓ ("last draft created in January": eval row)
  • AC-50 — the list's top row and the chat header end on one line at 768 and 1440 px ✓
  • AC-51 — placeholders have the size and place of the content; layout shift ≤ 0.01 ✓
  • AC-52 — a switch reads no list, keeps the same list node, its scroll, search and rows ✓
  • AC-53 — the avatar is level with the first line, for one-line and five-line replies ✓

Design

  • Spec: docs/features/ai-assistant/spec.md, docs/features/ai-chat-v2/spec.md (each with ux-flows.md, screens.md)
  • Architecture: docs/features/ai-assistant/sad.md, docs/features/ai-chat-v2/sad.md
  • Decisions: docs/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)
  • Data model + migrations: 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_scope
  • API: docs/features/ai-assistant/contracts/openapi.yaml, docs/features/ai-chat-v2/contracts/openapi.yaml 0.3.0 (POST /api/chat, MCP create_draft_invoice, list_sender_profiles, list_products, list_customers), plus contracts/server-actions.md in each
  • Test plans: docs/features/ai-assistant/test-plan.md, docs/features/ai-chat-v2/test-plan.md
  • Reviews: docs/features/ai-assistant/_review/, docs/features/ai-chat-v2/_review/ — the last rounds (review-2026-10-16-*) are PASS
  • Security reviews: docs/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-Task trailers (git log --grep SDD-Task origin/main..HEAD).

  • ai-assistant: T01 – T46. Data layer and migrations, the shared read-tool registry, the draft resolver, the allowance, Draft permission and the per-key cap, the chat agent, router and route, the UI, the evaluation sets and the e2e flows.
  • ai-chat-v2: T01 – T133.
    • T01 – T49: first build and presentation rewrite.
    • T50 – T89: owner-feedback revision, its review rounds and the consistency pass.
    • T90 – T112: model switch to Claude Haiku 5.5, security addendum (output and tool-answer bounds, 10 messages a minute, Anthropic pin, no prompt training), privacy and terms.
    • T113 – T114: zero data retention dropped (Hobby plan); Anthropic's 30-day retention on the pages.
    • T115 – T133: fixes from the real-model evaluation on Haiku. Tolerant draft-tool arguments, a Customer question built in code, router card markers, instructions for Haiku, failure reasons in the eval report.

Verification

All runs on macOS with Docker, at 32ae635.

Gate Result
Typecheck (tsc --noEmit) clean
Lint 0 errors
Unit 313 files, 3498 passed
Integration (testcontainers Postgres) full suite: 1631 passed / 61 skipped, 1 stale T27 test, then updated to the T115 contract (its file 13/13)
E2E (local production build, scripted model) 159 passed / 7 skipped / 0 failed; skips are the macOS-skipped screenshot tests and the opt-in PDF timing run
Linux baselines (Playwright 1.63.0 noble image) 6/6
Real-model evaluation (pnpm eval:assistant, Claude Haiku 5.5) topic boundary PASS (off-topic refused 100 %, in-scope refused 0 %); no-guessing 38/40, 0 ungrounded values
  • Remaining evaluation rows:
    • 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.
    • Five evaluation runs in total while tuning for Haiku.
  • Ran the feature. E2E runs the production build against a throwaway Postgres with all migrations, real HTTP and Chromium. It covers the AI section, draft → Create → issue, Question forms, Markdown replies, preview and issue, a Draft-permission key, Conversation reopen and the visual baselines. Direct Gateway calls confirmed that the Anthropic pin plus disallowPromptTraining works on the team's plan, and that zeroDataRetention is refused on Hobby (hence T113).

Operational notes

  • Migrations (before the app deploy): 20 additive migrations. Run prisma migrate deploy with the direct (non-pooler) prod URL, then pnpm mastra:migrate (creates the mastra schema; safe to repeat).
    • Two migrations backfill new columns.
    • 20261015100000_add_chat_message_limit_scope is required by the per-minute limit; without it every chat message answers 503.
    • Down scripts are in docs/features/ai-assistant/migrations/ and docs/features/ai-chat-v2/migrations/.
  • Settings:
    • Required: AI_GATEWAY_API_KEY, set for Preview and Production.
    • Optional: AI_CHAT_MODEL, which accepts anthropic/* models only.
    • Remove GOOGLE_GENERATIVE_AI_API_KEY; nothing reads it.
    • Never set E2E or AI_CHAT_MODEL=stub:e2e.
  • Cron: the existing daily /api/cron/purge-limits also purges Conversations (90 days), repeat marks (24 h), traces (30 days), and turn and counter rows (400 days). It is reported to Sentry Crons as purge-limits.
  • CI: integration tests now run on manual start only (gh workflow run test.yml --ref <branch>). This works once this workflow is on main.
  • Dependencies: ai, @ai-sdk/gateway, @ai-sdk/react, @mastra/*, react-markdown, remark-gfm, all on exact pins.
  • Owner, before merge:
    • Apply the migrations on prod.
    • Set an AI Gateway spend budget and alert.
    • Send one chat message on the preview.
    • Confirm that Vercel's DPA covers the SCC transfer to Anthropic and that the 30-day retention applies.
    • Approve the privacy and terms wording (Last Updated: October 16, 2026).
    • Squash and merge.
  • After deploy: send one chat message on prod, and check the Sentry purge-limits monitor after the first night.
  • Rollback: redeploy the previous build. The new tables and columns are unused by the old code and can stay.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
invoice-forge Ready Ready Preview Oct 16, 2026 10:14am UTC

GopkoDev and others added 27 commits October 13, 2026 04:05
…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>
GopkoDev and others added 24 commits October 16, 2026 02:15
… 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>
@GopkoDev GopkoDev changed the title ai-assistant: an in-app AI chat that answers from your data and drafts an invoice from one sentence AI chat: answers from your data and drafts invoices from one sentence (ai-assistant + ai-chat-v2) Oct 16, 2026
@GopkoDev GopkoDev closed this Oct 16, 2026
@GopkoDev GopkoDev reopened this Oct 16, 2026
@GopkoDev
GopkoDev merged commit e9930a8 into main Oct 16, 2026
6 checks passed
@GopkoDev
GopkoDev deleted the feat/ai-assistant branch October 20, 2026 13:34

This branch was successfully deployed

1 active deployment
Preview — 32ae6351 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant