Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AUDIT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This log tracks all significant changes, updates, and versions in the PaperCache project.

## 2026-06-29 (Code Quality Refactor & Test Suite)
**Change:** refactor(shortcuts): extract helper to deduplicate global shortcut trigger logic; fix(timers): manage completion timeout lifecycle in store; test(editor): add comprehensive unit test suite for `VariableScope`

**Details/Why:**
1. **Shortcut Deduplication**: Extracted `handle_shortcut_trigger` helper in `src-tauri/src/commands/shortcuts.rs` to replace 16 lines of identical duplicate code across `update_global_shortcut` and `resume_shortcuts`.
2. **Managed Timeout Lifecycle**: Replaced unmanaged 5-second `setTimeout` in `useTimerStore.ts` with a tracked Map of active timeouts aligned to `COMPLETED_TIMER_CLEANUP_MS` (10s), ensuring timers cleaned up early or removed explicitly do not trigger orphan state updates.
3. **VariableScope Unit Tests**: Created `src/lib/editor/VariableScope.test.ts` testing global/note scope merging and debounced regex mathematical expression parsing (`/var x = ...`) using fake timers.

**Files changed:** `src-tauri/src/commands/shortcuts.rs`, `src/store/useTimerStore.ts`, `src/lib/editor/VariableScope.test.ts`, `AUDIT_LOG.md`, `CHANGELOG.md`.
## 2026-06-29 (Security & Auto-Update Overhaul)
**Change:** fix(security): pin third-party GitHub Action references in release workflow to immutable SHA-1 digests; fix(updater): overhaul Tauri auto-update mechanism to emit granular status events and require user-triggered restarts

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **Code Quality & Test Reliability**: Refactored global shortcut registration to remove duplicate event handling logic in the backend. Improved countdown timer cleanup reliability by properly tracking and clearing async timeouts when timers complete or are removed. Added comprehensive unit tests for inline DSL variable evaluation (`VariableScope`).
### Added
- **Contextual Auto-Update UI**: When checking for updates in Settings, visual feedback is now displayed ("Checking…"). When an update is downloaded and ready, a persistent toast notification appears with a prominent "Restart Now" button so users can restart when convenient rather than experiencing unexpected application restarts.

Expand Down
44 changes: 18 additions & 26 deletions src-tauri/src/commands/shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ impl Default for GlobalShortcutState {
}
}

fn handle_shortcut_trigger(app: &AppHandle, action: &str) {
if action == "new-note" {
if let Some(window) = app.get_webview_window("main") {
if !window.is_visible().unwrap_or(false) {
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
crate::macos::force_focus();
}
}
} else {
crate::commands::system::toggle_window(app);
}
let _ = app.emit(&format!("trigger-{}", action), ());
}

#[tauri::command]
pub fn update_global_shortcut(
app: AppHandle,
Expand All @@ -39,19 +55,7 @@ pub fn update_global_shortcut(
app.global_shortcut()
.on_shortcut(shortcut, move |app, _shortcut, event| {
if event.state() == ShortcutState::Pressed {
if action_clone == "new-note" {
if let Some(window) = app.get_webview_window("main") {
if !window.is_visible().unwrap_or(false) {
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
crate::macos::force_focus();
}
}
} else {
crate::commands::system::toggle_window(app);
}
let _ = app.emit(&format!("trigger-{}", action_clone), ());
handle_shortcut_trigger(app, &action_clone);
}
})
.map_err(|e| format!("Failed to register shortcut: {}", e))?;
Expand Down Expand Up @@ -84,19 +88,7 @@ pub fn resume_shortcuts(app: AppHandle) -> Result<(), String> {
.global_shortcut()
.on_shortcut(shortcut, move |app, _, event| {
if event.state() == ShortcutState::Pressed {
if action_clone == "new-note" {
if let Some(window) = app.get_webview_window("main") {
if !window.is_visible().unwrap_or(false) {
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
crate::macos::force_focus();
}
}
} else {
crate::commands::system::toggle_window(app);
}
let _ = app.emit(&format!("trigger-{}", action_clone), ());
handle_shortcut_trigger(app, &action_clone);
}
});
}
Expand Down
55 changes: 55 additions & 0 deletions src/lib/editor/VariableScope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { VariableScope, getScope } from './VariableScope'
import { useVariableStore } from '../../store/useVariableStore'

describe('VariableScope', () => {
beforeEach(() => {
vi.useFakeTimers()
useVariableStore.getState().setGlobals({})
useVariableStore.getState().setNoteScope({})
})

afterEach(() => {
vi.useRealTimers()
})

it('merges global and note scopes in getScope', () => {
useVariableStore.getState().setGlobals({ globalA: 10, shared: 'global' })
useVariableStore.getState().setNoteScope({ noteB: 20, shared: 'note' })

const scope = getScope()
expect(scope).toEqual({
globalA: 10,
noteB: 20,
shared: 'note',
})
})

it('parses mathematical expressions and updates note scope after debounce', () => {
const scopeMgr = new VariableScope()
const doc = '/var x = 10 + 5\n/var y = x * 2'

scopeMgr.triggerScopeUpdate(doc, null)

expect(useVariableStore.getState().getNoteScope()).toEqual({})

vi.advanceTimersByTime(300)

expect(useVariableStore.getState().getNoteScope()).toEqual({
x: 15,
y: 30,
})
})

it('falls back to raw trimmed string if expression parsing fails', () => {
const scopeMgr = new VariableScope()
const doc = '/var greeting = Hello World '

scopeMgr.triggerScopeUpdate(doc, null)
vi.advanceTimersByTime(300)

expect(useVariableStore.getState().getNoteScope()).toEqual({
greeting: 'Hello World',
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})
28 changes: 23 additions & 5 deletions src/store/useTimerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import { create } from 'zustand'

const COMPLETED_TIMER_CLEANUP_MS = 10000

const completionTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

function clearCompletionTimeout(id: string) {
const timeout = completionTimeouts.get(id)
if (timeout) {
clearTimeout(timeout)
completionTimeouts.delete(id)
}
}

export type TimerStatus = 'running' | 'paused' | 'completed'

export interface Timer {
Expand Down Expand Up @@ -40,9 +50,13 @@ export const useTimerStore = create<TimerState>((set) => ({
cleanExpiredTimers: () => {
const now = Date.now()
set((state) => ({
timers: state.timers.filter(
(t) => t.status !== 'completed' || now - t.endsAt < COMPLETED_TIMER_CLEANUP_MS
),
timers: state.timers.filter((t) => {
if (t.status === 'completed' && now - t.endsAt >= COMPLETED_TIMER_CLEANUP_MS) {
clearCompletionTimeout(t.id)
return false
}
return true
}),
}))
},

Expand All @@ -59,6 +73,7 @@ export const useTimerStore = create<TimerState>((set) => ({
},

removeTimer: (id) => {
clearCompletionTimeout(id)
set((state) => ({ timers: state.timers.filter((t) => t.id !== id) }))
},

Expand All @@ -79,14 +94,17 @@ export const useTimerStore = create<TimerState>((set) => ({
const existing = useTimerStore.getState().timers.find((t) => t.id === id)
if (!existing || existing.status === 'completed') return

clearCompletionTimeout(id)
set((state) => ({
timers: state.timers.map((t) =>
t.id === id ? { ...t, remainingMs: 0, status: 'completed' } : t
),
}))
setTimeout(() => {
const timeout = setTimeout(() => {
completionTimeouts.delete(id)
useTimerStore.getState().removeTimer(id)
}, 5000)
}, COMPLETED_TIMER_CLEANUP_MS)
completionTimeouts.set(id, timeout)
},

pauseTimer: (id) => {
Expand Down
Loading