diff --git a/CHANGELOG.md b/CHANGELOG.md index 697f11d9d..4ca71058d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ Format: weekly entries grouped by feature area. --- +## 2026-04-03 — Dead Code Cleanup + +### Changed +- Remove 6 unused files and 11 unused exports across monorepo (~762 lines) +- Remove superseded per-type reminder schemas in favor of unified `CreateReminderSchema` +- Un-export internal-only `ReminderTargetTypeSchema` and `ReminderStatusSchema` + +--- + ## 2026-04-03 — Clean Code Audit ### Changed diff --git a/apps/desktop/src/main/database/fts-tasks.ts b/apps/desktop/src/main/database/fts-tasks.ts index c6f19d6a9..8e48da946 100644 --- a/apps/desktop/src/main/database/fts-tasks.ts +++ b/apps/desktop/src/main/database/fts-tasks.ts @@ -83,11 +83,6 @@ export function clearFtsTasksTable(db: DrizzleDb): void { db.run(sql`DELETE FROM fts_tasks`) } -export function getFtsTasksCount(db: DrizzleDb): number { - const result = db.get<{ count: number }>(sql`SELECT COUNT(*) as count FROM fts_tasks`) - return result?.count ?? 0 -} - export function initializeFtsTasks(db: DrizzleDb): void { createFtsTasksTable(db) createFtsTasksTriggers(db) diff --git a/apps/desktop/src/main/ipc/handler-utils.ts b/apps/desktop/src/main/ipc/handler-utils.ts deleted file mode 100644 index fc663b0a2..000000000 --- a/apps/desktop/src/main/ipc/handler-utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function extractError(error: unknown, fallback: string): string { - return error instanceof Error ? error.message : fallback -} - -export type MutationResult = Record> = - | ({ success: true } & T) - | ({ success: false; error: string } & { [K in keyof T]?: null }) diff --git a/apps/desktop/src/main/lib/reminders.ts b/apps/desktop/src/main/lib/reminders.ts index cf1ba5c30..be33c1514 100644 --- a/apps/desktop/src/main/lib/reminders.ts +++ b/apps/desktop/src/main/lib/reminders.ts @@ -713,31 +713,6 @@ export function isSchedulerRunning(): boolean { return schedulerInterval !== null } -// ============================================================================ -// Cleanup -// ============================================================================ - -/** - * Delete all reminders for a target (used when target is deleted) - * @param targetType - Type of target - * @param targetId - ID of the target - * @returns Number of reminders deleted - */ -export function deleteRemindersForTarget(targetType: string, targetId: string): number { - const db = getDatabase() - - const result = db - .delete(reminders) - .where(and(eq(reminders.targetType, targetType), eq(reminders.targetId, targetId))) - .run() - - if (result.changes > 0) { - logger.info(`Deleted ${result.changes} reminders for ${targetType}:${targetId}`) - } - - return result.changes -} - /** * Count pending reminders (for badge display) * @returns Number of pending reminders diff --git a/apps/desktop/src/renderer/src/hooks/use-account-info.ts b/apps/desktop/src/renderer/src/hooks/use-account-info.ts deleted file mode 100644 index 111e3faa5..000000000 --- a/apps/desktop/src/renderer/src/hooks/use-account-info.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useState, useEffect } from 'react' -import { extractErrorMessage } from '@/lib/ipc-error' - -export interface AccountInfo { - email: string | null - joinedAt: number | null -} - -interface UseAccountInfoReturn { - accountInfo: AccountInfo | null - isLoading: boolean - error: string | null - refresh: () => void -} - -export function useAccountInfo(): UseAccountInfoReturn { - const [accountInfo, setAccountInfo] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const [refreshKey, setRefreshKey] = useState(0) - - useEffect(() => { - let mounted = true - const load = async (): Promise => { - try { - setIsLoading(true) - setError(null) - const result = await window.api.account.getInfo() - if (mounted) setAccountInfo(result) - } catch (err) { - if (mounted) setError(extractErrorMessage(err, 'Failed to load account info')) - } finally { - if (mounted) setIsLoading(false) - } - } - void load() - return () => { - mounted = false - } - }, [refreshKey]) - - return { - accountInfo, - isLoading, - error, - refresh: () => setRefreshKey((k) => k + 1) - } -} diff --git a/apps/desktop/src/renderer/src/hooks/use-backup-settings.ts b/apps/desktop/src/renderer/src/hooks/use-backup-settings.ts deleted file mode 100644 index 0f87c0670..000000000 --- a/apps/desktop/src/renderer/src/hooks/use-backup-settings.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { extractErrorMessage } from '@/lib/ipc-error' -import type { BackupSettingsDTO } from '../../../preload/index.d' - -const DEFAULTS: BackupSettingsDTO = { - autoBackup: false, - frequencyHours: 24, - maxBackups: 5, - lastBackupAt: null -} - -interface UseBackupSettingsReturn { - settings: BackupSettingsDTO - isLoading: boolean - error: string | null - updateSettings: (updates: Partial) => Promise -} - -export function useBackupSettings(): UseBackupSettingsReturn { - const [settings, setSettings] = useState(DEFAULTS) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - let mounted = true - const load = async (): Promise => { - try { - setIsLoading(true) - setError(null) - const result = await window.api.settings.getBackupSettings() - if (mounted) setSettings(result) - } catch (err) { - if (mounted) setError(extractErrorMessage(err, 'Failed to load backup settings')) - } finally { - if (mounted) setIsLoading(false) - } - } - load() - return () => { - mounted = false - } - }, []) - - useEffect(() => { - const unsubscribe = window.api.onSettingsChanged((event) => { - if (event.key === 'backup') { - setSettings((prev) => ({ ...prev, ...(event.value as Partial) })) - } - }) - return unsubscribe - }, []) - - const updateSettings = useCallback( - async (updates: Partial): Promise => { - try { - const result = await window.api.settings.setBackupSettings(updates) - if (result.success) { - setSettings((prev) => ({ ...prev, ...updates })) - return true - } - setError(result.error ?? 'Update failed') - return false - } catch (err) { - setError(extractErrorMessage(err, 'Failed to update backup settings')) - return false - } - }, - [] - ) - - return { settings, isLoading, error, updateSettings } -} diff --git a/apps/desktop/src/renderer/src/hooks/use-devices.ts b/apps/desktop/src/renderer/src/hooks/use-devices.ts deleted file mode 100644 index 851c1831b..000000000 --- a/apps/desktop/src/renderer/src/hooks/use-devices.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { extractErrorMessage } from '@/lib/ipc-error' -import { deviceService } from '@/services/device-service' - -export interface Device { - id: string - name: string - platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' - linkedAt: number - lastSyncAt?: number - isCurrentDevice: boolean -} - -interface UseDevicesReturn { - devices: Device[] - email: string | undefined - isLoading: boolean - error: string | null - removeDevice: (deviceId: string) => Promise - refresh: () => void -} - -export function useDevices(): UseDevicesReturn { - const [devices, setDevices] = useState([]) - const [email, setEmail] = useState(undefined) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const [refreshKey, setRefreshKey] = useState(0) - - useEffect(() => { - let mounted = true - const load = async (): Promise => { - try { - setIsLoading(true) - setError(null) - const result = await deviceService.getDevices() - if (mounted) { - setDevices(result.devices as Device[]) - setEmail(result.email) - } - } catch (err) { - if (mounted) setError(extractErrorMessage(err, 'Failed to load devices')) - } finally { - if (mounted) setIsLoading(false) - } - } - void load() - return () => { - mounted = false - } - }, [refreshKey]) - - const removeDevice = useCallback(async (deviceId: string): Promise => { - try { - const result = await deviceService.removeDevice({ deviceId }) - if (result.success) { - setDevices((prev) => prev.filter((d) => d.id !== deviceId)) - return true - } - setError(result.error ?? 'Failed to remove device') - return false - } catch (err) { - setError(extractErrorMessage(err, 'Failed to remove device')) - return false - } - }, []) - - return { - devices, - email, - isLoading, - error, - removeDevice, - refresh: () => setRefreshKey((k) => k + 1) - } -} diff --git a/apps/desktop/src/renderer/src/hooks/use-inbox-mutations.ts b/apps/desktop/src/renderer/src/hooks/use-inbox-mutations.ts index 0e76b4654..c85b1e9dd 100644 --- a/apps/desktop/src/renderer/src/hooks/use-inbox-mutations.ts +++ b/apps/desktop/src/renderer/src/hooks/use-inbox-mutations.ts @@ -176,20 +176,6 @@ export function useConvertToTask() { }) } -export function useLinkToNote() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: ({ itemId, noteId }: { itemId: string; noteId: string }) => - inboxService.linkToNote(itemId, noteId), - onSuccess: (_, { itemId }) => { - void queryClient.invalidateQueries({ queryKey: inboxKeys.item(itemId) }) - void queryClient.invalidateQueries({ queryKey: inboxKeys.lists() }) - void queryClient.invalidateQueries({ queryKey: inboxKeys.stats() }) - } - }) -} - // ============================================================================= // Tag Mutations // ============================================================================= diff --git a/apps/desktop/src/renderer/src/hooks/use-inbox-queries.ts b/apps/desktop/src/renderer/src/hooks/use-inbox-queries.ts index 2dbff0aed..301be66ee 100644 --- a/apps/desktop/src/renderer/src/hooks/use-inbox-queries.ts +++ b/apps/desktop/src/renderer/src/hooks/use-inbox-queries.ts @@ -22,7 +22,6 @@ import { onInboxSnoozeDue, onInboxTranscriptionComplete, onInboxMetadataComplete, - onInboxProcessingError, onInboxSnoozed, type InboxListInput } from '@/services/inbox-service' @@ -415,12 +414,3 @@ export function useInboxFilingHistory(options?: { staleTime: ITEM_STALE_TIME }) } - -export function useInboxProcessingErrors( - callback: (event: { id: string; operation: string; error: string }) => void -): void { - useEffect(() => { - const unsub = onInboxProcessingError(callback) - return unsub - }, [callback]) -} diff --git a/apps/desktop/src/renderer/src/hooks/use-inbox.ts b/apps/desktop/src/renderer/src/hooks/use-inbox.ts index 122b9f560..e95c42114 100644 --- a/apps/desktop/src/renderer/src/hooks/use-inbox.ts +++ b/apps/desktop/src/renderer/src/hooks/use-inbox.ts @@ -10,8 +10,7 @@ export { useInboxPatterns, useInboxStaleThreshold, useInboxArchived, - useInboxFilingHistory, - useInboxProcessingErrors + useInboxFilingHistory } from './use-inbox-queries' export type { UseInboxListOptions, @@ -33,7 +32,6 @@ export { useFileInboxItem, useConvertToNote, useConvertToTask, - useLinkToNote, useAddInboxTag, useRemoveInboxTag, useSnoozeInboxItem, diff --git a/apps/desktop/src/renderer/src/hooks/use-is-item-active.ts b/apps/desktop/src/renderer/src/hooks/use-is-item-active.ts index 68e5ccebd..8de76bf7f 100644 --- a/apps/desktop/src/renderer/src/hooks/use-is-item-active.ts +++ b/apps/desktop/src/renderer/src/hooks/use-is-item-active.ts @@ -82,29 +82,4 @@ export const useIsItemActive = () => { return isActiveItem } -/** - * Hook that returns the active tab identity for components that need - * to react to active tab changes (e.g., highlighting) - * - * Use this when you need to RE-RENDER on active tab change. - * Use useIsItemActive when you just need to CHECK if something is active. - */ -export const useActiveTabIdentity = (): ActiveTabIdentity | null => { - const { state } = useTabs() - - return useMemo((): ActiveTabIdentity | null => { - const group = state.tabGroups[state.activeGroupId] - if (!group || !group.activeTabId) return null - - const activeTab = group.tabs.find((t) => t.id === group.activeTabId) - if (!activeTab) return null - - return { - type: activeTab.type, - entityId: activeTab.entityId ?? '', - path: activeTab.path ?? '' - } - }, [state.tabGroups, state.activeGroupId]) -} - export default useIsItemActive diff --git a/apps/desktop/src/renderer/src/hooks/use-linking-events.ts b/apps/desktop/src/renderer/src/hooks/use-linking-events.ts deleted file mode 100644 index c991570bc..000000000 --- a/apps/desktop/src/renderer/src/hooks/use-linking-events.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import type { LinkingRequestEvent } from '@memry/contracts/ipc-events' - -interface LinkingEventsState { - pendingRequest: LinkingRequestEvent | null - recentlyApproved: string | null - clearRequest: () => void - clearApproved: () => void -} - -export function useLinkingEvents(): LinkingEventsState { - const [pendingRequest, setPendingRequest] = useState(null) - const [recentlyApproved, setRecentlyApproved] = useState(null) - - useEffect(() => { - const unsubRequest = window.api.onLinkingRequest((event) => { - setPendingRequest(event) - }) - - const unsubApproved = window.api.onLinkingApproved((event) => { - setPendingRequest(null) - setRecentlyApproved(event.sessionId) - }) - - return () => { - unsubRequest() - unsubApproved() - } - }, []) - - const clearRequest = useCallback(() => setPendingRequest(null), []) - const clearApproved = useCallback(() => setRecentlyApproved(null), []) - - return { pendingRequest, recentlyApproved, clearRequest, clearApproved } -} diff --git a/apps/desktop/src/renderer/src/hooks/use-notes-query.ts b/apps/desktop/src/renderer/src/hooks/use-notes-query.ts index bc21ead41..4c13a3465 100644 --- a/apps/desktop/src/renderer/src/hooks/use-notes-query.ts +++ b/apps/desktop/src/renderer/src/hooks/use-notes-query.ts @@ -519,21 +519,5 @@ export function useNoteMutations() { // Prefetch Utilities // ============================================================================= -/** - * Prefetch a note into the cache. - * Useful for hover prefetching in lists. - */ -export function usePrefetchNote() { - const queryClient = useQueryClient() - - return (id: string) => { - queryClient.prefetchQuery({ - queryKey: notesKeys.note(id), - queryFn: () => notesService.get(id), - staleTime: NOTE_STALE_TIME - }) - } -} - // Re-export types export type { Note, NoteListItem, NoteListResponse, NoteLinksResponse } diff --git a/apps/desktop/src/renderer/src/hooks/use-reminders.ts b/apps/desktop/src/renderer/src/hooks/use-reminders.ts index 162ead93a..4c0fffd00 100644 --- a/apps/desktop/src/renderer/src/hooks/use-reminders.ts +++ b/apps/desktop/src/renderer/src/hooks/use-reminders.ts @@ -14,15 +14,13 @@ import { onReminderCreated, onReminderUpdated, onReminderDeleted, - onReminderDue, onReminderDismissed, onReminderSnoozed, type CreateReminderInput, type UpdateReminderInput, type SnoozeReminderInput, type ListRemindersInput, - type ReminderTargetType, - type ReminderDueEvent + type ReminderTargetType } from '@/services/reminder-service' // ============================================================================ @@ -157,16 +155,6 @@ export function useRemindersForTarget( } } -/** - * Hook for subscribing to due reminders (for notifications) - */ -export function useDueReminderNotifications(onDue: (event: ReminderDueEvent) => void) { - useEffect(() => { - const unsub = onReminderDue(onDue) - return unsub - }, [onDue]) -} - // ============================================================================ // Mutations // ============================================================================ diff --git a/apps/desktop/src/renderer/src/hooks/use-sync-settings.ts b/apps/desktop/src/renderer/src/hooks/use-sync-settings.ts deleted file mode 100644 index ae6d7e5db..000000000 --- a/apps/desktop/src/renderer/src/hooks/use-sync-settings.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { extractErrorMessage } from '@/lib/ipc-error' -import type { SyncSettingsDTO } from '../../../preload/index.d' - -const DEFAULTS: SyncSettingsDTO = { - enabled: true, - autoSync: true -} - -interface UseSyncSettingsReturn { - settings: SyncSettingsDTO - isLoading: boolean - error: string | null - updateSettings: (updates: Partial) => Promise -} - -export function useSyncSettings(): UseSyncSettingsReturn { - const [settings, setSettings] = useState(DEFAULTS) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - let mounted = true - const load = async (): Promise => { - try { - setIsLoading(true) - setError(null) - const result = await window.api.settings.getSyncSettings() - if (mounted) setSettings(result) - } catch (err) { - if (mounted) setError(extractErrorMessage(err, 'Failed to load sync settings')) - } finally { - if (mounted) setIsLoading(false) - } - } - load() - return () => { - mounted = false - } - }, []) - - useEffect(() => { - const unsubscribe = window.api.onSettingsChanged((event) => { - if (event.key === 'sync') { - setSettings((prev) => ({ ...prev, ...(event.value as Partial) })) - } - }) - return unsubscribe - }, []) - - const updateSettings = useCallback( - async (updates: Partial): Promise => { - try { - const result = await window.api.settings.setSyncSettings(updates) - if (result.success) { - setSettings((prev) => ({ ...prev, ...updates })) - return true - } - setError(result.error ?? 'Update failed') - return false - } catch (err) { - setError(extractErrorMessage(err, 'Failed to update sync settings')) - return false - } - }, - [] - ) - - return { settings, isLoading, error, updateSettings } -} diff --git a/apps/desktop/src/renderer/src/lib/graph-builder.ts b/apps/desktop/src/renderer/src/lib/graph-builder.ts index 686761ee0..b602987ea 100644 --- a/apps/desktop/src/renderer/src/lib/graph-builder.ts +++ b/apps/desktop/src/renderer/src/lib/graph-builder.ts @@ -188,13 +188,3 @@ export function computeFocusSet(graph: Graph, nodeId: string, depth: number): Se return visited } - -export function extractAllTags(data: GraphDataResponse): string[] { - const tagSet = new Set() - for (const node of data.nodes) { - for (const tag of node.tags) { - tagSet.add(tag) - } - } - return Array.from(tagSet).sort() -} diff --git a/docs/superpowers/plans/2026-04-03-voice-transcription.md b/docs/superpowers/plans/2026-04-03-voice-transcription.md new file mode 100644 index 000000000..96277d52f --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-voice-transcription.md @@ -0,0 +1,166 @@ +# Voice Transcription Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add explicit local/OpenAI voice transcription with local Whisper-small download-on-demand, click-time mic gating, and OpenAI BYOK storage. + +**Architecture:** Introduce a dedicated voice-transcription settings and runtime path separate from embeddings and inline AI. Main process owns provider readiness, local model lifecycle, BYOK key access, and provider dispatch; renderer only shows settings state and asks readiness before opening the recorder. + +**Tech Stack:** Electron IPC, React, TanStack Query, Drizzle settings storage, keytar, OpenAI SDK, `@huggingface/transformers` + +--- + +## File Map + +- Modify: `packages/contracts/src/settings-schemas.ts` +- Modify: `packages/contracts/src/ipc-channels.ts` +- Modify: `apps/desktop/src/preload/index.ts` +- Modify: `apps/desktop/src/preload/index.d.ts` +- Modify: `apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts` +- Modify: `apps/desktop/src/main/ipc/settings-handlers.ts` +- Modify: `apps/desktop/src/main/inbox/capture.ts` +- Modify: `apps/desktop/src/main/inbox/transcription.ts` +- Modify: `apps/desktop/src/renderer/src/pages/settings/ai-section.tsx` +- Modify: `apps/desktop/src/renderer/src/components/capture-input.tsx` +- Modify: `apps/desktop/src/renderer/src/components/quick-capture.tsx` +- Create: `apps/desktop/src/main/inbox/voice-model.ts` +- Create: `apps/desktop/src/main/inbox/voice-settings.ts` +- Create: `apps/desktop/src/main/inbox/voice-keychain.ts` +- Create: `apps/desktop/src/renderer/src/lib/voice-recording-readiness.ts` +- Test: `apps/desktop/src/main/ipc/settings-handlers.test.ts` +- Test: `apps/desktop/src/main/inbox/transcription.test.ts` +- Test: renderer/component tests for mic gating and Settings redirect + +## Chunk 1: Contracts And Settings Surface + +### Task 1: Add contract types for voice transcription settings + +**Files:** +- Modify: `packages/contracts/src/settings-schemas.ts` +- Modify: `packages/contracts/src/ipc-channels.ts` + +- [ ] Step 1: Write failing contract tests or type assertions around new voice settings shape. +- [ ] Step 2: Run targeted contract/type checks and verify failure. +- [ ] Step 3: Add `VoiceTranscriptionSettings` schema/defaults and new IPC channels. +- [ ] Step 4: Run targeted type checks and verify pass. + +### Task 2: Expose new settings methods through preload + +**Files:** +- Modify: `apps/desktop/src/preload/index.ts` +- Modify: `apps/desktop/src/preload/index.d.ts` +- Modify: `apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts` + +- [ ] Step 1: Add failing preload typings or IPC map expectations. +- [ ] Step 2: Run `pnpm ipc:check` and confirm failure if signatures are missing. +- [ ] Step 3: Add preload methods for settings, model status, download, readiness, and API-key save. +- [ ] Step 4: Run `pnpm ipc:check` and verify pass. + +## Chunk 2: Main-Process Voice Settings And Model Runtime + +### Task 3: Add voice settings persistence and BYOK access helpers + +**Files:** +- Create: `apps/desktop/src/main/inbox/voice-settings.ts` +- Create: `apps/desktop/src/main/inbox/voice-keychain.ts` +- Test: `apps/desktop/src/main/ipc/settings-handlers.test.ts` + +- [ ] Step 1: Write failing tests for default provider, provider updates, and masked/present API-key behavior. +- [ ] Step 2: Run `pnpm test -- --project main settings-handlers` or the exact Vitest target and confirm failure. +- [ ] Step 3: Implement SQLite-backed non-secret settings plus keychain-backed OpenAI key helpers. +- [ ] Step 4: Re-run the targeted tests and verify pass. + +### Task 4: Add local Whisper-small model lifecycle manager + +**Files:** +- Create: `apps/desktop/src/main/inbox/voice-model.ts` +- Modify: `apps/desktop/src/main/ipc/settings-handlers.ts` +- Test: `apps/desktop/src/main/ipc/settings-handlers.test.ts` + +- [ ] Step 1: Write failing tests for model status, download initiation, and readiness outcomes. +- [ ] Step 2: Run targeted main tests and confirm failure. +- [ ] Step 3: Implement model cache path resolution, status reporting, download/load orchestration, and progress events. +- [ ] Step 4: Re-run the targeted tests and verify pass. + +## Chunk 3: Provider Dispatch In Inbox Transcription + +### Task 5: Refactor transcription to route through selected provider + +**Files:** +- Modify: `apps/desktop/src/main/inbox/transcription.ts` +- Modify: `apps/desktop/src/main/inbox/capture.ts` +- Test: `apps/desktop/src/main/inbox/transcription.test.ts` + +- [ ] Step 1: Write failing tests for local dispatch, OpenAI dispatch, missing-key block, missing-model block, and no-fallback behavior. +- [ ] Step 2: Run the targeted transcription tests and confirm failure. +- [ ] Step 3: Replace env-only OpenAI logic with provider resolution from voice settings plus readiness-aware transcription dispatch. +- [ ] Step 4: Re-run the targeted transcription tests and verify pass. + +### Task 6: Wire settings IPC to the new voice settings/runtime helpers + +**Files:** +- Modify: `apps/desktop/src/main/ipc/settings-handlers.ts` +- Test: `apps/desktop/src/main/ipc/settings-handlers.test.ts` + +- [ ] Step 1: Extend failing tests for new IPC handlers and returned payload shapes. +- [ ] Step 2: Run targeted main tests and confirm failure. +- [ ] Step 3: Register handlers for voice settings, model status, download, readiness, and BYOK save. +- [ ] Step 4: Re-run targeted tests and verify pass. + +## Chunk 4: Settings UI + +### Task 7: Add the Voice Transcription settings group + +**Files:** +- Modify: `apps/desktop/src/renderer/src/pages/settings/ai-section.tsx` + +- [ ] Step 1: Write failing renderer tests for provider selector, local model status card, and OpenAI key field visibility. +- [ ] Step 2: Run the targeted renderer test and confirm failure. +- [ ] Step 3: Implement the new `Voice Transcription` settings group in the existing AI Settings page. +- [ ] Step 4: Re-run the targeted renderer test and verify pass. + +## Chunk 5: Mic Gating In Renderer + +### Task 8: Add shared readiness guard before opening any recorder + +**Files:** +- Create: `apps/desktop/src/renderer/src/lib/voice-recording-readiness.ts` +- Modify: `apps/desktop/src/renderer/src/components/capture-input.tsx` +- Modify: `apps/desktop/src/renderer/src/components/quick-capture.tsx` + +- [ ] Step 1: Write failing renderer tests asserting mic click opens Settings instead of recorder when the provider is not ready. +- [ ] Step 2: Run the targeted renderer tests and confirm failure. +- [ ] Step 3: Implement a shared readiness helper and use it from each mic-click entry point. +- [ ] Step 4: Re-run the targeted renderer tests and verify pass. + +### Task 9: Keep recorder behavior unchanged once readiness succeeds + +**Files:** +- Modify: `apps/desktop/src/renderer/src/components/voice-recorder.tsx` only if needed +- Test: existing or new renderer/component tests + +- [ ] Step 1: Write or extend a failing test showing recorder still opens and records when readiness is `ready`. +- [ ] Step 2: Run the targeted renderer test and confirm failure. +- [ ] Step 3: Make the minimal code change needed, preferably outside the recorder component. +- [ ] Step 4: Re-run the targeted renderer test and verify pass. + +## Chunk 6: Verification + +### Task 10: Regenerate IPC artifacts and run focused checks + +**Files:** +- Modify: generated artifacts as needed + +- [ ] Step 1: Run `pnpm ipc:generate`. +- [ ] Step 2: Run targeted tests for main and renderer files touched by this feature. +- [ ] Step 3: Fix any failures with the smallest possible change. +- [ ] Step 4: Re-run the same targeted tests until green. + +### Task 11: Run repository verification for the touched surfaces + +- [ ] Step 1: Run `pnpm lint`. +- [ ] Step 2: Run `pnpm typecheck`. +- [ ] Step 3: Run `pnpm test`. +- [ ] Step 4: If any command fails because of known unrelated issues, document the exact failure and confirm whether the touched tests still passed. + +Plan complete and saved to `docs/superpowers/plans/2026-04-03-voice-transcription.md`. Ready to execute? diff --git a/packages/contracts/src/reminders-api.test.ts b/packages/contracts/src/reminders-api.test.ts index 84d935b44..673fc0ddf 100644 --- a/packages/contracts/src/reminders-api.test.ts +++ b/packages/contracts/src/reminders-api.test.ts @@ -1,10 +1,5 @@ import { describe, it, expect } from 'vitest' import { - ReminderTargetTypeSchema, - ReminderStatusSchema, - CreateNoteReminderSchema, - CreateJournalReminderSchema, - CreateHighlightReminderSchema, CreateReminderSchema, UpdateReminderSchema, SnoozeReminderSchema, @@ -13,238 +8,6 @@ import { BulkDismissSchema } from './reminders-api' -// ============================================================================= -// ReminderTargetTypeSchema Tests -// ============================================================================= - -describe('ReminderTargetTypeSchema', () => { - it('should validate note target type', () => { - const result = ReminderTargetTypeSchema.safeParse('note') - expect(result.success).toBe(true) - }) - - it('should validate journal target type', () => { - const result = ReminderTargetTypeSchema.safeParse('journal') - expect(result.success).toBe(true) - }) - - it('should validate highlight target type', () => { - const result = ReminderTargetTypeSchema.safeParse('highlight') - expect(result.success).toBe(true) - }) - - it('should reject invalid target type', () => { - const result = ReminderTargetTypeSchema.safeParse('task') - expect(result.success).toBe(false) - }) -}) - -// ============================================================================= -// ReminderStatusSchema Tests -// ============================================================================= - -describe('ReminderStatusSchema', () => { - it('should validate pending status', () => { - const result = ReminderStatusSchema.safeParse('pending') - expect(result.success).toBe(true) - }) - - it('should validate triggered status', () => { - const result = ReminderStatusSchema.safeParse('triggered') - expect(result.success).toBe(true) - }) - - it('should validate dismissed status', () => { - const result = ReminderStatusSchema.safeParse('dismissed') - expect(result.success).toBe(true) - }) - - it('should validate snoozed status', () => { - const result = ReminderStatusSchema.safeParse('snoozed') - expect(result.success).toBe(true) - }) - - it('should reject invalid status', () => { - const result = ReminderStatusSchema.safeParse('completed') - expect(result.success).toBe(false) - }) -}) - -// ============================================================================= -// CreateNoteReminderSchema Tests -// ============================================================================= - -describe('CreateNoteReminderSchema', () => { - it('should validate correct note reminder', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: 'note-abc123', - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(true) - }) - - it('should validate with optional title and note', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: 'note-abc123', - remindAt: '2025-01-15T09:00:00.000Z', - title: 'Review this note', - note: 'Check the action items' - }) - expect(result.success).toBe(true) - }) - - it('should reject empty noteId', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: '', - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) - - it('should reject invalid datetime format', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: 'note-abc123', - remindAt: '2025-01-15' - }) - expect(result.success).toBe(false) - }) - - it('should reject title exceeding 200 characters', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: 'note-abc123', - remindAt: '2025-01-15T09:00:00.000Z', - title: 'a'.repeat(201) - }) - expect(result.success).toBe(false) - }) - - it('should reject note exceeding 1000 characters', () => { - const result = CreateNoteReminderSchema.safeParse({ - noteId: 'note-abc123', - remindAt: '2025-01-15T09:00:00.000Z', - note: 'a'.repeat(1001) - }) - expect(result.success).toBe(false) - }) -}) - -// ============================================================================= -// CreateJournalReminderSchema Tests -// ============================================================================= - -describe('CreateJournalReminderSchema', () => { - it('should validate correct journal reminder', () => { - const result = CreateJournalReminderSchema.safeParse({ - journalDate: '2025-01-03', - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(true) - }) - - it('should validate with optional fields', () => { - const result = CreateJournalReminderSchema.safeParse({ - journalDate: '2025-01-03', - remindAt: '2025-01-15T09:00:00.000Z', - title: 'Revisit this entry', - note: 'Remember the insights' - }) - expect(result.success).toBe(true) - }) - - it('should reject invalid date format', () => { - const result = CreateJournalReminderSchema.safeParse({ - journalDate: '01-03-2025', - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) -}) - -// ============================================================================= -// CreateHighlightReminderSchema Tests -// ============================================================================= - -describe('CreateHighlightReminderSchema', () => { - it('should validate correct highlight reminder', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'Important passage to remember', - highlightStart: 100, - highlightEnd: 130, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(true) - }) - - it('should validate with optional title and note', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'Key insight', - highlightStart: 50, - highlightEnd: 61, - remindAt: '2025-01-15T09:00:00.000Z', - title: 'Review highlight', - note: 'This was important' - }) - expect(result.success).toBe(true) - }) - - it('should reject highlightEnd not greater than highlightStart', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'Some text', - highlightStart: 100, - highlightEnd: 100, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) - - it('should reject highlightEnd less than highlightStart', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'Some text', - highlightStart: 100, - highlightEnd: 50, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) - - it('should reject empty highlightText', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: '', - highlightStart: 0, - highlightEnd: 10, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) - - it('should reject highlightText exceeding 5000 characters', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'a'.repeat(5001), - highlightStart: 0, - highlightEnd: 5001, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) - - it('should reject negative highlightStart', () => { - const result = CreateHighlightReminderSchema.safeParse({ - noteId: 'note-abc123', - highlightText: 'Some text', - highlightStart: -1, - highlightEnd: 10, - remindAt: '2025-01-15T09:00:00.000Z' - }) - expect(result.success).toBe(false) - }) -}) - // ============================================================================= // CreateReminderSchema (Discriminated Union) Tests // ============================================================================= diff --git a/packages/contracts/src/reminders-api.ts b/packages/contracts/src/reminders-api.ts index aef05d5b6..b8631b978 100644 --- a/packages/contracts/src/reminders-api.ts +++ b/packages/contracts/src/reminders-api.ts @@ -26,45 +26,8 @@ export { reminderTargetType, reminderStatus, type ReminderTargetType, type Remin // Zod Schemas // ============================================================================ -export const ReminderTargetTypeSchema = z.enum(['note', 'journal', 'highlight']) -export const ReminderStatusSchema = z.enum(['pending', 'triggered', 'dismissed', 'snoozed']) - -/** - * Schema for creating a reminder for a note - */ -export const CreateNoteReminderSchema = z.object({ - noteId: z.string().min(1), - remindAt: z.string().datetime(), - title: z.string().max(200).optional(), - note: z.string().max(1000).optional() -}) - -/** - * Schema for creating a reminder for a journal entry - */ -export const CreateJournalReminderSchema = z.object({ - journalDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), // YYYY-MM-DD - remindAt: z.string().datetime(), - title: z.string().max(200).optional(), - note: z.string().max(1000).optional() -}) - -/** - * Schema for creating a reminder for highlighted text - */ -export const CreateHighlightReminderSchema = z - .object({ - noteId: z.string().min(1), - highlightText: z.string().min(1).max(5000), - highlightStart: z.number().int().min(0), - highlightEnd: z.number().int().min(0), - remindAt: z.string().datetime(), - title: z.string().max(200).optional(), - note: z.string().max(1000).optional() - }) - .refine((data) => data.highlightEnd > data.highlightStart, { - message: 'highlightEnd must be greater than highlightStart' - }) +const ReminderTargetTypeSchema = z.enum(['note', 'journal', 'highlight']) +const ReminderStatusSchema = z.enum(['pending', 'triggered', 'dismissed', 'snoozed']) /** * Generic create schema that accepts all reminder types @@ -146,9 +109,6 @@ export const BulkDismissSchema = z.object({ // TypeScript Types // ============================================================================ -export type CreateNoteReminderInput = z.infer -export type CreateJournalReminderInput = z.infer -export type CreateHighlightReminderInput = z.infer export type CreateReminderInput = z.infer export type UpdateReminderInput = z.infer export type SnoozeReminderInput = z.infer diff --git a/packages/shared/src/file-types.ts b/packages/shared/src/file-types.ts index 41d058ac2..2eb48d612 100644 --- a/packages/shared/src/file-types.ts +++ b/packages/shared/src/file-types.ts @@ -129,46 +129,6 @@ export function isEditable(fileType: FileType): boolean { return fileType === 'markdown' } -/** - * Check if a file type is a media type (audio or video) - */ -export function isMedia(fileType: FileType): boolean { - return fileType === 'audio' || fileType === 'video' -} - -/** - * Get a human-readable label for a file type - */ -export function getFileTypeLabel(fileType: FileType): string { - const labels: Record = { - markdown: 'Markdown', - pdf: 'PDF', - image: 'Image', - audio: 'Audio', - video: 'Video' - } - return labels[fileType] -} - -// ============================================================================ -// File Type Icons (for use with @/lib/icons) -// ============================================================================ - -/** - * Get the icon name for a file type - * Use with: import { FileText, FileType2, Image, Music, Video } from '@/lib/icons' - */ -export function getFileTypeIconName(fileType: FileType): string { - const icons: Record = { - markdown: 'FileText', - pdf: 'FileType2', - image: 'Image', - audio: 'Music', - video: 'Video' - } - return icons[fileType] -} - /** * Get the tab icon identifier for a file type * Used when opening file tabs to set the correct icon @@ -188,15 +148,6 @@ export function getTabIconForFileType(fileType: FileType): string { // Glob Patterns (for file watching) // ============================================================================ -/** - * Get glob patterns for all supported file types - * Used by chokidar file watcher - */ -export function getSupportedGlobPatterns(): string[] { - const extensions = getAllSupportedExtensions() - return extensions.map((ext) => `**/*.${ext}`) -} - /** * Check if a path matches any supported file type */