Imported from TNG/tb-llm-composer (
AGENTS.md). Install upstream withnpx skills add TNG/tb-llm-composer. Copyright stays with the author.
tb-llm-composer
Working efficiently (keep token use low)
- Read only the files you need; prefer targeted
grep/search over reading whole files. Don't re-read a file already in context. - Make focused edits with minimal context; never echo large unchanged regions or full-file dumps back to the user.
- Don't paste full build/test logs. Run checks, then summarize results in 1-2 lines; show only failing snippets.
- Keep replies short: a brief plan, the edits, and a concise outcome. Skip restating code you just wrote.
- Batch independent reads/searches in one step instead of many sequential calls.
What this project is
LLM Composer is a Thunderbird MV3 WebExtension in TypeScript that adds LLM help while writing emails.
Features:
- Compose from a short prompt (
Ctrl+Alt+L) - Summarize the reply thread (
Ctrl+Alt+K) - Cancel an in-flight LLM request (
Ctrl+Alt+C) - Sort a folder by classifying each message into configured folders (toolbar action popup)
- Generate a report over the mailbox via agentic LLM tool-calling (toolbar action popup)
It uses any OpenAI-compatible chat-completions HTTP endpoint. Endpoint URL, optional bearer token, and model are configured on the options page. The endpoint origin is an opt-in host permission (not granted at install); the options page requests it via hostPermissions.ts on a user gesture, otherwise fetch is blocked by CORS.
Tech stack
| Concern | Choice |
|---|---|
| Language | TypeScript 5 (strict), ES2022, ESM |
| Package manager | pnpm (v10+), Node >=22 (CI uses Node 24) |
| Bundler | Webpack 5 (ts-loader, Terser) |
| Linter/format | Biome (biome.json): 2 spaces, double quotes, width 120 |
| Tests | Vitest (node env, vitest-fetch-mock) |
| Types | @types/thunderbird-webext-browser (browser.*) |
Post-edit checklist (run in order)
Run after every code change. CI (.github/workflows/ci.yaml) runs build, lint, test, and pnpm audit.
pnpm run lint
pnpm run test
pnpm run build
- Lint auto-fix:
pnpm run lint-fix(safe) orpnpm run lint-fix-unsafe(unsafe) - Optional coverage:
pnpm run test-coverage - Release package only:
pnpm run ship->llm-thunderbird.xpi
Project layout
src/
background.ts Entry point; registers all browser.* listeners for commands/menus/tabs/alarms and folder-sort action.
options.ts Options page logic (DOM in public/options.html).
optionsParams.ts Option/parameter types, DEFAULT_OPTIONS, getPluginOptions() (reads browser.storage.sync).
llmButtonClickHandling.ts compose()/summarize()/cancel() flows; per-tab request state (AllRequestsStatus); think-tag stripping;
writes results via browser.compose.setComposeDetails.
llmConnection.ts sendContentToLlm()/callLlmApi(); runAgenticLlm() tool-calling loop; fetch to LLM endpoint;
request/response + tool (LlmToolDefinition/LlmToolHandler) types; abort + timeout.
promptAndContext.ts Builds system/user messages (context + prompt) for body, subject, and summary generation.
emailOrganising.ts organiseCurrentFolder(); organise folder messages via LLM and move with browser.messages.move() to FolderRule targets.
Runs the deterministic pre-filter pass first (see preFilters.ts) so matched mail never reaches the LLM.
Also exports resolveFolderPath()/extractTextFromPart() reused by report tools.
preFilters.ts Pure matcher for PreFilterRule (field × operator × value, case-insensitive; invalid regex = no match).
Thunderbird's own message filters cannot be invoked from a WebExtension, so this reimplements the useful subset.
hostPermissions.ts Derive/check/request the opt-in host permission for the configured endpoint origin.
reportGeneration.ts generateReport(ReportRequest); builds the report system/scope prompt and drives runAgenticLlm.
reportTools.ts Report tool definitions + handlers (ReportScope): search_messages (compact metadata + filters, returns {hits,returned,truncated}), get_messages (batched, always-full bodies, bounded by a shared per-run budget of maxMessageBodies + maxTotalBodyChars), get_thread (References/headerMessageId + normalized-subject reconstruction across all folders; reference lookups are capped and every search page is timeout-guarded so a stalled IMAP search degrades to a partial thread instead of hanging), aggregate_messages (grouped counts, no bodies). assertSearchCapabilities() probe. Folder-only search restricts search_messages to the target folder; threads bridge to Sent.
reports.ts Report window UI logic (public/reports.html): create/cancel, refine by continuing the agent conversation, "New report" to start over, folder picker, copy/save txt|md, save/load reusable prompts.
reportPrompts.ts Persist reusable report prompts (name + text) in browser.storage.sync: getSavedPrompts/savePrompt/deletePrompt.
menu.ts Native menu entries for the compose_action (compose/summarize) and toolbar action (organise/report) menus + shortcut labels.
retrieveSentContext.ts Gets recent sent mail to a recipient (writing style context).
originalTabConversation.ts Caches reply quote per tab in browser.storage.local.
emailHelpers.ts MIME text extraction; first-recipient address helper.
keepAlive.ts Alarm-based keep-alive to prevent MV3 suspension during long LLM calls (~90s idle threshold).
notifications.ts timedNotification() + notifyOnError() wrapper.
utils.ts stripHtml(), getInputElement() helper.
thunderbird-alarms.d.ts Ambient types for alarms API.
__tests__/ Vitest specs + setupVitest.ts + testUtils.ts.
manifest.json MV3 source manifest. Webpack rewrites paths and strips " (dev)" / dev id for production.
webpack.config.js Builds background.ts + options.ts + reports.ts into build/; copies icons/, public/, and transformed manifest.json.
public/options.html Options page markup. Includes a "Query available models" control that GETs the OpenAI-style {base}/models (base derived from the chat URL) and, per model, an arrow that upserts params.model into the "Other options" JSON.
public/reports.html Report window markup.
icons/ Extension icons + busy indicator (loader-32px.gif).
docs/CONTRIBUTING.md Dev/test/release instructions + sample test emails.
docs/create_new_release.md Release process.
build/ Generated webpack output (do not edit manually).
Key flows
- Listener ordering is critical: in
background.ts,browser.commands.onCommandmust remain the first statement or shortcuts can fail after event-page restart. - Compose flow:
executeLlmAction->compose()gathers compose details, recent sent mails, and reply quote; builds prompt/context inpromptAndContext.ts; optionally generates subject; callssendContentToLlm; writes result back and re-appends signature/quote. - Per-tab request state: singleton
allRequestsStatus(AllRequestsStatus) holds anAbortControllerbytabId; cancel aborts active request; compose-action icon switches toloader-32px.gifwhile running. - LLM HTTP call:
callLlmApiPOSTs{ messages, ...params }tooptions.model(endpoint URL), addsAuthorization: Bearerif token exists, merges user-abort and timeout signals, and wraps fetch with keep-alive. - Toolbar action menu:
browser.actionis a native menu ("type": "menu") whose entries (organise-folder,create-report) are registered viabrowser.menusinmenu.ts, matching the compose_action menu.background.tsmenus.onClickedroutes those ids totoggleOrganiseFolder/openReportWindow; other ids fall through to the LLM action handler. - Pre-filter pass: before any LLM call,
prepareOrganiseappliesoptions.preFilterRules(first match wins) — a match moves the message to the rule'stargetFolderPath, or leaves it in place when that is empty, and removes it from the classifier's set either way. Its tallies are folded into the run summary and reported in their own notification. Configured in the options "Organise Folder" section, which also links to Thunderbird's native message filters (Tools ▸ Message Filters) since those cannot be triggered from an add-on. - Folder sort flow:
organiseCurrentFolderclassifies + moves messages one chunk at a time (moves happen right after each chunk is labelled, so progress is durable), reporting a percentage via anonProgresscallback. While it runs, the toolbar action entries are replaced by a singlecancel-organiseentry showing the percentage (showOrganiseProgressMenu/updateOrganiseProgressMenu), and the action title mirrors it; clicking it (or the button) re-togglestoggleOrganiseFolder, which aborts the in-flight run. On abort the run still shows the final popup summarising what was moved so far. - Report flow: report window (
reports.ts) sendsgenerate-reportwith aReportRequest;generateReportprobes search capability (assertSearchCapabilities), thenrunAgenticLlmloops the model withsearch_messages/get_messages/get_thread/aggregate_messagestools (token-frugal: metadata first, full bodies fetched in batches on demand under a per-run body budget) up toreportMaxSteps. While it runs,runAgenticLlmemitsAgenticProgress(llmCalls/toolCalls/phase) via anonProgresscallback; the background relays it to the window asreport-progressmessages, which the popup shows as a live counter row with a Stop button. Stop (or the send-button stop glyph) sendscancel-report, which aborts. Refining continues the same agent conversation:runAgenticLlmreturns the full message history, the background keeps it per report window (reportSessions), and a follow-up Create sendscontinueConversation: truesocontinueReportappends the new instruction to that history instead of starting over. The "New report" button sendsreset-reportto drop the stored conversation. Prompts can be saved by name and re-loaded via a dropdown (reportPrompts.ts,browser.storage.sync). - Think-tag handling:
<think>...</think>is stripped unlessoptions.strip_think_tagisfalse.
Conventions and gotchas
- Use Promise-based
browser.*, neverchrome.*. - Persisted settings:
browser.storage.syncunderoptions; per-tab cache:browser.storage.local. Always read options viagetPluginOptions()(merges withDEFAULT_OPTIONS). - TypeScript is strict: no unused locals/params, no implicit
any, no fallthrough; prefer narrow typing andLlmRolesfor message roles. - Add really concise docstrings to functions whose behavior is not obvious from the prototype.
- Log prefixes are namespaced (
SORT:,MENU:,LLM-CONNECTION:,KEEP-ALIVE:,LLM-CONVO-CACHE:). Production build dropsconsole.log/console.infovia Terserdrop_console; keep user-visible failures onconsole.error+ notifications. - Preserve dev/prod split: new top-level buttons/titles should include
" (dev)"inmanifest.jsonso dev installs do not clash with production (webpack.config.jstransform strips it for prod). - Tests run in node with
globals: true; fetch/network is mocked byvitest-fetch-mock(src/__tests__/setupVitest.ts). - Plain-text compose is the only fully supported mode; HTML reply content is normalized with
stripHtml, and storing HTML reply throws.
References
- Thunderbird WebExtension API: https://webextension-api.thunderbird.net/en/stable/
- Thunderbird add-on docs: https://developer.thunderbird.net/add-ons/about-add-ons
- Local dev and temporary add-on loading:
docs/CONTRIBUTING.md