Skip to content

Commit dde08e0

Browse files
createpjfclaude
andcommitted
feat: V2.0 Spotlight — chat windows, global shortcuts, usage tracking
- Add Spotlight window (⌘⇧X): floating quick-ask with streaming markdown - Add Chat window: multi-turn conversations with sidebar, compare mode - Add clipboard shortcuts: ⌘⇧T translate, ⌘⇧S summarize, ⌘⇧E explain - Add My Usage tab: today/month stats, 7-day trend, model breakdown - Inject routebox.meta in SSE streams for per-response cost tracking - Add conversations/messages/spotlight_history SQLite tables + CRUD - Add 13 new API endpoints (usage, conversations, spotlight) - Hash-based multi-window routing with React.lazy code splitting - Shared components: MarkdownRenderer, ModelSwitcher, CostBar, useStreamChat Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f867d30 commit dde08e0

File tree

25 files changed

+3425
-20
lines changed

25 files changed

+3425
-20
lines changed

apps/desktop/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@routebox/desktop",
33
"private": true,
4-
"version": "1.2.0",
4+
"version": "2.0.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
@@ -23,7 +23,10 @@
2323
"lucide-react": "^0.460.0",
2424
"react": "^19.1.0",
2525
"react-dom": "^19.1.0",
26-
"recharts": "^2.15.0"
26+
"react-markdown": "^9.1.0",
27+
"recharts": "^2.15.0",
28+
"rehype-highlight": "^7.0.2",
29+
"remark-gfm": "^4.0.1"
2730
},
2831
"devDependencies": {
2932
"@tailwindcss/postcss": "^4.0.0",

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "routebox-desktop"
3-
version = "1.2.0"
3+
version = "2.0.0"
44
description = "RouteBox macOS Menu Bar App"
55
authors = ["RouteBox"]
66
edition = "2021"

apps/desktop/src-tauri/src/commands.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,62 @@ pub fn toggle_panel_internal(app: &tauri::AppHandle) -> Result<(), String> {
7070
Ok(())
7171
}
7272

73+
// ── Spotlight toggle ────────────────────────────────────────────────────────
74+
75+
#[tauri::command]
76+
pub async fn toggle_spotlight(app: tauri::AppHandle) -> Result<(), String> {
77+
toggle_spotlight_internal(&app)
78+
}
79+
80+
pub fn toggle_spotlight_internal(app: &tauri::AppHandle) -> Result<(), String> {
81+
if let Some(window) = app.get_webview_window("spotlight") {
82+
if window.is_visible().unwrap_or(false) {
83+
window.hide().map_err(|e| e.to_string())?;
84+
} else {
85+
window.center().map_err(|e| e.to_string())?;
86+
window.show().map_err(|e| e.to_string())?;
87+
window.set_focus().map_err(|e| e.to_string())?;
88+
}
89+
}
90+
Ok(())
91+
}
92+
93+
// ── Chat window ─────────────────────────────────────────────────────────────
94+
95+
#[tauri::command]
96+
pub async fn open_chat(app: tauri::AppHandle) -> Result<(), String> {
97+
if let Some(window) = app.get_webview_window("chat") {
98+
window.show().map_err(|e| e.to_string())?;
99+
window.set_focus().map_err(|e| e.to_string())?;
100+
}
101+
Ok(())
102+
}
103+
104+
// ── Clipboard read ──────────────────────────────────────────────────────────
105+
106+
#[tauri::command]
107+
pub async fn read_clipboard() -> Result<String, String> {
108+
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
109+
clipboard.get_text().map_err(|e| e.to_string())
110+
}
111+
112+
// ── Clipboard action (read + emit event + show spotlight) ───────────────────
113+
114+
#[tauri::command]
115+
pub async fn clipboard_action(app: tauri::AppHandle, action: String) -> Result<(), String> {
116+
clipboard_action_sync(&app, &action)
117+
}
118+
119+
/// Synchronous version for use from shortcut handlers (no async runtime needed)
120+
pub fn clipboard_action_sync(app: &tauri::AppHandle, action: &str) -> Result<(), String> {
121+
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
122+
let text = clipboard.get_text().unwrap_or_default();
123+
app.emit("spotlight-action", serde_json::json!({ "action": action, "text": text }))
124+
.map_err(|e| e.to_string())?;
125+
toggle_spotlight_internal(app)?;
126+
Ok(())
127+
}
128+
73129
// ── Gateway process management ──────────────────────────────────────────────
74130

75131
#[tauri::command]

apps/desktop/src-tauri/src/lib.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ pub fn run() {
2929
window.on_window_event(move |event| {
3030
if let tauri::WindowEvent::Focused(false) = event {
3131
let win = w.clone();
32-
// Small delay: if focus returns within 200ms (e.g. DevTools, HMR),
33-
// don't hide. This prevents the panel flickering in dev mode.
3432
std::thread::spawn(move || {
3533
std::thread::sleep(std::time::Duration::from_millis(200));
3634
if !win.is_focused().unwrap_or(true) {
@@ -41,27 +39,70 @@ pub fn run() {
4139
});
4240
}
4341

44-
// Register global hotkey: Cmd+Shift+R
42+
// Hide spotlight when it loses focus
43+
if let Some(window) = app.get_webview_window("spotlight") {
44+
let w = window.clone();
45+
window.on_window_event(move |event| {
46+
if let tauri::WindowEvent::Focused(false) = event {
47+
let win = w.clone();
48+
std::thread::spawn(move || {
49+
std::thread::sleep(std::time::Duration::from_millis(200));
50+
if !win.is_focused().unwrap_or(true) {
51+
let _ = win.hide();
52+
}
53+
});
54+
}
55+
});
56+
}
57+
58+
// Register global hotkeys
4559
#[cfg(desktop)]
4660
{
4761
use tauri_plugin_global_shortcut::{
4862
Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState,
4963
};
5064

51-
let shortcut =
65+
let shortcut_panel =
5266
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyR);
67+
let shortcut_spotlight =
68+
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyX);
69+
let shortcut_translate =
70+
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyT);
71+
let shortcut_summarize =
72+
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyS);
73+
let shortcut_explain =
74+
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyE);
5375

5476
let handle = app.handle().clone();
77+
let sc_panel = shortcut_panel.clone();
78+
let sc_spotlight = shortcut_spotlight.clone();
79+
let sc_translate = shortcut_translate.clone();
80+
let sc_summarize = shortcut_summarize.clone();
81+
5582
app.handle().plugin(
5683
tauri_plugin_global_shortcut::Builder::new()
57-
.with_handler(move |_app, _shortcut, event| {
84+
.with_handler(move |_app, shortcut, event| {
5885
if event.state() == ShortcutState::Pressed {
59-
let _ = commands::toggle_panel_internal(&handle);
86+
if *shortcut == sc_panel {
87+
let _ = commands::toggle_panel_internal(&handle);
88+
} else if *shortcut == sc_spotlight {
89+
let _ = commands::toggle_spotlight_internal(&handle);
90+
} else if *shortcut == sc_translate {
91+
let _ = commands::clipboard_action_sync(&handle, "translate");
92+
} else if *shortcut == sc_summarize {
93+
let _ = commands::clipboard_action_sync(&handle, "summarize");
94+
} else {
95+
let _ = commands::clipboard_action_sync(&handle, "explain");
96+
}
6097
}
6198
})
6299
.build(),
63100
)?;
64-
app.global_shortcut().register(shortcut)?;
101+
app.global_shortcut().register(shortcut_panel)?;
102+
app.global_shortcut().register(shortcut_spotlight)?;
103+
app.global_shortcut().register(shortcut_translate)?;
104+
app.global_shortcut().register(shortcut_summarize)?;
105+
app.global_shortcut().register(shortcut_explain)?;
65106
}
66107

67108
Ok(())
@@ -73,6 +114,10 @@ pub fn run() {
73114
commands::copy_to_clipboard,
74115
commands::show_notification,
75116
commands::toggle_panel,
117+
commands::toggle_spotlight,
118+
commands::open_chat,
119+
commands::read_clipboard,
120+
commands::clipboard_action,
76121
commands::spawn_gateway,
77122
commands::stop_gateway,
78123
commands::is_gateway_running,

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "RouteBox",
4-
"version": "1.2.0",
4+
"version": "2.0.0",
55
"identifier": "com.routebox.desktop",
66
"build": {
77
"beforeDevCommand": "pnpm dev",
@@ -22,6 +22,34 @@
2222
"visible": false,
2323
"alwaysOnTop": true,
2424
"skipTaskbar": true
25+
},
26+
{
27+
"label": "spotlight",
28+
"title": "RouteBox Spotlight",
29+
"url": "index.html#/spotlight",
30+
"width": 540,
31+
"height": 420,
32+
"resizable": false,
33+
"decorations": false,
34+
"transparent": true,
35+
"visible": false,
36+
"alwaysOnTop": true,
37+
"skipTaskbar": true,
38+
"center": true
39+
},
40+
{
41+
"label": "chat",
42+
"title": "RouteBox Chat",
43+
"url": "index.html#/chat",
44+
"width": 900,
45+
"height": 700,
46+
"minWidth": 700,
47+
"minHeight": 500,
48+
"resizable": true,
49+
"decorations": true,
50+
"transparent": false,
51+
"visible": false,
52+
"alwaysOnTop": false
2553
}
2654
],
2755
"security": {

apps/desktop/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Settings } from "@/components/Settings";
1111
import { Onboarding } from "@/components/Onboarding";
1212
import { RequestDetail } from "@/components/RequestDetail";
1313
import { AnalyticsPage } from "@/components/AnalyticsPage";
14+
import { UsagePage } from "@/components/UsagePage";
1415
import { AlertBanner } from "@/components/AlertBanner";
1516
import { ToastContainer } from "@/components/ToastContainer";
1617
import { useRealtimeStats } from "@/hooks/useRealtimeStats";
@@ -196,6 +197,7 @@ export function App() {
196197
onSelectEntry={setSelectedRequest}
197198
/>
198199
)}
200+
{activeTab === "usage" && <UsagePage />}
199201
{activeTab === "analytics" && <AnalyticsPage />}
200202
</div>
201203

apps/desktop/src/__tests__/components.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ vi.mock("lucide-react", async () => {
1414
Wallet: icon, Activity: icon, XCircle: icon,
1515
Key: icon, Shield: icon, AlertCircle: icon, BookOpen: icon, ArrowRight: icon,
1616
BarChart3: icon, Pin: icon, Ban: icon, Plus: icon, Square: icon,
17-
TrendingUp: icon, Cpu: icon, Clock: icon,
17+
TrendingUp: icon, Cpu: icon, Clock: icon, PieChart: icon,
1818
};
1919
});
2020

@@ -163,9 +163,10 @@ describe("RequestLogPage", () => {
163163
});
164164

165165
describe("TabBar", () => {
166-
test("renders four tabs", () => {
166+
test("renders five tabs", () => {
167167
render(<TabBar activeTab="dashboard" onTabChange={() => {}} />);
168168
expect(screen.getByText("Dashboard")).toBeDefined();
169+
expect(screen.getByText("My Usage")).toBeDefined();
169170
expect(screen.getByText("Routing")).toBeDefined();
170171
expect(screen.getByText("Logs")).toBeDefined();
171172
expect(screen.getByText("Analytics")).toBeDefined();

apps/desktop/src/components/TabBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { LayoutDashboard, Route, ScrollText, BarChart3 } from "lucide-react";
1+
import { LayoutDashboard, Route, ScrollText, BarChart3, PieChart } from "lucide-react";
22
import clsx from "clsx";
33

4-
export type TabId = "dashboard" | "routing" | "logs" | "analytics";
4+
export type TabId = "dashboard" | "usage" | "routing" | "logs" | "analytics";
55

66
interface TabBarProps {
77
activeTab: TabId;
@@ -10,6 +10,7 @@ interface TabBarProps {
1010

1111
const TABS: { id: TabId; label: string; icon: typeof LayoutDashboard }[] = [
1212
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
13+
{ id: "usage", label: "My Usage", icon: PieChart },
1314
{ id: "routing", label: "Routing", icon: Route },
1415
{ id: "logs", label: "Logs", icon: ScrollText },
1516
{ id: "analytics", label: "Analytics", icon: BarChart3 },

0 commit comments

Comments
 (0)