// Typed Tauri client — the frontend's ONLY way to reach the core. // Mirrors docs/04-api-contracts.md. Keep these signatures in sync with // src-tauri/src/commands.rs (a contract test enforces this — see docs/06). import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; export type MeetingId = string; export type BackendId = "npu" | "nvidia" | "amd" | "intel" | "cpu"; // Every backend command rejects an `Err` as this shape, not a native Error // (see src-tauri/src/error.rs) — `e instanceof Error` is always false for it. export interface WaError { kind: string; message: string; } export function errorMessage(e: unknown): string { if (typeof e === "string") return e; if (e instanceof Error) return e.message; if (e && typeof e === "object" && typeof (e as WaError).message === "string") { return (e as WaError).message; } return String(e); } export interface BackendInfo { id: BackendId; name: string; available: boolean; rank: number; vram_mb: number | null; } export interface AppInfo { version: string; commit: string; } export interface HardwareStatus { backends: BackendInfo[]; active: BackendId; modelSize: string; estRtf: number; /** NPU package state (T3.4): chip detected, runtime staged, model fetched. */ npu?: { present: boolean; runtimeReady: boolean; modelInstalled: boolean }; /** DirectML GPU package (P2): applicable = staging would help this build. */ directml?: { applicable: boolean; runtimeReady: boolean; modelInstalled: boolean }; } // One enumerated audio device — a render (playback) device for the loopback // picker (FR-CAP-1) or a capture (microphone) device for the mic picker // (FR-CAP-7). `id` is the persisted `Device::get_id()`; `name` is display-only. export interface AudioDeviceInfo { id: string; name: string; } export interface LlmStatus { provider: string; // ollama|custom|anthropic|off (ADR-0011; "openai" not yet wired) reachable: boolean; isLocal: boolean; models: string[]; } export interface ModelInfo { id: string; label: string; size_mb: number; installed: boolean; active: boolean; /** `false` for `.en` (English-only) ggml variants; `true` for multilingual * ones — gates the Settings language picker (T8.7, FR-TRX-4, M4.2). */ multilingual: boolean; } // One selectable transcription language (T8.7, FR-TRX-4) — ISO-639-1 code // as accepted by whisper.cpp, plus a display label. export interface LanguageOption { code: string; label: string; } export type MeetingStatus = "recording" | "transcribing" | "ready" | "recovering" | "error"; export interface MeetingListItem { id: MeetingId; title: string; started_at: number; duration_secs: number | null; status: MeetingStatus; tags: string[]; } // Full-text search hit (Phase 8, FR-SEARCH-1) — same shape as MeetingListItem // plus a highlighted excerpt of what matched. export interface SearchHit { id: MeetingId; title: string; started_at: number; duration_secs: number | null; status: MeetingStatus; tags: string[]; snippet: string; } // list_meetings filters (Phase 8, FR-SEARCH-2). All fields optional/ANDed. export interface MeetingFilter { query?: string; tag?: string; participantId?: string; from?: number; to?: number; } export interface TranscriptSegment { id: number; start_ms: number; end_ms: number; speaker: string; text: string; confidence: number | null; interim: boolean; } export interface SpeakerInfo { label: string; display_name: string | null; participant_id: string | null; } // Drafted action item from a summary (Phase 5, FR-LLM-3). `id` is only set // once `confirmActionItems` has persisted it as a row. export interface ActionItem { id: string | null; text: string; owner: string | null; due_at: number | null; confirmed: boolean; // Schedule a local OS reminder for due_at (Phase 8, T8.6, FR-CAL-5). reminder_set: boolean; } // On-disk shape of summary.json (FR-LLM-2/4) — `null` until generate_summary // has run at least once for a meeting. export interface SummaryFile { schema: number; generated_at: number; provider: string; model: string; summary_md: string; decisions: string[]; action_items: ActionItem[]; } // Full meeting detail (get_meeting) — DB row + transcript + speakers + notes. export interface Meeting { id: MeetingId; title: string; started_at: number; ended_at: number | null; duration_secs: number | null; status: MeetingStatus; recorded: boolean; // Transcription language actually used/selected for this meeting (T8.7, // FR-TRX-4) — an ISO-639-1 code, or null if never resolved (e.g. no audio // was ever decoded). language: string | null; backend_used: string | null; model_used: string | null; segments: TranscriptSegment[]; speakers: SpeakerInfo[]; notes_markdown: string; summary: SummaryFile | null; // Confirmed/edited action items (table-backed source of truth) — falls back // to summary.action_items drafts until anything is saved. Manage via // confirmActionItems (add/edit/delete). action_items: ActionItem[]; calendar_event_id: string | null; tags: string[]; template_id: string | null; } // Note template (Phase 8, T8.1, FR-NOTE-5) — section headers applied to // notes.md at recording-start time. export interface NoteTemplate { id: string; name: string; sections: string[]; } // Calendar / .pst (Phase 6, FR-CAL-*). export interface CalendarEvent { id: string; source: string; // pst|graph|ics subject: string | null; organizer: string | null; starts_at: number | null; ends_at: number | null; description: string | null; raw_uid: string | null; } export interface Participant { id: string; name: string; email: string | null; role: string | null; // organizer|required|optional } export interface CalendarEventDetail { event: CalendarEvent; participants: Participant[]; } export interface CalendarCleanupResult { deleted: number; protected: number; } export type SyncKind = "webdav" | "onedrive" | "dropbox" | "box"; // What the UI sees about a target — NEVER includes the secret (FR-SYNC-6). export interface SyncTargetInfo { id: string; name: string; kind: SyncKind; provider_hint: string | null; // nextcloud|owncloud|cloudreve|seafile|synology|generic base_url: string | null; remote_base_path: string; username: string | null; enabled: boolean; third_party: boolean; host: string | null; upload_transcript: boolean; upload_notes: boolean; upload_summary: boolean; upload_recording: boolean; trigger_on_finalize: boolean; allow_plaintext_lan: boolean; encrypt_before_upload: boolean; } // privacy_self_check() response (FR-SEC-2) — proves local-only handling. export interface PrivacySyncTarget { name: string; host: string; thirdParty: boolean; tls: boolean; } export interface PrivacySelfCheck { llmEndpoint: string; llmIsLocal: boolean; syncEnabled: boolean; syncTargets: PrivacySyncTarget[]; allowlistedHosts: string[]; } // Payload for add/update. `secret` is write-only (stored in the OS credential store). export interface SyncTargetConfig { id?: string; name: string; kind: SyncKind; provider_hint?: string; base_url?: string; remote_base_path?: string; username?: string; secret?: string; enabled?: boolean; upload_transcript?: boolean; upload_notes?: boolean; upload_summary?: boolean; upload_recording?: boolean; trigger_on_finalize?: boolean; allow_plaintext_lan?: boolean; encrypt_before_upload?: boolean; } // A durable upload job for one artifact × target (sync_status). Snake_case, // matching the Rust SyncJobInfo; the sync://job *event* is camelCase (onSyncJob). export interface SyncJobInfo { id: string; target_id: string; meeting_id: MeetingId; artifact: string; status: string; // pending|uploading|done|failed|skipped attempts: number; bytes_sent: number; bytes_total: number | null; last_error: string | null; } export interface AppSettings { theme: string; storage_root: string; llm_provider: string; llm_endpoint: string; llm_model: string; /** Advanced Ollama tuning; sparse (only non-default overrides). */ llm_advanced?: { system?: string; think?: string; keep_alive?: string; options?: Record; } | null; preferred_backend: string; whisper_model: string; /** Default transcription language (T8.7, FR-TRX-4): `null` = auto-detect, * an ISO-639-1 code forces that language. Only takes effect with a * multilingual model — see `ModelInfo.multilingual`. */ whisper_language: string | null; low_overhead: boolean; default_record: boolean; consent_acknowledged: boolean; /** One-time "data leaves your device" ack for hosted (non-local) AI * providers — Anthropic today (ADR-0011, T10.3). Independent of * consent_acknowledged (that one's about recording law). */ hosted_ai_acknowledged: boolean; sync_enabled: boolean; mcp_enabled: boolean; mcp_transport: string; // http|stdio mcp_port: number; mcp_expose: string; // none|selected|all mcp_expose_recordings: boolean; retention_max_age_days: number | null; retention_max_size_gb: number | null; pst_last_path: string | null; pst_auto_sync: boolean; // Days of history to import (both a manual Import click and pst_auto_sync); // null = full mailbox history. pst_import_range_days: number | null; /** Auto-start recording when a calendar event begins while the app is open * (opt-in, off by default). Armed as a one-shot UI timer — nothing polls. */ auto_record_calendar: boolean; audio_output_device: string | null; microphone_enabled: boolean; audio_input_device: string | null; } // Feature brief — agent-ready spec distilled from a meeting (ADR-0011). export interface FeatureBrief { id: string; meeting_id: MeetingId; title: string; problem: string; desired_outcome: string; acceptance_criteria: string[]; target_repo: string | null; context_excerpts: { speaker: string; text: string }[]; } export interface FeatureBriefInfo { id: string; meeting_id: MeetingId; title: string; target_repo: string | null; exposed: boolean; } export interface McpAccessEntry { at: number; tool: string; meeting_id: MeetingId | null; client: string | null; } // mcp_status() response (FR-MCP-1/6). `endpoint` is empty while disabled. export interface McpStatus { enabled: boolean; transport: "http" | "stdio"; endpoint: string; tokenSet: boolean; exposeScope: "none" | "selected" | "all"; } // ---- Commands ---- export const api = { // `record` controls audio RETENTION (default false / off — ADR-0009). // `language` (T8.7, FR-TRX-4): omit/undefined falls back to // Settings.whisper_language; "auto" or omitted both mean auto-detect. startRecording: ( meetingTitle?: string, calendarEventId?: string, record = false, templateId?: string, language?: string, ) => invoke("start_recording", { args: { meetingTitle, calendarEventId, record, templateId, language }, }), listNoteTemplates: () => invoke("list_note_templates"), stopRecording: (meetingId: MeetingId) => invoke("stop_recording", { meetingId }), // Abandon an accidental recording: stop + delete files + drop the DB row. cancelRecording: (meetingId: MeetingId) => invoke("cancel_recording", { meetingId }), // Absolute path to a playable audio.wav (decrypted if sealed), for convertFileSrc. recordingPlaybackPath: (meetingId: MeetingId) => invoke("recording_playback_path", { meetingId }), pauseRecording: (meetingId: MeetingId) => invoke("pause_recording", { meetingId }), resumeRecording: (meetingId: MeetingId) => invoke("resume_recording", { meetingId }), setRecordingRetention: (meetingId: MeetingId, record: boolean) => invoke("set_recording_retention", { meetingId, record }), acknowledgeRecordingConsent: () => invoke("acknowledge_recording_consent"), // Live notes (Granola-style redesign): both live-session only, err once // the meeting is finalized — use updateNotes on the merged notes.md instead. updateLiveNotes: (meetingId: MeetingId, markdown: string) => invoke("update_live_notes", { meetingId, markdown }), // anchorMs: the clicked segment's start_ms (not its id). text: "" clears it. setSegmentNote: (meetingId: MeetingId, anchorMs: number, text: string) => invoke("set_segment_note", { meetingId, anchorMs, text }), appInfo: () => invoke("app_info"), openUrl: (url: string) => invoke("open_url", { url }), hardwareStatus: () => invoke("hardware_status"), listAudioDevices: () => invoke("list_audio_devices"), listInputDevices: () => invoke("list_input_devices"), setPreferredBackend: (backend: BackendId | "auto") => invoke("set_preferred_backend", { args: { backend } }), downloadNpuPackage: () => invoke("download_npu_package"), downloadDirectmlPackage: () => invoke("download_directml_package"), listModels: () => invoke("list_models"), // The fixed segmentation+embedding pair that speaker diarization needs // installed before it can separate speakers (T4.7, FR-MODEL-1). Same // ModelInfo shape as whisper models; download via `downloadModel` with the // `diar-seg`/`diar-emb` kind, remove via the shared `removeModel`. listDiarizationModels: () => invoke("list_diarization_models"), // T8.7/FR-TRX-4: static catalog of whisper.cpp-recognized language codes // for the Settings dropdown; "Auto-detect" is a frontend-only addition. listWhisperLanguages: () => invoke("list_whisper_languages"), downloadModel: (id: string, kind: "whisper" | "diar-seg" | "diar-emb" = "whisper") => invoke("download_model", { args: { kind, id } }), removeModel: (id: string) => invoke("remove_model", { id }), // `language` (T8.7): omit/undefined reuses whatever language the meeting // already had rather than resetting it to auto. reprocessTranscript: (meetingId: MeetingId, model: string, language?: string) => invoke("reprocess_transcript", { meetingId, model, language }), // Manually add a meeting from an existing recording — a local audio/video // file path or a URL (YouTube/streaming page or direct media URL). Requires // ffmpeg (and yt-dlp for URLs) on PATH; neither is bundled. Returns the new // meeting's id once transcription + diarization have finished. importMedia: (source: string, title?: string) => invoke("import_media", { source, title }), resumeTranscription: (meetingId: MeetingId) => invoke("resume_transcription", { meetingId }), listMeetings: (filter?: MeetingFilter) => invoke("list_meetings", { query: filter?.query, tag: filter?.tag, participantId: filter?.participantId, from: filter?.from, to: filter?.to, }), // Full-text search across transcripts + notes — distinct from listMeetings' // `query`, which only substring-matches the title. search: (query: string) => invoke("search", { query }), setTags: (meetingId: MeetingId, tags: string[]) => invoke("set_tags", { meetingId, tags }), listTags: () => invoke("list_tags"), getMeeting: (meetingId: MeetingId) => invoke("get_meeting", { meetingId }), deleteMeeting: (meetingId: MeetingId) => invoke("delete_meeting", { meetingId }), updateNotes: (meetingId: MeetingId, markdown: string) => invoke("update_notes", { meetingId, markdown }), // dest is a file path for md/pdf/docx/obsidian, a folder for bundle. // "obsidian" writes one self-contained vault note (no audio) — FR-STORE-4. exportMeeting: ( meetingId: MeetingId, dest: string, format: "md" | "pdf" | "docx" | "bundle" | "obsidian", ) => invoke("export_meeting", { meetingId, dest, format }), // Every meeting matching tag/date filters, one file (or bundle folder) per // meeting under destDir. Returns the count actually exported (T8.5, FR-STORE-4). bulkExportMeetings: ( destDir: string, format: "md" | "pdf" | "docx" | "bundle", filter?: Pick, ) => invoke("bulk_export_meetings", { destDir, format, tag: filter?.tag, from: filter?.from, to: filter?.to, }), // Import bundle(s) exported with format "bundle" — dir is a single bundle // folder or a parent folder of them. Reconstructs each under a fresh id and // returns the count imported (FR-STORE-4). importMeetingBundle: (dir: string) => invoke("import_meeting_bundle", { dir }), llmStatus: () => invoke("llm_status"), // provider ∈ ollama|custom|anthropic|openai|off; apiKey (hosted) → OS credential store (ADR-0011). setLlmProvider: (config: { provider: string; endpoint?: string; model?: string; apiKey?: string; }) => invoke("set_llm_provider", { args: config }), generateSummary: (meetingId: MeetingId, templateId?: string) => invoke("generate_summary", { meetingId, templateId }), confirmActionItems: (meetingId: MeetingId, items: ActionItem[]) => invoke("confirm_action_items", { meetingId, items }), generateTags: (meetingId: MeetingId) => invoke("generate_tags", { meetingId }), // rangeDays: only import events starting within the last N days; omitted/undefined imports // the full mailbox history (a long-lived .pst otherwise re-imports years of recurring/holiday // entries on every launch when pst_auto_sync is on). importPst: (path: string, password?: string, rangeDays?: number) => invoke("import_pst", { path, password, rangeDays }), // olderThanDays: undefined deletes every unlinked event ("Delete all"). // An event attached to a recorded meeting is always kept either way. cleanupCalendarEvents: (olderThanDays?: number) => invoke("cleanup_calendar_events", { olderThanDays }), listCalendarEvents: (from?: number, to?: number) => invoke("list_calendar_events", { from, to }), getCalendarEvent: (eventId: string) => invoke("get_calendar_event", { eventId }), attachMeetingToEvent: (meetingId: MeetingId, eventId: string) => invoke("attach_meeting_to_event", { meetingId, eventId }), renameMeeting: (meetingId: MeetingId, title: string) => invoke("rename_meeting", { meetingId, title }), renameSpeaker: (meetingId: MeetingId, label: string, name: string) => invoke("rename_speaker", { meetingId, label, name }), mapSpeakerToParticipant: (meetingId: MeetingId, label: string, participantId: string) => invoke("map_speaker_to_participant", { meetingId, label, participantId }), // Sync (ADR-0010) — off by default; secrets never returned by listSyncTargets. listSyncTargets: () => invoke("list_sync_targets"), addSyncTarget: (config: SyncTargetConfig) => invoke("add_sync_target", { config }), updateSyncTarget: (config: SyncTargetConfig & { id: string }) => invoke("update_sync_target", { config }), removeSyncTarget: (id: string) => invoke("remove_sync_target", { id }), testSyncTarget: (configOrId: SyncTargetConfig | { id: string }) => invoke<{ ok: boolean; message: string }>("test_sync_target", { config: configOrId }), setSyncEnabled: (enabled: boolean) => invoke("set_sync_enabled", { enabled }), beginOauthLink: (kind: "onedrive" | "dropbox" | "box") => invoke<{ authUrl: string }>("begin_oauth_link", { kind }), syncMeeting: (meetingId: MeetingId, targetId?: string) => invoke("sync_meeting", { meetingId, targetId }), syncStatus: (meetingId?: MeetingId) => invoke("sync_status", { meetingId }), retrySyncJob: (jobId: string) => invoke("retry_sync_job", { jobId }), // Feature briefs + MCP server (ADR-0011). MCP server is loopback-only and off by default. createFeatureBrief: (meetingId: MeetingId, targetRepo?: string) => invoke("create_feature_brief", { meetingId, targetRepo }), listFeatureBriefs: (meetingId?: MeetingId) => invoke("list_feature_briefs", { meetingId }), getFeatureBrief: (id: string) => invoke("get_feature_brief", { id }), setBriefExposed: (id: string, exposed: boolean) => invoke("set_brief_exposed", { id, exposed }), mcpStatus: () => invoke("mcp_status"), setMcpEnabled: (enabled: boolean, transport?: "http" | "stdio", port?: number) => invoke<{ endpoint: string; token: string }>("set_mcp_enabled", { enabled, transport, port }), setMcpScope: (expose: "none" | "selected" | "all", exposeRecordings?: boolean) => invoke("set_mcp_scope", { expose, exposeRecordings }), mcpAccessLog: (limit?: number) => invoke("mcp_access_log", { limit }), // Layer 3 (later) push handoff. runAgent: ( briefId: string, tool: "claude" | "codex" | "opencode" | "copilot", repoPath: string, ) => invoke<{ ok: boolean }>("run_agent", { briefId, tool, repoPath }), createIssueFromBrief: (briefId: string, tracker: "github", assignCopilot?: boolean) => invoke<{ url: string }>("create_issue_from_brief", { briefId, tracker, assignCopilot }), getSettings: () => invoke("get_settings"), updateSettings: (patch: Partial) => invoke("update_settings", { patch }), privacySelfCheck: () => invoke("privacy_self_check"), // At-rest encryption vault (Phase 8, T8.8, FR-SEC-3). vaultStatus: () => invoke<{ enabled: boolean; unlocked: boolean }>("vault_status"), enableVault: (password: string) => invoke("enable_vault", { args: { password } }), unlockVault: (password: string) => invoke("unlock_vault", { args: { password } }), lockVault: () => invoke("lock_vault"), changeVaultPassword: (oldPassword: string, newPassword: string) => invoke("change_vault_password", { args: { oldPassword, newPassword } }), }; // ---- Events (Rust → UI) ---- export const events = { onRecordingState: (cb: (p: unknown) => void): Promise => listen("recording://state", (e) => cb(e.payload)), onRetention: (cb: (p: { meetingId: string; record: boolean }) => void): Promise => listen("recording://retention", (e) => cb(e.payload as never)), onLevel: ( cb: (p: { meetingId: string; rms: number; peak: number; mic: boolean }) => void, ): Promise => listen("recording://level", (e) => cb(e.payload as never)), onDeviceChanged: ( cb: (p: { meetingId: string; recovered: boolean; message: string }) => void, ): Promise => listen("recording://device", (e) => cb(e.payload as never)), onSegment: ( cb: (p: { meetingId: string; segment: TranscriptSegment }) => void, ): Promise => listen("transcript://segment", (e) => cb(e.payload as never)), onFinalized: ( cb: (p: { meetingId: string; segmentCount: number }) => void, ): Promise => listen("transcript://finalized", (e) => cb(e.payload as never)), onLlmToken: (cb: (p: { meetingId: string; text: string }) => void): Promise => listen("llm://token", (e) => cb(e.payload as never)), onLlmDone: (cb: (p: { meetingId: string; summary: SummaryFile }) => void): Promise => listen("llm://done", (e) => cb(e.payload as never)), onModelProgress: ( cb: (p: { id: string; receivedBytes: number; totalBytes: number | null }) => void, ): Promise => listen("model://progress", (e) => cb(e.payload as never)), onHardwareChanged: ( cb: (p: { active: BackendId; reason: string }) => void, ): Promise => listen("hardware://changed", (e) => cb(e.payload as never)), onNpuDownload: ( cb: (p: { stage: "model" | "runtime" | "done"; file?: number; received?: number; total?: number | null; copied?: number; ready?: boolean; }) => void, ): Promise => listen("npu://download", (e) => cb(e.payload as never)), onSyncJob: ( cb: (p: { jobId: string; meetingId: string; targetId: string; artifact: string; status: string; bytesSent: number; bytesTotal: number | null; attempts?: number; error?: string | null; }) => void, ): Promise => listen("sync://job", (e) => cb(e.payload as never)), onSyncLinked: ( cb: (p: { ok: boolean; kind: string; error?: string }) => void, ): Promise => listen("sync://linked", (e) => cb(e.payload as never)), // Live tail of the FR-MCP-5 audit log (camelCase on the wire, unlike the // snake_case McpAccessEntry rows `mcpAccessLog()` returns). onMcpAccess: ( cb: (p: { at: number; tool: string; meetingId?: MeetingId; client?: string }) => void, ): Promise => listen("mcp://access", (e) => cb(e.payload as never)), onAgentProgress: ( cb: (p: { briefId: string; tool: string; line: string }) => void, ): Promise => listen("agent://progress", (e) => cb(e.payload as never)), onPstProgress: (cb: (p: { processed: number; total: number }) => void): Promise => listen("pst://progress", (e) => cb(e.payload as never)), onError: (cb: (p: { kind: string; message: string }) => void): Promise => listen("error", (e) => cb(e.payload as never)), };