Skip to content

Commit 1d012da

Browse files
Merge branch 'master' into cleanup/linux-install
2 parents 53114f7 + d46c75c commit 1d012da

File tree

30 files changed

+2222
-47
lines changed

30 files changed

+2222
-47
lines changed

Cargo.lock

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

docs/develop/windows.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Windows Development Setup
22

3-
Quick Setup (Recommended)
3+
## Quick Setup (Recommended)
44

55
Use the automated setup script:
66

@@ -17,7 +17,7 @@ bun setup-win
1717

1818
### Setup
1919

20-
```cmd
20+
```bash
2121
# Install project dependencies
2222
bun install
2323

src-tauri/capabilities/main.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
"http:allow-fetch",
5353
"process:allow-restart",
5454
"process:allow-exit",
55+
"shell:default",
56+
"shell:allow-open",
5557
{
5658
"identifier": "shell:allow-execute",
5759
"allow": [

src-tauri/packages/interceptor/src/server.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ pub struct AppState {
2525
}
2626

2727
pub async fn start_proxy_server_with_ws(
28-
proxy_port: u16,
28+
requested_port: Option<u16>,
2929
) -> Result<(
3030
UnboundedReceiver<InterceptorMessage>,
3131
WsState,
3232
JoinHandle<()>,
33+
u16,
3334
)> {
3435
let (tx, rx) = unbounded_channel::<InterceptorMessage>();
3536
let interceptor_state = InterceptorState::new(tx);
@@ -59,22 +60,27 @@ pub async fn start_proxy_server_with_ws(
5960
))
6061
.with_state(app_state);
6162

62-
let listener = TcpListener::bind(format!("127.0.0.1:{proxy_port}"))
63+
let port = requested_port.unwrap_or(0);
64+
let listener = TcpListener::bind(format!("127.0.0.1:{port}"))
6365
.await
64-
.with_context(|| format!("Failed to bind `TcpListener` to port {proxy_port}"))?;
66+
.with_context(|| format!("Failed to bind `TcpListener` to port {port}"))?;
67+
let assigned_port = listener
68+
.local_addr()
69+
.with_context(|| "Failed to get local address for proxy server")?
70+
.port();
6571

6672
info!(
6773
"Claude Code Proxy with WebSocket running on http://localhost:{}",
68-
proxy_port
74+
assigned_port
6975
);
7076

71-
info!("WebSocket endpoint: ws://localhost:{}/ws", proxy_port);
77+
info!("WebSocket endpoint: ws://localhost:{}/ws", assigned_port);
7278

7379
let server_handle = tokio::spawn(async move {
7480
if let Err(e) = axum::serve(listener, app).await {
7581
error!("Proxy server error: {}", e);
7682
}
7783
});
7884

79-
Ok((rx, ws_state, server_handle))
85+
Ok((rx, ws_state, server_handle, assigned_port))
8086
}

src-tauri/src/claude_bridge.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct ClaudeCodeBridge {
2222
interceptor_handle: Option<tokio::task::JoinHandle<()>>,
2323
server_handle: Option<tokio::task::JoinHandle<()>>,
2424
ws_connected: bool,
25+
proxy_port: Option<u16>,
2526
app_handle: AppHandle,
2627
}
2728

@@ -33,6 +34,7 @@ impl ClaudeCodeBridge {
3334
interceptor_handle: None,
3435
server_handle: None,
3536
ws_connected: false,
37+
proxy_port: None,
3638
app_handle,
3739
}
3840
}
@@ -44,11 +46,8 @@ impl ClaudeCodeBridge {
4446

4547
log::info!("Starting interceptor as embedded service...");
4648

47-
// todo: we shouldn't hardcode this
48-
let proxy_port = 3456;
49-
50-
// Start the interceptor proxy server
51-
let (rx, ws_state, server_handle) = start_proxy_server_with_ws(proxy_port).await?;
49+
// Start the interceptor proxy server on an ephemeral port and get the assigned port
50+
let (rx, ws_state, server_handle, assigned_port) = start_proxy_server_with_ws(None).await?;
5251

5352
// Create channels for message distribution
5453
let (broadcast_tx, mut broadcast_rx) = mpsc::unbounded_channel::<InterceptorMessage>();
@@ -76,8 +75,9 @@ impl ClaudeCodeBridge {
7675
self.interceptor_handle = Some(message_handler);
7776
self.server_handle = Some(server_handle);
7877
self.ws_connected = true;
78+
self.proxy_port = Some(assigned_port);
7979

80-
log::info!("Interceptor started successfully on port {}", proxy_port);
80+
log::info!("Interceptor started successfully on port {}", assigned_port);
8181
Ok(())
8282
}
8383

@@ -87,6 +87,10 @@ impl ClaudeCodeBridge {
8787
"Claude Code is already running"
8888
);
8989

90+
let proxy_port = self
91+
.proxy_port
92+
.context("Interceptor must be started before starting Claude Code")?;
93+
9094
let mut cmd = Command::new("claude");
9195
cmd.args([
9296
"--dangerously-skip-permissions",
@@ -97,7 +101,10 @@ impl ClaudeCodeBridge {
97101
"--input-format",
98102
"stream-json",
99103
])
100-
.env("ANTHROPIC_BASE_URL", "http://localhost:3456")
104+
.env(
105+
"ANTHROPIC_BASE_URL",
106+
format!("http://localhost:{proxy_port}"),
107+
)
101108
.stdin(Stdio::piped())
102109
.stdout(Stdio::piped())
103110
.stderr(Stdio::piped());

src-tauri/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ fn main() {
130130
"open_folder" => {
131131
let _ = window.emit("menu_open_folder", ());
132132
}
133+
"close_folder" => {
134+
let _ = window.emit("menu_close_folder", ());
135+
}
133136
"save" => {
134137
let _ = window.emit("menu_save", ());
135138
}

src-tauri/src/menu.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ pub fn create_menu<R: tauri::Runtime>(
4242
true,
4343
Some("CmdOrCtrl+O"),
4444
)?)
45+
.item(&MenuItem::with_id(
46+
app,
47+
"close_folder",
48+
"Close Folder",
49+
true,
50+
None::<String>,
51+
)?)
4552
.separator()
4653
.item(&MenuItem::with_id(
4754
app,
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import {
3+
getVimCommandSuggestions,
4+
parseAndExecuteVimCommand,
5+
type VimCommand,
6+
} from "@/stores/vim-commands";
7+
import { useVimStore } from "@/stores/vim-store";
8+
import Command, {
9+
CommandEmpty,
10+
CommandHeader,
11+
CommandInput,
12+
CommandItem,
13+
CommandList,
14+
} from "../../ui/command";
15+
16+
const VimCommandBar = () => {
17+
const isCommandMode = useVimStore.use.isCommandMode();
18+
const commandInput = useVimStore.use.commandInput();
19+
const { exitCommandMode, updateCommandInput, executeCommand } = useVimStore.use.actions();
20+
21+
const [selectedIndex, setSelectedIndex] = useState(0);
22+
const [suggestions, setSuggestions] = useState<VimCommand[]>([]);
23+
const inputRef = useRef<HTMLInputElement>(null);
24+
const scrollContainerRef = useRef<HTMLDivElement>(null);
25+
26+
// Focus input when vim command mode becomes active
27+
useEffect(() => {
28+
if (isCommandMode && inputRef.current) {
29+
inputRef.current.focus();
30+
}
31+
}, [isCommandMode]);
32+
33+
// Update suggestions when command input changes
34+
useEffect(() => {
35+
if (commandInput) {
36+
setSuggestions(getVimCommandSuggestions(commandInput));
37+
} else {
38+
setSuggestions(getVimCommandSuggestions(""));
39+
}
40+
setSelectedIndex(0);
41+
}, [commandInput]);
42+
43+
// Handle keyboard navigation
44+
useEffect(() => {
45+
if (!isCommandMode) return;
46+
47+
const handleKeyDown = (e: KeyboardEvent) => {
48+
if (e.key === "Escape") {
49+
e.preventDefault();
50+
exitCommandMode();
51+
return;
52+
}
53+
54+
if (e.key === "Enter") {
55+
e.preventDefault();
56+
57+
// If there are suggestions and one is selected, use that
58+
if (suggestions.length > 0 && selectedIndex < suggestions.length) {
59+
const selectedCommand = suggestions[selectedIndex];
60+
const commandToExecute = selectedCommand.name;
61+
executeCommand(commandToExecute);
62+
parseAndExecuteVimCommand(commandToExecute);
63+
} else if (commandInput.trim()) {
64+
// Execute the typed command
65+
executeCommand(commandInput);
66+
parseAndExecuteVimCommand(commandInput);
67+
}
68+
return;
69+
}
70+
71+
if (e.key === "ArrowDown" && suggestions.length > 0) {
72+
e.preventDefault();
73+
setSelectedIndex((prev) => (prev + 1) % suggestions.length);
74+
return;
75+
}
76+
77+
if (e.key === "ArrowUp" && suggestions.length > 0) {
78+
e.preventDefault();
79+
setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length);
80+
return;
81+
}
82+
83+
if (e.key === "Tab" && suggestions.length > 0) {
84+
e.preventDefault();
85+
const selectedCommand = suggestions[selectedIndex];
86+
if (selectedCommand) {
87+
updateCommandInput(selectedCommand.name);
88+
}
89+
return;
90+
}
91+
};
92+
93+
document.addEventListener("keydown", handleKeyDown);
94+
95+
return () => {
96+
document.removeEventListener("keydown", handleKeyDown);
97+
};
98+
}, [
99+
isCommandMode,
100+
commandInput,
101+
suggestions,
102+
selectedIndex,
103+
exitCommandMode,
104+
updateCommandInput,
105+
executeCommand,
106+
]);
107+
108+
// Auto-scroll selected item into view
109+
useEffect(() => {
110+
if (!isCommandMode || !scrollContainerRef.current) return;
111+
112+
const selectedElement = scrollContainerRef.current.querySelector(
113+
`[data-item-index="${selectedIndex}"]`,
114+
) as HTMLElement;
115+
116+
if (selectedElement) {
117+
selectedElement.scrollIntoView({
118+
behavior: "smooth",
119+
block: "nearest",
120+
});
121+
}
122+
}, [selectedIndex, isCommandMode, suggestions]);
123+
124+
const handleInputChange = useCallback(
125+
(value: string) => {
126+
updateCommandInput(value);
127+
},
128+
[updateCommandInput],
129+
);
130+
131+
const handleItemSelect = useCallback(
132+
(command: VimCommand) => {
133+
executeCommand(command.name);
134+
parseAndExecuteVimCommand(command.name);
135+
},
136+
[executeCommand],
137+
);
138+
139+
if (!isCommandMode) {
140+
return null;
141+
}
142+
143+
return (
144+
<Command isVisible={isCommandMode} className="max-h-80">
145+
<CommandHeader onClose={exitCommandMode}>
146+
<span className="font-mono text-accent text-sm">:</span>
147+
<CommandInput
148+
ref={inputRef}
149+
value={commandInput}
150+
onChange={handleInputChange}
151+
placeholder="Enter vim command..."
152+
className="font-mono"
153+
/>
154+
</CommandHeader>
155+
156+
<CommandList ref={scrollContainerRef}>
157+
{suggestions.length === 0 ? (
158+
<CommandEmpty>
159+
<div className="font-mono">
160+
{commandInput ? "No matching commands" : "Type a command"}
161+
</div>
162+
</CommandEmpty>
163+
) : (
164+
<div className="p-0">
165+
{suggestions.map((command, index) => {
166+
const isSelected = index === selectedIndex;
167+
const displayName = command.aliases?.length
168+
? `${command.name} (${command.aliases.join(", ")})`
169+
: command.name;
170+
171+
return (
172+
<CommandItem
173+
key={command.name}
174+
data-item-index={index}
175+
onClick={() => handleItemSelect(command)}
176+
isSelected={isSelected}
177+
className="font-mono"
178+
>
179+
<div className="min-w-0 flex-1">
180+
<div className="truncate text-xs">
181+
<span className="text-accent">:</span>
182+
<span className="text-text">{displayName}</span>
183+
</div>
184+
<div className="mt-0.5 truncate text-[10px] text-text-lighter opacity-60">
185+
{command.description}
186+
</div>
187+
</div>
188+
</CommandItem>
189+
);
190+
})}
191+
</div>
192+
)}
193+
</CommandList>
194+
</Command>
195+
);
196+
};
197+
198+
export default VimCommandBar;

src/components/editor-footer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { useUIState } from "../stores/ui-state-store";
2929
import { getGitStatus } from "../version-control/git/controllers/git";
3030
import { useGitStore } from "../version-control/git/controllers/git-store";
3131
import GitBranchManager from "../version-control/git/views/git-branch-manager";
32+
import VimStatusIndicator from "./vim-status/vim-status-indicator";
3233

3334
// LSP Status Dropdown Component
3435
const LspStatusDropdown = ({ activeBuffer }: { activeBuffer: any }) => {
@@ -290,6 +291,9 @@ const EditorFooter = () => {
290291
</div>
291292
)}
292293

294+
{/* Vim status indicator */}
295+
<VimStatusIndicator />
296+
293297
{/* Update indicator */}
294298
{available && (
295299
<button

0 commit comments

Comments
 (0)