From 898108db9b29d64dc1b749721a2916dc56056de7 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 8 May 2026 23:21:55 +0800 Subject: [PATCH 01/27] {"schema":"maestro/commit/1","summary":"Record contextual voice layer decision","authority":"manual"} --- docs/decisions/contextual-voice-layer.md | 30 ++++++++++++++++++++++++ docs/decisions/index.md | 3 +++ 2 files changed, 33 insertions(+) create mode 100644 docs/decisions/contextual-voice-layer.md diff --git a/docs/decisions/contextual-voice-layer.md b/docs/decisions/contextual-voice-layer.md new file mode 100644 index 0000000..77ac40b --- /dev/null +++ b/docs/decisions/contextual-voice-layer.md @@ -0,0 +1,30 @@ +# Contextual Voice Layer + +Status: accepted + +Date: 2026-05-08 + +Question: What product shape should Voxit use as it grows beyond basic speech-to-text? + +Decision: Voxit is a menu bar-first contextual voice input layer for macOS. It should +feel like an input utility that works inside the user's current app, not like a +standalone voice chat app. The menu bar owns always-available control and status, a +recording HUD owns the active dictation moment, the main Voxit window owns user-facing +work assets such as profiles and prompt routing, and Settings owns app preferences. + +Consequences: + +- The primary product action happens in the focused app, with Voxit capturing context, + transforming speech, and inserting output back into that app. +- Voxit differentiates from basic ASR by selecting a prompt profile from the current + app context before final output is produced. +- The main Voxit window is a control center for activity, app rules, profiles, + glossary, prompt experiments, and debug/evaluation surfaces. +- The Settings window stays separate and limited to app preferences such as startup, + shortcuts, microphone, permissions, account defaults, privacy, logging, and + notifications. +- Swift owns the native macOS presentation layer and UI glue. Rust owns durable product + logic, context classification, prompt profile selection, voice session planning, + output policy, and provider orchestration. +- Platform-specific hosts may add their own UI surfaces, but they must consume + Rust-owned contracts instead of redefining contextual voice behavior in each host. diff --git a/docs/decisions/index.md b/docs/decisions/index.md index 984d4bf..048c025 100644 --- a/docs/decisions/index.md +++ b/docs/decisions/index.md @@ -27,3 +27,6 @@ Question this index answers: "why is it shaped this way?" - [`native-ui-boundary.md`](./native-ui-boundary.md) records the Rust Core plus platform-native UI boundary for the macOS-first host. +- [`contextual-voice-layer.md`](./contextual-voice-layer.md) records the + menu bar-first contextual voice input product shape and main-window versus Settings + split. From e1f2c8a419dab786e9da00c7345f0375a6180d32 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 8 May 2026 23:24:28 +0800 Subject: [PATCH 02/27] {"schema":"maestro/commit/1","summary":"Add contextual voice planning contract","authority":"manual"} --- docs/spec/contextual-voice.md | 168 ++++++++++++++ docs/spec/index.md | 3 + docs/spec/runtime.md | 3 + packages/voxit-core/src/contextual.rs | 303 ++++++++++++++++++++++++++ packages/voxit-core/src/lib.rs | 5 + 5 files changed, 482 insertions(+) create mode 100644 docs/spec/contextual-voice.md create mode 100644 packages/voxit-core/src/contextual.rs diff --git a/docs/spec/contextual-voice.md b/docs/spec/contextual-voice.md new file mode 100644 index 0000000..5dba632 --- /dev/null +++ b/docs/spec/contextual-voice.md @@ -0,0 +1,168 @@ +# Contextual Voice Specification + +Purpose: Define the product contract for context-aware voice input, prompt profile +routing, interaction tiers, and host/core ownership. + +Status: normative + +Read this when: You need the authoritative contract for app-specific voice behavior, +prompt profile selection, reasoning effort, output policy, or the Rust versus Swift +boundary for contextual dictation. + +Not this document: Step-by-step onboarding, visual design rationale, or provider API +integration details. + +Defines: + +- contextual voice input as the product layer above raw transcription +- the three user-facing interaction tiers +- `FocusedAppContext -> VoiceSessionPlan` routing ownership +- prompt profile, reasoning effort, and output policy contracts +- Swift host responsibilities versus Rust Core responsibilities + +## 1) Product Contract + +Voxit is a contextual voice input layer. The app should not treat speech-to-text as +the final product boundary. A session must produce output that is shaped by the app +where the user started dictation, the configured prompt profile, and the selected +output policy. + +The durable product pipeline is: + +```text +audio input +-> live transcript or semantic speech turns +-> focused app context snapshot +-> prompt profile selection +-> voice session plan +-> final text or action proposal +-> guarded insert, paste, or confirmation +``` + +The pipeline may use a transcription-only model, a text rewrite model, a realtime +reasoning voice model, or a combination. Provider selection is an implementation +detail; the contract is the context-aware session plan. + +## 2) Interaction Tiers + +### Fast Dictation + +Fast Dictation is the lowest-friction path. It is used when the user needs clean text +quickly and there is no strong app-specific transformation requirement. + +Required behavior: + +- minimize latency +- default to direct insertion or paste +- avoid broad expansion +- preserve spoken content unless a configured cleanup rule applies + +### Context Rewrite + +Context Rewrite is the primary differentiator from basic ASR. It uses the focused app +context and a prompt profile to turn speech into the form expected by the destination. + +Required behavior: + +- select or confirm an app-specific prompt profile before final output +- shape tone, structure, vocabulary, and formatting for the destination app +- preserve high-precision entities unless the user explicitly asks to change them +- expose enough state for the HUD to show which profile is active + +Examples: + +- Linear or GitHub: issue, comment, review, or acceptance-criteria style +- Slack or Discord: concise conversational text +- Mail: complete but restrained email prose +- Code editors: code-editing instructions that preserve identifiers and file names +- Terminal: command proposals, with confirmation before execution-oriented output + +### Voice Intent + +Voice Intent is used when the user asks Voxit to produce an action proposal, structured +artifact, or multi-step transformation rather than plain text. + +Required behavior: + +- prefer preview or confirmation before externally visible or destructive outcomes +- separate the proposed output from any future execution step +- use stronger reasoning only when the workflow needs it +- make the selected output policy explicit in the session plan + +## 3) Focused App Context + +`FocusedAppContext` is the host-collected input to Rust-owned routing. It may contain: + +- bundle id +- app name +- window title +- URL domain +- focused element role +- selected-text presence + +Hosts should collect only the least sensitive data needed for routing. Full selected +text or document contents are not part of the default context contract. + +## 4) Prompt Profiles + +A `PromptProfile` defines how speech should become output for a destination. Profiles +belong to Rust Core so platform hosts share the same behavior. + +Profile contracts include: + +- stable profile id +- display title +- interaction tier +- default reasoning effort +- default output policy + +Custom profiles may be user-defined later, but built-in profile routing remains +deterministic and testable. + +## 5) Reasoning Effort + +Reasoning effort is a session-planning property, not a UI preference alone. + +Default policy: + +- `minimal` for Fast Dictation +- `low` for common Context Rewrite +- `medium` for Voice Intent or high-precision routing +- `high` only when deeper reasoning materially improves the result + +Latency-sensitive paths should use the lowest reasoning effort that satisfies the +workflow. + +## 6) Output Policy + +Output policy defines what the app may do with the final output: + +- `insert_text`: insert or paste final text directly +- `preview_before_insert`: show the output before insertion +- `confirm_before_action`: require confirmation before action-like or risky output + +Terminal and future automation surfaces must not skip confirmation for action-like +output. + +## 7) Ownership Boundary + +Rust Core owns: + +- focused-context data contracts +- prompt profile definitions +- deterministic routing from context to profile +- voice session planning +- reasoning effort and output policy selection +- provider orchestration and model-specific prompt construction + +Swift hosts own: + +- menu bar, HUD, main window, and Settings presentation +- macOS-specific context capture +- permission prompts and native controls +- rendering Rust-owned snapshots and session plans +- user confirmation UX + +Swift must not become the durable source of contextual voice rules. If a rule affects +which profile, prompt, reasoning effort, or output policy applies, the rule belongs in +Rust Core. diff --git a/docs/spec/index.md b/docs/spec/index.md index 60156d0..fb69764 100644 --- a/docs/spec/index.md +++ b/docs/spec/index.md @@ -57,3 +57,6 @@ Then keep the body explicit: - [`runtime.md`](./runtime.md) defines the runtime scope, auth flow, audio capture, transcript pipeline, paste behavior, configuration keys, and known gaps. +- [`contextual-voice.md`](./contextual-voice.md) defines context-aware voice input, + prompt profile routing, interaction tiers, reasoning effort, output policy, and the + host/core ownership boundary. diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 638f4ca..8cc2bf2 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -24,6 +24,9 @@ Defines: - Build entrypoint is the SwiftPM native macOS host under `native/macos-host/`. - Voxit uses Rust Core plus a platform host: shared runtime and platform-neutral model contracts stay in Rust crates, while Swift owns the macOS UI. +- Context-aware voice behavior is governed by + [`contextual-voice.md`](./contextual-voice.md). The runtime must treat raw + transcription as one step in a broader contextual voice input pipeline. - The staged app bundle is a menu bar utility (`LSUIElement = true`) with a SwiftUI `MenuBarExtra` and an on-demand Voxit window. - The app supports English-first behavior and configuration defaults (`language = "en"`). diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs new file mode 100644 index 0000000..165b113 --- /dev/null +++ b/packages/voxit-core/src/contextual.rs @@ -0,0 +1,303 @@ +//! Context-aware voice input planning contracts. +//! +//! Rust Core owns contextual voice behavior so native hosts can stay focused on +//! platform UI, context capture, and user confirmation. + +/// User-facing interaction tier selected for a voice session. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoiceInteractionTier { + /// Lowest-latency speech-to-clean-text path. + FastDictation, + /// App-aware rewrite path that shapes output for the destination. + ContextRewrite, + /// Intent-oriented path that produces a preview or action proposal. + VoiceIntent, +} + +/// Reasoning effort requested for a contextual voice session. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoiceReasoningEffort { + /// Use the fastest viable reasoning path. + Minimal, + /// Use light reasoning for common contextual rewrites. + Low, + /// Use deeper reasoning for multi-step or high-precision output. + Medium, + /// Use the strongest reasoning for constrained or failure-sensitive output. + High, +} + +/// Policy for applying the final voice output. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoiceOutputPolicy { + /// Insert or paste final text directly. + InsertText, + /// Show the output before insertion. + PreviewBeforeInsert, + /// Require confirmation before action-like output. + ConfirmBeforeAction, +} + +/// Host-collected context for the app that was focused when dictation started. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct FocusedAppContext { + /// Focused app bundle id. + pub bundle_id: Option, + /// Focused app display name. + pub app_name: Option, + /// Focused window title when available. + pub window_title: Option, + /// Browser or webview URL domain when available. + pub url_domain: Option, + /// Focused accessibility element role when available. + pub focused_element_role: Option, + /// Whether selected text was present when capture started. + pub selected_text_present: bool, +} +impl FocusedAppContext { + /// Build an empty focused app context. + pub fn new() -> Self { + Self::default() + } + + /// Attach app identity to the context. + pub fn with_app(mut self, bundle_id: impl Into, app_name: impl Into) -> Self { + self.bundle_id = Some(bundle_id.into()); + self.app_name = Some(app_name.into()); + + self + } + + /// Attach a URL domain to the context. + pub fn with_url_domain(mut self, url_domain: impl Into) -> Self { + self.url_domain = Some(url_domain.into()); + + self + } +} + +/// Prompt profile selected by contextual routing. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PromptProfile { + /// Stable profile id. + pub id: String, + /// Human-readable profile title. + pub title: String, + /// User-facing interaction tier. + pub tier: VoiceInteractionTier, + /// Default reasoning effort for this profile. + pub reasoning_effort: VoiceReasoningEffort, + /// Default output policy for this profile. + pub output_policy: VoiceOutputPolicy, +} +impl PromptProfile { + /// Build a prompt profile. + pub fn new( + id: impl Into, + title: impl Into, + tier: VoiceInteractionTier, + reasoning_effort: VoiceReasoningEffort, + output_policy: VoiceOutputPolicy, + ) -> Self { + Self { id: id.into(), title: title.into(), tier, reasoning_effort, output_policy } + } +} + +/// Concrete plan for one voice session. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoiceSessionPlan { + /// Selected profile id. + pub profile_id: String, + /// Selected profile display title. + pub profile_title: String, + /// Selected interaction tier. + pub tier: VoiceInteractionTier, + /// Selected reasoning effort. + pub reasoning_effort: VoiceReasoningEffort, + /// Selected output policy. + pub output_policy: VoiceOutputPolicy, +} +impl VoiceSessionPlan { + fn from_profile(profile: PromptProfile) -> Self { + Self { + profile_id: profile.id, + profile_title: profile.title, + tier: profile.tier, + reasoning_effort: profile.reasoning_effort, + output_policy: profile.output_policy, + } + } +} + +/// Deterministic router from focused app context to a voice session plan. +#[derive(Clone, Debug, Default)] +pub struct ContextualVoiceRouter; +impl ContextualVoiceRouter { + /// Plan a contextual voice session from focused app context. + pub fn plan_for_context(&self, context: &FocusedAppContext) -> VoiceSessionPlan { + let profile = if context_matches_any(context, &["com.tinyspeck.slackmacgap", "discord"]) { + messaging_profile() + } else if context_matches_any(context, &["com.apple.mail"]) { + mail_profile() + } else if context_matches_any(context, &["cursor", "vscode", "xcode"]) { + code_editor_profile() + } else if context_matches_any(context, &["terminal", "iterm"]) { + terminal_profile() + } else if domain_matches_any(context, &["linear.app", "github.com"]) { + work_tracker_profile() + } else { + default_dictation_profile() + }; + + VoiceSessionPlan::from_profile(profile) + } +} + +fn context_matches_any(context: &FocusedAppContext, needles: &[&str]) -> bool { + context_text(context).is_some_and(|text| needles.iter().any(|needle| text.contains(needle))) +} + +fn domain_matches_any(context: &FocusedAppContext, domains: &[&str]) -> bool { + context + .url_domain + .as_deref() + .map(str::to_ascii_lowercase) + .is_some_and(|domain| domains.iter().any(|needle| domain.ends_with(needle))) +} + +fn context_text(context: &FocusedAppContext) -> Option { + let mut values = Vec::new(); + + if let Some(bundle_id) = context.bundle_id.as_deref() { + values.push(bundle_id); + } + if let Some(app_name) = context.app_name.as_deref() { + values.push(app_name); + } + + if values.is_empty() { None } else { Some(values.join(" ").to_ascii_lowercase()) } +} + +fn default_dictation_profile() -> PromptProfile { + PromptProfile::new( + "fast-dictation", + "Fast Dictation", + VoiceInteractionTier::FastDictation, + VoiceReasoningEffort::Minimal, + VoiceOutputPolicy::InsertText, + ) +} + +fn messaging_profile() -> PromptProfile { + PromptProfile::new( + "messaging", + "Messaging", + VoiceInteractionTier::ContextRewrite, + VoiceReasoningEffort::Low, + VoiceOutputPolicy::InsertText, + ) +} + +fn mail_profile() -> PromptProfile { + PromptProfile::new( + "mail", + "Mail", + VoiceInteractionTier::ContextRewrite, + VoiceReasoningEffort::Low, + VoiceOutputPolicy::PreviewBeforeInsert, + ) +} + +fn code_editor_profile() -> PromptProfile { + PromptProfile::new( + "code-editor", + "Code Editor", + VoiceInteractionTier::ContextRewrite, + VoiceReasoningEffort::Low, + VoiceOutputPolicy::PreviewBeforeInsert, + ) +} + +fn terminal_profile() -> PromptProfile { + PromptProfile::new( + "terminal", + "Terminal", + VoiceInteractionTier::VoiceIntent, + VoiceReasoningEffort::Medium, + VoiceOutputPolicy::ConfirmBeforeAction, + ) +} + +fn work_tracker_profile() -> PromptProfile { + PromptProfile::new( + "work-tracker", + "Work Tracker", + VoiceInteractionTier::ContextRewrite, + VoiceReasoningEffort::Medium, + VoiceOutputPolicy::PreviewBeforeInsert, + ) +} + +#[cfg(test)] +mod tests { + use crate::contextual::{ + ContextualVoiceRouter, FocusedAppContext, VoiceInteractionTier, VoiceOutputPolicy, + VoiceReasoningEffort, + }; + + #[test] + fn default_context_uses_fast_dictation() { + let router = ContextualVoiceRouter; + let plan = router.plan_for_context(&FocusedAppContext::new()); + + assert_eq!(plan.profile_id, "fast-dictation"); + assert_eq!(plan.tier, VoiceInteractionTier::FastDictation); + assert_eq!(plan.output_policy, VoiceOutputPolicy::InsertText); + assert_eq!(plan.reasoning_effort, VoiceReasoningEffort::Minimal); + } + + #[test] + fn slack_context_uses_messaging_profile() { + let router = ContextualVoiceRouter; + let context = FocusedAppContext::new().with_app("com.tinyspeck.slackmacgap", "Slack"); + let plan = router.plan_for_context(&context); + + assert_eq!(plan.profile_id, "messaging"); + assert_eq!(plan.tier, VoiceInteractionTier::ContextRewrite); + assert_eq!(plan.output_policy, VoiceOutputPolicy::InsertText); + } + + #[test] + fn cursor_context_previews_code_editor_output() { + let router = ContextualVoiceRouter; + let context = FocusedAppContext::new().with_app("com.todesktop.230313mzl4w4u92", "Cursor"); + let plan = router.plan_for_context(&context); + + assert_eq!(plan.profile_id, "code-editor"); + assert_eq!(plan.output_policy, VoiceOutputPolicy::PreviewBeforeInsert); + } + + #[test] + fn terminal_context_requires_confirmation() { + let router = ContextualVoiceRouter; + let context = FocusedAppContext::new().with_app("com.apple.Terminal", "Terminal"); + let plan = router.plan_for_context(&context); + + assert_eq!(plan.profile_id, "terminal"); + assert_eq!(plan.tier, VoiceInteractionTier::VoiceIntent); + assert_eq!(plan.output_policy, VoiceOutputPolicy::ConfirmBeforeAction); + assert_eq!(plan.reasoning_effort, VoiceReasoningEffort::Medium); + } + + #[test] + fn linear_domain_uses_work_tracker_profile() { + let router = ContextualVoiceRouter; + let context = FocusedAppContext::new() + .with_app("com.apple.Safari", "Safari") + .with_url_domain("linear.app"); + let plan = router.plan_for_context(&context); + + assert_eq!(plan.profile_id, "work-tracker"); + assert_eq!(plan.reasoning_effort, VoiceReasoningEffort::Medium); + } +} diff --git a/packages/voxit-core/src/lib.rs b/packages/voxit-core/src/lib.rs index 0fe134a..b9397ce 100644 --- a/packages/voxit-core/src/lib.rs +++ b/packages/voxit-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod config; +pub mod contextual; pub mod inference; pub mod openai; pub mod realtime; @@ -17,6 +18,10 @@ pub use self::{ sign_in_with_device_code_with_progress, sign_out, status, }, config::Config, + contextual::{ + ContextualVoiceRouter, FocusedAppContext, PromptProfile, VoiceInteractionTier, + VoiceOutputPolicy, VoiceReasoningEffort, VoiceSessionPlan, + }, inference::{ InferenceEvent, RewriteResult, RewriteState, rewrite_only, start_realtime_session, transcribe_only, From 87b467b001f4c6f2956d49752ea365b4b566e98e Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 8 May 2026 23:26:50 +0800 Subject: [PATCH 03/27] {"schema":"maestro/commit/1","summary":"Align main window with control center shape","authority":"manual"} --- docs/reference/repository-layout.md | 11 +- docs/spec/runtime.md | 16 +-- .../Models/NavigationItem.swift | 38 ++++--- .../Views/ContentView.swift | 2 +- .../VoxitNativeHostKit/Views/DetailView.swift | 100 +++++++++++------- 5 files changed, 103 insertions(+), 64 deletions(-) diff --git a/docs/reference/repository-layout.md b/docs/reference/repository-layout.md index 7aa49ae..004ff28 100644 --- a/docs/reference/repository-layout.md +++ b/docs/reference/repository-layout.md @@ -16,10 +16,12 @@ files. ## Top-level surfaces - `native/macos-host/` holds the SwiftPM native macOS host. It owns platform UI - composition, the menu bar extra, and links Rust through the host FFI static library. + composition, the menu bar extra, the Voxit control-center window, the Settings + window, and links Rust through the host FFI static library. - `packages/voxit-core/` holds the shared runtime logic, auth, OpenAI integration, and - dictation pipeline code. Platform-neutral UI model types also live here so hosts do - not invent divergent state names. + dictation pipeline code. Platform-neutral UI model types and contextual voice + planning contracts also live here so hosts do not invent divergent state names, + profile routing, or output policies. - `packages/voxit-audio/` holds audio-capture specific functionality. - `packages/voxit-host-ffi/` holds the thin C ABI consumed by `native/macos-host/`. - `packages/voxit-macos/` holds macOS-specific integration surfaces. @@ -37,7 +39,8 @@ files. - Runtime authority stays in the application and package crates plus the governing specs under `docs/spec/`. - Native UI code may depend on `packages/voxit-host-ffi/`, but it must not duplicate - provider/auth/audio runtime policy already owned by Rust core crates. + provider/auth/audio runtime policy or contextual voice routing already owned by Rust + core crates. - `docs/runbook/`, `docs/reference/`, and `docs/decisions/` must not override runtime or configuration authority. - `Makefile.toml` is the source of truth for named repository tasks. diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 8cc2bf2..b1b6a9e 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -171,12 +171,14 @@ State transitions: ## 9) UI and Onboarding Contract -- UI contains: - - auth status and sign-in actions - - runtime controls (start/stop, rewrite toggle, hotkey mode) - - live stream sections (committed plus draft) - - final transcript sections - - onboarding checklist statuses for microphone, accessibility, and input monitoring +- UI surfaces are split by responsibility: + - menu bar: always-available status and control + - recording HUD: live session state, transcript preview, active profile, and paste + controls + - Voxit control-center window: activity, app rules, profiles, glossary, prompt lab, + and debug/evaluation surfaces + - Settings window: app preferences, shortcuts, microphone, permissions, account + defaults, privacy, logging, and notifications - Onboarding checklist provides request actions for required macOS permissions. The UI prompts permission requests in order: - Microphone: probe-based request and retry loop when denied @@ -188,7 +190,7 @@ State transitions: bypass Pass3. - The Swift native host must render platform-neutral Rust model snapshots from `packages/voxit-core/` through `packages/voxit-host-ffi/` instead of defining a - separate UI state machine. + separate UI state machine, contextual routing policy, or prompt profile registry. ## 10) Configuration Contract diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Models/NavigationItem.swift b/native/macos-host/Sources/VoxitNativeHostKit/Models/NavigationItem.swift index 92e1f20..d955446 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Models/NavigationItem.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Models/NavigationItem.swift @@ -1,9 +1,11 @@ import Foundation public enum NavigationItem: String, CaseIterable, Identifiable, Sendable { - case dictation - case auth - case audio + case activity + case appRules + case profiles + case glossary + case promptLab public var id: String { rawValue @@ -11,23 +13,31 @@ public enum NavigationItem: String, CaseIterable, Identifiable, Sendable { var title: String { switch self { - case .dictation: - return "Dictation" - case .auth: - return "ChatGPT" - case .audio: - return "Audio" + case .activity: + return "Activity" + case .appRules: + return "App Rules" + case .profiles: + return "Profiles" + case .glossary: + return "Glossary" + case .promptLab: + return "Prompt Lab" } } var systemImage: String { switch self { - case .dictation: + case .activity: return "waveform" - case .auth: - return "person.crop.circle.badge.checkmark" - case .audio: - return "mic" + case .appRules: + return "rectangle.3.group" + case .profiles: + return "person.text.rectangle" + case .glossary: + return "text.book.closed" + case .promptLab: + return "slider.horizontal.3" } } } diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift index 9ebcc71..da763c7 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift @@ -3,7 +3,7 @@ import VoxitHostBridge public struct ContentView: View { @ObservedObject var store: HostStore - @SceneStorage("selection") private var selection: NavigationItem = .dictation + @SceneStorage("selection") private var selection: NavigationItem = .activity public init(store: HostStore) { self.store = store diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift index 201f65d..fb3869e 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift @@ -17,12 +17,16 @@ struct DetailView: View { } switch selection { - case .dictation: - DictationDetail(snapshot: snapshot) - case .auth: - AuthDetail(snapshot: snapshot) - case .audio: - AudioDetail() + case .activity: + ActivityDetail(snapshot: snapshot) + case .appRules: + AppRulesDetail() + case .profiles: + ProfilesDetail() + case .glossary: + GlossaryDetail() + case .promptLab: + PromptLabDetail() } } .padding(24) @@ -35,24 +39,11 @@ struct DetailView: View { VStack(alignment: .leading, spacing: 5) { Text(selection.title) .font(.largeTitle.weight(.semibold)) - Text(subtitle) - .foregroundStyle(.secondary) - } - } - - private var subtitle: String { - switch selection { - case .dictation: - return "Record, finalize, rewrite, and paste." - case .auth: - return "ChatGPT device-code authorization." - case .audio: - return "Microphone input and permission state." } } } -private struct DictationDetail: View { +private struct ActivityDetail: View { var snapshot: HostSnapshot? var body: some View { @@ -63,9 +54,19 @@ private struct DictationDetail: View { systemImage: "waveform" ) StatusCard( - title: "Rewrite", - value: snapshot?.rewriteEnabled == true ? "Enabled" : "Disabled", - systemImage: "wand.and.stars" + title: "Auth", + value: snapshot?.authState.label ?? "Loading", + systemImage: "person.crop.circle.badge.checkmark" + ) + StatusCard( + title: "Profile", + value: "Fast Dictation", + systemImage: "person.text.rectangle" + ) + StatusCard( + title: "Policy", + value: "Insert Text", + systemImage: "text.cursor" ) } @@ -79,34 +80,57 @@ private struct DictationDetail: View { } } -private struct AuthDetail: View { - var snapshot: HostSnapshot? - +private struct AppRulesDetail: View { var body: some View { LabeledContentGrid { StatusCard( - title: "Status", - value: snapshot?.authState.label ?? "Loading", - systemImage: "person.crop.circle.badge.checkmark" + title: "Work Tracker", + value: "Linear, GitHub", + systemImage: "checklist" ) StatusCard( - title: "Method", - value: snapshot?.authMethod.label ?? "Device Code", - systemImage: "rectangle.and.pencil.and.ellipsis" + title: "Messaging", + value: "Slack, Discord", + systemImage: "bubble.left.and.bubble.right" ) + StatusCard( + title: "Code Editor", + value: "Cursor, VS Code, Xcode", + systemImage: "curlybraces" + ) + StatusCard( + title: "Terminal", + value: "Confirm", + systemImage: "terminal" + ) + } + } +} + +private struct ProfilesDetail: View { + var body: some View { + LabeledContentGrid { + StatusCard(title: "Fast Dictation", value: "Minimal", systemImage: "bolt") + StatusCard(title: "Context Rewrite", value: "Low", systemImage: "wand.and.stars") + StatusCard(title: "Voice Intent", value: "Medium", systemImage: "arrow.triangle.branch") } + } +} - Button("Sign In", systemImage: "arrow.right.circle") {} - .buttonStyle(.borderedProminent) - .disabled(true) +private struct GlossaryDetail: View { + var body: some View { + LabeledContentGrid { + StatusCard(title: "Custom Terms", value: "None", systemImage: "text.book.closed") + StatusCard(title: "Entity Guard", value: "Numbers, dates, money", systemImage: "number") + } } } -private struct AudioDetail: View { +private struct PromptLabDetail: View { var body: some View { LabeledContentGrid { - StatusCard(title: "Input", value: "System Default", systemImage: "mic") - StatusCard(title: "Permission", value: "Unknown", systemImage: "checkmark.shield") + StatusCard(title: "Comparison", value: "No Runs", systemImage: "rectangle.split.2x1") + StatusCard(title: "Reasoning", value: "Profile Default", systemImage: "brain") } } } From aabe8a6b6542e862109fa1fa09a406978b2697e0 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 8 May 2026 23:28:50 +0800 Subject: [PATCH 04/27] {"schema":"maestro/commit/1","summary":"Document contextual voice rollout gaps","authority":"manual"} --- docs/spec/runtime.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index b1b6a9e..86aaf5a 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -242,6 +242,13 @@ Current Swift Settings window: ## 13) Known Gaps +- Contextual routing is defined in Rust Core, but focused-app context capture, + `VoiceSessionPlan` FFI exposure, and profile-backed Swift rendering are not wired + yet. +- The Voxit control-center window currently exposes the target Activity, App Rules, + Profiles, Glossary, and Prompt Lab navigation shape, but most values are placeholder + UI until the Rust-owned contextual session plan crosses the host boundary. +- The recording HUD is a target surface in the UI contract and is not implemented yet. - Swift Settings write-through to `config.toml` is not implemented yet. - Menu-driven start/stop dictation is visible but not wired to the Rust runtime command yet. From d52fe867053845ff9472a141e85b36067fdbfe2a Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:26:35 +0800 Subject: [PATCH 05/27] {"schema":"maestro/commit/1","summary":"Add built-in prompt profile tags","authority":"manual"} --- packages/voxit-core/src/contextual.rs | 40 +++++++++++++++++++++++++-- packages/voxit-core/src/lib.rs | 4 +-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs index 165b113..97eb6be 100644 --- a/packages/voxit-core/src/contextual.rs +++ b/packages/voxit-core/src/contextual.rs @@ -38,6 +38,23 @@ pub enum VoiceOutputPolicy { ConfirmBeforeAction, } +/// Built-in prompt profile selected by contextual routing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PromptProfileKind { + /// Default low-latency dictation profile. + FastDictation, + /// Messaging profile for short conversational destinations. + Messaging, + /// Mail profile for complete email prose. + Mail, + /// Code editor profile for programming-related dictation. + CodeEditor, + /// Terminal profile for command-like proposals. + Terminal, + /// Work tracker profile for issue, review, and planning destinations. + WorkTracker, +} + /// Host-collected context for the app that was focused when dictation started. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct FocusedAppContext { @@ -79,6 +96,8 @@ impl FocusedAppContext { /// Prompt profile selected by contextual routing. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PromptProfile { + /// Built-in profile kind. + pub kind: PromptProfileKind, /// Stable profile id. pub id: String, /// Human-readable profile title. @@ -93,19 +112,22 @@ pub struct PromptProfile { impl PromptProfile { /// Build a prompt profile. pub fn new( + kind: PromptProfileKind, id: impl Into, title: impl Into, tier: VoiceInteractionTier, reasoning_effort: VoiceReasoningEffort, output_policy: VoiceOutputPolicy, ) -> Self { - Self { id: id.into(), title: title.into(), tier, reasoning_effort, output_policy } + Self { kind, id: id.into(), title: title.into(), tier, reasoning_effort, output_policy } } } /// Concrete plan for one voice session. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VoiceSessionPlan { + /// Selected built-in prompt profile. + pub profile_kind: PromptProfileKind, /// Selected profile id. pub profile_id: String, /// Selected profile display title. @@ -120,6 +142,7 @@ pub struct VoiceSessionPlan { impl VoiceSessionPlan { fn from_profile(profile: PromptProfile) -> Self { Self { + profile_kind: profile.kind, profile_id: profile.id, profile_title: profile.title, tier: profile.tier, @@ -180,6 +203,7 @@ fn context_text(context: &FocusedAppContext) -> Option { fn default_dictation_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::FastDictation, "fast-dictation", "Fast Dictation", VoiceInteractionTier::FastDictation, @@ -190,6 +214,7 @@ fn default_dictation_profile() -> PromptProfile { fn messaging_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::Messaging, "messaging", "Messaging", VoiceInteractionTier::ContextRewrite, @@ -200,6 +225,7 @@ fn messaging_profile() -> PromptProfile { fn mail_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::Mail, "mail", "Mail", VoiceInteractionTier::ContextRewrite, @@ -210,6 +236,7 @@ fn mail_profile() -> PromptProfile { fn code_editor_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::CodeEditor, "code-editor", "Code Editor", VoiceInteractionTier::ContextRewrite, @@ -220,6 +247,7 @@ fn code_editor_profile() -> PromptProfile { fn terminal_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::Terminal, "terminal", "Terminal", VoiceInteractionTier::VoiceIntent, @@ -230,6 +258,7 @@ fn terminal_profile() -> PromptProfile { fn work_tracker_profile() -> PromptProfile { PromptProfile::new( + PromptProfileKind::WorkTracker, "work-tracker", "Work Tracker", VoiceInteractionTier::ContextRewrite, @@ -241,8 +270,8 @@ fn work_tracker_profile() -> PromptProfile { #[cfg(test)] mod tests { use crate::contextual::{ - ContextualVoiceRouter, FocusedAppContext, VoiceInteractionTier, VoiceOutputPolicy, - VoiceReasoningEffort, + ContextualVoiceRouter, FocusedAppContext, PromptProfileKind, VoiceInteractionTier, + VoiceOutputPolicy, VoiceReasoningEffort, }; #[test] @@ -250,6 +279,7 @@ mod tests { let router = ContextualVoiceRouter; let plan = router.plan_for_context(&FocusedAppContext::new()); + assert_eq!(plan.profile_kind, PromptProfileKind::FastDictation); assert_eq!(plan.profile_id, "fast-dictation"); assert_eq!(plan.tier, VoiceInteractionTier::FastDictation); assert_eq!(plan.output_policy, VoiceOutputPolicy::InsertText); @@ -262,6 +292,7 @@ mod tests { let context = FocusedAppContext::new().with_app("com.tinyspeck.slackmacgap", "Slack"); let plan = router.plan_for_context(&context); + assert_eq!(plan.profile_kind, PromptProfileKind::Messaging); assert_eq!(plan.profile_id, "messaging"); assert_eq!(plan.tier, VoiceInteractionTier::ContextRewrite); assert_eq!(plan.output_policy, VoiceOutputPolicy::InsertText); @@ -273,6 +304,7 @@ mod tests { let context = FocusedAppContext::new().with_app("com.todesktop.230313mzl4w4u92", "Cursor"); let plan = router.plan_for_context(&context); + assert_eq!(plan.profile_kind, PromptProfileKind::CodeEditor); assert_eq!(plan.profile_id, "code-editor"); assert_eq!(plan.output_policy, VoiceOutputPolicy::PreviewBeforeInsert); } @@ -283,6 +315,7 @@ mod tests { let context = FocusedAppContext::new().with_app("com.apple.Terminal", "Terminal"); let plan = router.plan_for_context(&context); + assert_eq!(plan.profile_kind, PromptProfileKind::Terminal); assert_eq!(plan.profile_id, "terminal"); assert_eq!(plan.tier, VoiceInteractionTier::VoiceIntent); assert_eq!(plan.output_policy, VoiceOutputPolicy::ConfirmBeforeAction); @@ -297,6 +330,7 @@ mod tests { .with_url_domain("linear.app"); let plan = router.plan_for_context(&context); + assert_eq!(plan.profile_kind, PromptProfileKind::WorkTracker); assert_eq!(plan.profile_id, "work-tracker"); assert_eq!(plan.reasoning_effort, VoiceReasoningEffort::Medium); } diff --git a/packages/voxit-core/src/lib.rs b/packages/voxit-core/src/lib.rs index b9397ce..6eb6571 100644 --- a/packages/voxit-core/src/lib.rs +++ b/packages/voxit-core/src/lib.rs @@ -19,8 +19,8 @@ pub use self::{ }, config::Config, contextual::{ - ContextualVoiceRouter, FocusedAppContext, PromptProfile, VoiceInteractionTier, - VoiceOutputPolicy, VoiceReasoningEffort, VoiceSessionPlan, + ContextualVoiceRouter, FocusedAppContext, PromptProfile, PromptProfileKind, + VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, VoiceSessionPlan, }, inference::{ InferenceEvent, RewriteResult, RewriteState, rewrite_only, start_realtime_session, From 9194845406833e0a328394df1fdd761a7dbb16e7 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:28:43 +0800 Subject: [PATCH 06/27] {"schema":"maestro/commit/1","summary":"Expose voice session plan through host bridge","authority":"manual"} --- .../Sources/VoxitHostBridge/HostFFI.swift | 120 +++++++++++++++- .../voxit-host-ffi/include/voxit_host_ffi.h | 34 ++++- packages/voxit-host-ffi/src/lib.rs | 135 +++++++++++++++++- 3 files changed, 280 insertions(+), 9 deletions(-) diff --git a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift index 101afbe..826c0a4 100644 --- a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift +++ b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift @@ -30,6 +30,34 @@ public enum HotkeyMode: Equatable, Sendable { case hold } +public enum PromptProfileKind: Equatable, Sendable { + case fastDictation + case messaging + case mail + case codeEditor + case terminal + case workTracker +} + +public enum VoiceInteractionTier: Equatable, Sendable { + case fastDictation + case contextRewrite + case voiceIntent +} + +public enum VoiceReasoningEffort: Equatable, Sendable { + case minimal + case low + case medium + case high +} + +public enum VoiceOutputPolicy: Equatable, Sendable { + case insertText + case previewBeforeInsert + case confirmBeforeAction +} + public struct HostSnapshot: Equatable, Sendable { public var platform: HostPlatform public var authMethod: AuthMethod @@ -39,6 +67,10 @@ public struct HostSnapshot: Equatable, Sendable { public var panelWidth: Int public var panelHeight: Int public var rewriteEnabled: Bool + public var promptProfileKind: PromptProfileKind + public var voiceTier: VoiceInteractionTier + public var reasoningEffort: VoiceReasoningEffort + public var outputPolicy: VoiceOutputPolicy public init( platform: HostPlatform, @@ -48,7 +80,11 @@ public struct HostSnapshot: Equatable, Sendable { hotkeyMode: HotkeyMode, panelWidth: Int, panelHeight: Int, - rewriteEnabled: Bool + rewriteEnabled: Bool, + promptProfileKind: PromptProfileKind, + voiceTier: VoiceInteractionTier, + reasoningEffort: VoiceReasoningEffort, + outputPolicy: VoiceOutputPolicy ) { self.platform = platform self.authMethod = authMethod @@ -58,6 +94,10 @@ public struct HostSnapshot: Equatable, Sendable { self.panelWidth = panelWidth self.panelHeight = panelHeight self.rewriteEnabled = rewriteEnabled + self.promptProfileKind = promptProfileKind + self.voiceTier = voiceTier + self.reasoningEffort = reasoningEffort + self.outputPolicy = outputPolicy } } @@ -70,6 +110,10 @@ public enum HostBridgeError: Error, Equatable, CustomStringConvertible { case invalidAuthState(UInt32) case invalidDictationState(UInt32) case invalidHotkeyMode(UInt32) + case invalidPromptProfileKind(UInt32) + case invalidVoiceInteractionTier(UInt32) + case invalidVoiceReasoningEffort(UInt32) + case invalidVoiceOutputPolicy(UInt32) public var description: String { switch self { @@ -89,6 +133,14 @@ public enum HostBridgeError: Error, Equatable, CustomStringConvertible { return "Unknown dictation state \(rawValue)" case .invalidHotkeyMode(let rawValue): return "Unknown hotkey mode \(rawValue)" + case .invalidPromptProfileKind(let rawValue): + return "Unknown prompt profile kind \(rawValue)" + case .invalidVoiceInteractionTier(let rawValue): + return "Unknown voice interaction tier \(rawValue)" + case .invalidVoiceReasoningEffort(let rawValue): + return "Unknown voice reasoning effort \(rawValue)" + case .invalidVoiceOutputPolicy(let rawValue): + return "Unknown voice output policy \(rawValue)" } } } @@ -143,7 +195,11 @@ public final class VoxitHostSession { hotkeyMode: try decode(hotkeyMode: snapshot.hotkey_mode), panelWidth: Int(snapshot.panel_width_px), panelHeight: Int(snapshot.panel_height_px), - rewriteEnabled: snapshot.rewrite_enabled != 0 + rewriteEnabled: snapshot.rewrite_enabled != 0, + promptProfileKind: try decode(promptProfileKind: snapshot.prompt_profile_kind), + voiceTier: try decode(voiceTier: snapshot.voice_tier), + reasoningEffort: try decode(reasoningEffort: snapshot.reasoning_effort), + outputPolicy: try decode(outputPolicy: snapshot.output_policy) ) } @@ -209,4 +265,64 @@ public final class VoxitHostSession { throw HostBridgeError.invalidHotkeyMode(hotkeyMode.rawValue) } } + + private func decode(promptProfileKind: VoxitPromptProfileKind) throws -> PromptProfileKind { + switch promptProfileKind.rawValue { + case VOXIT_PROMPT_PROFILE_FAST_DICTATION.rawValue: + return .fastDictation + case VOXIT_PROMPT_PROFILE_MESSAGING.rawValue: + return .messaging + case VOXIT_PROMPT_PROFILE_MAIL.rawValue: + return .mail + case VOXIT_PROMPT_PROFILE_CODE_EDITOR.rawValue: + return .codeEditor + case VOXIT_PROMPT_PROFILE_TERMINAL.rawValue: + return .terminal + case VOXIT_PROMPT_PROFILE_WORK_TRACKER.rawValue: + return .workTracker + default: + throw HostBridgeError.invalidPromptProfileKind(promptProfileKind.rawValue) + } + } + + private func decode(voiceTier: VoxitVoiceInteractionTier) throws -> VoiceInteractionTier { + switch voiceTier.rawValue { + case VOXIT_VOICE_TIER_FAST_DICTATION.rawValue: + return .fastDictation + case VOXIT_VOICE_TIER_CONTEXT_REWRITE.rawValue: + return .contextRewrite + case VOXIT_VOICE_TIER_VOICE_INTENT.rawValue: + return .voiceIntent + default: + throw HostBridgeError.invalidVoiceInteractionTier(voiceTier.rawValue) + } + } + + private func decode(reasoningEffort: VoxitVoiceReasoningEffort) throws -> VoiceReasoningEffort { + switch reasoningEffort.rawValue { + case VOXIT_REASONING_EFFORT_MINIMAL.rawValue: + return .minimal + case VOXIT_REASONING_EFFORT_LOW.rawValue: + return .low + case VOXIT_REASONING_EFFORT_MEDIUM.rawValue: + return .medium + case VOXIT_REASONING_EFFORT_HIGH.rawValue: + return .high + default: + throw HostBridgeError.invalidVoiceReasoningEffort(reasoningEffort.rawValue) + } + } + + private func decode(outputPolicy: VoxitVoiceOutputPolicy) throws -> VoiceOutputPolicy { + switch outputPolicy.rawValue { + case VOXIT_OUTPUT_POLICY_INSERT_TEXT.rawValue: + return .insertText + case VOXIT_OUTPUT_POLICY_PREVIEW_BEFORE_INSERT.rawValue: + return .previewBeforeInsert + case VOXIT_OUTPUT_POLICY_CONFIRM_BEFORE_ACTION.rawValue: + return .confirmBeforeAction + default: + throw HostBridgeError.invalidVoiceOutputPolicy(outputPolicy.rawValue) + } + } } diff --git a/packages/voxit-host-ffi/include/voxit_host_ffi.h b/packages/voxit-host-ffi/include/voxit_host_ffi.h index 364b632..249274b 100644 --- a/packages/voxit-host-ffi/include/voxit_host_ffi.h +++ b/packages/voxit-host-ffi/include/voxit_host_ffi.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define VOXIT_HOST_FFI_ABI_VERSION 1u +#define VOXIT_HOST_FFI_ABI_VERSION 2u typedef struct VoxitHostSessionHandle VoxitHostSessionHandle; @@ -47,6 +47,34 @@ typedef enum VoxitHotkeyMode { VOXIT_HOTKEY_MODE_HOLD = 1, } VoxitHotkeyMode; +typedef enum VoxitPromptProfileKind { + VOXIT_PROMPT_PROFILE_FAST_DICTATION = 0, + VOXIT_PROMPT_PROFILE_MESSAGING = 1, + VOXIT_PROMPT_PROFILE_MAIL = 2, + VOXIT_PROMPT_PROFILE_CODE_EDITOR = 3, + VOXIT_PROMPT_PROFILE_TERMINAL = 4, + VOXIT_PROMPT_PROFILE_WORK_TRACKER = 5, +} VoxitPromptProfileKind; + +typedef enum VoxitVoiceInteractionTier { + VOXIT_VOICE_TIER_FAST_DICTATION = 0, + VOXIT_VOICE_TIER_CONTEXT_REWRITE = 1, + VOXIT_VOICE_TIER_VOICE_INTENT = 2, +} VoxitVoiceInteractionTier; + +typedef enum VoxitVoiceReasoningEffort { + VOXIT_REASONING_EFFORT_MINIMAL = 0, + VOXIT_REASONING_EFFORT_LOW = 1, + VOXIT_REASONING_EFFORT_MEDIUM = 2, + VOXIT_REASONING_EFFORT_HIGH = 3, +} VoxitVoiceReasoningEffort; + +typedef enum VoxitVoiceOutputPolicy { + VOXIT_OUTPUT_POLICY_INSERT_TEXT = 0, + VOXIT_OUTPUT_POLICY_PREVIEW_BEFORE_INSERT = 1, + VOXIT_OUTPUT_POLICY_CONFIRM_BEFORE_ACTION = 2, +} VoxitVoiceOutputPolicy; + typedef struct VoxitHostConfig { enum VoxitPlatformTag platform; } VoxitHostConfig; @@ -60,6 +88,10 @@ typedef struct VoxitHostSnapshot { uint32_t panel_width_px; uint32_t panel_height_px; uint8_t rewrite_enabled; + enum VoxitPromptProfileKind prompt_profile_kind; + enum VoxitVoiceInteractionTier voice_tier; + enum VoxitVoiceReasoningEffort reasoning_effort; + enum VoxitVoiceOutputPolicy output_policy; } VoxitHostSnapshot; uint32_t voxit_host_ffi_abi_version(void); diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index fb1f17a..c3473c6 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -7,16 +7,21 @@ use std::ptr::NonNull; use voxit_core::{ - Config, NativeHostSnapshot, PlatformHost, + Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, + VoiceSessionPlan, + contextual::{ + PromptProfileKind, VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, + }, ui_model::{AuthMethod, AuthSurfaceState, DictationSurfaceState, HotkeySurfaceMode}, }; /// ABI version exported by the thin C host bridge. -pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 1; +pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 2; /// Opaque session handle owned by the native host through the C ABI. pub struct VoxitHostSessionHandle { snapshot: NativeHostSnapshot, + voice_plan: VoiceSessionPlan, } /// Result code returned by FFI entry points. @@ -91,6 +96,62 @@ pub enum VoxitHotkeyMode { Hold = 1, } +/// FFI-safe built-in prompt profile kind. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoxitPromptProfileKind { + /// Default low-latency dictation profile. + FastDictation = 0, + /// Messaging profile for conversational destinations. + Messaging = 1, + /// Mail profile for complete email prose. + Mail = 2, + /// Code editor profile for programming-related dictation. + CodeEditor = 3, + /// Terminal profile for command-like proposals. + Terminal = 4, + /// Work tracker profile for issue, review, and planning destinations. + WorkTracker = 5, +} + +/// FFI-safe interaction tier. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoxitVoiceInteractionTier { + /// Lowest-latency speech-to-clean-text path. + FastDictation = 0, + /// App-aware rewrite path. + ContextRewrite = 1, + /// Intent-oriented path. + VoiceIntent = 2, +} + +/// FFI-safe reasoning effort. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoxitVoiceReasoningEffort { + /// Fastest viable reasoning path. + Minimal = 0, + /// Light reasoning for common contextual rewrites. + Low = 1, + /// Deeper reasoning for multi-step output. + Medium = 2, + /// Strongest reasoning for constrained output. + High = 3, +} + +/// FFI-safe output policy. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoxitVoiceOutputPolicy { + /// Insert or paste final text directly. + InsertText = 0, + /// Show the output before insertion. + PreviewBeforeInsert = 1, + /// Require confirmation before action-like output. + ConfirmBeforeAction = 2, +} + /// FFI-safe session configuration. #[repr(C)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -119,6 +180,14 @@ pub struct VoxitHostSnapshot { pub panel_height_px: u32, /// Non-zero when pass-3 rewrite is enabled. pub rewrite_enabled: u8, + /// Selected prompt profile kind. + pub prompt_profile_kind: VoxitPromptProfileKind, + /// Selected voice interaction tier. + pub voice_tier: VoxitVoiceInteractionTier, + /// Selected reasoning effort. + pub reasoning_effort: VoxitVoiceReasoningEffort, + /// Selected output policy. + pub output_policy: VoxitVoiceOutputPolicy, } impl Default for VoxitHostSnapshot { fn default() -> Self { @@ -131,6 +200,10 @@ impl Default for VoxitHostSnapshot { panel_width_px: 0, panel_height_px: 0, rewrite_enabled: 0, + prompt_profile_kind: VoxitPromptProfileKind::FastDictation, + voice_tier: VoxitVoiceInteractionTier::FastDictation, + reasoning_effort: VoxitVoiceReasoningEffort::Minimal, + output_policy: VoxitVoiceOutputPolicy::InsertText, } } } @@ -151,8 +224,9 @@ pub extern "C" fn voxit_host_session_create( VoxitPlatformTag::Unsupported => PlatformHost::Unsupported, }; let snapshot = NativeHostSnapshot::initial(platform, &Config::default()); + let voice_plan = ContextualVoiceRouter.plan_for_context(&FocusedAppContext::new()); - Box::into_raw(Box::new(VoxitHostSessionHandle { snapshot })) + Box::into_raw(Box::new(VoxitHostSessionHandle { snapshot, voice_plan })) } /// Destroys a Rust-owned native-host session. @@ -186,13 +260,17 @@ pub unsafe extern "C" fn voxit_host_session_copy_snapshot( return VoxitStatus::NullOutput; }; let snapshot = unsafe { &handle.as_ref().snapshot }; + let voice_plan = unsafe { &handle.as_ref().voice_plan }; - unsafe { out.as_ptr().write(encode_snapshot(snapshot)) }; + unsafe { out.as_ptr().write(encode_snapshot(snapshot, voice_plan)) }; VoxitStatus::Ok } -fn encode_snapshot(snapshot: &NativeHostSnapshot) -> VoxitHostSnapshot { +fn encode_snapshot( + snapshot: &NativeHostSnapshot, + voice_plan: &VoiceSessionPlan, +) -> VoxitHostSnapshot { VoxitHostSnapshot { platform: encode_platform(snapshot.platform), auth_method: encode_auth_method(snapshot.auth_method), @@ -202,6 +280,10 @@ fn encode_snapshot(snapshot: &NativeHostSnapshot) -> VoxitHostSnapshot { panel_width_px: snapshot.panel_width_px, panel_height_px: snapshot.panel_height_px, rewrite_enabled: u8::from(snapshot.rewrite_enabled), + prompt_profile_kind: encode_prompt_profile_kind(voice_plan.profile_kind), + voice_tier: encode_voice_tier(voice_plan.tier), + reasoning_effort: encode_reasoning_effort(voice_plan.reasoning_effort), + output_policy: encode_output_policy(voice_plan.output_policy), } } @@ -244,11 +326,48 @@ fn encode_hotkey_mode(mode: HotkeySurfaceMode) -> VoxitHotkeyMode { } } +fn encode_prompt_profile_kind(kind: PromptProfileKind) -> VoxitPromptProfileKind { + match kind { + PromptProfileKind::FastDictation => VoxitPromptProfileKind::FastDictation, + PromptProfileKind::Messaging => VoxitPromptProfileKind::Messaging, + PromptProfileKind::Mail => VoxitPromptProfileKind::Mail, + PromptProfileKind::CodeEditor => VoxitPromptProfileKind::CodeEditor, + PromptProfileKind::Terminal => VoxitPromptProfileKind::Terminal, + PromptProfileKind::WorkTracker => VoxitPromptProfileKind::WorkTracker, + } +} + +fn encode_voice_tier(tier: VoiceInteractionTier) -> VoxitVoiceInteractionTier { + match tier { + VoiceInteractionTier::FastDictation => VoxitVoiceInteractionTier::FastDictation, + VoiceInteractionTier::ContextRewrite => VoxitVoiceInteractionTier::ContextRewrite, + VoiceInteractionTier::VoiceIntent => VoxitVoiceInteractionTier::VoiceIntent, + } +} + +fn encode_reasoning_effort(effort: VoiceReasoningEffort) -> VoxitVoiceReasoningEffort { + match effort { + VoiceReasoningEffort::Minimal => VoxitVoiceReasoningEffort::Minimal, + VoiceReasoningEffort::Low => VoxitVoiceReasoningEffort::Low, + VoiceReasoningEffort::Medium => VoxitVoiceReasoningEffort::Medium, + VoiceReasoningEffort::High => VoxitVoiceReasoningEffort::High, + } +} + +fn encode_output_policy(policy: VoiceOutputPolicy) -> VoxitVoiceOutputPolicy { + match policy { + VoiceOutputPolicy::InsertText => VoxitVoiceOutputPolicy::InsertText, + VoiceOutputPolicy::PreviewBeforeInsert => VoxitVoiceOutputPolicy::PreviewBeforeInsert, + VoiceOutputPolicy::ConfirmBeforeAction => VoxitVoiceOutputPolicy::ConfirmBeforeAction, + } +} + #[cfg(test)] mod tests { use crate::{ VoxitAuthMethod, VoxitDictationState, VoxitHostConfig, VoxitHostSnapshot, VoxitPlatformTag, - VoxitStatus, + VoxitPromptProfileKind, VoxitStatus, VoxitVoiceInteractionTier, VoxitVoiceOutputPolicy, + VoxitVoiceReasoningEffort, }; #[test] @@ -263,6 +382,10 @@ mod tests { assert_eq!(snapshot.auth_method, VoxitAuthMethod::ChatGptDeviceCode); assert_eq!(snapshot.dictation_state, VoxitDictationState::Idle); assert_eq!(snapshot.rewrite_enabled, 1); + assert_eq!(snapshot.prompt_profile_kind, VoxitPromptProfileKind::FastDictation); + assert_eq!(snapshot.voice_tier, VoxitVoiceInteractionTier::FastDictation); + assert_eq!(snapshot.reasoning_effort, VoxitVoiceReasoningEffort::Minimal); + assert_eq!(snapshot.output_policy, VoxitVoiceOutputPolicy::InsertText); unsafe { crate::voxit_host_session_destroy(handle) }; } From 99de4d99df29e70e3101d8ac9a79d2baa0cb44f8 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:29:26 +0800 Subject: [PATCH 07/27] {"schema":"maestro/commit/1","summary":"Render voice session plan in control center","authority":"manual"} --- .../VoxitNativeHostKit/Support/Labels.swift | 60 +++++++++++++++++++ .../VoxitNativeHostKit/Views/DetailView.swift | 31 ++++++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift index c9fcdf1..f2f5a07 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift @@ -51,3 +51,63 @@ extension HotkeyMode { } } } + +extension PromptProfileKind { + var label: String { + switch self { + case .fastDictation: + return "Fast Dictation" + case .messaging: + return "Messaging" + case .mail: + return "Mail" + case .codeEditor: + return "Code Editor" + case .terminal: + return "Terminal" + case .workTracker: + return "Work Tracker" + } + } +} + +extension VoiceInteractionTier { + var label: String { + switch self { + case .fastDictation: + return "Fast Dictation" + case .contextRewrite: + return "Context Rewrite" + case .voiceIntent: + return "Voice Intent" + } + } +} + +extension VoiceReasoningEffort { + var label: String { + switch self { + case .minimal: + return "Minimal" + case .low: + return "Low" + case .medium: + return "Medium" + case .high: + return "High" + } + } +} + +extension VoiceOutputPolicy { + var label: String { + switch self { + case .insertText: + return "Insert Text" + case .previewBeforeInsert: + return "Preview" + case .confirmBeforeAction: + return "Confirm" + } + } +} diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift index fb3869e..f3bc105 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift @@ -22,7 +22,7 @@ struct DetailView: View { case .appRules: AppRulesDetail() case .profiles: - ProfilesDetail() + ProfilesDetail(snapshot: snapshot) case .glossary: GlossaryDetail() case .promptLab: @@ -60,12 +60,22 @@ private struct ActivityDetail: View { ) StatusCard( title: "Profile", - value: "Fast Dictation", + value: snapshot?.promptProfileKind.label ?? "Loading", systemImage: "person.text.rectangle" ) StatusCard( - title: "Policy", - value: "Insert Text", + title: "Tier", + value: snapshot?.voiceTier.label ?? "Loading", + systemImage: "square.stack.3d.up" + ) + StatusCard( + title: "Reasoning", + value: snapshot?.reasoningEffort.label ?? "Loading", + systemImage: "brain" + ) + StatusCard( + title: "Output", + value: snapshot?.outputPolicy.label ?? "Loading", systemImage: "text.cursor" ) } @@ -108,9 +118,20 @@ private struct AppRulesDetail: View { } private struct ProfilesDetail: View { + var snapshot: HostSnapshot? + var body: some View { LabeledContentGrid { - StatusCard(title: "Fast Dictation", value: "Minimal", systemImage: "bolt") + StatusCard( + title: "Current", + value: snapshot?.promptProfileKind.label ?? "Loading", + systemImage: "scope" + ) + StatusCard( + title: "Reasoning", + value: snapshot?.reasoningEffort.label ?? "Loading", + systemImage: "brain" + ) StatusCard(title: "Context Rewrite", value: "Low", systemImage: "wand.and.stars") StatusCard(title: "Voice Intent", value: "Medium", systemImage: "arrow.triangle.branch") } From 937985a770d5a23bfa03b1d8e3bf8cff8b3585be Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:29:46 +0800 Subject: [PATCH 08/27] {"schema":"maestro/commit/1","summary":"Update contextual bridge rollout gaps","authority":"manual"} --- docs/spec/runtime.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 86aaf5a..71a239c 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -242,12 +242,12 @@ Current Swift Settings window: ## 13) Known Gaps -- Contextual routing is defined in Rust Core, but focused-app context capture, - `VoiceSessionPlan` FFI exposure, and profile-backed Swift rendering are not wired - yet. +- Contextual routing is defined in Rust Core and the current `VoiceSessionPlan` is + exposed through the host snapshot, but focused-app context capture and non-default + profile selection are not wired yet. - The Voxit control-center window currently exposes the target Activity, App Rules, - Profiles, Glossary, and Prompt Lab navigation shape, but most values are placeholder - UI until the Rust-owned contextual session plan crosses the host boundary. + Profiles, Glossary, and Prompt Lab navigation shape, but profile editing, app-rule + editing, glossary persistence, and prompt lab comparisons are not implemented yet. - The recording HUD is a target surface in the UI contract and is not implemented yet. - Swift Settings write-through to `config.toml` is not implemented yet. - Menu-driven start/stop dictation is visible but not wired to the Rust runtime command From 56909f40ffbc4ef3868b619030f03e22b5069695 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:41:48 +0800 Subject: [PATCH 09/27] {"schema":"maestro/commit/1","summary":"Wire focused context into voice plans","authority":"manual"} --- Cargo.lock | 2 + native/macos-host/Package.swift | 11 +- .../Sources/VoxitHostBridge/HostFFI.swift | 56 ++++++ .../VoxitNativeHostKit/Stores/HostStore.swift | 10 + .../VoxitNativeHostKit/Support/Labels.swift | 15 ++ .../Views/ContentView.swift | 10 +- .../VoxitNativeHostKit/Views/DetailView.swift | 17 +- packages/voxit-core/src/contextual.rs | 31 +++ packages/voxit-host-ffi/Cargo.toml | 3 + .../voxit-host-ffi/include/voxit_host_ffi.h | 20 +- packages/voxit-host-ffi/src/lib.rs | 190 +++++++++++++++++- packages/voxit-macos/Cargo.toml | 1 + packages/voxit-macos/src/lib.rs | 111 ++++++++-- 13 files changed, 449 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16b9a94..5b0cf1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,6 +1634,7 @@ name = "voxit-host-ffi" version = "0.1.0" dependencies = [ "voxit-core", + "voxit-macos", ] [[package]] @@ -1644,6 +1645,7 @@ dependencies = [ "objc2-app-kit", "objc2-av-foundation", "tracing", + "url", ] [[package]] diff --git a/native/macos-host/Package.swift b/native/macos-host/Package.swift index f7cfe34..f5a118d 100644 --- a/native/macos-host/Package.swift +++ b/native/macos-host/Package.swift @@ -28,10 +28,13 @@ let package = Package( name: "VoxitHostBridge", dependencies: ["CVoxitHostFFI"], linkerSettings: [ - .linkedFramework("AppKit"), - .linkedFramework("AudioToolbox"), - .linkedFramework("CoreAudio"), - .linkedFramework("Security"), + .linkedFramework("AppKit"), + .linkedFramework("ApplicationServices"), + .linkedFramework("AudioToolbox"), + .linkedFramework("AVFoundation"), + .linkedFramework("CoreAudio"), + .linkedFramework("CoreFoundation"), + .linkedFramework("Security"), .unsafeFlags([ "-L", rustLibDir, diff --git a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift index 826c0a4..a02f3cd 100644 --- a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift +++ b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift @@ -67,6 +67,14 @@ public struct HostSnapshot: Equatable, Sendable { public var panelWidth: Int public var panelHeight: Int public var rewriteEnabled: Bool + public var hasFocusedContext: Bool + public var selectedTextPresent: Bool + public var focusedBundleID: String? + public var focusedAppName: String? + public var focusedWindowTitle: String? + public var focusedURLDomain: String? + public var focusedElementRole: String? + public var promptProfileID: String? public var promptProfileKind: PromptProfileKind public var voiceTier: VoiceInteractionTier public var reasoningEffort: VoiceReasoningEffort @@ -81,6 +89,14 @@ public struct HostSnapshot: Equatable, Sendable { panelWidth: Int, panelHeight: Int, rewriteEnabled: Bool, + hasFocusedContext: Bool, + selectedTextPresent: Bool, + focusedBundleID: String?, + focusedAppName: String?, + focusedWindowTitle: String?, + focusedURLDomain: String?, + focusedElementRole: String?, + promptProfileID: String?, promptProfileKind: PromptProfileKind, voiceTier: VoiceInteractionTier, reasoningEffort: VoiceReasoningEffort, @@ -94,6 +110,14 @@ public struct HostSnapshot: Equatable, Sendable { self.panelWidth = panelWidth self.panelHeight = panelHeight self.rewriteEnabled = rewriteEnabled + self.hasFocusedContext = hasFocusedContext + self.selectedTextPresent = selectedTextPresent + self.focusedBundleID = focusedBundleID + self.focusedAppName = focusedAppName + self.focusedWindowTitle = focusedWindowTitle + self.focusedURLDomain = focusedURLDomain + self.focusedElementRole = focusedElementRole + self.promptProfileID = promptProfileID self.promptProfileKind = promptProfileKind self.voiceTier = voiceTier self.reasoningEffort = reasoningEffort @@ -179,6 +203,15 @@ public final class VoxitHostSession { return try decode(snapshot: outSnapshot) } + public func refreshFocusedContext() throws -> HostSnapshot { + try requireOk( + voxit_host_session_refresh_focused_context(handle), + context: "refreshing focused context" + ) + + return try currentSnapshot() + } + private func requireOk(_ status: VoxitStatus, context: String) throws { let code = voxit_status_code(status) if code != 0 { @@ -196,6 +229,14 @@ public final class VoxitHostSession { panelWidth: Int(snapshot.panel_width_px), panelHeight: Int(snapshot.panel_height_px), rewriteEnabled: snapshot.rewrite_enabled != 0, + hasFocusedContext: snapshot.has_focused_context != 0, + selectedTextPresent: snapshot.selected_text_present != 0, + focusedBundleID: try copyString(field: VOXIT_HOST_STRING_FOCUSED_BUNDLE_ID), + focusedAppName: try copyString(field: VOXIT_HOST_STRING_FOCUSED_APP_NAME), + focusedWindowTitle: try copyString(field: VOXIT_HOST_STRING_FOCUSED_WINDOW_TITLE), + focusedURLDomain: try copyString(field: VOXIT_HOST_STRING_FOCUSED_URL_DOMAIN), + focusedElementRole: try copyString(field: VOXIT_HOST_STRING_FOCUSED_ELEMENT_ROLE), + promptProfileID: try copyString(field: VOXIT_HOST_STRING_PROMPT_PROFILE_ID), promptProfileKind: try decode(promptProfileKind: snapshot.prompt_profile_kind), voiceTier: try decode(voiceTier: snapshot.voice_tier), reasoningEffort: try decode(reasoningEffort: snapshot.reasoning_effort), @@ -203,6 +244,21 @@ public final class VoxitHostSession { ) } + private func copyString(field: VoxitHostStringField) throws -> String? { + var buffer = [CChar](repeating: 0, count: 1024) + let bufferCount = buffer.count + try buffer.withUnsafeMutableBufferPointer { pointer in + try requireOk( + voxit_host_session_copy_string(handle, field, pointer.baseAddress, UInt(bufferCount)), + context: "copying host string" + ) + } + let endIndex = buffer.firstIndex(of: 0) ?? buffer.endIndex + let bytes = buffer[.. HostPlatform { switch platform.rawValue { case VOXIT_PLATFORM_MACOS.rawValue: diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift index e457201..bbcd668 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift @@ -20,6 +20,16 @@ public final class HostStore: ObservableObject { } } + public func refreshFocusedContext() async { + do { + let session = try currentSession() + snapshot = try session.refreshFocusedContext() + errorMessage = nil + } catch { + errorMessage = String(describing: error) + } + } + private func currentSession() throws -> VoxitHostSession { if let session { return session diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift index f2f5a07..df9c075 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift @@ -111,3 +111,18 @@ extension VoiceOutputPolicy { } } } + +extension HostSnapshot { + var focusedAppLabel: String { + if let focusedAppName { + return focusedAppName + } + if let focusedBundleID { + return focusedBundleID + } + if let focusedURLDomain { + return focusedURLDomain + } + return "No Context" + } +} diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift index da763c7..569e416 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift @@ -13,7 +13,15 @@ public struct ContentView: View { NavigationSplitView { SidebarView(selection: $selection, snapshot: store.snapshot) } detail: { - DetailView(selection: selection, snapshot: store.snapshot, errorMessage: store.errorMessage) + DetailView( + selection: selection, + snapshot: store.snapshot, + errorMessage: store.errorMessage + ) { + Task { + await store.refreshFocusedContext() + } + } } } } diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift index f3bc105..7e570b1 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift @@ -5,6 +5,7 @@ struct DetailView: View { var selection: NavigationItem var snapshot: HostSnapshot? var errorMessage: String? + var refreshFocusedContext: () -> Void var body: some View { ScrollView { @@ -18,7 +19,7 @@ struct DetailView: View { switch selection { case .activity: - ActivityDetail(snapshot: snapshot) + ActivityDetail(snapshot: snapshot, refreshFocusedContext: refreshFocusedContext) case .appRules: AppRulesDetail() case .profiles: @@ -45,6 +46,7 @@ struct DetailView: View { private struct ActivityDetail: View { var snapshot: HostSnapshot? + var refreshFocusedContext: () -> Void var body: some View { LabeledContentGrid { @@ -78,9 +80,22 @@ private struct ActivityDetail: View { value: snapshot?.outputPolicy.label ?? "Loading", systemImage: "text.cursor" ) + StatusCard( + title: "Focused App", + value: snapshot?.focusedAppLabel ?? "No Context", + systemImage: "app.connected.to.app.below.fill" + ) + StatusCard( + title: "Window", + value: snapshot?.focusedWindowTitle ?? snapshot?.focusedURLDomain ?? "No Context", + systemImage: "macwindow" + ) } HStack(spacing: 10) { + Button("Refresh Focus", systemImage: "scope") { + refreshFocusedContext() + } Button("Start Recording", systemImage: "record.circle") {} .buttonStyle(.borderedProminent) .disabled(true) diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs index 97eb6be..547cca3 100644 --- a/packages/voxit-core/src/contextual.rs +++ b/packages/voxit-core/src/contextual.rs @@ -77,6 +77,16 @@ impl FocusedAppContext { Self::default() } + /// Whether the context has no routing signal. + pub fn is_empty(&self) -> bool { + self.bundle_id.as_deref().is_none_or(str::is_empty) + && self.app_name.as_deref().is_none_or(str::is_empty) + && self.window_title.as_deref().is_none_or(str::is_empty) + && self.url_domain.as_deref().is_none_or(str::is_empty) + && self.focused_element_role.as_deref().is_none_or(str::is_empty) + && !self.selected_text_present + } + /// Attach app identity to the context. pub fn with_app(mut self, bundle_id: impl Into, app_name: impl Into) -> Self { self.bundle_id = Some(bundle_id.into()); @@ -91,6 +101,27 @@ impl FocusedAppContext { self } + + /// Attach a focused window title to the context. + pub fn with_window_title(mut self, window_title: impl Into) -> Self { + self.window_title = Some(window_title.into()); + + self + } + + /// Attach the focused accessibility role to the context. + pub fn with_focused_element_role(mut self, focused_element_role: impl Into) -> Self { + self.focused_element_role = Some(focused_element_role.into()); + + self + } + + /// Attach selected-text presence to the context. + pub fn with_selected_text_present(mut self, selected_text_present: bool) -> Self { + self.selected_text_present = selected_text_present; + + self + } } /// Prompt profile selected by contextual routing. diff --git a/packages/voxit-host-ffi/Cargo.toml b/packages/voxit-host-ffi/Cargo.toml index 3f2fdc5..9058749 100644 --- a/packages/voxit-host-ffi/Cargo.toml +++ b/packages/voxit-host-ffi/Cargo.toml @@ -15,3 +15,6 @@ path = "src/lib.rs" [dependencies] voxit-core = { path = "../voxit-core" } + +[target.'cfg(target_os = "macos")'.dependencies] +voxit-macos = { path = "../voxit-macos" } diff --git a/packages/voxit-host-ffi/include/voxit_host_ffi.h b/packages/voxit-host-ffi/include/voxit_host_ffi.h index 249274b..9ba5fdd 100644 --- a/packages/voxit-host-ffi/include/voxit_host_ffi.h +++ b/packages/voxit-host-ffi/include/voxit_host_ffi.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define VOXIT_HOST_FFI_ABI_VERSION 2u +#define VOXIT_HOST_FFI_ABI_VERSION 3u typedef struct VoxitHostSessionHandle VoxitHostSessionHandle; @@ -75,6 +75,15 @@ typedef enum VoxitVoiceOutputPolicy { VOXIT_OUTPUT_POLICY_CONFIRM_BEFORE_ACTION = 2, } VoxitVoiceOutputPolicy; +typedef enum VoxitHostStringField { + VOXIT_HOST_STRING_FOCUSED_BUNDLE_ID = 0, + VOXIT_HOST_STRING_FOCUSED_APP_NAME = 1, + VOXIT_HOST_STRING_FOCUSED_WINDOW_TITLE = 2, + VOXIT_HOST_STRING_FOCUSED_URL_DOMAIN = 3, + VOXIT_HOST_STRING_FOCUSED_ELEMENT_ROLE = 4, + VOXIT_HOST_STRING_PROMPT_PROFILE_ID = 5, +} VoxitHostStringField; + typedef struct VoxitHostConfig { enum VoxitPlatformTag platform; } VoxitHostConfig; @@ -88,6 +97,8 @@ typedef struct VoxitHostSnapshot { uint32_t panel_width_px; uint32_t panel_height_px; uint8_t rewrite_enabled; + uint8_t has_focused_context; + uint8_t selected_text_present; enum VoxitPromptProfileKind prompt_profile_kind; enum VoxitVoiceInteractionTier voice_tier; enum VoxitVoiceReasoningEffort reasoning_effort; @@ -97,10 +108,17 @@ typedef struct VoxitHostSnapshot { uint32_t voxit_host_ffi_abi_version(void); VoxitHostSessionHandle *voxit_host_session_create(struct VoxitHostConfig config); void voxit_host_session_destroy(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_refresh_focused_context(VoxitHostSessionHandle *handle); enum VoxitStatus voxit_host_session_copy_snapshot( VoxitHostSessionHandle *handle, struct VoxitHostSnapshot *out ); +enum VoxitStatus voxit_host_session_copy_string( + VoxitHostSessionHandle *handle, + enum VoxitHostStringField field, + char *out, + uintptr_t out_len +); #ifdef __cplusplus } diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index c3473c6..e2f1010 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -4,7 +4,7 @@ //! This gives the Swift host a stable Rust-owned model without moving audio, auth, or //! inference orchestration across FFI before those boundaries are ready. -use std::ptr::NonNull; +use std::{ffi::c_char, ptr::NonNull}; use voxit_core::{ Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, @@ -16,11 +16,12 @@ use voxit_core::{ }; /// ABI version exported by the thin C host bridge. -pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 2; +pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 3; /// Opaque session handle owned by the native host through the C ABI. pub struct VoxitHostSessionHandle { snapshot: NativeHostSnapshot, + focused_context: FocusedAppContext, voice_plan: VoiceSessionPlan, } @@ -152,6 +153,24 @@ pub enum VoxitVoiceOutputPolicy { ConfirmBeforeAction = 2, } +/// FFI-safe string fields exposed through copy-out buffers. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VoxitHostStringField { + /// Focused app bundle id. + FocusedBundleId = 0, + /// Focused app display name. + FocusedAppName = 1, + /// Focused window title. + FocusedWindowTitle = 2, + /// Focused URL domain. + FocusedUrlDomain = 3, + /// Focused accessibility element role. + FocusedElementRole = 4, + /// Selected prompt profile id. + PromptProfileId = 5, +} + /// FFI-safe session configuration. #[repr(C)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -180,6 +199,10 @@ pub struct VoxitHostSnapshot { pub panel_height_px: u32, /// Non-zero when pass-3 rewrite is enabled. pub rewrite_enabled: u8, + /// Non-zero when focused app context has at least one routing signal. + pub has_focused_context: u8, + /// Non-zero when selected text was present at context capture time. + pub selected_text_present: u8, /// Selected prompt profile kind. pub prompt_profile_kind: VoxitPromptProfileKind, /// Selected voice interaction tier. @@ -200,6 +223,8 @@ impl Default for VoxitHostSnapshot { panel_width_px: 0, panel_height_px: 0, rewrite_enabled: 0, + has_focused_context: 0, + selected_text_present: 0, prompt_profile_kind: VoxitPromptProfileKind::FastDictation, voice_tier: VoxitVoiceInteractionTier::FastDictation, reasoning_effort: VoxitVoiceReasoningEffort::Minimal, @@ -223,10 +248,12 @@ pub extern "C" fn voxit_host_session_create( VoxitPlatformTag::MacOS => PlatformHost::MacOS, VoxitPlatformTag::Unsupported => PlatformHost::Unsupported, }; - let snapshot = NativeHostSnapshot::initial(platform, &Config::default()); - let voice_plan = ContextualVoiceRouter.plan_for_context(&FocusedAppContext::new()); + let app_config = Config::load().unwrap_or_else(|_| Config::default()); + let snapshot = NativeHostSnapshot::initial(platform, &app_config); + let focused_context = FocusedAppContext::new(); + let voice_plan = ContextualVoiceRouter.plan_for_context(&focused_context); - Box::into_raw(Box::new(VoxitHostSessionHandle { snapshot, voice_plan })) + Box::into_raw(Box::new(VoxitHostSessionHandle { snapshot, focused_context, voice_plan })) } /// Destroys a Rust-owned native-host session. @@ -260,13 +287,64 @@ pub unsafe extern "C" fn voxit_host_session_copy_snapshot( return VoxitStatus::NullOutput; }; let snapshot = unsafe { &handle.as_ref().snapshot }; + let focused_context = unsafe { &handle.as_ref().focused_context }; let voice_plan = unsafe { &handle.as_ref().voice_plan }; - unsafe { out.as_ptr().write(encode_snapshot(snapshot, voice_plan)) }; + unsafe { + out.as_ptr().write(encode_snapshot_with_context(snapshot, focused_context, voice_plan)) + }; + + VoxitStatus::Ok +} + +/// Refreshes focused app context and recomputes the Rust-owned voice session plan. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_refresh_focused_context( + handle: *mut VoxitHostSessionHandle, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + handle.focused_context = capture_focused_context(); + handle.voice_plan = ContextualVoiceRouter.plan_for_context(&handle.focused_context); VoxitStatus::Ok } +/// Copies a Rust-owned string field into caller-owned memory. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +/// `out` must point to writable memory for `out_len` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_copy_string( + handle: *mut VoxitHostSessionHandle, + field: VoxitHostStringField, + out: *mut c_char, + out_len: usize, +) -> VoxitStatus { + let Some(handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let Some(out) = NonNull::new(out) else { + return VoxitStatus::NullOutput; + }; + if out_len == 0 { + return VoxitStatus::InvalidInput; + } + let handle = unsafe { handle.as_ref() }; + let value = string_field_value(handle, field); + + write_c_string(out, out_len, value) +} + fn encode_snapshot( snapshot: &NativeHostSnapshot, voice_plan: &VoiceSessionPlan, @@ -280,6 +358,8 @@ fn encode_snapshot( panel_width_px: snapshot.panel_width_px, panel_height_px: snapshot.panel_height_px, rewrite_enabled: u8::from(snapshot.rewrite_enabled), + has_focused_context: 0, + selected_text_present: 0, prompt_profile_kind: encode_prompt_profile_kind(voice_plan.profile_kind), voice_tier: encode_voice_tier(voice_plan.tier), reasoning_effort: encode_reasoning_effort(voice_plan.reasoning_effort), @@ -287,6 +367,19 @@ fn encode_snapshot( } } +fn encode_snapshot_with_context( + snapshot: &NativeHostSnapshot, + focused_context: &FocusedAppContext, + voice_plan: &VoiceSessionPlan, +) -> VoxitHostSnapshot { + let mut encoded = encode_snapshot(snapshot, voice_plan); + + encoded.has_focused_context = u8::from(!focused_context.is_empty()); + encoded.selected_text_present = u8::from(focused_context.selected_text_present); + + encoded +} + fn encode_platform(platform: PlatformHost) -> VoxitPlatformTag { match platform { PlatformHost::MacOS => VoxitPlatformTag::MacOS, @@ -362,12 +455,69 @@ fn encode_output_policy(policy: VoiceOutputPolicy) -> VoxitVoiceOutputPolicy { } } +fn string_field_value(handle: &VoxitHostSessionHandle, field: VoxitHostStringField) -> &str { + match field { + VoxitHostStringField::FocusedBundleId => + handle.focused_context.bundle_id.as_deref().unwrap_or_default(), + VoxitHostStringField::FocusedAppName => + handle.focused_context.app_name.as_deref().unwrap_or_default(), + VoxitHostStringField::FocusedWindowTitle => + handle.focused_context.window_title.as_deref().unwrap_or_default(), + VoxitHostStringField::FocusedUrlDomain => + handle.focused_context.url_domain.as_deref().unwrap_or_default(), + VoxitHostStringField::FocusedElementRole => + handle.focused_context.focused_element_role.as_deref().unwrap_or_default(), + VoxitHostStringField::PromptProfileId => &handle.voice_plan.profile_id, + } +} + +fn write_c_string(out: NonNull, out_len: usize, value: &str) -> VoxitStatus { + let bytes = value.as_bytes(); + let copy_len = bytes.len().min(out_len.saturating_sub(1)); + + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_ptr().cast::(), copy_len); + *out.as_ptr().add(copy_len) = 0; + } + + VoxitStatus::Ok +} + +fn capture_focused_context() -> FocusedAppContext { + #[cfg(target_os = "macos")] + if let Some(target) = voxit_macos::capture_frontmost_app() { + return focused_context_from_target(target); + } + + FocusedAppContext::new() +} + +#[cfg(target_os = "macos")] +fn focused_context_from_target(target: voxit_macos::TargetApp) -> FocusedAppContext { + let mut context = FocusedAppContext::new(); + + if let (Some(bundle_id), Some(app_name)) = (target.bundle_id, target.app_name) { + context = context.with_app(bundle_id, app_name); + } + if let Some(window_title) = target.window_title { + context = context.with_window_title(window_title); + } + if let Some(url_domain) = target.url_domain { + context = context.with_url_domain(url_domain); + } + if let Some(role) = target.focused_element_role { + context = context.with_focused_element_role(role); + } + + context.with_selected_text_present(target.selected_text_present) +} + #[cfg(test)] mod tests { use crate::{ - VoxitAuthMethod, VoxitDictationState, VoxitHostConfig, VoxitHostSnapshot, VoxitPlatformTag, - VoxitPromptProfileKind, VoxitStatus, VoxitVoiceInteractionTier, VoxitVoiceOutputPolicy, - VoxitVoiceReasoningEffort, + VoxitAuthMethod, VoxitDictationState, VoxitHostConfig, VoxitHostSnapshot, + VoxitHostStringField, VoxitPlatformTag, VoxitPromptProfileKind, VoxitStatus, + VoxitVoiceInteractionTier, VoxitVoiceOutputPolicy, VoxitVoiceReasoningEffort, }; #[test] @@ -382,6 +532,8 @@ mod tests { assert_eq!(snapshot.auth_method, VoxitAuthMethod::ChatGptDeviceCode); assert_eq!(snapshot.dictation_state, VoxitDictationState::Idle); assert_eq!(snapshot.rewrite_enabled, 1); + assert_eq!(snapshot.has_focused_context, 0); + assert_eq!(snapshot.selected_text_present, 0); assert_eq!(snapshot.prompt_profile_kind, VoxitPromptProfileKind::FastDictation); assert_eq!(snapshot.voice_tier, VoxitVoiceInteractionTier::FastDictation); assert_eq!(snapshot.reasoning_effort, VoxitVoiceReasoningEffort::Minimal); @@ -389,4 +541,24 @@ mod tests { unsafe { crate::voxit_host_session_destroy(handle) }; } + + #[test] + fn string_copy_null_terminates_buffer() { + let handle = + crate::voxit_host_session_create(VoxitHostConfig { platform: VoxitPlatformTag::MacOS }); + let mut buffer = [1_i8; 4]; + let status = unsafe { + crate::voxit_host_session_copy_string( + handle, + VoxitHostStringField::PromptProfileId, + buffer.as_mut_ptr(), + buffer.len(), + ) + }; + + assert_eq!(status, VoxitStatus::Ok); + assert_eq!(buffer[3], 0); + + unsafe { crate::voxit_host_session_destroy(handle) }; + } } diff --git a/packages/voxit-macos/Cargo.toml b/packages/voxit-macos/Cargo.toml index b67af28..e07c6d0 100644 --- a/packages/voxit-macos/Cargo.toml +++ b/packages/voxit-macos/Cargo.toml @@ -11,6 +11,7 @@ version.workspace = true [dependencies] tracing = { version = "0.1" } +url = { version = "2.5" } [target.'cfg(target_os = "macos")'.dependencies] block2 = "0.6.2" diff --git a/packages/voxit-macos/src/lib.rs b/packages/voxit-macos/src/lib.rs index 2777d05..a403525 100644 --- a/packages/voxit-macos/src/lib.rs +++ b/packages/voxit-macos/src/lib.rs @@ -10,6 +10,7 @@ use std::{ffi::c_void, mem, thread, time::Duration}; use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; #[cfg(target_os = "macos")] use objc2_av_foundation::{AVAuthorizationStatus, AVCaptureDevice, AVMediaTypeAudio}; +use url::Url; #[cfg(target_os = "macos")] type CfDictionaryRef = *const c_void; @@ -25,11 +26,25 @@ pub struct TargetApp { pub bundle_id: Option, /// Frontmost application name. pub app_name: Option, + /// Focused window title when available. + pub window_title: Option, + /// Browser or webview URL domain when available. + pub url_domain: Option, + /// Focused accessibility element role when available. + pub focused_element_role: Option, + /// Whether selected text was present when capture started. + pub selected_text_present: bool, } impl TargetApp { /// Whether all fields are missing. pub fn is_empty(&self) -> bool { - self.pid.is_none() && self.bundle_id.is_none() && self.app_name.is_none() + self.pid.is_none() + && self.bundle_id.is_none() + && self.app_name.is_none() + && self.window_title.is_none() + && self.url_domain.is_none() + && self.focused_element_role.is_none() + && !self.selected_text_present } fn log_id(&self) -> String { @@ -301,23 +316,53 @@ pub fn activate_target(_target: &TargetApp, _attempts: u32, _base_delay: Duratio #[cfg(target_os = "macos")] fn capture_frontmost_app_impl() -> Result { let script = r#"tell application "System Events" - set front_process to first application process whose frontmost is true - set front_pid to unix id of front_process - set front_name to name of front_process + set front_process to first application process whose frontmost is true + set front_pid to unix id of front_process + set front_name to name of front_process try set front_bundle to bundle identifier of front_process - on error - set front_bundle to "" - end try - return front_pid & "|" & front_bundle & "|" & front_name - end tell"#; + on error + set front_bundle to "" + end try + set front_window_title to "" + try + set front_window_title to name of front window of front_process + end try + set focused_role to "" + set selected_text_present to "false" + try + set focused_element to value of attribute "AXFocusedUIElement" of front_process + try + set focused_role to role of focused_element + end try + try + set selected_text to value of attribute "AXSelectedText" of focused_element + if selected_text is not missing value and selected_text is not "" then + set selected_text_present to "true" + end if + end try + end try + return front_pid & "|" & front_bundle & "|" & front_name & "|" & front_window_title & "|" & focused_role & "|" & selected_text_present + end tell"#; let output = execute_applescript_raw(script)?; - let mut parts = output.splitn(3, '|'); + let mut parts = output.splitn(6, '|'); let pid = parts.next().and_then(parse_u32_trimmed); let bundle_id = parts.next().and_then(normalize_optional_string); let app_name = parts.next().and_then(normalize_optional_string); - - Ok(TargetApp { pid, bundle_id, app_name }) + let window_title = parts.next().and_then(normalize_optional_string); + let focused_element_role = parts.next().and_then(normalize_optional_string); + let selected_text_present = parts.next().is_some_and(|raw| raw.trim() == "true"); + let url_domain = capture_url_domain(bundle_id.as_deref(), app_name.as_deref()); + + Ok(TargetApp { + pid, + bundle_id, + app_name, + window_title, + url_domain, + focused_element_role, + selected_text_present, + }) } #[cfg(not(target_os = "macos"))] @@ -358,6 +403,48 @@ fn execute_applescript_raw(script: &str) -> Result { Ok(stdout.trim().to_string()) } +#[cfg(target_os = "macos")] +fn capture_url_domain(bundle_id: Option<&str>, app_name: Option<&str>) -> Option { + let script = browser_url_script(bundle_id, app_name)?; + let url = execute_applescript_raw(&script).ok()?; + + parse_domain(&url) +} + +#[cfg(target_os = "macos")] +fn browser_url_script(bundle_id: Option<&str>, app_name: Option<&str>) -> Option { + let identity = bundle_id.or(app_name)?.to_ascii_lowercase(); + + if identity.contains("safari") { + return Some(r#"tell application "Safari" to return URL of front document"#.to_string()); + } + if identity.contains("chrome") { + return Some( + r#"tell application "Google Chrome" to return URL of active tab of front window"# + .to_string(), + ); + } + if identity.contains("microsoft.edgemac") || identity.contains("microsoft edge") { + return Some( + r#"tell application "Microsoft Edge" to return URL of active tab of front window"# + .to_string(), + ); + } + if identity.contains("company.thebrowser.browser") || identity.contains("arc") { + return Some( + r#"tell application "Arc" to return URL of active tab of front window"#.to_string(), + ); + } + + None +} + +fn parse_domain(raw_url: &str) -> Option { + let url = Url::parse(raw_url.trim()).ok()?; + + url.host_str().map(|domain| domain.trim_start_matches("www.").to_ascii_lowercase()) +} + #[cfg(not(target_os = "macos"))] fn execute_applescript_raw(_script: &str) -> Result { Err("activation is not available on non-macOS".to_string()) From 21ca20a9a61c5582ffa9a747b9bbfbc7793f441e Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:45:45 +0800 Subject: [PATCH 10/27] {"schema":"maestro/commit/1","summary":"Apply voice plans to rewrite prompts","authority":"manual"} --- packages/voxit-core/src/contextual.rs | 82 +++++++++++++++++++- packages/voxit-core/src/inference.rs | 74 +++++++++++++++++- packages/voxit-core/src/lib.rs | 4 +- packages/voxit-core/src/openai.rs | 3 +- packages/voxit-core/src/providers.rs | 1 + packages/voxit-core/src/providers/chatgpt.rs | 3 +- 6 files changed, 157 insertions(+), 10 deletions(-) diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs index 547cca3..e190e7f 100644 --- a/packages/voxit-core/src/contextual.rs +++ b/packages/voxit-core/src/contextual.rs @@ -133,6 +133,8 @@ pub struct PromptProfile { pub id: String, /// Human-readable profile title. pub title: String, + /// Prompt direction applied to app-aware rewrite and future reasoning sessions. + pub prompt_directive: String, /// User-facing interaction tier. pub tier: VoiceInteractionTier, /// Default reasoning effort for this profile. @@ -146,11 +148,20 @@ impl PromptProfile { kind: PromptProfileKind, id: impl Into, title: impl Into, + prompt_directive: impl Into, tier: VoiceInteractionTier, reasoning_effort: VoiceReasoningEffort, output_policy: VoiceOutputPolicy, ) -> Self { - Self { kind, id: id.into(), title: title.into(), tier, reasoning_effort, output_policy } + Self { + kind, + id: id.into(), + title: title.into(), + prompt_directive: prompt_directive.into(), + tier, + reasoning_effort, + output_policy, + } } } @@ -163,6 +174,8 @@ pub struct VoiceSessionPlan { pub profile_id: String, /// Selected profile display title. pub profile_title: String, + /// Prompt direction selected for this session. + pub prompt_directive: String, /// Selected interaction tier. pub tier: VoiceInteractionTier, /// Selected reasoning effort. @@ -176,11 +189,34 @@ impl VoiceSessionPlan { profile_kind: profile.kind, profile_id: profile.id, profile_title: profile.title, + prompt_directive: profile.prompt_directive, tier: profile.tier, reasoning_effort: profile.reasoning_effort, output_policy: profile.output_policy, } } + + /// Build provider instructions for a contextual rewrite pass. + pub fn rewrite_instructions(&self, style: &str, max_output_chars: u32) -> String { + let max_output_chars = max_output_chars.max(1); + + format!( + "You are Voxit, a contextual voice input layer. Rewrite the transcript for the destination app, not as generic ASR cleanup.\n\ + Active profile: {profile_title} ({profile_id}).\n\ + Profile direction: {prompt_directive}\n\ + Interaction tier: {tier}.\n\ + Reasoning effort target: {reasoning_effort}.\n\ + Output policy: {output_policy}.\n\ + Style preset: {style}.\n\ + Constraints: preserve meaning, numbers, dates, money amounts, names, identifiers, and file paths unless the user explicitly said to change them. Keep the answer under {max_output_chars} characters. Return only the final text to insert or preview.", + profile_title = self.profile_title, + profile_id = self.profile_id, + prompt_directive = self.prompt_directive, + tier = interaction_tier_label(self.tier), + reasoning_effort = reasoning_effort_label(self.reasoning_effort), + output_policy = output_policy_label(self.output_policy), + ) + } } /// Deterministic router from focused app context to a voice session plan. @@ -237,6 +273,7 @@ fn default_dictation_profile() -> PromptProfile { PromptProfileKind::FastDictation, "fast-dictation", "Fast Dictation", + "Clean punctuation and readability with the lowest possible latency. Do not expand terse speech into a different structure.", VoiceInteractionTier::FastDictation, VoiceReasoningEffort::Minimal, VoiceOutputPolicy::InsertText, @@ -248,6 +285,7 @@ fn messaging_profile() -> PromptProfile { PromptProfileKind::Messaging, "messaging", "Messaging", + "Shape the transcript as a concise conversational message for chat. Prefer natural short paragraphs and avoid email-like signoffs.", VoiceInteractionTier::ContextRewrite, VoiceReasoningEffort::Low, VoiceOutputPolicy::InsertText, @@ -259,6 +297,7 @@ fn mail_profile() -> PromptProfile { PromptProfileKind::Mail, "mail", "Mail", + "Shape the transcript as complete but restrained email prose. Preserve intent while adding only necessary greeting, punctuation, and paragraph structure.", VoiceInteractionTier::ContextRewrite, VoiceReasoningEffort::Low, VoiceOutputPolicy::PreviewBeforeInsert, @@ -270,6 +309,7 @@ fn code_editor_profile() -> PromptProfile { PromptProfileKind::CodeEditor, "code-editor", "Code Editor", + "Shape the transcript for code-editing work. Preserve symbols, identifiers, filenames, APIs, and quoted code-like phrases exactly when possible.", VoiceInteractionTier::ContextRewrite, VoiceReasoningEffort::Low, VoiceOutputPolicy::PreviewBeforeInsert, @@ -281,6 +321,7 @@ fn terminal_profile() -> PromptProfile { PromptProfileKind::Terminal, "terminal", "Terminal", + "Produce a terminal-focused command proposal or explanation. Never imply that a command has run; keep risky shell actions clearly previewable.", VoiceInteractionTier::VoiceIntent, VoiceReasoningEffort::Medium, VoiceOutputPolicy::ConfirmBeforeAction, @@ -292,12 +333,38 @@ fn work_tracker_profile() -> PromptProfile { PromptProfileKind::WorkTracker, "work-tracker", "Work Tracker", + "Shape the transcript as a practical issue, pull request, review, status, or acceptance-criteria note. Prefer concrete bullets when they improve scanability.", VoiceInteractionTier::ContextRewrite, VoiceReasoningEffort::Medium, VoiceOutputPolicy::PreviewBeforeInsert, ) } +fn interaction_tier_label(tier: VoiceInteractionTier) -> &'static str { + match tier { + VoiceInteractionTier::FastDictation => "fast_dictation", + VoiceInteractionTier::ContextRewrite => "context_rewrite", + VoiceInteractionTier::VoiceIntent => "voice_intent", + } +} + +fn reasoning_effort_label(effort: VoiceReasoningEffort) -> &'static str { + match effort { + VoiceReasoningEffort::Minimal => "minimal", + VoiceReasoningEffort::Low => "low", + VoiceReasoningEffort::Medium => "medium", + VoiceReasoningEffort::High => "high", + } +} + +fn output_policy_label(policy: VoiceOutputPolicy) -> &'static str { + match policy { + VoiceOutputPolicy::InsertText => "insert_text", + VoiceOutputPolicy::PreviewBeforeInsert => "preview_before_insert", + VoiceOutputPolicy::ConfirmBeforeAction => "confirm_before_action", + } +} + #[cfg(test)] mod tests { use crate::contextual::{ @@ -365,4 +432,17 @@ mod tests { assert_eq!(plan.profile_id, "work-tracker"); assert_eq!(plan.reasoning_effort, VoiceReasoningEffort::Medium); } + + #[test] + fn rewrite_instructions_include_profile_policy_and_limits() { + let router = ContextualVoiceRouter; + let context = FocusedAppContext::new().with_app("com.apple.Terminal", "Terminal"); + let plan = router.plan_for_context(&context); + let instructions = plan.rewrite_instructions("concise", 1200); + + assert!(instructions.contains("Terminal")); + assert!(instructions.contains("confirm_before_action")); + assert!(instructions.contains("concise")); + assert!(instructions.contains("1200")); + } } diff --git a/packages/voxit-core/src/inference.rs b/packages/voxit-core/src/inference.rs index dff7be6..a38b69e 100644 --- a/packages/voxit-core/src/inference.rs +++ b/packages/voxit-core/src/inference.rs @@ -6,6 +6,7 @@ use std::sync::mpsc::{Receiver, Sender}; #[cfg(target_os = "macos")] use crate::providers::{self, InferenceProvider, RewriteRequest, TranscriptionRequest}; use crate::{ + ContextualVoiceRouter, FocusedAppContext, VoiceSessionPlan, providers::chatgpt::ChatGptProvider, realtime::{RealtimeError, RealtimeEvent, RealtimeSession, RealtimeSessionConfig}, }; @@ -33,6 +34,22 @@ pub struct RewriteResult { pub reason: Option, } +/// Settings applied to a contextual rewrite pass. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RewriteSettings { + /// Preserve numeric, date, and currency tokens. + pub guard_protected_tokens: bool, + /// Maximum accepted rewritten output length. + pub max_output_chars: u32, + /// Style preset supplied to prompt construction. + pub style: String, +} +impl Default for RewriteSettings { + fn default() -> Self { + Self { guard_protected_tokens: true, max_output_chars: 8_000, style: "clean".to_string() } + } +} + /// Background event sent to the UI thread. #[derive(Debug)] pub enum InferenceEvent { @@ -103,6 +120,19 @@ pub fn transcribe_only(_wav: &[u8], _model: &str) -> Result<(String, u64), Strin /// Rewrites transcript text with protected-token guard checks. #[cfg(target_os = "macos")] pub fn rewrite_only(text: &str, model: &str) -> Result<(RewriteResult, u64), String> { + let plan = ContextualVoiceRouter.plan_for_context(&FocusedAppContext::new()); + + rewrite_only_with_plan(text, model, &plan, &RewriteSettings::default()) +} + +/// Rewrites transcript text with the selected contextual voice plan. +#[cfg(target_os = "macos")] +pub fn rewrite_only_with_plan( + text: &str, + model: &str, + plan: &VoiceSessionPlan, + settings: &RewriteSettings, +) -> Result<(RewriteResult, u64), String> { if text.trim().is_empty() { return Ok(( RewriteResult { @@ -115,7 +145,7 @@ pub fn rewrite_only(text: &str, model: &str) -> Result<(RewriteResult, u64), Str } let started = Instant::now(); - let result = rewrite_with_guard(text, model)?; + let result = rewrite_with_guard(text, model, plan, settings)?; Ok((result, started.elapsed().as_millis() as u64)) } @@ -126,18 +156,54 @@ pub fn rewrite_only(_text: &str, _model: &str) -> Result<(RewriteResult, u64), S Err("inference pipeline is only enabled on macOS builds.".to_string()) } +/// Inference pipeline is unavailable on non-macOS placeholder builds. +#[cfg(not(target_os = "macos"))] +pub fn rewrite_only_with_plan( + _text: &str, + _model: &str, + _plan: &VoiceSessionPlan, + _settings: &RewriteSettings, +) -> Result<(RewriteResult, u64), String> { + Err("inference pipeline is only enabled on macOS builds.".to_string()) +} + #[cfg(target_os = "macos")] fn default_provider() -> Result { providers::chatgpt_oauth_provider() } #[cfg(target_os = "macos")] -fn rewrite_with_guard(text: &str, model: &str) -> Result { +fn rewrite_with_guard( + text: &str, + model: &str, + plan: &VoiceSessionPlan, + settings: &RewriteSettings, +) -> Result { let provider = default_provider()?; - let rewritten = provider.rewrite(RewriteRequest { text, model })?; + let instructions = plan.rewrite_instructions(&settings.style, settings.max_output_chars); + let rewritten = + provider.rewrite(RewriteRequest { text, model, instructions: &instructions })?; + + if rewritten.chars().count() > settings.max_output_chars as usize { + return Ok(RewriteResult { + rewritten_transcript: None, + state: RewriteState::Rejected, + reason: Some(format!( + "rewrite exceeded max output length (limit={} chars). Using ASR transcript for safety.", + settings.max_output_chars + )), + }); + } + if !settings.guard_protected_tokens { + return Ok(RewriteResult { + rewritten_transcript: Some(rewritten), + state: RewriteState::Applied, + reason: None, + }); + } + let baseline = protected_token_multiset(text); let candidate = protected_token_multiset(&rewritten); - if baseline != candidate { return Ok(RewriteResult { rewritten_transcript: None, diff --git a/packages/voxit-core/src/lib.rs b/packages/voxit-core/src/lib.rs index 6eb6571..0443f90 100644 --- a/packages/voxit-core/src/lib.rs +++ b/packages/voxit-core/src/lib.rs @@ -23,8 +23,8 @@ pub use self::{ VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, VoiceSessionPlan, }, inference::{ - InferenceEvent, RewriteResult, RewriteState, rewrite_only, start_realtime_session, - transcribe_only, + InferenceEvent, RewriteResult, RewriteSettings, RewriteState, rewrite_only, + rewrite_only_with_plan, start_realtime_session, transcribe_only, }, realtime::{ REALTIME_ENDPOINT, RealtimeError, RealtimeEvent, RealtimeSession, RealtimeSessionConfig, diff --git a/packages/voxit-core/src/openai.rs b/packages/voxit-core/src/openai.rs index 03f7173..a2b06ef 100644 --- a/packages/voxit-core/src/openai.rs +++ b/packages/voxit-core/src/openai.rs @@ -1,5 +1,6 @@ //! Backward-compatible inference re-exports. pub use crate::inference::{ - InferenceEvent, RewriteResult, RewriteState, rewrite_only, transcribe_only, + InferenceEvent, RewriteResult, RewriteSettings, RewriteState, rewrite_only, + rewrite_only_with_plan, transcribe_only, }; diff --git a/packages/voxit-core/src/providers.rs b/packages/voxit-core/src/providers.rs index 9c3ff84..0904c11 100644 --- a/packages/voxit-core/src/providers.rs +++ b/packages/voxit-core/src/providers.rs @@ -37,6 +37,7 @@ pub(crate) struct TranscriptionRequest<'a> { pub(crate) struct RewriteRequest<'a> { pub(crate) text: &'a str, pub(crate) model: &'a str, + pub(crate) instructions: &'a str, } /// Resolve the only provider enabled in the first provider-abstraction version. diff --git a/packages/voxit-core/src/providers/chatgpt.rs b/packages/voxit-core/src/providers/chatgpt.rs index 8fb74ee..d202947 100644 --- a/packages/voxit-core/src/providers/chatgpt.rs +++ b/packages/voxit-core/src/providers/chatgpt.rs @@ -56,11 +56,10 @@ impl ChatGptProvider { } fn rewrite_chatgpt(&self, request: RewriteRequest<'_>) -> Result { - let prompt = "Rewrite the transcript for punctuation and readability. Keep the meaning, numbers, and names intact."; let body = serde_json::json!({ "model": request.model, "input": format!("Transcript: {}", request.text), - "instructions": prompt, + "instructions": request.instructions, "temperature": 0.2, }); let body = self.post_json(OPENAI_RESPONSES_ENDPOINT, body)?; From f29d034c45fc4966afbc0b762245a84b4110285b Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:52:58 +0800 Subject: [PATCH 11/27] {"schema":"maestro/commit/1","summary":"Wire dictation commands through native host","authority":"manual"} --- Cargo.lock | 1 + .../Sources/VoxitHostBridge/HostFFI.swift | 52 ++- .../App/VoxitNativeHostApp.swift | 58 +++- .../VoxitNativeHostKit/Stores/HostStore.swift | 30 ++ .../VoxitNativeHostKit/Support/Labels.swift | 10 + .../Views/ContentView.swift | 26 +- .../VoxitNativeHostKit/Views/DetailView.swift | 63 +++- .../Views/RecordingHUDView.swift | 76 +++++ packages/voxit-host-ffi/Cargo.toml | 1 + .../voxit-host-ffi/include/voxit_host_ffi.h | 13 +- packages/voxit-host-ffi/src/lib.rs | 314 +++++++++++++++++- packages/voxit-macos/src/lib.rs | 53 ++- 12 files changed, 658 insertions(+), 39 deletions(-) create mode 100644 native/macos-host/Sources/VoxitNativeHostKit/Views/RecordingHUDView.swift diff --git a/Cargo.lock b/Cargo.lock index 5b0cf1c..d3f9341 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,6 +1633,7 @@ dependencies = [ name = "voxit-host-ffi" version = "0.1.0" dependencies = [ + "voxit-audio", "voxit-core", "voxit-macos", ] diff --git a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift index a02f3cd..8f2ed49 100644 --- a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift +++ b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift @@ -69,12 +69,20 @@ public struct HostSnapshot: Equatable, Sendable { public var rewriteEnabled: Bool public var hasFocusedContext: Bool public var selectedTextPresent: Bool + public var hasRawTranscript: Bool + public var hasFinalOutput: Bool + public var hasError: Bool + public var recordingDurationMS: UInt64 public var focusedBundleID: String? public var focusedAppName: String? public var focusedWindowTitle: String? public var focusedURLDomain: String? public var focusedElementRole: String? public var promptProfileID: String? + public var promptDirective: String? + public var rawTranscript: String? + public var finalOutput: String? + public var lastError: String? public var promptProfileKind: PromptProfileKind public var voiceTier: VoiceInteractionTier public var reasoningEffort: VoiceReasoningEffort @@ -91,12 +99,20 @@ public struct HostSnapshot: Equatable, Sendable { rewriteEnabled: Bool, hasFocusedContext: Bool, selectedTextPresent: Bool, + hasRawTranscript: Bool, + hasFinalOutput: Bool, + hasError: Bool, + recordingDurationMS: UInt64, focusedBundleID: String?, focusedAppName: String?, focusedWindowTitle: String?, focusedURLDomain: String?, focusedElementRole: String?, promptProfileID: String?, + promptDirective: String?, + rawTranscript: String?, + finalOutput: String?, + lastError: String?, promptProfileKind: PromptProfileKind, voiceTier: VoiceInteractionTier, reasoningEffort: VoiceReasoningEffort, @@ -112,12 +128,20 @@ public struct HostSnapshot: Equatable, Sendable { self.rewriteEnabled = rewriteEnabled self.hasFocusedContext = hasFocusedContext self.selectedTextPresent = selectedTextPresent + self.hasRawTranscript = hasRawTranscript + self.hasFinalOutput = hasFinalOutput + self.hasError = hasError + self.recordingDurationMS = recordingDurationMS self.focusedBundleID = focusedBundleID self.focusedAppName = focusedAppName self.focusedWindowTitle = focusedWindowTitle self.focusedURLDomain = focusedURLDomain self.focusedElementRole = focusedElementRole self.promptProfileID = promptProfileID + self.promptDirective = promptDirective + self.rawTranscript = rawTranscript + self.finalOutput = finalOutput + self.lastError = lastError self.promptProfileKind = promptProfileKind self.voiceTier = voiceTier self.reasoningEffort = reasoningEffort @@ -212,6 +236,24 @@ public final class VoxitHostSession { return try currentSnapshot() } + public func startDictation() throws -> HostSnapshot { + try requireOk(voxit_host_session_start_dictation(handle), context: "starting dictation") + + return try currentSnapshot() + } + + public func stopDictation() throws -> HostSnapshot { + try requireOk(voxit_host_session_stop_dictation(handle), context: "stopping dictation") + + return try currentSnapshot() + } + + public func pasteFinalOutput() throws -> HostSnapshot { + try requireOk(voxit_host_session_paste_final_output(handle), context: "pasting final output") + + return try currentSnapshot() + } + private func requireOk(_ status: VoxitStatus, context: String) throws { let code = voxit_status_code(status) if code != 0 { @@ -231,12 +273,20 @@ public final class VoxitHostSession { rewriteEnabled: snapshot.rewrite_enabled != 0, hasFocusedContext: snapshot.has_focused_context != 0, selectedTextPresent: snapshot.selected_text_present != 0, + hasRawTranscript: snapshot.has_raw_transcript != 0, + hasFinalOutput: snapshot.has_final_output != 0, + hasError: snapshot.has_error != 0, + recordingDurationMS: snapshot.recording_duration_ms, focusedBundleID: try copyString(field: VOXIT_HOST_STRING_FOCUSED_BUNDLE_ID), focusedAppName: try copyString(field: VOXIT_HOST_STRING_FOCUSED_APP_NAME), focusedWindowTitle: try copyString(field: VOXIT_HOST_STRING_FOCUSED_WINDOW_TITLE), focusedURLDomain: try copyString(field: VOXIT_HOST_STRING_FOCUSED_URL_DOMAIN), focusedElementRole: try copyString(field: VOXIT_HOST_STRING_FOCUSED_ELEMENT_ROLE), promptProfileID: try copyString(field: VOXIT_HOST_STRING_PROMPT_PROFILE_ID), + promptDirective: try copyString(field: VOXIT_HOST_STRING_PROMPT_DIRECTIVE), + rawTranscript: try copyString(field: VOXIT_HOST_STRING_RAW_TRANSCRIPT), + finalOutput: try copyString(field: VOXIT_HOST_STRING_FINAL_OUTPUT), + lastError: try copyString(field: VOXIT_HOST_STRING_LAST_ERROR), promptProfileKind: try decode(promptProfileKind: snapshot.prompt_profile_kind), voiceTier: try decode(voiceTier: snapshot.voice_tier), reasoningEffort: try decode(reasoningEffort: snapshot.reasoning_effort), @@ -245,7 +295,7 @@ public final class VoxitHostSession { } private func copyString(field: VoxitHostStringField) throws -> String? { - var buffer = [CChar](repeating: 0, count: 1024) + var buffer = [CChar](repeating: 0, count: 65_536) let bufferCount = buffer.count try buffer.withUnsafeMutableBufferPointer { pointer in try requireOk( diff --git a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift index 4d87356..f97a2df 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift @@ -27,12 +27,21 @@ public struct VoxitNativeHostApp: App { } CommandGroup(after: .appInfo) { - Button("Start Dictation") {} - .keyboardShortcut( - settingsStore.settings.dictationHotkeyPresentation.swiftUIKeyEquivalent, - modifiers: settingsStore.settings.dictationHotkeyPresentation.swiftUIModifiers - ) - .disabled(true) + Button("Start Dictation") { + startDictation() + } + .keyboardShortcut( + settingsStore.settings.dictationHotkeyPresentation.swiftUIKeyEquivalent, + modifiers: settingsStore.settings.dictationHotkeyPresentation.swiftUIModifiers + ) + + Button("Stop Dictation") { + Task { + await store.stopDictation() + } + } + .keyboardShortcut(".", modifiers: [.command]) + .disabled(store.snapshot?.dictationState != .listening) Divider() @@ -45,6 +54,15 @@ public struct VoxitNativeHostApp: App { } } + Window("Voxit Recording", id: "recording-hud") { + RecordingHUDView(store: store) + .task { + await store.reload() + } + } + .windowResizability(.contentSize) + .defaultPosition(.topTrailing) + MenuBarExtra { Button("Open Voxit") { openWindow(id: "main") @@ -52,12 +70,20 @@ public struct VoxitNativeHostApp: App { } .keyboardShortcut("o", modifiers: [.command]) - Button("Start Dictation") {} - .keyboardShortcut( - settingsStore.settings.dictationHotkeyPresentation.swiftUIKeyEquivalent, - modifiers: settingsStore.settings.dictationHotkeyPresentation.swiftUIModifiers - ) - .disabled(true) + Button("Start Dictation") { + startDictation() + } + .keyboardShortcut( + settingsStore.settings.dictationHotkeyPresentation.swiftUIKeyEquivalent, + modifiers: settingsStore.settings.dictationHotkeyPresentation.swiftUIModifiers + ) + + Button("Stop Dictation") { + Task { + await store.stopDictation() + } + } + .disabled(store.snapshot?.dictationState != .listening) Divider() @@ -86,6 +112,14 @@ public struct VoxitNativeHostApp: App { } } + @MainActor + private func startDictation() { + openWindow(id: "recording-hud") + Task { + await store.startDictation() + } + } + @MainActor private func presentSettings() { if settingsWindowController == nil { diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift index bbcd668..584572d 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift @@ -30,6 +30,36 @@ public final class HostStore: ObservableObject { } } + public func startDictation() async { + do { + let session = try currentSession() + snapshot = try session.startDictation() + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + + public func stopDictation() async { + do { + let session = try currentSession() + snapshot = try session.stopDictation() + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + + public func pasteFinalOutput() async { + do { + let session = try currentSession() + snapshot = try session.pasteFinalOutput() + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + private func currentSession() throws -> VoxitHostSession { if let session { return session diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift index df9c075..0b5d148 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Support/Labels.swift @@ -125,4 +125,14 @@ extension HostSnapshot { } return "No Context" } + + var recordingSummary: String { + if recordingDurationMS > 0 { + return "\(recordingDurationMS) ms" + } + if hasRawTranscript || hasFinalOutput { + return "Completed" + } + return "No Runs" + } } diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift index 569e416..c4f50fb 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift @@ -16,12 +16,28 @@ public struct ContentView: View { DetailView( selection: selection, snapshot: store.snapshot, - errorMessage: store.errorMessage - ) { - Task { - await store.refreshFocusedContext() + errorMessage: store.errorMessage, + refreshFocusedContext: { + Task { + await store.refreshFocusedContext() + } + }, + startDictation: { + Task { + await store.startDictation() + } + }, + stopDictation: { + Task { + await store.stopDictation() + } + }, + pasteFinalOutput: { + Task { + await store.pasteFinalOutput() + } } - } + ) } } } diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift index 7e570b1..e1b57cb 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift @@ -6,6 +6,9 @@ struct DetailView: View { var snapshot: HostSnapshot? var errorMessage: String? var refreshFocusedContext: () -> Void + var startDictation: () -> Void + var stopDictation: () -> Void + var pasteFinalOutput: () -> Void var body: some View { ScrollView { @@ -19,7 +22,13 @@ struct DetailView: View { switch selection { case .activity: - ActivityDetail(snapshot: snapshot, refreshFocusedContext: refreshFocusedContext) + ActivityDetail( + snapshot: snapshot, + refreshFocusedContext: refreshFocusedContext, + startDictation: startDictation, + stopDictation: stopDictation, + pasteFinalOutput: pasteFinalOutput + ) case .appRules: AppRulesDetail() case .profiles: @@ -47,6 +56,9 @@ struct DetailView: View { private struct ActivityDetail: View { var snapshot: HostSnapshot? var refreshFocusedContext: () -> Void + var startDictation: () -> Void + var stopDictation: () -> Void + var pasteFinalOutput: () -> Void var body: some View { LabeledContentGrid { @@ -80,6 +92,11 @@ private struct ActivityDetail: View { value: snapshot?.outputPolicy.label ?? "Loading", systemImage: "text.cursor" ) + StatusCard( + title: "Last Run", + value: snapshot?.recordingSummary ?? "No Runs", + systemImage: "timer" + ) StatusCard( title: "Focused App", value: snapshot?.focusedAppLabel ?? "No Context", @@ -96,11 +113,26 @@ private struct ActivityDetail: View { Button("Refresh Focus", systemImage: "scope") { refreshFocusedContext() } - Button("Start Recording", systemImage: "record.circle") {} - .buttonStyle(.borderedProminent) - .disabled(true) - Button("Paste Raw", systemImage: "text.badge.checkmark") {} - .disabled(true) + Button("Start Recording", systemImage: "record.circle") { + startDictation() + } + .buttonStyle(.borderedProminent) + .disabled(snapshot?.dictationState == .listening) + Button("Stop", systemImage: "stop.circle") { + stopDictation() + } + .disabled(snapshot?.dictationState != .listening) + Button("Paste Final", systemImage: "text.badge.checkmark") { + pasteFinalOutput() + } + .disabled(snapshot?.hasFinalOutput != true) + } + + if let finalOutput = snapshot?.finalOutput { + TranscriptPreview(title: "Final Output", text: finalOutput) + } + if let rawTranscript = snapshot?.rawTranscript { + TranscriptPreview(title: "Raw Transcript", text: rawTranscript) } } } @@ -171,6 +203,25 @@ private struct PromptLabDetail: View { } } +private struct TranscriptPreview: View { + var title: String + var text: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + Text(text) + .font(.body) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(14) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + } +} + private struct LabeledContentGrid: View { @ViewBuilder var content: Content diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/RecordingHUDView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/RecordingHUDView.swift new file mode 100644 index 0000000..de41c80 --- /dev/null +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/RecordingHUDView.swift @@ -0,0 +1,76 @@ +import SwiftUI +import VoxitHostBridge + +struct RecordingHUDView: View { + @ObservedObject var store: HostStore + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 2) { + Text(store.snapshot?.dictationState.label ?? "Loading") + .font(.headline) + Text(store.snapshot?.promptProfileKind.label ?? "Fast Dictation") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Circle() + .fill(statusColor) + .frame(width: 10, height: 10) + } + + Text(previewText) + .font(.body) + .textSelection(.enabled) + .frame(maxWidth: .infinity, minHeight: 72, alignment: .topLeading) + .padding(10) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 8)) + + HStack(spacing: 10) { + Button("Stop", systemImage: "stop.circle") { + Task { + await store.stopDictation() + } + } + .buttonStyle(.borderedProminent) + .disabled(store.snapshot?.dictationState != .listening) + + Button("Paste", systemImage: "text.badge.checkmark") { + Task { + await store.pasteFinalOutput() + } + } + .disabled(store.snapshot?.hasFinalOutput != true) + } + } + .padding(16) + .frame(width: 380) + } + + private var previewText: String { + if let finalOutput = store.snapshot?.finalOutput { + return finalOutput + } + if let rawTranscript = store.snapshot?.rawTranscript { + return rawTranscript + } + if let error = store.snapshot?.lastError { + return error + } + return store.snapshot?.focusedAppLabel ?? "No focused app context" + } + + private var statusColor: Color { + switch store.snapshot?.dictationState { + case .listening: + return .red + case .finalizing, .rewriting: + return .orange + case .done: + return .green + default: + return .secondary + } + } +} diff --git a/packages/voxit-host-ffi/Cargo.toml b/packages/voxit-host-ffi/Cargo.toml index 9058749..3f140d4 100644 --- a/packages/voxit-host-ffi/Cargo.toml +++ b/packages/voxit-host-ffi/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["rlib", "staticlib"] path = "src/lib.rs" [dependencies] +voxit-audio = { path = "../voxit-audio" } voxit-core = { path = "../voxit-core" } [target.'cfg(target_os = "macos")'.dependencies] diff --git a/packages/voxit-host-ffi/include/voxit_host_ffi.h b/packages/voxit-host-ffi/include/voxit_host_ffi.h index 9ba5fdd..77e8af3 100644 --- a/packages/voxit-host-ffi/include/voxit_host_ffi.h +++ b/packages/voxit-host-ffi/include/voxit_host_ffi.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define VOXIT_HOST_FFI_ABI_VERSION 3u +#define VOXIT_HOST_FFI_ABI_VERSION 4u typedef struct VoxitHostSessionHandle VoxitHostSessionHandle; @@ -82,6 +82,10 @@ typedef enum VoxitHostStringField { VOXIT_HOST_STRING_FOCUSED_URL_DOMAIN = 3, VOXIT_HOST_STRING_FOCUSED_ELEMENT_ROLE = 4, VOXIT_HOST_STRING_PROMPT_PROFILE_ID = 5, + VOXIT_HOST_STRING_PROMPT_DIRECTIVE = 6, + VOXIT_HOST_STRING_RAW_TRANSCRIPT = 7, + VOXIT_HOST_STRING_FINAL_OUTPUT = 8, + VOXIT_HOST_STRING_LAST_ERROR = 9, } VoxitHostStringField; typedef struct VoxitHostConfig { @@ -99,6 +103,10 @@ typedef struct VoxitHostSnapshot { uint8_t rewrite_enabled; uint8_t has_focused_context; uint8_t selected_text_present; + uint8_t has_raw_transcript; + uint8_t has_final_output; + uint8_t has_error; + uint64_t recording_duration_ms; enum VoxitPromptProfileKind prompt_profile_kind; enum VoxitVoiceInteractionTier voice_tier; enum VoxitVoiceReasoningEffort reasoning_effort; @@ -109,6 +117,9 @@ uint32_t voxit_host_ffi_abi_version(void); VoxitHostSessionHandle *voxit_host_session_create(struct VoxitHostConfig config); void voxit_host_session_destroy(VoxitHostSessionHandle *handle); enum VoxitStatus voxit_host_session_refresh_focused_context(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_start_dictation(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_stop_dictation(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_paste_final_output(VoxitHostSessionHandle *handle); enum VoxitStatus voxit_host_session_copy_snapshot( VoxitHostSessionHandle *handle, struct VoxitHostSnapshot *out diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index e2f1010..da3819e 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -8,21 +8,31 @@ use std::{ffi::c_char, ptr::NonNull}; use voxit_core::{ Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, - VoiceSessionPlan, + RewriteSettings, VoiceSessionPlan, contextual::{ PromptProfileKind, VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, }, + rewrite_only_with_plan, transcribe_only, ui_model::{AuthMethod, AuthSurfaceState, DictationSurfaceState, HotkeySurfaceMode}, }; /// ABI version exported by the thin C host bridge. -pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 3; +pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 4; /// Opaque session handle owned by the native host through the C ABI. pub struct VoxitHostSessionHandle { + config: Config, snapshot: NativeHostSnapshot, focused_context: FocusedAppContext, voice_plan: VoiceSessionPlan, + last_raw_transcript: String, + last_final_output: String, + last_error: String, + recording_duration_ms: u64, + #[cfg(target_os = "macos")] + recorder: Option, + #[cfg(target_os = "macos")] + target_app: Option, } /// Result code returned by FFI entry points. @@ -169,6 +179,14 @@ pub enum VoxitHostStringField { FocusedElementRole = 4, /// Selected prompt profile id. PromptProfileId = 5, + /// Selected prompt directive. + PromptDirective = 6, + /// Latest raw Pass2 transcript. + RawTranscript = 7, + /// Latest final output after rewrite or fallback. + FinalOutput = 8, + /// Latest user-actionable error. + LastError = 9, } /// FFI-safe session configuration. @@ -203,6 +221,14 @@ pub struct VoxitHostSnapshot { pub has_focused_context: u8, /// Non-zero when selected text was present at context capture time. pub selected_text_present: u8, + /// Non-zero when a raw Pass2 transcript is available. + pub has_raw_transcript: u8, + /// Non-zero when a final output is available. + pub has_final_output: u8, + /// Non-zero when the last command failed or produced a warning. + pub has_error: u8, + /// Last recording duration reported by audio capture. + pub recording_duration_ms: u64, /// Selected prompt profile kind. pub prompt_profile_kind: VoxitPromptProfileKind, /// Selected voice interaction tier. @@ -225,6 +251,10 @@ impl Default for VoxitHostSnapshot { rewrite_enabled: 0, has_focused_context: 0, selected_text_present: 0, + has_raw_transcript: 0, + has_final_output: 0, + has_error: 0, + recording_duration_ms: 0, prompt_profile_kind: VoxitPromptProfileKind::FastDictation, voice_tier: VoxitVoiceInteractionTier::FastDictation, reasoning_effort: VoxitVoiceReasoningEffort::Minimal, @@ -248,12 +278,25 @@ pub extern "C" fn voxit_host_session_create( VoxitPlatformTag::MacOS => PlatformHost::MacOS, VoxitPlatformTag::Unsupported => PlatformHost::Unsupported, }; - let app_config = Config::load().unwrap_or_else(|_| Config::default()); - let snapshot = NativeHostSnapshot::initial(platform, &app_config); + let config = Config::load().unwrap_or_else(|_| Config::default()); + let snapshot = NativeHostSnapshot::initial(platform, &config); let focused_context = FocusedAppContext::new(); let voice_plan = ContextualVoiceRouter.plan_for_context(&focused_context); - Box::into_raw(Box::new(VoxitHostSessionHandle { snapshot, focused_context, voice_plan })) + Box::into_raw(Box::new(VoxitHostSessionHandle { + config, + snapshot, + focused_context, + voice_plan, + last_raw_transcript: String::new(), + last_final_output: String::new(), + last_error: String::new(), + recording_duration_ms: 0, + #[cfg(target_os = "macos")] + recorder: None, + #[cfg(target_os = "macos")] + target_app: None, + })) } /// Destroys a Rust-owned native-host session. @@ -286,12 +329,18 @@ pub unsafe extern "C" fn voxit_host_session_copy_snapshot( let Some(out) = NonNull::new(out) else { return VoxitStatus::NullOutput; }; - let snapshot = unsafe { &handle.as_ref().snapshot }; - let focused_context = unsafe { &handle.as_ref().focused_context }; - let voice_plan = unsafe { &handle.as_ref().voice_plan }; + let handle_ref = unsafe { handle.as_ref() }; + let snapshot = &handle_ref.snapshot; + let focused_context = &handle_ref.focused_context; + let voice_plan = &handle_ref.voice_plan; unsafe { - out.as_ptr().write(encode_snapshot_with_context(snapshot, focused_context, voice_plan)) + out.as_ptr().write(encode_snapshot_with_context( + handle_ref, + snapshot, + focused_context, + voice_plan, + )) }; VoxitStatus::Ok @@ -311,12 +360,63 @@ pub unsafe extern "C" fn voxit_host_session_refresh_focused_context( }; let handle = unsafe { handle.as_mut() }; - handle.focused_context = capture_focused_context(); + refresh_focused_context(handle); handle.voice_plan = ContextualVoiceRouter.plan_for_context(&handle.focused_context); VoxitStatus::Ok } +/// Starts a native dictation capture session. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_start_dictation( + handle: *mut VoxitHostSessionHandle, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + start_dictation(handle) +} + +/// Stops capture, finalizes transcription, optionally rewrites, and applies output policy. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_stop_dictation( + handle: *mut VoxitHostSessionHandle, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + stop_dictation(handle) +} + +/// Pastes the latest final output into the captured target app. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_paste_final_output( + handle: *mut VoxitHostSessionHandle, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + paste_final_output(handle) +} + /// Copies a Rust-owned string field into caller-owned memory. /// /// # Safety @@ -345,6 +445,174 @@ pub unsafe extern "C" fn voxit_host_session_copy_string( write_c_string(out, out_len, value) } +fn start_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { + clear_run_output(handle); + refresh_focused_context(handle); + handle.voice_plan = ContextualVoiceRouter.plan_for_context(&handle.focused_context); + + #[cfg(target_os = "macos")] + { + if handle.recorder.is_some() { + set_error(handle, "recording is already active"); + + return VoxitStatus::Ok; + } + + let preferred_device_id = (handle.config.audio.input_device_id != 0) + .then_some(handle.config.audio.input_device_id); + match voxit_audio::start_recording_with_stream(64, preferred_device_id) { + Ok((recorder, _chunk_rx, selection)) => { + handle.recorder = Some(recorder); + handle.snapshot.dictation_state = DictationSurfaceState::Listening; + handle.recording_duration_ms = 0; + if selection.fallback_to_default { + handle.last_error = format!( + "requested microphone unavailable; using {}", + selection.selected_device_name + ); + } + }, + Err(err) => { + handle.snapshot.dictation_state = DictationSurfaceState::Idle; + set_error(handle, format!("failed to start recording: {err}")); + }, + } + + return VoxitStatus::Ok; + } + + #[cfg(not(target_os = "macos"))] + { + handle.snapshot.dictation_state = DictationSurfaceState::Idle; + set_error(handle, "recording is only supported on macOS in this build"); + + VoxitStatus::Ok + } +} + +fn stop_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { + #[cfg(target_os = "macos")] + { + let Some(recorder) = handle.recorder.take() else { + set_error(handle, "recording is not active"); + + return VoxitStatus::Ok; + }; + + handle.snapshot.dictation_state = DictationSurfaceState::Finalizing; + let recording = match voxit_audio::stop_recording(recorder) { + Ok(recording) => recording, + Err(err) => { + handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, format!("failed to stop recording: {err}")); + + return VoxitStatus::Ok; + }, + }; + handle.recording_duration_ms = recording.duration_ms; + + let (raw_transcript, _) = + match transcribe_only(&recording.wav, &handle.config.openai.finalize_model) { + Ok(result) => result, + Err(err) => { + handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, format!("transcription failed: {err}")); + + return VoxitStatus::Ok; + }, + }; + handle.last_raw_transcript = raw_transcript; + handle.last_final_output = handle.last_raw_transcript.clone(); + + if handle.config.rewrite.enabled && handle.config.rewrite.auto { + handle.snapshot.dictation_state = DictationSurfaceState::Rewriting; + let settings = rewrite_settings(&handle.config); + match rewrite_only_with_plan( + &handle.last_raw_transcript, + &handle.config.openai.rewrite_model, + &handle.voice_plan, + &settings, + ) { + Ok((result, _)) => { + if let Some(rewritten) = result.rewritten_transcript { + handle.last_final_output = rewritten; + } + if let Some(reason) = result.reason { + handle.last_error = reason; + } + }, + Err(err) => { + handle.last_error = format!("rewrite failed: {err}"); + }, + } + } + + handle.snapshot.dictation_state = DictationSurfaceState::Done; + if matches!(handle.voice_plan.output_policy, VoiceOutputPolicy::InsertText) { + let _ = paste_final_output(handle); + } + + return VoxitStatus::Ok; + } + + #[cfg(not(target_os = "macos"))] + { + handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, "recording is only supported on macOS in this build"); + + VoxitStatus::Ok + } +} + +fn paste_final_output(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { + #[cfg(target_os = "macos")] + { + if handle.last_final_output.is_empty() { + set_error(handle, "no final output is available to paste"); + + return VoxitStatus::Ok; + } + let target = + if handle.config.paste.lock_frontmost_app { handle.target_app.as_ref() } else { None }; + + if let Err(err) = voxit_macos::paste_text( + target, + &handle.last_final_output, + handle.config.paste.lock_frontmost_app, + ) { + set_error(handle, format!("paste failed: {err}")); + } + + return VoxitStatus::Ok; + } + + #[cfg(not(target_os = "macos"))] + { + set_error(handle, "paste is only supported on macOS in this build"); + + VoxitStatus::Ok + } +} + +fn clear_run_output(handle: &mut VoxitHostSessionHandle) { + handle.last_raw_transcript.clear(); + handle.last_final_output.clear(); + handle.last_error.clear(); + handle.recording_duration_ms = 0; +} + +fn set_error(handle: &mut VoxitHostSessionHandle, message: impl Into) { + handle.last_error = message.into(); +} + +fn rewrite_settings(config: &Config) -> RewriteSettings { + RewriteSettings { + guard_protected_tokens: config.rewrite.guard_numbers, + max_output_chars: config.rewrite.max_output_chars, + style: config.rewrite.style.clone(), + } +} + fn encode_snapshot( snapshot: &NativeHostSnapshot, voice_plan: &VoiceSessionPlan, @@ -360,6 +628,10 @@ fn encode_snapshot( rewrite_enabled: u8::from(snapshot.rewrite_enabled), has_focused_context: 0, selected_text_present: 0, + has_raw_transcript: 0, + has_final_output: 0, + has_error: 0, + recording_duration_ms: 0, prompt_profile_kind: encode_prompt_profile_kind(voice_plan.profile_kind), voice_tier: encode_voice_tier(voice_plan.tier), reasoning_effort: encode_reasoning_effort(voice_plan.reasoning_effort), @@ -368,6 +640,7 @@ fn encode_snapshot( } fn encode_snapshot_with_context( + handle: &VoxitHostSessionHandle, snapshot: &NativeHostSnapshot, focused_context: &FocusedAppContext, voice_plan: &VoiceSessionPlan, @@ -376,6 +649,10 @@ fn encode_snapshot_with_context( encoded.has_focused_context = u8::from(!focused_context.is_empty()); encoded.selected_text_present = u8::from(focused_context.selected_text_present); + encoded.has_raw_transcript = u8::from(!handle.last_raw_transcript.is_empty()); + encoded.has_final_output = u8::from(!handle.last_final_output.is_empty()); + encoded.has_error = u8::from(!handle.last_error.is_empty()); + encoded.recording_duration_ms = handle.recording_duration_ms; encoded } @@ -468,6 +745,10 @@ fn string_field_value(handle: &VoxitHostSessionHandle, field: VoxitHostStringFie VoxitHostStringField::FocusedElementRole => handle.focused_context.focused_element_role.as_deref().unwrap_or_default(), VoxitHostStringField::PromptProfileId => &handle.voice_plan.profile_id, + VoxitHostStringField::PromptDirective => &handle.voice_plan.prompt_directive, + VoxitHostStringField::RawTranscript => &handle.last_raw_transcript, + VoxitHostStringField::FinalOutput => &handle.last_final_output, + VoxitHostStringField::LastError => &handle.last_error, } } @@ -483,13 +764,20 @@ fn write_c_string(out: NonNull, out_len: usize, value: &str) -> VoxitSta VoxitStatus::Ok } -fn capture_focused_context() -> FocusedAppContext { +fn refresh_focused_context(handle: &mut VoxitHostSessionHandle) { #[cfg(target_os = "macos")] if let Some(target) = voxit_macos::capture_frontmost_app() { - return focused_context_from_target(target); + handle.focused_context = focused_context_from_target(target.clone()); + handle.target_app = Some(target); + + return; } - FocusedAppContext::new() + handle.focused_context = FocusedAppContext::new(); + #[cfg(target_os = "macos")] + { + handle.target_app = None; + } } #[cfg(target_os = "macos")] diff --git a/packages/voxit-macos/src/lib.rs b/packages/voxit-macos/src/lib.rs index a403525..9acca2c 100644 --- a/packages/voxit-macos/src/lib.rs +++ b/packages/voxit-macos/src/lib.rs @@ -1,9 +1,12 @@ //! macOS target app capture and activation helpers. #[cfg(not(target_os = "macos"))] use std::io::{self, Error, ErrorKind}; -#[cfg(target_os = "macos")] use std::process::Command; #[cfg(target_os = "macos")] use std::ptr; use std::{ffi::c_void, mem, thread, time::Duration}; +#[cfg(target_os = "macos")] use std::{ + io::Write as _, + process::{Command, Stdio}, +}; #[cfg(target_os = "macos")] use block2::RcBlock; #[cfg(target_os = "macos")] @@ -313,6 +316,30 @@ pub fn activate_target(_target: &TargetApp, _attempts: u32, _base_delay: Duratio false } +/// Copy text to the clipboard and dispatch a paste gesture into the selected target. +#[cfg(target_os = "macos")] +pub fn paste_text(target: Option<&TargetApp>, text: &str, lock_target: bool) -> Result<(), String> { + if text.is_empty() { + return Err("nothing to paste".to_string()); + } + if lock_target && let Some(target) = target { + let _ = activate_target(target, 3, Duration::from_millis(80)); + } + + copy_to_clipboard(text)?; + dispatch_command_v() +} + +/// Paste helper for non-macOS builds. +#[cfg(not(target_os = "macos"))] +pub fn paste_text( + _target: Option<&TargetApp>, + _text: &str, + _lock_target: bool, +) -> Result<(), String> { + Err("paste is only supported on macOS in this build".to_string()) +} + #[cfg(target_os = "macos")] fn capture_frontmost_app_impl() -> Result { let script = r#"tell application "System Events" @@ -403,6 +430,30 @@ fn execute_applescript_raw(script: &str) -> Result { Ok(stdout.trim().to_string()) } +#[cfg(target_os = "macos")] +fn copy_to_clipboard(text: &str) -> Result<(), String> { + let mut child = Command::new("pbcopy") + .stdin(Stdio::piped()) + .spawn() + .map_err(|err| format!("spawn pbcopy failed: {err}"))?; + let Some(stdin) = child.stdin.as_mut() else { + return Err("pbcopy stdin unavailable".to_string()); + }; + + stdin.write_all(text.as_bytes()).map_err(|err| format!("write pbcopy failed: {err}"))?; + let status = child.wait().map_err(|err| format!("wait pbcopy failed: {err}"))?; + + if status.success() { Ok(()) } else { Err(format!("pbcopy failed with status {status}")) } +} + +#[cfg(target_os = "macos")] +fn dispatch_command_v() -> Result<(), String> { + execute_applescript_raw( + r#"tell application "System Events" to keystroke "v" using command down"#, + ) + .map(|_| ()) +} + #[cfg(target_os = "macos")] fn capture_url_domain(bundle_id: Option<&str>, app_name: Option<&str>) -> Option { let script = browser_url_script(bundle_id, app_name)?; From 551cc7780fa2c5a183c9765d0ab034773784ae79 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:55:14 +0800 Subject: [PATCH 12/27] {"schema":"maestro/commit/1","summary":"Persist settings through Rust host config","authority":"manual"} --- .../Sources/VoxitHostBridge/HostFFI.swift | 32 ++++++++ .../App/VoxitNativeHostApp.swift | 11 +++ .../VoxitNativeHostKit/Stores/HostStore.swift | 16 ++++ .../Stores/VoxitSettingsStore.swift | 16 ++++ .../voxit-host-ffi/include/voxit_host_ffi.h | 12 +++ packages/voxit-host-ffi/src/lib.rs | 81 ++++++++++++++++++- 6 files changed, 167 insertions(+), 1 deletion(-) diff --git a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift index 8f2ed49..867f826 100644 --- a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift +++ b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift @@ -254,6 +254,29 @@ public final class VoxitHostSession { return try currentSnapshot() } + public func savePreferences( + hotkeyChord: String, + hotkeyMode: HotkeyMode, + startHidden: Bool, + pasteAfterTranscription: Bool, + rewriteAfterTranscription: Bool + ) throws -> HostSnapshot { + let preferences = VoxitHostPreferences( + start_hidden: startHidden ? 1 : 0, + hotkey_mode: encode(hotkeyMode: hotkeyMode), + paste_after_transcription: pasteAfterTranscription ? 1 : 0, + rewrite_after_transcription: rewriteAfterTranscription ? 1 : 0 + ) + try hotkeyChord.withCString { chord in + try requireOk( + voxit_host_session_save_preferences(handle, preferences, chord), + context: "saving host preferences" + ) + } + + return try currentSnapshot() + } + private func requireOk(_ status: VoxitStatus, context: String) throws { let code = voxit_status_code(status) if code != 0 { @@ -372,6 +395,15 @@ public final class VoxitHostSession { } } + private func encode(hotkeyMode: HotkeyMode) -> VoxitHotkeyMode { + switch hotkeyMode { + case .toggle: + return VOXIT_HOTKEY_MODE_TOGGLE + case .hold: + return VOXIT_HOTKEY_MODE_HOLD + } + } + private func decode(promptProfileKind: VoxitPromptProfileKind) throws -> PromptProfileKind { switch promptProfileKind.rawValue { case VOXIT_PROMPT_PROFILE_FAST_DICTATION.rawValue: diff --git a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift index f97a2df..bcb1d69 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift @@ -15,7 +15,9 @@ public struct VoxitNativeHostApp: App { .frame(minWidth: 720, minHeight: 460) .task { VoxitArtwork.applyApplicationIcon() + configureSettingsSync() await store.reload() + await store.savePreferences(settingsStore.settings) } } .commands { @@ -112,6 +114,15 @@ public struct VoxitNativeHostApp: App { } } + @MainActor + private func configureSettingsSync() { + settingsStore.setSyncHandler { settings in + Task { + await store.savePreferences(settings) + } + } + } + @MainActor private func startDictation() { openWindow(id: "recording-hud") diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift index 584572d..13b921c 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift @@ -60,6 +60,22 @@ public final class HostStore: ObservableObject { } } + func savePreferences(_ settings: VoxitSettings) async { + do { + let session = try currentSession() + snapshot = try session.savePreferences( + hotkeyChord: settings.dictationHotkey, + hotkeyMode: settings.hotkeyMode.hostBridgeValue, + startHidden: settings.startHidden, + pasteAfterTranscription: settings.pasteAfterTranscription, + rewriteAfterTranscription: settings.rewriteAfterTranscription + ) + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + private func currentSession() throws -> VoxitHostSession { if let session { return session diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift index cefd978..ac31705 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift @@ -1,5 +1,6 @@ import AppKit import Foundation +import VoxitHostBridge @MainActor final class VoxitSettingsStore: ObservableObject { @@ -18,6 +19,7 @@ final class VoxitSettingsStore: ObservableObject { } private let defaults: UserDefaults + private var syncHandler: ((VoxitSettings) -> Void)? init(defaults: UserDefaults = .standard) { self.defaults = defaults @@ -53,9 +55,14 @@ final class VoxitSettingsStore: ObservableObject { let sanitized = next.sanitized() settings = sanitized Self.persist(sanitized, into: defaults) + syncHandler?(sanitized) NotificationCenter.default.post(name: Self.didChangeNotification, object: self) } + func setSyncHandler(_ syncHandler: @escaping (VoxitSettings) -> Void) { + self.syncHandler = syncHandler + } + private static func persist(_ settings: VoxitSettings, into defaults: UserDefaults) { defaults.set(settings.dictationHotkey, forKey: DefaultsKey.dictationHotkey) defaults.set(settings.hotkeyMode.rawValue, forKey: DefaultsKey.hotkeyMode) @@ -207,6 +214,15 @@ enum VoxitHotkeyModePreference: String, CaseIterable, Identifiable { return "Hold" } } + + var hostBridgeValue: HotkeyMode { + switch self { + case .toggle: + return .toggle + case .hold: + return .hold + } + } } enum VoxitAuthRoutePreference: String, CaseIterable, Identifiable { diff --git a/packages/voxit-host-ffi/include/voxit_host_ffi.h b/packages/voxit-host-ffi/include/voxit_host_ffi.h index 77e8af3..053494c 100644 --- a/packages/voxit-host-ffi/include/voxit_host_ffi.h +++ b/packages/voxit-host-ffi/include/voxit_host_ffi.h @@ -92,6 +92,13 @@ typedef struct VoxitHostConfig { enum VoxitPlatformTag platform; } VoxitHostConfig; +typedef struct VoxitHostPreferences { + uint8_t start_hidden; + enum VoxitHotkeyMode hotkey_mode; + uint8_t paste_after_transcription; + uint8_t rewrite_after_transcription; +} VoxitHostPreferences; + typedef struct VoxitHostSnapshot { enum VoxitPlatformTag platform; enum VoxitAuthMethod auth_method; @@ -120,6 +127,11 @@ enum VoxitStatus voxit_host_session_refresh_focused_context(VoxitHostSessionHand enum VoxitStatus voxit_host_session_start_dictation(VoxitHostSessionHandle *handle); enum VoxitStatus voxit_host_session_stop_dictation(VoxitHostSessionHandle *handle); enum VoxitStatus voxit_host_session_paste_final_output(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_save_preferences( + VoxitHostSessionHandle *handle, + struct VoxitHostPreferences preferences, + const char *hotkey_chord +); enum VoxitStatus voxit_host_session_copy_snapshot( VoxitHostSessionHandle *handle, struct VoxitHostSnapshot *out diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index da3819e..91685d7 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -4,7 +4,10 @@ //! This gives the Swift host a stable Rust-owned model without moving audio, auth, or //! inference orchestration across FFI before those boundaries are ready. -use std::{ffi::c_char, ptr::NonNull}; +use std::{ + ffi::{CStr, c_char}, + ptr::NonNull, +}; use voxit_core::{ Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, @@ -197,6 +200,20 @@ pub struct VoxitHostConfig { pub platform: VoxitPlatformTag, } +/// FFI-safe user preference payload written through Rust config. +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VoxitHostPreferences { + /// Non-zero when the app should start hidden/menu-bar first. + pub start_hidden: u8, + /// Hotkey mode. + pub hotkey_mode: VoxitHotkeyMode, + /// Non-zero when final output should paste automatically when policy allows it. + pub paste_after_transcription: u8, + /// Non-zero when pass-3 rewrite is enabled. + pub rewrite_after_transcription: u8, +} + /// FFI-safe native-host snapshot. #[repr(C)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -417,6 +434,35 @@ pub unsafe extern "C" fn voxit_host_session_paste_final_output( paste_final_output(handle) } +/// Saves host preferences through the Rust-owned config file. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +/// `hotkey_chord` must point to a null-terminated UTF-8 string. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_save_preferences( + handle: *mut VoxitHostSessionHandle, + preferences: VoxitHostPreferences, + hotkey_chord: *const c_char, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let Some(hotkey_chord) = NonNull::new(hotkey_chord.cast_mut()) else { + return VoxitStatus::InvalidInput; + }; + let handle = unsafe { handle.as_mut() }; + let hotkey_chord = unsafe { CStr::from_ptr(hotkey_chord.as_ptr()) }; + let Ok(hotkey_chord) = hotkey_chord.to_str() else { + set_error(handle, "hotkey chord is not valid UTF-8"); + + return VoxitStatus::Ok; + }; + + save_preferences(handle, preferences, hotkey_chord) +} + /// Copies a Rust-owned string field into caller-owned memory. /// /// # Safety @@ -594,6 +640,39 @@ fn paste_final_output(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { } } +fn save_preferences( + handle: &mut VoxitHostSessionHandle, + preferences: VoxitHostPreferences, + hotkey_chord: &str, +) -> VoxitStatus { + handle.config.ui.start_hidden = preferences.start_hidden != 0; + handle.config.hotkey.chord = hotkey_chord.to_ascii_lowercase().replace('-', "+"); + handle.config.hotkey.mode = match preferences.hotkey_mode { + VoxitHotkeyMode::Hold => "hold".to_string(), + VoxitHotkeyMode::Toggle => "toggle".to_string(), + }; + handle.config.rewrite.enabled = preferences.rewrite_after_transcription != 0; + handle.config.rewrite.auto = preferences.rewrite_after_transcription != 0; + handle.config.paste.method = if preferences.paste_after_transcription != 0 { + "clipboard_cmd_v".to_string() + } else { + "manual".to_string() + }; + handle.snapshot.hotkey_mode = match preferences.hotkey_mode { + VoxitHotkeyMode::Hold => HotkeySurfaceMode::Hold, + VoxitHotkeyMode::Toggle => HotkeySurfaceMode::Toggle, + }; + handle.snapshot.rewrite_enabled = handle.config.rewrite.enabled; + + if let Err(err) = handle.config.save() { + set_error(handle, format!("failed to save config: {err}")); + } else { + handle.last_error.clear(); + } + + VoxitStatus::Ok +} + fn clear_run_output(handle: &mut VoxitHostSessionHandle) { handle.last_raw_transcript.clear(); handle.last_final_output.clear(); From 11961a5eb99dd1d07e4c45e863c8240c42966edb Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 09:59:00 +0800 Subject: [PATCH 13/27] {"schema":"maestro/commit/1","summary":"Add contextual profile and glossary controls","authority":"manual"} --- .../Sources/VoxitHostBridge/HostFFI.swift | 41 ++++++ .../App/VoxitNativeHostApp.swift | 4 + .../VoxitNativeHostKit/Stores/HostStore.swift | 24 ++++ .../Views/ContentView.swift | 10 ++ .../VoxitNativeHostKit/Views/DetailView.swift | 133 ++++++++++++++++-- packages/voxit-core/src/contextual.rs | 24 ++++ packages/voxit-core/src/inference.rs | 15 +- .../voxit-host-ffi/include/voxit_host_ffi.h | 9 ++ packages/voxit-host-ffi/src/lib.rs | 111 ++++++++++++++- 9 files changed, 354 insertions(+), 17 deletions(-) diff --git a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift index 867f826..0c9ec68 100644 --- a/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift +++ b/native/macos-host/Sources/VoxitHostBridge/HostFFI.swift @@ -277,6 +277,30 @@ public final class VoxitHostSession { return try currentSnapshot() } + public func setProfileOverride(_ profileKind: PromptProfileKind) throws -> HostSnapshot { + try requireOk( + voxit_host_session_set_profile_override(handle, encode(promptProfileKind: profileKind)), + context: "setting profile override" + ) + + return try currentSnapshot() + } + + public func clearProfileOverride() throws -> HostSnapshot { + try requireOk( + voxit_host_session_clear_profile_override(handle), context: "clearing profile override") + + return try currentSnapshot() + } + + public func setGlossary(_ glossaryTerms: String) throws -> HostSnapshot { + try glossaryTerms.withCString { raw in + try requireOk(voxit_host_session_set_glossary(handle, raw), context: "setting glossary") + } + + return try currentSnapshot() + } + private func requireOk(_ status: VoxitStatus, context: String) throws { let code = voxit_status_code(status) if code != 0 { @@ -404,6 +428,23 @@ public final class VoxitHostSession { } } + private func encode(promptProfileKind: PromptProfileKind) -> VoxitPromptProfileKind { + switch promptProfileKind { + case .fastDictation: + return VOXIT_PROMPT_PROFILE_FAST_DICTATION + case .messaging: + return VOXIT_PROMPT_PROFILE_MESSAGING + case .mail: + return VOXIT_PROMPT_PROFILE_MAIL + case .codeEditor: + return VOXIT_PROMPT_PROFILE_CODE_EDITOR + case .terminal: + return VOXIT_PROMPT_PROFILE_TERMINAL + case .workTracker: + return VOXIT_PROMPT_PROFILE_WORK_TRACKER + } + } + private func decode(promptProfileKind: VoxitPromptProfileKind) throws -> PromptProfileKind { switch promptProfileKind.rawValue { case VOXIT_PROMPT_PROFILE_FAST_DICTATION.rawValue: diff --git a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift index bcb1d69..c50795f 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/App/VoxitNativeHostApp.swift @@ -18,6 +18,10 @@ public struct VoxitNativeHostApp: App { configureSettingsSync() await store.reload() await store.savePreferences(settingsStore.settings) + await store.setGlossary(UserDefaults.standard.string(forKey: "glossaryTerms") ?? "") + let profileOverrideRaw = + UserDefaults.standard.string(forKey: "profileOverride") ?? ProfileOverride.auto.rawValue + await store.setProfileOverride(ProfileOverride(rawValue: profileOverrideRaw)?.profileKind) } } .commands { diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift index 13b921c..8bcac9d 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/HostStore.swift @@ -76,6 +76,30 @@ public final class HostStore: ObservableObject { } } + func setProfileOverride(_ profileKind: PromptProfileKind?) async { + do { + let session = try currentSession() + if let profileKind { + snapshot = try session.setProfileOverride(profileKind) + } else { + snapshot = try session.clearProfileOverride() + } + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + + func setGlossary(_ glossaryTerms: String) async { + do { + let session = try currentSession() + snapshot = try session.setGlossary(glossaryTerms) + errorMessage = snapshot?.lastError + } catch { + errorMessage = String(describing: error) + } + } + private func currentSession() throws -> VoxitHostSession { if let session { return session diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift index c4f50fb..f58196f 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/ContentView.swift @@ -36,6 +36,16 @@ public struct ContentView: View { Task { await store.pasteFinalOutput() } + }, + setProfileOverride: { profileKind in + Task { + await store.setProfileOverride(profileKind) + } + }, + setGlossary: { glossaryTerms in + Task { + await store.setGlossary(glossaryTerms) + } } ) } diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift index e1b57cb..750003d 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Views/DetailView.swift @@ -9,6 +9,8 @@ struct DetailView: View { var startDictation: () -> Void var stopDictation: () -> Void var pasteFinalOutput: () -> Void + var setProfileOverride: (PromptProfileKind?) -> Void + var setGlossary: (String) -> Void var body: some View { ScrollView { @@ -32,11 +34,11 @@ struct DetailView: View { case .appRules: AppRulesDetail() case .profiles: - ProfilesDetail(snapshot: snapshot) + ProfilesDetail(snapshot: snapshot, setProfileOverride: setProfileOverride) case .glossary: - GlossaryDetail() + GlossaryDetail(setGlossary: setGlossary) case .promptLab: - PromptLabDetail() + PromptLabDetail(snapshot: snapshot) } } .padding(24) @@ -166,8 +168,17 @@ private struct AppRulesDetail: View { private struct ProfilesDetail: View { var snapshot: HostSnapshot? + var setProfileOverride: (PromptProfileKind?) -> Void + @AppStorage("profileOverride") private var profileOverride = ProfileOverride.auto.rawValue var body: some View { + Picker("Profile", selection: profileOverrideBinding) { + ForEach(ProfileOverride.allCases) { profile in + Text(profile.title).tag(profile.rawValue) + } + } + .pickerStyle(.menu) + LabeledContentGrid { StatusCard( title: "Current", @@ -179,26 +190,132 @@ private struct ProfilesDetail: View { value: snapshot?.reasoningEffort.label ?? "Loading", systemImage: "brain" ) - StatusCard(title: "Context Rewrite", value: "Low", systemImage: "wand.and.stars") - StatusCard(title: "Voice Intent", value: "Medium", systemImage: "arrow.triangle.branch") + StatusCard( + title: "Directive", + value: snapshot?.promptDirective ?? "Profile Default", + systemImage: "wand.and.stars" + ) + StatusCard( + title: "Output", value: snapshot?.outputPolicy.label ?? "Loading", + systemImage: "arrow.triangle.branch") } } + + private var profileOverrideBinding: Binding { + Binding( + get: { profileOverride }, + set: { value in + profileOverride = value + setProfileOverride(ProfileOverride(rawValue: value)?.profileKind) + } + ) + } } private struct GlossaryDetail: View { + var setGlossary: (String) -> Void + @AppStorage("glossaryTerms") private var glossaryTerms = "" + var body: some View { + TextEditor(text: glossaryBinding) + .font(.body) + .frame(minHeight: 140) + .padding(10) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + LabeledContentGrid { - StatusCard(title: "Custom Terms", value: "None", systemImage: "text.book.closed") + StatusCard(title: "Custom Terms", value: glossarySummary, systemImage: "text.book.closed") StatusCard(title: "Entity Guard", value: "Numbers, dates, money", systemImage: "number") } } + + private var glossaryBinding: Binding { + Binding( + get: { glossaryTerms }, + set: { value in + glossaryTerms = value + setGlossary(value) + } + ) + } + + private var glossarySummary: String { + let count = glossaryTerms.split(whereSeparator: \.isNewline).filter { !$0.isEmpty }.count + return count == 0 ? "None" : "\(count) Terms" + } } private struct PromptLabDetail: View { + var snapshot: HostSnapshot? + @AppStorage("promptLabSample") private var sample = "Summarize what I just said for this app." + var body: some View { + TextEditor(text: $sample) + .font(.body) + .frame(minHeight: 96) + .padding(10) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + LabeledContentGrid { - StatusCard(title: "Comparison", value: "No Runs", systemImage: "rectangle.split.2x1") - StatusCard(title: "Reasoning", value: "Profile Default", systemImage: "brain") + StatusCard( + title: "Profile", value: snapshot?.promptProfileKind.label ?? "Loading", + systemImage: "rectangle.split.2x1") + StatusCard( + title: "Reasoning", value: snapshot?.reasoningEffort.label ?? "Profile Default", + systemImage: "brain") + StatusCard( + title: "Directive", value: snapshot?.promptDirective ?? "Profile Default", + systemImage: "text.alignleft") + } + } +} + +enum ProfileOverride: String, CaseIterable, Identifiable { + case auto + case fastDictation + case messaging + case mail + case codeEditor + case terminal + case workTracker + + var id: Self { self } + + var title: String { + switch self { + case .auto: + return "Auto" + case .fastDictation: + return "Fast Dictation" + case .messaging: + return "Messaging" + case .mail: + return "Mail" + case .codeEditor: + return "Code Editor" + case .terminal: + return "Terminal" + case .workTracker: + return "Work Tracker" + } + } + + var profileKind: PromptProfileKind? { + switch self { + case .auto: + return nil + case .fastDictation: + return .fastDictation + case .messaging: + return .messaging + case .mail: + return .mail + case .codeEditor: + return .codeEditor + case .terminal: + return .terminal + case .workTracker: + return .workTracker } } } diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs index e190e7f..3a0d4d9 100644 --- a/packages/voxit-core/src/contextual.rs +++ b/packages/voxit-core/src/contextual.rs @@ -223,6 +223,20 @@ impl VoiceSessionPlan { #[derive(Clone, Debug, Default)] pub struct ContextualVoiceRouter; impl ContextualVoiceRouter { + /// Plan a contextual voice session from an explicit built-in profile. + pub fn plan_for_profile_kind(&self, kind: PromptProfileKind) -> VoiceSessionPlan { + let profile = match kind { + PromptProfileKind::FastDictation => default_dictation_profile(), + PromptProfileKind::Messaging => messaging_profile(), + PromptProfileKind::Mail => mail_profile(), + PromptProfileKind::CodeEditor => code_editor_profile(), + PromptProfileKind::Terminal => terminal_profile(), + PromptProfileKind::WorkTracker => work_tracker_profile(), + }; + + VoiceSessionPlan::from_profile(profile) + } + /// Plan a contextual voice session from focused app context. pub fn plan_for_context(&self, context: &FocusedAppContext) -> VoiceSessionPlan { let profile = if context_matches_any(context, &["com.tinyspeck.slackmacgap", "discord"]) { @@ -445,4 +459,14 @@ mod tests { assert!(instructions.contains("concise")); assert!(instructions.contains("1200")); } + + #[test] + fn explicit_profile_kind_builds_matching_plan() { + let router = ContextualVoiceRouter; + let plan = router.plan_for_profile_kind(PromptProfileKind::Mail); + + assert_eq!(plan.profile_kind, PromptProfileKind::Mail); + assert_eq!(plan.profile_id, "mail"); + assert_eq!(plan.output_policy, VoiceOutputPolicy::PreviewBeforeInsert); + } } diff --git a/packages/voxit-core/src/inference.rs b/packages/voxit-core/src/inference.rs index a38b69e..8ebd8a5 100644 --- a/packages/voxit-core/src/inference.rs +++ b/packages/voxit-core/src/inference.rs @@ -43,10 +43,17 @@ pub struct RewriteSettings { pub max_output_chars: u32, /// Style preset supplied to prompt construction. pub style: String, + /// Optional user glossary terms to preserve or prefer. + pub glossary_terms: String, } impl Default for RewriteSettings { fn default() -> Self { - Self { guard_protected_tokens: true, max_output_chars: 8_000, style: "clean".to_string() } + Self { + guard_protected_tokens: true, + max_output_chars: 8_000, + style: "clean".to_string(), + glossary_terms: String::new(), + } } } @@ -180,7 +187,11 @@ fn rewrite_with_guard( settings: &RewriteSettings, ) -> Result { let provider = default_provider()?; - let instructions = plan.rewrite_instructions(&settings.style, settings.max_output_chars); + let mut instructions = plan.rewrite_instructions(&settings.style, settings.max_output_chars); + if !settings.glossary_terms.trim().is_empty() { + instructions.push_str("\nGlossary terms to preserve or prefer:\n"); + instructions.push_str(settings.glossary_terms.trim()); + } let rewritten = provider.rewrite(RewriteRequest { text, model, instructions: &instructions })?; diff --git a/packages/voxit-host-ffi/include/voxit_host_ffi.h b/packages/voxit-host-ffi/include/voxit_host_ffi.h index 053494c..de2b6b8 100644 --- a/packages/voxit-host-ffi/include/voxit_host_ffi.h +++ b/packages/voxit-host-ffi/include/voxit_host_ffi.h @@ -132,6 +132,15 @@ enum VoxitStatus voxit_host_session_save_preferences( struct VoxitHostPreferences preferences, const char *hotkey_chord ); +enum VoxitStatus voxit_host_session_set_profile_override( + VoxitHostSessionHandle *handle, + enum VoxitPromptProfileKind profile_kind +); +enum VoxitStatus voxit_host_session_clear_profile_override(VoxitHostSessionHandle *handle); +enum VoxitStatus voxit_host_session_set_glossary( + VoxitHostSessionHandle *handle, + const char *glossary_terms +); enum VoxitStatus voxit_host_session_copy_snapshot( VoxitHostSessionHandle *handle, struct VoxitHostSnapshot *out diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index 91685d7..6368e85 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -27,7 +27,9 @@ pub struct VoxitHostSessionHandle { config: Config, snapshot: NativeHostSnapshot, focused_context: FocusedAppContext, + profile_override: Option, voice_plan: VoiceSessionPlan, + glossary_terms: String, last_raw_transcript: String, last_final_output: String, last_error: String, @@ -304,7 +306,9 @@ pub extern "C" fn voxit_host_session_create( config, snapshot, focused_context, + profile_override: None, voice_plan, + glossary_terms: String::new(), last_raw_transcript: String::new(), last_final_output: String::new(), last_error: String::new(), @@ -378,7 +382,7 @@ pub unsafe extern "C" fn voxit_host_session_refresh_focused_context( let handle = unsafe { handle.as_mut() }; refresh_focused_context(handle); - handle.voice_plan = ContextualVoiceRouter.plan_for_context(&handle.focused_context); + update_voice_plan(handle); VoxitStatus::Ok } @@ -463,6 +467,77 @@ pub unsafe extern "C" fn voxit_host_session_save_preferences( save_preferences(handle, preferences, hotkey_chord) } +/// Sets a manual prompt-profile override for the current host session. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_set_profile_override( + handle: *mut VoxitHostSessionHandle, + profile_kind: VoxitPromptProfileKind, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + handle.profile_override = Some(decode_prompt_profile_kind(profile_kind)); + update_voice_plan(handle); + + VoxitStatus::Ok +} + +/// Clears any manual prompt-profile override for the current host session. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_clear_profile_override( + handle: *mut VoxitHostSessionHandle, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let handle = unsafe { handle.as_mut() }; + + handle.profile_override = None; + update_voice_plan(handle); + + VoxitStatus::Ok +} + +/// Sets newline-separated glossary terms for contextual rewrite prompts. +/// +/// # Safety +/// +/// `handle` must be a valid pointer returned by [`voxit_host_session_create`]. +/// `glossary_terms` must point to a null-terminated UTF-8 string. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn voxit_host_session_set_glossary( + handle: *mut VoxitHostSessionHandle, + glossary_terms: *const c_char, +) -> VoxitStatus { + let Some(mut handle) = NonNull::new(handle) else { + return VoxitStatus::NullHandle; + }; + let Some(glossary_terms) = NonNull::new(glossary_terms.cast_mut()) else { + return VoxitStatus::InvalidInput; + }; + let handle = unsafe { handle.as_mut() }; + let glossary_terms = unsafe { CStr::from_ptr(glossary_terms.as_ptr()) }; + let Ok(glossary_terms) = glossary_terms.to_str() else { + set_error(handle, "glossary terms are not valid UTF-8"); + + return VoxitStatus::Ok; + }; + + handle.glossary_terms = glossary_terms.to_string(); + + VoxitStatus::Ok +} + /// Copies a Rust-owned string field into caller-owned memory. /// /// # Safety @@ -494,7 +569,7 @@ pub unsafe extern "C" fn voxit_host_session_copy_string( fn start_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { clear_run_output(handle); refresh_focused_context(handle); - handle.voice_plan = ContextualVoiceRouter.plan_for_context(&handle.focused_context); + update_voice_plan(handle); #[cfg(target_os = "macos")] { @@ -572,7 +647,7 @@ fn stop_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { if handle.config.rewrite.enabled && handle.config.rewrite.auto { handle.snapshot.dictation_state = DictationSurfaceState::Rewriting; - let settings = rewrite_settings(&handle.config); + let settings = rewrite_settings(handle); match rewrite_only_with_plan( &handle.last_raw_transcript, &handle.config.openai.rewrite_model, @@ -684,14 +759,25 @@ fn set_error(handle: &mut VoxitHostSessionHandle, message: impl Into) { handle.last_error = message.into(); } -fn rewrite_settings(config: &Config) -> RewriteSettings { +fn rewrite_settings(handle: &VoxitHostSessionHandle) -> RewriteSettings { RewriteSettings { - guard_protected_tokens: config.rewrite.guard_numbers, - max_output_chars: config.rewrite.max_output_chars, - style: config.rewrite.style.clone(), + guard_protected_tokens: handle.config.rewrite.guard_numbers, + max_output_chars: handle.config.rewrite.max_output_chars, + style: handle.config.rewrite.style.clone(), + glossary_terms: handle.glossary_terms.clone(), } } +fn update_voice_plan(handle: &mut VoxitHostSessionHandle) { + let router = ContextualVoiceRouter; + + handle.voice_plan = if let Some(kind) = handle.profile_override { + router.plan_for_profile_kind(kind) + } else { + router.plan_for_context(&handle.focused_context) + }; +} + fn encode_snapshot( snapshot: &NativeHostSnapshot, voice_plan: &VoiceSessionPlan, @@ -786,6 +872,17 @@ fn encode_prompt_profile_kind(kind: PromptProfileKind) -> VoxitPromptProfileKind } } +fn decode_prompt_profile_kind(kind: VoxitPromptProfileKind) -> PromptProfileKind { + match kind { + VoxitPromptProfileKind::FastDictation => PromptProfileKind::FastDictation, + VoxitPromptProfileKind::Messaging => PromptProfileKind::Messaging, + VoxitPromptProfileKind::Mail => PromptProfileKind::Mail, + VoxitPromptProfileKind::CodeEditor => PromptProfileKind::CodeEditor, + VoxitPromptProfileKind::Terminal => PromptProfileKind::Terminal, + VoxitPromptProfileKind::WorkTracker => PromptProfileKind::WorkTracker, + } +} + fn encode_voice_tier(tier: VoiceInteractionTier) -> VoxitVoiceInteractionTier { match tier { VoiceInteractionTier::FastDictation => VoxitVoiceInteractionTier::FastDictation, From 8d77c546bc361bf1a822c4ae134965d9d3f12db3 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 10:00:15 +0800 Subject: [PATCH 14/27] {"schema":"maestro/commit/1","summary":"Update contextual runtime status","authority":"manual"} --- docs/spec/contextual-voice.md | 12 ++++++-- docs/spec/runtime.md | 58 +++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/docs/spec/contextual-voice.md b/docs/spec/contextual-voice.md index 5dba632..393dbeb 100644 --- a/docs/spec/contextual-voice.md +++ b/docs/spec/contextual-voice.md @@ -112,12 +112,18 @@ Profile contracts include: - stable profile id - display title +- prompt directive used for model-specific prompt construction - interaction tier - default reasoning effort - default output policy -Custom profiles may be user-defined later, but built-in profile routing remains -deterministic and testable. +The native host may set a manual built-in profile override for the current session. +When no override is active, built-in profile routing remains deterministic and +testable from focused app context. + +User glossary terms are not routing rules. They are rewrite prompt inputs that help +preserve preferred spellings, names, and domain terms after a session plan has already +selected the profile. ## 5) Reasoning Effort @@ -154,6 +160,8 @@ Rust Core owns: - voice session planning - reasoning effort and output policy selection - provider orchestration and model-specific prompt construction +- applying manual built-in profile overrides exposed by host UI +- applying glossary terms to rewrite prompt construction Swift hosts own: diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 71a239c..5a6f907 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -35,7 +35,8 @@ Defines: ## 2) State Machine -The runtime state is user-visible in `self.state` and UI status labels: +The runtime state is user-visible through the Rust-owned native-host snapshot rendered +by Swift: - `Ready to listen.` - `Listening` @@ -46,16 +47,19 @@ The runtime state is user-visible in `self.state` and UI status labels: State transitions: -- `Start Dictation` or hotkey start in **toggle** mode -> `Listening`. +- `Start Dictation` or menu shortcut start in **toggle** mode -> capture focused + context, start recording, and enter `Listening`. - `Stop Dictation` or hotkey release in **hold** mode -> stop capture, encode WAV, then `FinalizingPass2`. - Pass2 completion: - if auto rewrite is enabled -> `RewritingPass3` - - else -> paste raw final transcript and `Done` + - else -> set final output to raw transcript and `Done` - Pass3 completion: - - if guard passes -> paste rewritten result and `Done` - - if skipped or rejected -> paste raw result and `Done` -- `Paste raw now (skip rewrite)` during Pass2 or Pass3 forces raw paste and sets `Done`. + - if guard passes -> set final output to rewritten result and `Done` + - if skipped or rejected -> set final output to raw transcript and `Done` +- If the active output policy is `insert_text`, the runtime pastes final output into the + captured target automatically. Preview and confirmation policies leave output in the + HUD for explicit paste. ## 3) Authentication Contract @@ -140,6 +144,8 @@ State transitions: - keep meaning - preserve numeric, date, and currency tokens - reject rewrite when the protected token multiset changes + - enforce `rewrite.max_output_chars` + - apply `rewrite.style` and any user glossary terms to prompt construction - Guarded outcomes: - `Applied`: paste rewritten text - `Rejected`: fallback to raw Pass2 and paste raw @@ -149,6 +155,8 @@ State transitions: - Before starting recording, capture frontmost app metadata (pid, bundle id, name) if `lock_frontmost_app = true`. +- Focus context capture also records available window title, URL domain, focused element + role, and selected-text presence for Rust-owned prompt routing. - On paste: - attempt to reactivate captured target app with retries - copy to clipboard @@ -159,13 +167,13 @@ State transitions: - Hotkey chord handling: - supported mode switch: toggle or hold - - currently recognized physical combo: `Ctrl+Shift+Space` - - configuration exposes `hotkey.chord` for future use + - the menu command uses the configured `hotkey.chord` presentation + - system-wide hotkey capture is not active yet - Menu bar behavior: - `MenuBarExtra` exposes `Open Voxit` (`Cmd+O`), `Settings...` (`Cmd+,`), - `Refresh Status` (`Cmd+R`), and `Quit Voxit` (`Cmd+Q`). - - `Start Dictation` displays the configured dictation shortcut presentation, but - remains disabled until the Swift menu action is wired to the Rust runtime command. + `Start Dictation`, `Stop Dictation`, `Refresh Status` (`Cmd+R`), and `Quit Voxit` + (`Cmd+Q`). + - `Start Dictation` and `Stop Dictation` call the Rust host FFI command surface. - `Settings...` opens a dedicated AppKit-hosted Settings window. - The Settings window handles `Cmd+W` to close and `Cmd+Q` to terminate. @@ -188,6 +196,9 @@ State transitions: re-check in Voxit before continuing. - "Paste raw now" is always available when finalization or rewrite is active and should bypass Pass3. +- The Control Center exposes the current focused context, selected profile, profile + override, glossary terms, and prompt lab sample state. Profile override and glossary + terms are passed back through Rust FFI before model calls. - The Swift native host must render platform-neutral Rust model snapshots from `packages/voxit-core/` through `packages/voxit-host-ffi/` instead of defining a separate UI state machine, contextual routing policy, or prompt profile registry. @@ -222,7 +233,7 @@ On load: Current Swift Settings window: - persists shell preferences in macOS `UserDefaults` -- does not yet write those preferences through the Rust `config.toml` path +- writes supported preferences through the Rust host FFI into `config.toml` ## 11) CI and Release @@ -242,20 +253,13 @@ Current Swift Settings window: ## 13) Known Gaps -- Contextual routing is defined in Rust Core and the current `VoiceSessionPlan` is - exposed through the host snapshot, but focused-app context capture and non-default - profile selection are not wired yet. -- The Voxit control-center window currently exposes the target Activity, App Rules, - Profiles, Glossary, and Prompt Lab navigation shape, but profile editing, app-rule - editing, glossary persistence, and prompt lab comparisons are not implemented yet. -- The recording HUD is a target surface in the UI contract and is not implemented yet. -- Swift Settings write-through to `config.toml` is not implemented yet. -- Menu-driven start/stop dictation is visible but not wired to the Rust runtime command - yet. -- Configured hotkey chord string is not yet mapped; current hardcoded gesture is - `Ctrl+Shift+Space` only. +- System-wide global hotkey capture is not implemented yet; the configured shortcut is + currently a Swift menu command. +- The native HUD does not yet render Pass1 realtime draft/committed transcript events; + it shows active profile/state plus raw and final output after Pass2/Pass3. +- App-rule authoring is not implemented yet; users can refresh focus context and + manually override the active built-in profile. +- The Swift Settings audio picker still exposes only System Default even though Rust can + resolve configured CoreAudio input device ids. - CPAL fallback capture is not implemented despite a configuration option; only the VoiceProcessingIO path is active. -- `rewrite.max_output_chars` and `rewrite.style` are persisted but not strictly - enforced in the rewrite prompt yet. -- No explicit audio resampling step to 24 kHz is implemented in the current path. From 58e74de77eda00ec2e6d64b0524a7bb08288e1ea Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 10:06:53 +0800 Subject: [PATCH 15/27] {"schema":"maestro/commit/1","summary":"Satisfy repository formatting gates","authority":"manual"} --- packages/voxit-core/src/contextual.rs | 2 +- packages/voxit-core/src/inference.rs | 3 ++ packages/voxit-host-ffi/Cargo.toml | 2 +- packages/voxit-host-ffi/src/lib.rs | 48 +++++++++++++++++++-------- packages/voxit-macos/src/lib.rs | 2 ++ 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/voxit-core/src/contextual.rs b/packages/voxit-core/src/contextual.rs index 3a0d4d9..d599f80 100644 --- a/packages/voxit-core/src/contextual.rs +++ b/packages/voxit-core/src/contextual.rs @@ -452,7 +452,7 @@ mod tests { let router = ContextualVoiceRouter; let context = FocusedAppContext::new().with_app("com.apple.Terminal", "Terminal"); let plan = router.plan_for_context(&context); - let instructions = plan.rewrite_instructions("concise", 1200); + let instructions = plan.rewrite_instructions("concise", 1_200); assert!(instructions.contains("Terminal")); assert!(instructions.contains("confirm_before_action")); diff --git a/packages/voxit-core/src/inference.rs b/packages/voxit-core/src/inference.rs index 8ebd8a5..99a4c37 100644 --- a/packages/voxit-core/src/inference.rs +++ b/packages/voxit-core/src/inference.rs @@ -188,10 +188,12 @@ fn rewrite_with_guard( ) -> Result { let provider = default_provider()?; let mut instructions = plan.rewrite_instructions(&settings.style, settings.max_output_chars); + if !settings.glossary_terms.trim().is_empty() { instructions.push_str("\nGlossary terms to preserve or prefer:\n"); instructions.push_str(settings.glossary_terms.trim()); } + let rewritten = provider.rewrite(RewriteRequest { text, model, instructions: &instructions })?; @@ -215,6 +217,7 @@ fn rewrite_with_guard( let baseline = protected_token_multiset(text); let candidate = protected_token_multiset(&rewritten); + if baseline != candidate { return Ok(RewriteResult { rewritten_transcript: None, diff --git a/packages/voxit-host-ffi/Cargo.toml b/packages/voxit-host-ffi/Cargo.toml index 3f140d4..53e86e1 100644 --- a/packages/voxit-host-ffi/Cargo.toml +++ b/packages/voxit-host-ffi/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] voxit-audio = { path = "../voxit-audio" } -voxit-core = { path = "../voxit-core" } +voxit-core = { path = "../voxit-core" } [target.'cfg(target_os = "macos")'.dependencies] voxit-macos = { path = "../voxit-macos" } diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index 6368e85..cec6e4f 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -6,18 +6,19 @@ use std::{ ffi::{CStr, c_char}, - ptr::NonNull, + ptr::{self, NonNull}, }; +use voxit_audio::Recorder; use voxit_core::{ - Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, + self, Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, RewriteSettings, VoiceSessionPlan, contextual::{ PromptProfileKind, VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, }, - rewrite_only_with_plan, transcribe_only, ui_model::{AuthMethod, AuthSurfaceState, DictationSurfaceState, HotkeySurfaceMode}, }; +use voxit_macos::TargetApp; /// ABI version exported by the thin C host bridge. pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 4; @@ -35,9 +36,9 @@ pub struct VoxitHostSessionHandle { last_error: String, recording_duration_ms: u64, #[cfg(target_os = "macos")] - recorder: Option, + recorder: Option, #[cfg(target_os = "macos")] - target_app: Option, + target_app: Option, } /// Result code returned by FFI entry points. @@ -483,6 +484,7 @@ pub unsafe extern "C" fn voxit_host_session_set_profile_override( let handle = unsafe { handle.as_mut() }; handle.profile_override = Some(decode_prompt_profile_kind(profile_kind)); + update_voice_plan(handle); VoxitStatus::Ok @@ -503,6 +505,7 @@ pub unsafe extern "C" fn voxit_host_session_clear_profile_override( let handle = unsafe { handle.as_mut() }; handle.profile_override = None; + update_voice_plan(handle); VoxitStatus::Ok @@ -557,9 +560,11 @@ pub unsafe extern "C" fn voxit_host_session_copy_string( let Some(out) = NonNull::new(out) else { return VoxitStatus::NullOutput; }; + if out_len == 0 { return VoxitStatus::InvalidInput; } + let handle = unsafe { handle.as_ref() }; let value = string_field_value(handle, field); @@ -581,11 +586,13 @@ fn start_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { let preferred_device_id = (handle.config.audio.input_device_id != 0) .then_some(handle.config.audio.input_device_id); + match voxit_audio::start_recording_with_stream(64, preferred_device_id) { Ok((recorder, _chunk_rx, selection)) => { handle.recorder = Some(recorder); handle.snapshot.dictation_state = DictationSurfaceState::Listening; handle.recording_duration_ms = 0; + if selection.fallback_to_default { handle.last_error = format!( "requested microphone unavailable; using {}", @@ -595,16 +602,17 @@ fn start_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { }, Err(err) => { handle.snapshot.dictation_state = DictationSurfaceState::Idle; + set_error(handle, format!("failed to start recording: {err}")); }, } - return VoxitStatus::Ok; + VoxitStatus::Ok } - #[cfg(not(target_os = "macos"))] { handle.snapshot.dictation_state = DictationSurfaceState::Idle; + set_error(handle, "recording is only supported on macOS in this build"); VoxitStatus::Ok @@ -621,34 +629,42 @@ fn stop_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { }; handle.snapshot.dictation_state = DictationSurfaceState::Finalizing; + let recording = match voxit_audio::stop_recording(recorder) { Ok(recording) => recording, Err(err) => { handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, format!("failed to stop recording: {err}")); return VoxitStatus::Ok; }, }; + handle.recording_duration_ms = recording.duration_ms; let (raw_transcript, _) = - match transcribe_only(&recording.wav, &handle.config.openai.finalize_model) { + match voxit_core::transcribe_only(&recording.wav, &handle.config.openai.finalize_model) + { Ok(result) => result, Err(err) => { handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, format!("transcription failed: {err}")); return VoxitStatus::Ok; }, }; + handle.last_raw_transcript = raw_transcript; handle.last_final_output = handle.last_raw_transcript.clone(); if handle.config.rewrite.enabled && handle.config.rewrite.auto { handle.snapshot.dictation_state = DictationSurfaceState::Rewriting; + let settings = rewrite_settings(handle); - match rewrite_only_with_plan( + + match voxit_core::rewrite_only_with_plan( &handle.last_raw_transcript, &handle.config.openai.rewrite_model, &handle.voice_plan, @@ -669,16 +685,17 @@ fn stop_dictation(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { } handle.snapshot.dictation_state = DictationSurfaceState::Done; + if matches!(handle.voice_plan.output_policy, VoiceOutputPolicy::InsertText) { let _ = paste_final_output(handle); } - return VoxitStatus::Ok; + VoxitStatus::Ok } - #[cfg(not(target_os = "macos"))] { handle.snapshot.dictation_state = DictationSurfaceState::Done; + set_error(handle, "recording is only supported on macOS in this build"); VoxitStatus::Ok @@ -693,6 +710,7 @@ fn paste_final_output(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { return VoxitStatus::Ok; } + let target = if handle.config.paste.lock_frontmost_app { handle.target_app.as_ref() } else { None }; @@ -704,7 +722,7 @@ fn paste_final_output(handle: &mut VoxitHostSessionHandle) -> VoxitStatus { set_error(handle, format!("paste failed: {err}")); } - return VoxitStatus::Ok; + VoxitStatus::Ok } #[cfg(not(target_os = "macos"))] @@ -752,6 +770,7 @@ fn clear_run_output(handle: &mut VoxitHostSessionHandle) { handle.last_raw_transcript.clear(); handle.last_final_output.clear(); handle.last_error.clear(); + handle.recording_duration_ms = 0; } @@ -933,7 +952,8 @@ fn write_c_string(out: NonNull, out_len: usize, value: &str) -> VoxitSta let copy_len = bytes.len().min(out_len.saturating_sub(1)); unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_ptr().cast::(), copy_len); + ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_ptr().cast::(), copy_len); + *out.as_ptr().add(copy_len) = 0; } @@ -957,7 +977,7 @@ fn refresh_focused_context(handle: &mut VoxitHostSessionHandle) { } #[cfg(target_os = "macos")] -fn focused_context_from_target(target: voxit_macos::TargetApp) -> FocusedAppContext { +fn focused_context_from_target(target: TargetApp) -> FocusedAppContext { let mut context = FocusedAppContext::new(); if let (Some(bundle_id), Some(app_name)) = (target.bundle_id, target.app_name) { diff --git a/packages/voxit-macos/src/lib.rs b/packages/voxit-macos/src/lib.rs index 9acca2c..cfc2d14 100644 --- a/packages/voxit-macos/src/lib.rs +++ b/packages/voxit-macos/src/lib.rs @@ -327,6 +327,7 @@ pub fn paste_text(target: Option<&TargetApp>, text: &str, lock_target: bool) -> } copy_to_clipboard(text)?; + dispatch_command_v() } @@ -441,6 +442,7 @@ fn copy_to_clipboard(text: &str) -> Result<(), String> { }; stdin.write_all(text.as_bytes()).map_err(|err| format!("write pbcopy failed: {err}"))?; + let status = child.wait().map_err(|err| format!("wait pbcopy failed: {err}"))?; if status.success() { Ok(()) } else { Err(format!("pbcopy failed with status {status}")) } From 15bec0f4d2727dc8273b70dc186c4200f923e2cc Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:07:42 +0800 Subject: [PATCH 16/27] {"schema":"decodex/commit/1","summary":"Split Voxit language checks","authority":"manual"} --- .github/workflows/language.yml | 110 +++++++++++------- Makefile.toml | 80 ++++++++++--- .../Stores/VoxitSettingsStore.swift | 4 +- packages/voxit-audio/src/lib.rs | 4 +- packages/voxit-core/src/auth.rs | 5 +- 5 files changed, 140 insertions(+), 63 deletions(-) diff --git a/.github/workflows/language.yml b/.github/workflows/language.yml index 3131e45..49aef75 100644 --- a/.github/workflows/language.yml +++ b/.github/workflows/language.yml @@ -26,15 +26,15 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - rust: - name: Rust checks + rust-check: + name: Rust check runs-on: macos-latest steps: - name: Fetch latest code - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: cache: true rustflags: '' @@ -44,71 +44,95 @@ jobs: run: rustup toolchain install nightly --component rustfmt - name: Install cargo-make - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: cargo-make - - name: Install vibe-style (latest release) - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - VERSION="$( - curl -fsSL \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${GITHUB_TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/hack-ink/vibe-style/releases/latest \ - | grep -oE '"tag_name": "v[^"]+"' \ - | cut -d'"' -f4 - )" - TARGET="$(rustc -vV | awk -F': ' '/^host:/ {print $2}')" - ASSET="vibe-style-${TARGET}-${VERSION}.tgz" - - curl -fsSLO "https://github.com/hack-ink/vibe-style/releases/download/${VERSION}/${ASSET}" - tar -xzf "${ASSET}" - - mkdir -p "$HOME/.cargo/bin" - install -m 0755 "vibe-style-${TARGET}-${VERSION}/vstyle" "$HOME/.cargo/bin/vstyle" - install -m 0755 "vibe-style-${TARGET}-${VERSION}/cargo-vstyle" "$HOME/.cargo/bin/cargo-vstyle" - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - - name: Install nextest - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: nextest - - name: Run lint - run: cargo make lint - - name: Run Rust format checks run: cargo make fmt-rust-check - - name: Run tests + - name: Run Rust style check + uses: hack-ink/vibe-style@bfb4d2d2f5e4b5e5ce8de4ed1d708b3a2f0e61fe # v0.2.1 + with: + language: rust + workspace: true + args: --all-features + version: v0.2.1 + + - name: Run Rust clippy + run: cargo make lint-rust + + - name: Run Rust tests + run: cargo make test-rust + + swift-check: + name: Swift check + runs-on: macos-latest + steps: + - name: Fetch latest code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Print Apple toolchain + run: | + sw_vers + swift --version + xcodebuild -version + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: true + rustflags: '' + + - name: Install cargo-make + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 + with: + tool: cargo-make + + - name: Run Swift format checks + run: cargo make fmt-swift-check + + - name: Run Swift style check + uses: hack-ink/vibe-style@bfb4d2d2f5e4b5e5ce8de4ed1d708b3a2f0e61fe # v0.2.1 + with: + language: swift + workspace: true + args: --all-features + version: v0.2.1 + + - name: Run Swift lint and strict build + run: cargo make lint-swift + + - name: Run native host tests env: VOXIT_NATIVE_HOST_SIGN_IDENTITY: '-' - run: cargo make test + run: cargo make test-swift - toml: - name: TOML checks - runs-on: macos-latest + toml-check: + name: TOML check + runs-on: ubuntu-latest steps: - name: Fetch latest code - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: cache: true rustflags: '' - name: Install cargo-make - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: cargo-make - name: Install taplo - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: taplo diff --git a/Makefile.toml b/Makefile.toml index 0a035a0..41d9121 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,16 +1,20 @@ # Rust workspace tasks. # Lint -# | task | type | cwd | -# | --------------- | --------- | --- | -# | lint | composite | | -# | lint-fix | composite | | -# | lint-rust | command | | -# | lint-fix-rust | extend | | -# | lint-swift | composite | | -# | lint-fix-swift | composite | | -# | lint-vstyle | command | | -# | lint-fix-vstyle | command | | +# | task | type | cwd | +# | ----------------------- | --------- | --- | +# | lint | composite | | +# | lint-fix | composite | | +# | lint-rust | command | | +# | lint-fix-rust | extend | | +# | lint-swift | composite | | +# | lint-fix-swift | composite | | +# | lint-vstyle | composite | | +# | lint-vstyle-rust | command | | +# | lint-vstyle-swift | command | | +# | lint-fix-vstyle | composite | | +# | lint-fix-vstyle-rust | command | | +# | lint-fix-vstyle-swift | command | | [tasks.lint] workspace = false @@ -111,20 +115,63 @@ args = [ [tasks.lint-vstyle] workspace = false +dependencies = [ + "lint-vstyle-rust", + "lint-vstyle-swift", +] + +[tasks.lint-vstyle-rust] +workspace = false +command = "cargo" +args = [ + "vstyle", + "curate", + "--language", + "rust", + "--workspace", + "--all-features", +] + +[tasks.lint-vstyle-swift] +workspace = false command = "cargo" args = [ "vstyle", "curate", + "--language", + "swift", "--workspace", - "--all-features" + "--all-features", ] [tasks.lint-fix-vstyle] workspace = false +dependencies = [ + "lint-fix-vstyle-rust", + "lint-fix-vstyle-swift", +] + +[tasks.lint-fix-vstyle-rust] +workspace = false +command = "cargo" +args = [ + "vstyle", + "tune", + "--language", + "rust", + "--workspace", + "--all-features", + "--strict", +] + +[tasks.lint-fix-vstyle-swift] +workspace = false command = "cargo" args = [ "vstyle", "tune", + "--language", + "swift", "--workspace", "--all-features", "--strict", @@ -132,10 +179,11 @@ args = [ # Test -# | task | type | cwd | -# | --------- | --------- | --- | +# | task | type | cwd | +# | ---------------------------- | --------- | --- | # | test | composite | | # | test-rust | command | | +# | test-swift | composite | | # | test-host-ffi-header | command | | # | test-macos-native-host | command | | # | test-macos-native-host-stage | command | | @@ -144,6 +192,12 @@ args = [ workspace = false dependencies = [ "test-rust", + "test-swift", +] + +[tasks.test-swift] +workspace = false +dependencies = [ "test-host-ffi-header", "test-macos-native-host", "test-macos-native-host-stage", diff --git a/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift b/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift index ac31705..12b22d9 100644 --- a/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift +++ b/native/macos-host/Sources/VoxitNativeHostKit/Stores/VoxitSettingsStore.swift @@ -119,7 +119,7 @@ struct VoxitSettings: Equatable { private static func parseHotkeyPresentation(_ raw: String) -> VoxitHotkeyPresentation? { let tokens = hotkeyTokens(from: raw) - guard !tokens.isEmpty else { + guard tokens.isEmpty == false else { return nil } @@ -190,7 +190,7 @@ struct VoxitSettings: Equatable { character == "+" || character == "-" } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } + .filter { $0.isEmpty == false } } } diff --git a/packages/voxit-audio/src/lib.rs b/packages/voxit-audio/src/lib.rs index 8f89940..87839a8 100644 --- a/packages/voxit-audio/src/lib.rs +++ b/packages/voxit-audio/src/lib.rs @@ -19,7 +19,7 @@ use coreaudio::audio_unit::{ render_callback::{Args, data::Interleaved}, }; use coreaudio::error::Error; -use hound::WavWriter; +use hound::{WavSpec, WavWriter}; #[cfg(target_os = "macos")] use objc2_audio_toolbox::{ kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, @@ -499,7 +499,7 @@ fn encode_wav(samples: &[i16], sample_rate: u32, channels: u16) -> Result Option { let mut parts = jwt.split('.'); let _header = parts.next()?; let payload = parts.next()?; - let payload = - base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload.as_bytes()).ok()?; + let payload = URL_SAFE_NO_PAD.decode(payload.as_bytes()).ok()?; serde_json::from_slice(&payload).ok() } From 88167fc362d8b9ac6e03b596a1edf578f98119ad Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:15:18 +0800 Subject: [PATCH 17/27] {"schema":"decodex/commit/1","summary":"Sort Voxit check tasks","authority":"manual"} --- .github/workflows/language.yml | 14 +-- Makefile.toml | 193 +++++++++++++++++---------------- 2 files changed, 105 insertions(+), 102 deletions(-) diff --git a/.github/workflows/language.yml b/.github/workflows/language.yml index 49aef75..2f51a4b 100644 --- a/.github/workflows/language.yml +++ b/.github/workflows/language.yml @@ -40,18 +40,13 @@ jobs: rustflags: '' components: rustfmt, clippy - - name: Install nightly rustfmt - run: rustup toolchain install nightly --component rustfmt - - name: Install cargo-make uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: cargo-make - - name: Install nextest - uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 - with: - tool: nextest + - name: Install nightly rustfmt + run: rustup toolchain install nightly --component rustfmt - name: Run Rust format checks run: cargo make fmt-rust-check @@ -67,6 +62,11 @@ jobs: - name: Run Rust clippy run: cargo make lint-rust + - name: Install nextest + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 + with: + tool: nextest + - name: Run Rust tests run: cargo make test-rust diff --git a/Makefile.toml b/Makefile.toml index 41d9121..afc7075 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -5,16 +5,17 @@ # | ----------------------- | --------- | --- | # | lint | composite | | # | lint-fix | composite | | -# | lint-rust | command | | # | lint-fix-rust | extend | | -# | lint-swift | composite | | # | lint-fix-swift | composite | | -# | lint-vstyle | composite | | -# | lint-vstyle-rust | command | | -# | lint-vstyle-swift | command | | # | lint-fix-vstyle | composite | | # | lint-fix-vstyle-rust | command | | # | lint-fix-vstyle-swift | command | | +# | lint-rust | command | | +# | lint-swift | composite | | +# | lint-swift-format | command | | +# | lint-vstyle | composite | | +# | lint-vstyle-rust | command | | +# | lint-vstyle-swift | command | | [tasks.lint] workspace = false @@ -32,11 +33,12 @@ dependencies = [ "lint-fix-vstyle", ] -[tasks.lint-rust] -workspace = false -command = "cargo" +[tasks.lint-fix-rust] +extend = "lint-rust" args = [ "clippy", + "--fix", + "--allow-dirty", "--all-features", "--all-targets", "--workspace", @@ -59,38 +61,51 @@ args = [ "warnings", ] -[tasks.lint-swift] +[tasks.lint-fix-swift] workspace = false dependencies = [ - "lint-swift-format", + "fmt-swift", "build-swift-strict", ] -[tasks.lint-fix-swift] +[tasks.lint-fix-vstyle] workspace = false dependencies = [ - "fmt-swift", - "build-swift-strict", + "lint-fix-vstyle-rust", + "lint-fix-vstyle-swift", ] -[tasks.lint-swift-format] +[tasks.lint-fix-vstyle-rust] workspace = false -command = "swift" +command = "cargo" args = [ - "format", - "lint", - "--recursive", + "vstyle", + "tune", + "--language", + "rust", + "--workspace", + "--all-features", "--strict", - "--parallel", - "native/macos-host/Sources", ] -[tasks.lint-fix-rust] -extend = "lint-rust" +[tasks.lint-fix-vstyle-swift] +workspace = false +command = "cargo" +args = [ + "vstyle", + "tune", + "--language", + "swift", + "--workspace", + "--all-features", + "--strict", +] + +[tasks.lint-rust] +workspace = false +command = "cargo" args = [ "clippy", - "--fix", - "--allow-dirty", "--all-features", "--all-targets", "--workspace", @@ -113,105 +128,93 @@ args = [ "warnings", ] -[tasks.lint-vstyle] +[tasks.lint-swift] workspace = false dependencies = [ - "lint-vstyle-rust", - "lint-vstyle-swift", -] - -[tasks.lint-vstyle-rust] -workspace = false -command = "cargo" -args = [ - "vstyle", - "curate", - "--language", - "rust", - "--workspace", - "--all-features", + "lint-swift-format", + "build-swift-strict", ] -[tasks.lint-vstyle-swift] +[tasks.lint-swift-format] workspace = false -command = "cargo" +command = "swift" args = [ - "vstyle", - "curate", - "--language", - "swift", - "--workspace", - "--all-features", + "format", + "lint", + "--recursive", + "--strict", + "--parallel", + "native/macos-host/Sources", ] -[tasks.lint-fix-vstyle] +[tasks.lint-vstyle] workspace = false dependencies = [ - "lint-fix-vstyle-rust", - "lint-fix-vstyle-swift", + "lint-vstyle-rust", + "lint-vstyle-swift", ] -[tasks.lint-fix-vstyle-rust] +[tasks.lint-vstyle-rust] workspace = false command = "cargo" args = [ "vstyle", - "tune", + "curate", "--language", "rust", "--workspace", "--all-features", - "--strict", ] -[tasks.lint-fix-vstyle-swift] +[tasks.lint-vstyle-swift] workspace = false command = "cargo" args = [ "vstyle", - "tune", + "curate", "--language", "swift", "--workspace", "--all-features", - "--strict", ] # Test # | task | type | cwd | # | ---------------------------- | --------- | --- | +# | build-host-ffi-staticlib | command | | +# | build-swift-strict | command | | # | test | composite | | -# | test-rust | command | | -# | test-swift | composite | | # | test-host-ffi-header | command | | # | test-macos-native-host | command | | # | test-macos-native-host-stage | command | | +# | test-rust | command | | +# | test-swift | composite | | -[tasks.test] +[tasks.build-host-ffi-staticlib] workspace = false -dependencies = [ - "test-rust", - "test-swift", -] +script = ''' +MACOSX_DEPLOYMENT_TARGET=14.0 cargo build -p voxit-host-ffi +''' -[tasks.test-swift] +[tasks.build-swift-strict] workspace = false dependencies = [ - "test-host-ffi-header", - "test-macos-native-host", - "test-macos-native-host-stage", + "build-host-ffi-staticlib", ] +script = ''' +VOXIT_HOST_FFI_LIB_DIR="$PWD/target/debug" \ +swift build --package-path native/macos-host \ + --explicit-target-dependency-import-check error \ + -Xswiftc -warnings-as-errors \ + -Xswiftc -strict-concurrency=complete +''' -[tasks.test-rust] +[tasks.test] workspace = false -command = "cargo" -args = [ - "nextest", - "run", - "--workspace", - "--all-targets", - "--all-features", +dependencies = [ + "test-rust", + "test-swift", ] [tasks.test-host-ffi-header] @@ -225,25 +228,6 @@ args = [ "packages/voxit-host-ffi/tests/header_smoke.c", ] -[tasks.build-host-ffi-staticlib] -workspace = false -script = ''' -MACOSX_DEPLOYMENT_TARGET=14.0 cargo build -p voxit-host-ffi -''' - -[tasks.build-swift-strict] -workspace = false -dependencies = [ - "build-host-ffi-staticlib", -] -script = ''' -VOXIT_HOST_FFI_LIB_DIR="$PWD/target/debug" \ -swift build --package-path native/macos-host \ - --explicit-target-dependency-import-check error \ - -Xswiftc -warnings-as-errors \ - -Xswiftc -strict-concurrency=complete -''' - [tasks.test-macos-native-host] workspace = false dependencies = [ @@ -275,6 +259,25 @@ plutil -extract CFBundleIconFile raw "$APP_PATH/Contents/Info.plist" | grep -qx plutil -extract LSUIElement raw "$APP_PATH/Contents/Info.plist" | grep -qx 'true' ''' +[tasks.test-rust] +workspace = false +command = "cargo" +args = [ + "nextest", + "run", + "--workspace", + "--all-targets", + "--all-features", +] + +[tasks.test-swift] +workspace = false +dependencies = [ + "test-host-ffi-header", + "test-macos-native-host", + "test-macos-native-host-stage", +] + # Format # | task | type | cwd | @@ -351,7 +354,7 @@ args = [ [tasks.checks] workspace = false dependencies = [ + "fmt-check", "lint", "test", - "fmt-check", ] From 2d1e2b9522a756e341023363be602d3c69e0a702 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:19:09 +0800 Subject: [PATCH 18/27] {"schema":"decodex/commit/1","summary":"Move format checks before test tasks","authority":"manual"} --- Makefile.toml | 162 +++++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index afc7075..c32efa3 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,5 +1,72 @@ # Rust workspace tasks. +# Format +# | task | type | cwd | +# | --------------- | --------- | --- | +# | fmt | composite | | +# | fmt-check | composite | | +# | fmt-rust | command | | +# | fmt-rust-check | extend | | +# | fmt-swift | command | | +# | fmt-swift-check | command | | +# | fmt-toml | command | | +# | fmt-toml-check | extend | | + +[tasks.fmt] +workspace = false +dependencies = [ + "fmt-rust", + "fmt-swift", + "fmt-toml", +] + +[tasks.fmt-check] +workspace = false +dependencies = [ + "fmt-rust-check", + "fmt-swift-check", + "fmt-toml-check", +] + +[tasks.fmt-rust] +workspace = false +script = "cargo +nightly fmt --all" + +[tasks.fmt-rust-check] +extend = "fmt-rust" +script = "cargo +nightly fmt --all -- --check" + +[tasks.fmt-swift] +workspace = false +command = "swift" +args = [ + "format", + "format", + "--in-place", + "--recursive", + "--parallel", + "native/macos-host/Sources", +] + +[tasks.fmt-swift-check] +workspace = false +extend = "lint-swift-format" + +[tasks.fmt-toml] +workspace = false +command = "taplo" +args = [ + "fmt", +] + +[tasks.fmt-toml-check] +extend = "fmt-toml" +args = [ + "fmt", + "--check", +] + + # Lint # | task | type | cwd | # | ----------------------- | --------- | --- | @@ -179,6 +246,20 @@ args = [ ] +# Meta +# | task | type | cwd | +# | ------ | --------- | --- | +# | checks | composite | | + +[tasks.checks] +workspace = false +dependencies = [ + "fmt-check", + "lint", + "test", +] + + # Test # | task | type | cwd | # | ---------------------------- | --------- | --- | @@ -277,84 +358,3 @@ dependencies = [ "test-macos-native-host", "test-macos-native-host-stage", ] - - -# Format -# | task | type | cwd | -# | -------------- | --------- | --- | -# | fmt | composite | | -# | fmt-check | composite | | -# | fmt-rust | command | | -# | fmt-rust-check | extend | | -# | fmt-swift | command | | -# | fmt-swift-check| command | | -# | fmt-toml | command | | -# | fmt-toml-check | extend | | - -[tasks.fmt] -workspace = false -dependencies = [ - "fmt-rust", - "fmt-swift", - "fmt-toml", -] - -[tasks.fmt-check] -workspace = false -dependencies = [ - "fmt-rust-check", - "fmt-swift-check", - "fmt-toml-check", -] - -[tasks.fmt-rust] -workspace = false -script = "cargo +nightly fmt --all" - -[tasks.fmt-rust-check] -extend = "fmt-rust" -script = "cargo +nightly fmt --all -- --check" - -[tasks.fmt-swift] -workspace = false -command = "swift" -args = [ - "format", - "format", - "--in-place", - "--recursive", - "--parallel", - "native/macos-host/Sources", -] - -[tasks.fmt-swift-check] -workspace = false -extend = "lint-swift-format" - -[tasks.fmt-toml] -workspace = false -command = "taplo" -args = [ - "fmt", -] - -[tasks.fmt-toml-check] -extend = "fmt-toml" -args = [ - "fmt", - "--check", -] - - -# Meta -# | task | type | cwd | -# | ------ | --------- | --- | -# | checks | composite | | - -[tasks.checks] -workspace = false -dependencies = [ - "fmt-check", - "lint", - "test", -] From d05eacb0cd59847fb187b32971c156e03720e944 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:27:37 +0800 Subject: [PATCH 19/27] {"schema":"decodex/commit/1","summary":"Align Voxit checks with Rsnap","authority":"manual"} --- .github/workflows/language.yml | 32 ++-- Makefile.toml | 320 +++++++++++++++++---------------- 2 files changed, 179 insertions(+), 173 deletions(-) diff --git a/.github/workflows/language.yml b/.github/workflows/language.yml index 2f51a4b..7728532 100644 --- a/.github/workflows/language.yml +++ b/.github/workflows/language.yml @@ -28,7 +28,7 @@ concurrency: jobs: rust-check: name: Rust check - runs-on: macos-latest + runs-on: ubuntu-latest steps: - name: Fetch latest code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -40,15 +40,20 @@ jobs: rustflags: '' components: rustfmt, clippy + - name: Install nightly rustfmt + run: rustup toolchain install nightly --component rustfmt + - name: Install cargo-make uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 with: tool: cargo-make - - name: Install nightly rustfmt - run: rustup toolchain install nightly --component rustfmt + - name: Install nextest + uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 + with: + tool: nextest - - name: Run Rust format checks + - name: Run Rust format check run: cargo make fmt-rust-check - name: Run Rust style check @@ -60,19 +65,14 @@ jobs: version: v0.2.1 - name: Run Rust clippy - run: cargo make lint-rust - - - name: Install nextest - uses: taiki-e/install-action@3fa6878dc4ae603f73960271565a082bf196ab96 # v2.77.2 - with: - tool: nextest + run: cargo make check-rust - name: Run Rust tests run: cargo make test-rust swift-check: name: Swift check - runs-on: macos-latest + runs-on: macos-26 steps: - name: Fetch latest code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -94,7 +94,7 @@ jobs: with: tool: cargo-make - - name: Run Swift format checks + - name: Run Swift format check run: cargo make fmt-swift-check - name: Run Swift style check @@ -105,10 +105,10 @@ jobs: args: --all-features version: v0.2.1 - - name: Run Swift lint and strict build - run: cargo make lint-swift + - name: Run Swift strict build + run: cargo make check-swift - - name: Run native host tests + - name: Run Swift tests env: VOXIT_NATIVE_HOST_SIGN_IDENTITY: '-' run: cargo make test-swift @@ -136,5 +136,5 @@ jobs: with: tool: taplo - - name: Run TOML format checks + - name: Run TOML format check run: cargo make fmt-toml-check diff --git a/Makefile.toml b/Makefile.toml index c32efa3..d11613e 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,4 +1,89 @@ -# Rust workspace tasks. +# Voxit workspace tasks. + +# Check +# | task | type | cwd | +# | ------------------ | --------- | --- | +# | check | composite | | +# | check-rust | command | | +# | check-swift | composite | | +# | check-vstyle | composite | | +# | check-vstyle-rust | command | | +# | check-vstyle-swift | command | | + +[tasks.check] +clear = true +workspace = false +dependencies = [ + "fmt-check", + "check-rust", + "check-swift", + "check-vstyle", + "test", +] + +[tasks.check-rust] +workspace = false +command = "cargo" +args = [ + "clippy", + "--all-features", + "--all-targets", + "--workspace", + "--", + "-D", + "clippy::all", + "-D", + "clippy::too_many_lines", + "-D", + "clippy::unwrap_used", + "-D", + "clippy::use_self", + "-D", + "clippy::wildcard_imports", + "-D", + "missing-docs", + "-D", + "unused-crate-dependencies", + "-D", + "warnings", +] + +[tasks.check-swift] +workspace = false +dependencies = [ + "build-swift-strict", +] + +[tasks.check-vstyle] +workspace = false +dependencies = [ + "check-vstyle-rust", + "check-vstyle-swift", +] + +[tasks.check-vstyle-rust] +workspace = false +command = "cargo" +args = [ + "vstyle", + "curate", + "--language", + "rust", + "--workspace", + "--all-features", +] + +[tasks.check-vstyle-swift] +workspace = false +command = "cargo" +args = [ + "vstyle", + "curate", + "--language", + "swift", + "--workspace", + "--all-features", +] # Format # | task | type | cwd | @@ -50,7 +135,15 @@ args = [ [tasks.fmt-swift-check] workspace = false -extend = "lint-swift-format" +command = "swift" +args = [ + "format", + "lint", + "--recursive", + "--strict", + "--parallel", + "native/macos-host/Sources", +] [tasks.fmt-toml] workspace = false @@ -66,23 +159,15 @@ args = [ "--check", ] - # Lint -# | task | type | cwd | -# | ----------------------- | --------- | --- | -# | lint | composite | | -# | lint-fix | composite | | -# | lint-fix-rust | extend | | -# | lint-fix-swift | composite | | -# | lint-fix-vstyle | composite | | -# | lint-fix-vstyle-rust | command | | -# | lint-fix-vstyle-swift | command | | -# | lint-rust | command | | -# | lint-swift | composite | | -# | lint-swift-format | command | | -# | lint-vstyle | composite | | -# | lint-vstyle-rust | command | | -# | lint-vstyle-swift | command | | +# | task | type | cwd | +# | ------------------ | --------- | --- | +# | lint | composite | | +# | lint-rust | command | | +# | lint-swift | composite | | +# | lint-vstyle | composite | | +# | lint-vstyle-rust | command | | +# | lint-vstyle-swift | command | | [tasks.lint] workspace = false @@ -92,16 +177,9 @@ dependencies = [ "lint-vstyle", ] -[tasks.lint-fix] +[tasks.lint-rust] workspace = false -dependencies = [ - "lint-fix-rust", - "lint-fix-swift", - "lint-fix-vstyle", -] - -[tasks.lint-fix-rust] -extend = "lint-rust" +command = "cargo" args = [ "clippy", "--fix", @@ -128,21 +206,21 @@ args = [ "warnings", ] -[tasks.lint-fix-swift] +[tasks.lint-swift] workspace = false dependencies = [ "fmt-swift", "build-swift-strict", ] -[tasks.lint-fix-vstyle] +[tasks.lint-vstyle] workspace = false dependencies = [ - "lint-fix-vstyle-rust", - "lint-fix-vstyle-swift", + "lint-vstyle-rust", + "lint-vstyle-swift", ] -[tasks.lint-fix-vstyle-rust] +[tasks.lint-vstyle-rust] workspace = false command = "cargo" args = [ @@ -155,7 +233,7 @@ args = [ "--strict", ] -[tasks.lint-fix-vstyle-swift] +[tasks.lint-vstyle-swift] workspace = false command = "cargo" args = [ @@ -168,134 +246,41 @@ args = [ "--strict", ] -[tasks.lint-rust] -workspace = false -command = "cargo" -args = [ - "clippy", - "--all-features", - "--all-targets", - "--workspace", - "--", - "-D", - "clippy::all", - "-D", - "clippy::too_many_lines", - "-D", - "clippy::unwrap_used", - "-D", - "clippy::use_self", - "-D", - "clippy::wildcard_imports", - "-D", - "missing-docs", - "-D", - "unused-crate-dependencies", - "-D", - "warnings", -] - -[tasks.lint-swift] -workspace = false -dependencies = [ - "lint-swift-format", - "build-swift-strict", -] - -[tasks.lint-swift-format] -workspace = false -command = "swift" -args = [ - "format", - "lint", - "--recursive", - "--strict", - "--parallel", - "native/macos-host/Sources", -] +# Test +# | task | type | cwd | +# | ---------------------------- | --------- | --- | +# | test | composite | | +# | test-rust | command | | +# | test-swift | composite | | +# | test-host-ffi-header | command | | +# | test-macos-native-host | command | | +# | test-macos-native-host-stage | command | | -[tasks.lint-vstyle] +[tasks.test] +clear = true workspace = false dependencies = [ - "lint-vstyle-rust", - "lint-vstyle-swift", -] - -[tasks.lint-vstyle-rust] -workspace = false -command = "cargo" -args = [ - "vstyle", - "curate", - "--language", - "rust", - "--workspace", - "--all-features", + "test-rust", + "test-swift", ] -[tasks.lint-vstyle-swift] +[tasks.test-rust] workspace = false command = "cargo" args = [ - "vstyle", - "curate", - "--language", - "swift", + "nextest", + "run", "--workspace", + "--all-targets", "--all-features", ] - -# Meta -# | task | type | cwd | -# | ------ | --------- | --- | -# | checks | composite | | - -[tasks.checks] -workspace = false -dependencies = [ - "fmt-check", - "lint", - "test", -] - - -# Test -# | task | type | cwd | -# | ---------------------------- | --------- | --- | -# | build-host-ffi-staticlib | command | | -# | build-swift-strict | command | | -# | test | composite | | -# | test-host-ffi-header | command | | -# | test-macos-native-host | command | | -# | test-macos-native-host-stage | command | | -# | test-rust | command | | -# | test-swift | composite | | - -[tasks.build-host-ffi-staticlib] -workspace = false -script = ''' -MACOSX_DEPLOYMENT_TARGET=14.0 cargo build -p voxit-host-ffi -''' - -[tasks.build-swift-strict] -workspace = false -dependencies = [ - "build-host-ffi-staticlib", -] -script = ''' -VOXIT_HOST_FFI_LIB_DIR="$PWD/target/debug" \ -swift build --package-path native/macos-host \ - --explicit-target-dependency-import-check error \ - -Xswiftc -warnings-as-errors \ - -Xswiftc -strict-concurrency=complete -''' - -[tasks.test] +[tasks.test-swift] workspace = false dependencies = [ - "test-rust", - "test-swift", + "test-host-ffi-header", + "test-macos-native-host", + "test-macos-native-host-stage", ] [tasks.test-host-ffi-header] @@ -340,21 +325,42 @@ plutil -extract CFBundleIconFile raw "$APP_PATH/Contents/Info.plist" | grep -qx plutil -extract LSUIElement raw "$APP_PATH/Contents/Info.plist" | grep -qx 'true' ''' -[tasks.test-rust] +# Build +# | task | type | cwd | +# | ------------------------ | ------- | --- | +# | build-host-ffi-staticlib | command | | +# | build-swift-strict | command | | + +[tasks.build-host-ffi-staticlib] workspace = false +env = { MACOSX_DEPLOYMENT_TARGET = "14.0" } command = "cargo" args = [ - "nextest", - "run", - "--workspace", - "--all-targets", - "--all-features", + "build", + "-p", + "voxit-host-ffi", ] -[tasks.test-swift] +[tasks.build-swift-strict] workspace = false dependencies = [ - "test-host-ffi-header", - "test-macos-native-host", - "test-macos-native-host-stage", + "build-host-ffi-staticlib", +] +script = ''' +VOXIT_HOST_FFI_LIB_DIR="$PWD/target/debug" \ +swift build --package-path native/macos-host \ + --explicit-target-dependency-import-check error \ + -Xswiftc -warnings-as-errors \ + -Xswiftc -strict-concurrency=complete +''' + +# Meta +# | task | type | cwd | +# | ------ | --------- | --- | +# | checks | composite | | + +[tasks.checks] +workspace = false +dependencies = [ + "check", ] From 2b64f1cedd775bb98164c14ba303563542b7baf2 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:37:26 +0800 Subject: [PATCH 20/27] {"schema":"decodex/commit/1","summary":"Normalize dependency style","authority":"manual"} --- .github/workflows/release.yml | 12 ++++----- Cargo.toml | 30 +++++++++++++++++++++ packages/voxit-audio/Cargo.toml | 8 +++--- packages/voxit-core/Cargo.toml | 42 +++++++++++++++--------------- packages/voxit-host-ffi/Cargo.toml | 6 ++--- packages/voxit-macos/Cargo.toml | 10 +++---- 6 files changed, 69 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ad0edb..44a9e7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,10 @@ jobs: ] steps: - name: Fetch latest code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1 with: cache: true components: rustfmt, clippy @@ -71,7 +71,7 @@ jobs: # tar -czvf name_placeholder-${{ matrix.target.name }}.tar.gz name_placeholder - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: voxit-${{ matrix.target.name }} path: voxit-${{ matrix.target.name }}.zip @@ -82,7 +82,7 @@ jobs: # runs-on: ubuntu-latest # steps: # - name: Publish - # uses: softprops/action-gh-release@v2 + # uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 # with: # discussion_category_name: Announcements # generate_release_notes: true @@ -93,7 +93,7 @@ jobs: needs: [build] steps: - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - name: Hash run: | @@ -106,7 +106,7 @@ jobs: mv ../MD5 . - name: Publish - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: discussion_category_name: Announcements generate_release_notes: true diff --git a/Cargo.toml b/Cargo.toml index 81ecb79..9f722e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,36 @@ readme = "README.md" repository = "https://github.com/hack-ink/voxit" version = "0.1.0" +[workspace.dependencies] +base64 = { version = "0.22" } +block2 = { version = "0.6.2" } +core-foundation = { version = "0.10" } +core-foundation-sys = { version = "0.8" } +coreaudio = { package = "coreaudio-rs", version = "0.14" } +directories = { version = "6.0" } +futures-util = { version = "0.3" } +hound = { version = "3.5" } +http = { version = "1.3" } +keyring = { version = "3.6", features = ["apple-native"] } +objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSRunningApplication"] } +objc2-audio-toolbox = { version = "0.3" } +objc2-av-foundation = { version = "0.3.2", default-features = false, features = ["AVCaptureDevice", "AVMediaFormat", "block2"] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSObjCRuntime", "NSObject", "NSString", "alloc", "std"] } +objc2-local-authentication = { version = "0.3.2", default-features = false, features = ["LAContext", "alloc", "std"] } +reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } +security-framework-sys = { version = "2.16" } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +sha2 = { version = "0.10" } +tokio = { version = "1.47", features = ["rt-multi-thread", "time"] } +tokio-tungstenite = { version = "0.28", default-features = false, features = ["connect", "rustls-tls-native-roots"] } +tracing = { version = "0.1" } +url = { version = "2.5" } +voxit-audio = { version = "0.1.0", path = "packages/voxit-audio" } +voxit-core = { version = "0.1.0", path = "packages/voxit-core" } +voxit-macos = { version = "0.1.0", path = "packages/voxit-macos" } +webbrowser = { version = "1.1" } + [profile.final-release] inherits = "release" lto = true diff --git a/packages/voxit-audio/Cargo.toml b/packages/voxit-audio/Cargo.toml index b0886e8..f26e563 100644 --- a/packages/voxit-audio/Cargo.toml +++ b/packages/voxit-audio/Cargo.toml @@ -10,9 +10,9 @@ repository.workspace = true version.workspace = true [dependencies] -hound = { version = "3.5" } -tracing = { version = "0.1" } +hound = { workspace = true } +tracing = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -coreaudio = { package = "coreaudio-rs", version = "0.14" } -objc2-audio-toolbox = { version = "0.3" } +coreaudio = { workspace = true } +objc2-audio-toolbox = { workspace = true } diff --git a/packages/voxit-core/Cargo.toml b/packages/voxit-core/Cargo.toml index 829fccf..b6d5eb5 100644 --- a/packages/voxit-core/Cargo.toml +++ b/packages/voxit-core/Cargo.toml @@ -14,26 +14,26 @@ default = ["voxit-realtime"] voxit-realtime = ["dep:futures-util", "dep:http", "dep:tokio", "dep:tokio-tungstenite"] [dependencies] -base64 = { version = "0.22" } -directories = { version = "6.0" } -futures-util = { version = "0.3", optional = true } -hound = { version = "3.5" } -http = { version = "1.3", optional = true } -keyring = { version = "3.6", features = ["apple-native"] } -reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -sha2 = { version = "0.10" } -tokio = { version = "1.47", features = ["rt-multi-thread", "time"], optional = true } -tokio-tungstenite = { version = "0.28", default-features = false, features = ["connect", "rustls-tls-native-roots"], optional = true } -tracing = { version = "0.1" } -url = { version = "2.5" } -voxit-audio = { path = "../voxit-audio" } -webbrowser = { version = "1.1" } +base64 = { workspace = true } +directories = { workspace = true } +futures-util = { workspace = true, optional = true } +hound = { workspace = true } +http = { workspace = true, optional = true } +keyring = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +tokio = { workspace = true, optional = true } +tokio-tungstenite = { workspace = true, optional = true } +tracing = { workspace = true } +url = { workspace = true } +voxit-audio = { workspace = true } +webbrowser = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { version = "0.10" } -core-foundation-sys = { version = "0.8" } -objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSObjCRuntime", "NSObject", "NSString", "alloc", "std"] } -objc2-local-authentication = { version = "0.3.2", default-features = false, features = ["LAContext", "alloc", "std"] } -security-framework-sys = { version = "2.16" } +core-foundation = { workspace = true } +core-foundation-sys = { workspace = true } +objc2-foundation = { workspace = true } +objc2-local-authentication = { workspace = true } +security-framework-sys = { workspace = true } diff --git a/packages/voxit-host-ffi/Cargo.toml b/packages/voxit-host-ffi/Cargo.toml index 53e86e1..fc2b3e7 100644 --- a/packages/voxit-host-ffi/Cargo.toml +++ b/packages/voxit-host-ffi/Cargo.toml @@ -14,8 +14,8 @@ crate-type = ["rlib", "staticlib"] path = "src/lib.rs" [dependencies] -voxit-audio = { path = "../voxit-audio" } -voxit-core = { path = "../voxit-core" } +voxit-audio = { workspace = true } +voxit-core = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -voxit-macos = { path = "../voxit-macos" } +voxit-macos = { workspace = true } diff --git a/packages/voxit-macos/Cargo.toml b/packages/voxit-macos/Cargo.toml index e07c6d0..7223e52 100644 --- a/packages/voxit-macos/Cargo.toml +++ b/packages/voxit-macos/Cargo.toml @@ -10,10 +10,10 @@ repository.workspace = true version.workspace = true [dependencies] -tracing = { version = "0.1" } -url = { version = "2.5" } +tracing = { workspace = true } +url = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -block2 = "0.6.2" -objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSRunningApplication"] } -objc2-av-foundation = { version = "0.3.2", default-features = false, features = ["AVCaptureDevice", "AVMediaFormat", "block2"] } +block2 = { workspace = true } +objc2-app-kit = { workspace = true } +objc2-av-foundation = { workspace = true } From 877d2880bad7654b3582a0a3a469aab40ee8fab2 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 21:42:32 +0800 Subject: [PATCH 21/27] {"schema":"decodex/commit/1","summary":"Roll dependencies","authority":"manual"} --- .github/workflows/release.yml | 12 +- Cargo.lock | 452 +++++++++++++++------------------- Cargo.toml | 8 +- 3 files changed, 209 insertions(+), 263 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44a9e7b..9b56b87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,10 @@ jobs: ] steps: - name: Fetch latest code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1 + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: cache: true components: rustfmt, clippy @@ -71,7 +71,7 @@ jobs: # tar -czvf name_placeholder-${{ matrix.target.name }}.tar.gz name_placeholder - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: voxit-${{ matrix.target.name }} path: voxit-${{ matrix.target.name }}.zip @@ -82,7 +82,7 @@ jobs: # runs-on: ubuntu-latest # steps: # - name: Publish - # uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + # uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 # with: # discussion_category_name: Announcements # generate_release_notes: true @@ -93,7 +93,7 @@ jobs: needs: [build] steps: - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - name: Hash run: | @@ -106,7 +106,7 @@ jobs: mv ../MD5 . - name: Publish - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: discussion_category_name: Announcements generate_release_notes: true diff --git a/Cargo.lock b/Cargo.lock index d3f9341..e9bcedf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -20,9 +20,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -38,9 +38,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -74,9 +74,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -84,12 +84,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -104,9 +98,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -149,9 +143,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreaudio-rs" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15c3c3cee7c087938f7ad1c3098840b3ef1f1bdc7f6e496336c3b1e7a6f3914" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ "bitflags", "libc", @@ -182,9 +176,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "digest" @@ -410,9 +404,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -423,7 +417,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -431,15 +424,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -470,12 +462,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -483,9 +476,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -496,9 +489,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -510,15 +503,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -530,15 +523,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -562,9 +555,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -572,47 +565,64 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -626,10 +636,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -648,25 +660,24 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", ] [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" @@ -704,9 +715,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -829,9 +840,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl-probe" @@ -857,17 +868,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -904,7 +909,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -912,9 +917,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -926,7 +931,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -948,9 +953,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -963,9 +968,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -998,14 +1003,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", @@ -1057,15 +1062,24 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -1089,9 +1103,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -1099,9 +1113,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -1126,9 +1140,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -1153,9 +1167,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -1196,6 +1210,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1267,6 +1287,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1281,12 +1317,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1332,33 +1368,13 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1374,9 +1390,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1384,9 +1400,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1399,9 +1415,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -1454,20 +1470,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -1534,15 +1550,15 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror", "utf-8", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -1676,18 +1692,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -1698,23 +1714,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.63" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1722,9 +1734,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -1735,18 +1747,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.90" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -1764,9 +1776,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", "jni", @@ -1780,9 +1792,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -1802,15 +1814,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -1838,21 +1841,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -1886,12 +1874,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1904,12 +1886,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1922,12 +1898,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1952,12 +1922,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1970,12 +1934,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1988,12 +1946,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2006,12 +1958,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2026,21 +1972,21 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -2049,9 +1995,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -2061,18 +2007,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -2081,18 +2027,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -2108,9 +2054,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -2119,9 +2065,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -2130,9 +2076,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9f722e2..81eceef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ coreaudio = { package = "coreaudio-rs", version = "0.14" } directories = { version = "6.0" } futures-util = { version = "0.3" } hound = { version = "3.5" } -http = { version = "1.3" } +http = { version = "1.4" } keyring = { version = "3.6", features = ["apple-native"] } objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSRunningApplication"] } objc2-audio-toolbox = { version = "0.3" } @@ -31,18 +31,18 @@ objc2-av-foundation = { version = "0.3.2", default-features = false, feat objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSObjCRuntime", "NSObject", "NSString", "alloc", "std"] } objc2-local-authentication = { version = "0.3.2", default-features = false, features = ["LAContext", "alloc", "std"] } reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } -security-framework-sys = { version = "2.16" } +security-framework-sys = { version = "2.17" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } sha2 = { version = "0.10" } -tokio = { version = "1.47", features = ["rt-multi-thread", "time"] } +tokio = { version = "1.52", features = ["rt-multi-thread", "time"] } tokio-tungstenite = { version = "0.28", default-features = false, features = ["connect", "rustls-tls-native-roots"] } tracing = { version = "0.1" } url = { version = "2.5" } voxit-audio = { version = "0.1.0", path = "packages/voxit-audio" } voxit-core = { version = "0.1.0", path = "packages/voxit-core" } voxit-macos = { version = "0.1.0", path = "packages/voxit-macos" } -webbrowser = { version = "1.1" } +webbrowser = { version = "1.2" } [profile.final-release] inherits = "release" From 584f4c29f66893974f2f6f1290bc6fa0bbc9e777 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:02:18 +0800 Subject: [PATCH 22/27] {"schema":"decodex/commit/1","summary":"Clean up release workflow metadata","authority":"manual"} --- .github/rulesets/any.json | 79 ------------------- .github/rulesets/default.json | 138 ---------------------------------- .github/workflows/release.yml | 13 +--- 3 files changed, 3 insertions(+), 227 deletions(-) delete mode 100644 .github/rulesets/any.json delete mode 100644 .github/rulesets/default.json diff --git a/.github/rulesets/any.json b/.github/rulesets/any.json deleted file mode 100644 index 8a00e58..0000000 --- a/.github/rulesets/any.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "id": 12430557, - "name": "any", - "target": "branch", - "source_type": "Repository", - "source": "hack-ink/ELF", - "enforcement": "active", - "conditions": { - "ref_name": { - "exclude": [], - "include": [ - "~ALL" - ] - } - }, - "rules": [ - { - "type": "required_signatures" - }, - { - "type": "code_scanning", - "parameters": { - "code_scanning_tools": [ - { - "tool": "CodeQL", - "security_alerts_threshold": "high_or_higher", - "alerts_threshold": "errors" - } - ] - } - }, - { - "type": "code_quality", - "parameters": { - "severity": "errors" - } - }, - { - "type": "copilot_code_review", - "parameters": { - "review_on_push": true, - "review_draft_pull_requests": true - } - }, - { - "type": "copilot_code_review_analysis_tools", - "parameters": { - "tools": [ - { - "name": "CodeQL" - }, - { - "name": "ESLint" - }, - { - "name": "PMD" - } - ] - } - } - ], - "bypass_actors": [ - { - "actor_id": null, - "actor_type": "OrganizationAdmin", - "bypass_mode": "always" - }, - { - "actor_id": null, - "actor_type": "DeployKey", - "bypass_mode": "always" - }, - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - } - ] -} \ No newline at end of file diff --git a/.github/rulesets/default.json b/.github/rulesets/default.json deleted file mode 100644 index c2abc4a..0000000 --- a/.github/rulesets/default.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "id": 12430561, - "name": "default", - "target": "branch", - "source_type": "Repository", - "source": "hack-ink/ELF", - "enforcement": "active", - "conditions": { - "ref_name": { - "exclude": [], - "include": [ - "~DEFAULT_BRANCH" - ] - } - }, - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "creation" - }, - { - "type": "required_linear_history" - }, - { - "type": "merge_queue", - "parameters": { - "merge_method": "SQUASH", - "max_entries_to_build": 5, - "min_entries_to_merge": 1, - "max_entries_to_merge": 5, - "min_entries_to_merge_wait_minutes": 5, - "grouping_strategy": "ALLGREEN", - "check_response_timeout_minutes": 60 - } - }, - { - "type": "required_signatures" - }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": true, - "required_reviewers": [], - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": true, - "allowed_merge_methods": [ - "squash" - ] - } - }, - { - "type": "required_status_checks", - "parameters": { - "strict_required_status_checks_policy": false, - "do_not_enforce_on_create": false, - "required_status_checks": [ - { - "context": "Task cargo clippy", - "integration_id": 15368 - }, - { - "context": "Task cargo fmt", - "integration_id": 15368 - }, - { - "context": "Task cargo nextest", - "integration_id": 15368 - } - ] - } - }, - { - "type": "code_scanning", - "parameters": { - "code_scanning_tools": [ - { - "tool": "CodeQL", - "security_alerts_threshold": "high_or_higher", - "alerts_threshold": "errors" - } - ] - } - }, - { - "type": "code_quality", - "parameters": { - "severity": "errors" - } - }, - { - "type": "copilot_code_review_analysis_tools", - "parameters": { - "tools": [ - { - "name": "CodeQL" - }, - { - "name": "ESLint" - }, - { - "name": "PMD" - } - ] - } - }, - { - "type": "copilot_code_review", - "parameters": { - "review_on_push": true, - "review_draft_pull_requests": false - } - } - ], - "bypass_actors": [ - { - "actor_id": null, - "actor_type": "OrganizationAdmin", - "bypass_mode": "always" - }, - { - "actor_id": null, - "actor_type": "DeployKey", - "bypass_mode": "always" - }, - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - } - ] -} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b56b87..52a9cf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,16 +94,9 @@ jobs: steps: - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - - - name: Hash - run: | - mkdir -p artifacts - mv voxit-*/* artifacts/ - cd artifacts - sha256sum ./* | tee ../SHA256 - md5sum ./* | tee ../MD5 - mv ../SHA256 . - mv ../MD5 . + with: + path: artifacts + merge-multiple: true - name: Publish uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 From a431c752d2f9922cd370b32483a55f59102e8aba Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:06:43 +0800 Subject: [PATCH 23/27] {"schema":"decodex/commit/1","summary":"Fix non-macOS audio build","authority":"manual"} --- packages/voxit-audio/Cargo.toml | 6 ++---- packages/voxit-audio/src/lib.rs | 15 +++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/voxit-audio/Cargo.toml b/packages/voxit-audio/Cargo.toml index f26e563..f2f8367 100644 --- a/packages/voxit-audio/Cargo.toml +++ b/packages/voxit-audio/Cargo.toml @@ -9,10 +9,8 @@ readme.workspace = true repository.workspace = true version.workspace = true -[dependencies] -hound = { workspace = true } -tracing = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] coreaudio = { workspace = true } +hound = { workspace = true } objc2-audio-toolbox = { workspace = true } +tracing = { workspace = true } diff --git a/packages/voxit-audio/src/lib.rs b/packages/voxit-audio/src/lib.rs index 87839a8..d084ee3 100644 --- a/packages/voxit-audio/src/lib.rs +++ b/packages/voxit-audio/src/lib.rs @@ -1,13 +1,10 @@ //! Audio capture primitives for Voxit. +use std::sync::mpsc::Receiver; +#[cfg(target_os = "macos")] use std::{ io::Cursor, - sync::{ - Arc, Mutex, - atomic::AtomicU64, - mpsc, - mpsc::{Receiver, SyncSender}, - }, + sync::{Arc, Mutex, atomic::AtomicU64, mpsc, mpsc::SyncSender}, time::Instant, }; @@ -18,8 +15,8 @@ use coreaudio::audio_unit::{ macos_helpers, render_callback::{Args, data::Interleaved}, }; -use coreaudio::error::Error; -use hound::{WavSpec, WavWriter}; +#[cfg(target_os = "macos")] use coreaudio::error::Error; +#[cfg(target_os = "macos")] use hound::{WavSpec, WavWriter}; #[cfg(target_os = "macos")] use objc2_audio_toolbox::{ kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, @@ -95,6 +92,7 @@ pub struct Recorder { #[cfg(not(target_os = "macos"))] #[derive(Debug)] +/// Stub recording handle for unsupported platforms. pub struct Recorder; #[cfg(target_os = "macos")] impl Recorder { @@ -234,6 +232,7 @@ impl Recorder { #[cfg(not(target_os = "macos"))] #[derive(Debug)] +/// Stub recording payload for unsupported platforms. pub struct Recording; #[cfg(not(target_os = "macos"))] From 21b61f25e811112070d1926056331f3e0140a8bf Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:09:45 +0800 Subject: [PATCH 24/27] {"schema":"decodex/commit/1","summary":"Fix non-macOS platform build","authority":"manual"} --- packages/voxit-macos/Cargo.toml | 6 ++---- packages/voxit-macos/src/lib.rs | 26 ++++++-------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/voxit-macos/Cargo.toml b/packages/voxit-macos/Cargo.toml index 7223e52..62346e7 100644 --- a/packages/voxit-macos/Cargo.toml +++ b/packages/voxit-macos/Cargo.toml @@ -9,11 +9,9 @@ readme.workspace = true repository.workspace = true version.workspace = true -[dependencies] -tracing = { workspace = true } -url = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] block2 = { workspace = true } objc2-app-kit = { workspace = true } objc2-av-foundation = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } diff --git a/packages/voxit-macos/src/lib.rs b/packages/voxit-macos/src/lib.rs index cfc2d14..75a6296 100644 --- a/packages/voxit-macos/src/lib.rs +++ b/packages/voxit-macos/src/lib.rs @@ -1,8 +1,8 @@ //! macOS target app capture and activation helpers. -#[cfg(not(target_os = "macos"))] use std::io::{self, Error, ErrorKind}; #[cfg(target_os = "macos")] use std::ptr; -use std::{ffi::c_void, mem, thread, time::Duration}; +use std::time::Duration; +#[cfg(target_os = "macos")] use std::{ffi::c_void, mem, thread}; #[cfg(target_os = "macos")] use std::{ io::Write as _, process::{Command, Stdio}, @@ -13,7 +13,7 @@ use std::{ffi::c_void, mem, thread, time::Duration}; use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; #[cfg(target_os = "macos")] use objc2_av_foundation::{AVAuthorizationStatus, AVCaptureDevice, AVMediaTypeAudio}; -use url::Url; +#[cfg(target_os = "macos")] use url::Url; #[cfg(target_os = "macos")] type CfDictionaryRef = *const c_void; @@ -50,6 +50,7 @@ impl TargetApp { && !self.selected_text_present } + #[cfg(target_os = "macos")] fn log_id(&self) -> String { if let Some(bundle_id) = self.bundle_id.as_ref().filter(|value| !value.is_empty()) { return bundle_id.clone(); @@ -62,6 +63,7 @@ impl TargetApp { "unknown".to_string() } + #[cfg(target_os = "macos")] fn matches(&self, other: &Self) -> bool { if self.is_empty() || other.is_empty() { return false; @@ -272,8 +274,6 @@ pub fn capture_frontmost_app() -> Option { /// Capture the frontmost app from non-macOS builds. #[cfg(not(target_os = "macos"))] pub fn capture_frontmost_app() -> Option { - let _ = Error::new(ErrorKind::Unsupported, "frontmost capture is macOS-only"); - None } @@ -393,11 +393,6 @@ fn capture_frontmost_app_impl() -> Result { }) } -#[cfg(not(target_os = "macos"))] -fn capture_frontmost_app_impl() -> Result { - Err("frontmost capture is not available on non-macOS".to_string()) -} - #[cfg(target_os = "macos")] fn activation_script(target: &TargetApp) -> Option { if let Some(bundle_id) = target.bundle_id.as_deref().filter(|value| !value.is_empty()) { @@ -407,11 +402,6 @@ fn activation_script(target: &TargetApp) -> Option { target.app_name.as_deref().filter(|value| !value.is_empty()).map(activation_script_for_app_name) } -#[cfg(not(target_os = "macos"))] -fn activation_script(_target: &TargetApp) -> Option { - None -} - #[cfg(target_os = "macos")] fn execute_applescript_raw(script: &str) -> Result { let output = Command::new("osascript") @@ -492,17 +482,13 @@ fn browser_url_script(bundle_id: Option<&str>, app_name: Option<&str>) -> Option None } +#[cfg(target_os = "macos")] fn parse_domain(raw_url: &str) -> Option { let url = Url::parse(raw_url.trim()).ok()?; url.host_str().map(|domain| domain.trim_start_matches("www.").to_ascii_lowercase()) } -#[cfg(not(target_os = "macos"))] -fn execute_applescript_raw(_script: &str) -> Result { - Err("activation is not available on non-macOS".to_string()) -} - #[cfg(target_os = "macos")] fn activation_script_for_bundle_id(bundle_id: &str) -> String { let escaped = escape_applescript_string(bundle_id); From 80abb459cb2e409fcf4f9d45465887a7ec451179 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:15:06 +0800 Subject: [PATCH 25/27] {"schema":"decodex/commit/1","summary":"Fix non-macOS core build","authority":"manual"} --- packages/voxit-core/Cargo.toml | 2 +- packages/voxit-core/src/inference.rs | 12 +++++++---- packages/voxit-core/src/realtime.rs | 32 +++++++++++----------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/voxit-core/Cargo.toml b/packages/voxit-core/Cargo.toml index b6d5eb5..f71da16 100644 --- a/packages/voxit-core/Cargo.toml +++ b/packages/voxit-core/Cargo.toml @@ -17,7 +17,6 @@ voxit-realtime = ["dep:futures-util", "dep:http", "dep:tokio", "dep:tokio-tungst base64 = { workspace = true } directories = { workspace = true } futures-util = { workspace = true, optional = true } -hound = { workspace = true } http = { workspace = true, optional = true } keyring = { workspace = true } reqwest = { workspace = true } @@ -34,6 +33,7 @@ webbrowser = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = { workspace = true } core-foundation-sys = { workspace = true } +hound = { workspace = true } objc2-foundation = { workspace = true } objc2-local-authentication = { workspace = true } security-framework-sys = { workspace = true } diff --git a/packages/voxit-core/src/inference.rs b/packages/voxit-core/src/inference.rs index 99a4c37..7f5b3ec 100644 --- a/packages/voxit-core/src/inference.rs +++ b/packages/voxit-core/src/inference.rs @@ -4,10 +4,14 @@ use std::sync::mpsc::{Receiver, Sender}; #[cfg(target_os = "macos")] use std::{collections::BTreeMap, time::Instant}; #[cfg(target_os = "macos")] -use crate::providers::{self, InferenceProvider, RewriteRequest, TranscriptionRequest}; use crate::{ - ContextualVoiceRouter, FocusedAppContext, VoiceSessionPlan, - providers::chatgpt::ChatGptProvider, + ContextualVoiceRouter, FocusedAppContext, + providers::{ + self, InferenceProvider, RewriteRequest, TranscriptionRequest, chatgpt::ChatGptProvider, + }, +}; +use crate::{ + VoiceSessionPlan, realtime::{RealtimeError, RealtimeEvent, RealtimeSession, RealtimeSessionConfig}, }; use voxit_audio::AudioChunk; @@ -103,7 +107,7 @@ pub fn start_realtime_session( let _ = chunk_rx; let _ = event_tx; - Err(RealtimeError::DependencyUnavailable { + Err(RealtimeError::RuntimeError { reason: "inference pipeline is only enabled on macOS builds".to_string(), }) } diff --git a/packages/voxit-core/src/realtime.rs b/packages/voxit-core/src/realtime.rs index 8d43399..01c0a11 100644 --- a/packages/voxit-core/src/realtime.rs +++ b/packages/voxit-core/src/realtime.rs @@ -1,24 +1,18 @@ //! Realtime transcription session helpers for Pass1 streaming. -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] use std::time::Duration; use std::{ fmt::{Display, Formatter}, - sync::{ - mpsc, - mpsc::{Receiver, Sender}, - }, - thread::{self, JoinHandle}, + sync::mpsc::{Receiver, Sender}, + thread::JoinHandle, }; +#[cfg(feature = "voxit-realtime")] use std::{sync::mpsc, thread, time::Duration}; use base64::{Engine, engine::general_purpose::STANDARD}; -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] -use futures_util::{SinkExt as _, StreamExt as _}; -use http::Request; +#[cfg(feature = "voxit-realtime")] use futures_util::{SinkExt as _, StreamExt as _}; +#[cfg(feature = "voxit-realtime")] use http::Request; use serde_json::Value; -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] use tokio::runtime::Runtime; -use tokio::time; -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] -use tokio_tungstenite::tungstenite::protocol::Message; +#[cfg(feature = "voxit-realtime")] use tokio::{runtime::Runtime, time}; +#[cfg(feature = "voxit-realtime")] use tokio_tungstenite::tungstenite::protocol::Message; use crate::transcript::TranscriptEvent; use voxit_audio::AudioChunk; @@ -82,7 +76,7 @@ pub enum RealtimeEvent { #[derive(Clone, Debug)] pub enum RealtimeError { /// Required websocket client feature is not enabled for this build. - #[cfg(not(all(target_os = "macos", feature = "voxit-realtime")))] + #[cfg(not(feature = "voxit-realtime"))] DependencyUnavailable { /// Human-readable reason. reason: String, @@ -97,7 +91,7 @@ impl Display for RealtimeError { /// Format error to string. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - #[cfg(not(all(target_os = "macos", feature = "voxit-realtime")))] + #[cfg(not(feature = "voxit-realtime"))] Self::DependencyUnavailable { reason } => write!(f, "{reason}"), Self::RuntimeError { reason } => write!(f, "{reason}"), } @@ -105,7 +99,7 @@ impl Display for RealtimeError { } /// Start a Pass1 websocket session and stream chunks to OpenAI Realtime. -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] +#[cfg(feature = "voxit-realtime")] pub fn start_realtime_session( api_key: String, account_id: Option, @@ -117,7 +111,7 @@ pub fn start_realtime_session( } /// Start a Pass1 websocket session and stream chunks to OpenAI Realtime. -#[cfg(not(all(target_os = "macos", feature = "voxit-realtime")))] +#[cfg(not(feature = "voxit-realtime"))] pub fn start_realtime_session( api_key: String, account_id: Option, @@ -136,7 +130,7 @@ pub fn start_realtime_session( }) } -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] +#[cfg(feature = "voxit-realtime")] fn start_realtime_session_impl( api_key: String, account_id: Option, @@ -152,7 +146,7 @@ fn start_realtime_session_impl( Ok(RealtimeSession { stop_tx: Some(stop_tx), worker: Some(worker) }) } -#[cfg(all(target_os = "macos", feature = "voxit-realtime"))] +#[cfg(feature = "voxit-realtime")] fn run_realtime_worker( api_key: String, account_id: Option, From d00da5ae2d6129d43100ebc3b67158a2f2ca0e4c Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:17:35 +0800 Subject: [PATCH 26/27] {"schema":"decodex/commit/1","summary":"Fix non-macOS host FFI build","authority":"manual"} --- packages/voxit-host-ffi/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index cec6e4f..35ffaaa 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -9,7 +9,7 @@ use std::{ ptr::{self, NonNull}, }; -use voxit_audio::Recorder; +#[cfg(target_os = "macos")] use voxit_audio::Recorder; use voxit_core::{ self, Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, RewriteSettings, VoiceSessionPlan, @@ -18,7 +18,7 @@ use voxit_core::{ }, ui_model::{AuthMethod, AuthSurfaceState, DictationSurfaceState, HotkeySurfaceMode}, }; -use voxit_macos::TargetApp; +#[cfg(target_os = "macos")] use voxit_macos::TargetApp; /// ABI version exported by the thin C host bridge. pub const VOXIT_HOST_FFI_ABI_VERSION: u32 = 4; From 45c1d33f80f58b1e5de624695c6c2d92e145c7c2 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Sat, 9 May 2026 22:19:44 +0800 Subject: [PATCH 27/27] {"schema":"decodex/commit/1","summary":"Fix host FFI target dependencies","authority":"manual"} --- packages/voxit-host-ffi/Cargo.toml | 4 ++-- packages/voxit-host-ffi/src/lib.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/voxit-host-ffi/Cargo.toml b/packages/voxit-host-ffi/Cargo.toml index fc2b3e7..fa743f0 100644 --- a/packages/voxit-host-ffi/Cargo.toml +++ b/packages/voxit-host-ffi/Cargo.toml @@ -14,8 +14,8 @@ crate-type = ["rlib", "staticlib"] path = "src/lib.rs" [dependencies] -voxit-audio = { workspace = true } -voxit-core = { workspace = true } +voxit-core = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] +voxit-audio = { workspace = true } voxit-macos = { workspace = true } diff --git a/packages/voxit-host-ffi/src/lib.rs b/packages/voxit-host-ffi/src/lib.rs index 35ffaaa..3ee054d 100644 --- a/packages/voxit-host-ffi/src/lib.rs +++ b/packages/voxit-host-ffi/src/lib.rs @@ -10,9 +10,10 @@ use std::{ }; #[cfg(target_os = "macos")] use voxit_audio::Recorder; +#[cfg(target_os = "macos")] use voxit_core::RewriteSettings; use voxit_core::{ self, Config, ContextualVoiceRouter, FocusedAppContext, NativeHostSnapshot, PlatformHost, - RewriteSettings, VoiceSessionPlan, + VoiceSessionPlan, contextual::{ PromptProfileKind, VoiceInteractionTier, VoiceOutputPolicy, VoiceReasoningEffort, }, @@ -778,6 +779,7 @@ fn set_error(handle: &mut VoxitHostSessionHandle, message: impl Into) { handle.last_error = message.into(); } +#[cfg(target_os = "macos")] fn rewrite_settings(handle: &VoxitHostSessionHandle) -> RewriteSettings { RewriteSettings { guard_protected_tokens: handle.config.rewrite.guard_numbers,