Feat/windows track1 parity#8067
Conversation
The synchronous DELETE+INSERT transaction in replaceLocalGraph was running
on the Electron main thread, blocking all IPC for 1–3 s on large graphs.
Fix:
- kgWorker.ts: new worker_thread with its own WAL better-sqlite3
connection; prepares all statements once at startup, runs the full
replace transaction off the main thread, returns `{type:'done',ms}`.
- kg.ts: lazy worker lifecycle with coalescing queue (only the latest
pending graph is kept while a write is in flight); in-memory kgSnapshot
cache so empty-query reads skip SQLite entirely; kg:status and
kg:saveGraph IPC return immediately.
- db.ts: WAL + NORMAL sync unconditional (was bench-only); main thread
reads are no longer blocked while the worker holds the write lock.
- electron.vite.config.ts: second rollupOptions.input entry emits
out/main/kgWorker.js alongside out/main/index.js.
- electron-builder.yml: kgWorker.js added to asarUnpack so worker_threads
can require() it in packaged builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dispatch(): wrap ensureWorker().postMessage() in try-catch so a Worker construction failure (e.g. missing kgWorker.js in packaged build) resets workerBusy and retries via flushPending() instead of silently deadlocking all future kg:saveGraph calls for the session. - kg:queryNodes: resolve cap = limit ?? 80 once before the snapshot/DB branch so both paths return the same node count; previously the DB fallback used queryKgNodes(q, limit) which defaulted to 12, while the snapshot path defaulted to 80 — callers saw different context depending on whether the first worker write had completed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: add Memories (Brain icon) as primary nav item between Conversations and Tasks — matches macOS sidebar order - Rewind: surface the already-built RewindSearchBar with a Search button; add Close search button to return to the timeline view (showSearch was permanently false with no toggle) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Electron Tray created at launch with the existing app icon - Right-click menu: Open Omi, Screen Capture toggle (checkbox, reads/writes rewind settings and broadcasts rewind:settings to renderer so the sidebar toggle stays in sync), Quit Omi - Left-click on tray icon shows/focuses the main window - Close-to-tray: window X button hides the window instead of destroying it; app stays alive in the tray (mirrors macOS menu-bar-app behavior) - isQuitting flag prevents hide-to-tray from blocking actual app.quit() calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rsations widget - Sidebar: rename nav label "Home" → "Dashboard" to match macOS first-screen naming - Add QuickConversationsWidget: fetches /v1/conversations, shows 3 most recent with emoji + title + relative time (Today/Xm ago/Yesterday/date), links to Conversations, spans both grid columns as a full-width "Recent Activity" row - Home: add convsReady to the 3-widget reveal gate (all 3 appear together to avoid layout jank); safety timer covers the 6s fallback for all three - Widget grid now: [Tasks][Goals] / [Recent Conversations — full width] - Chat flow unchanged; existing route /home preserved Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add RecordingStatusBar component that mirrors the macOS sidebar recording panel: a pulsing rose dot + Listening/Connecting label + MM:SS elapsed timer + last-6-words transcript snippet, placed just above the mic/screen toggles. - Subscribes to liveConversation singleton (status: idle|connecting|live|error, transcript segments) so it reacts to always-on continuous recording - Also shows when recorder.recording is true (manual one-off recording) - Collapsed sidebar: shows only the dot with a tooltip label + elapsed - Hidden when idle — no visual noise when not recording - No new IPC or backend changes required Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Settings (gear icon) to navItems after Apps — Windows sidebar now has 7 items matching macOS exactly (Dashboard · Conversations · Memories · Tasks · Rewind · Apps · Settings) - Convert account-avatar row from NavLink → Link so it doesn't double-highlight alongside the new Settings nav item - Update TRACK1_SWIFT_PARITY.md: App Shell now Partial→improved, Settings nav placement noted as done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix Memories.tsx: replace macOS-only ⌘/Ctrl shortcut hint with Windows-correct 'Ctrl+Enter to save' - Update TRACK1_SWIFT_PARITY.md: revised summary totals (6 ✅ / 13 🟡 / 3 ❌), mark all P0 gaps as resolved, document remaining P1/P2 gaps honestly - Add TRACK1_SUBMISSION_CHECKLIST.md: build commands, installer path, full manual test checklist, 10-step demo script, known limitations table, PR summary bullets, submission readiness verdict npm run typecheck ✅ | npm run build:win ✅ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Enable window resize: resizable=true with minWidth/maxWidth locked to OVERLAY_WIDTH (336px) so only height is user-resizable; minHeight=80 - Improve drag handle: h-7 / 3px / w-10 / 60% opacity (more visible) - Add OmiPill: green status dot + 'Omi' label above the input row, matching macOS AgentPillsRowView style (static default agent only; no agent VM backend on Windows) - Add ResizeGrip: 3-dot SVG at bottom-right, mirrors macOS FloatingControlBarView ResizeHandleView (pointer-events-none, visual only) - Update TRACK1_SWIFT_PARITY.md: Floating Overlay Partial→improved npm run typecheck ✅ | npm run build:win ✅ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…abs, starred conversations
Source-trace all 14 remaining macOS parity gaps. Implement the top 4 IMPLEMENT_NOW items:
1. Chat citation cards — parse `done:` SSE event (base64 JSON), extract `memories`
array, attach to last assistant `ChatMsg`, render tappable source cards below
the reply bubble (links to /conversations/{id}). Data was already in the stream
but discarded. `useChat.ts` + `ChatMessages.tsx`.
2. Shortcuts settings tab — `ShortcutsTab.tsx` exposes the overlay accelerator
(already wired to main via `setAccelerator`) so users can change the Ask Omi
shortcut without re-running onboarding.
3. Integrations settings tab — `IntegrationsTab.tsx` (Sticky Notes + Google OAuth)
was fully built but not registered. Added `shortcuts` and `integrations` to
`SETTINGS_TABS` and `TAB_COMPONENTS`.
4. Conversations starred filter — `ConversationRow` now carries `starred` from the
API response; star toggle button appears on hover for cloud rows (calls
`PATCH /v1/conversations/{id}/starred`); new Starred filter chip added.
Also adds `TRACK1_REMAINING_SOURCE_TRACE.md` with full source evidence for all 14 gaps.
typecheck ✅ build:win ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…export
Add OcrPanel (collapsible, selectable, empty state) to RewindPlayer toggled
by new Text button; add Maximize2 fullscreen button floating top-right of
player; add Export button downloading omi-rewind-{date}.json via blob URL
(no new IPC). Update parity docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…act view Add date filter dropdown (All time/Today/This week/This month) using client-side sortAt filtering; load /v1/folders for folder tab strip; add compact view toggle with macOS-style emoji badge row; switch cloud row timestamps to macOS format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cOS parity Add SupportTab with app identity card (logo + version + Electron/Node runtime), external links (omi.me, help.omi.me, GitHub issues, Privacy, ToS), local data note. Inject __APP_VERSION__ via electron-vite renderer define. Reorder Settings tabs: Integrations before Shortcuts, Support at end matching macOS About position. Update TRACK1 docs: mark Support/About DONE, document remaining infeasible gaps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, category filters, conversation row actions, folder CRUD
- Insights.tsx: new page showing SQLite-persisted insight history with category
tabs (All/Productivity/Communication/Learning/Health/Other), search, expandable
reasoning cards; wired into Sidebar (Lightbulb icon) and MainViews
- Rewind.tsx: date picker filters to any past day via rewindFrames(from,to);
Markdown export alongside JSON (one ## section per frame with app/window/OCR)
- Memories.tsx: category filter tab strip derived from live memory.category values;
interactive={true} on BrainGraph so nodes are clickable
- Conversations.tsx: hover action toolbar on each row (edit title inline, copy
preview to clipboard, delete single); folder strip now always visible with
+ New folder (POST /v1/folders) and × delete (DELETE /v1/folders/{id})
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…elete single, folder CRUD
Hover toolbar on each row (expanded + compact): Pencil (inline title edit with
PATCH /v1/conversations/{id}), Clipboard (copy title+preview), Trash (5s undo
delete). updateLocalConversationTitle used for local rows.
Folder strip always visible: + New folder creates via POST /v1/folders; × on
folder pill deletes via DELETE /v1/folders/{id} with optimistic removal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y tabs, doc updates
Rewind: date picker (type=date, max=today) fetches rewindFrames for any past
date; Markdown export downloads omi-rewind-{date}.md with ## timestamp sections
per frame; JSON export filename now uses selected date not today.
Memories: category filter tabs strip above memory grid; tabs computed from
live memory.category values; persists across manage mode; BrainGraph now
interactive (nodes clickable).
Docs: TRACK1_REMAINING_SOURCE_TRACE.md updated to reflect all implemented items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aker names
- Conversations: move conversation to folder (PATCH /v1/conversations/{id}/folder),
copy shareable link (visibility=shared + h.omi.me URL), multi-select merge
(POST /v1/conversations/merge), folder picker dropdown in both compact and expanded rows
- Memories: inline edit via pencil button (PATCH /v3/memories/{id}?value=),
cancel/save/Ctrl+Enter, optimistic update via useMemories.editMemory
- ConversationDetail: speaker display names resolved from people[] returned by
GET /v1/conversations/{id} — shows person name instead of SPEAKER_00 when known
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…assignment - BYOK: add byokKeys to preferences; inject X-BYOK-* headers on every omiApi request; new API Keys settings tab with 4 masked inputs, SHA-256 fingerprint activation (POST /v1/users/me/byok-active) and deactivation - Chat audio: paperclip button in chat bar opens file picker; uploads audio to POST /v2/voice-messages as multipart; streams SSE response (message: + data:) into the chat thread with live transcript update - Speaker assignment: click speaker chip in ConversationDetail to open person picker; fetches GET /v1/users/people; calls PATCH assign-speaker endpoint; supports creating new people inline; updates display names immediately - Support tab: add Check for Updates row linking to GitHub releases page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tail, audio bar button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ignment done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cker - Focus: manual Pomodoro timer + Rewind-powered app time breakdown (focus vs distraction by category heuristic); session history in localStorage - Notifications: new Settings tab consolidating insight notification settings (interval, style, denylist) + recording-saved Web Notification toggle - Devices: honest Settings tab listing supported Omi device families with clear "BLE not yet available on Windows" message and mobile-app workaround - Updates: GitHub API release checker in Support tab — shows available version or "up to date"; falls back to Releases link; no electron-updater dep required - Recording-saved notification fires via Web Notification API after conversation is saved (respects notifyOnRecordingSaved preference) - Add Focus to sidebar nav (Target icon, between Tasks and Rewind) - typecheck: PASS; build:win: PASS (✓ built in 11.43s) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…/Updates done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Focus: LLM-based focus/distracted/neutral classification via existing Gemini proxy (geminiClient.ts + insightActivity.ts) with heuristic fallback. Periodic analysis loop, observation history, sustained-distraction Web Notification. Configurable via Notifications tab (interval, distraction alert toggle). BLE: Web Bluetooth requestDevice() in DevicesTab with runtime feature detection. Main process registers setPermissionCheckHandler + setDevicePermissionHandler for bluetooth and handles select-bluetooth-device with a native Electron dialog. Scan button visible only when navigator.bluetooth is available. Notifications: added Focus analysis section (enable, interval, distraction alert). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… screenshots Adds three-tier focus classification to match macOS FocusAssistant.swift: 1. Vision tier — selects 1-2 most recent Rewind frames with stored JPEGs, fetches them via existing validated rewind:frameImage IPC, sends as inlineData parts to Gemini Vision with 8 s total timeout. Returns visual_evidence field (max 20 words describing what Gemini sees). In-memory cache keyed by frame ts+path avoids redundant Gemini calls. 2. Text/OCR tier — summarizeActivity() → Gemini text prompt (existing path). 3. Heuristic tier — keyword match on exe/app name, no network. New preference focusVisionEnabled (default off) controls tier 1. Toggle exposed in Notifications → Focus analysis → "Screenshot vision analysis". Focus.tsx shows method badge (Vision / Text-OCR / Heuristic), visualEvidence description, and a fallback note when vision is enabled but fell through. Typecheck and build:win both pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updates TRACK1_REMAINING_SOURCE_TRACE.md and TRACK1_SWIFT_PARITY.md to reflect the three-tier focus engine (Vision → Text-OCR → Heuristic), the vision toggle in NotificationsTab, and the method/visualEvidence display in Focus.tsx. Removes the ✗ "no ML pipeline" entry; replaces with ✓ row pointing to focusEngine.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DevicesTab — move from discovery-only to full connect flow: - requestDevice with optionalServices: [battery_service, device_information] so standard GATT services are accessible post-connection - gatt.connect() with phase state machine: scanning → connecting → reading → connected; gattserverdisconnected event fires → disconnected phase - Battery Service (0x180F / 0x2A19): read level, show percentage; show "Battery unavailable" if service absent (battery === -1 sentinel) - Device Information Service (0x180A): read manufacturer_name_string + model_number_string via TextDecoder; silently skip missing characteristics - Disconnect button calls gatt.disconnect(); cleans up event listener - Persist last device name/id/seenAt to localStorage (omi.ble.lastDevice.v1) - Local Web Bluetooth type stubs (dom lib doesn't include these) - Minimal type stubs: BleDevice/BleServer/BleService/BleChar/BleApi SupportTab — UX improvements since electron-updater install is blocked: - Show "Installed: X.X.X" in all update states for quick comparison - When update available: show both installed and latest version + note "Native auto-install unavailable until release feed is configured" - "Recheck" button always visible alongside Download link when available Blocker documented: npm junction C:\Program Files\nodejs\node_modules\npm still points to nvm\v22.9.0 even with Node v24.16.0 active; npm install fails with "Class extends value undefined" (minipass-flush/Node compat). electron-updater not installed; GitHub API checker remains the mechanism. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ate UX TRACK1_REMAINING_SOURCE_TRACE.md: - Section 6: Devices now DONE — Web Bluetooth connect + Battery Service (0x180F/0x2A19) + Device Information Service (0x180A) read; disconnect; last-device localStorage. Source-traced to iOS OmiBleManager (CBUUID 2A19) and Android OmiBleManager (UUID 00002a19-...). - Section 15: Update auto-install blocker documented exactly — npm junction at C:\Program Files\nodejs\node_modules\npm still routes to nvm v22.9.0. - Summary table updated: BLE connect row ✓; auto-update row updated. TRACK1_SWIFT_PARITY.md: - Settings section: Remaining gaps narrowed; documents what was added (Notifications tab, Devices connect+battery, Support version compare). TRACK1_SUBMISSION_CHECKLIST.md: - Known Limitations: BLE row updated to reflect connect+battery support. - Known Limitations: explicit native auto-update blocker row added. - Manual test steps: added Devices scan/disconnect and Support check/recheck. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce, copy actions 7 small parity wins, all source-backed: Rewind keyboard shortcuts (macOS RewindPage parity): - ArrowLeft / ArrowRight: step one frame back / forward (uses mutable ref updated each render to avoid stale closure on the single registered listener) - Space: toggle play/pause (today timeline only) - Ctrl+F: toggle search bar (macOS Cmd+F equivalent on Windows) - Escape: close search bar when open RewindPlayer fullscreen + OCR copy: - Fullscreen overlay extracted to FullscreenOverlay component; adds keydown listener that closes on Escape (mounted only while expanded to avoid leak) - OcrPanel: Copy button with ✓ Copied feedback; uses navigator.clipboard Settings tab persistence: - Active tab saved to localStorage 'omi.settings.lastTab' on every tab select - Restored on Settings mount (validated against known tab ids) Last-route persistence (App.tsx): - Current pathname saved to localStorage 'omi.lastRoute' on every route change (skips /settings and /home which are implicit start states) - Restored on AppShellInner mount after consuming pending route from onboarding Window bounds persistence (main/index.ts): - Saves getBounds() to userData/main-window-bounds.json on resize and move (skips when maximized/minimized/fullscreen) - Reads saved bounds on createWindow() to restore position and size Sidebar Ctrl+1–9 keyboard navigation: - useNavigate + keydown handler maps Ctrl+digit to navItems[digit-1].to - Guards: ctrlKey only, no alt/meta/shift, no active input, digit in range Chat message copy on hover (ChatMessages.tsx): - CopyMsgButton appears on `group-hover` below each completed assistant bubble - Not shown while streaming (isStreaming guard); opacity-0 → opacity-100 on hover - ✓ Copied feedback with 1.5 s timeout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rity - SettingsGear component (⚙ button, left side of header) opens Settings in the main window via overlay:openMainRoute — mirrors macOS gear icon - ResizeGrip opacity raised from 20% → 50% so the three-dot handle is visible, matching the macOS FloatingControlBarView ResizeHandleView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…S parity - Profile menu popover: clicking account row shows a floating menu with Refer a Friend (affiliate.omi.me), Discord, Settings — mirrors macOS profileMenuPopover with identical options - MoreHorizontal ellipsis icon in account row matches macOS ellipsis - Update available widget: checks GitHub releases API on startup; shows purple pulsing card with version number when newer build exists (same glow animation as macOS sidebar updateAvailableWidget) - updateGlow @Keyframes in globals.css Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Filter dismissed insights out by default (dismissed !== 0 hidden) - Show dismissed toggle in PageHeader actions — matches macOS menu option - Dismissed count shown in subtitle when filter is active Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create audioAnalyser.ts singleton; wire AnalyserNode into the omiListenClient mic pipeline between the source node and the ScriptProcessor. Set/clear the global analyser on session start/stop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace CSS-animated bars with requestAnimationFrame loop reading real mic amplitude from the global AnalyserNode. Add lock icons on nav items when currentTierLevel (omi.tier.level in localStorage) is below the item's required tier — mirrors macOS onboarding tier flow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New LiveTranscriptPanel component mirrors macOS LiveTranscriptPanel: floats bottom-right during recording, shows speaker-labeled bubbles (You = accent color, others = neutral), elapsed timer, collapse/dismiss controls, and auto-scrolls to bottom on new segments. Mounted in App alongside other background hosts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ date grouping macOS parity: ChatSessionsSidebar (220px, matches Swift app width) groups past chat sessions by Today / Yesterday / This Week / This Month. New Chat button, starred filter, inline search, per-row rename/star/delete. Starred state in localStorage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reload + persistence Replaces single-lifetime useChat for the Chat page. Reloads history by ID when sessionId prop changes; streams /v2/messages SSE; persists thread as kind='chat' local conversation with messages array. Returned sessionTitle tracks the stored title. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eSessionChat Rewrites Chat.tsx to use ChatSessionsSidebar (left rail) + useSessionChat (main area). Active session persisted to localStorage. New session button creates a fresh ID. Suggested prompts on empty state match macOS first-launch experience. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve recording Floating bottom-left panel (mirrors macOS LiveNotesView). AI mode calls /v2/chat/completions every 50 spoken words to generate a concise note. Manual input field + delete per note. Collapse toggle, dismiss, AI-on/off badge. Resets on each new session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speaker labels in the live transcript are now clickable — clicking opens an inline input to rename "Speaker 1" → "Alice". The name applies to all existing and future bubbles for that speaker (session-scoped). LiveNotesPanel mounted in AppShellInner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches macOS ChatLabView. Two prompt editors (floating bar + main system prompt), persistent test question list, Run All → calls /v2/messages per question + AI auto-grades responses 0-5, human star rating, summary avg scores. Generate Improved Prompt uses eval results to call backend for a suggested next version. Version history in localStorage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, Claude, Agents/MCP Matches macOS MemoryExportDestinationSheet. Export button in Memories header opens a two-pane modal: destination list (Obsidian, Notion, ChatGPT, Claude, Agents/MCP) on the left, per-destination panel on the right. Obsidian: copy Markdown + open obsidian:// URI. Notion/ChatGPT/Claude: copy context prompts. Agents: MCP server URL + setup prompt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After each AI response, 3 contextual follow-up chips appear (generated via /v2/messages). Clicking a chip fires it as the next message. Chips clear on new input or session switch. Send button no longer disabled while streaming — follow-up sends are allowed mid-response, matching macOS pendingFollowUpText behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…typography Accent: #5b02e0 → #8B5CF6 (OmiColors.purplePrimary exact match) Nav selection bg: #141414 → #252525 (OmiColors.backgroundTertiary) Chat user bubble: glass dark → solid #43389F (OmiColors.userBubble) Chat assistant bubble: glass-subtle → bg-[#252525]/95 (macOS backgroundTertiary @ 95%) Bubble corner radius: 16px → 20px continuous (matches SwiftUI .cornerRadius(20, .continuous)) Bubble padding: px-4 py-3 → px-[14px] py-[10px] (matches .padding(.horizontal,14).padding(.vertical,10)) Message gap: 8px → 18px (matches LazyVStack(spacing: 18)) Typing indicator: static '…' → animated 3-dot bounce (mirrors TypingIndicator component) Copy button: icon+text label → icon-only (matches macOS doc.on.doc icon-only) Message truncation: none → 500-char limit with "Show more" expand Sidebar width: 240px → 260px (matches SidebarView.expandedWidth = 260) Nav icon size: 16px → 18px (matches .scaledFont(size: 18)) Nav label font: 14px → 13px (matches .scaledFont(size: 13, weight: .medium)) Nav horizontal padding: 10px → 16px (matches macOS .padding(.horizontal, 16)) Nav transition: 150ms → 200ms easeInOut (matches .animation(.easeInOut(duration: 0.2))) Audio bar width: 2px → 3px (matches macOS bar thickness) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tyle status card - Sidebar collapse animation: ease-out → ease-in-out (matches macOS .easeInOut(duration:0.2)) - Device widget: shows battery % with macOS-exact color coding (<20% red, 20-40% orange, 40%+ green); DevicesTab now saves batteryLevel to LAST_DEVICE_KEY so sidebar reads it on next open - Focus Analysis Card: replaced surface-card glass with macOS-exact colored overlay (bg-[status]/8%, border [status]/20%, rounded-[16px]); status display upgraded to macOS 56×56 circle icon + 20px semibold title + app name + 12px pulse dot; ObsRow gets rounded-lg + bg-[#252525]/40 default / bg-[#252525] hover matching macOS backgroundTertiary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DevicesTab now persists batteryLevel in LAST_DEVICE_KEY so the sidebar widget can display it color-coded (<20% red, 20-40% orange, 40%+ green), matching macOS batteryColor() and batteryIconName() sidebar logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches macOS FocusStatusCard exactly: - Status card bg: Color.[status].opacity(0.08), border: opacity(0.2), cornerRadius(16) - Hero row: 56×56 filled circle (opacity 0.2) + Eye/EyeOff/Clock icon at 24px + 20pt semibold status title + 13pt app name + 12×12 pulse dot - ObsRow session rows: rounded-lg, bg-[#252525]/40 default, bg-[#252525] on hover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation transcript drawer + NameSpeakerSheet, overlay TTS + proactive notifications - Tasks: 19-tag filter panel (Status/Date/Source/Priority/Category groups, AND across groups OR within), replaces basic open/done tabs - Tasks: TaskDetailModal (550px) with status/priority/category chips, core fields card, tags, source conversation link, delete confirmation - Tasks: DailyTaskSheet (450px) matching macOS DailyTaskCreationSheet — description + priority chips + create button - Tasks: task rows grouped by due bucket (Overdue/Today/Tomorrow/Upcoming/No date) - ConversationDetail: 450px right-side slide-in TranscriptDrawer button in header - ConversationDetail: NameSpeakerSheet modal (400px) replacing inline picker — You/person chips + Add Person + allSegments toggle - Overlay: Web Speech API TTS reads each AI reply aloud (window.speechSynthesis) - Overlay: ProactiveCard notification (bell + title + body + Execute/Dismiss) via overlay:notification IPC channel - tailwind.config: add animate-slide-in-right keyframe Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r speaker fix - People.tsx: new /people page matching macOS People library — list with avatar initials + color, conversation count, create (inline form, duplicate check), delete with confirm, search filter - Sidebar: added People nav item (Users icon, tier 3, between Tasks and Focus) - MainViews: wired PeoplePanel to /people route - Tasks filter: expanded to 34 tags across 6 groups — Status(4: Open/In Progress/Done/Canceled), Date(4: Due Today/Last 7 Days/This Month/Overdue), Source(5: From Conversation/Manual/iOS/Voice/Friend), Priority(3), Category(10), Origin(6: iOS Mic/Omi Recording/Friend Device/Computer/Web/No Device) - OverlayApp: replaced single OmiPill with multi-pill row (Omi + Memory + Screen) matching macOS FloatingBarAgentPillsView - ConversationDetail: fixed NameSpeakerSheet "You" assignment — uses is_user assign_type with graceful fallback + populates personNames['user']='You' for immediate display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The overlay BrowserWindow loaded stale module code via HMR, causing the done: base64 payload to be decoded with plain atob() (Latin-1) instead of TextDecoder (UTF-8). The garbled cleanTextFromDone then replaced the correctly-streamed assistantText at stream-end. Fix: remove all done: post-processing from both useChat and useSessionChat. Streaming data: chunks are decoded by the browser's native SSE UTF-8 layer and are always correct — no replacement needed. Also remove the Anthropic BYOK fast-path from useChat so the overlay always uses Omi /v2/messages with full server-side RAG. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove [chat:perf] timing logs (fired in all builds), overlay console-message diagnostic routing, and add *.tsbuildinfo to .gitignore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bling Parse the done: payload from Omi SSE for citation metadata only using TextDecoder (correct UTF-8). Never use done: text to replace streaming text — streaming data: chunks decoded by the browser native UTF-8 layer are always correct. The old text replacement was the source of garbled emoji. Citations now appear in both overlay (useChat) and main Chat tab (useSessionChat). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove electron.vite.config.*.mjs (vite temp file with timestamp in name) and tsconfig.node.tsbuildinfo (TS incremental cache) from git tracking. Both were accidentally committed; *.tsbuildinfo was already gitignored but git rm --cached was missed for the node variant. Add electron.vite.config.*.mjs pattern to prevent recurrence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… parsing and SSE line parsing Extract parseDonePayload and parseSseLine from inline hook closures into a pure lib/chatSse.ts module (no React, no Firebase) so they can be unit tested. Both useChat and useSessionChat now import from chatSse, removing the code duplication. Tests cover: UTF-8 emoji roundtrip (the regression), all fallback id/title/ preview/emoji fields, key aliasing (memories/citations/sources), malformed input resilience, __CRLF__ replacement, done:/think:/data: line routing, and preview truncation at 120 chars. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
28 issues found across 81 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/windows/src/renderer/src/components/ui/GoalCelebration.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/ui/GoalCelebration.tsx:48">
P2: Timeouts are not tracked/cleared, so overlapping celebrations can be interrupted by stale callbacks. This also leaves pending state updates after unmount.</violation>
</file>
<file name="desktop/windows/src/renderer/src/pages/Help.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/pages/Help.tsx:18">
P2: Wrong Crisp query keys (`email`, `nickname`) prevent user identity prefill in the help chat. Use `user_email` and `user_nickname` to match Crisp embed parameters.</violation>
</file>
<file name="desktop/windows/src/main/ipc/kg.ts">
<violation number="1" location="desktop/windows/src/main/ipc/kg.ts:58">
P2: Failed worker dispatch drops the current graph instead of retrying or surfacing an error. This can silently lose a save request on worker startup/postMessage failures.</violation>
<violation number="2" location="desktop/windows/src/main/ipc/kg.ts:108">
P2: `kg:saveGraph` now acknowledges success before the graph is written, so callers can show a successful rebuild even when persistence is still pending or failed.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/layout/Sidebar.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/layout/Sidebar.tsx:239">
P2: Version ordering is incorrect because semver strings are compared lexicographically. This can show/hide the update card for the wrong release.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx:10">
P2: Optional-chained `getAppVersion` is followed by an unconditional `.then`, which can throw when the bridge method is unavailable.</violation>
<violation number="2" location="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx:17">
P2: The update check reports success even when `checkForUpdates` is unavailable, producing a false “up to date” status.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/preferences.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/preferences.ts:44">
P1: Raw BYOK API keys are being persisted in localStorage. Move secret storage to OS-backed secure storage (e.g., keychain/credential vault) and keep preferences limited to non-secret metadata.</violation>
</file>
<file name="desktop/windows/src/main/ipc/db.ts">
<violation number="1" location="desktop/windows/src/main/ipc/db.ts:76">
P1: `synchronous = NORMAL` is now unconditional, which weakens durability for user conversation data in `omi.db`. On OS/power-loss crashes, the latest committed write can be lost.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/chatSse.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/chatSse.ts:34">
P2: Unsafe string casts can throw on malformed citation fields and cause the whole done payload to be discarded. Guard field types before calling `.trim()` so one bad item does not erase all parsed citations.</violation>
</file>
<file name="backend/routers/chat.py">
<violation number="1" location="backend/routers/chat.py:348">
P2: Fallback done-path skips DB persistence for AI replies. Users can see a streamed reply that disappears from stored chat history.</violation>
</file>
<file name="desktop/windows/scripts/copy-koffi-native.mjs">
<violation number="1" location="desktop/windows/scripts/copy-koffi-native.mjs:21">
P1: Binary selection is tied to build host, not packaging target. This can copy the wrong koffi.node when building `--win --x64` from a different host arch/platform.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/omiListenClient.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/omiListenClient.ts:80">
P2: Global audio analyser is set for all sources; system capture can replace mic analyser and drive mic UI from wrong input.</violation>
<violation number="2" location="desktop/windows/src/renderer/src/lib/omiListenClient.ts:120">
P2: Analyser cleanup is not ownership-safe; one session can null out another live session's analyser.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/recording/RecordingStatusBar.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/recording/RecordingStatusBar.tsx:64">
P2: Status text can show stale transcript during manual recording. Only use live snippet while `liveStatus` is active; otherwise fall back to the status label.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/chat/ChatMessages.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/chat/ChatMessages.tsx:88">
P2: Truncated preview slices raw code units and can cut an emoji surrogate pair. Use the same boundary snap used by streaming reveal before slicing.</violation>
</file>
<file name="desktop/windows/src/renderer/src/App.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/App.tsx:42">
P2: Last-route restore is effectively broken on normal startup because `/` is persisted first and replaces the prior saved route.</violation>
</file>
<file name="desktop/windows/src/preload/index.ts">
<violation number="1" location="desktop/windows/src/preload/index.ts:218">
P2: `onNotification` listens on an IPC channel that is never emitted by main. Proactive overlay notifications will never fire.</violation>
</file>
<file name="desktop/windows/src/main/index.ts">
<violation number="1" location="desktop/windows/src/main/index.ts:248">
P2: Bounds persistence performs sync file writes on every resize/move event. This can introduce visible jank while moving or resizing the window.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/overlay/OverlayApp.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/overlay/OverlayApp.tsx:174">
P2: TTS dedup keys on message text instead of message identity. Repeated assistant replies with the same content will not be spoken on later turns.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/conversations/NameSpeakerSheet.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/conversations/NameSpeakerSheet.tsx:58">
P2: `commitNewPerson` lacks an in-flight guard, so repeated Enter presses can fire duplicate create requests.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Partial review: This PR has more than 50 files, so cubic reviewed the highest-priority files first. During the trial, paid plans get a higher file limit.
You can try an ultrareview to bypass the file limit, comment @cubic-dev-ai ultrareview. Learn more.
Re-trigger cubic
| onboardingCompletedAt?: number | ||
| // BYOK (Bring Your Own Keys) — stored locally; SHA-256 fingerprints are sent to | ||
| // the backend on activation, but actual keys only leave this device as request headers. | ||
| byokKeys?: { |
There was a problem hiding this comment.
P1: Raw BYOK API keys are being persisted in localStorage. Move secret storage to OS-backed secure storage (e.g., keychain/credential vault) and keep preferences limited to non-secret metadata.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/renderer/src/lib/preferences.ts, line 44:
<comment>Raw BYOK API keys are being persisted in localStorage. Move secret storage to OS-backed secure storage (e.g., keychain/credential vault) and keep preferences limited to non-secret metadata.</comment>
<file context>
@@ -39,6 +39,50 @@ export type Preferences = {
onboardingCompletedAt?: number
+ // BYOK (Bring Your Own Keys) — stored locally; SHA-256 fingerprints are sent to
+ // the backend on activation, but actual keys only leave this device as request headers.
+ byokKeys?: {
+ openai?: string
+ anthropic?: string
</file context>
| // (may lose the last committed transaction on OS power-loss; acceptable for | ||
| // this derived cache). Previously bench-only; now unconditional. | ||
| db.pragma('journal_mode = WAL') | ||
| db.pragma('synchronous = NORMAL') |
There was a problem hiding this comment.
P1: synchronous = NORMAL is now unconditional, which weakens durability for user conversation data in omi.db. On OS/power-loss crashes, the latest committed write can be lost.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/ipc/db.ts, line 76:
<comment>`synchronous = NORMAL` is now unconditional, which weakens durability for user conversation data in `omi.db`. On OS/power-loss crashes, the latest committed write can be lost.</comment>
<file context>
@@ -68,12 +68,12 @@ function get(): Database.Database {
+ // (may lose the last committed transaction on OS power-loss; acceptable for
+ // this derived cache). Previously bench-only; now unconditional.
+ db.pragma('journal_mode = WAL')
+ db.pragma('synchronous = NORMAL')
// Migrate away the incompatible local_kg_* schema from the parked KG experiment.
dropIfMissingColumn(db, 'local_kg_nodes', 'summary')
</file context>
| const root = join(__dirname, '..') | ||
| const pnpmStore = join(root, 'node_modules', '.pnpm') | ||
|
|
||
| const platform = process.platform // e.g. win32 |
There was a problem hiding this comment.
P1: Binary selection is tied to build host, not packaging target. This can copy the wrong koffi.node when building --win --x64 from a different host arch/platform.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/scripts/copy-koffi-native.mjs, line 21:
<comment>Binary selection is tied to build host, not packaging target. This can copy the wrong koffi.node when building `--win --x64` from a different host arch/platform.</comment>
<file context>
@@ -0,0 +1,61 @@
+const root = join(__dirname, '..')
+const pnpmStore = join(root, 'node_modules', '.pnpm')
+
+const platform = process.platform // e.g. win32
+const arch = process.arch // e.g. x64
+const triplet = `${platform}_${arch}` // e.g. win32_x64
</file context>
P1 - ChatSessionsSidebar: add optional chain before .slice() on possibly-undefined trim() result P1 - index.ts: replace blanket permission-allow-all with explicit allowlist (bluetooth, media, display-capture, notifications) P2 - ChatSessionsSidebar: wrap title update in try/finally so editingId always clears on IPC failure P2 - ChatSessionsSidebar: replace outer <button> with <div role=button> to fix invalid button-in-button nesting P2 - QuickConversationsWidget: guard /home pathname effect with userId check before fetch P3 - NameSpeakerSheet: fix speaker label regex — SPEAKER_00 was producing empty index; now uses capture group P3 - NameSpeakerSheet: scope duplicate-name error to addingNew state only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/windows/src/renderer/src/components/ui/GoalCelebration.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/ui/GoalCelebration.tsx:48">
P2: Timeouts are not tracked/cleared, so overlapping celebrations can be interrupted by stale callbacks. This also leaves pending state updates after unmount.</violation>
</file>
<file name="desktop/windows/src/renderer/src/pages/Help.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/pages/Help.tsx:18">
P2: Wrong Crisp query keys (`email`, `nickname`) prevent user identity prefill in the help chat. Use `user_email` and `user_nickname` to match Crisp embed parameters.</violation>
</file>
<file name="desktop/windows/src/main/ipc/kg.ts">
<violation number="1" location="desktop/windows/src/main/ipc/kg.ts:58">
P2: Failed worker dispatch drops the current graph instead of retrying or surfacing an error. This can silently lose a save request on worker startup/postMessage failures.</violation>
<violation number="2" location="desktop/windows/src/main/ipc/kg.ts:108">
P2: `kg:saveGraph` now acknowledges success before the graph is written, so callers can show a successful rebuild even when persistence is still pending or failed.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/layout/Sidebar.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/layout/Sidebar.tsx:239">
P2: Version ordering is incorrect because semver strings are compared lexicographically. This can show/hide the update card for the wrong release.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx:10">
P2: Optional-chained `getAppVersion` is followed by an unconditional `.then`, which can throw when the bridge method is unavailable.</violation>
<violation number="2" location="desktop/windows/src/renderer/src/components/settings/tabs/AboutTab.tsx:17">
P2: The update check reports success even when `checkForUpdates` is unavailable, producing a false “up to date” status.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/preferences.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/preferences.ts:44">
P1: Raw BYOK API keys are being persisted in localStorage. Move secret storage to OS-backed secure storage (e.g., keychain/credential vault) and keep preferences limited to non-secret metadata.</violation>
</file>
<file name="desktop/windows/src/main/ipc/db.ts">
<violation number="1" location="desktop/windows/src/main/ipc/db.ts:76">
P1: `synchronous = NORMAL` is now unconditional, which weakens durability for user conversation data in `omi.db`. On OS/power-loss crashes, the latest committed write can be lost.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/chatSse.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/chatSse.ts:34">
P2: Unsafe string casts can throw on malformed citation fields and cause the whole done payload to be discarded. Guard field types before calling `.trim()` so one bad item does not erase all parsed citations.</violation>
</file>
<file name="backend/routers/chat.py">
<violation number="1" location="backend/routers/chat.py:348">
P2: Fallback done-path skips DB persistence for AI replies. Users can see a streamed reply that disappears from stored chat history.</violation>
</file>
<file name="desktop/windows/scripts/copy-koffi-native.mjs">
<violation number="1" location="desktop/windows/scripts/copy-koffi-native.mjs:21">
P1: Binary selection is tied to build host, not packaging target. This can copy the wrong koffi.node when building `--win --x64` from a different host arch/platform.</violation>
</file>
<file name="desktop/windows/src/renderer/src/lib/omiListenClient.ts">
<violation number="1" location="desktop/windows/src/renderer/src/lib/omiListenClient.ts:80">
P2: Global audio analyser is set for all sources; system capture can replace mic analyser and drive mic UI from wrong input.</violation>
<violation number="2" location="desktop/windows/src/renderer/src/lib/omiListenClient.ts:120">
P2: Analyser cleanup is not ownership-safe; one session can null out another live session's analyser.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/recording/RecordingStatusBar.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/recording/RecordingStatusBar.tsx:64">
P2: Status text can show stale transcript during manual recording. Only use live snippet while `liveStatus` is active; otherwise fall back to the status label.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/chat/ChatMessages.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/chat/ChatMessages.tsx:88">
P2: Truncated preview slices raw code units and can cut an emoji surrogate pair. Use the same boundary snap used by streaming reveal before slicing.</violation>
</file>
<file name="desktop/windows/src/renderer/src/App.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/App.tsx:42">
P2: Last-route restore is effectively broken on normal startup because `/` is persisted first and replaces the prior saved route.</violation>
</file>
<file name="desktop/windows/src/preload/index.ts">
<violation number="1" location="desktop/windows/src/preload/index.ts:218">
P2: `onNotification` listens on an IPC channel that is never emitted by main. Proactive overlay notifications will never fire.</violation>
</file>
<file name="desktop/windows/src/main/index.ts">
<violation number="1" location="desktop/windows/src/main/index.ts:248">
P2: Bounds persistence performs sync file writes on every resize/move event. This can introduce visible jank while moving or resizing the window.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/overlay/OverlayApp.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/overlay/OverlayApp.tsx:174">
P2: TTS dedup keys on message text instead of message identity. Repeated assistant replies with the same content will not be spoken on later turns.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/conversations/NameSpeakerSheet.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/conversations/NameSpeakerSheet.tsx:58">
P2: `commitNewPerson` lacks an in-flight guard, so repeated Enter presses can fire duplicate create requests.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
- saveEdit: only close editor on success or empty input; keep open on IPC error so the user can retry without losing their unsaved title text - index.ts: add clipboard-sanitized-write to permission allowlist to prevent copy-to-clipboard actions from silently breaking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
This PR brings the Windows Electron desktop app to Track 1 parity with the macOS app. All features were built against the existing Omi backend with no API changes required.
Features added
useSessionChatNameSpeakerSheet)DailyTaskSheet)Bug fixes
done:base64 payload was decoded withatob()(Latin-1), then used to replace the streaming text. Fixed by ignoringdone:text entirely; streamingdata:chunks decoded by the browser's native UTF-8 layer are used directly.done:payload is now parsed withTextDecoder(correct UTF-8) for citation metadata only, never for text replacement. Both the overlay (useChat) and main Chat tab (useSessionChat) are fixed./v2/messagesnow.Tests
parseDonePayloadandparseSseLineintolib/chatSse.ts(pure, no React/Firebase)memories/citations/sources,memory_id,conversation_id,structured.title/emoji), malformed input, preview truncation,__CRLF__replacement,done:/think:/data:line routingChores
electron.vite.config.*.mjs,tsconfig.node.tsbuildinfo).gitignoreto cover both patterns