Skip to content

Commit 59569dd

Browse files
committed
Merge branch 'main' into feat/card-embed-fields
2 parents 195d9d4 + 2407780 commit 59569dd

69 files changed

Lines changed: 7320 additions & 1855 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ CLAUDE.md
2929
docs/phases/
3030
docs/specs/
3131
PROJECT-STATUS.md
32+
.worktrees/

Cargo.lock

Lines changed: 44 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
1717
serde_json = "1.0"
1818

1919
# LLM / Rig framework
20-
rig = { version = "0.31", package = "rig-core", features = ["derive"] }
20+
rig = { version = "0.33", package = "rig-core", features = ["derive"] }
2121

2222
# HTTP clients for LLM providers
2323
reqwest = { version = "0.13", features = ["json", "stream", "form", "query", "gzip"] }

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
9696
libxkbcommon0 \
9797
libxss1 \
9898
libxtst6 \
99+
libxfixes3 \
99100
&& rm -rf /var/lib/apt/lists/*
100101

101102
COPY --from=builder /usr/local/bin/spacebot /usr/local/bin/spacebot

desktop/src-tauri/src/main.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33

44
use std::fs;
55
use std::path::PathBuf;
6+
use tauri::Emitter;
67
use tauri::Manager;
8+
use tauri_plugin_global_shortcut::{Code, Modifiers, Shortcut};
9+
10+
// ── Voice overlay dimensions ─────────────────────────────────────────────
11+
const OVERLAY_INITIAL_WIDTH: f64 = 520.0;
12+
const OVERLAY_INITIAL_HEIGHT: f64 = 100.0;
13+
const OVERLAY_BOTTOM_MARGIN: f64 = 40.0;
714

815
/// Resolve the path to the connection settings file in the app data directory.
916
fn settings_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
@@ -43,6 +50,146 @@ fn set_server_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
4350
Ok(())
4451
}
4552

53+
/// Toggle the voice overlay window visibility.
54+
#[tauri::command]
55+
fn toggle_voice_overlay(app: tauri::AppHandle) -> Result<(), String> {
56+
toggle_overlay(&app);
57+
Ok(())
58+
}
59+
60+
/// Resize a named overlay window to the given logical dimensions.
61+
/// Repositions so the window stays horizontally centred and bottom-pinned.
62+
/// The frontend owns the layout — it measures its own content and tells us
63+
/// the exact size it needs.
64+
#[tauri::command]
65+
fn resize_overlay_window(
66+
app: tauri::AppHandle,
67+
label: String,
68+
width: f64,
69+
height: f64,
70+
) -> Result<(), String> {
71+
let Some(window) = app.get_webview_window(&label) else {
72+
return Ok(());
73+
};
74+
75+
let monitor = app.primary_monitor().ok().flatten();
76+
let screen_width = monitor
77+
.as_ref()
78+
.map(|m| m.size().width as f64 / m.scale_factor())
79+
.unwrap_or(1920.0);
80+
let screen_height = monitor
81+
.as_ref()
82+
.map(|m| m.size().height as f64 / m.scale_factor())
83+
.unwrap_or(1080.0);
84+
85+
let x = (screen_width - width) / 2.0;
86+
let y = screen_height - height - OVERLAY_BOTTOM_MARGIN;
87+
88+
use tauri::LogicalPosition;
89+
use tauri::LogicalSize;
90+
let _ = window.set_size(LogicalSize::new(width, height));
91+
let _ = window.set_position(LogicalPosition::new(x, y));
92+
93+
Ok(())
94+
}
95+
96+
fn activate_voice_overlay(app: &tauri::AppHandle) {
97+
if app.get_webview_window("voice-overlay").is_none() {
98+
create_overlay_window(app);
99+
} else if let Some(overlay) = app.get_webview_window("voice-overlay") {
100+
if !overlay.is_visible().unwrap_or(false) {
101+
apply_overlay_window_chrome(&overlay);
102+
let _ = overlay.show();
103+
let _ = overlay.set_focus();
104+
}
105+
}
106+
}
107+
108+
fn toggle_overlay(app: &tauri::AppHandle) {
109+
if let Some(overlay) = app.get_webview_window("voice-overlay") {
110+
// Toggle visibility
111+
if overlay.is_visible().unwrap_or(false) {
112+
let _ = overlay.hide();
113+
} else {
114+
apply_overlay_window_chrome(&overlay);
115+
let _ = overlay.show();
116+
let _ = overlay.set_focus();
117+
}
118+
} else {
119+
// Create the overlay window on first toggle
120+
create_overlay_window(app);
121+
}
122+
}
123+
124+
fn create_overlay_window(app: &tauri::AppHandle) {
125+
use tauri::window::Color;
126+
use tauri::WebviewWindowBuilder;
127+
128+
// Get the primary monitor to position at bottom center
129+
let monitor = app.primary_monitor().ok().flatten();
130+
131+
let screen_width = monitor
132+
.as_ref()
133+
.map(|m| m.size().width as f64 / m.scale_factor())
134+
.unwrap_or(1920.0);
135+
let screen_height = monitor
136+
.as_ref()
137+
.map(|m| m.size().height as f64 / m.scale_factor())
138+
.unwrap_or(1080.0);
139+
140+
// Start collapsed (pill-only). The frontend measures its own content
141+
// and calls resize_overlay_window when the layout changes.
142+
let x = (screen_width - OVERLAY_INITIAL_WIDTH) / 2.0;
143+
let y = screen_height - OVERLAY_INITIAL_HEIGHT - OVERLAY_BOTTOM_MARGIN;
144+
145+
match WebviewWindowBuilder::new(
146+
app,
147+
"voice-overlay",
148+
tauri::WebviewUrl::App("/overlay".into()),
149+
)
150+
.title("Voice")
151+
.inner_size(OVERLAY_INITIAL_WIDTH, OVERLAY_INITIAL_HEIGHT)
152+
.position(x, y)
153+
.decorations(false)
154+
.shadow(false)
155+
.transparent(true)
156+
.background_color(Color(0, 0, 0, 0))
157+
.always_on_top(true)
158+
.visible(true)
159+
.resizable(false)
160+
.skip_taskbar(true)
161+
.focused(true)
162+
.maximizable(false)
163+
.minimizable(false)
164+
.closable(false)
165+
.build()
166+
{
167+
Ok(window) => {
168+
apply_overlay_window_chrome(&window);
169+
tracing::info!("voice overlay window created");
170+
// Apply dark theme on macOS
171+
#[cfg(target_os = "macos")]
172+
{
173+
if let Ok(ns_window) = window.ns_window() {
174+
unsafe {
175+
sb_desktop_macos::lock_app_theme(1);
176+
}
177+
let _ = ns_window;
178+
}
179+
}
180+
}
181+
Err(error) => {
182+
tracing::error!(%error, "failed to create voice overlay window");
183+
}
184+
}
185+
}
186+
187+
fn apply_overlay_window_chrome(window: &tauri::WebviewWindow) {
188+
let _ = window.set_decorations(false);
189+
let _ = window.set_shadow(false);
190+
let _ = window.set_always_on_top(true);
191+
}
192+
46193
fn main() {
47194
tracing_subscriber::fmt()
48195
.with_env_filter(
@@ -51,9 +198,47 @@ fn main() {
51198
)
52199
.init();
53200

201+
// Option+Space toggles the overlay. Option+Shift+Space is hold-to-talk.
202+
let toggle_shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
203+
let voice_shortcut = Shortcut::new(Some(Modifiers::ALT | Modifiers::SHIFT), Code::Space);
204+
54205
tauri::Builder::default()
55206
.plugin(tauri_plugin_shell::init())
56-
.invoke_handler(tauri::generate_handler![get_server_url, set_server_url])
207+
.plugin(
208+
tauri_plugin_global_shortcut::Builder::new()
209+
.with_shortcut(toggle_shortcut.clone())
210+
.unwrap()
211+
.with_shortcut(voice_shortcut.clone())
212+
.unwrap()
213+
.with_handler(
214+
move |app, _shortcut, event| match (_shortcut, event.state) {
215+
(shortcut, tauri_plugin_global_shortcut::ShortcutState::Pressed)
216+
if shortcut == &toggle_shortcut =>
217+
{
218+
toggle_overlay(app);
219+
}
220+
(shortcut, tauri_plugin_global_shortcut::ShortcutState::Pressed)
221+
if shortcut == &voice_shortcut =>
222+
{
223+
activate_voice_overlay(app);
224+
let _ = app.emit("voice-overlay:start-recording", ());
225+
}
226+
(shortcut, tauri_plugin_global_shortcut::ShortcutState::Released)
227+
if shortcut == &voice_shortcut =>
228+
{
229+
let _ = app.emit("voice-overlay:stop-recording", ());
230+
}
231+
_ => {}
232+
},
233+
)
234+
.build(),
235+
)
236+
.invoke_handler(tauri::generate_handler![
237+
get_server_url,
238+
set_server_url,
239+
toggle_voice_overlay,
240+
resize_overlay_window,
241+
])
57242
.setup(|app| {
58243
// Apply macOS titlebar style (invisible toolbar for traffic light padding)
59244
#[cfg(target_os = "macos")]

0 commit comments

Comments
 (0)