Spaces:
Runtime error
A newer version of the Gradio SDK is available: 6.14.0
T-002: IPC Command Registry β All Typed Serde Structs & TypeScript Mirror Types
Type: Task
Phase: 0 β Foundation
Autonomy: agent:review-required β Agent writes all types and stub handlers. Human reviews PR before merge. A type error here cascades to all 148 remaining tasks.
Stack: stack:rust stack:typescript
Version: v0.1
Iteration: iter-1
Effort: M (1β2 days)
β οΈ Agent Scope: Write the type system and stub command registry only. Every command handler returns
IpcError::NotImplemented. No audio, no models, no cameras, no controllers. Logic arrives in T-003 through T-150. Your deliverable is a compile-clean, fully-typed contract.
β οΈ Contract Rule: Once merged, no type may be renamed, removed, or have a field removed without a new migration sub-issue. Adding
Option<T>fields to existing structs is safe without a migration.
β οΈ Naming: The Tauri backend directory is
kansas/(notsrc-tauri/). All paths below reflect this. See AMENDMENT-001.
Context
Tauri IPC is the nervous system of Synesthesia. Every subsystem communicates with the Rust backend through typed invoke() calls and typed emit() events. If Rust and TypeScript types diverge, the mismatch surfaces as a silent runtime undefined or a JSON parse panic β not a compile error.
This task eliminates that class of bug by defining the complete type surface in one place on both sides, enforcing serde(rename_all = "camelCase") so the JSON wire format matches TypeScript conventions, and providing typed wrappers for every command and event in the full v1 system. Many commands will have stub handlers for months. That is correct. The stubs are compile-time documentation.
Prerequisites
- T-001 merged β
kansas/,src/, workspaceCargo.tomlwithedition = "2024"and wildcard deps exist -
kansas/src/commands/mod.rscontains onlyping()from T-001 -
cargo check --workspacepasses on T-001 output before starting
Acceptance Criteria
Rust
-
cargo check --workspaceβ zero errors -
cargo clippy --workspace -- -D warningsβ zero warnings - Every public type derives
Debug,Clone,Serialize,Deserialize - Every struct/enum has
// [T-NNN]doc comment for the implementing task - All serde types use
#[serde(rename_all = "camelCase")] -
Option<T>fields use#[serde(skip_serializing_if = "Option::is_none")] - All stubs return
Result<_, IpcError>β nounwrap!,panic!,todo!() -
ipcmodule declared inkansas/src/lib.rs
TypeScript
-
pnpm typecheckβ zero errors -
src/ipc/types.tsβ all interfaces match Rust counterparts field-for-field -
src/ipc/commands.tsβ typedinvoke()wrapper for every command -
src/ipc/events.tsβ typedlisten()wrapper for every event - No
anytypes in the IPC layer
Registration
- All commands in the Commands Catalog registered in
tauri::generate_handler![] - All events in the Events Catalog have a TypeScript
listen()wrapper -
pingfrom T-001 preserved and moved into the new module structure
Capability
-
kansas/capabilities/default.jsonlists every new command - No command callable without an explicit capability entry
Serialization Contract
Rule: Rust is snake_case internally. JSON wire format and TypeScript are camelCase.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExamplePayload {
pub some_field: f32, // β "someField"
#[serde(skip_serializing_if = "Option::is_none")]
pub optional_field: Option<u32>,
}
// Unit enum β tagged string
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TransportState { Playing, Paused, Stopped }
// β { "kind": "playing" }
// Data-carrying enum β adjacent tagged
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
pub enum HapticKind {
BeatPulse { intensity: f32 },
ChunkBoundary,
}
// β { "type": "beatPulse", "data": { "intensity": 0.8 } }
File Structure After This Task
kansas/src/
βββ main.rs β registers all commands
βββ lib.rs β declares ipc + commands modules
βββ ipc/
β βββ mod.rs β re-exports types, error
β βββ types.rs β ALL serde structs and enums
β βββ error.rs β IpcError enum
βββ commands/
βββ mod.rs β re-exports all submodules
βββ ping.rs β moved from T-001
βββ transport.rs β stub β T-003
βββ audio.rs β stub β T-004
βββ models.rs β stub β T-046+
βββ camera.rs β stub β T-031+
βββ controllers.rs β stub β T-016+
βββ spatial.rs β stub β T-096+
βββ devices.rs β stub β T-013
βββ app_state.rs β stub β T-048
src/ipc/
βββ types.ts β ALL TypeScript mirror interfaces
βββ commands.ts β typed invoke() wrappers
βββ events.ts β typed listen() wrappers
IPC Error Type
// kansas/src/ipc/error.rs
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Error, Serialize)]
#[serde(rename_all = "camelCase", tag = "kind", content = "message")]
pub enum IpcError {
#[error("Not yet implemented: {0}")]
NotImplemented(String),
#[error("Invalid parameter: {0}")]
InvalidParam(String),
#[error("Device not found: {0}")]
DeviceNotFound(String),
#[error("Model not loaded: {0}")]
ModelNotLoaded(String),
#[error("Audio error: {0}")]
AudioError(String),
#[error("Internal error: {0}")]
Internal(String),
}
impl From<IpcError> for String {
fn from(e: IpcError) -> Self {
serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())
}
}
// src/ipc/types.ts (add at top)
export type IpcErrorKind =
| 'notImplemented' | 'invalidParam' | 'deviceNotFound'
| 'modelNotLoaded' | 'audioError' | 'internal';
export interface IpcError { kind: IpcErrorKind; message: string; }
export function parseIpcError(raw: unknown): IpcError {
if (typeof raw === 'string') {
try { return JSON.parse(raw) as IpcError; } catch { /* fall through */ }
}
return { kind: 'internal', message: String(raw) };
}
Complete Types β Rust (kansas/src/ipc/types.rs)
use serde::{Deserialize, Serialize};
// βββ Primitives βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// 3D vector; units depend on context (cm for head pos, meters for spatial audio)
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vec3 { pub x: f32, pub y: f32, pub z: f32 }
/// Unit quaternion for orientation
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Quaternion { pub x: f32, pub y: f32, pub z: f32, pub w: f32 }
impl Default for Quaternion {
fn default() -> Self { Self { x: 0.0, y: 0.0, z: 0.0, w: 1.0 } }
}
/// RGB color 0β255
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rgb { pub r: u8, pub g: u8, pub b: u8 }
// βββ Transport [T-003] ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-003] Partial update β only set fields that should change
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransportSetCmd {
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<TransportState>,
/// BPM 20.0β300.0
#[serde(skip_serializing_if = "Option::is_none")]
pub bpm: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_sig_numerator: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_sig_denominator: Option<u8>,
}
/// [T-003]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TransportState { #[default] Stopped, Playing, Paused }
/// [T-003] Full snapshot returned by transport_get
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransportStatus {
pub state: TransportState,
pub bpm: f32,
pub time_sig_numerator: u8,
pub time_sig_denominator: u8,
pub bar: u32,
pub beat: u8, // 1-indexed, 1..=time_sig_numerator
pub tick: u32, // 0..=95 (96 PPQN)
pub elapsed_ms: u64,
}
/// [T-003] Emitted every tick by transport thread
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransportTickEvent {
pub bar: u32,
pub beat: u8,
pub tick: u32,
pub bpm: f32,
pub elapsed_ms: u64,
pub is_downbeat: bool,
}
// βββ Parameter Bus [T-024] ββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-024] Set any named parameter. value always 0.0β1.0 normalized.
/// param_id dot-notation: "perfrnn.lead.temperature", "magenta_rt.style_weight.0"
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParamSetCmd {
pub param_id: String,
pub value: f32,
pub source: ParamSource,
}
/// [T-024]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum ParamSource {
Ui,
Controller { id: ControllerId },
Gesture,
HeadTracking,
Internal,
}
/// [T-024] Broadcast when any parameter changes
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParamChangedEvent {
pub param_id: String,
pub value: f32,
pub source: ParamSource,
pub timestamp_ms: u64,
}
// βββ Model Inference [T-046+] βββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-046]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum ModelId {
MusicVaeEncoder, MusicVaeDecoder,
PerfRnnStep,
SpectroStreamEncode, SpectroStreamDecode,
MusicCocaText, MusicCocaAudio,
MagnetaRtGenerate,
DdspEncode, DdspDecode,
GemmaInference,
}
/// [T-046] input is model-specific JSON; handler validates + deserializes
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelInferCmd {
pub model: ModelId,
pub input: serde_json::Value,
}
/// [T-046]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelInferResult {
pub model: ModelId,
pub output: serde_json::Value,
pub latency_ms: f32,
}
/// [T-127] Hot-swap ONNX file without restart
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelReloadCmd {
pub model: ModelId,
pub path: String,
}
/// [T-046] Emitted after inference completes (Rerun logging hook)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelInferenceCompleteEvent {
pub model: ModelId,
pub latency_ms: f32,
pub timestamp_ms: u64,
}
// βββ Devices [T-013] ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-013]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum DeviceKind { AudioInput, AudioOutput, Camera }
/// [T-013]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
pub id: u32,
pub name: String,
pub kind: DeviceKind,
pub is_default: bool,
}
/// [T-013]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceListResult { pub devices: Vec<DeviceInfo> }
// βββ Camera [T-031βT-034] βββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-034]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum CameraRole {
Pose, // Primary: 30 FPS, MediaPipe + Gemma-3N
Head, // Secondary: 120 FPS stereo, head tracking only
}
/// [T-034]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraAssignCmd { pub role: CameraRole, pub device_id: u32 }
/// [T-031]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraAssignment {
#[serde(skip_serializing_if = "Option::is_none")]
pub pose_device_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_device_id: Option<u32>,
}
// βββ Skeleton Pose [T-035βT-038] βββββββββββββββββββββββββββββββββββββββββββββ
/// [T-038] Single MediaPipe BlazePose landmark (full topology: developers.google.com/mediapipe/solutions/vision/pose_landmarker)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PoseLandmark {
pub x: f32, // normalized 0.0 (left) β 1.0 (right)
pub y: f32, // normalized 0.0 (top) β 1.0 (bottom)
pub z: f32, // depth relative to hips, normalized
pub visibility: f32, // confidence 0.0β1.0
}
/// [T-038] 33-landmark skeleton; landmarks[0]=nose, [11]=L shoulder, [16]=R wrist
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkeletonPoseEvent {
pub landmarks: Vec<PoseLandmark>, // always 33 elements
pub timestamp_ms: u64,
pub detected: bool,
}
// βββ Head Tracking [T-039βT-041] βββββββββββββββββββββββββββββββββββββββββββββ
/// [T-041] 6-DOF head pose from secondary 120 FPS stereo camera, broadcast at 120 Hz
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HeadPoseEvent {
pub position: Vec3, // cm from camera origin
pub orientation: Quaternion,
pub yaw_deg: f32,
pub pitch_deg: f32,
pub roll_deg: f32,
pub timestamp_ms: u64,
pub confidence: f32, // 0.0β1.0; below 0.5 treat as unreliable
}
// βββ Controllers [T-016βT-029] ββββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-016]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum ControllerId { DualSense, JoyConLeft, JoyConRight }
/// [T-022] State snapshot broadcast at 60 Hz per connected controller
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerStateEvent {
pub controller: ControllerId,
pub axes: Vec<f32>, // β1.0β1.0, layout controller-specific (T-017, T-018)
pub buttons: Vec<bool>, // layout controller-specific
#[serde(skip_serializing_if = "Option::is_none")]
pub gyro: Option<Vec3>, // rad/s
#[serde(skip_serializing_if = "Option::is_none")]
pub accel: Option<Vec3>, // m/sΒ²
#[serde(skip_serializing_if = "Option::is_none")]
pub left_trigger: Option<f32>, // DualSense only, 0.0β1.0
#[serde(skip_serializing_if = "Option::is_none")]
pub right_trigger: Option<f32>, // DualSense only, 0.0β1.0
#[serde(skip_serializing_if = "Option::is_none")]
pub touchpad: Option<[f32; 2]>, // DualSense only, normalized
pub timestamp_ms: u64,
}
/// [T-019βT-021] Send haptic or LED command to a controller
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HapticCmd { pub controller: ControllerId, pub kind: HapticKind }
/// [T-019βT-021]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
pub enum HapticKind {
BeatPulse { intensity: f32 },
NoteOnset { note: u8, velocity: u8 },
ChunkBoundary,
MoodChange,
TriggerResistance { trigger: TriggerSide, force: f32, start_pos: f32 },
LedColor { r: u8, g: u8, b: u8 },
RumbleLf { intensity: f32, duration_ms: u32 },
}
/// [T-019]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TriggerSide { Left, Right }
/// [T-016] Emitted on connect/disconnect
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerConnectionEvent {
pub controller: ControllerId,
pub connected: bool,
pub timestamp_ms: u64,
}
// βββ Spatial Audio [T-096βT-102] βββββββββββββββββββββββββββββββββββββββββββββ
/// [T-099] Position a named audio source for Steam Audio.
/// source_id: "perfrnn.lead" | "perfrnn.pad" | "perfrnn.bass" |
/// "magenta_rt" | "ddsp" | "pitch_class.{0..11}"
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceSetPositionCmd {
pub source_id: String,
pub position: Vec3, // meters, right-hand, origin at listener default
pub gain: f32, // 0.0β1.0
}
/// [T-101] Initialize/update Steam Audio room model
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoomModelCmd {
pub width: f32,
pub height: f32,
pub depth: f32,
pub absorption: f32,
}
/// [T-133] Load SOFA HRTF personalization file
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HrtfLoadCmd { pub path: String }
// βββ Gemma-3N / Mood [T-049βT-052] βββββββββββββββββββββββββββββββββββββββββββ
/// [T-052] Output of Gemma-3N visual analysis, broadcast at 0.5 Hz
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MoodState {
pub mood: String, // e.g. "mysterious"
pub energy: f32, // 0.0β1.0
pub texture: String, // e.g. "dense"
pub timestamp_ms: u64,
}
// βββ Magenta RT Style [T-083βT-086] ββββββββββββββββββββββββββββββββββββββββββ
/// [T-084] Macro knobs 1β6 β MusicCoCa embedding blend weights. 0.0β1.0 each.
/// Need not sum to 1.0; backend normalizes before applying.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StyleBlendSetCmd { pub weights: [f32; 6] }
/// [T-088] Emitted after each 2-second Magenta RT generation chunk
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerationChunkEvent {
pub source: GenerationSource,
pub duration_ms: u32,
pub timestamp_ms: u64,
}
/// [T-088]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind", content = "data")]
pub enum GenerationSource {
MagnetaRt,
PerfRnn { channel: RnnChannel },
Ddsp,
}
/// [T-070]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum RnnChannel { Lead, Pad, Bass }
// βββ Application State [T-048+] βββββββββββββββββββββββββββββββββββββββββββββββ
/// [T-048] Full snapshot returned by get_app_state
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState {
pub transport: TransportStatus,
pub webgpu_active: bool,
pub controllers_connected: Vec<ControllerId>,
pub camera_assignment: CameraAssignment,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_mood: Option<MoodState>,
pub style_weights: [f32; 6],
}
Stub Command Pattern
Every unimplemented handler follows this exactly:
// kansas/src/commands/transport.rs
use crate::ipc::{error::IpcError, types::*};
/// [T-003] Set transport state and/or BPM
#[tauri::command]
pub fn transport_set(cmd: TransportSetCmd) -> Result<TransportStatus, IpcError> {
Err(IpcError::NotImplemented("T-003".into()))
}
/// [T-003] Get current transport status
#[tauri::command]
pub fn transport_get() -> Result<TransportStatus, IpcError> {
Err(IpcError::NotImplemented("T-003".into()))
}
Repeat this pattern for every command below. The T-NNN string in NotImplemented must match the task that implements it.
Commands Catalog
Register ALL in tauri::generate_handler![] in kansas/src/main.rs.
| Command | Input | Return | Implements |
|---|---|---|---|
ping |
β | &'static str |
T-001 β |
transport_set |
TransportSetCmd |
TransportStatus |
T-003 |
transport_get |
β | TransportStatus |
T-003 |
model_infer |
ModelInferCmd |
ModelInferResult |
T-046 |
model_reload |
ModelReloadCmd |
() |
T-127 |
camera_assign |
CameraAssignCmd |
CameraAssignment |
T-034 |
param_set |
ParamSetCmd |
() |
T-024 |
haptic_send |
HapticCmd |
() |
T-019 |
source_set_position |
SourceSetPositionCmd |
() |
T-099 |
room_model_set |
RoomModelCmd |
() |
T-101 |
hrtf_load |
HrtfLoadCmd |
() |
T-133 |
style_blend_set |
StyleBlendSetCmd |
() |
T-084 |
device_list |
β | DeviceListResult |
T-013 |
get_app_state |
β | AppState |
T-048 |
Events Catalog
Fire-and-forget from Rust backend via app_handle.emit(name, payload).
| Event | Payload | Emitted by |
|---|---|---|
transport_tick |
TransportTickEvent |
T-003 |
param_changed |
ParamChangedEvent |
T-024 |
skeleton_pose |
SkeletonPoseEvent |
T-038 |
head_pose |
HeadPoseEvent |
T-041 |
controller_state |
ControllerStateEvent |
T-022 |
controller_connection |
ControllerConnectionEvent |
T-016 |
mood_changed |
MoodState |
T-052 |
generation_chunk |
GenerationChunkEvent |
T-088 |
model_inference_complete |
ModelInferenceCompleteEvent |
T-046 |
device_list_updated |
DeviceListResult |
T-013 |
TypeScript Mirror Types (src/ipc/types.ts)
// βββ Primitives βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface Vec3 { x: number; y: number; z: number }
export interface Quaternion { x: number; y: number; z: number; w: number }
export interface Rgb { r: number; g: number; b: number }
export type IpcErrorKind = 'notImplemented'|'invalidParam'|'deviceNotFound'|'modelNotLoaded'|'audioError'|'internal'
export interface IpcError { kind: IpcErrorKind; message: string }
export function parseIpcError(raw: unknown): IpcError {
if (typeof raw === 'string') { try { return JSON.parse(raw) as IpcError } catch {} }
return { kind: 'internal', message: String(raw) }
}
// βββ Transport ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type TransportStateKind = 'stopped' | 'playing' | 'paused'
export interface TransportState { kind: TransportStateKind }
export interface TransportSetCmd {
state?: TransportState; bpm?: number
timeSigNumerator?: number; timeSigDenominator?: number
}
export interface TransportStatus {
state: TransportState; bpm: number
timeSigNumerator: number; timeSigDenominator: number
bar: number; beat: number; tick: number; elapsedMs: number
}
export interface TransportTickEvent {
bar: number; beat: number; tick: number
bpm: number; elapsedMs: number; isDownbeat: boolean
}
// βββ Parameter Bus ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type ParamSourceKind = 'ui' | 'controller' | 'gesture' | 'headTracking' | 'internal'
export interface ParamSource { kind: ParamSourceKind; id?: ControllerId }
export interface ParamSetCmd { paramId: string; value: number; source: ParamSource }
export interface ParamChangedEvent { paramId: string; value: number; source: ParamSource; timestampMs: number }
// βββ Models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type ModelIdKind =
| 'musicVaeEncoder' | 'musicVaeDecoder' | 'perfRnnStep'
| 'spectroStreamEncode' | 'spectroStreamDecode'
| 'musicCocaText' | 'musicCocaAudio' | 'magnetaRtGenerate'
| 'ddspEncode' | 'ddspDecode' | 'gemmaInference'
export interface ModelId { kind: ModelIdKind }
export interface ModelInferCmd { model: ModelId; input: unknown }
export interface ModelInferResult { model: ModelId; output: unknown; latencyMs: number }
export interface ModelReloadCmd { model: ModelId; path: string }
export interface ModelInferenceCompleteEvent { model: ModelId; latencyMs: number; timestampMs: number }
// βββ Devices ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type DeviceKind = 'audioInput' | 'audioOutput' | 'camera'
export interface DeviceInfo { id: number; name: string; kind: DeviceKind; isDefault: boolean }
export interface DeviceListResult { devices: DeviceInfo[] }
// βββ Camera βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type CameraRoleKind = 'pose' | 'head'
export interface CameraRole { kind: CameraRoleKind }
export interface CameraAssignCmd { role: CameraRole; deviceId: number }
export interface CameraAssignment { poseDeviceId?: number; headDeviceId?: number }
// βββ Skeleton Pose ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface PoseLandmark { x: number; y: number; z: number; visibility: number }
export interface SkeletonPoseEvent {
landmarks: PoseLandmark[] // always 33
timestampMs: number; detected: boolean
}
// βββ Head Tracking ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface HeadPoseEvent {
position: Vec3; orientation: Quaternion
yawDeg: number; pitchDeg: number; rollDeg: number
timestampMs: number; confidence: number
}
// βββ Controllers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type ControllerIdKind = 'dualSense' | 'joyConLeft' | 'joyConRight'
export interface ControllerId { kind: ControllerIdKind }
export interface ControllerStateEvent {
controller: ControllerId; axes: number[]; buttons: boolean[]
gyro?: Vec3; accel?: Vec3
leftTrigger?: number; rightTrigger?: number
touchpad?: [number, number]; timestampMs: number
}
export type TriggerSideKind = 'left' | 'right'
export type HapticKindType =
| { type: 'beatPulse'; data: { intensity: number } }
| { type: 'noteOnset'; data: { note: number; velocity: number } }
| { type: 'chunkBoundary'; data: Record<string, never> }
| { type: 'moodChange'; data: Record<string, never> }
| { type: 'triggerResistance'; data: { trigger: { kind: TriggerSideKind }; force: number; startPos: number } }
| { type: 'ledColor'; data: { r: number; g: number; b: number } }
| { type: 'rumbleLf'; data: { intensity: number; durationMs: number } }
export interface HapticCmd { controller: ControllerId; kind: HapticKindType }
export interface ControllerConnectionEvent { controller: ControllerId; connected: boolean; timestampMs: number }
// βββ Spatial Audio ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface SourceSetPositionCmd { sourceId: string; position: Vec3; gain: number }
export interface RoomModelCmd { width: number; height: number; depth: number; absorption: number }
export interface HrtfLoadCmd { path: string }
// βββ Mood βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface MoodState { mood: string; energy: number; texture: string; timestampMs: number }
// βββ Magenta RT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface StyleBlendSetCmd { weights: [number,number,number,number,number,number] }
export type RnnChannelKind = 'lead' | 'pad' | 'bass'
export interface RnnChannel { kind: RnnChannelKind }
export type GenerationSource =
| { kind: 'magnetaRt' }
| { kind: 'perfRnn'; data: { channel: RnnChannel } }
| { kind: 'ddsp' }
export interface GenerationChunkEvent { source: GenerationSource; durationMs: number; timestampMs: number }
// βββ App State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface AppState {
transport: TransportStatus; webgpuActive: boolean
controllersConnected: ControllerId[]; cameraAssignment: CameraAssignment
currentMood?: MoodState
styleWeights: [number,number,number,number,number,number]
}
TypeScript Wrappers
src/ipc/commands.ts
import { invoke } from '@tauri-apps/api/core'
import type {
TransportSetCmd, TransportStatus,
ModelInferCmd, ModelInferResult, ModelReloadCmd,
CameraAssignCmd, CameraAssignment,
ParamSetCmd, HapticCmd,
SourceSetPositionCmd, RoomModelCmd, HrtfLoadCmd,
StyleBlendSetCmd, DeviceListResult, AppState,
} from './types'
export const ping = (): Promise<string> => invoke('ping')
export const transportSet = (cmd: TransportSetCmd): Promise<TransportStatus> => invoke('transport_set', { cmd })
export const transportGet = (): Promise<TransportStatus> => invoke('transport_get')
export const modelInfer = (cmd: ModelInferCmd): Promise<ModelInferResult>=> invoke('model_infer', { cmd })
export const modelReload = (cmd: ModelReloadCmd): Promise<void> => invoke('model_reload', { cmd })
export const cameraAssign = (cmd: CameraAssignCmd): Promise<CameraAssignment>=> invoke('camera_assign', { cmd })
export const paramSet = (cmd: ParamSetCmd): Promise<void> => invoke('param_set', { cmd })
export const hapticSend = (cmd: HapticCmd): Promise<void> => invoke('haptic_send', { cmd })
export const sourceSetPosition = (cmd: SourceSetPositionCmd): Promise<void> => invoke('source_set_position', { cmd })
export const roomModelSet = (cmd: RoomModelCmd): Promise<void> => invoke('room_model_set', { cmd })
export const hrtfLoad = (cmd: HrtfLoadCmd): Promise<void> => invoke('hrtf_load', { cmd })
export const styleBlendSet = (cmd: StyleBlendSetCmd): Promise<void> => invoke('style_blend_set', { cmd })
export const deviceList = (): Promise<DeviceListResult>=> invoke('device_list')
export const getAppState = (): Promise<AppState> => invoke('get_app_state')
src/ipc/events.ts
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import type {
TransportTickEvent, ParamChangedEvent,
SkeletonPoseEvent, HeadPoseEvent,
ControllerStateEvent, ControllerConnectionEvent,
MoodState, GenerationChunkEvent,
ModelInferenceCompleteEvent, DeviceListResult,
} from './types'
// IMPORTANT: Every returned UnlistenFn MUST be called on component unmount.
// Dangling listeners accumulate and will OOM the WebView over long sessions.
const on = <T>(event: string, cb: (p: T) => void): Promise<UnlistenFn> =>
listen<T>(event, (e) => cb(e.payload))
export const onTransportTick = (cb: (e: TransportTickEvent) => void) => on<TransportTickEvent> ('transport_tick', cb)
export const onParamChanged = (cb: (e: ParamChangedEvent) => void) => on<ParamChangedEvent> ('param_changed', cb)
export const onSkeletonPose = (cb: (e: SkeletonPoseEvent) => void) => on<SkeletonPoseEvent> ('skeleton_pose', cb)
export const onHeadPose = (cb: (e: HeadPoseEvent) => void) => on<HeadPoseEvent> ('head_pose', cb)
export const onControllerState = (cb: (e: ControllerStateEvent) => void) => on<ControllerStateEvent> ('controller_state', cb)
export const onControllerConnection = (cb: (e: ControllerConnectionEvent) => void) => on<ControllerConnectionEvent> ('controller_connection', cb)
export const onMoodChanged = (cb: (e: MoodState) => void) => on<MoodState> ('mood_changed', cb)
export const onGenerationChunk = (cb: (e: GenerationChunkEvent) => void) => on<GenerationChunkEvent> ('generation_chunk', cb)
export const onModelInferenceComplete = (cb: (e: ModelInferenceCompleteEvent) => void) => on<ModelInferenceCompleteEvent>('model_inference_complete', cb)
export const onDeviceListUpdated = (cb: (e: DeviceListResult) => void) => on<DeviceListResult> ('device_list_updated', cb)
Capability Configuration (kansas/capabilities/default.json)
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for Synesthesia",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"synesthesia:allow-ping",
"synesthesia:allow-transport-set",
"synesthesia:allow-transport-get",
"synesthesia:allow-model-infer",
"synesthesia:allow-model-reload",
"synesthesia:allow-camera-assign",
"synesthesia:allow-param-set",
"synesthesia:allow-haptic-send",
"synesthesia:allow-source-set-position",
"synesthesia:allow-room-model-set",
"synesthesia:allow-hrtf-load",
"synesthesia:allow-style-blend-set",
"synesthesia:allow-device-list",
"synesthesia:allow-get-app-state"
]
}
Testing
Automated
-
cargo check --workspaceβ zero errors -
cargo clippy --workspace -- -D warningsβ zero warnings -
pnpm typecheckβ zero errors
Manual
cargo tauri devβ DevTools console:await window.__TAURI__.core.invoke('transport_set', { cmd: { bpm: 140.0 } }) // Expected rejection: { "kind": "notImplemented", "message": "T-003" }- Call every command from the catalog β each must reject with structured
IpcError, not crash. await window.__TAURI__.core.invoke('ping')β"pong"β
Do Not
- Implement any command logic β
NotImplementedonly exceptping - Add
AppHandlestate β T-003+ own Tauri managed state - Use
serde_json::Valuefor any input exceptModelInferCmd - Use
#[allow(dead_code)]β fix the registration if warnings appear - Use TypeScript
classβinterfaceandtypeonly - Use
as unknown as Tcasts in event listeners
Iteration Protocol
PR required before closing. Human reviews before merge.
## Review Blocker β [date]
**Type affected:** [TypeName]
**Problem:** [mismatch or design flaw]
**Downstream impact:** [which T-NNN tasks already use this]
**Proposed fix:** [recommendation]
GitHub CLI
gh issue create \
--title "T-002: IPC command registry β typed serde structs & TypeScript mirror types" \
--label "type:task,stack:rust,stack:typescript,agent:review-required,priority:critical,status:ready,day:1" \
--body-file T-002.md
Parent: #GENESIS
Blocks: T-003βT-005, T-010, T-013βT-014, T-016βT-029, T-031βT-041, T-046βT-052, T-062βT-090, T-096βT-102, T-121βT-135
Blocked By: T-001
Version: v0.1 Β· Iteration: iter-1 Β· Effort: M (1β2 days)