Skip to content

Commit bd4867e

Browse files
authored
Merge pull request #2 from createpjf/feat/v2-spotlight
Feat/v2 spotlight
2 parents f867d30 + db11677 commit bd4867e

Some content is hidden

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

41 files changed

+4782
-97
lines changed

README.md

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# RouteBox
22

3-
macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, and cost tracking.
3+
macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, cost tracking, built-in chat, and web search.
4+
5+
**v2.3.0** · [Download DMG](https://github.com/createpjf/RouteBox/releases/latest) · MIT License
46

57
## What it does
68

@@ -9,9 +11,12 @@ RouteBox runs a local OpenAI-compatible proxy (`http://localhost:3001/v1`). You
911
1. **Routes** requests to the best provider based on your rules (cheapest, fastest, or smartest)
1012
2. **Tracks** tokens, cost, latency, and savings in real-time
1113
3. **Manages** multiple provider API keys securely in macOS Keychain
14+
4. **Chat** directly with any model via the built-in Chat window or Spotlight
15+
5. **Search** the web in real-time with Brave Search integration
1216

1317
```
1418
Your App → RouteBox (localhost:3001) → OpenAI / Anthropic / Google / DeepSeek / MiniMax / Kimi / FLock.io
19+
↑ Brave Search results injected as context
1520
```
1621

1722
## Supported Providers
@@ -26,7 +31,66 @@ Your App → RouteBox (localhost:3001) → OpenAI / Anthropic / Google / Dee
2631
| [Kimi](https://kimi.ai) | Kimi K2.5, Kimi K2, Moonshot | [platform.moonshot.ai](https://platform.moonshot.ai/) |
2732
| [FLock.io](https://flock.io) | Qwen3-235B, Qwen3-30B, DeepSeek-V3.2, Kimi K2.5 | [platform.flock.io](https://platform.flock.io) |
2833

29-
> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi)— a good option for cost-effective routing.
34+
**Local models**: Ollama and LM Studio are auto-discovered on your network — no API key needed.
35+
36+
> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi) — a good option for cost-effective routing.
37+
38+
## Features
39+
40+
### Three Windows
41+
42+
| Window | Shortcut | Description |
43+
|--------|----------|-------------|
44+
| **Panel** | `⌘⇧R` | Menu bar dashboard — routing, analytics, logs, settings |
45+
| **Spotlight** | `⌘⇧Space` | Quick floating window — ask a question, get an instant answer |
46+
| **Chat** | Via panel | Full chat interface with conversation history and sidebar |
47+
48+
### Dashboard Tabs
49+
50+
| Tab | What it shows |
51+
|-----|---------------|
52+
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
53+
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
54+
| **Logs** | Full request history with model, provider, latency, cost per request |
55+
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |
56+
| **My Usage** | Today/month usage, budget tracking, weekly trends, model breakdown |
57+
58+
### Web Search (Brave Search)
59+
60+
RouteBox can inject real-time web search results into any LLM conversation:
61+
62+
1. Go to **Settings → Web Search** and add your [Brave Search API key](https://brave.com/search/api) (free tier available)
63+
2. Toggle the 🌐 button in Chat or Spotlight to enable search for a message
64+
3. RouteBox searches the web, injects results as context, and the model responds with up-to-date information and source citations
65+
66+
### Intelligent Routing
67+
68+
| Strategy | Behavior |
69+
|----------|----------|
70+
| Smart Auto | AI picks the best route per request |
71+
| Cost First | Always pick the cheapest provider |
72+
| Speed First | Always pick the lowest latency provider |
73+
| Quality First | Always pick the best available model |
74+
75+
### Routing Rules
76+
77+
| Rule Type | Triggers when... | Example use |
78+
|-----------|-------------------|-------------|
79+
| **Alias** | Model name matches your virtual name | `route-code``deepseek-coder` |
80+
| **Code** | Request contains ≥3 code markers | Auto-route code tasks to DeepSeek |
81+
| **Long** | Message ≥8,000 characters | Auto-route long context to Gemini |
82+
| **General** | Catch-all fallback | Default model for everything else |
83+
84+
### Model Preferences
85+
86+
Pin a model to a specific provider, or exclude a provider for a model:
87+
88+
- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
89+
- **Exclude**: `gpt-4o` → never use provider X
90+
91+
### Thinking Model Support
92+
93+
Models that output `<think>` blocks (DeepSeek-R1, Qwen3, etc.) are automatically handled — thinking content is hidden behind a collapsible "💭 Thinking..." section.
3094

3195
## Setup
3296

@@ -78,45 +142,11 @@ client = OpenAI(
78142
)
79143
```
80144

81-
## App Tabs
82-
83-
| Tab | What it shows |
84-
|-----|---------------|
85-
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
86-
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
87-
| **Logs** | Full request history with model, provider, latency, cost per request |
88-
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |
89-
90-
## Routing
91-
92-
### Strategy
93-
94-
Pick one in the Routing tab:
95-
96-
| Strategy | Behavior |
97-
|----------|----------|
98-
| Smart Auto | AI picks the best route per request |
99-
| Cost First | Always pick the cheapest provider |
100-
| Speed First | Always pick the lowest latency provider |
101-
| Quality First | Always pick the best available model |
102-
103-
### Rules
104-
105-
Create rules to route specific request types:
145+
### 4. Enable Web Search (Optional)
106146

107-
| Rule Type | Triggers when... | Example use |
108-
|-----------|-------------------|-------------|
109-
| **Alias** | Model name matches your virtual name | `route-code``deepseek-coder` |
110-
| **Code** | Request contains ≥3 code markers | Auto-route code tasks to DeepSeek |
111-
| **Long** | Message ≥8,000 characters | Auto-route long context to Gemini |
112-
| **General** | Catch-all fallback | Default model for everything else |
147+
Go to **Settings → Web Search** → Paste your Brave Search API key → Save.
113148

114-
### Model Preferences
115-
116-
Pin a model to a specific provider, or exclude a provider for a model:
117-
118-
- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
119-
- **Exclude**: `gpt-4o` → never use provider X
149+
Then toggle 🌐 in Chat or Spotlight before sending a message to include web results.
120150

121151
## Build DMG
122152

@@ -146,6 +176,7 @@ docker build -t routebox-gateway .
146176
docker run -p 3001:3001 \
147177
-e OPENAI_API_KEY=sk-... \
148178
-e ANTHROPIC_API_KEY=sk-ant-... \
179+
-e BRAVE_API_KEY=BSA-... \
149180
-v routebox-data:/data \
150181
routebox-gateway
151182
```
@@ -158,6 +189,7 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
158189
|---------|----------|-------|
159190
| Provider API Keys | Settings → Providers | Stored in macOS Keychain |
160191
| Monthly Budget | Settings → Budget | Alerts at 80% and 100% |
192+
| Web Search | Settings → Web Search | Brave Search API key for real-time search |
161193
| Gateway URL | Settings → Connection | Default `http://localhost:3001`, customizable |
162194
| Auth Token | Settings → Authentication | Auto-generated, stored in Keychain |
163195
| Auto-start Gateway | Settings → Gateway | On/off toggle |
@@ -168,13 +200,29 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
168200
| Shortcut | Action |
169201
|----------|--------|
170202
| `⌘⇧R` | Toggle panel (global) |
203+
| `⌘⇧Space` | Toggle Spotlight (global) |
171204
| `⌘C` | Copy API key |
172205
| `⌘P` | Pause/resume traffic |
206+
| `⌘⏎` | Send message (Spotlight) |
207+
| `Esc` | Close Spotlight |
208+
209+
## Architecture
210+
211+
```
212+
RouteBox/
213+
├── apps/
214+
│ ├── desktop/ Tauri v2 + React 19 (panel, spotlight, chat windows)
215+
│ │ └── src-tauri/ Rust backend (system tray, global shortcuts, keychain)
216+
│ └── gateway/ Bun + Hono (proxy, routing, analytics, search)
217+
├── package.json pnpm monorepo root
218+
└── README.md
219+
```
173220

174221
## Tech Stack
175222

176223
- **Desktop**: Tauri v2 (Rust) + React 19 + TypeScript + Tailwind CSS v4
177224
- **Gateway**: Bun + Hono + bun:sqlite
225+
- **Search**: Brave Search API
178226
- **Design**: SF Pro, frosted glass (macOS native)
179227

180228
## License

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.3.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.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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.3.0"
44
description = "RouteBox macOS Menu Bar App"
55
authors = ["RouteBox"]
66
edition = "2021"

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"global-shortcut:default",
1111
"notification:default",
1212
"store:default",
13-
"process:default"
13+
"process:default",
14+
"updater:default"
1415
]
1516
}

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::keychain;
2-
use tauri::Manager;
2+
use tauri::{Emitter, Manager};
33
use tauri_plugin_positioner::{Position, WindowExt};
44
use arboard::Clipboard;
55
use std::sync::Mutex;
@@ -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: tauri::Error| 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,

0 commit comments

Comments
 (0)