Skip to content

Add a native GPUI desktop shell that reads the shared app.db - #7356

Draft
ComputelessComputer wants to merge 171 commits into
mainfrom
cursor/gpui-desktop-foundation-1ade
Draft

Add a native GPUI desktop shell that reads the shared app.db#7356
ComputelessComputer wants to merge 171 commits into
mainfrom
cursor/gpui-desktop-foundation-1ade

Conversation

@ComputelessComputer

@ComputelessComputer ComputelessComputer commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Problem: ANLG-320 calls for a standalone apps/desktop-gpui application alongside apps/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's app.db / store.json with 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: a desktop-shell marker file next to app.db holds tauri (default) or gpui; ANARLOG_SHELL=tauri|gpui overrides it from a terminal.
  • Tauri (apps/desktop/src-tauri/src/shell.rs): before the launch lock or any webview, if the marker says gpui and anarlog-gpui sits 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.
  • Tauri General settings: Try the new Anarlog (beta) row with a Switch button (is_native_shell_available / switch_to_native_shell commands, tauri.gen.ts regenerated, lingui catalogs extracted and compiled). The row renders only when the sidecar is installed, so dev builds and platforms without the sidecar are unchanged.
  • GPUI General settings: Use the classic app → Switch back rewrites the marker to tauri, launches the Tauri binary (anarlog / anarlog-dev / anarlog-staging by identifier) and quits.
  • Packaging: cargo xtask prepare-binaries builds desktop-gpui when ANARLOG_GPUI_SIDECAR=1, and desktop_cd.yaml appends binaries/anarlog-gpui to externalBin on the Linux x64/arm64 lanes, the platform whose GPUI build is a required check. macOS and Windows sidecars follow once their desktop_ci.yaml checks stop being continue-on-error.
  • Both shells already share the database, store.json, settings and keychain entries, so switching keeps every note, transcript and setting.
  • Verified on the pod with both debug binaries side by side (target/debug/anarlog-devdesktop): marker gpui makes the Tauri binary exit 0 at once with anarlog-gpui --identifier com.hyprnote.dev running; GPUI's Switch back flips the marker to tauri, quits, and leaves anarlog-dev running 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):

  • Window chrome and multi-window (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 shared theme setting, 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 like react-resizable-panels' panel — a persisted share of the panel group split the way its two flex items split the free space (the sidebar's exact flex-grow, the content's toPrecision(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'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 store.json scope, so either shell opens at the width the other left.
  • Sidebar timeline (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, scrollTimelineItemIntoView for a newly opened note's row, Now / Open calendar chips, Grouping and Ordering submenus, ignored-event exclusion, store.json recents 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, otherwise Show Deleted Events / Hide Deleted Events, which lists ignored events at opacity-40 with a struck-through, inert title), and bulk delete: Backspace / Delete opens Delete N selected notes? and confirming soft-deletes the batch under one N notes deleted toast whose Undo restores the group and reopens the first note (useRestoreGroup).
  • Settings (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 cataloguessettings/ai/shared/list-*.ts ported per provider with list-common.ts's ignore rules, recency order and removeNonStreamingModels, cached per provider config like the ["models", …] query, shown in the ModelCombobox with displayLlmModelId names, Search or create new, the freeform Select "…" row, ignored models behind the eye toggle with Deprecated badges, refresh; getDefaultLlmSelection / handleProviderChange resolving the provider's preferred model and resetting the effort; the Reasoning effort row per supportsReasoningEffort; key verificationpackages/provider-validation's verifyProviderCredentials with its per-provider probe table, control request and one-minute memory gates configured like useProviderAvailability and 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 healthuseConnectionHealth's Hi generation against the selected model (OpenAI-compatible, Anthropic, Google, Azure shapes, five retries) drives the green check, the spinner and the Connection failed: … alert), Privacy, Permissions (real probe_mic / probe_speaker), Account, Developers (CLI, MCP config, agent skills, webhooks), Dictionary (PlanGate), Stats, Teams, Sync, Imports.
  • Folders, Templates, Calendar, Contacts, Automations tabs (folders/*, templates/*, calendar/*, contacts/*, automations/*, settings/automations/*): editors, dialogs, vault mirroring through anlg-fs-sync-core, template form / Auto format (the section textareas' resize-y grip drawn pixel for pixel, dragging it sets the field's height above min-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, advanceCompact arrows, Today re-anchoring), contacts with the bit-identical facehash avatar raster, uploaded photos (metadata_json.avatarDataUrl shown in rows / headers / cards; the AvatarUploadButton opens the native picker and stores a centre-cropped 70px JPEG q0.85 data URL like compressAvatarImage; 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, the Related Notes header's Search... field (sortAndFilterRelatedNotes: a trimmed lower-case title filter, the list by Date.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 stored metadata_json.contactSummary was built from a different set of meetings (createContactSummarySourceHash — FNV-1a over the newest eight sessions' ids, created_at and the MAX(session, participant mapping, note / summary / template_output documents, transcripts) updated_at stamp — as the query key) the brief is generated through the enhance model with the ported system prompt, the target / existing_facts / meetings JSON (getIncrementalUpdate reads only the meetings the saved sources have not seen and passes the saved facts; an edited or removed source forces the full rebuild; getMeetingSource's Summary: / Notes: / Transcript: text through extractPlainText and cleanSourceText, truncateAtWord at 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's maxRetries: 2 backoff and the query's retry: 1, normalizeFacts, the Contact summary requires at least three facts guard, and updateHumanContactSummary's json_set write; the header spinner while fetching, the animate-pulse skeleton (three dotted bars on a two-second 1 → 0.5 → 1 curve, 150ms apart), Summary generation failed + Try again (a refetch), the facts list with the failure footer when a refresh fails, and the explainer copy otherwise; the Automations tab with AutomationsNav (starters, drafts, workflows and automation chats, search, +, row context menus), the overview, StarterAutomationDetails (steps, the folder picker / integration connect row, preview, Save draft / Save & enable disabled like the signed-out billing.isReady), CustomWorkflowDetails with the WorkflowBuilder (trigger select, stacked action steps with their config, add / remove, Needs setup badges) persisting automation_workflows in the frontend's JSON shape, and the forced-open right chat panel; the automations engine and webhooks (automations/engine.ts, the local-api plugin's dispatch_event): the plugin's outbound webhook dispatcher, meeting markdown export and cloud-snapshot shape now live in the shared crates/local-api-core (the plugin delegates with its bindings unchanged; the core emits every object's keys sorted, the byte layout the plugin's serde_json has always produced), and the shell runs the same automations at the same points — meeting.completed once a fresh stop's post-stop processing settles (the awaited batch repair included, the recovery passes excepted) and note.enhanced once 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 on note.enhanced, the stale title's file removed) and its automation_markdown_export_last_run record, the Slack / Linear / Notion starters (which end where requireSupabaseSession ends for the signed-out shell — sign in to run this automation after the recap / action-item checks, no action items found for this meeting as a success), and the enabled custom workflows with lastRun (ok or the joined step details, the first error otherwise) and their fifty processed sessions, the integration steps failing with choose a Slack channel first and the frontend's other messages.
  • Note surface and editor (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 default clipboardTextParser, and copies join blocks with a blank line like clipboardTextSerializer) with debounced writes; links as packages/editor's url.ts / autolinkPlugin / linkBoundaryGuardPlugin / linkOpenPlugin: URL-looking text is linked as you type (bare domains against the tlds list, trailing punctuation and unbalanced closers excluded, email domains skipped), a link's href follows 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 honours inclusive: false, typing over a selection carries the link like marksAcross, and a click on linked text opens http(s) hrefs; task items as tasks.ts / taskListRule / taskIdentityPlugin / TaskItemView and 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 per task-list.css (checked content at 50% with the fit-content strikethrough), and every memo write mirrors the items into action_items with the frontend's own statements (tombstone dropped rows, upsert the rest keeping due_at, no-op when unchanged); mentions as the mention widget and node view: @ at a word start opens the suggestion popup (findMention with atoms masked like textBetween's object replacement), Up / Down / Enter / Escape and hover / click act on it, choosing an item replaces @query with the mention-@ 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 like mentionSkipPlugin, and a click navigates like MentionNodeView (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's top placement (offset(8), flipped below and shifted inside the note's scroll container, the ring-1 outside the box, active buttons on bg-primary), never over the enforced title heading, and the highlight mark renders like .note-typography mark (yellow-200 over 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.be link, <iframe> / <Clip> snippet, or clip link (resolved through the clip page) becomes the clip block atom with the embed URL through replaceSelectionWith (a selection deleted first, an empty textblock replaced, an edge caret following insertPoint up 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): image nodes resolve their attachment id to <session>/attachments/<id> (the fs-sync core's attachment functions, now a shared attachments module the Tauri plugin delegates to) and render at editorWidth% of the editor with rounded-md bg-card, the ring-border ring-offset-2 hover ring and the two resize pills whose drag writes editorWidth back like handleResizeStart (min(maxWidth, max(120, start + delta)), rounded to a percentage of the editor); fileAttachment nodes render the my-1 rounded-lg border bg-muted px-3 py-2.5 card with the mime icon or image thumbnail, name, size and the hover open / remove buttons; a pasted clipboard image or dropped files are saved under attachments, 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 the image / fileAttachment node with the schema's attr order and the convertFileSrc URL (a dropped audio file runs the audio import instead; Attachments must be smaller than 4 MB and upload errors are sonnerToast.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.
  • Search index (plugins/tantivy, search_index.rs): the tantivy schema, tokenizers, query construction, collection management, and the SQLite → tantivy projection worker now live in the shared crates/search-index crate; the Tauri plugin and worker delegate to it (bindings and behaviour unchanged), and the shell opens the same <vault>/search_index collection, runs the same initialize / drain_queue steps (retrying while another process holds the writer lock), and answers @ mention queries with tantivy hits like useSearchEngine().search.
  • Transcript (header-transcript.tsx, transcript/renderer, useTranscriptScreen, stt/utils.ts): shared anlg-transcript segmenter, 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}x button with its 0.5x2x list driving rodio's speed, gated on the shared isPro like useBillingAccess; the position is tracked from a wall-clock anchor because rodio's get_pos reports 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 on bg-yellow-100/50), the right-edge scroll-to-top / bottom pill (useScrollDetection, smooth scrolling, disabled at either end), usePlaybackAutoScroll centring the current line until the user wheels, Space toggling the player outside text fields; speaker assignment (SpeakerAssignPopover / SpeakerParticipantPicker: participants and event attendees first, people, the Add "…" create option that creates a human and links it to the session, Apply to all writing the scope: speaker hint across the session's transcripts or a scope: segment hint for the words; upsertSpeakerAssignment / findSpeakerAssignmentAnchorWordId / mergeTranscriptSegmentAssignments ported over the stored JSON behind the mutateTranscript revision check, with utils.test.ts's fixtures); edit mode (the pill's pencil / check: segments become editors saving updateTranscriptSegmentText on blur or Cmd/Ctrl+Enter with Escape restoring, sections toggle a selection — Shift ranges, Cmd/Ctrl-click outside edit mode — with the MultiSelectionBar above the chat CTA: N selected, Change speaker picker, Merge when 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 floating Change speaker / Play from here / Copy menu opens under the selection or at the pointer, Change speaker swapping 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, the n/m count, 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 in bg-yellow-200/50 with the active match in bg-yellow-500; the memo counts getEditorMatches' occurrences, decorates them like prosemirror-search, and Cmd/Ctrl+H adds the replace row replacing the current or every match); and the Ask anything chat CTA pill of FloatingActionButton / FloatingChatCTA growing into the prompt bar on hover.
  • Capture (transcription plugin over listener-core, useSTTConnection, createCaptureLifecycle): the same RootActor, provider credentials in SessionParams, live delta journaling / materialization / flush (the queue stays open while the engine finalizes and is flushed at Inactive, where onStopped awaits it), session-audio cataloguing, floating recording bar (compact pill and the expanded live-transcript panel: title, bubbles from the engine's live segments with renderLabel speaker labels and overlap outlines, pinned auto-scroll with the Go to bottom chip, expand / collapse writing live_caption_minimized, new sessions starting minimized like LiveCaptionDefaultVisibilitySync; amplitude scaled like updateLiveAmplitude), upload-audio import and listener2-core batch transcription with transformBatch and the whole_session promotion; the batch progress screen's Stop transcription (aborting the run_batch task into the Transcription stopped. state), the transcript pill spinning while finalizing or batch transcribing, and useEnsureDefaultSummary creating 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 #hashtags from the note / title / event, normalised and capped at 50, sent as keywords on live and batch starts, and the full getTranscriptionLanguages codes such as en-US); voiceprints (services/voiceprint.ts + the plugin's voiceprint.rs, now the shared crates/voiceprint-service behind a SecretStore trait with the plugin as thin command wrappers: candidate extraction after a live capture's final flush and after a batch transcript, ahead of markSessionAudioTranscriptionComplete, honouring remember_speakers and the transcription.mic_isolation store scope fed by MicIsolated events; promotion to exemplars on an Apply to all speaker assignment; known_speakers for 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's Inactive details meet, getPostCaptureAction runs the post-stop batch repair for record-only, degraded (needsBatchRepair) or unpersisted captures — current_capture promotion with the audio offset and the live transcript replaced, or whole_session — or the summary alone with the completion cue; the summary runs in regenerate mode when the capture extended an existing transcript; a fresh stop whose live text was saved starts the summary at once and records refreshSummaryAfterRepair in 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's automatic / preserveExistingAudio / initialTitle (false / true / the session title for a manual capture, the auto-start's values otherwise); live captures carry participant_human_ids / self_human_id; the capture_lifecycle_pending: recovery marker is written with the frontend's field names and order, moves to finalizing with 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: the audio_retention policy, the minute tick finishing tombstoned audio and deleting idle, processed, expired recordings, and deleteProcessedAudioForRetention after a capture under none); notifications (showBatchCompletedNotification / showSummaryReadyNotification through the shared notification crate the plugin wraps: the Transcription complete card after a re-transcribe / import batch or a post-stop repair without live text (notifyOnCompletion), the Summary ready card ("<title>" is ready to read.) after a summary lands, both only while the window is inactive and notification_transcription_complete / notification_summary_complete allow it, Open Anarlog bringing the window back on the session like openNew; the tray and the GTK notification windows share one GTK main-loop thread); and notifyFailure's toasts (Anarlog could not finish saving the transcript… / Post-meeting transcription failed… on the stop whose repair failed, and The transcript was saved, but Anarlog could not start the summary… when the finalizing marker write fails — the summary and audio completion wait for that write like finalizeStopped, 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): 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), opens the meeting link when auto_join_scheduled_meetings is on, and records with automatic: 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 real preserveExistingAudio; 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).
  • System tray (plugins/tray): the tray's schedule logic (agenda_sections, menu_bar_title, next_schedule_refresh_ms with their tests), icon bitmaps and menu labels now live in the shared crates/tray-core; the Tauri plugin re-exports them unchanged and the shell drives the tray-icon crate from a GTK thread with the same indicator: Today / Tomorrow agenda sections built like buildTrayScheduleEvents from the timeline's timed, non-ignored events within seven days, Show events in menu bar (a check item persisted in store.json's anlg-tray scope like scoped_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's set_start_disabled / set_degraded / set_recording; show_tray_icon shows or hides it at once. The shell has no updater, so Check for Updates is absent, and since gpui 0.2.2's X11 client ends the run loop with the last window, Hide iconifies the window rather than unmapping it and a window close still quits.
  • Chat (chat/components/*, chat/store/*, chat/transport/*, chat/tools/*, chat/context/session-context-hydrator.ts, chat/store/chat-title.ts): the floating PersistentChat panel now runs the conversation — the Ask anything composer (submitShortcut="enter": Enter sends, Shift+Enter breaks the line; the send / Stop response controls; sends while a reply streams queue like ChatQueue; voice input as useDictation over the dictation plugin's Recorder — the microphone button records a temporary 16 kHz WAV through the app's audio provider with the five-minute cap, refuses with Voice input is unavailable while Anarlog is recording a meeting. during a capture, shows the VoiceStatus row (the fourteen chat-input-waveform-bars, m:ss, the bg-muted stop button, the send / Stop response control) with the Starting… / Transcribing… spinners, runs the batch transcriber over the file (numSpeakers: 1, no keywords) and inserts the joined words like ChatEditorHandle.insertText; No speech detected and the Could not start / transcribe voice input toasts 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 (findMention at 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 — the TextArea now carries atoms with mentionSkipPlugin's caret skipping and whole-chip deletion, the MentionAvatar painted 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 a session:manual ref for the next send and dropped onto a note editor lands as a mention plus a space at the pointer like sessionMentionDropPlugin; the sent message flattens the chips to @label like proseMirrorJsonToText (which also joins the blocks with nothing, so a Shift+Enter break is not in the text) and carries session:auto, the dropped notes, and the mentions deduped by key (mergeContextRefs; queued sends keep the refs they had), with ContextRef modelling 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 (no Full Meeting Transcript when nothing rendered, like buildTranscript), contacts as Referenced contact: with title / organization / email / notes, organizations as Referenced organization:, folders as Folder 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 the History N/M line 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), the bg-blue-100 user bubbles and the assistant markdown rendered with chatComponents' text-sm typography (headings, lists, code) plus the reasoning disclosure title, Thinking..., the border-red-200 bg-red-50 error bubble with its hover Retry, the hover Copy message / Regenerate message actions, useChatAutoScroll's pinned scrolling with Go to recent, the RECENT CHATS dropdown (last five groups of the scope with formatDistanceToNow, No recent chats) and New chat; a press outside closes only while the draft is empty. chat.mode follows the FloatingClosed / FloatingOpen / RightPanelOpen machine: Open in right panel docks the chat beside the note surface (MainChatPanels ported in chat_panel_layout.rs / workspace/chat_panels.rs: the body and the panel split the group by the react-resizable-panels share persisted under react-resizable-panels:main-chat in the web view's localStorage — 30% default, 20–50%, the w-0 handle's 5px hit area drags it and either app reads the other's layout — with the body minimums (NOTE_SURFACE_MIN_WIDTH_PX 500, automations 600, settings min(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; useNoteSurfaceWindowWidthGuard on the note-surface tabs collapses the sidebar when the note would drop under 500px, grows the window by the deficit through window.resize — restored when the chat closes, GPUI has no frame-origin API so the macOS expandLeft grows rightwards — and collapses the sidebar on a narrowing resize) with the h-9 toolbar (history, New chat, Float chat, Close chat), the messages pinned to the bottom of the column and the elevated rounded-xl composer whose send button is always shown; Escape closes the floating panel only. The automations tab shows its own automations chat scope in that panel (no toolbar, no note context, chatScope: automations on its rows, useRecentChatGroups over the scope's groups) and each automation selection keeps its own chat thread like switchTo — a chat row opens its group, a workflow its chatGroupId, 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 canned This recording is using batch transcription… assistant row without a generation. The transport is CustomChatTransport over the shell's streaming client: chat.system.md.jinja for the configured ai_language plus MEETING_CONTEXT_TOOL_GUIDANCE, the ContextBlock template over hydrateSessionContext's snapshot (title, date, memo and enhanced markdown via json2md'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 uses queries.ts' statements and buildPersistedChatMessage's shapes: the user row lands before the stream (createChatGroupWithMessage with createFallbackChatTitle, then generateChatTitle's prompt → normalizeGeneratedChatTitlesetChatGroupTitleIfCurrent; Could not save this chat message. rolls back), the assistant row on finish (step-start / reasoning / text parts with state: done, metadata.createdAt), aborted replies stay unpersisted, and regenerate replaces the previous row like replaceChatMessage. The message metadata carries chatScope: general and the session:auto:<id> context ref, so groups written by either app open in the other. The request advertises the fifteen chat-general tools with the schemas the Tauri app sends (tool_choice: auto) and runs the ToolLoopAgent loop (stepCountIs(5)): streamed tool calls are assembled per provider family (OpenAI tool_calls deltas, Anthropic tool_use / input_json_delta, Gemini functionCall), executed locally, appended as tool-<name> parts (input-availableoutput-available / output-error, step-start between steps) and sent back as the assistant's tool_calls plus tool results, with earlier search_meetings outputs hydrated under contextText like expandSearchMeetingsOutput. list_meetings / get_meeting / get_meeting_transcript / get_recurring_meeting_history run the shared agent-access crate the db plugin delegates to; search_meetings runs the search index with filters.created_at (absolute or recent_days) or the folder's notes; search_meeting_content / find_related_meetings are note-files.ts ported (sections, queryTerms, the 20 / 8 / 3 scoring, 180-character snippets, same-event / shared-participant / nearby-date reasons); search_contacts / search_calendar_events use the frontend's SQL; web_search answers Sign in to use web search. like the signed-out app. The cards are Disclosures: the search card (Searching for: … / Searched for: …, the Query / Date / Limit lines, one result card per view with the carousel arrows, No results found) for list_meetings / search_meetings, the generic wrench card (Running …, the key: value inputs, the output JSON, … failed with 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) and apply_session_correction (exact / bounded / loose replacements over the summaries, transcript words and title, the guarded transaction, dictionary terms saved); edit_memo / edit_summary persist a session_proposals row like persistChatSessionProposal (target document, base_updated_at, source: chat) and open the edit tab (edit/tab-content.tsx): the session title and Memo / summary title header with Decline / Apply to memo|summary, the error row (This proposal is stale. The meeting changed after it was created. when the target's updated_at moved), and @pierre/diffs' unified MultiFileDiff rebuilt from its measured output — the 44px file header with the modified-file ring and -N +M, jsdiff's structuredPatch hunks (its Myers variant ported, four lines of context, the \ No newline at end of file rows), 20px mono rows with the 4px change bars (dotted for deletions), the right-aligned number column, pierre-light / pierre-dark line and gutter backgrounds mixed in Lab like WebKit's color-mix, the theme's markdown token colours (headings, list markers, bold / italic / code / links), and diffWordsWithSpace word emphasis in word-alt mode (a blank between two changed words folds into the span) as 17px border-radius: 3px boxes; lines stay unwrapped and scroll sideways like overflow: scroll. The chat shows the defineTool card for both (ToolCardHeader with the spinner → the pencil in emerald once applied, Edit memo — review tab opened / Memo updated / Memo edit declined / … edit failed, the Streamdown MarkdownPreview of the proposed content at the classes the desktop bundle actually generates — mt-6 mb-2 font-semibold text-3xl headings, list-disc outside the unpadded list, li { py-1 } — the red ToolCardFooterError with the summary candidates, and Decline / Apply to … while the tool waits); applying from either surface runs applySessionProposal (memo raw_md upsert or updateEnhancedNoteContent, applied), declining or replacing the review declines a chat proposal like declineOnUnmount, and the note's PendingProposalsBanner (1 pending edit / N pending edits with a Review memo / Review summary button per row, refreshed with the note) opens proposals written by the CLI / MCP or an earlier session through loadSessionProposal, with Loading edit… / This edit is no longer pending..
  • Find a note, toasts, share popover, onboarding (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 of onboarding-video.mp4 (416×232, 24fps, 5s loop, pre-blurred since GPUI has no backdrop blur, 31 KB) at opacity-28 under the same gradients — the onboarding root now carries the resolved font-sans family and base text style, so the font-semibold section headings render bold like the web view's; the welcome note's Join & record runs HeaderMeetingControl.joinMeeting: the demo opens in the browser with buildWelcomeNoteDemoUrl's autojoin=1 and a completion_url on a loopback callback server while the capture starts, and the demo's completion callback stops it like stopActiveWelcomeDemo; recognised remote meeting links open alongside the capture and unknown links fall back to Record per canJoinFromHeader).
  • Deep links and single instance (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 shared crates/deeplink-core (the plugin keeps its tauri_specta events, pending queues and axum server over it, bindings unchanged); the shell accepts anarlog://… URLs as positional arguments, the Tauri launcher forwards its arguments on hand-off, a second anarlog-gpui launch hands its URLs to the running instance over a Unix socket next to app.db and exits (a bare relaunch or {scheme}://focus just brings the window back, reopening it when closed), macOS on_open_urls / on_reopen feed 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.
  • AI summaries (services/enhancer/index.ts, store/zustand/ai-task/*, note-input/enhanced/*, header-enhanced.tsx): the enhancer service and the enhance / title tasks — 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 shared anlg-template-app prompts with formatSummaryLengthModeGuidance / formatSummaryLengthGuidance / preferred names; a streaming chat client for the OpenAI-compatible, Anthropic, and Gemini request shapes with reasoningProviderOptions, the <think> / <thinking> extraction, maxRetries: 4 backoff, and llm_health_error_message errors; 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 into tags / session_tags, ensureMarkdownFirstLineTitle, the title task for untitled sessions with getPersistableGeneratedTitle, persistGeneratedEnhancedNote's guarded body swap and pending-marker clear, applyGeneratedSessionTitle); collectEnhanceImageContext for 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 (OpenAI image_url data URL, Anthropic base64 source, Gemini inlineData) with the IMAGE_CONTEXT_NOTE ahead of the length guidance; requestAutoEnhance("if_empty") after a capture's final transcript flush and queueAutoEnhanceIfSummaryEmpty after an audio import, with the durable auto_enhance_pending: markers, the 500ms eligibility recheck loop, the 5s pending-job resume, the exponential failure budget, and auto-enhance-started switching the open note to the summary; Summary wasn't generated toasts for short transcripts. The enhanced tab renders StreamingView (Analyzing structure... / Model is thinking... with the tip line, the SummaryTitleSpace with Generating title..., the streamed markdown with the block caret), EnhanceError with Retry, ConfigError, and the persisted document with enforceTitleHeading (the 1.5rem title h1 and the Untitled placeholder); the header pill shows the spinner while generating and turns red on failure; the template picker's Regenerate regenerates under the note's own template and choosing another template runs service.enhance with targetNoteId / templateTitle. The persisted summary is editable like EnhancedEditor: a BodyEditor over ensureFirstLineTitle(content, sessionTitle) with the titleHeadingPlugin behaviour (an h1 stays first, a lone empty title gets a paragraph) and documentTitlePlaceholder while the anchor sits in the empty title; flushes run updateEnhancedNoteContent (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. playCompletionSound renders the chosen cuelume cue (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_GAIN into 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's Preview plays the selected cue.
  • Icon set and chrome: main's outline icon set (Use outline icons across desktop and mobile #7398 — the transcript tab's Lucide audio-lines glyph, outline Play / Pause, the bold 2px stroke for a pinned contact, the favourited template's heart and the non-default notes filter), the sidebar chrome row without the toggle spacer (Move Windows sidebar actions into the title bar #7395: Linux keeps Search / New note / Filter flush at pl-2, Windows moves them into the title bar beside the toggle and drops the row), DropdownMenuRadioItem's 8px dot in the pl-8 slot for the sort menus, and Radix's flip: an inline menu that would leave the window opens 4px above its trigger instead of below.
  • Rendering fidelity: Tailwind rings drawn as border boxes (ui::ring — GPUI's shadow shader integrates over 3× the blur radius, so a spread-only box-shadow paints nothing; the shadcn Input's focus-visible:ring-1 ring-ring on the New folder dialog and webhook URL fields, the template textarea's ring-2 ring-primary/20, the note image's ring-border ring-offset-2 hover ring, with --ring in the theme), WebKit-style line breaking (pre-wrap + break-word, and a port of InlineContentConstrainer::prettifyRange for text-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 blurred bg-card/60 resolves to.

Deliberate deviations: the Auto format editor's paragraphs sit 20px apart where the web view's prompt-editor gives each <p> a 4px bottom margin (the shell's TextArea lays the text out as one block; the field is Pro-gated and read-only signed out); the openai / azure_openai providers speak chat completions (response_format.json_schema for structured output) where @ai-sdk/openai / @ai-sdk/azure use 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 it bottom-full, where the top player's overflow-hidden shell 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 shows TranscriptEmptyState's error screen (Tauri's useTranscriptScreen counts batchError as visible state, so it lands on the ready screen with nothing to render — the error branch there is unreachable); the transcription hints omit retext-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: getFolderFilter reads a sidebar folder filter nothing sets, so both apps answer No folder is selected); the remaining capture lifecycle (local model server, the settled-diarization refinement and the anarlog cloud batch target behind sign-in; live partials are not rendered by the shipping app either — partialWordsByChannel only 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, not text/html) and image title tooltips; 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 and pretty wrapping, 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 and JSON.stringify order / readiness / formatDistanceToNow, transcript word ranges / sentence lines / seekability / timeline offsets, the ported upsertSpeakerAssignment / anchor / merge / updateTranscriptSegmentText fixtures, 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, the list-common / list-openai / LM Studio / Ollama / OpenRouter / Unsloth fixtures and model-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 ported summary-length.test.ts fixtures, 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, the convertFileSrc encoding 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 WebKit ItemTable read / 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 desktop
  • Shell switch: cargo 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 predates import.meta.main, which lingui 6's CLI entry points use; CI's Node 22 has it); the hand-off smoke test described above
  • Deep links: cargo 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 (the export_types output matches the committed bindings; the committed bindings.gen.ts / apps/web/content/deeplinks were already stale on main and are left alone), cargo clippy --locked -p deeplink-core -p tauri-plugin-deeplink2 --no-deps -- -D warnings, cargo check --locked -p desktop; the shell's deeplink tests cover classification, argument filtering, the demo URL builder, a second launch forwarding over the socket and the loopback server answering once and shutting down
  • Search index extraction: cargo 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 desktop
  • Voiceprints extraction: cargo 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 trip
  • Attachments extraction: cargo test --locked -p fs-sync-core (94, unchanged: the attachments module wraps the existing helpers), cargo check --locked -p tauri-plugin-fs-sync
  • Capture lifecycle: the shell's tests add getPostCaptureAction's cases, the current_capture audio offset and word promotion, the recovery marker's serialisation / parsing (now with automatic / preserveExistingAudio / initialTitle), and the retention policy / expiry / row decisions
  • cargo metadata --locked stable; pnpm exec dprint check on all changed non-Swift files
  • uvx zizmor --format sarif . (1.30.0): 410 findings, none on the gpui_linux_ci job — the new CD line uses $TAURI_CONF_PATH rather than a template expansion; the code-scanning alerts on the new job (unpinned checkout, default permissions, credential persistence, and the two uses: ./… lines zizmor now wants in GitHub's $/ self-repository form) are fixed by pinning actions/checkout to the v4.4.0 hash, persist-credentials: false, permissions: contents: read, and uses: $/.github/actions/…; the 97 remaining self-repository findings are the repository's pre-existing ./ references, left as they are
  • Side-by-side runs of the Tauri dev app and the shell on the same database (Linux/X11, WebKitGTK), with DOM boxes measured through WEBKIT_INSPECTOR_HTTP_SERVER to confirm identical row, heading, and control positions
  • Timeline selection: cmd-click in both apps folds the anchored row into the selection and paints both rows bg-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 (#d5 vs #d6); in the shell the shift-range → Delete → Undo round trip restores every note (0 tombstones) and reopens the first; Show Deleted Events lists the ignored recurring Weekly sync struck through, and Show This Event writes ignored_events = [] and the row returns at the future opacity
  • Calendar compact layout: both windows at 800×640 (surface 595px → 4 columns; container measured through the inspector at x=205, 595 wide) show the same September strip anchored on today with the Weekly sync chip on Mon 7, the same weekday header row, and after › the same Thu 10 – Sun 13 window with column boundaries on the same pixels (353 / 502 / 650); ‹ twice from there lands on Wed 2 with the Stats demo chip on Fri 4
  • Editor links: typing See linear.app/fastrepl-inc/overview. Ping support@x.com or x.com/docs into a new note in each app persists byte-identical session_documents.body JSON (two link marks with target: null, the trailing . and the email left plain) and renders the same blue underlined runs; a click on x.com/docs in the shell reaches open_url with https://x.com/docs (the portal handled it on the pod); the six autolink.test.ts fixtures run against the model
  • Task items: typing [ ] Ship it, Enter, [x] Done thing in both apps persists identical session_documents.body JSON and identical action_items rows (ids masked: three rows in document order, the nested item's parent with an empty preview, created_by from the workspace binding); clicking the first checkbox in each app flips its attrs to status: done, checked: true and the row to done; 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. hydrateTaskContent is 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 duplicate
  • Mentions: Met with @gr in both apps opens the popup with the same tantivy hits (Grace Hopper, then the two Untitled notes whose contents match) once the shell's worker owns the index; typing Quokka sighting report in the shell and then @quok lists that note after the projection drained (search_index_dirty back to 0 pending); Enter then today persists 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's findMention / candidate / node-shape rules are unit-tested
  • Contacts: with a seeded organization of two members, both apps render the organization column with the avatar block, Name row, People · 2 members heading and member cards on the same rows (borders at y=200 / 253 / 314; card bottom at 490 after matching the size-7 icon buttons), and the person column's rows at 206 / 259 / 312 / 369 / 422 (the facehash's inline strut included); picking /tmp/avatar-src.png through the shell's native dialog writes metadata_json.avatarDataUrl as a data:image/jpeg;base64 string that Tauri renders identically after restart (same centre crop), Remove photo leaves {}, and typing in the Name field updates organizations.name / updated_at with the header and list row following
  • Floating panel: with the fake Deepgram websocket streaming an 8-line two-speaker script into both apps (MOCK_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 same text-wrap: pretty breaks (The build passed overnight, so / we are on track for Thursday.); expanding in the shell writes live_caption_minimized=false, resizes 111×67 → 368×459 keeping the top-right corner, collapsing writes true, and a new session resets it to true. The live-caption window is not ported: LiveCaptionWindowDisabled hides it unconditionally in the shipping app
  • Automations: both apps render the overview (title, description, dashed empty card) and the Slack starter details on the same rows (card borders at 146/147, 258/259 — the description's 22.75px line box rounds differently — with 76px step cards, the size-7 button row, the 19px squircle badges and the Connect Slack config row) and the same disabled Save draft / Save & enable while signed out; in the shell + prepends {"id":…,"title":"Untitled automation","enabled":false,"trigger":"note_enhanced","steps":[],"lastRun":null,"processedSessionIds":[],"chatGroupId":null} to automation_workflows, Add step → Markdown export appends {"id","type":"markdown_export","directory":""}, the native folder dialog writes the step's directory and drops Needs setup, and a restarted Tauri renders that workflow with the same card layout (trash button flush right, Add step select, footer hint)
  • Transcript: on a 1774-word two-speaker session with audio, both apps render the same segment rows, hover the same word with bg-accent/60, and clicking release seeks 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; assigning Ada Lovelace with Apply to all writes {"human_id","scope":"speaker","channel":0,"speaker_index":0} and typing Zed Tester with the checkbox off creates the human, links the participant and writes the scope: segment hint 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 and 1 selected · Change speaker · Merge · × bar, appending Edited! to a segment and blurring stores here. Edited! on its last word like updateTranscriptSegmentText, 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. Dragging tart with … so we are across two segments highlights the same range in both apps and copies tart with the release status and then go over open bugs.Speaker 1The build passed overnight, so we are (a DOM range's toString() runs through the label); the menu is 147px wide with 27px rows in both; Cmd/Ctrl+F the on 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 memo alpha beta alpha gammaalpha counts 1/2, Cmd/Ctrl+H delta Enter yields delta beta alpha gamma and 1/1
  • Tray: with a systray plugin added to the pod's XFCE panel, both apps show the same indicator whose menus list Today / 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 adds Check for Updates); in the shell Hide iconifies and Open restores the window, Settings opens General, Start a new meeting opens a recording note (the Settings tab replaced like openNew), disables Start and cycles the recording frames until Stop restores the default icon, unchecking Show events drops the agenda and writes {"anlg-tray":"{\"show_events_in_menu_bar\":false}"}, and Quit Completely… prompts Quit Anarlog Dev Completely? and exits on confirm
  • Intelligence settings: against a mock OpenAI-compatible server on 127.0.0.1:8765 (seven models, 401 for any key but test-key), both apps list GPT 5.6 / Alpha Chat / Zeta Chat with 3 items ignored. in the same 2:3 provider / model row, auto-select custom / gpt-5.6 with the green health check and show the Reasoning effort row 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 shows Connection 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-model and the After recording badges
  • Onboarding BGM: launching against a fresh store.json shows the onboarding with a PulseAudio sink input from anarlog-gpui playing; the stream ends when the process exits
  • Summaries: against the mock OpenAI-compatible server streaming a fixed two-section summary and the title Release Readiness Sync, Regenerate on the 1774-word session persists a byte-identical session_documents.body in both apps (the # Long transcript demo title heading, two h1 sections, bullet lists), and the system prompts are identical while the user prompts match once the shell trims the trailing newline tiptap_json_to_md adds (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 muted Untitled title; recording the 8-line mock-Deepgram script in each app and stopping produces auto_enhance_pending:<session> → the streaming view with Generating title... → the generated title Release Readiness Sync on 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-word Seek test session shows Summary 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 appends Edited. to the first bullet in the stored body after the 500ms flush, typing 2 at the end of the title heading writes sessions.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 the Untitled placeholder shows only while the caret sits in the empty title. The completion cue: cuelume's five recipes rendered through a headless-Chrome OfflineAudioContext (its own renderRecipe graph) and the shell's renderer agree on length and on the 50ms peak envelopes after the first window (e.g. ready 0.118 / 0.120, 0.073 / 0.072, 0.019 / 0.019; chime 0.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's Preview recorded 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]; both Preview buttons open a short playback stream on the null sink
  • Batch controls: with the fake Deepgram batch endpoint delayed 30s, Re-transcribe in both apps shows Generating transcript... / 12% complete / ☐ Stop transcription on the same rows (the Summary tab appearing next to Memos and the pill spinning), and Stop in the shell lands on Transcription failed / Transcription stopped. / ↻ Re-transcribe while the request is aborted (Tauri leaves the body blank, see deviations); the batch request now carries language=en-US like Tauri's (Tauri's also carries keyterm=Release&keyterm=Readiness&… from its part-of-speech pass)
  • Rate menu (with isPro forced on in both apps, since neither has a signed-in session here): the 1x button sits in the same slot after the meta in both; picking 2x in the shell advances the clock two seconds per second (00:02 / 00:04 / 00:06) and the 10s clip ends after five
  • Voiceprints on the pod: Re-transcribe on the 10s session through the fake batch endpoint logs voiceprint_candidates_extracted … stored=2 and adds two voiceprint_candidates rows; a live capture of the 8-line mock-Deepgram script stopped after 25s stores the same six ch0:s0 / ch1:s0 spans (quality 0.36 / 0.33) Tauri stored for its own capture of that script, and the attachment's transcript_status becomes complete; assigning Grace Hopper with Apply to all logs voiceprint_candidates_promoted … promoted=1 and leaves one voiceprint_exemplars row (manual_speaker_assignment, confidence 1.0) whose vector sits in the voiceprint_exemplars keyring scope
  • Post-capture lifecycle on the pod: with the fake Deepgram websocket refusing upgrades, a capture records without live transcription (listen_ws_connect_failed retries) and Stop logs starting post-stop transcript repair … live_active=false needs_batch_repair=true → the batch endpoint → a 24-word batch_transcription row, voiceprint_candidates_extracted … stored=2, transcript_status: complete, the Summary document, and auto-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"} under capture_lifecycle_pending:<session> and the row is gone once note.enhanced lands; kill -9 ten seconds into a capture leaves that marker, audio.wav, a 9-word live transcript and four journaled deltas, and the relaunch logs recovering 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; with audio_retention = "none" the minute tick deleted the seven idle, processed recordings on the pod and marked their attachments absent (files restored afterwards); a capture whose websocket drops after 20s of live text (the fake server closing it, then refusing upgrades) logs starting live transcript summary before starting post-stop transcript repair in the shell, writes the marker with "refreshSummaryAfterRepair":true, produces the summary from the live text while the 30s batch runs, regenerates after completed 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 a finalizing marker 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)
  • Deep links on the pod: with the shell running, anarlog-gpui --identifier com.hyprnote.dev anarlog-dev://focus exits 0 after forwarded launch to the running instance and the shell's window becomes the active X11 window; on the welcome note, Join & record opened Chrome at anarlog.so/onboarding-demo/?autojoin=1&completion_url=http%3A%2F%2F127.0.0.1%3A<port>%2Fonboarding-demo%2Fcomplete (the demo's Ready to join? page) while the capture started (sidebar collapsed like setLeftSidebarExpanded(false), Tauri's meeting-detection toast fired on the shell's anarlog-speaker-capture stream); curl on that completion URL returned the shared Demo complete page pointing at anarlog-dev://focus, the capture stopped, and the header turned into Share with the Memos / transcript switcher over the saved audio; the STT provider select shows Anarlog alone in both apps on Linux
  • Observed in Tauri here: GlassDialogCancelButton renders a <button> without type="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_core never logs Created folder; the Create button works) — the shell's Enter creates the folder, as the form intends; a one-line Tauri fix for a separate PR
  • Observed in Tauri here: proseMirrorJsonToText joins the composer's paragraphs with nothing between them, so first line Shift+Enter second line is sent and stored as first linesecond line; the shell reproduces it
  • Observed in Tauri here: after a capture, useAudioExists stayed false for the recorded sessions although fs-sync's audio_exist finds audio.mp3; the shell follows getCanShowTranscript / meetingOver as coded, so verify the post-capture header on macOS
  • Chat: against the mock OpenAI-compatible server, What 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's body_to_markdown emits json2md'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 fifteen tools and tool_choice: auto. Both persist the same rows: chat_groups (title from the fallback then Meeting Action Items after 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's RECENT CHATS lists Tauri's group and opens its messages. With the mock emitting a list_meetings call for Please list meetings, both apps send the same follow-up (the assistant tool_calls entry with {"limit":3} as JSON text, then the tool message with the 807-character output) and persist byte-identical assistant parts (step-start, tool-list_meetings with toolCallId / state: output-available / input / output, step-start, text); the Searched for: meetings card, its Query: none / Limit: 3 lines and the Weekly sync · 9/7/2026, 10:00:00 AM · No excerpt available result card render on the same rows. search_meeting_content (64 sessions scanned, the same scores and snippets), find_related_meetings, search_contacts, search_calendar_events and web_search return byte-identical outputs from both apps; search_meetings returns 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 panel in 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's Context chars: 17 is the bare question) and list the new group under My 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 / searchNote scoring / snippet cuts / the created-at filters, the tool part key order and model-turn conversion, and the card titles / result parsing
  • Edit tools: with the mock server emitting edit_memo / edit_summary calls, both apps write the same session_proposals row shape (memo_replace targeting the memo document, current_markdown with json2md's loose list blank lines, base_updated_at = the document's updated_at, pending / chat) and, on Apply to memo, a byte-identical session_documents.body (md2json of the proposal) with the row applied; 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, # Agenda in #d52c36, the Old item / Item emphasis boxes at x=66–127 / 66–96, the No newline at end of file rows) with the measured #ffeae6 / #ffefec / #eaf9f5 / #effbf8 fills and the -3 +1 counts 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 the Zed Corp shipped / build and The build passed overnight, so / team is on track for the Thursday release spans 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 same No active session selected… error card (111 output chars in both), a stale target shows the red error row, Decline marks the row declined and the card Memo edit declined, and the banner lists an orphaned pending proposal and opens it. Cmd/Ctrl+J now works after the composer or another field unmounts (the workspace takes focus back), and text-red-500/600/200 / amber-500 / emerald-500 use the Tailwind v4 values the desktop renders (#fb2c36 / #e7000b / #ffc9c9 / #fd9a00 / #00bc7d)
  • Voice input: with MOCK_AUDIO=1 and the fake Deepgram batch endpoint, the microphone button in both apps turns the composer into the editor-over-VoiceStatus column 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), Stop inserts the identical Good 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's
  • Editor clipboard and attachments: Ctrl+V of https://www.youtube.com/watch?v=dQw4w9WgXcQ at the start of abc, 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 writes attachments/image.png, the session_attachments row (image.png, attachments/image.png, image/png, 756 bytes, the file's sha256, local_file, note_upload, source_id = image.png, {}), the present local-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 own attachmentList at the same 694×434 box (x 221..914) with the same rows below it (WebKitGTK itself pastes clipboard images as a blob: <img> with no attachment on this pod, so the macOS flow is the reference); dragging the shell's right pill 102px writes editorWidth: 68, and both apps then draw the image 590 / 589px wide with the hover ring and pills on the same pixels; a seeded release-notes.txt fileAttachment renders the card at 536..597 with the 40px icon box at 234..274, the file-text glyph at 558..575 and the 14px / 12px text rows in both apps (the card colours #e7e5e4 / #f5f5f4 / #f2f2f1 match); selecting abc shows the toolbar at the same 36..223 × 101..138 box below the selection in both (flipped off the header), B writes the bold mark and paints the active button identically, the highlighter writes highlight and both draw the #fef08a box over rows 7..28 of the crop
  • Image context: with the red PNG pasted into the Long transcript demo memo and the mock server's gpt-5.6 (an image-capable id), Regenerate in both apps sends a byte-identical 21,217-byte request — the same system prompt, the user text ending in the IMAGE_CONTEXT_NOTE and the length guidance, and one image_url data-URL part — once the shared tiptap converter serialises a block image like json2md (its own paragraph, char-editor-width=80 in the title) and body_to_markdown keeps 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
  • Contact summary: against the mock OpenAI-compatible server answering the brief prompt with {"facts":[…]}, opening a contact in either app sends the same generateText body (model, max_tokens: 4096, response_format: {"type":"json_object"}, the 1,185-char system prompt, the {"target":…,"meetings":[…]} user message — byte-identical apart from the target fields of the two contacts sharing the session) and stores the same contactSummary record shape; the two apps compute the same sourceHash for 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_facts plus the one new meeting) and store the two-source record; a 500 from the mock makes both run six attempts (maxRetries: 2 × retry: 1) before Summary generation failed, Try again re-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's space-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-sdk shapes), whole-reply parsing and OpenAPI-schema conversion fixtures
  • Chat context refs: Tell 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; sending Tell me about @Stats demo one please stores the same text and the same contextRefs (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); dragging Seek test from the timeline onto either chat panel and sending hi stores 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 under History 1/1 (Tauri 1/2 with an earlier send) and eight Backspaces from the end remove please and the whole chip in both. On the pod, Tauri's @Gra shows 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 fixtures
  • Chat panel layout: measured with the inspector against the shell at the same window widths — 1400px settings + chat: body 900 / chat 496 after the same drag (Tauri's 37.2349570201% read from localStorage by the shell, the shell's 30.0715239342% written back in the library's shape with the {} entry kept); 900px automations: body 800 / chat 320 clipped at the window edge with Ask anythin cut 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's leading-relaxed / leading-snug copy 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's overflow-y-auto scroller 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 Section is 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 #1c1917 1px ring and shadow-xs (pixel-compared), the focused template textarea the same 2px primary/20 ring, and the hovered note image the same 2px card + 1px border ring at x=218..220
  • Scheduled auto-start on the pod (auto_start_scheduled_meetings on, 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 and Stop in 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 same audio.mp3 attachment, present state, transcript and note / summary documents 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 its note document and the marker cleared (discarded empty automatic capture in the shell's log); a relaunch inside the grace window re-fires a still-due meeting in both (firedEventIds is per run). The Tauri debug binary had to be rebuilt first: the one on the pod predated audio_has_speech, so its discardEmptyAutomaticCapture kept the audio after Command not found
  • Sidebar width: with Tauri's saved layout ([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] into http_localhost_1422.localstorage and {"gpui":{"left_sidebar_fraction":0.2372…}} into store.json, and a relaunched Tauri opens with flex: 23.722712 and 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 copy
  • Local-api extraction: cargo 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 desktop
  • Automations on the pod (a webhook endpoint pointed at a local sink, the markdown-export starter to one directory and an enabled markdown_export workflow to another, each app run alone with the records reset in between): Regenerate on the 1774-word note fires note.enhanced in both — one signed delivery whose body is byte-identical apart from the event id, created_at and the document's updated_at (same created_at, data, event, id order once the core sorts its keys), the starter's and the workflow's exports byte-identical (11,179 bytes), the workflow's lastRun / processedSessionIds and the starter's {"at","detail","status"} record in the same shape; a 20s mock capture stopped in each app fires meeting.completed at the stop (empty title, the transcript text) and note.enhanced after the summary (Release Readiness Sync), the meeting_completed workflow exports 2026-09-07 Untitled meeting [<id8>].md and the starter's Untitled meeting file is replaced by Release Readiness Sync [<id8>].md in 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 at Finalizing; the queue now stays open until Inactive and the shell stores the same 124 words
  • Marker failure on the pod: with a temporary SQLite trigger refusing capture_lifecycle_pending:% inserts, stopping a capture in the shell logs failed to persist summary recovery state … marker save refused, shows the red The transcript was saved, but Anarlog could not start the summary. Try generating it again. toast, starts no summary and logs recovering capture … attempt=1, attempt=2 quietly until the trigger is dropped, after which the marker clears; the normal stop still produces meeting.completed, the summary title and a cleared marker
  • Focus after a tab switch: once a recording's stop swapped the memo editor for the summary tab, Escape and Ctrl+Shift+N were dead in the shell (the editor's focus handle stayed alive off-tree, so keys went to the bare root); with the workspace reclaiming focus whenever the focused handle is not under it, Ctrl+Shift+N after the first capture opens and records a second note
  • Onboarding on the pod (OnboardingNeeded2 set, both apps at 1100×1000): the Welcome to Anarlog title, Create account heading, description and Get started button 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 its import.meta.env.DEV back 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 #fefefe bottom band), so the macOS build is the reference for the moving backdrop
  • Regression sweep after the sidebar and focus changes (both apps at 900×950 and 1100×1000): the General settings page's text bands match row for row (max 1px glyph offset) once the Where your notes and recordings are stored label stopped wrapping (Taffy sized its min-w-0 column 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 the Related Notes search box on the same pixels (662–870, borders 663 / 868), typing zzz gives the same focused fill #f0f0ef, the same text / clear-button extents and No 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 apps
  • Main's icon and chrome refresh (Move Windows sidebar actions into the title bar #7395, Use outline icons across desktop and mobile #7398, Validate Custom STT without requiring an OpenAI model catalog #7446, rebased on): the sidebar chrome row's Search / New note / Filter glyphs start on the same x in both apps (21 / 49 / 77 vs 21 / 50 / 80, the same Hugeicons outlines), the transcript tab shows the same six-bar audio-lines glyph, the Related Notes sort 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)
  • Notifications on the pod: Re-transcribe through the 30s-delayed fake batch endpoint with the Tauri window active shows the shared GTK Transcription complete / Your transcript is ready. / Open Anarlog card at the screen's top-right, and Open Anarlog makes the shell the active window on that session; a record-only capture stopped with the fake server down raises Anarlog 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 capturecompleted post-stop transcript repair) when the server returns
  • Not run: macOS/Windows builds of the new crate (no runner available here); gpui_linux_ci covers Linux, and the macOS cargo check stays continue-on-error until the Metal build is validated on macos-26 runners
Open in Web Open in Cursor 

@netlify

netlify Bot commented Sep 5, 2026

Copy link
Copy Markdown

Deploy Preview for anarlog canceled.

Name Link
🔨 Latest commit 16d1543
🔍 Latest deploy log https://app.netlify.com/projects/anarlog/deploys/6a9fa58f06fbf50008cbcd4d

@cla-assistant

cla-assistant Bot commented Sep 5, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment thread .github/workflows/desktop_ci.yaml Fixed
Comment thread .github/workflows/desktop_ci.yaml Fixed
Comment thread .github/workflows/desktop_ci.yaml Fixed
Comment thread .github/workflows/desktop_ci.yaml Fixed
@cursor
cursor Bot force-pushed the cursor/gpui-desktop-foundation-1ade branch from 6f1bf23 to 4b9d87c Compare September 5, 2026 03:54
@cursor
cursor Bot force-pushed the cursor/gpui-desktop-foundation-1ade branch 5 times, most recently from 90ec761 to 1a1b3e7 Compare September 5, 2026 19:24
Comment thread .github/workflows/desktop_ci.yaml Fixed
Comment thread .github/workflows/desktop_ci.yaml Fixed
Comment thread .github/workflows/desktop_ci.yaml Fixed
@cursor
cursor Bot force-pushed the cursor/gpui-desktop-foundation-1ade branch 14 times, most recently from e84633e to 671be7c Compare September 7, 2026 02:54
@huanglinfei091-cmd

Copy link
Copy Markdown

Hi @ComputelessComputer — I noticed the fmt job failed at Check formatting in this GitHub Actions run for this PR.

I’m testing RunBack, an early alpha CLI that reconstructs a failed public Actions run locally with act. For this run, the useful question is whether the same formatting files and diagnostics are reported, or whether reconstruction stops with an honest blocked result.

runback doctor
runback https://github.com/fastrepl/anarlog/actions/runs/34015472318

The Linux amd64 binary and checksum are in the v0.1.0-alpha release. If you try it, the verdict (SAME_FAILURE, DIFFERENT_FAILURE, or REPLAY_BLOCKED), Stage/Cause, and any install friction would be useful. Please do not post tokens or secrets. I’m asking for candid alpha evidence rather than a star or promotion; a failed reproduction is useful too.

@cursor
cursor Bot force-pushed the cursor/gpui-desktop-foundation-1ade branch 4 times, most recently from 4c0dd06 to d461361 Compare September 7, 2026 08:36
cursoragent and others added 24 commits September 8, 2026 03:47
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>
@cursor
cursor Bot force-pushed the cursor/gpui-desktop-foundation-1ade branch from 411b4ca to c86fef9 Compare September 8, 2026 03:48
cursoragent and others added 5 commits September 8, 2026 04:22
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants