Merge pull request #378 from H-Chris233/fix/issue-296-coordinator-modules
拆分 coordinator 子状态机模块
This commit is contained in:
4 files changed
+1394
-1335
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,124 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::coordinator_state::{initial_session_id, SessionId};
|
||||
use crate::selection::SelectionContext;
|
||||
use crate::types::CapsuleState;
|
||||
|
||||
use super::{
|
||||
begin_qa_session, cancel_qa_session, capture_frontmost_app, emit_capsule, end_qa_session, Inner,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum QaPhase {
|
||||
Idle,
|
||||
Recording,
|
||||
Processing,
|
||||
}
|
||||
|
||||
pub(super) struct QaSessionState {
|
||||
pub(super) phase: QaPhase,
|
||||
pub(super) cancelled: bool,
|
||||
pub(super) selection: Option<SelectionContext>,
|
||||
pub(super) front_app: Option<String>,
|
||||
/// 用于忽略迟到的 RMS / runtime error。
|
||||
pub(super) session_id: SessionId,
|
||||
/// QA 浮窗是否被用户钉住(pinned)。pinned=true 时不自动隐藏。
|
||||
pub(super) pinned: bool,
|
||||
/// 浮窗是否对用户可见。Cmd+Shift+; 边沿 toggle 此 flag;
|
||||
/// 主听写 hotkey(rightOption)边沿来时,看这个 flag 决定是走 QA 还是走 dictation。
|
||||
/// 详见 issue #118 v2。
|
||||
pub(super) panel_visible: bool,
|
||||
/// 多轮对话累积。每轮 user→assistant 加两条;关浮窗清空。
|
||||
pub(super) messages: Vec<crate::types::QaChatMessage>,
|
||||
}
|
||||
|
||||
impl Default for QaSessionState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
phase: QaPhase::Idle,
|
||||
cancelled: false,
|
||||
selection: None,
|
||||
front_app: None,
|
||||
session_id: initial_session_id(),
|
||||
pinned: false,
|
||||
panel_visible: false,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_qa_hotkey_pressed(inner: &Arc<Inner>) {
|
||||
// QA hotkey(默认 Cmd+Shift+;)现在只 toggle 浮窗可见性。
|
||||
// 浮窗内的录音 / 提问由 Option 边沿驱动(handle_pressed_edge → handle_qa_option_edge)。
|
||||
let visible = inner.qa_state.lock().panel_visible;
|
||||
log::info!("[coord] QA hotkey edge (panel_visible={visible})");
|
||||
if visible {
|
||||
close_qa_panel(inner);
|
||||
} else {
|
||||
open_qa_panel(inner);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_qa_option_edge(inner: &Arc<Inner>) {
|
||||
let phase = inner.qa_state.lock().phase;
|
||||
log::info!("[coord] QA option edge (phase={phase:?})");
|
||||
match phase {
|
||||
QaPhase::Idle => {
|
||||
let _ = begin_qa_session(inner).await;
|
||||
}
|
||||
QaPhase::Recording => {
|
||||
let _ = end_qa_session(inner).await;
|
||||
}
|
||||
// Processing 阶段再次按键忽略(避免与正在跑的 LLM 冲突)。
|
||||
QaPhase::Processing => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn open_qa_panel(inner: &Arc<Inner>) {
|
||||
{
|
||||
let mut state = inner.qa_state.lock();
|
||||
state.panel_visible = true;
|
||||
state.phase = QaPhase::Idle;
|
||||
state.cancelled = false;
|
||||
state.messages.clear();
|
||||
state.selection = None;
|
||||
state.front_app = capture_frontmost_app();
|
||||
}
|
||||
// 先把胶囊清干净,避免主听写上一次 Done 状态残留的 message/insertedChars
|
||||
// 在 QA Done 阶段被 capsule UI 错误复用("已之一粘贴这个 0" 那种)。
|
||||
emit_capsule(inner, CapsuleState::Idle, 0.0, 0, None, None);
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::show_qa_window(&app, "idle");
|
||||
let _ = app.emit_to(
|
||||
"qa",
|
||||
"qa:state",
|
||||
serde_json::json!({
|
||||
"kind": "idle",
|
||||
"messages": Vec::<crate::types::QaChatMessage>::new(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
log::info!("[coord] QA panel opened (awaiting Option to record)");
|
||||
}
|
||||
|
||||
pub(super) fn close_qa_panel(inner: &Arc<Inner>) {
|
||||
cancel_qa_session(inner);
|
||||
{
|
||||
let mut state = inner.qa_state.lock();
|
||||
state.panel_visible = false;
|
||||
state.pinned = false;
|
||||
state.messages.clear();
|
||||
state.selection = None;
|
||||
state.front_app = None;
|
||||
state.phase = QaPhase::Idle;
|
||||
state.cancelled = false;
|
||||
}
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::hide_qa_window(&app);
|
||||
}
|
||||
// 胶囊一同收掉,避免浮窗关了胶囊还在显示。
|
||||
emit_capsule(inner, CapsuleState::Idle, 0.0, 0, None, None);
|
||||
log::info!("[coord] QA panel closed, history cleared");
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::coordinator_state::{SessionId, SessionPhase};
|
||||
use crate::recorder::Recorder;
|
||||
use crate::types::CapsuleState;
|
||||
use tauri::Manager;
|
||||
|
||||
use super::{emit_capsule, ActiveAsr, Inner};
|
||||
|
||||
pub(super) struct SessionResource<T> {
|
||||
pub(super) session_id: SessionId,
|
||||
resource: T,
|
||||
}
|
||||
|
||||
impl<T> SessionResource<T> {
|
||||
pub(super) fn new(session_id: SessionId, resource: T) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
resource,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_inner(self) -> T {
|
||||
self.resource
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SharedRecordingMuteState {
|
||||
guard: Option<crate::audio_mute::AudioMuteGuard>,
|
||||
holders: u32,
|
||||
}
|
||||
|
||||
impl SharedRecordingMuteState {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
guard: None,
|
||||
holders: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn take_session_resource<T>(
|
||||
slot: &mut Option<SessionResource<T>>,
|
||||
session_id: SessionId,
|
||||
) -> Option<T> {
|
||||
if slot
|
||||
.as_ref()
|
||||
.map(|resource| resource.session_id == session_id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
slot.take().map(SessionResource::into_inner)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn store_asr_for_session(inner: &Arc<Inner>, session_id: SessionId, asr: ActiveAsr) {
|
||||
*inner.asr.lock() = Some(SessionResource::new(session_id, asr));
|
||||
}
|
||||
|
||||
pub(super) fn take_asr_for_session(inner: &Arc<Inner>, session_id: SessionId) -> Option<ActiveAsr> {
|
||||
let mut slot = inner.asr.lock();
|
||||
take_session_resource(&mut slot, session_id)
|
||||
}
|
||||
|
||||
pub(super) fn cancel_active_asr(asr: ActiveAsr) {
|
||||
match asr {
|
||||
ActiveAsr::Volcengine(v) => v.cancel(),
|
||||
ActiveAsr::Whisper(w) => w.cancel(),
|
||||
#[cfg(target_os = "windows")]
|
||||
ActiveAsr::FoundryLocalWhisper(local) => local.cancel(),
|
||||
#[cfg(target_os = "macos")]
|
||||
ActiveAsr::Local(local) => local.cancel(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cancel_asr_for_session(inner: &Arc<Inner>, session_id: SessionId) {
|
||||
if let Some(asr) = take_asr_for_session(inner, session_id) {
|
||||
cancel_active_asr(asr);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn store_recorder_for_session(
|
||||
inner: &Arc<Inner>,
|
||||
session_id: SessionId,
|
||||
recorder: Recorder,
|
||||
) {
|
||||
*inner.recorder.lock() = Some(SessionResource::new(session_id, recorder));
|
||||
}
|
||||
|
||||
pub(super) fn selected_microphone_device_name(inner: &Arc<Inner>) -> Option<String> {
|
||||
let name = inner.prefs.get().microphone_device_name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn stop_microphone_preview_monitor(inner: &Arc<Inner>, owner: &str) {
|
||||
let Some(app) = inner.app.lock().as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let state = app.state::<crate::commands::MicrophoneMonitorState>();
|
||||
let recorder = state.lock().take();
|
||||
if let Some(recorder) = recorder {
|
||||
log::info!("[recorder] stopping microphone preview monitor before {owner}");
|
||||
recorder.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn acquire_recording_mute(inner: &Arc<Inner>, owner: &str) {
|
||||
if !inner.prefs.get().mute_during_recording {
|
||||
return;
|
||||
}
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
match crate::audio_mute::AudioMuteGuard::activate() {
|
||||
Ok(guard) => {
|
||||
mute.guard = Some(guard);
|
||||
log::info!("[audio-mute] system output muted for recording");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
mute.holders = mute.holders.saturating_add(1);
|
||||
log::info!("[audio-mute] acquired by {owner}; holders={}", mute.holders);
|
||||
}
|
||||
|
||||
pub(super) fn release_recording_mute(inner: &Arc<Inner>, owner: &str) {
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
return;
|
||||
}
|
||||
mute.holders -= 1;
|
||||
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
|
||||
if mute.holders == 0 {
|
||||
mute.guard.take();
|
||||
log::info!("[audio-mute] system output mute restored after recording");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn stop_qa_recorder(inner: &Arc<Inner>) {
|
||||
if let Some(rec) = inner.qa_recorder.lock().take() {
|
||||
rec.stop();
|
||||
release_recording_mute(inner, "qa");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn take_recorder_for_session(
|
||||
inner: &Arc<Inner>,
|
||||
session_id: SessionId,
|
||||
) -> Option<Recorder> {
|
||||
let mut slot = inner.recorder.lock();
|
||||
take_session_resource(&mut slot, session_id)
|
||||
}
|
||||
|
||||
pub(super) fn stop_recorder_for_session(inner: &Arc<Inner>, session_id: SessionId) {
|
||||
if let Some(recorder) = take_recorder_for_session(inner, session_id) {
|
||||
recorder.stop();
|
||||
release_recording_mute(inner, "dictation");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn discard_startup_resources_for_session(inner: &Arc<Inner>, session_id: SessionId) {
|
||||
stop_recorder_for_session(inner, session_id);
|
||||
cancel_asr_for_session(inner, session_id);
|
||||
}
|
||||
|
||||
pub(super) fn stop_recorder_if_pending_start_stop(inner: &Arc<Inner>) {
|
||||
let (should_stop, session_id) = {
|
||||
let state = inner.state.lock();
|
||||
(
|
||||
state.phase == SessionPhase::Starting && state.pending_stop,
|
||||
state.session_id,
|
||||
)
|
||||
};
|
||||
if !should_stop {
|
||||
return;
|
||||
}
|
||||
if let Some(rec) = take_recorder_for_session(inner, session_id) {
|
||||
rec.stop();
|
||||
release_recording_mute(inner, "dictation");
|
||||
let elapsed = inner.state.lock().started_at.elapsed().as_millis() as u64;
|
||||
emit_capsule(inner, CapsuleState::Transcribing, 0.0, elapsed, None, None);
|
||||
log::info!("[coord] stopped recorder while ASR is still connecting");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user