Land the opt-in GPUI desktop shell and shell-neutral crates in one change - #7655
devin-ai-integration[bot] wants to merge 378 commits into
Conversation
The reply stream runs up to five steps: tool calls execute locally, their parts (input-available → output-available / output-error) join the assistant message in the AI SDK shape, and the results feed the next generation with search_meetings outputs hydrated like expandSearchMeetingsOutput. Tool parts render as the Disclosure cards: the search card with its result carousel and the generic card. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
…iption reply The chat follows the FloatingClosed / FloatingOpen / RightPanelOpen mode machine: Open in right panel docks the chat beside the note surface with the right-panel toolbar (history, new, float, close) and the elevated composer; the automations tab shows its own automations-scoped chat (no note context, chatScope: automations) with each selection keeping its chat thread like switchTo; a send while the open note is still batch transcribing gets the local canned reply persisted like Tauri's. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Port moveSessionContents: the busy / same-meeting / empty-source / occupied- target checks with the frontend's messages, the audio copy and catalogue, the transcript / summary / action-item / voiceprint re-parenting and the note merge in one transaction, the copied-audio rollback, and the moved / nothing_to_move / error result shapes. The tool runner now holds the store so tools can use its helpers. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Port session-correction.ts: the exact / bounded / loose replacements over the summaries' markdown, the transcript words (single-token bounded replacement or the comparable phrase split across the span with :correction:n ids) and memos, and the title; the guarded transaction of applySessionContentCorrections; the dictionary merge into personalization_dictionary_terms; and the applied / partial / not_found / error results. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Ports the edit tools and their review surface from the Tauri app: - session_proposals persistence (persist, load, apply with the staleness check, decline) in db/proposals.rs - the edit tab (TabContentEdit) as an overlay page: header with Decline / Apply, the @pierre/diffs unified diff (file header with the modified icon and counts, 20px mono rows, change bars, word emphasis, pierre-light/dark markdown token colours, no-newline notes) - the ToolCard for edit_memo / edit_summary in the chat (spinner, pencil, Streamdown-styled MarkdownPreview, error footer, Decline / Apply while the tool waits) - the pending-proposals banner above the note body, opening reviews for proposals created by the CLI / MCP or an earlier session - the workspace takes focus back when the focused field unmounts so the document-level shortcuts keep working; tool session ids follow the active sessions tab like useSessionTab Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
The desktop app renders Tailwind v4 colours (red-600 is #e7000b, red-500 #fb2c36, red-200 #ffc9c9, amber-500 #fd9a00); the tool card, edit review, chat error bubble, delete menu items, and live-capture indicators used the v3 hexes. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
@pierre/diffs builds its hunks with jsdiff (structuredPatch, four lines of context, deletions preferred on ties) and emphasises changed words with diffWordsWithSpace in word-alt mode, folding the blank between two changed words into one span. Port that Myers variant instead of similar so the review lines up with the Tauri app, keep lines unwrapped with sideways scrolling like overflow: scroll, and drop the border under the error row. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Mirrors main's capture lifecycle change (#7437): a fresh stop that saved live text requests the summary right away, records refreshSummaryAfterRepair in the recovery marker, and the batch repair regenerates the summary when it lands (also after a recovered marker carrying the flag). The memo editor's pending body is flushed before the snapshot like flushCanonicalSessionEditorChanges. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
finish_capture spawned the finalizing marker save and the post-audio clear independently, so the clear could run first and leave a finalizing marker behind. The clear now waits on the save. Summary document creation from the enhancer and the batch start also serialise per session like enqueueDatabaseWrite, so a live-text summary starting alongside a repair no longer inserts a duplicate summary row. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Ports useDictation and the plugin's Recorder: the microphone button records a temporary 16 kHz WAV (five-minute cap) through the app's audio provider, the batch transcriber turns it into words with numSpeakers 1 and no keywords, and the text lands in the draft with insertText's blank padding. The VoiceStatus row (waveform bars, m:ss, stop, send / stop response), the Starting… / Transcribing… spinners, the meeting-recording warning, the no-speech warning and the start / transcribe error toasts with their descriptions follow the web view; the recording is cancelled when the panel closes or the scope swaps. The composer editor now takes its width from the panel: a percent-wide text child is measured before its parent's width resolves and the narrow, tall measurement stuck, leaving a gap under multi-line drafts. Escape closing the floating chat hands focus back to the workspace. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Ports clipPastePlugin: an embed snippet, a watch / short / embed / youtu.be link or a clip link (resolved through the clip page) pasted into the memo or summary editor inserts the clip block atom with the embed URL, and the paste is consumed like the plugin's handlePaste. The model gains replaceSelectionWith for block atoms: a selection goes first, an empty textblock is replaced, an edge caret follows insertPoint up to the nearest ancestor that takes a block, and the middle splits the textblock around the node with the same type and attrs. The node renders as nothing, as it does in the desktop app. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Renders image nodes as ResizableImageView does: the attachment id resolves
to <session>/attachments/<id> through a new fs-sync-core attachments module
the Tauri plugin now delegates to, the image takes editorWidth% of the
editor with rounded-md, the hover ring and the two resize pills whose drag
writes editorWidth back like handleResizeStart; the inline-block frame's
strut gap and the block padding follow the measured DOM. fileAttachment
nodes render FileAttachmentView's card with the mime icon or thumbnail,
name, size, and the hover open / remove actions. Clip embeds keep their
block padding.
Pasting an image from the clipboard or dropping files on the editor runs
useFileUpload: the bytes are saved under attachments, catalogued with
catalogLocalNoteAttachment's statements (session_attachments,
attachment_local_state, the transfer jobs) under the session lock, rolled
back on failure, and inserted as the image / fileAttachment node with the
web view's attr order and convertFileSrc URL; a dropped audio file runs the
audio import instead. imageTrailingParagraph keeps a paragraph after every
top-level image, and the editor fills the viewport with a flex tail like
.prosemirror-editor { min-height: 100% } instead of a fixed block.
Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Ports FormatToolbar: a non-empty selection in the memo or summary editor (outside the enforced title heading) floats the bold / italic / underline / strikethrough / code / highlight buttons over it — floating-ui's top placement with the 8px offset, flipped below the selection and shifted inside the note's scroll container, the ring-1 outside the box, active buttons on bg-primary. The highlight mark parses and paints like .note-typography mark: yellow-200 over the inline box with the 2px radius (dark text in dark mode), which ProseText now paints as an inset background instead of a full-line run background. Shift+Home / Shift+End extend the selection to the line edge like the web view. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
main's #7444 added automatic, preserveExistingAudio and initialTitle to CaptureLifecycleMarker. The shell writes them for its manual captures (false / true / the session title at start), parses them from the web view's markers and keeps them through recovery, so both shells read each other's markers unchanged. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Shows the desktop's notification cards through the shared notification crate: showBatchCompletedNotification after a re-transcribe / import batch and after a post-stop repair that ran without live text (notifyOnCompletion), showSummaryReadyNotification after a summary lands, both only while the window is inactive and the notification_transcription_complete / notification_summary_complete settings allow it, with Open Anarlog bringing the window back on the session like openNew. The tray and the GTK notification windows now share one GTK main-loop thread. A failed post-stop repair on the stop that started it raises notifyFailure's toast (the transcript-save wording without live text, the batch wording with it) unless the note was deleted; the recovery retries stay quiet. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
Ports collectEnhanceImageContext: when the selected model takes images (modelSupportsImageInput's rules, the Cloudflare vision list), the memo's image and image-attachment nodes and the transcript memos' markdown images are read from the session's attachments (by id, then by file name) or their data URLs, capped at 8 MB each, squeezed under 128 KB apiece and 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 (OpenAI image_url data URL, Anthropic base64 source, Gemini inlineData) with the IMAGE_CONTEXT_NOTE ahead of the length guidance. The shared tiptap converter now serialises a block-level image like json2md: its own paragraph, with editorWidth in the title as char-editor-width=NN, and the shell's body_to_markdown keeps prosemirror-markdown's newlines for empty paragraphs (the one imageTrailingParagraph leaves after an image), so the enhance request for a note with a pasted image is byte-identical to the web view's. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
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>
…p-fixtures' into feat/1789571352-gpui-migration
…s' into feat/1789571352-gpui-migration
…-core' into feat/1789571352-gpui-migration
…-core' into feat/1789571352-gpui-migration Co-Authored-By: John <john@fastrepl.com> # Conflicts: # .github/workflows/desktop_cd.yaml # apps/desktop-gpui/src/main.rs
…untime' into feat/1789571352-gpui-migration Co-Authored-By: John <john@fastrepl.com> # Conflicts: # Cargo.lock # Cargo.toml
…into feat/1789571352-gpui-migration Co-Authored-By: John <john@fastrepl.com> # Conflicts: # Cargo.lock
…ter-core' into feat/1789571352-gpui-migration Co-Authored-By: John <john@fastrepl.com> # Conflicts: # Cargo.lock # Cargo.toml # apps/desktop-gpui/Cargo.toml # apps/desktop-gpui/src/main.rs
Co-Authored-By: John <john@fastrepl.com>
Co-Authored-By: John <john@fastrepl.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…us portal setup has a reactor Co-Authored-By: John <john@fastrepl.com>
…rate; keep zbus on async-io for GPUI portal threads Co-Authored-By: John <john@fastrepl.com>
|
Linux runtime QA at Results
Caveats found during setup
|
|
Recorded exhaustive Linux GPUI click-through at What was exercisedRendering: nested Sway (pixman) + lavapipe on the Linux VM; plain X11 / Xvfb presentation stayed stale (VM graphics limitation, not app code).
Issues found (no data loss established)
Untested: authenticated cloud/provider flows, OAuth imports, real audio capture/transcription, billing/Teams, CLI/MCP/webhooks. The vendored Linux |
Co-Authored-By: John <john@fastrepl.com>
Summary
Problem: The Tauri → GPUI migration (ANLG-320) was spread across a twelve-PR stack (#7356, #7358, #7359, #7536–#7545) that had drifted from
main, carried a staleCargo.lock, and had to be reviewed and landed strictly in order. Each PR only made sense with the others applied, and their catalogs/lockfiles conflicted with recentmainwork (calendar, billing, transcript deletion).Fix: One branch rebuilt on current
mainthat lands the whole migration as a coherent, opt-in change. Tauri stays the default shell and rollback path; nothing here changes the SQLite schema or the ProseMirror document format.apps/desktop-gpui: the native GPUI shell (main window, note view + ProseMirror editor, transcript, recording, settings, onboarding, tray, deep links, single-instance forwarding). Launched viaSettings → General → Try the new Anarlog (beta); the Tauri launcher execsanarlog-gpuifrom next to its own binary (apps/desktop/src-tauri/src/shell.rs),ANARLOG_SHELL=tauri|gpuioverrides for one run.crash-reporting(consent, Sentry init, redaction),desktop-auth(account parsing/persistence/secret store),desktop-db-runtime(DB open + CloudSync runtime/config, moved out ofplugins/db),desktop-updater(update policy + backend/event abstraction; GPUI backend checks/downloads but does not install/relaunch yet),desktop-bench(process-tree benchmark harness), plusdeeplink-core/tray-coresharing routes, icons, and agenda logic.packages/editor/src/note/roundtrip-fixtures.gen.test.ts) consumed byapps/desktop-gpui/src/editor/pm/roundtrip.rs, with UTF-16 position semantics so selections match the web editor.gixauthor API fix (Fix tauri-plugin-git build after the gix 0.77 bump #7540) needed by the bench harness.gpui_linux_cijob (clippy-D warnings+ tests),desktop-benchchecks on Linux, macOScargo check -p desktop-gpuiascontinue-on-erroruntil Metal shaders are validated, Linux CD ships theanarlog-gpuisidecar (macOS optional).pr-description.ymlreads the event file instead ofenvso large PR bodies don't fail the step.apps/desktop-gpui/docs/tauri-inventory.md.Consolidation-only changes vs. the stack: conflicts resolved additively (startup in
desktop-gpui/src/main.rskeeps crash reporting + auth + CloudSync + updater;Cargo.tomlkeeps all extracted crates;desktop_cd.yamlkeeps Sentry and Supabase env),Cargo.lockregenerated, and the desktop i18n catalogs regenerated on currentmain(the stack's catalogs were extracted with a Node < 22.18 wherelingui extractis a silent no-op, so they had drifted; the diff vs.mainis now only the three shell-switch strings).Not in scope / known gaps (unchanged from the stack): GPUI updater install/relaunch, update state in GPUI UI, activity deferral (no GPUI recording pipeline yet), signed macOS/Windows packaging and hardware validation, Windows single-instance forwarding, rich clipboard embeds.
Supersedes #7356, #7358, #7359, #7536, #7537, #7538, #7539, #7540, #7541, #7542, #7543, #7545.
Verification
pnpm install --frozen-lockfile,pnpm -F @anlg/ui build,pnpm -F @anlg/desktop typecheck,pnpm -F @anlg/desktop test,pnpm -F @anlg/editor test,pnpm exec oxlint --quiet --format=github apps/desktop/src/(0 errors)lingui extract --clean --workers 1+lingui compile --strict --workers 1(Node 24; stable, committed)cargo clippy --locked -p desktop-gpui --all-targets --no-deps -- -D warnings,cargo test --locked -p desktop-gpui(490 passed)cargo test --locked -p crash-reporting -p desktop-auth -p desktop-updater -p desktop-bench(all pass)cargo test --locked -p desktop-db-runtime --lib: 66 pass; 12 tests that open the CloudSync extension hang/PoolTimedOuton this VM.cargo test -p db-core 'cloudsync::'on unchangedmaincode hangs identically here, so this is the sandbox (no network for the extension), not the port — relying on CI for that gate.dprint check+rustfmt --checkon all changed files;ci.yamllicense-boundary scripts andnode --test(67 pass);zizmor --offlineshows no findings on the changed workflow lines.Link to Devin session: https://app.devin.ai/sessions/42ab0e9111f44a4c9e79fa361842f703
Open in Devin Desktop: https://app.devin.ai/desktop/session/42ab0e9111f44a4c9e79fa361842f703?variant=devin
Requested by: @ComputelessComputer
Summary by cubic
Lands the ANLG-320 Tauri → GPUI migration as one opt-in change. Tauri remains the default and rollback path; Settings → General → Try the new Anarlog (beta) launches
anarlog-gpui, withANARLOG_SHELLavailable per run, without changing the SQLite schema or ProseMirror document format.GPUI shell
apps/desktop-gpuiwith the main window, notes and ProseMirror editor, transcript, recording, settings, onboarding, tray, deep links, single-instance forwarding, and shared window state (persisted alongside Tauri's, close hides to the tray).CI and release
anarlog-gpuisidecar while macOS remains optional.Cargo.lockand desktop i18n catalogs against currentmain.Written for commit ccb4743. Summary will update on new commits.