feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
import { useState, useRef, useCallback, useEffect } from 'react'
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
import { X, Eye, EyeSlash, GithubLogo, SignOut } from '@phosphor-icons/react'
|
2026-02-28 14:02:38 +01:00
|
|
|
import { GitHubDeviceFlow } from './GitHubDeviceFlow'
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
import type { Settings } from '../types'
|
2026-02-22 13:38:18 +01:00
|
|
|
|
|
|
|
|
interface SettingsPanelProps {
|
|
|
|
|
open: boolean
|
|
|
|
|
settings: Settings
|
|
|
|
|
onSave: (settings: Settings) => void
|
|
|
|
|
onClose: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface KeyFieldProps {
|
|
|
|
|
label: string
|
|
|
|
|
placeholder: string
|
|
|
|
|
value: string
|
|
|
|
|
onChange: (value: string) => void
|
|
|
|
|
onClear: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function KeyField({ label, placeholder, value, onChange, onClear }: KeyFieldProps) {
|
|
|
|
|
const [revealed, setRevealed] = useState(false)
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
|
|
|
<label style={{ fontSize: 12, fontWeight: 500, color: 'var(--foreground)' }}>{label}</label>
|
|
|
|
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
|
|
|
<input
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
type={revealed ? 'text' : 'password'}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={e => onChange(e.target.value)}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
className="w-full border border-border bg-transparent text-foreground rounded"
|
|
|
|
|
style={{ fontSize: 13, padding: '8px 60px 8px 10px', outline: 'none', fontFamily: 'inherit' }}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
data-testid={`settings-key-${label.toLowerCase().replace(/\s+/g, '-')}`}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ position: 'absolute', right: 8, display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
|
|
|
{value && (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
className="border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground"
|
|
|
|
|
onClick={() => setRevealed(r => !r)}
|
|
|
|
|
title={revealed ? 'Hide key' : 'Reveal key'}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
{revealed ? <EyeSlash size={14} /> : <Eye size={14} />}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground"
|
|
|
|
|
onClick={() => { onClear(); setRevealed(false) }}
|
|
|
|
|
title="Clear key"
|
|
|
|
|
type="button"
|
|
|
|
|
data-testid={`clear-${label.toLowerCase().replace(/\s+/g, '-')}`}
|
|
|
|
|
>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 20:01:29 +01:00
|
|
|
// --- GitHub OAuth Section ---
|
|
|
|
|
|
2026-02-23 19:56:30 +01:00
|
|
|
interface GitHubSectionProps {
|
|
|
|
|
githubUsername: string | null
|
|
|
|
|
githubToken: string | null
|
|
|
|
|
onConnected: (token: string, username: string) => void
|
|
|
|
|
onDisconnect: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GitHubSection({ githubUsername, githubToken, onConnected, onDisconnect }: GitHubSectionProps) {
|
|
|
|
|
const isConnected = !!githubToken && !!githubUsername
|
|
|
|
|
|
2026-02-28 14:02:38 +01:00
|
|
|
if (isConnected) {
|
|
|
|
|
return <GitHubConnectedRow username={githubUsername!} onDisconnect={onDisconnect} />
|
2026-02-23 19:56:30 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 14:02:38 +01:00
|
|
|
return <GitHubDeviceFlow onConnected={onConnected} />
|
2026-02-23 20:01:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GitHubConnectedRow({ username, onDisconnect }: { username: string; onDisconnect: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center gap-2 border border-border rounded px-3 py-2 flex-1"
|
|
|
|
|
style={{ minHeight: 36 }}
|
|
|
|
|
data-testid="github-connected"
|
|
|
|
|
>
|
|
|
|
|
<GithubLogo size={16} weight="fill" style={{ color: 'var(--foreground)' }} />
|
|
|
|
|
<span style={{ fontSize: 13, color: 'var(--foreground)', fontWeight: 500 }}>{username}</span>
|
|
|
|
|
<span style={{ fontSize: 12, color: 'var(--muted-foreground)' }}>Connected</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
className="border border-border bg-transparent text-muted-foreground rounded cursor-pointer hover:text-foreground hover:border-foreground"
|
|
|
|
|
style={{ fontSize: 12, padding: '6px 12px', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
|
|
|
onClick={onDisconnect}
|
|
|
|
|
title="Disconnect GitHub account"
|
|
|
|
|
data-testid="github-disconnect"
|
|
|
|
|
>
|
|
|
|
|
<SignOut size={14} />
|
|
|
|
|
Disconnect
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Settings Panel ---
|
|
|
|
|
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
export function SettingsPanel({ open, settings, onSave, onClose }: SettingsPanelProps) {
|
2026-02-22 13:38:18 +01:00
|
|
|
if (!open) return null
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
return <SettingsPanelInner settings={settings} onSave={onSave} onClose={onClose} />
|
2026-02-23 08:53:43 +01:00
|
|
|
}
|
|
|
|
|
|
refactor: remove theming system entirely
Remove all theme switching, creation, and management functionality
from the app — backend (Rust theme module, Tauri commands, menu items,
startup tasks, vault seeding), frontend (useThemeManager, ThemePropertyEditor,
themeSchema, command palette Appearance group, settings panel appearance
section, isDarkTheme prop chain), mock data, and all related tests.
Editor styling via theme.json/EditorTheme.css is preserved (not user theming).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:57:58 +01:00
|
|
|
function SettingsPanelInner({ settings, onSave, onClose }: Omit<SettingsPanelProps, 'open'>) {
|
2026-02-23 08:53:43 +01:00
|
|
|
const [openaiKey, setOpenaiKey] = useState(settings.openai_key ?? '')
|
|
|
|
|
const [googleKey, setGoogleKey] = useState(settings.google_key ?? '')
|
2026-02-23 19:56:30 +01:00
|
|
|
const [githubToken, setGithubToken] = useState(settings.github_token)
|
|
|
|
|
const [githubUsername, setGithubUsername] = useState(settings.github_username)
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
const [pullInterval, setPullInterval] = useState(settings.auto_pull_interval_minutes ?? 5)
|
2026-03-25 17:51:33 +01:00
|
|
|
const [updateChannel, setUpdateChannel] = useState(settings.update_channel ?? 'stable')
|
2026-03-25 16:10:39 +01:00
|
|
|
const [crashReporting, setCrashReporting] = useState(settings.crash_reporting_enabled ?? false)
|
|
|
|
|
const [analytics, setAnalytics] = useState(settings.analytics_enabled ?? false)
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
// Auto-focus first input when settings panel opens
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
const input = panelRef.current?.querySelector('input')
|
|
|
|
|
input?.focus()
|
|
|
|
|
}, 50)
|
|
|
|
|
return () => clearTimeout(timer)
|
|
|
|
|
}, [])
|
2026-02-23 19:56:30 +01:00
|
|
|
|
2026-02-23 20:01:29 +01:00
|
|
|
const buildSettings = useCallback((ghOverride?: { token: string | null; username: string | null }): Settings => ({
|
2026-02-23 19:56:30 +01:00
|
|
|
openai_key: openaiKey.trim() || null,
|
|
|
|
|
google_key: googleKey.trim() || null,
|
2026-02-23 20:01:29 +01:00
|
|
|
github_token: ghOverride ? ghOverride.token : (githubToken ?? null),
|
|
|
|
|
github_username: ghOverride ? ghOverride.username : (githubUsername ?? null),
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
auto_pull_interval_minutes: pullInterval,
|
2026-03-25 16:10:39 +01:00
|
|
|
telemetry_consent: (crashReporting || analytics) ? true : (settings.telemetry_consent === null ? null : false),
|
|
|
|
|
crash_reporting_enabled: crashReporting,
|
|
|
|
|
analytics_enabled: analytics,
|
|
|
|
|
anonymous_id: (crashReporting || analytics) ? (settings.anonymous_id ?? crypto.randomUUID()) : settings.anonymous_id,
|
2026-03-25 17:51:33 +01:00
|
|
|
update_channel: updateChannel === 'stable' ? null : updateChannel,
|
|
|
|
|
}), [openaiKey, googleKey, githubToken, githubUsername, pullInterval, updateChannel, crashReporting, analytics, settings.telemetry_consent, settings.anonymous_id])
|
2026-02-22 13:38:18 +01:00
|
|
|
|
|
|
|
|
const handleSave = () => {
|
2026-02-23 19:56:30 +01:00
|
|
|
onSave(buildSettings())
|
|
|
|
|
onClose()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleGitHubConnected = useCallback((token: string, username: string) => {
|
|
|
|
|
setGithubToken(token)
|
|
|
|
|
setGithubUsername(username)
|
2026-02-23 20:01:29 +01:00
|
|
|
onSave(buildSettings({ token, username }))
|
|
|
|
|
}, [onSave, buildSettings])
|
2026-02-23 19:56:30 +01:00
|
|
|
|
|
|
|
|
const handleGitHubDisconnect = useCallback(() => {
|
|
|
|
|
setGithubToken(null)
|
|
|
|
|
setGithubUsername(null)
|
2026-02-23 20:01:29 +01:00
|
|
|
onSave(buildSettings({ token: null, username: null }))
|
|
|
|
|
}, [onSave, buildSettings])
|
2026-02-22 13:38:18 +01:00
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
onClose()
|
|
|
|
|
}
|
|
|
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
handleSave()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 flex items-center justify-center z-50"
|
|
|
|
|
style={{ background: 'rgba(0,0,0,0.4)' }}
|
|
|
|
|
onClick={e => { if (e.target === e.currentTarget) onClose() }}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
data-testid="settings-panel"
|
|
|
|
|
>
|
|
|
|
|
<div
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
ref={panelRef}
|
2026-02-22 13:38:18 +01:00
|
|
|
className="bg-background border border-border rounded-lg shadow-xl"
|
|
|
|
|
style={{ width: 520, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
|
|
|
|
|
>
|
2026-02-23 20:01:29 +01:00
|
|
|
<SettingsHeader onClose={onClose} />
|
|
|
|
|
<SettingsBody
|
|
|
|
|
openaiKey={openaiKey} setOpenaiKey={setOpenaiKey}
|
|
|
|
|
googleKey={googleKey} setGoogleKey={setGoogleKey}
|
|
|
|
|
githubToken={githubToken ?? null} githubUsername={githubUsername ?? null}
|
|
|
|
|
onGitHubConnected={handleGitHubConnected} onGitHubDisconnect={handleGitHubDisconnect}
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
pullInterval={pullInterval} setPullInterval={setPullInterval}
|
2026-03-25 17:51:33 +01:00
|
|
|
updateChannel={updateChannel} setUpdateChannel={setUpdateChannel}
|
2026-03-25 16:10:39 +01:00
|
|
|
crashReporting={crashReporting} setCrashReporting={setCrashReporting}
|
|
|
|
|
analytics={analytics} setAnalytics={setAnalytics}
|
2026-02-23 20:01:29 +01:00
|
|
|
/>
|
|
|
|
|
<SettingsFooter onClose={onClose} onSave={handleSave} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SettingsHeader({ onClose }: { onClose: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center justify-between shrink-0"
|
|
|
|
|
style={{ height: 56, padding: '0 24px', borderBottom: '1px solid var(--border)' }}
|
|
|
|
|
>
|
|
|
|
|
<span style={{ fontSize: 16, fontWeight: 600, color: 'var(--foreground)' }}>Settings</span>
|
|
|
|
|
<button
|
|
|
|
|
className="border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
title="Close settings"
|
|
|
|
|
>
|
|
|
|
|
<X size={16} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SettingsBodyProps {
|
|
|
|
|
openaiKey: string; setOpenaiKey: (v: string) => void
|
|
|
|
|
googleKey: string; setGoogleKey: (v: string) => void
|
|
|
|
|
githubToken: string | null; githubUsername: string | null
|
|
|
|
|
onGitHubConnected: (token: string, username: string) => void
|
|
|
|
|
onGitHubDisconnect: () => void
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
pullInterval: number; setPullInterval: (v: number) => void
|
2026-03-25 17:51:33 +01:00
|
|
|
updateChannel: string; setUpdateChannel: (v: string) => void
|
2026-03-25 16:10:39 +01:00
|
|
|
crashReporting: boolean; setCrashReporting: (v: boolean) => void
|
|
|
|
|
analytics: boolean; setAnalytics: (v: boolean) => void
|
2026-02-23 20:01:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SettingsBody(props: SettingsBodyProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, overflow: 'auto' }}>
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>AI Provider Keys</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
|
|
|
|
|
API keys are stored locally on your device. Never sent to our servers.
|
2026-02-22 13:38:18 +01:00
|
|
|
</div>
|
2026-02-23 20:01:29 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<KeyField label="OpenAI" placeholder="sk-..." value={props.openaiKey} onChange={props.setOpenaiKey} onClear={() => props.setOpenaiKey('')} />
|
|
|
|
|
<KeyField label="Google AI" placeholder="AIza..." value={props.googleKey} onChange={props.setGoogleKey} onClear={() => props.setGoogleKey('')} />
|
2026-02-22 13:38:18 +01:00
|
|
|
|
2026-02-23 20:01:29 +01:00
|
|
|
<div style={{ height: 1, background: 'var(--border)' }} />
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>GitHub</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
|
|
|
|
|
Connect your GitHub account to clone and sync vaults.
|
2026-02-22 13:38:18 +01:00
|
|
|
</div>
|
2026-02-23 20:01:29 +01:00
|
|
|
</div>
|
2026-02-22 13:38:18 +01:00
|
|
|
|
2026-02-23 20:01:29 +01:00
|
|
|
<GitHubSection
|
|
|
|
|
githubUsername={props.githubUsername}
|
|
|
|
|
githubToken={props.githubToken}
|
|
|
|
|
onConnected={props.onGitHubConnected}
|
|
|
|
|
onDisconnect={props.onGitHubDisconnect}
|
|
|
|
|
/>
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
|
|
|
|
|
<div style={{ height: 1, background: 'var(--border)' }} />
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>Sync</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
|
|
|
|
|
Automatically pull vault changes from Git in the background.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
|
|
|
<label style={{ fontSize: 12, fontWeight: 500, color: 'var(--foreground)' }}>Pull interval (minutes)</label>
|
|
|
|
|
<select
|
|
|
|
|
value={props.pullInterval}
|
|
|
|
|
onChange={(e) => props.setPullInterval(Number(e.target.value))}
|
|
|
|
|
className="border border-border bg-transparent text-foreground rounded"
|
|
|
|
|
style={{ fontSize: 13, padding: '8px 10px', outline: 'none', fontFamily: 'inherit' }}
|
|
|
|
|
data-testid="settings-pull-interval"
|
|
|
|
|
>
|
|
|
|
|
<option value={1}>1</option>
|
|
|
|
|
<option value={2}>2</option>
|
|
|
|
|
<option value={5}>5</option>
|
|
|
|
|
<option value={10}>10</option>
|
|
|
|
|
<option value={15}>15</option>
|
|
|
|
|
<option value={30}>30</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2026-03-25 16:10:39 +01:00
|
|
|
|
|
|
|
|
<div style={{ height: 1, background: 'var(--border)' }} />
|
|
|
|
|
|
2026-03-25 17:51:33 +01:00
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>Updates</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
|
|
|
|
|
Canary builds include the latest features but may be less stable. Restart required after changing.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
|
|
|
<label style={{ fontSize: 12, fontWeight: 500, color: 'var(--foreground)' }}>Update channel</label>
|
|
|
|
|
<select
|
|
|
|
|
value={props.updateChannel}
|
|
|
|
|
onChange={(e) => props.setUpdateChannel(e.target.value)}
|
|
|
|
|
className="border border-border bg-transparent text-foreground rounded"
|
|
|
|
|
style={{ fontSize: 13, padding: '8px 10px', outline: 'none', fontFamily: 'inherit' }}
|
|
|
|
|
data-testid="settings-update-channel"
|
|
|
|
|
>
|
|
|
|
|
<option value="stable">Stable</option>
|
|
|
|
|
<option value="canary">Canary (pre-release)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ height: 1, background: 'var(--border)' }} />
|
|
|
|
|
|
2026-03-25 16:10:39 +01:00
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>Privacy & Telemetry</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
|
|
|
|
|
Anonymous data helps us fix bugs and improve Laputa. No vault content, note titles, or file paths are ever sent.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<TelemetryToggle label="Crash reporting" description="Send anonymous error reports" checked={props.crashReporting} onChange={props.setCrashReporting} testId="settings-crash-reporting" />
|
|
|
|
|
<TelemetryToggle label="Usage analytics" description="Share anonymous usage patterns" checked={props.analytics} onChange={props.setAnalytics} testId="settings-analytics" />
|
2026-02-23 20:01:29 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 16:10:39 +01:00
|
|
|
function TelemetryToggle({ label, description, checked, onChange, testId }: { label: string; description: string; checked: boolean; onChange: (v: boolean) => void; testId: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }} data-testid={testId}>
|
|
|
|
|
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} style={{ width: 16, height: 16, accentColor: 'var(--primary)' }} />
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--foreground)' }}>{label}</div>
|
|
|
|
|
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>{description}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 20:01:29 +01:00
|
|
|
function SettingsFooter({ onClose, onSave }: { onClose: () => void; onSave: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center justify-between shrink-0"
|
|
|
|
|
style={{ height: 56, padding: '0 24px', borderTop: '1px solid var(--border)' }}
|
|
|
|
|
>
|
|
|
|
|
<span style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>{'\u2318'}, to open settings</span>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
className="border border-border bg-transparent text-foreground rounded cursor-pointer hover:bg-accent"
|
|
|
|
|
style={{ fontSize: 13, padding: '6px 16px' }}
|
|
|
|
|
onClick={onClose}
|
2026-02-22 13:38:18 +01:00
|
|
|
>
|
2026-02-23 20:01:29 +01:00
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="border-none rounded cursor-pointer"
|
|
|
|
|
style={{ fontSize: 13, padding: '6px 16px', background: 'var(--primary)', color: 'white' }}
|
|
|
|
|
onClick={onSave}
|
|
|
|
|
data-testid="settings-save"
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
2026-02-22 13:38:18 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|