Skip to content

Feature/quick search#3

Merged
khuepm merged 17 commits into
mainfrom
feature/quick-search
Mar 30, 2026
Merged

Feature/quick search#3
khuepm merged 17 commits into
mainfrom
feature/quick-search

Conversation

@khuepm
Copy link
Copy Markdown
Owner

@khuepm khuepm commented Mar 30, 2026

  • Disable auto-save for new/unsaved documents by setting isFilePersisted to false on creation
  • Keep auto-save enabled for existing loaded documents
  • Verify useAutoSave respects updated isFilePersisted flag

khuepm added 14 commits March 30, 2026 23:16
- Add .kiro configuration for quick-settings-search spec with requirements-first workflow
- Add comprehensive requirements document defining Quick Settings Search functionality with 7 key requirements covering activation, search, results display, navigation, registry, schema, and backend integration
- Add design document placeholder for UI/UX specifications
- Add tasks document placeholder for implementation tracking
- Add preferences-config documentation to wordai-editor for feature reference
- Establishes foundation for implementing command palette-style settings search with Cmd+Shift+P/Ctrl+Shift+P activation
- Create src/types/preferences.ts with Tab union type, SettingEntry interface,
  Preferences interface (general/aiEngine/typography/privacy groups),
  and defaultPreferences constant
- Requirements: 6.1, 6.2, 6.3, 6.4
… property tests

- Create src/data/settingRegistry.ts with 20 SETTING_REGISTRY entries across 4 tabs
  (general, ai-engine, typography, privacy), each with id in 'tab.settingName' format
- Create src/data/settingRegistry.test.ts with property tests:
  Property 1: all required fields non-empty
  Property 2: id format and tab prefix match
- Requirements: 5.1-5.6
…(tasks 2.1-2.4)

- Add src-tauri/src/preferences_store.rs with load/save/reset logic
  - load_preferences: reads user_{userId}.json, merges with default.json
  - save_preferences: writes user_{userId}.json, creates dir if needed
  - reset_preferences: resets one group or all to default.json values
  - merge_with_defaults: deep-merge helper for filling missing keys
  - 10 unit tests covering merge, load, save, reset, and round-trip

- Add public/preferences/default.json with all default preference values
  matching defaultPreferences in src/types/preferences.ts

- Register preferences_store module and 3 Tauri commands in lib.rs:
  load_preferences, save_preferences, reset_preferences

- Add tempfile dev-dependency to Cargo.toml for unit tests

All 48 Rust tests pass.
…ckend

- Create src-tauri/src/preferences_store.rs with load/save/reset commands
  and merge_with_defaults deep-merge helper
- Create public/preferences/default.json with all default preference values
- Register preferences commands in lib.rs invoke_handler
- Add unit tests for merge, load, save, round-trip, and reset logic
- Add tempfile dev-dependency to Cargo.toml
- Requirements: 7.1-7.7, 8.1-8.4
- Create src/services/preferencesService.ts with loadPreferences,
  savePreferences, resetPreferences Tauri IPC wrappers
- Update src/mocks/tauri.ts with in-memory mock handlers for all 3 commands
- Create src/services/preferencesService.test.ts with 6 unit tests
  covering correct IPC args and error propagation
- Requirements: 6.1, 7.1, 7.3, 7.4, 7.7
…sk 5.1)

- Create QuickSearchPopup.tsx with centered overlay modal and backdrop
- Export filterSettings() utility for property-based testing
- Auto-focus search input on open via useEffect + ref
- Real-time case-insensitive filtering by label, description, keywords
- Show full list when empty; 'No settings found' when no matches
- Cap results at 8 with overflow-y scroll
- Highlight first result by default; ArrowUp/ArrowDown navigation
- Show label, description, and tab badge per result item
- Close on Escape key or backdrop click
- Call onSelect(entry) on Enter or result click
- Return null when isOpen is false
…ask 5.2)

- Property 3: For any non-empty query, every result contains the query
  in label, description, or at least one keyword (case-insensitive)
- Property 4: Empty/whitespace query returns full SETTING_REGISTRY
- 27 tests passing across diverse query inputs
- Validates: Requirements 2.2, 2.3, 2.4
…k 5.3)

- Test Escape key calls onClose (Req 1.3)
- Test backdrop click calls onClose (Req 1.4)
- Test 'No settings found' renders when no matches (Req 2.5)
- Test ArrowDown/ArrowUp navigation moves highlight (Req 3.4)
- Test Enter on highlighted item calls onSelect with correct entry (Req 4.2)
- Test click on result calls onSelect (Req 4.1)
- Test label, description, and tab badge display (Req 3.1, 3.2)
- Test first result highlighted by default (Req 3.5)
- Fix scrollIntoView guard in QuickSearchPopup for jsdom compatibility
- 16 tests passing
- Create src/components/QuickSearchPopup.tsx with overlay modal, auto-focus,
  real-time filtering, 8-result cap, arrow key navigation, tab badges,
  Escape/backdrop close, Enter/click selection
- Export filterSettings() utility for property testing
- Create QuickSearchPopup.property.test.ts with Property 3 & 4 tests (27 tests)
- Create QuickSearchPopup.test.tsx with 16 unit tests
- Requirements: 1.3, 1.4, 2.2-2.6, 3.1-3.5, 4.1, 4.2, 4.4
…vigation wiring

- App.tsx: add isQuickSearchOpen state, Cmd+Shift+P/Ctrl+Shift+P keydown listener,
  handleQuickSearchSelect, preferencesInitialTab/targetSettingId state,
  render QuickSearchPopup, update PreferencesDialog props
- PreferencesDialog.tsx: import Tab from types/preferences, add initialTab/targetSettingId
  props, scroll-to-setting useEffect, data-setting-id attributes on all 16 setting rows
- Create QuickSearchShortcut.test.tsx with 4 keyboard shortcut tests
- Requirements: 1.1, 1.2, 4.1, 4.2, 4.3
…ssing

All quick-settings-search feature tests pass (240/243).
3 pre-existing failures in EditorCanvas/VisualEffects are unrelated to this feature.
…cut detection

- Add console logging for QuickSearch initialization and keyboard events
- Fix keyboard shortcut detection by converting key to lowercase for case-insensitive matching
- Simplify event listener registration by removing unnecessary document existence check
- Switch from document to window for keydown event listener attachment
- Improve debugging visibility for keyboard event handling flow
…ave enabling

- Add isFilePersisted state flag to track when document has been saved to disk at least once
- Add markFilePersisted action and dispatcher to update persistence state on successful save
- Add enabled parameter to useAutoSave hook to conditionally disable auto-save when file not persisted
- Pass isFilePersisted to useAutoSave to prevent auto-save attempts before initial file save
- Update handleSaveSuccess callback to mark file as persisted after successful save
- Add null coalescing for filePath in useAutoSave invocation to handle undefined paths safely
- Update dependency arrays in useAutoSave effects to include enabled flag for proper re-evaluation
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a command-palette style Quick Settings Search to WordAI Editor and introduces a preferences storage system (types, registry, frontend IPC service, and a Rust Tauri backend) to support loading/saving/resetting user preferences.

Changes:

  • Implement QuickSearchPopup UI + keyboard shortcut wiring to open PreferencesDialog on the correct tab and scroll to a selected setting.
  • Add a centralized SettingRegistry and TypeScript preferences schema/defaults.
  • Add Tauri IPC commands + Rust preferences store for load/save/reset, along with mocks and tests.

Reviewed changes

Copilot reviewed 22 out of 24 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
apps/wordai-editor/src/types/preferences.ts Adds preferences schema/types and defaultPreferences.
apps/wordai-editor/src/services/stateManager.tsx Adds isFilePersisted flag and related actions to global state.
apps/wordai-editor/src/services/preferencesService.ts Adds IPC wrappers for load/save/reset preferences.
apps/wordai-editor/src/services/preferencesService.test.ts Unit tests for preferencesService IPC calls.
apps/wordai-editor/src/mocks/tauri.ts Adds mock handlers for preferences IPC in browser dev mode.
apps/wordai-editor/src/hooks/useAutoSave.ts Adds enabled flag to control auto-save effects.
apps/wordai-editor/src/data/settingRegistry.ts Introduces flat registry of searchable settings metadata.
apps/wordai-editor/src/data/settingRegistry.test.ts Property tests validating registry completeness/consistency.
apps/wordai-editor/src/components/QuickSearchShortcut.test.tsx Tests for Cmd/Ctrl+Shift+P shortcut behavior.
apps/wordai-editor/src/components/QuickSearchPopup.tsx Implements quick search modal UI + filtering/navigation behavior.
apps/wordai-editor/src/components/QuickSearchPopup.test.tsx Unit tests for popup interactions and keyboard navigation.
apps/wordai-editor/src/components/QuickSearchPopup.property.test.ts Property tests for filter correctness.
apps/wordai-editor/src/components/PreferencesDialog.tsx Adds initialTab/targetSettingId support and setting anchors.
apps/wordai-editor/src/App.tsx Wires keyboard shortcut, popup, and preferences navigation; adjusts autosave enablement.
apps/wordai-editor/src-tauri/src/preferences_store.rs Adds Rust preferences store (load/save/reset + merge defaults) and tests.
apps/wordai-editor/src-tauri/src/lib.rs Registers new preferences IPC commands.
apps/wordai-editor/src-tauri/Cargo.toml Adds dev dependency for Rust tests.
apps/wordai-editor/src-tauri/Cargo.lock Locks new Rust dev dependency.
apps/wordai-editor/public/preferences/default.json Adds bundled default preferences JSON.
apps/wordai-editor/docs/features/preferences-config.md Documents preferences config system and quick search flow.
.kiro/specs/quick-settings-search/tasks.md Implementation plan/spec checklist for the feature.
.kiro/specs/quick-settings-search/requirements.md Requirements spec for quick settings search + preferences.
.kiro/specs/quick-settings-search/.config.kiro Kiro spec metadata for the feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (!targetSettingId || !isOpen) return;
const timer = setTimeout(() => {
const el = document.querySelector(`[data-setting-id="${targetSettingId}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect calls el.scrollIntoView(...) without checking that scrollIntoView exists. In test/SSR environments (and some older webviews), scrollIntoView can be undefined, which would throw when opening the dialog via Quick Search. Add a type/feature check before calling it (similar to the guard used in QuickSearchPopup).

Suggested change
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (el && typeof (el as any).scrollIntoView === 'function') {
(el as any).scrollIntoView({ behavior: 'smooth', block: 'center' });
}

Copilot uses AI. Check for mistakes.
Comment on lines 365 to 367
{sliders.map((s, i) => (
<div key={s.label}>
<div key={s.label} data-setting-id={i === 0 ? 'ai-engine.creativity' : 'ai-engine.contextWindowTokens'}>
{i > 0 && <div style={{ marginBottom: '1.5rem' }} />}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the slider loop index to assign data-setting-id is brittle (it silently breaks if the sliders array is reordered or extended). Prefer adding an explicit settingId field to each slider definition and using that, similar to how smartFeatures does it.

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +119
load_preferences({ userId }: { userId: string }) {
return preferencesStore[userId] ?? defaultPreferences;
},
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_preferences returns defaultPreferences by reference. If any caller mutates the returned object, it will mutate the shared default object for all users/sessions. Return a deep clone (and similarly clone on save) to keep defaults immutable and avoid cross-test/session state leaks.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
prefs_dir: &PathBuf,
defaults: &Value,
user_id: &str,
) -> Result<Value, IPCError> {
let user_file = prefs_dir.join(format!("user_{}.json", user_id));
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_id is interpolated directly into the filename (user_{user_id}.json). If user_id can contain path separators (e.g., ../), this can lead to path traversal and reading/writing outside the preferences directory. Validate/sanitize user_id (e.g., allow only [A-Za-z0-9_-]), or derive the filename from a safe encoding/hash.

Copilot uses AI. Check for mistakes.
Comment thread apps/wordai-editor/src/App.tsx Outdated
Comment on lines +66 to +72
console.log('[QuickSearch] typeof window:', typeof window);
console.log('[QuickSearch] registering keydown listener on window');
const handleKeyDown = (e: KeyboardEvent) => {
console.log('[QuickSearch] keydown:', e.key, 'meta:', e.metaKey, 'ctrl:', e.ctrlKey, 'shift:', e.shiftKey);
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault();
console.log('[QuickSearch] opening popup');
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the console.log debug statements from the global QuickSearch keydown listener (or guard them behind a dev-only flag). Leaving these in will spam logs on every keypress in production and can impact performance/privacy.

Suggested change
console.log('[QuickSearch] typeof window:', typeof window);
console.log('[QuickSearch] registering keydown listener on window');
const handleKeyDown = (e: KeyboardEvent) => {
console.log('[QuickSearch] keydown:', e.key, 'meta:', e.metaKey, 'ctrl:', e.ctrlKey, 'shift:', e.shiftKey);
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault();
console.log('[QuickSearch] opening popup');
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault();

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +72
} else if (e.key === 'ArrowDown') {
e.preventDefault();
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When results.length === 0, results.length - 1 becomes -1, so ArrowDown sets highlightIndex to -1. That can put the component into an invalid state (negative index) and breaks the assumption that a highlighted option index is >= 0. Guard ArrowUp/ArrowDown/Enter when there are no results, or clamp the max index to at least 0.

Suggested change
} else if (e.key === 'ArrowDown') {
e.preventDefault();
return;
}
const isNavigationKey =
e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter';
if (isNavigationKey && results.length === 0) {
// No results to navigate or select; just prevent default behavior.
e.preventDefault();
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();

Copilot uses AI. Check for mistakes.
<button style={{ background: 'rgba(255,255,255,0.2)', backdropFilter: 'blur(12px)', border: 'none', borderRadius: '0.5rem', padding: '0.5rem 1rem', fontSize: '0.75rem', fontWeight: 700, color: '#ffffff', cursor: 'pointer' }}>Audit Security</button>
</div>
</div>
<div data-setting-id="privacy.localProcessingOnly" style={{ display: 'none' }} />
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

privacy.localProcessingOnly is represented only by a display: none placeholder, so Quick Search navigation will scroll to something invisible and users still can’t change the setting. Either add a real UI control for this setting in the Privacy tab or remove it from SETTING_REGISTRY until it’s supported.

Suggested change
<div data-setting-id="privacy.localProcessingOnly" style={{ display: 'none' }} />
<div data-setting-id="privacy.localProcessingOnly" style={{ padding: '1.25rem 1.5rem', borderRadius: '1rem', border: '1px solid #e0e0e0', backgroundColor: '#fafafa', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div>
<h3 style={{ margin: 0, fontSize: '0.9rem', fontWeight: 600 }}>Local processing only</h3>
<p style={{ margin: '0.35rem 0 0', fontSize: '0.8rem', color: '#555' }}>
When enabled, WordAI Editor will process your content only on this device where possible, and avoid sending data to remote services except when strictly required.
</p>
</div>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', color: '#333', cursor: 'pointer' }}>
<input type="checkbox" style={{ width: '14px', height: '14px' }} />
<span>Prefer on-device processing only</span>
</label>
</div>

Copilot uses AI. Check for mistakes.
Comment on lines 84 to 91
case 'SET_DOCUMENT':
return {
...state,
document: action.payload.document,
filePath: action.payload.filePath,
hasUnsavedChanges: false,
isFilePersisted: false,
};
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SET_DOCUMENT always resets isFilePersisted to false, which disables auto-save (since App passes isFilePersisted as the enabled flag) even for documents that were just created or loaded from disk. Consider setting isFilePersisted based on how the document was obtained (e.g., true for load/create flows that already persist), or include an explicit flag in the action payload so existing docs don’t lose auto-save.

Copilot uses AI. Check for mistakes.
Comment thread apps/wordai-editor/src/App.tsx Outdated
Comment on lines +164 to +165
markFilePersisted();
}, [markSaved, markFilePersisted]);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleSaveSuccess calls both markSaved(doc) and markFilePersisted(), but MARK_SAVED already sets isFilePersisted: true in the reducer. Consider removing the extra dispatch (or removing isFilePersisted mutation from MARK_SAVED) to avoid redundant state updates and confusion about the source of truth.

Suggested change
markFilePersisted();
}, [markSaved, markFilePersisted]);
}, [markSaved]);

Copilot uses AI. Check for mistakes.
const [activeTab, setActiveTab] = useState<Tab>(initialTab ?? 'general');
const [searchQuery, setSearchQuery] = useState('');

useEffect(() => {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeTab is initialized from initialTab only on the first mount. Since the component returns null when closed (but stays mounted), subsequent opens won’t respect a new initialTab from Quick Search and may show the previously selected tab. Sync activeTab from initialTab when isOpen becomes true (or when initialTab changes), and consider resetting preferencesInitialTab on close in App.

Suggested change
useEffect(() => {
useEffect(() => {
if (!isOpen || !initialTab) return;
if (activeTab !== initialTab) {
setActiveTab(initialTab);
}
}, [isOpen, initialTab, activeTab]);
useEffect(() => {

Copilot uses AI. Check for mistakes.
@khuepm
Copy link
Copy Markdown
Owner Author

khuepm commented Mar 30, 2026

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

@copilot apply changes based on the comments in this thread

Applied all changes from the review thread in commit dfc3444:

  1. Removed console.log statements from the QuickSearch keydown listener (App.tsx)
  2. Removed redundant markFilePersisted() call — MARK_SAVED already sets isFilePersisted: true (App.tsx)
  3. Guarded navigation keys (ArrowUp/ArrowDown/Enter) when results list is empty to prevent invalid highlight index (QuickSearchPopup.tsx)
  4. Added explicit settingId field to each slider definition instead of using the loop index (PreferencesDialog.tsx)
  5. Added real UI control for privacy.localProcessingOnly setting (checkbox with description) replacing the hidden display:none placeholder (PreferencesDialog.tsx)
  6. Added useEffect to sync activeTab from initialTab when isOpen becomes true, so Quick Search correctly navigates to the right tab on subsequent opens (PreferencesDialog.tsx)
  7. Added type check typeof el.scrollIntoView === 'function' before calling it to guard against SSR/test environments (PreferencesDialog.tsx)
  8. Return deep clones via JSON.parse(JSON.stringify(...)) in all three preferences mock handlers to prevent shared-state mutation (tauri.ts)
  9. Added validate_user_id() in Rust to reject any user_id containing characters outside [A-Za-z0-9_-], preventing path traversal attacks (preferences_store.rs)
  10. Added optional isFilePersisted flag to the SET_DOCUMENT action payload so documents loaded from or created on disk start with auto-save enabled (stateManager.tsx)

- Add effect to reset saveError state when auto-save is disabled
- Add null check for saveError.message before rendering error banner
- Prevents stale error messages from displaying when document not yet persisted
@khuepm khuepm merged commit 41dae2d into main Mar 30, 2026
@khuepm khuepm deleted the feature/quick-search branch April 1, 2026 07:38
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.

3 participants