Status: Deferred from the 2026-04-23 refactor session. Wave 1 (hardener rule trait, loop_handlers decide/execute split, ui_bridge partial split, endpoint registry) and Wave 2A POC (5 modules converted to Tauri plugins) are already landed on main. This plan covers the remaining three workstreams.
Baseline commits:
61fb15f83— Wave 1 structural refactorsc74ea498d— Commands plugin migration, 5 modules
Scope: src-tauri/src/mcp/ui_bridge/mod.rs is currently ~10,078 lines after Wave 1. It still holds ~150 handlers plus routes() and route_manifest(). Extract the remaining handler families into submodules.
Families remaining (priority order — smallest and most self-contained first):
errors.rs— error_snapshots, error_report, error_sessions (start/end/list), error_baselines (capture/compare), diagnostics, readiness, diagnose_stuck, page_health (~10 handlers).network.rs— network_requests, in_flight, wait, single, network_chains, browser_events, timeline, console_errors (+ clear), performance_entries (~10 handlers).exploration.rs— start/status/results/stop_ui_bridge_exploration, discover_states_from_renders, list_windows (~6 handlers).screenshots.rs— annotated_screenshot, capture_, apply_annotation, annotations CRUD, media/ find/audit/snapshot, element_images, image-diff, element_screenshot (~20 handlers).page.rs— navigate, refresh, hard_refresh, close, back, forward, evaluate (+raw/safe/batch), set_tab, activate_tab, navigate_and_wait, summary, query_selector, scroll (~15 handlers).elements.rs— get_elements, get_element, assert_element, execute_action, batch_actions, wait_for_element (+ variants), find, find_by_text, click_by_text, click_by_selector, read_value, type_into (~18 handlers).ai.rs— ai_search, ai_find, ai_execute, ai_assert (+ batch), ai_snapshot, ai_summary, ai_semantic_search, ai/diff, ai/analyze/, ai/recovery/, ai/media/*, action_plan cache (~15 handlers).intents.rs— intents/* CRUD + find + execute, state_machine (states/transitions), wait_for_route/navigation/idle/element_state/element_stable/targets (~15 handlers).capabilities.rs— capabilities, keyboard_shortcuts, idle_status, workflow endpoints, render_log, invoke proxy (/commands,/invoke/{name}), control/batch, control/batch-execute, batch, with_diff, execute_action_plan (~15 handlers).
Pattern (proven in Wave 1 passes):
- Each family module exposes
pub fn routes() -> axum::Router<Arc<ApiState>>andpub fn route_entries() -> &'static [(&'static str, &'static str)]. mod.rs::routes()merges submodule routers:.merge(errors::routes()).merge(network::routes())....mod.rs::route_manifest()already derives from per-submoduleroute_entries()via aOnceLock-cached concatenation. Just extend the list as each family is extracted.- Shared helpers already live in
ui_bridge/helpers.rs(extracted Wave 1). Reference viause super::helpers::{...}. - Re-export handlers via
pub use <family>::*;inmod.rsto preserve any external caller paths.
Constraints:
- Do not change URL paths, methods, request/response types.
- Do not collapse
/control/*vs/ai/*alias duplicates in this pass — that's Workstream D below, and it should come after all families are extracted. - Build after each family via
cargo-guard.sh check.
Out-of-scope for this workstream: the manifest_drift_tests test at mod.rs:10067 references a no-longer-existent src/mcp/ui_bridge.rs path; it needs a separate fix to scan all submodule files.
Estimated effort: 3–5 hours, agent-executable in 2 parallel passes (families 1-4 in one agent, 5-9 in another; they don't share files beyond mod.rs which requires serialization on route registration).
Scope: ~85 command modules still use the central tauri::generate_handler![...] block in main.rs. Convert them to the plugin pattern established in commit c74ea498d.
Pattern (documented in src-tauri/src/commands/mod.rs header):
Per module:
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::Runtime;
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("qontinui_<module_name>")
.invoke_handler(tauri::generate_handler![
fn_one,
fn_two,
// ... every pub #[tauri::command]
])
.build()
}Plugin naming: qontinui_<module> to avoid collisions with Tauri's own plugins.
Documented footgun: handlers taking AppHandle / Window / WebviewWindow (not AppHandle<R> / Window<R> / WebviewWindow<R>) must be rewritten with <R: Runtime> generics. The generated handler fixes the runtime to Wry, which doesn't unify with the plugin's R. Any batch conversion must scan for and rewrite these parameters.
Migration batches (suggested grouping, ~15 modules per batch):
Batch 1 — Simple CRUD modules (likely low footgun hit rate):
checkpoints,checks,comparison,container_settings,dag_workflows,database,dataset,debug(done),discoveries,dev_findings(done),event_search,findings,hooks,issues,known_issues.
Batch 2 — Settings modules:
ai_settings,accessibility,execution_variables,mobile_settings,otel_settings,playwright_settings,security_settings,self_healing_settings,web_integration.
Batch 3 — Reporting / read-only:
activity_timeline,agentic_metrics,ai_data,cost_dashboard,learning,performance_metrics,recap,terminal_analysis,token_analytics,transcript.
Batch 4 — Tool-heavy modules:
accessibility,adaptive_learning,ai_generation,ai_session,backup,checkpoint_browser,config,context,library_sync,logging,meta_optimizer,rag.
Batch 5 — Most-state-coupled (do last):
auth,execution(directory — needs special handling for sub-modules),execution_reporting,extraction,interaction,screenshot,screenshots,state_explorer,state_machine,state_machine_configs,storage,testing,ui_bridge,verification,video,websocket,workflow_events,task_sync,global_log_sources,spec_drift,project_logs,step_outputs,test_orchestrator,tiered_info,ui_bridge_baselines,watchers,setup_wizard,shell_commands,script_emitter,mobile,mcp,instances,flow,file_browser(done),clipboard(done),window_manager(done).
Directory-style modules (like commands/execution/) need per-submodule conversion or a flattening into execution::plugin() that handles sub-module handlers — decide per-module at conversion time.
Verification per batch: run cargo-guard.sh check after every module, not just the whole batch. A broken plugin conversion is easier to debug in isolation.
Estimated effort: 2–3 hours per batch, 5 batches = 10–15 hours total. Batches 1–3 are straightforward; 4–5 likely have more footguns.
Scope: src-tauri/src/commands/mod.rs::AppState has 30+ fields mixing Mutex, TokioMutex, Arc<RwLock>, AtomicBool, AtomicU16, OnceCell. Group into cohesive sub-structs.
Proposed compartments:
pub struct BridgeState {
pub bridge_manager: TokioMutex<Option<Arc<BridgeManager>>>,
pub extraction_executor: Mutex<Option<ExtractionExecutor>>,
pub url_lock_manager: Arc<UrlLockManager>,
pub file_registry_manager: Arc<FileRegistryManager>,
pub file_lock_manager: Arc<FileLockManager>,
pub ui_bridge_failure_tracker: UiBridgeFailureTracker,
pub exploration_cancel: Arc<TokioMutex<Option<CancellationToken>>>,
}
pub struct ExecutionState {
pub run_cost_trackers: TokioMutex<HashMap<String, Arc<RunCostTrackers>>>,
pub orchestration_loops: SharedLoopStates,
pub working_representation_cache: Arc<WorkingRepresentationCache>,
pub process_capture_manager: TokioMutex<Option<Arc<ProcessCaptureManager>>>,
pub container_executor: TokioMutex<Option<IsolatedExecutor>>,
pub run_recording_handler: Arc<RunRecordingHandler>,
pub current_config: Mutex<Option<QontinuiConfig>>,
pub ai_pid_tracker: Arc<Mutex<Vec<u32>>>,
pub canvas_state: Arc<RwLock<CanvasState>>,
}
pub struct IntegrationState {
pub sdk_connection: Arc<TokioMutex<SdkConnectionManager>>,
pub mcp_client_manager: TokioMutex<McpClientManager>,
pub server_mode: Arc<RwLock<Option<ServerModeState>>>,
pub token_flow: Arc<TokenFlowStore>,
pub usb_transport: Arc<OnceCell<UsbTransport>>,
pub event_broadcast: broadcast::Sender<serde_json::Value>,
}
pub struct HealthState {
pub doctor_handle: TokioMutex<Option<DoctorHandle>>,
pub error_monitor_handle: TokioMutex<Option<ErrorMonitorHandle>>,
pub crash_dumps: Arc<CrashDumpState>,
pub ui_error: Arc<UiErrorState>,
pub api_ready: AtomicBool,
pub api_port: AtomicU16,
}
pub struct StorageState {
pub pg_db: Arc<PgDb>,
pub local_storage: Arc<Mutex<LocalStorage>>,
pub video_recorder: Arc<Mutex<VideoRecordingService>>,
pub display_processor: Arc<TokioMutex<DisplayProcessor>>,
}
pub struct AppState {
pub bridge: Arc<BridgeState>,
pub execution: Arc<ExecutionState>,
pub integration: Arc<IntegrationState>,
pub health: Arc<HealthState>,
pub storage: Arc<StorageState>,
}Why sequence this AFTER Workstream B:
Every plugin converted under Workstream B becomes an opportunity to take only the compartment it needs — e.g., commands::clipboard::plugin() takes Arc<IntegrationState>, not the whole AppState. Compartmenting now and then converting to plugins later updates every call site twice. Doing them together per module is the only sane sequencing.
Migration per module (bundled with Workstream B):
For each module migrated to a plugin:
- Identify which AppState fields it reads.
- Identify which compartment owns those fields.
- Change
State<'_, Arc<AppState>>toState<'_, Arc<CompartmentState>>in handler signatures. - Update call sites from
state.foo_fieldtostate.foo_field(same, now on compartment). - Ensure main.rs
.manage()calls register each compartment as a separate state.
Constraint: during migration, the old AppState must still delegate-expose its old field access for un-migrated modules. Strategy: keep fields on AppState during migration; the fields become references/clones of the compartment. Remove them only once every consumer is migrated.
Or simpler: AppState derefs to its own fields that are cloned Arc<Compartment>s, and new code accesses via state.bridge.bridge_manager while old code still uses state.bridge_manager until the module migrates. This avoids the two-pass problem by tolerating both during the transition.
Estimated effort: 1–2 hours of compartment-design + tooling-up. Then ~30 extra minutes per module migrated under Workstream B.
Scope: Every -> Result<T, String> internal function. Introduce per-domain error enums, keep Tauri ABI stable via impl From<X> for String.
Why sequence this AFTER Workstream C:
Command signatures will churn during C (state parameter types change). Running D concurrently means two signature rewrites per function. Do C first, then D against settled signatures.
Proposed error hierarchy:
// src-tauri/src/error.rs (expand existing)
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
#[error("{0}")]
BadRequest(String),
#[error("authorization failed: {0}")]
Unauthorized(String),
#[error("not found: {0}")]
NotFound(String),
#[error(transparent)]
Storage(#[from] StorageError),
#[error(transparent)]
Bridge(#[from] BridgeError),
#[error(transparent)]
Workflow(#[from] WorkflowError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<CommandError> for String {
fn from(e: CommandError) -> String { e.to_string() }
}
// Per-domain errors:
#[derive(Debug, thiserror::Error)]
pub enum BridgeError {
#[error("bridge not initialized")]
NotInitialized,
#[error("transport error: {0}")]
Transport(String),
#[error("timeout after {0}ms")]
Timeout(u64),
#[error("assertion failed: {detail}")]
AssertFailed { detail: String },
#[error(transparent)]
Io(#[from] std::io::Error),
}Each compartment (bridge, execution, integration, etc.) gets its own error type. Command signatures become Result<T, CommandError> and the Tauri boundary converts to String automatically.
Rollout (per module, small batches):
- Convert handler signatures module-by-module. Use
?withCommandError::from(...)conversions. - Internal helpers that currently return
Result<T, String>can be migrated eagerly when they clearly sit behind a command boundary. Leave cross-module helpers that returnStringalone until both caller and callee are migrated. - Preserve the exact error message format externally — users or parsers may depend on current strings. Use
#[error("...")]patterns that reproduce the current text.
UiBridgeError already exists at src-tauri/src/mcp/ui_bridge/types.rs with error codes, classification, recovery hints. Don't replace it; bridge from UiBridgeError to BridgeError via a From impl so the rest of the codebase can benefit from the classification.
Estimated effort: 8–12 hours total, spread across multiple sessions. Can be done incrementally — partial migration is fine, as long as the From impls keep the boundary stable.
- Session N+1: Workstream A (finish ui_bridge extraction) + Workstream B Batch 1 in parallel. Low risk, high readability win.
- Session N+2: Workstream C compartment design + AppState delegation shim. Build tooling for per-module migration.
- Session N+3: Workstream B Batch 2–3 migrations, each with Workstream C compartment scoping applied to the migrated module.
- Session N+4: Workstream B Batch 4, continuing C per-module.
- Session N+5: Workstream B Batch 5, wrapping up remaining modules. Delete the central
generate_handler![...]in main.rs. - Session N+6: Workstream C cleanup — drop delegation shim, remove legacy AppState fields.
- Session N+7+: Workstream D (typed errors) in batches.
manifest_drift_testsinsrc-tauri/src/mcp/ui_bridge/mod.rs:10067references a no-longer-existent file path. Fix during Workstream A./control/*vs/ai/*alias collapse — after all ui_bridge families are extracted, introduce analias!(control, ai, path, method, handler)helper and collapse the ~40 duplicate routes.- ui_bridge hardcoded URL literals at
mod.rs:2774, 2785andhelpers.rs:254, 265— should usecrate::api_config::get_ipc_response_url(). Noted by the Wave 1 endpoint-registry agent. - Pre-existing snake-case warning in
src-tauri/src/workflow_generation/hardener/mod.rs:2866(unrelated to Wave 1).