Add a native GPUI desktop shell that reads the shared app.db - #7356
Add a native GPUI desktop shell that reads the shared app.db#7356ComputelessComputer wants to merge 171 commits into
Conversation
✅ Deploy Preview for anarlog canceled.
|
|
|
6f1bf23 to
4b9d87c
Compare
90ec761 to
1a1b3e7
Compare
e84633e to
671be7c
Compare
|
Hi @ComputelessComputer — I noticed the I’m testing RunBack, an early alpha CLI that reconstructs a failed public Actions run locally with runback doctor
runback https://github.com/fastrepl/anarlog/actions/runs/34015472318The Linux amd64 binary and checksum are in the v0.1.0-alpha release. If you try it, the verdict ( |
4c0dd06 to
d461361
Compare
Port `useContactSummary` / `ContactSummarySection`: the person view now
generates the relationship brief through the enhance model when the
stored `metadata_json.contactSummary` was built from a different set of
meetings, with the frontend's source hash (FNV-1a over the newest eight
sessions' ids and stamps), incremental updates over `existing_facts`,
per-meeting and total source caps, fact normalisation, one query-level
retry, and the spinner / skeleton / failure / footer states.
`llm_stream` gains a non-streaming `generate` / `generate_object` with
each provider's structured-output shape (`response_format`, Anthropic's
`output_config.format` or `json` tool, Gemini's `responseSchema`), and
orders request keys like the AI SDK. `HumanSession` carries the
`source_updated_at` stamp the brief keys on.
Verified against Tauri on one shared database: identical prompts and
system text, identical source hashes for 1, 2, and 8 sessions, identical
stored records, and pixel-matched list, skeleton, and error layouts. The
app's `* { margin: 0 }` reset disables Tailwind's `space-y-*`, so the
lists render gapless, which the port mirrors.
Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…yntax GitHub's `$/path` form resolves sibling actions at the running commit, which is what zizmor now asks for on the `uses: ./…` lines this job added; the pre-existing `./` references elsewhere are unchanged. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Port the composer's context flows from `chat/context/*` and `chat/components/input/*`: `@` opens the same suggestion popup the note editor uses and the chosen row becomes an inline chip (`TextArea` gains atoms with the editor's caret-skip and whole-chip deletion, painted with the shared avatar), a timeline note dragged onto the chat panel becomes a `session:manual` ref for the next send and dropped onto a note editor an inline mention plus a space, and the sent message carries the current note, the dropped notes, and the mentions deduped by key. `ContextRef` now models the session / human / organization / folder kinds and keeps unknown ones verbatim; the prompt hydrates contact, organization, and folder refs as the text blocks after the `<context>` block, and omits the transcript section when nothing rendered, like `buildTranscript`. The composer also recalls sent drafts with Up / Down at its edges under the `History N/M` indicator, undoes with Mod-Z, and flattens the draft the way `proseMirrorJsonToText` does (chips as `@label`, block breaks dropped). Verified against Tauri on one database: identical stored rows and byte-identical prompts for a mention and for a dropped note, identical documents from an editor drop, and the same popup rows. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Port `ScheduledMeetingAutoStart` / `ScheduledSessionAutoStart`: with `auto_start_scheduled_meetings` on, a linked, timed calendar event that starts (or started within the five-minute grace) opens its session — created from the event like `getOrCreateSessionForEventId` — optionally opens the meeting link, and records automatically, with the same due-meeting selection (newest overlapping start first, fired ids claimed, ignored events and series skipped, a live or finalizing capture skipping or retrying) and a 30s pending timeout. The capture marker now records `automatic` and the real `preserveExistingAudio`, and `finalizeStopped`'s order is kept: after the flush, an automatic capture that recorded no speech into a session left untouched (title, attachments, `isSessionEmpty`) loses its audio and clears its marker before anything is catalogued; the audio is otherwise catalogued at that point rather than at `Inactive`. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Size the timeline sidebar the way `react-resizable-panels` does in the Tauri app: a persisted share of the panel group (the window minus `pl-1`) split like its two flex items — the sidebar's exact `flex-grow` against the library's `toPrecision(3)` content share over the free space minus the handle — and clamped to 200–360px, so the sidebar scales with the window exactly as the web view's does. The share starts as 200px of the first frame's group, follows the handle drag, and is read from and written to the webview's `localStorage` (`react-resizable-panels:classic-main-sidebar` in WebKit's `ItemTable` files under the data directory or the WKWebView website data), with a copy in the shell's own `store.json` scope, so both shells open at the width the other left. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…ntend Without a sidebar folder view (which the shipping app never enters) the tool returns Tauri's `No folder is selected` error instead of the shell's placeholder. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…ared crate `crates/local-api-core` holds the outbound webhook dispatcher, the meeting markdown export (`exportMeetingMarkdown` and the `automation_markdown_export_*` automation) and the cloud snapshot shape that `plugins/local-api` exposed as Tauri commands; the plugin now delegates to it with its bindings and tests unchanged. The core emits its JSON with every object's keys sorted, which is the byte layout the plugin has always produced (its build links `serde_json` without `preserve_order`), so a build that does keep insertion order sends the same webhook bodies and run records. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The engine still emits transcript deltas between `Finalizing` and `Inactive`; the shell moved the persistence queue aside at `Finalizing` and flushed it at once, so those tail words never landed (a 20s mock capture kept 103 of Tauri's 124 words). The queue now stays open through the finalization and is flushed at `Inactive`, where `onStopped` awaits `transcriptPersistence.flush()`. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Port `automations/engine.ts` and the `local-api` plugin's `dispatch_event` over the shared `local-api-core`: `meeting.completed` once a fresh stop's post-stop processing settles (the awaited batch included) and `note.enhanced` once a summary lands run the outbound webhooks, the markdown-export starter with its `automation_markdown_export_last_run` record, the Slack / Linear / Notion starters (which end where `requireSupabaseSession` ends for the signed-out shell), and the enabled custom workflows with their `lastRun` and fifty processed sessions — the `markdown_export` step writing `<date> <title> [id].md` and replacing the stale file, the integration steps failing with the frontend's own messages. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
`finalizeStopped` awaits the `finalizing` marker write before it requests the summary; when that write fails it shows `The transcript was saved, but Anarlog could not start the summary. Try generating it again.` and asks the recovery to retry instead of starting the summary. The shell requested the summary at once and only logged a failed save; it now sequences the request and the audio completion behind the save and takes the same failure path (the toast only for a fresh stop, the recovery retries quiet). Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The workspace reclaimed focus only when no handle was focused, but a focus handle outlives its element: once the memo editor gave way to the summary tab after a recording, keystrokes were dispatched to the bare root past the workspace bindings, so Escape and the app shortcuts stopped working until something was clicked. The check now asks whether the focused handle is still under the workspace element. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The onboarding's `<video autoPlay loop muted>` becomes an animated WebP of `onboarding-video.mp4` (832×464 → 416×232, 24fps, 5s loop, pre-blurred with `gblur=6` since GPUI has no backdrop blur, 31 KB) that GPUI's `img` plays frame by frame — the element id keeps its frame clock — in place of the single blurred frame that stood in for it. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The onboarding branch of the workspace root skipped the resolved `font-sans` family and the base text style the main root sets, so GPUI's fallback font rendered the `font-semibold` section headings at regular weight and `Skip` in the wrong face. The root now carries the same family, colour and size, and the headings measure like Tauri's. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The General page's `Where your notes and recordings are stored` label wrapped after `and` in the shell: with `min-w-0` alone Taffy sized the text column at its min-content width, where the web view's flex item keeps its max-content width. The column now takes the remaining space and the path keeps truncating, and the page's text bands match Tauri's row for row. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
… from `leaveOverlayTab`: Escape on a settings / folders / templates / calendar / contacts / automations tab, and the custom sidebar's back arrow, return to the tab the overlay was opened from — `openNew` from the settings nav records the settings tab as the return origin, so Contacts opened there goes back to Settings, while settings opened over a note goes back to the note. The shell's Escape ignored overlay tabs and its back arrows always dropped to the note. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…bar gutter The `Related Notes` header gained a `Search...` field in the frontend (`h-8 w-52 max-w-[48%] rounded-lg border bg-muted/50 focus-within:bg-accent`, the 14px glass, a clear button, Escape clearing) with `sortAndFilterRelatedNotes` — a trimmed lower-case title filter, the list by `Date.parse(createdAt)` in the chosen direction with the id as the tiebreak, `No results found.` when the filter hides everything — which the shell's port predated. The person, organization and list columns also reserve WebKit's 6px scrollbar gutter once they overflow, so their dividers and right-aligned dates end where Tauri's do (x=893 at a 900px window), and the box lands on the same pixels (662–870) with a growing spacer: Taffy leaves 8px of an `ml-auto` margin unused after a gapped row. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Main switched every icon to its outline form (#7398): the transcript tab keeps a Lucide audio-lines glyph (`TranscriptAudioIcon`, six 2px bars) where the transcript screens keep `Waveform`, Play / Pause lost their fill for the 1.5px stroke, and the states that used the filled weight — a pinned contact or organization, the favourited template's heart, the non-default notes filter — now use the bold (2px) stroke. The shell's assets and the tab / list / menu / template callers follow. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Main's #7395 removed the `size-7` spacer the sidebar chrome row kept where the toggle sits in the title bar, so on Linux the Search / New note / Filter buttons start flush at `pl-2`; on Windows the row is gone and the actions render in the title bar beside the toggle while the sidebar is expanded (`usesTitleBarSidebarActions`). The shell mirrors both: the actions live in one `SidebarNoteActions` renderer, the row shows the toggle only on macOS, and the Windows title bar hosts the actions. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
`DropdownMenuRadioItem` (the contacts and related-notes sort menus) shows an 8px `bg-current` dot in the `left-2 size-3.5` slot of a `pl-8` row rather than a trailing check, and `DropdownMenuContent` flips above its trigger when it would leave the window. The inline menus now anchor at the trigger's vertical centre with the panel padded by half the trigger plus the 4px `sideOffset` on both sides, so GPUI's anchor switch lands the panel 4px above the trigger like Radix instead of snapping it over the button. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The workspace's focus recovery ran inside `render` against the previous frame's dispatch tree, so a field mounted in the same frame (the Find a note dialog's input) was judged off-tree and lost its focus before it ever took a keystroke. The check now runs once the frame is drawn. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Main's #7446 makes `verifyProviderCredentials` return early for a Custom STT credential once the static checks pass, since a Deepgram-compatible listen server need not expose a model catalog or a probe endpoint. The shell's verification takes the provider type from both callers (the save path and the availability gate) and does the same; the LLM side still probes. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The body and the right chat panel now split the panel group by the persisted react-resizable-panels share (30% default, 20–50%, min 320px), read from and written to the web view's localStorage under react-resizable-panels:main-chat like the sidebar's, with the w-0 handle's 5px hit area for dragging. Body minimums (note 500, automations 600, settings min(700, 100%), plus the 200px sidebar) win over the share and overflow the window the way the flex layout does, and the timeline sidebar sizes itself against the body panel rather than the window. useNoteSurfaceWindowWidthGuard is ported: on note-surface tabs, docking the chat collapses the sidebar when the note would drop under 500px, grows the window by the deficit (restored when the chat closes), reopening the sidebar grows it again, and a narrowing resize collapses the sidebar. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The section textareas are resize-y in the web view: WebKit's resizer grip sits in the bottom-right corner and dragging it sets the field's height, never under min-h-[100px]. The shell draws the same pixel glyph, focuses the field on press like WebKit does, and follows the pointer vertically. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
WebKit floors a fractional computed line-height (leading-relaxed at 14px is 22.75px, laid out as 22px; leading-snug 19.25px as 19px), so the shell's multi-line copy drifted a pixel per line against the web view. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
411b4ca to
c86fef9
Compare
GPUI's shadow shader integrates a gaussian over 3x the blur radius, so a spread-only box-shadow (blur 0) paints nothing: the template textarea's ring-2 ring-primary/20, the note image's hover ring-border ring-offset-2 and the shadcn Input's focus-visible:ring-1 were all invisible. ui::ring floats a bordered box outside the element instead (stepping over the parent's border, since GPUI positions absolute children against the padding box and paints the border over children), the theme gains --ring, and the New folder dialog and webhook URL inputs get the focus ring and shadow-xs the web view shows. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The template and Auto form bodies reserve WebKit's 6px scrollbar gutter outside px-6 once they overflow and paint its thumb; the content-sized DropdownMenuContent menus (folder / template / section actions, the templates, contacts and related-notes sort menus) take the measured 128px min-width instead of 160; Add Section stays fit-width with the icon's mr-2; and the Auto format editor hugs its content like the web view, where .ProseMirror's min-height: 100% overrides min-h-[28rem]. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
DialogDescription is a p, so the app's global text-wrap: pretty applies; the shell's greedy wrap broke the three lines at different words. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The Toaster sets closeButton, so every flash toast and the persistent promotions (sign-in-benefits, upgrade-to-pro) carry sonner's 20px round X at the top-left; dismissing a promotion writes DismissedToasts like setDismissedToasts, so the shell can put the sign-in toast away for good. Flash toasts take an action button (notifyPlanRequired's Upgrade), warnings last the 6s of TOAST_DURATIONS, and the warning palette is sonner's --warning-bg / -border / -text (#fffcf0 / #fdf5d3 / #dc7609, measured) rather than Tailwind amber. Both promotions' actions run the browser sign-in like onSignIn (the SignIn / UpgradeToPro actions land with the instruction screen). Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…lance openUrlWithInstruction / dismissInstruction: Get started, the promotion toasts' Sign in / Upgrade and the Pro toast's Upgrade now swap the window for InstructionScreen (Back, the app icon, the sign-in or checkout copy, Reopen checkout page, the callback-link fallback with its mono field and Submit button) while the web app URL — buildWebAppUrl's flow / scheme / period / source — opens in the browser; Back returns to the app. macOS gets the 340×500 resize the plugin animates (GPUI cannot move the frame); a pasted callback takes the deep-link route. Headings wrap with WebKit's text-wrap: balance (InlineContentBalancer's line-count-preserving squared deviation DP), which the app's global h1–h6 rule applies. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Summary
Problem: ANLG-320 calls for a standalone
apps/desktop-gpuiapplication alongsideapps/desktop, migrated incrementally rather than as a big-bang replacement.Fix: Add
apps/desktop-gpui(anarlog-gpui), a GPUI shell that opens the Tauri app'sapp.db/store.jsonwith the same path resolution and reproduces the Tauri UI surface by surface (pixel-matched against the running Tauri app on the same database), plus an opt-in shell switch so one install ships both shells and the user can move between them at any time.Incremental rollout (opt-in shell switch)
Same shape as Cap's Tauri → GPUI toggle: one install ships both shells, the Tauri binary stays the launcher, and the user picks.
anlg_storage::shell: adesktop-shellmarker file next toapp.dbholdstauri(default) orgpui;ANARLOG_SHELL=tauri|gpuioverrides it from a terminal.apps/desktop/src-tauri/src/shell.rs): before the launch lock or any webview, if the marker saysgpuiandanarlog-gpuisits next to the main binary, spawn it with--identifier(and the launcher's own arguments, so OS deep links follow) and exit. A missing sidecar or spawn failure logs and falls through to the classic app, so a broken opt-in can never leave a dead launcher.is_native_shell_available/switch_to_native_shellcommands,tauri.gen.tsregenerated, lingui catalogs extracted and compiled). The row renders only when the sidecar is installed, so dev builds and platforms without the sidecar are unchanged.tauri, launches the Tauri binary (anarlog/anarlog-dev/anarlog-stagingby identifier) and quits.cargo xtask prepare-binariesbuildsdesktop-gpuiwhenANARLOG_GPUI_SIDECAR=1, anddesktop_cd.yamlappendsbinaries/anarlog-gpuitoexternalBinon the Linux x64/arm64 lanes, the platform whose GPUI build is a required check. macOS and Windows sidecars follow once theirdesktop_ci.yamlchecks stop beingcontinue-on-error.store.json, settings and keychain entries, so switching keeps every note, transcript and setting.target/debug/anarlog-dev→desktop): markergpuimakes the Tauri binary exit 0 at once withanarlog-gpui --identifier com.hyprnote.devrunning; GPUI's Switch back flips the marker totauri, quits, and leavesanarlog-devrunning with its window mapped. With a stale Tauri instance still open, the relaunch correctly defers to the existing launch lock.Surfaces at parity with the Tauri app
Each surface is a port of the named Tauri source, using the frontend's own SQL statements for reads and writes (no migrations; the Tauri app remains the schema owner and the rollback path):
windows-title-bar.tsx,shell-scaffold,windows/v1.rs,main/left-sidebar-panel.ts): client-side decorations, File/Edit/View/Help menus, collapsed chrome, Open in New Window, light/dark from the sharedthemesetting, the app's hotkeys (leaveOverlayTab: Escape on a settings / folders / templates / calendar / contacts / automations tab and the custom sidebar's back arrow return to the tab it was opened from — the settings tab for one opened from the settings nav, the note otherwise); the resizable sidebar is sized likereact-resizable-panels' panel — a persisted share of the panel group split the way its two flex items split the free space (the sidebar's exactflex-grow, the content'stoPrecision(3)share, the 4px handle excluded), clamped to 200–360px, so it scales with the window as the web view's does — and the share is read from and written to the webview'slocalStorage(react-resizable-panels:classic-main-sidebarin WebKit'sItemTablefiles under the data directory or the WKWebView website data) with a copy in the shell'sstore.jsonscope, so either shell opens at the width the other left.sidebar/timeline/*,calendar/queries.ts,note-filter*,store/zustand/timeline-selection.ts): sessions and calendar events, buckets, sticky headers, current-time line and launch anchoring,scrollTimelineItemIntoViewfor a newly opened note's row, Now / Open calendar chips, Grouping and Ordering submenus, ignored-event exclusion,store.jsonrecents and pinned tabs; multi-selection (cmd/ctrl-click toggles with the anchor folded in, shift-click ranges in flat order, mod+A with sidebar note context, a press outside the timeline / its dialog / its menu clears), row context menus (Open in New Window / Show in folder / Delete Note; Delete Event / Delete This Event / Delete All Recurring Events, or Show Event / Show This Event / Show All Recurring Events for an ignored event), the list menu (Delete Selected (N)with a selection, otherwiseShow Deleted Events/Hide Deleted Events, which lists ignored events atopacity-40with a struck-through, inert title), and bulk delete: Backspace / Delete opensDelete N selected notes?and confirming soft-deletes the batch under oneN notes deletedtoast whose Undo restores the group and reopens the first note (useRestoreGroup).sidebar/settings.tsx,settings/**): nav with search and Pro locks; General (switches, language / timezone / week-start selects, spoken-language chips, storage), Appearance, Notifications, Meetings, Transcription and Intelligence (provider registries, cards, keyring credentials; live LLM catalogues —settings/ai/shared/list-*.tsported per provider withlist-common.ts's ignore rules, recency order andremoveNonStreamingModels, cached per provider config like the["models", …]query, shown in theModelComboboxwithdisplayLlmModelIdnames,Search or create new, the freeformSelect "…"row, ignored models behind the eye toggle withDeprecatedbadges, refresh;getDefaultLlmSelection/handleProviderChangeresolving the provider's preferred model and resetting the effort; theReasoning effortrow persupportsReasoningEffort; key verification —packages/provider-validation'sverifyProviderCredentialswith its per-provider probe table, control request and one-minute memory gatesconfiguredlikeuseProviderAvailabilityand runs before a key saves, its message shown under the form (a Custom STT credential is accepted after the static checks without a probe, Validate Custom STT without requiring an OpenAI model catalog #7446); local servers (Ollama / LM Studio / Unsloth) are probed every 5s while the page is open with the selected one bypassing the gate; connection health —useConnectionHealth'sHigeneration against the selected model (OpenAI-compatible, Anthropic, Google, Azure shapes, five retries) drives the green check, the spinner and theConnection failed: …alert), Privacy, Permissions (realprobe_mic/probe_speaker), Account, Developers (CLI, MCP config, agent skills, webhooks), Dictionary (PlanGate), Stats, Teams, Sync, Imports.folders/*,templates/*,calendar/*,contacts/*,automations/*,settings/automations/*): editors, dialogs, vault mirroring throughanlg-fs-sync-core, template form / Auto format (the section textareas'resize-ygrip drawn pixel for pixel, dragging it sets the field's height abovemin-h-[100px]and focuses the field like WebKit), month grid with event / session chips and context menus plus the compact 4 / 2 / 1-column day strip below 700px (VIEW_BREAKPOINTS, 84-day scroll range,advanceCompactarrows, Today re-anchoring), contacts with the bit-identical facehash avatar raster, uploaded photos (metadata_json.avatarDataUrlshown in rows / headers / cards; the AvatarUploadButton opens the native picker and stores a centre-cropped 70px JPEG q0.85 data URL likecompressAvatarImage; Remove photo in the header menu), the organization column (editable name written through on change, People heading with the member count, three-column member cards with mail / LinkedIn buttons), Escape / view switches dismissing open dropdowns like Radix, theRelated Notesheader'sSearch...field (sortAndFilterRelatedNotes: a trimmed lower-case title filter, the list byDate.parse(createdAt)in the chosen direction with the id as tiebreak,No results found., the clear button and Escape clearing) and WebKit's 6px scrollbar gutter on the list and detail columns once they overflow; the person view's Summary section (contacts/contact-summary.ts,ContactSummarySection): when the storedmetadata_json.contactSummarywas built from a different set of meetings (createContactSummarySourceHash— FNV-1a over the newest eight sessions' ids,created_atand theMAX(session, participant mapping, note / summary / template_output documents, transcripts)updated_atstamp — as the query key) the brief is generated through the enhance model with the ported system prompt, thetarget/existing_facts/meetingsJSON (getIncrementalUpdatereads only the meetings the saved sources have not seen and passes the saved facts; an edited or removed source forces the full rebuild;getMeetingSource'sSummary:/Notes:/Transcript:text throughextractPlainTextandcleanSourceText,truncateAtWordat 6,000 UTF-16 units per meeting and 48,000 in total),Output.object's schema in each provider's structured-output shape,maxOutputTokens: 4096, the 45s total timeout, the SDK'smaxRetries: 2backoff and the query'sretry: 1,normalizeFacts, theContact summary requires at least three factsguard, andupdateHumanContactSummary'sjson_setwrite; the header spinner while fetching, theanimate-pulseskeleton (three dotted bars on a two-second 1 → 0.5 → 1 curve, 150ms apart),Summary generation failed+Try again(arefetch), the facts list with the failure footer when a refresh fails, and the explainer copy otherwise; the Automations tab withAutomationsNav(starters, drafts, workflows and automation chats, search,+, row context menus), the overview,StarterAutomationDetails(steps, the folder picker / integration connect row, preview,Save draft/Save & enabledisabled like the signed-outbilling.isReady),CustomWorkflowDetailswith theWorkflowBuilder(trigger select, stacked action steps with their config, add / remove,Needs setupbadges) persistingautomation_workflowsin the frontend's JSON shape, and the forced-open right chat panel; the automations engine and webhooks (automations/engine.ts, thelocal-apiplugin'sdispatch_event): the plugin's outbound webhook dispatcher, meeting markdown export and cloud-snapshot shape now live in the sharedcrates/local-api-core(the plugin delegates with its bindings unchanged; the core emits every object's keys sorted, the byte layout the plugin'sserde_jsonhas always produced), and the shell runs the same automations at the same points —meeting.completedonce a fresh stop's post-stop processing settles (the awaited batch repair included, the recovery passes excepted) andnote.enhancedonce a summary lands: the HMAC-signed webhook deliveries (x-anarlog-event/-delivery/-timestamp/-signature, the{created_at, data: {meeting, transcript_text}, event, id}envelope, the retry ladder), the markdown-export starter writing<date> <title> [id8].md(re-exported with the summary onnote.enhanced, the stale title's file removed) and itsautomation_markdown_export_last_runrecord, the Slack / Linear / Notion starters (which end whererequireSupabaseSessionends for the signed-out shell —sign in to run this automationafter the recap / action-item checks,no action items found for this meetingas a success), and the enabled custom workflows withlastRun(okor the joined step details, the first error otherwise) and their fifty processed sessions, the integration steps failing withchoose a Slack channel firstand the frontend's other messages.outer-header,note-input,packages/editor): breadcrumb title, Record / Join & record CTA, overflow menu (Folder, Meeting info with participants, Export to PDF / TXT / MD / Org, Upload audio / transcript, listening controls, Open in New Window, Delete with undo toast), view switcher, template picker and suggestions, default summary creation, memo editor operating directly on TipTap JSON (input rules, keymaps, marks, lists, undo/redo, IME, clipboard — pasted text splits into paragraphs on runs of line breaks like the defaultclipboardTextParser, and copies join blocks with a blank line likeclipboardTextSerializer) with debounced writes; links aspackages/editor'surl.ts/autolinkPlugin/linkBoundaryGuardPlugin/linkOpenPlugin: URL-looking text is linked as you type (bare domains against thetldslist, trailing punctuation and unbalanced closers excluded, email domains skipped), a link'shreffollows its text and extends over path text typed after it, links whose text stops being a URL are dropped, typing at a link's edge honoursinclusive: false, typing over a selection carries the link likemarksAcross, and a click on linked text opens http(s) hrefs; task items astasks.ts/taskListRule/taskIdentityPlugin/TaskItemViewand the desktop's SQLite task storage:[ ]/[x]make a task list (nesting after an item's first paragraph like ProseMirror's fitter), items carry{ status, checked, taskId, taskItemId }with unique ids re-issued after splits and pastes, the checkbox toggles done/todo and renders pertask-list.css(checked content at 50% with the fit-content strikethrough), and every memo write mirrors the items intoaction_itemswith the frontend's own statements (tombstone dropped rows, upsert the rest keepingdue_at, no-op when unchanged); mentions as the mention widget and node view:@at a word start opens the suggestion popup (findMentionwith atoms masked liketextBetween's object replacement), Up / Down / Enter / Escape and hover / click act on it, choosing an item replaces@querywith themention-@node plus a space, chips render like.mention(20-character label at weight 500 after a 1em facehash avatar or Note / Buildings / User glyph), the caret skips the atom likementionSkipPlugin, and a click navigates likeMentionNodeView(sessions open; people and organizations open the Contacts tab); the format toolbar (widgets/format-toolbar.tsx): a non-empty selection floats the bold / italic / underline / strikethrough / code / highlight buttons over it with floating-ui'stopplacement (offset(8), flipped below and shifted inside the note's scroll container, thering-1outside the box, active buttons onbg-primary), never over the enforced title heading, and thehighlightmark renders like.note-typography mark(yellow-200over the inline box, 2px radius, dark text in dark mode); Shift+Home / Shift+End extend to the line edge; clip embeds (clip-paste.ts): a pasted YouTube watch / short / embed /youtu.belink,<iframe>/<Clip>snippet, or clip link (resolved through the clip page) becomes theclipblock atom with the embed URL throughreplaceSelectionWith(a selection deleted first, an empty textblock replaced, an edge caret followinginsertPointup to the nearest ancestor that takes a block, the middle splitting the textblock around it), rendering as the empty block it is in the desktop app; attachments (ResizableImageView,FileAttachmentView,fileHandlerPlugin,useFileUpload,catalogLocalNoteAttachment,imageTrailingParagraphPlugin):imagenodes resolve their attachment id to<session>/attachments/<id>(the fs-sync core's attachment functions, now a sharedattachmentsmodule the Tauri plugin delegates to) and render ateditorWidth%of the editor withrounded-md bg-card, thering-border ring-offset-2hover ring and the two resize pills whose drag writeseditorWidthback likehandleResizeStart(min(maxWidth, max(120, start + delta)), rounded to a percentage of the editor);fileAttachmentnodes render themy-1 rounded-lg border bg-muted px-3 py-2.5card with the mime icon or image thumbnail, name, size and the hover open / remove buttons; a pasted clipboard image or dropped files are saved underattachments, catalogued with the frontend's statements (session_attachments,attachment_local_state, the transfer jobs) under the session lock and rolled back on failure, then inserted as theimage/fileAttachmentnode with the schema's attr order and theconvertFileSrcURL (a dropped audio file runs the audio import instead;Attachments must be smaller than 4 MBand upload errors aresonnerToast.errors); every top-level image keeps a paragraph after it, and the editor fills the viewport (min-height: 100%) with a click-to-end tail instead of a fixed block.plugins/tantivy,search_index.rs): the tantivy schema, tokenizers, query construction, collection management, and the SQLite → tantivy projection worker now live in the sharedcrates/search-indexcrate; the Tauri plugin and worker delegate to it (bindings and behaviour unchanged), and the shell opens the same<vault>/search_indexcollection, runs the sameinitialize/drain_queuesteps (retrying while another process holds the writer lock), and answers@mention queries with tantivy hits likeuseSearchEngine().search.header-transcript.tsx,transcript/renderer,useTranscriptScreen,stt/utils.ts): sharedanlg-transcriptsegmenter, speaker labels and OKLCH colours, live / degraded / batch / empty screens, audio player (rodio playback, wavesurfer-style bars, seek, context menu, Delete recording, and the Pro{rate}xbutton with its0.5x–2xlist driving rodio's speed, gated on the sharedisProlikeuseBillingAccess; the position is tracked from a wall-clock anchor because rodio'sget_posreports output time that no longer maps to the recording once the speed changes); word spans (WordSpan: seekable words highlight on hover and seek-and-play, non-final words at 60% italic, the sentence line under the playhead onbg-yellow-100/50), the right-edge scroll-to-top / bottom pill (useScrollDetection, smooth scrolling, disabled at either end),usePlaybackAutoScrollcentring the current line until the user wheels, Space toggling the player outside text fields; speaker assignment (SpeakerAssignPopover/SpeakerParticipantPicker: participants and event attendees first, people, theAdd "…"create option that creates a human and links it to the session,Apply to allwriting thescope: speakerhint across the session's transcripts or ascope: segmenthint for the words;upsertSpeakerAssignment/findSpeakerAssignmentAnchorWordId/mergeTranscriptSegmentAssignmentsported over the stored JSON behind themutateTranscriptrevision check, withutils.test.ts's fixtures); edit mode (the pill's pencil / check: segments become editors savingupdateTranscriptSegmentTexton blur or Cmd/Ctrl+Enter with Escape restoring, sections toggle a selection — Shift ranges, Cmd/Ctrl-click outside edit mode — with theMultiSelectionBarabove the chat CTA:N selected,Change speakerpicker,Mergewhen contiguous same-channel sections follow a labelled one, clear); text selection (renderer/selection.ts/selection-menu.tsx: dragging across the words selects a DOM-range-like span across segments under--selection-overlay, a right-click selects the segment, and the floatingChange speaker / Play from here / Copymenu opens under the selection or at the pointer,Change speakerswapping in the picker, Escape and outside presses closing it); find in note (search/context.tsx/bar.tsx/matching.ts: Cmd/Ctrl+F opens the bar under the header — below the audio player on the Transcript tab — with match case, whole word, then/mcount, Enter / Shift+Enter and the carets stepping with the match scrolled to the centre, Esc closing; transcript matches use the space-joined word index and highlight per word inbg-yellow-200/50with the active match inbg-yellow-500; the memo countsgetEditorMatches' occurrences, decorates them likeprosemirror-search, and Cmd/Ctrl+H adds the replace row replacing the current or every match); and theAsk anythingchat CTA pill ofFloatingActionButton/FloatingChatCTAgrowing into the prompt bar on hover.transcriptionplugin overlistener-core,useSTTConnection,createCaptureLifecycle): the sameRootActor, provider credentials inSessionParams, live delta journaling / materialization / flush (the queue stays open while the engine finalizes and is flushed atInactive, whereonStoppedawaits it), session-audio cataloguing, floating recording bar (compact pill and the expanded live-transcript panel: title, bubbles from the engine's live segments withrenderLabelspeaker labels and overlap outlines, pinned auto-scroll with theGo to bottomchip, expand / collapse writinglive_caption_minimized, new sessions starting minimized likeLiveCaptionDefaultVisibilitySync; amplitude scaled likeupdateLiveAmplitude), upload-audio import andlistener2-corebatch transcription withtransformBatchand thewhole_sessionpromotion; the batch progress screen'sStop transcription(aborting therun_batchtask into theTranscription stopped.state), the transcript pill spinning while finalizing or batch transcribing, anduseEnsureDefaultSummarycreating the Summary document when a batch starts or fails; transcription hints (stt/useKeywords.ts:getSessionKeywords' SQL for mapped participants, attached event attendees, dictionary terms and#hashtagsfrom the note / title / event, normalised and capped at 50, sent askeywordson live and batch starts, and the fullgetTranscriptionLanguagescodes such asen-US); voiceprints (services/voiceprint.ts+ the plugin'svoiceprint.rs, now the sharedcrates/voiceprint-servicebehind aSecretStoretrait with the plugin as thin command wrappers: candidate extraction after a live capture's final flush and after a batch transcript, ahead ofmarkSessionAudioTranscriptionComplete, honouringremember_speakersand thetranscription.mic_isolationstore scope fed byMicIsolatedevents; promotion to exemplars on anApply to allspeaker assignment;known_speakersfor the batch params; the six-hourly expiry sweep; embeddings in the same keyring entries); the post-capture lifecycle (createCaptureLifecycle/finalizeStopped: once the transcript flush and the engine'sInactivedetails meet,getPostCaptureActionruns the post-stop batch repair for record-only, degraded (needsBatchRepair) or unpersisted captures —current_capturepromotion with the audio offset and the live transcript replaced, orwhole_session— or the summary alone with the completion cue; the summary runs inregeneratemode when the capture extended an existing transcript; a fresh stop whose live text was saved starts the summary at once and recordsrefreshSummaryAfterRepairin the marker so the repair regenerates it (main's Start summaries while post-meeting transcription runs #7437); the marker carries Protect recordings from empty captures and stale deletions #7444'sautomatic/preserveExistingAudio/initialTitle(false/true/ the session title for a manual capture, the auto-start's values otherwise); live captures carryparticipant_human_ids/self_human_id; thecapture_lifecycle_pending:recovery marker is written with the frontend's field names and order, moves tofinalizingwith the summary mode, clears once the audio is marked processed, and every leftover marker is finalized at launch as a recovered stop with the recovery component's retry budget; audio retention (services/audio-retention.ts: theaudio_retentionpolicy, the minute tick finishing tombstoned audio and deleting idle, processed, expired recordings, anddeleteProcessedAudioForRetentionafter a capture undernone); notifications (showBatchCompletedNotification/showSummaryReadyNotificationthrough the sharednotificationcrate the plugin wraps: theTranscription completecard after a re-transcribe / import batch or a post-stop repair without live text (notifyOnCompletion), theSummary readycard ("<title>" is ready to read.) after a summary lands, both only while the window is inactive andnotification_transcription_complete/notification_summary_completeallow it,Open Anarlogbringing the window back on the session likeopenNew; the tray and the GTK notification windows share one GTK main-loop thread); andnotifyFailure's toasts (Anarlog could not finish saving the transcript…/Post-meeting transcription failed…on the stop whose repair failed, andThe transcript was saved, but Anarlog could not start the summary…when thefinalizingmarker write fails — the summary and audio completion wait for that write likefinalizeStopped, and a failed write hands the stop to the recovery instead — unless the note was deleted; the recovery retries stay quiet)); scheduled auto-start (stt/scheduled-auto-start.tsx,scheduled-session-auto-start.tsx,empty-automatic-capture.ts): withauto_start_scheduled_meetingson, a linked, timed calendar event that starts — or started within the five-minute grace — opens its session (created from the event likegetOrCreateSessionForEventId), opens the meeting link whenauto_join_scheduled_meetingsis on, and records withautomatic: true; due meetings are picked newest start first with their overlaps claimed, ignored events and series are skipped, a live capture skips them and a finalizing one retries on the 15s tick, the armed note starts once loaded or gives up after 30s, and the marker carries the realpreserveExistingAudio; at stop,finalizeStopped's order holds: an automatic capture that recorded no speech (audioHasSpeech) into a session the start left untouched (title, attachments,isSessionEmpty) loses its audio and clears its marker before anything is catalogued, and the audio is catalogued at that point otherwise).plugins/tray): the tray's schedule logic (agenda_sections,menu_bar_title,next_schedule_refresh_mswith their tests), icon bitmaps and menu labels now live in the sharedcrates/tray-core; the Tauri plugin re-exports them unchanged and the shell drives thetray-iconcrate from a GTK thread with the same indicator:Today/Tomorrowagenda sections built likebuildTrayScheduleEventsfrom the timeline's timed, non-ignored events within seven days,Show events in menu bar(a check item persisted instore.json'sanlg-trayscope likescoped_store),Open Anarlog…,Start a new meeting(disabled while recording),Settings,v<version> (<channel>),Hide,Quit Completely…with its confirmation; the recording frames cycle every 250ms and the degraded icon follows the capture lifecycle like the listener runtime'sset_start_disabled/set_degraded/set_recording;show_tray_iconshows or hides it at once. The shell has no updater, soCheck for Updatesis absent, and since gpui 0.2.2's X11 client ends the run loop with the last window,Hideiconifies the window rather than unmapping it and a window close still quits.chat/components/*,chat/store/*,chat/transport/*,chat/tools/*,chat/context/session-context-hydrator.ts,chat/store/chat-title.ts): the floatingPersistentChatpanel now runs the conversation — theAsk anythingcomposer (submitShortcut="enter": Enter sends, Shift+Enter breaks the line; the send /Stop responsecontrols; sends while a reply streams queue likeChatQueue; voice input asuseDictationover the dictation plugin'sRecorder— the microphone button records a temporary 16 kHz WAV through the app's audio provider with the five-minute cap, refuses withVoice input is unavailable while Anarlog is recording a meeting.during a capture, shows theVoiceStatusrow (the fourteenchat-input-waveform-bars,m:ss, thebg-mutedstop button, the send /Stop responsecontrol) with theStarting…/Transcribing…spinners, runs the batch transcriber over the file (numSpeakers: 1, no keywords) and inserts the joined words likeChatEditorHandle.insertText;No speech detectedand theCould not start / transcribe voice inputtoasts carry their descriptions, and the recording is cancelled when the panel closes or the scope swaps), context refs (chat/context/*,mention-config.ts,session-drag.ts):@in the composer opens the same suggestion popup as the note editor (findMentionat a word start, five rows, tantivy for a query and the titled sessions / named people / named organizations for none, ↑ ↓ Enter Escape) and the chosen row becomes an inline chip — theTextAreanow carries atoms withmentionSkipPlugin's caret skipping and whole-chip deletion, theMentionAvatarpainted over the placeholder; a timeline note dragged onto the panel (application/x-anarlog-session-context, the row's title over its time as the drag image) becomes asession:manualref for the next send and dropped onto a note editor lands as a mention plus a space at the pointer likesessionMentionDropPlugin; the sent message flattens the chips to@labellikeproseMirrorJsonToText(which also joins the blocks with nothing, so a Shift+Enter break is not in the text) and carriessession:auto, the dropped notes, and the mentions deduped by key (mergeContextRefs; queued sends keep the refs they had), withContextRefmodelling the session / human / organization / folder kinds and keeping unknown ones verbatim; the transport hydrates every ref across the conversation once — sessions into the<context>block (noFull Meeting Transcriptwhen nothing rendered, likebuildTranscript), contacts asReferenced contact:with title / organization / email / notes, organizations asReferenced organization:, folders asFolder context:with instructions, materials and up to fifty notes — as the text blocks after it; message history (useMessageHistory): Up at the very start recalls the sent drafts newest first (fifty kept, shared across chats, the current draft set aside), Down walks back to it, with theHistory N/Mline above the surface, and Mod-Z / Mod-Shift-Z undo in the composer), the empty state's suggestion rows while a note is open (List action items./Draft follow-up email./Find key decisions.sending their prompts), thebg-blue-100user bubbles and the assistant markdown rendered withchatComponents'text-smtypography (headings, lists, code) plus the reasoning disclosure title,Thinking..., theborder-red-200 bg-red-50error bubble with its hover Retry, the hoverCopy message/Regenerate messageactions,useChatAutoScroll's pinned scrolling withGo to recent, theRECENT CHATSdropdown (last five groups of the scope withformatDistanceToNow,No recent chats) andNew chat; a press outside closes only while the draft is empty.chat.modefollows theFloatingClosed/FloatingOpen/RightPanelOpenmachine:Open in right paneldocks the chat beside the note surface (MainChatPanelsported inchat_panel_layout.rs/workspace/chat_panels.rs: the body and the panel split the group by thereact-resizable-panelsshare persisted underreact-resizable-panels:main-chatin the web view's localStorage — 30% default, 20–50%, thew-0handle's 5px hit area drags it and either app reads the other's layout — with the body minimums (NOTE_SURFACE_MIN_WIDTH_PX500, automations 600, settingsmin(700px, 100%), plus the 200px sidebar) winning over the share and overflowing the window like the flex layout, the timeline sidebar sized against the body panel,border-x bg-card rounded-tr-xl;useNoteSurfaceWindowWidthGuardon the note-surface tabs collapses the sidebar when the note would drop under 500px, grows the window by the deficit throughwindow.resize— restored when the chat closes, GPUI has no frame-origin API so the macOSexpandLeftgrows rightwards — and collapses the sidebar on a narrowing resize) with theh-9toolbar (history,New chat,Float chat,Close chat), the messages pinned to the bottom of the column and the elevatedrounded-xlcomposer whose send button is always shown; Escape closes the floating panel only. The automations tab shows its ownautomationschat scope in that panel (no toolbar, no note context,chatScope: automationson its rows,useRecentChatGroupsover the scope's groups) and each automation selection keeps its own chat thread likeswitchTo— a chat row opens its group, a workflow itschatGroupId, a starter or draft a fresh chat. While the open note is still batch transcribing without a transcript (isBatchTranscriptionPending && !hasAvailableTranscript), a send persists the user row and the cannedThis recording is using batch transcription…assistant row without a generation. The transport isCustomChatTransportover the shell's streaming client:chat.system.md.jinjafor the configuredai_languageplusMEETING_CONTEXT_TOOL_GUIDANCE, theContextBlocktemplate overhydrateSessionContext's snapshot (title, date, memo and enhanced markdown viajson2md's loose lists, participants, event, meeting chat, the rendered transcript) prepended to the last user message only, the 20 → 10 message window, and the earlier turns as history. Persistence usesqueries.ts' statements andbuildPersistedChatMessage's shapes: the user row lands before the stream (createChatGroupWithMessagewithcreateFallbackChatTitle, thengenerateChatTitle's prompt →normalizeGeneratedChatTitle→setChatGroupTitleIfCurrent;Could not save this chat message.rolls back), the assistant row on finish (step-start/reasoning/textparts withstate: done,metadata.createdAt), aborted replies stay unpersisted, andregeneratereplaces the previous row likereplaceChatMessage. The message metadata carrieschatScope: generaland thesession:auto:<id>context ref, so groups written by either app open in the other. The request advertises the fifteenchat-generaltools with the schemas the Tauri app sends (tool_choice: auto) and runs theToolLoopAgentloop (stepCountIs(5)): streamed tool calls are assembled per provider family (OpenAItool_callsdeltas, Anthropictool_use/input_json_delta, GeminifunctionCall), executed locally, appended astool-<name>parts (input-available→output-available/output-error,step-startbetween steps) and sent back as the assistant'stool_callsplustoolresults, with earliersearch_meetingsoutputs hydrated undercontextTextlikeexpandSearchMeetingsOutput.list_meetings/get_meeting/get_meeting_transcript/get_recurring_meeting_historyrun the sharedagent-accesscrate the db plugin delegates to;search_meetingsruns the search index withfilters.created_at(absolute orrecent_days) or the folder's notes;search_meeting_content/find_related_meetingsarenote-files.tsported (sections,queryTerms, the 20 / 8 / 3 scoring, 180-character snippets, same-event / shared-participant / nearby-date reasons);search_contacts/search_calendar_eventsuse the frontend's SQL;web_searchanswersSign in to use web search.like the signed-out app. The cards areDisclosures: the search card (Searching for: …/Searched for: …, theQuery/Date/Limitlines, one result card per view with the carousel arrows,No results found) forlist_meetings/search_meetings, the generic wrench card (Running …, thekey: valueinputs, the output JSON,… failedwith the error) for the rest. The mutating tools are ported over the frontend's own statements:move_meeting_contents(moveSessionContents: the busy-session and empty-target checks, the audio copy and catalogue rows, the transactional transcript / document / action-item / voiceprint moves with the audio rolled back on failure) andapply_session_correction(exact / bounded / loose replacements over the summaries, transcript words and title, the guarded transaction, dictionary terms saved);edit_memo/edit_summarypersist asession_proposalsrow likepersistChatSessionProposal(target document,base_updated_at,source: chat) and open the edit tab (edit/tab-content.tsx): the session title andMemo/ summary title header withDecline/Apply to memo|summary, the error row (This proposal is stale. The meeting changed after it was created.when the target'supdated_atmoved), and@pierre/diffs' unifiedMultiFileDiffrebuilt from its measured output — the 44px file header with the modified-file ring and-N +M, jsdiff'sstructuredPatchhunks (its Myers variant ported, four lines of context, the\ No newline at end of filerows), 20px mono rows with the 4px change bars (dotted for deletions), the right-aligned number column,pierre-light/pierre-darkline and gutter backgrounds mixed in Lab like WebKit'scolor-mix, the theme's markdown token colours (headings, list markers, bold / italic / code / links), anddiffWordsWithSpaceword emphasis inword-altmode (a blank between two changed words folds into the span) as 17pxborder-radius: 3pxboxes; lines stay unwrapped and scroll sideways likeoverflow: scroll. The chat shows thedefineToolcard for both (ToolCardHeaderwith the spinner → the pencil in emerald once applied,Edit memo — review tab opened/Memo updated/Memo edit declined/… edit failed, the StreamdownMarkdownPreviewof the proposed content at the classes the desktop bundle actually generates —mt-6 mb-2 font-semibold text-3xlheadings,list-discoutside the unpadded list,li { py-1 }— the redToolCardFooterErrorwith the summary candidates, andDecline/Apply to …while the tool waits); applying from either surface runsapplySessionProposal(memoraw_mdupsert orupdateEnhancedNoteContent,applied), declining or replacing the review declines a chat proposal likedeclineOnUnmount, and the note'sPendingProposalsBanner(1 pending edit/N pending editswith aReview memo/Review summarybutton per row, refreshed with the note) opens proposals written by the CLI / MCP or an earlier session throughloadSessionProposal, withLoading edit…/This edit is no longer pending..open-note-dialog.tsx,sidebar/toast/registry.tsx,SessionShareButton,onboarding/*; the onboarding loops the sfx plugin's own BGM track at 0.2 with the mute toggle and stops it on finish, and its<video autoPlay loop muted>backdrop plays as an animated WebP ofonboarding-video.mp4(416×232, 24fps, 5s loop, pre-blurred since GPUI has no backdrop blur, 31 KB) atopacity-28under the same gradients — the onboarding root now carries the resolvedfont-sansfamily and base text style, so thefont-semiboldsection headings render bold like the web view's; the welcome note's Join & record runsHeaderMeetingControl.joinMeeting: the demo opens in the browser withbuildWelcomeNoteDemoUrl'sautojoin=1and acompletion_urlon a loopback callback server while the capture starts, and the demo's completion callback stops it likestopActiveWelcomeDemo; recognised remote meeting links open alongside the capture and unknown links fall back toRecordpercanJoinFromHeader).plugins/deeplink2,tauri-plugin-single-instance,useDeeplinkHandler): the deep-link routes (/auth/callback,/billing/refresh,/integration/callback,/onboarding-demo/complete,share/open), their query types, redaction, and the browser callback page now live in the sharedcrates/deeplink-core(the plugin keeps its tauri_specta events, pending queues and axum server over it, bindings unchanged); the shell acceptsanarlog://…URLs as positional arguments, the Tauri launcher forwards its arguments on hand-off, a secondanarlog-gpuilaunch hands its URLs to the running instance over a Unix socket next toapp.dband exits (a bare relaunch or{scheme}://focusjust brings the window back, reopening it when closed), macOSon_open_urls/on_reopenfeed the same channel, and the loopback callback server answers the first request with the shared page and routes it. The onboarding-demo completion is handled; the account-bound routes are logged as unsupported until the signed-in flows exist.services/enhancer/index.ts,store/zustand/ai-task/*,note-input/enhanced/*,header-enhanced.tsx): the enhancer service and theenhance/titletasks — the session content snapshot with the rendered transcript segments,transformArgs(memo template sections, event / participants, supplemental meeting-platform and meeting-chat context,summary_length,auto_summary_prompt, dictionary terms) over the sharedanlg-template-appprompts withformatSummaryLengthModeGuidance/formatSummaryLengthGuidance/ preferred names; a streaming chat client for the OpenAI-compatible, Anthropic, and Gemini request shapes withreasoningProviderOptions, the<think>/<thinking>extraction,maxRetries: 4backoff, andllm_health_error_messageerrors;withEarlyValidationRetry(createEnhanceValidator: preamble strip,#requirement, the first template section within a 30% edit distance,IMPORTANT: Previous attempt failed.on retry),normalizeBulletPoints, the start (60s, 5 minutes for on-device servers) / idle (15s) timeouts and the 256 KB cap;runEnhanceSuccess(constrainSummaryLength, hashtag tags intotags/session_tags,ensureMarkdownFirstLineTitle, thetitletask for untitled sessions withgetPersistableGeneratedTitle,persistGeneratedEnhancedNote's guarded body swap and pending-marker clear,applyGeneratedSessionTitle);collectEnhanceImageContextfor models that take images (modelSupportsImageInput's rules and the Cloudflare vision list): the memo's image nodes and the transcript memos' markdown images read from the session's attachments or their data URLs, capped at 8 MB each, squeezed under 128 KB apiece / 768 KB in total by re-encoding as JPEG down the 1280 → 512 edge and 82 → 52 quality steps, sampled to ten, and sent after the user prompt as the provider's image part (OpenAIimage_urldata URL, Anthropic base64source, GeminiinlineData) with theIMAGE_CONTEXT_NOTEahead of the length guidance;requestAutoEnhance("if_empty")after a capture's final transcript flush andqueueAutoEnhanceIfSummaryEmptyafter an audio import, with the durableauto_enhance_pending:markers, the 500ms eligibility recheck loop, the 5s pending-job resume, the exponential failure budget, andauto-enhance-startedswitching the open note to the summary;Summary wasn't generatedtoasts for short transcripts. The enhanced tab rendersStreamingView(Analyzing structure.../Model is thinking...with the tip line, theSummaryTitleSpacewithGenerating title..., the streamed markdown with the block caret),EnhanceErrorwith Retry,ConfigError, and the persisted document withenforceTitleHeading(the 1.5rem title h1 and theUntitledplaceholder); the header pill shows the spinner while generating and turns red on failure; the template picker'sRegenerateregenerates under the note's own template and choosing another template runsservice.enhancewithtargetNoteId/templateTitle. The persisted summary is editable likeEnhancedEditor: aBodyEditoroverensureFirstLineTitle(content, sessionTitle)with thetitleHeadingPluginbehaviour (an h1 stays first, a lone empty title gets a paragraph) anddocumentTitlePlaceholderwhile the anchor sits in the empty title; flushes runupdateEnhancedNoteContent(body, stale pending marker, the session title from the first line, the canonical-empty-document skip), and find / replace target the summary editor on its tab.playCompletionSoundrenders the chosencuelumecue (ready/success/chime/sparkle/bloom) offline the way its Web Audio engine does — tone and filtered-noise layers with exponential envelopes and glides, the feedback shimmer,OUTPUT_GAINinto a port of Blink's compressor curve with its makeup gain and look-ahead — at 0.7 unless notifications or the completion sound are off, and the Notifications page'sPreviewplays the selected cue.pl-2, Windows moves them into the title bar beside the toggle and drops the row),DropdownMenuRadioItem's 8px dot in thepl-8slot for the sort menus, and Radix's flip: an inline menu that would leave the window opens 4px above its trigger instead of below.ui::ring— GPUI's shadow shader integrates over 3× the blur radius, so a spread-onlybox-shadowpaints nothing; the shadcnInput'sfocus-visible:ring-1 ring-ringon the New folder dialog and webhook URL fields, the template textarea'sring-2 ring-primary/20, the note image'sring-border ring-offset-2hover ring, with--ringin the theme), WebKit-style line breaking (pre-wrap+break-word, and a port ofInlineContentConstrainer::prettifyRangefortext-wrap: pretty), fontconfig-resolved font stacks, WebCore's truncated line heights,rounded-full= 0.5rem, Lisse squircle corners, 6px WebKit scrollbar gutters, sonner toast geometry, glass dialog cards composited to the colour Tauri's blurredbg-card/60resolves to.Deliberate deviations: the Auto format editor's paragraphs sit 20px apart where the web view's
prompt-editorgives each<p>a 4px bottom margin (the shell'sTextArealays the text out as one block; the field is Pro-gated and read-only signed out); theopenai/azure_openaiproviders speak chat completions (response_format.json_schemafor structured output) where@ai-sdk/openai/@ai-sdk/azureuse the Responses API (/responses,text.format); no backdrop blur or polygon clipping in GPUI (dialog overlay, System theme card, share gate); app-menu chrome instead of OS context menus; X11 lets the window manager place the floating bar (and GPUI has no window move, so a size change reopens the window at its top-right corner); the panel title's 13px bold glyphs rasterise 1–2px higher than WebKit's hinted ones; Tauri's live queries only see their own connection's writes, so shell writes appear in Tauri after its next write or restart; the rate list opens below its button (Tauri anchors itbottom-full, where the top player'soverflow-hiddenshell clips it to a sliver — the shell also flips it above when there is no room); a stopped or failed batch on a session without words showsTranscriptEmptyState's error screen (Tauri'suseTranscriptScreencountsbatchErroras visible state, so it lands on thereadyscreen with nothing to render — the error branch there is unreachable); the transcription hints omitretext-keywords' part-of-speech nouns and phrases (its Brill tagger ships a ~100k-word LGPL lexicon that is not bundled), so the shell sends the deterministic subset — fewer hints, never different ones.Not yet at parity: shared-folder / shared-template import and community templates; signed-in / Pro states (Teams, Sync, Dictionary, share flows, and the auth / billing / integration / share-open deep-link targets, which the shell routes and logs); on-device model rows and the subscription OAuth dialogs (the Soniqo / Apple Speech / BYO-model / Apple Foundation providers are hidden off Apple Silicon like Tauri's availability checks); the STT page's model catalogue stays the registry's list; health probes for the subscription providers and the responses API;
read_folder_material's material read (its folder view is unreachable in the shipping app too:getFolderFilterreads a sidebar folder filter nothing sets, so both apps answerNo folder is selected); the remaining capture lifecycle (local model server, the settled-diarization refinement and theanarlog cloudbatch target behind sign-in; live partials are not rendered by the shipping app either —partialWordsByChannelonly feeds its own store);requestAppAttention(GPUI has no window-attention API); the signed-in integration pickers and the automations' Slack / Linear / Notion deliveries (they need the signed-in API session); connected-calendar sync and selection UI; Windows single-instance forwarding (named pipes); close-to-tray and the macOS / Windows tray; rich HTML paste (the GPUI clipboard exposes text and images, nottext/html) and imagetitletooltips; UI localisation; macOS / Windows verification.Verification
cargo clippy --locked -p desktop-gpui --all-targets --no-deps -- -D warnings;cargo test --locked -p desktop-gpui(380 tests covering the timeline fixtures, editor model, input rules, store row shapes, settings parsing, transcript rendering and colours, live delta journaling, batch transforms, waveform, exports, squircle, avatar raster, WebKit line breaking andprettywrapping, shell binary mapping, timeline selection semantics, show-deleted-events listing, the glass card fill, the calendar breakpoints and compact scroll index, URL validation / autolink matching / the ported autolink fixtures /marksAcross, task status normalisation / id sweep / extraction, mention trigger detection / candidate search / node shape / facehash hash, query normalisation, avatar JPEG compression / data-URL decoding, floating-panel sizes / top-right resize / titles / segment deltas / bubble labels and overlaps, automation starters / workflow parsing andJSON.stringifyorder / readiness /formatDistanceToNow, transcript word ranges / sentence lines / seekability / timeline offsets, the portedupsertSpeakerAssignment/ anchor / merge /updateTranscriptSegmentTextfixtures, speaker picker groups / event options / create option, selection merge targets, text-selection caret ordering, the find bar's query preparation / occurrence scan / word attribution / highlight merging, tray menu ids / channel names / schedule events, pasted line splitting, thelist-common/list-openai/ LM Studio / Ollama / OpenRouter / Unsloth fixtures andmodel-display.test.ts, the credential request table / accept predicates / static validation, health error messages, preferred-model selection, deep-link classification / argument filtering / demo URL / single-instance forwarding / loopback callback server, the portedsummary-length.test.tsfixtures, eligibility floors, the enhance validator and early-check states, bullet normalisation, hashtag tags, generated-title unwrapping,hasSummaryContent, template-title hydration, prompt assembly, the provider request shapes / SSE parsing / reasoning-tag extraction,ensureFirstLineTitle/extractFirstLineTitle/isCanonicalEmptyDocument, the title-heading enforcement, the cue recipes / envelopes / shimmer tails, the jsdiff line alignment / hunk context / word-alt emphasis / markdown token scopes, the session-correction replacements, the move-contents note checks, the dictation recorder / transcript join / elapsed format, the YouTube link / snippet / clip-id parsing and the block-atom insertion cases, theconvertFileSrcencoding and attachment node shapes, the toolbar placement, the notification cards and click routing, the image-context capability rules / reference collection / sampling / JPEG budget / attachment reads and the provider image parts,body_to_markdown's empty-paragraph newlines, the scheduled auto-start's due selection / next start / event instants / status actions, the sidebar layout's flex split /toPrecision(3)/ persisted-document round trip and the WebKitItemTableread / write, the automations' processed-session cap / task-item collection / run-record JSON, the related-notes filter and sort, the Custom STT verification skip, the chat panel split / body minimums / drag share / persisted-document round trip and the width guard's steps);cargo test --locked -p tray-core(the 13 schedule tests moved from the plugin),cargo test -p tauri-plugin-anlg-tray(bindings unchanged),cargo check --locked -p desktopcargo test --locked -p storage shell;cargo check --locked -p desktop;cargo test -p desktop export_types(bindings regenerated);cargo check -p xtask;pnpm -F @anlg/desktop typecheck;pnpm -F @anlg/desktop test(444 files / 3938 tests); oxlint and eslint on the touched files; lingui extract / compile (run through a small shim because the pod's Node 22.14 predatesimport.meta.main, which lingui 6's CLI entry points use; CI's Node 22 has it); the hand-off smoke test described abovecargo test --locked -p deeplink-core(13 tests: the plugin's route / share-open / callback-page tests moved with the code),cargo test --locked -p tauri-plugin-deeplink2(theexport_typesoutput matches the committed bindings; the committedbindings.gen.ts/apps/web/content/deeplinkswere already stale onmainand are left alone),cargo clippy --locked -p deeplink-core -p tauri-plugin-deeplink2 --no-deps -- -D warnings,cargo check --locked -p desktop; the shell'sdeeplinktests cover classification, argument filtering, the demo URL builder, a second launch forwarding over the socket and the loopback server answering once and shutting downcargo test --locked -p search-index(24 tests, the plugin's tokenizer tests and the worker's acknowledgement / projection tests moved with the code),cargo test --locked -p tauri-plugin-tantivy(bindings regenerated with no diff),cargo clippy --locked -p search-index -p tauri-plugin-tantivy --all-targets --no-deps -- -D warnings,cargo check --locked -p desktopcargo test --locked -p voiceprint-service(the plugin's 14 span / embedding / assignment tests moved with the code, plus the ignored fixture replay),cargo test --locked -p tauri-plugin-transcription(44),cargo check --locked -p desktop,cargo clippy --locked -p desktop-gpui -p voiceprint-service --all-targets --no-deps -- -D warnings; the shell's tests add the keyword builder (hashtags, code stripping, attendee / participant priority, the 50 cap), the rate formatting and the playback clock, and the mic-isolation merge / store round tripcargo test --locked -p fs-sync-core(94, unchanged: theattachmentsmodule wraps the existing helpers),cargo check --locked -p tauri-plugin-fs-syncgetPostCaptureAction's cases, thecurrent_captureaudio offset and word promotion, the recovery marker's serialisation / parsing (now withautomatic/preserveExistingAudio/initialTitle), and the retention policy / expiry / row decisionscargo metadata --lockedstable;pnpm exec dprint checkon all changed non-Swift filesuvx zizmor --format sarif .(1.30.0): 410 findings, none on thegpui_linux_cijob — the new CD line uses$TAURI_CONF_PATHrather than a template expansion; the code-scanning alerts on the new job (unpinned checkout, default permissions, credential persistence, and the twouses: ./…lines zizmor now wants in GitHub's$/self-repository form) are fixed by pinningactions/checkoutto the v4.4.0 hash,persist-credentials: false,permissions: contents: read, anduses: $/.github/actions/…; the 97 remaining self-repository findings are the repository's pre-existing./references, left as they areWEBKIT_INSPECTOR_HTTP_SERVERto confirm identical row, heading, and control positionsbg-accent; Backspace opens the confirmation card at the same 352..671 × 284..415 box with the same title / description / button rows, and the card fill is now within one step of the probe (#d5vs#d6); in the shell the shift-range → Delete → Undo round trip restores every note (0 tombstones) and reopens the first;Show Deleted Eventslists the ignored recurringWeekly syncstruck through, andShow This Eventwritesignored_events = []and the row returns at the future opacitySee linear.app/fastrepl-inc/overview. Ping support@x.com or x.com/docsinto a new note in each app persists byte-identicalsession_documents.bodyJSON (twolinkmarks withtarget: null, the trailing.and the email left plain) and renders the same blue underlined runs; a click onx.com/docsin the shell reachesopen_urlwithhttps://x.com/docs(the portal handled it on the pod); the sixautolink.test.tsfixtures run against the model[ ] Ship it, Enter,[x] Done thingin both apps persists identicalsession_documents.bodyJSON and identicalaction_itemsrows (ids masked: three rows in document order, the nested item's parent with an empty preview,created_byfrom the workspace binding); clicking the first checkbox in each app flips its attrs tostatus: done, checked: trueand the row todone; reopening the note in each app shows the same three items; the checkbox box, check glyph, 50% fade and strikethrough land on the same rows in the enlarged crops.hydrateTaskContentis deliberately not ported: the shipping editor evaluates it against an empty task snapshot at mount (a nested list reopens without an appended copy), and porting it literally appended a duplicateMet with @grin both apps opens the popup with the same tantivy hits (Grace Hopper, then the twoUntitlednotes whose contents match) once the shell's worker owns the index; typingQuokka sighting reportin the shell and then@quoklists that note after the projection drained (search_index_dirtyback to 0 pending); Enter thentodaypersists byte-identical JSON (mention-@with{ id, type, label }between two text nodes), and the chips render the avatar circle, the medium-weight label, and the trailing text on the same line; the picker'sfindMention/ candidate / node-shape rules are unit-testedPeople · 2 membersheading and member cards on the same rows (borders at y=200 / 253 / 314; card bottom at 490 after matching thesize-7icon buttons), and the person column's rows at 206 / 259 / 312 / 369 / 422 (the facehash's inline strut included); picking/tmp/avatar-src.pngthrough the shell's native dialog writesmetadata_json.avatarDataUrlas adata:image/jpeg;base64string that Tauri renders identically after restart (same centre crop),Remove photoleaves{}, and typing in the Name field updatesorganizations.name/updated_atwith the header and list row followingMOCK_AUDIO=1, the Tauri dev-feature build), both expanded panels lay the same bubbles out on the same rows (56px two-line and 36px one-line bubbles,gap-2, labels at −16..−6 above their bubble, the 6px scrollbar gutter once the list scrolls, the sentinel's 8px above the bottom padding) with the sametext-wrap: prettybreaks (The build passed overnight, so / we are on track for Thursday.); expanding in the shell writeslive_caption_minimized=false, resizes 111×67 → 368×459 keeping the top-right corner, collapsing writestrue, and a new session resets it totrue. The live-caption window is not ported:LiveCaptionWindowDisabledhides it unconditionally in the shipping appsize-7button row, the 19px squircle badges and theConnect Slackconfig row) and the same disabledSave draft/Save & enablewhile signed out; in the shell+prepends{"id":…,"title":"Untitled automation","enabled":false,"trigger":"note_enhanced","steps":[],"lastRun":null,"processedSessionIds":[],"chatGroupId":null}toautomation_workflows,Add step → Markdown exportappends{"id","type":"markdown_export","directory":""}, the native folder dialog writes the step'sdirectoryand dropsNeeds setup, and a restarted Tauri renders that workflow with the same card layout (trash button flush right,Add stepselect, footer hint)bg-accent/60, and clickingreleaseseeks to 0:06 and plays (the pod's PulseAudio null sink stands in for a device) with the same yellow current line following playback; the scroll pill lands on the same pixels (top 390, 34×67, interior#fcfcfc, squircle corners) once the 24px dev status bar of the Tauri debug build is accounted for; assigningAda LovelacewithApply to allwrites{"human_id","scope":"speaker","channel":0,"speaker_index":0}and typingZed Testerwith the checkbox off creates the human, links the participant and writes thescope: segmenthint with the segment's word ids — a restarted Tauri renders the resulting labels identically in both cases; edit mode shows the same circles, check pill, selected section ring and1 selected · Change speaker · Merge · ×bar, appendingEdited!to a segment and blurring storeshere. Edited!on its last word likeupdateTranscriptSegmentText, and Escape restores the text without a write. Playback follow and the CTA's 150ms transitions are instant in the shell (GPUI animates neither), and the editor's glyphs sit 1px higher than the contentEditable's. Draggingtart with … so we areacross two segments highlights the same range in both apps and copiestart with the release status and then go over open bugs.Speaker 1The build passed overnight, so we are(a DOM range'stoString()runs through the label); the menu is 147px wide with 27px rows in both; Cmd/Ctrl+Ftheon the transcript renders the bar on the same row below the player with the same toggles, count and carets (1/227 in the shell), and on the memoalpha beta alpha gamma→alphacounts 1/2, Cmd/Ctrl+HdeltaEnter yieldsdelta beta alpha gammaand 1/1systrayplugin added to the pod's XFCE panel, both apps show the same indicator whose menus listToday / Weekly sync · 10:00 AM / ☑ Show events in menu bar / Open Anarlog Dev / Start a new meeting / Settings / v0.0.0 (dev) / Hide / Quit Completely…(Tauri addsCheck for Updates); in the shellHideiconifies andOpenrestores the window,Settingsopens General,Start a new meetingopens a recording note (the Settings tab replaced likeopenNew), disablesStartand cycles the recording frames untilStoprestores the default icon, uncheckingShow eventsdrops the agenda and writes{"anlg-tray":"{\"show_events_in_menu_bar\":false}"}, andQuit Completely…promptsQuit Anarlog Dev Completely?and exits on confirm127.0.0.1:8765(seven models, 401 for any key buttest-key), both apps listGPT 5.6 / Alpha Chat / Zeta Chatwith3 items ignored.in the same 2:3 provider / model row, auto-selectcustom / gpt-5.6with the green health check and show theReasoning effortrow on the same pixels; when the server accepts any key both leave the selection empty (This endpoint doesn’t support API key verification…under the form on save), and with the server down the shell showsConnection failed: error sending request for url (…)and no check like Tauri's health error; after main's settings refresh both render the 12px provider marks, the Waveform / Brain nav icons,BYO-modeland theAfter recordingbadgesstore.jsonshows the onboarding with a PulseAudio sink input fromanarlog-gpuiplaying; the stream ends when the process exitsRelease Readiness Sync,Regenerateon the 1774-word session persists a byte-identicalsession_documents.bodyin both apps (the# Long transcript demotitle heading, two h1 sections, bullet lists), and the system prompts are identical while the user prompts match once the shell trims the trailing newlinetiptap_json_to_mdadds (ProseMirror's serializer ends without one); both render the persisted summary on the same rows (title 106, headings 153 / 236, bullets 181 / 207 / 264 / 290) and an empty summary as the mutedUntitledtitle; recording the 8-line mock-Deepgram script in each app and stopping producesauto_enhance_pending:<session>→ the streaming view withGenerating title...→ the generated titleRelease Readiness Syncon the session and as the summary's first heading, identical bodies across the two sessions, and no pending marker left (the shell starts the summary right after the final flush while Tauri finalizes for a few seconds first); the 4-wordSeek testsession showsSummary wasn't generated: Not enough words recorded (4/5 minimum); an external write no longer scrolls the shell's timeline back to the top. Editing the shell's summary appendsEdited.to the first bullet in the stored body after the 500ms flush, typing2at the end of the title heading writessessions.title = "Long transcript demo 2"and the sidebar row follows (two Backspaces restore it), Return at the title's start leaves an empty h1 above the old title and clears the session title exactly as the Tauri editor does (reproduced there with a stray Return), Cmd/Ctrl+Z restores both, and theUntitledplaceholder shows only while the caret sits in the empty title. The completion cue: cuelume's five recipes rendered through a headless-ChromeOfflineAudioContext(its ownrenderRecipegraph) and the shell's renderer agree on length and on the 50ms peak envelopes after the first window (e.g.ready0.118 / 0.120, 0.073 / 0.072, 0.019 / 0.019;chime0.165 / 0.171, 0.136 / 0.141, 0.047 / 0.049) — the first window differs only by Web Audio's fresh-compressor start-up dip, which a warmed context (as in the running app) does not have: Tauri'sPreviewrecorded off the PulseAudio monitor gives[0.101, 0.016, 0.121, 0.048, 0.019, 0.008]against the shell's[0.133, 0.026, 0.120, 0.072, 0.019, 0.011]; bothPreviewbuttons open a short playback stream on the null sinkRe-transcribein both apps showsGenerating transcript... / 12% complete / ☐ Stop transcriptionon the same rows (the Summary tab appearing next to Memos and the pill spinning), andStopin the shell lands onTranscription failed / Transcription stopped. / ↻ Re-transcribewhile the request is aborted (Tauri leaves the body blank, see deviations); the batch request now carrieslanguage=en-USlike Tauri's (Tauri's also carrieskeyterm=Release&keyterm=Readiness&…from its part-of-speech pass)isProforced on in both apps, since neither has a signed-in session here): the1xbutton sits in the same slot after the meta in both; picking2xin the shell advances the clock two seconds per second (00:02 / 00:04 / 00:06) and the 10s clip ends after fiveRe-transcribeon the 10s session through the fake batch endpoint logsvoiceprint_candidates_extracted … stored=2and adds twovoiceprint_candidatesrows; a live capture of the 8-line mock-Deepgram script stopped after 25s stores the same sixch0:s0/ch1:s0spans (quality 0.36 / 0.33) Tauri stored for its own capture of that script, and the attachment'stranscript_statusbecomescomplete; assigningGrace HopperwithApply to alllogsvoiceprint_candidates_promoted … promoted=1and leaves onevoiceprint_exemplarsrow (manual_speaker_assignment, confidence 1.0) whose vector sits in thevoiceprint_exemplarskeyring scopelisten_ws_connect_failedretries) andStoplogsstarting post-stop transcript repair … live_active=false needs_batch_repair=true→ the batch endpoint → a 24-wordbatch_transcriptionrow,voiceprint_candidates_extracted … stored=2,transcript_status: complete, the Summary document, andauto-enhance-skipped … Transcript too short; a normal live capture writes{"version":1,"phase":"capturing","sessionId":…,"transcriptId":…,"startedAt":…,"createdAt":…,"audioOffsetMs":0,"preserveExistingTranscript":false,"automatic":false,"preserveExistingAudio":true,"initialTitle":…,"ownerUserId":…,"memo":"","provider":"deepgram","model":"nova-3"}undercapture_lifecycle_pending:<session>and the row is gone oncenote.enhancedlands;kill -9ten seconds into a capture leaves that marker,audio.wav, a 9-word live transcript and four journaled deltas, and the relaunch logsrecovering capture … attempt=1→ the journal flush (44 words) →starting post-stop transcript repair→ the live transcript tombstoned under a 24-word batch transcript,voiceprint_candidates_extracted … stored=2,transcript_status: complete, no marker left; withaudio_retention = "none"the minute tick deleted the seven idle, processed recordings on the pod and marked their attachmentsabsent(files restored afterwards); a capture whose websocket drops after 20s of live text (the fake server closing it, then refusing upgrades) logsstarting live transcript summarybeforestarting post-stop transcript repairin the shell, writes the marker with"refreshSummaryAfterRepair":true, produces the summary from the live text while the 30s batch runs, regenerates aftercompleted post-stop transcript repair, and ends with one summary document and no marker — the same 950-byte summary body and title Tauri's dev build produced for the same script (the earlier race that left afinalizingmarker behind, and the duplicate summary row from the enhancer and the batch start racing their inserts, are fixed by sequencing the marker save before the clear and a per-session write lock)anarlog-gpui --identifier com.hyprnote.dev anarlog-dev://focusexits 0 afterforwarded launch to the running instanceand the shell's window becomes the active X11 window; on the welcome note,Join & recordopened Chrome atanarlog.so/onboarding-demo/?autojoin=1&completion_url=http%3A%2F%2F127.0.0.1%3A<port>%2Fonboarding-demo%2Fcomplete(the demo'sReady to join?page) while the capture started (sidebar collapsed likesetLeftSidebarExpanded(false), Tauri's meeting-detection toast fired on the shell'sanarlog-speaker-capturestream);curlon that completion URL returned the sharedDemo completepage pointing atanarlog-dev://focus, the capture stopped, and the header turned intoSharewith the Memos / transcript switcher over the saved audio; the STT provider select showsAnarlogalone in both apps on LinuxGlassDialogCancelButtonrenders a<button>withouttype="button", so inside the New folder dialog's<form>it is the first submit button and Enter in the name field activates Cancel (the dialog closes,fs_sync_corenever logsCreated folder; theCreatebutton works) — the shell's Enter creates the folder, as the form intends; a one-line Tauri fix for a separate PRproseMirrorJsonToTextjoins the composer's paragraphs with nothing between them, sofirst lineShift+Entersecond lineis sent and stored asfirst linesecond line; the shell reproduces ituseAudioExistsstayedfalsefor the recorded sessions although fs-sync'saudio_existfindsaudio.mp3; the shell followsgetCanShowTranscript/meetingOveras coded, so verify the post-capture header on macOSWhat are my action items?on the 1774-word session in both apps sends an identical system prompt (5349 chars:chat.system+ the tool guidance) and an identical last user message (<context>block with the title, date, enhanced summary and full transcript, then the question — byte-equal once the shell'sbody_to_markdownemitsjson2md's loose lists), and a second turn (Anything else?) sends the same four messages in both (system, user 25, assistant 150, user 1869 with the context only on the last); both requests carry the same fifteentoolsandtool_choice: auto. Both persist the same rows:chat_groups(titlefrom the fallback thenMeeting Action Itemsafter the title generation,created_at= the first message's), the user row ({"chatScope":"general","createdAt":…,"contextRefs":[{"kind":"session","key":"session:auto:<id>","source":"auto-current","sessionId":…}]},[{"type":"text","text":…}]) and the assistant row ({"createdAt":…},[{"type":"step-start"},{"type":"text","text":…,"state":"done"}],status: ready); the shell'sRECENT CHATSlists Tauri's group and opens its messages. With the mock emitting alist_meetingscall forPlease list meetings, both apps send the same follow-up (the assistanttool_callsentry with{"limit":3}as JSON text, then thetoolmessage with the 807-character output) and persist byte-identical assistant parts (step-start,tool-list_meetingswithtoolCallId/state: output-available/input/output,step-start,text); theSearched for: meetingscard, itsQuery: none/Limit: 3lines and theWeekly sync · 9/7/2026, 10:00:00 AM · No excerpt availableresult card render on the same rows.search_meeting_content(64 sessions scanned, the same scores and snippets),find_related_meetings,search_contacts,search_calendar_eventsandweb_searchreturn byte-identical outputs from both apps;search_meetingsreturns the five hits from the shell while Tauri returned none — its tantivy plugin cannot open the index the shell's worker holds on this pod, the deviation already noted for mentions. Screenshots of the panel, the suggestions, the bubbles and the history dropdown line up on the same rows (the Tauri debug build's 24px dev status bar aside);Open in right panelin both apps docks the same toolbar / messages / composer column at the right 30% of the surface, and the Automations tab in both shows the composer-only automations chat whose sends persist{"chatScope":"automations","createdAt":…}with no context refs (the reply'sContext chars: 17is the bare question) and list the new group underMy automations. The shell's tests add the guidance / window / title / row-shape / context-ref fixtures, the multi-turn and tool-call request bodies per provider family, the streamed tool-call assembly, the tool registry order and schemas,queryTerms/searchNotescoring / snippet cuts / the created-at filters, the tool part key order and model-turn conversion, and the card titles / result parsingedit_memo/edit_summarycalls, both apps write the samesession_proposalsrow shape (memo_replacetargeting the memo document,current_markdownwithjson2md's loose list blank lines,base_updated_at= the document'supdated_at,pending/chat) and, onApply to memo, a byte-identicalsession_documents.body(md2jsonof the proposal) with the rowapplied; the edit tab renders on the same rows as@pierre/diffs(header border, the 44px file header, 20px lines from y=91 in the shared crop, the dotted / solid 4px bars, number column,# Agendain#d52c36, theOld item/Itememphasis boxes at x=66–127 / 66–96, theNo newline at end of filerows) with the measured#ffeae6/#ffefec/#eaf9f5/#effbf8fills and the-3 +1counts at the right padding, and the summary proposal lands on jsdiff's alignment (delete 1 / insert 1, context 2, delete 3 / insert 3–9) with theZed Corp shipped/buildandThe build passed overnight, so/team is on track for the Thursday releasespans Tauri draws; the tool card's header, preview box, heading / bullet rows, footer and buttons sit on the same rows relative to the card, the settings-tab send returns the sameNo active session selected…error card (111 output chars in both), a stale target shows the red error row,Declinemarks the rowdeclinedand the cardMemo edit declined, and the banner lists an orphaned pending proposal and opens it.Cmd/Ctrl+Jnow works after the composer or another field unmounts (the workspace takes focus back), andtext-red-500/600/200/amber-500/emerald-500use the Tailwind v4 values the desktop renders (#fb2c36/#e7000b/#ffc9c9/#fd9a00/#00bc7d)MOCK_AUDIO=1and the fake Deepgram batch endpoint, the microphone button in both apps turns the composer into the editor-over-VoiceStatuscolumn on the same rows (surface 74px:py-2, the 20px line,mt-2+min-h-7; bars on a 4px pitch,0:0N, the 28px stop button),Stopinserts the identicalGood morning everyone thanks for joining the weekly sync today and welcome. Morning happy to be here let's start with the release status now.into both drafts with the same wrap, and clicking the microphone during a capture shows the same amber warning toast in the shell. A two-line draft used to leave a ~90px gap under the shell's composer (the percent-wide editor was measured at a narrow width first, and GPUI's text layout keeps that height); the editor now takes an explicit width from the panel and the panel grows by one line like the web view'sCtrl+Vofhttps://www.youtube.com/watch?v=dQw4w9WgXcQat the start ofabc, at its end, and into the empty paragraph after it persists the byte-identical[clip, paragraph abc, clip, clip]body in both apps, and both render the clips as the 4px empty blocks they are; a red 320×200 PNG on the clipboard pasted into the shell writesattachments/image.png, thesession_attachmentsrow (image.png,attachments/image.png,image/png, 756 bytes, the file's sha256,local_file,note_upload,source_id = image.png,{}), thepresentlocal-state row and no transfer job (cloud_sync_enabled = 0), and inserts{"type":"image","attrs":{"src":"asset://localhost/%2F…%2Fattachments%2Fimage.png","alt":null,"title":null,"attachmentId":"image.png","sharedAttachmentId":null,"editorWidth":80}}plus the trailing paragraph — a restarted Tauri renders that node from its ownattachmentListat the same 694×434 box (x 221..914) with the same rows below it (WebKitGTK itself pastes clipboard images as ablob:<img>with no attachment on this pod, so the macOS flow is the reference); dragging the shell's right pill 102px writeseditorWidth: 68, and both apps then draw the image 590 / 589px wide with the hover ring and pills on the same pixels; a seededrelease-notes.txtfileAttachmentrenders the card at 536..597 with the 40px icon box at 234..274, thefile-textglyph at 558..575 and the 14px / 12px text rows in both apps (the card colours#e7e5e4/#f5f5f4/#f2f2f1match); selectingabcshows the toolbar at the same 36..223 × 101..138 box below the selection in both (flipped off the header),Bwrites theboldmark and paints the active button identically, the highlighter writeshighlightand both draw the#fef08abox over rows 7..28 of the cropLong transcript demomemo and the mock server'sgpt-5.6(an image-capable id),Regeneratein both apps sends a byte-identical 21,217-byte request — the same system prompt, the user text ending in theIMAGE_CONTEXT_NOTEand the length guidance, and oneimage_urldata-URL part — once the shared tiptap converter serialises a block image likejson2md(its own paragraph,char-editor-width=80in the title) andbody_to_markdownkeeps prosemirror-markdown's newlines for the trailing empty paragraph;cargo test --locked -p tiptap(66),-p agent-access,-p fs-sync-core(98) and-p anarlog-cli(40) pass with the converter change{"facts":[…]}, opening a contact in either app sends the samegenerateTextbody (model,max_tokens: 4096,response_format: {"type":"json_object"}, the 1,185-char system prompt, the{"target":…,"meetings":[…]}user message — byte-identical apart from thetargetfields of the two contacts sharing the session) and stores the samecontactSummaryrecord shape; the two apps compute the samesourceHashfor one (314e44ea), two (b9aac091) and eight (f03eb96b) sessions, so a brief written by one is left alone by the other, and adding a participant row makes both send the incremental request (existing_factsplus the one new meeting) and store the two-source record; a 500 from the mock makes both run six attempts (maxRetries: 2×retry: 1) beforeSummary generation failed,Try againre-runs and both land on the same new hash (7199e8df); the facts list (card 690→789, bullets at 716 / 738 / 760, marker at x=249), the skeleton (card 690→767, bars 711→746) and the failure card / footer (690→751; divider 785, footer text 807→819) measure identically — the app's* { margin: 0 }reset defeats Tailwind'sspace-y-*, so both lists render gapless; the shell's tests add the hash, record parsing / serialisation, incremental-update, source cleaning / bounding (the frontend's 6003 / 5981 lengths), prompt JSON, fact normalisation, schema, per-provider structured-output bodies (the captured@ai-sdkshapes), whole-reply parsing and OpenAPI-schema conversion fixturesTell me about @in both composers lists the same five rows (Stats demo one / Stats demo two / Welcome to Anarlog / Weekly sync / Seek test, 32px pitch, flipped above the trigger) and Enter inserts the same chip; sendingTell me about @Stats demo one pleasestores the same text and the samecontextRefs(session:auto:<open note>,session:manual:stats-demo-1) in both apps and sends a byte-identical 15,804-char user message (one<context>block: the open note,---, the mentioned note's title and date); draggingSeek testfrom the timeline onto either chat panel and sendinghistores identical refs and sends byte-identical 15,916-char prompts; dropping it onto an empty memo persists the identical[mention-@ {id, type: session, label: "Seek test"}, " "]document in both; Up in the emptied composer recalls the draft with its chip underHistory 1/1(Tauri1/2with an earlier send) and eight Backspaces from the end removepleaseand the whole chip in both. On the pod, Tauri's@Grashows nothing (its tantivy plugin cannot open the index the shell's worker holds — the known deviation), so the empty-query listing was compared; the shell's tests add the ref kinds' round trip and dedupe, the transcript omission, and the chip text / caret / splice fixtures37.2349570201%read from localStorage by the shell, the shell's30.0715239342%written back in the library's shape with the{}entry kept); 900px automations: body 800 / chat 320 clipped at the window edge withAsk anythincut on the same column; the width guard on a note tab in both apps: 800px + sidebar → dock → sidebar collapses and the window grows to 824, close → 800; sidebar collapsed at 800 → dock → 820, reopen the sidebar → 1024 (expandLeft, no restore), close → stays 1024. WebKit floors fractional line heights (22.75 → 22, 19.5 → 19, 19.25 → 19, measured), so the shell'sleading-relaxed/leading-snugcopy now lands on the same rows; the templates form's grip renders on the same pixels and a 60px drag grows the field by 60px; the form body now reserves the 6px gutter Tauri'soverflow-y-autoscroller does (the section fields end at x=834 in both, the thumb on the same column), the folder / template / section actions and the three sort menus measure Tauri's 128px,Add Sectionis the same 142px pill, and the Auto editor box ends within 28px of Tauri's (the paragraph margins above); the New folder dialog's focused field shows the same#1c19171px ring andshadow-xs(pixel-compared), the focused template textarea the same 2pxprimary/20ring, and the hovered note image the same 2px card + 1px border ring at x=218..220auto_start_scheduled_meetingson, an event with a meeting link seeded to start in a minute, each app running alone): both apps start at the event's second (22:23:21/22:28:41), create the session from the event, open its note with the sidebar collapsed andStopin the header, and write the same marker ("audioOffsetMs":0,"preserveExistingTranscript":false,"automatic":true,"preserveExistingAudio":false,"initialTitle":"<event title>"); with the scripted mock audio the stop leaves the sameaudio.mp3attachment,presentstate, transcript andnote/summarydocuments in both; with silent mock audio (ANLG_MOCK_MIC_AUDIO/_SPK_AUDIO) and the fake websocket connected but quiet, both discard the capture on stop — no attachment or local-state rows, no transcript, the audio gone, the calendar note kept with itsnotedocument and the marker cleared (discarded empty automatic capturein the shell's log); a relaunch inside the grace window re-fires a still-due meeting in both (firedEventIdsis per run). The Tauri debug binary had to be rebuilt first: the one on the pod predatedaudio_has_speech, so itsdiscardEmptyAutomaticCapturekept the audio afterCommand not found[18.2481751825, 81.7518248175], 200px of the 1100px window it was saved in) both apps put the sidebar / main boundary on the same pixel at 1100 (208), 1400 (262: the inspector's 253.890625px panel = 1392px of free space × 18.248175 / (18.248175 + 81.8)), 1250 (235) and 800 (208, the 200px minimum); dragging the shell's handle 60px writes[23.7227112707,76.2772887293]intohttp_localhost_1422.localstorageand{"gpui":{"left_sidebar_fraction":0.2372…}}intostore.json, and a relaunched Tauri opens withflex: 23.722712and a 259px panel; dragging Tauri's handle back to 200px saves[18.248258716, …]and a relaunched shell opens at 208 again, the shared value winning over its own copycargo test --locked -p local-api-core(10: the dispatcher's signature / subscription / endpoint / delivery tests moved with the code plus the sorted-JSON layout),cargo test --locked -p tauri-plugin-local-api(5, bindings unchanged),cargo clippy --locked -p local-api-core -p tauri-plugin-local-api --all-targets --no-deps -- -D warnings,cargo check --locked -p desktopmarkdown_exportworkflow to another, each app run alone with the records reset in between):Regenerateon the 1774-word note firesnote.enhancedin both — one signed delivery whose body is byte-identical apart from the event id,created_atand the document'supdated_at(samecreated_at, data, event, idorder once the core sorts its keys), the starter's and the workflow's exports byte-identical (11,179 bytes), the workflow'slastRun/processedSessionIdsand the starter's{"at","detail","status"}record in the same shape; a 20s mock capture stopped in each app firesmeeting.completedat the stop (empty title, the transcript text) andnote.enhancedafter the summary (Release Readiness Sync), themeeting_completedworkflow exports2026-09-07 Untitled meeting [<id8>].mdand the starter'sUntitled meetingfile is replaced byRelease Readiness Sync [<id8>].mdin both, with the two webhook bodies and both exports identical modulo ids and times. The first shell capture kept 103 of Tauri's 124 words — the engine's finalize-window deltas were dropped once the queue moved aside atFinalizing; the queue now stays open untilInactiveand the shell stores the same 124 wordscapture_lifecycle_pending:%inserts, stopping a capture in the shell logsfailed to persist summary recovery state … marker save refused, shows the redThe transcript was saved, but Anarlog could not start the summary. Try generating it again.toast, starts no summary and logsrecovering capture … attempt=1,attempt=2quietly until the trigger is dropped, after which the marker clears; the normal stop still producesmeeting.completed, the summary title and a cleared markerOnboardingNeeded2set, both apps at 1100×1000): theWelcome to Anarlogtitle,Create accountheading, description andGet startedbutton occupy the same text bands (y 67–103, 155–170, 192–206, 232–259) with the heading at the same stroke weight (834 vs 858 dark pixels in the crop; it was 477 before the root carried the font), Tauri's extra‹being itsimport.meta.env.DEVback button on the first step; the shell's backdrop changes between frames 0.7s apart (mean frame diff 0.46–0.53) while the Tauri window here shows no video at all (WebKitGTK on the pod cannot decode the H.264 asset — frame diff 0.0, a flat#fefefebottom band), so the macOS build is the reference for the moving backdropWhere your notes and recordings are storedlabel stopped wrapping (Taffy sized itsmin-w-0column at min-content); the Calendar and Folders tabs match (the Tauri window's 12px vertical offset in the empty folders state is its 24px dev status bar); the person view's bands match with theRelated Notessearch box on the same pixels (662–870, borders 663 / 868), typingzzzgives the same focused fill#f0f0ef, the same text / clear-button extents andNo results found.on the same row, and the detail column's dividers and dates end where Tauri's do (x=893 / 860) with the gutter reserved; Escape on the Contacts tab opened from the settings nav returns to Settings and a second Escape to the home surface in both appsRelated Notessort menu opens above its trigger in both when the header sits near the window's bottom (Tauri's menu at y=777 over a button at 863, 128×82, items 32px with the 8px dot at x=345 — the shell flips to the same rows with the dot on the left), and the Find a note dialog takes keystrokes again after the focus recovery moved off the render pass (it had unfocused the input mounted in the same frame)Re-transcribethrough the 30s-delayed fake batch endpoint with the Tauri window active shows the shared GTKTranscription complete / Your transcript is ready. / Open Anarlogcard at the screen's top-right, andOpen Anarlogmakes the shell the active window on that session; a record-only capture stopped with the fake server down raisesAnarlog could not finish saving the transcript. The recording was kept so you can try again.once, the retries stay quiet, and the marker resolves (recovering capture→completed post-stop transcript repair) when the server returnsgpui_linux_cicovers Linux, and the macOScargo checkstayscontinue-on-erroruntil the Metal build is validated onmacos-26runners