AI assist
The three AI features in the web app: what each one does, what leaves and to whom, who may ask, where the answers appear, and what happens when no provider is configured.
None of the shipped strings quoted here contains an em dash, so nothing is substituted; they are verbatim.
Three features, all of them in the web app
- AI bug draft. A QA card written from what this site already holds about one capture: a title, a summary, steps, expected and actual (apps/web/src/lib/ai/bug-draft.ts).
- AI title and summary. What the capture shows, from the same facts. It is deliberately not a diagnosis: the system prompt tells the model to say what the capture shows and not to write steps to reproduce a defect (apps/web/src/lib/ai/summary.ts:61-70).
- Help with a note. A rough note turned into what happened, what was expected, and how to see it again (apps/web/src/lib/ai/composer.ts).
Nothing else in the product reaches an AI provider. The desktop app and the extension reach none: the desktop's structured document never leaves the machine (DECISIONS.md, "core.js's structured document never leaves the machine"), and the extension reaches exactly one address, the SnapBuddy app, for exactly two things - signing in, and a picture you upload. Nothing is uploaded unless you press Upload on a card, and every other card stays in this browser. Neither of those two is a model: no capture, note or developer log is ever put in front of an AI provider from that surface (extension.md).
⚠ This paragraph used to deny the extension any network traffic whatsoever, a claim it had inherited from extension.md and which had been false for a release by the time anyone read it. The sentence above is now held word for word against the extension's own Settings row by an equality join in scripts/check-promises.mjs, rather than restated here in words of its own that nothing checks.
Exactly three modules under apps/web/src call the AI service, and scripts/check-promises.mjs counts
them on every run and prints the list when the count disagrees with the privacy notice
(AI_COUNT_SENTENCE and the module scan beside it - cited by construct, not by line, because the
line numbers that used to stand here had drifted onto unrelated rules; joined-to-code).
The note helper has no place to be pressed from yet. ComposerAssist is imported by nothing
outside its own file and its test, because the web app has no note composer:
assets.description is a column with no writer, and the note fields that exist belong to the
desktop board and the extension (apps/web/src/components/composer-assist.tsx:29-34). Everything
below describes what it does when it is mounted, which is how it is built and tested today.
Nothing is sent until somebody presses a button
There is no workspace setting that turns any of this on, nothing runs on upload, and no capture is sent because it was opened. Each feature is one press on one capture.
The draft and the summary are plain form submissions from the dashboard asset page, so they work with JavaScript off (apps/web/src/app/dashboard/a/[id]/page.tsx:759-767 and :677-688, apps/web/src/lib/ai/actions.ts:65 and :101). The buttons read "Write an AI draft" and "Write an AI summary", and "Write another AI draft" / "Write another AI summary" once one exists.
The note helper sends only from its own click handler. There is no effect, no timer, no blur
handler and no keystroke handler in the component (apps/web/src/components/composer-assist.tsx:60
and :104; joined-to-code: apps/web/src/lib/ai/composer.test.ts:384-393 reads the component source
and fails the build if a second call site, a useEffect, a setTimeout, a setInterval, an
onBlur, an onChange or an onKeyUp appears).
Its disclosure sits above the button and describes the press before it happens, verbatim: "Nothing is sent until you press this. Pressing it sends the note exactly as you typed it, along with what this capture already stores, to the AI provider named in the privacy notice." (apps/web/src/components/composer-assist.tsx:94-101; joined-to-code: composer.test.ts:395-402 fails if that sentence moves below the button).
What leaves, and to whom
The provider is Anthropic. The request goes to https://api.anthropic.com/v1/messages and is made
only when ANTHROPIC_API_KEY is set (packages/services/src/ai.ts:23, :56-59).
All three features are shown the same details, because there is one column list and one prompt
builder and each feature uses both. What is sent about a capture: its title, type, status and
triage state, duration, frame size, the marks that were drawn with the labels the person wrote on
them, and, for a recording, whether a camera and system audio were switched on
(loadCaptureFacts, apps/web/src/lib/ai/bug-draft.ts:263-305; buildCaptureContext, :176-203; used
by summary.ts:89-91 and composer.ts:110-125).
What is never sent: the picture, the video, the audio, an email address, a share link or token, a
storage key, a signed URL, an API key. The select names eight columns and omits storage_key,
stream_uid, thumbnail_key and owner_id, and the input type has no field for any of them
(bug-draft.ts:270, :58-81; joined-to-code: bug-draft.test.ts:130, summary.test.ts:129 and
composer.test.ts:158 each assert it against the built prompt string, not against the type).
On a recording, the position of a redact, blur or pixelate mark is withheld and the prompt says so, because those marks are painted over the player rather than burned into the file and the pixels are still there. On a screenshot the same marks are baked in, so their positions are stated (bug-draft.ts:104-140; joined-to-code: bug-draft.test.ts:164). See annotating.md.
No caption text is sent by anything today, although the prompt can carry it: the WebVTT is not parsed into a stored transcript, so no caller has any (bug-draft.ts:70-80, :303).
The note is the exception, and it is the one to know about
Help with a note sends everything above plus the note itself, as typed. It is not summarised, trimmed to a topic or rewritten first; newlines are preserved because a note is written in lines (apps/web/src/lib/ai/composer.ts:110-125; joined-to-code: composer.test.ts:180). It is bounded at 4,000 characters and the prompt says "Truncated." when it hits that (composer.ts:59, :119).
Pressing the button with an empty note sends nothing and says so: "There is nothing to work from yet, so nothing was sent. Write a note first." (composer.ts:169).
The suggestion is never stored. It sits beside the note until it is kept or discarded, and both answers keep the person's own words: discard returns the note byte for byte, and keep appends the suggestion underneath it rather than replacing it, so a mis-click costs a paragraph to delete rather than the note (composer.ts:238-246; joined-to-code: composer.test.ts:412-419, which was mutation-tested against a version that replaced the note).
Who may ask
An editor or above, and the check runs before the provider is called, not after. RLS would refuse the write anyway, but that refusal arrives on the insert, which is after the prompt has been built, sent and paid for, so a viewer holding the button would spend a model call per press (apps/web/src/lib/ai/actions.ts:69 and :107, apps/web/src/lib/ai/composer.ts:163; joined-to-code: actions.test.ts:195 and :257, composer.test.ts:312, all three asserting nothing reached the model).
A viewer is told: "You do not have permission to write an AI draft here.", "You do not have permission to write an AI summary here.", or "You do not have permission to ask for help with a note here." (actions.ts:70, :108; composer.ts:164).
Neither button appears on a capture in the trash (apps/web/src/app/dashboard/a/[id]/page.tsx:360).
Where answers appear: workspace members only, never a share link
AI output is member-only by owner ruling (DECISIONS.md, "AI summary visibility: member-only"). A model's guess about a bug, on a page a stranger opens from a link with no member present to correct it, is not revocable once it has unfurled into a chat.
Both panels live in the dashboard asset page file, behind getSessionContext, and in no shared
component the share viewer could import (apps/web/src/app/dashboard/a/[id]/page.tsx:639-767).
scripts/check-promises.mjs:1028-1039 fails the build if ai_artifacts, bug-draft, BugDraft or
AI_DRAFT appears in apps/web/src/app/s/[token]/page.tsx or apps/web/src/lib/share-access.ts
(joined-to-code). See sharing.md for what a share link does show.
Every answer says a machine wrote it
One constant per feature, exported from the module that generates it and imported wherever it renders, so no surface can describe the same artifact in a different register. The badge and the sentence come before the model's own title, so the first thing read is that a machine wrote it.
- Bug draft, badge "AI draft": "AI draft from this capture's own stored data. Check it before you
file it." (
DRAFT_NOTE, apps/web/src/lib/ai/bug-draft.ts:42; rendered at apps/web/src/app/dashboard/a/[id]/page.tsx:706-715). - Summary, badge "AI summary": "AI summary from this capture's own stored data. Check it before you
rely on it." (
SUMMARY_NOTE, apps/web/src/lib/ai/summary.ts:38; rendered at page.tsx:653-662). - Note suggestion, badge "AI suggestion": "AI suggestion, written from this capture and from the
note you wrote. Check it before you file it." (
ASSIST_NOTE, apps/web/src/lib/ai/composer.ts:55; rendered at composer-assist.tsx:120-125; joined-to-code: composer.test.ts:400 fails if the component stops rendering the constant).
The third one is a separate sentence rather than a reuse on purpose: the other two say the text came from the capture's own stored data, which for the note helper would be an understatement in the one direction that misleads, because the words the person typed went too (composer.ts:41-54). It is appended into the note when a suggestion is kept, not printed only on the panel, so the marker survives a paste into a ticket (composer.ts:244).
Beside each stored panel the page prints who wrote it and when, read from the row rather than baked
into the sentence: "Written by", the stored name, "on", then the row's timestamp in UTC. The stored
name is the service and not the model id, so it reads anthropic, because the AIService interface
does not expose a model id (bug-draft.ts:358-360, page.tsx:657-660). A row written before a name was
stored prints "an unnamed model" (page.tsx:657, :710).
When no provider is configured
With no ANTHROPIC_API_KEY the service is a stub that answers {"stub":true} to any JSON request.
Every feature refuses and says so rather than printing anything:
- Bug draft: "No AI provider is configured here, so there is no draft to show." (bug-draft.ts:331), shown through the page's error banner.
- Summary: "No AI provider is configured here, so there is no summary to write."
(
SUMMARY_UNCONFIGURED, summary.ts:45). The button is not rendered at all in this state; the sentence is printed where it would have been, so nobody presses a button that can only fail (page.tsx:677-688). - Note helper: "No AI provider is configured here, so nothing was sent and there is no suggestion to show." (composer.ts:179).
The two differ here, and it is a known inconsistency rather than a designed one: the summary hides its button, the draft still renders one and refuses on press. Do not describe either as the rule.
If the model answers and the answer is not usable, nothing is stored and the sentence says so: "The model answered with something that is not a draft, so nothing was saved." and its summary equivalent (bug-draft.ts:351, summary.ts:168). If the request fails, the vendor's own error text is never shown, because it can carry the prompt back into a page: "The model did not answer, so there is no draft. Try again." (bug-draft.ts:344, summary.ts:161, composer.ts:194).
What is stored
A draft is one ai_artifacts row of kind repro; a summary is one row of kind summary, holding
the title and the summary together so the two cannot drift apart (bug-draft.ts:361-373,
summary.ts:177-189). Neither kind needed a migration: both values already exist in the
ai_artifact_kind enum (supabase/migrations/0001_foundation.sql:15). Asking again appends a row and
the panel shows the newest, so the buttons read "Write another" rather than replacing anything
(page.tsx:296-320). A note suggestion is stored nowhere (composer.ts; joined-to-code:
composer.test.ts:281).
Known gap, stated because this KB is not a brochure
The shipped privacy notice still says in bold that a bug draft "is the only thing that goes to
Anthropic" (apps/web/src/app/privacy/page.tsx:296), and the processor list repeats it in other words
at :368-370. Three features now reach Anthropic, so both are false, and
scripts/check-promises.mjs fails pnpm test naming all three modules until the wording changes. The
page is owner-gated; the proposed replacement is drafted in
docs/opus5-handoff/AI-ASSIST-NOTICE-FOR-OWNER.md and waits on approval. This article is the accurate
account meanwhile.
The sentence a support agent can paste
"Nothing about your capture is sent to an AI provider unless somebody presses one of the AI buttons on it, and asking for help with a note also sends the note exactly as it was typed. What comes back is labelled as written by AI, is only visible to people signed in to your workspace, and never appears on a share link."