Skip to content

Commit c1457f7

Browse files
Merge remote-tracking branch 'origin/main' into left-right-gheu
Resolved conflicts: - src/tui/events.rs: Merged AccessMode field navigation with cursor movement logic in Prompt field Includes changes from main: - feat: add access mode selection to TUI session creation (#173) - feat: add comprehensive status tracking to session manager TUI (#172) - fix: make plan mode work via prompt instruction (#171) - feat: add Rust compiler cache sharing with sccache (#175) - feat: add HTTP Basic Auth for git operations (#174) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2 parents 3d47152 + 0bc133a commit c1457f7

30 files changed

Lines changed: 1453 additions & 168 deletions

packages/multiplexer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ fs2 = "0.4"
5959
# HTTP Proxy with TLS interception
6060
hudsucker = { version = "0.24", default-features = false, features = ["rcgen-ca", "rustls-client"] }
6161
http = "1"
62+
base64 = "0.22"
6263

6364
# TLS
6465
rustls = "0.23"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# Install Claude Code hooks for multiplexer status tracking
3+
4+
set -euo pipefail
5+
6+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
CLAUDE_SETTINGS="${HOME}/.claude/settings.json"
8+
9+
# Ensure .claude directory exists
10+
mkdir -p "${HOME}/.claude"
11+
12+
# Create/update settings.json with hooks
13+
cat > "$CLAUDE_SETTINGS" <<'EOF'
14+
{
15+
"hooks": {
16+
"UserPromptSubmit": [
17+
{
18+
"hooks": [
19+
{
20+
"type": "command",
21+
"command": ["bash", "-c", "${HOME}/.multiplexer/hooks/send_status.sh UserPromptSubmit"]
22+
}
23+
]
24+
}
25+
],
26+
"PreToolUse": [
27+
{
28+
"matcher": "*",
29+
"hooks": [
30+
{
31+
"type": "command",
32+
"command": ["bash", "-c", "${HOME}/.multiplexer/hooks/send_status.sh PreToolUse"]
33+
}
34+
]
35+
}
36+
],
37+
"PermissionRequest": [
38+
{
39+
"matcher": "*",
40+
"hooks": [
41+
{
42+
"type": "command",
43+
"command": ["bash", "-c", "${HOME}/.multiplexer/hooks/send_status.sh PermissionRequest"]
44+
}
45+
]
46+
}
47+
],
48+
"Stop": [
49+
{
50+
"hooks": [
51+
{
52+
"type": "command",
53+
"command": ["bash", "-c", "${HOME}/.multiplexer/hooks/send_status.sh Stop"]
54+
}
55+
]
56+
}
57+
],
58+
"Notification": [
59+
{
60+
"matcher": {
61+
"notification_type": "idle_prompt"
62+
},
63+
"hooks": [
64+
{
65+
"type": "command",
66+
"command": ["bash", "-c", "${HOME}/.multiplexer/hooks/send_status.sh IdlePrompt"]
67+
}
68+
]
69+
}
70+
]
71+
}
72+
}
73+
EOF
74+
75+
# Create hooks directory in ~/.multiplexer if it doesn't exist
76+
mkdir -p "${HOME}/.multiplexer/hooks"
77+
78+
# Copy hook script to ~/.multiplexer/hooks
79+
cp "$HOOK_DIR/send_status.sh" "${HOME}/.multiplexer/hooks/send_status.sh"
80+
chmod +x "${HOME}/.multiplexer/hooks/send_status.sh"
81+
82+
echo "✓ Claude Code hooks installed to $CLAUDE_SETTINGS"
83+
echo "✓ Hook script copied to ${HOME}/.multiplexer/hooks/send_status.sh"
84+
echo ""
85+
echo "Multiplexer status tracking is now active!"
86+
echo "Claude's working status will be displayed in the session manager TUI."
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bash
2+
# Send hook event to multiplexer daemon
3+
# Usage: send_status.sh <event_type>
4+
5+
set -euo pipefail
6+
7+
EVENT_TYPE="$1"
8+
9+
# Validate HOME is set
10+
if [ -z "${HOME:-}" ]; then
11+
exit 0
12+
fi
13+
14+
SOCKET_PATH="${HOME}/.multiplexer/hooks.sock"
15+
MUX_SOCKET="${HOME}/.multiplexer/mux.sock"
16+
17+
# Validate sockets exist
18+
if [ ! -S "$SOCKET_PATH" ]; then
19+
# Hooks socket doesn't exist yet (daemon not started), exit silently
20+
exit 0
21+
fi
22+
23+
if [ ! -S "$MUX_SOCKET" ]; then
24+
# Daemon socket doesn't exist, exit silently
25+
exit 0
26+
fi
27+
28+
# Extract session name from worktree path
29+
# Worktree path format: ~/.multiplexer/worktrees/<session-name>
30+
# CLAUDE_CWD is provided by Claude Code hooks
31+
WORKTREE_PATH="${CLAUDE_CWD:-}"
32+
33+
if [ -z "$WORKTREE_PATH" ]; then
34+
# Not running in Claude Code context, exit silently
35+
exit 0
36+
fi
37+
38+
# Check if this is a multiplexer worktree
39+
if [[ "$WORKTREE_PATH" != *"/.multiplexer/worktrees/"* ]]; then
40+
# Not a multiplexer session, exit silently
41+
exit 0
42+
fi
43+
44+
# Parse session name from path (last component)
45+
SESSION_NAME=$(basename "$WORKTREE_PATH")
46+
47+
# Query daemon for session ID by name
48+
# Using the Unix socket API
49+
SESSION_ID=$(echo '{"type":"GetSessionIdByName","payload":{"name":"'"$SESSION_NAME"'"}}' | \
50+
nc -U "$MUX_SOCKET" 2>/dev/null | \
51+
jq -r '.payload.session_id // empty' 2>/dev/null)
52+
53+
if [ -z "$SESSION_ID" ]; then
54+
# Session not found in daemon, exit silently
55+
exit 0
56+
fi
57+
58+
# Build hook message
59+
MESSAGE=$(cat <<EOF
60+
{
61+
"session_id": "$SESSION_ID",
62+
"event": {"type": "$EVENT_TYPE"},
63+
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
64+
}
65+
EOF
66+
)
67+
68+
# Send to hook socket (with timeout)
69+
echo "$MESSAGE" | timeout 1s nc -U "$SOCKET_PATH" 2>/dev/null || true
70+
71+
exit 0

packages/multiplexer/src/api/client.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,28 @@ impl Client {
287287
_ => anyhow::bail!("Unexpected response"),
288288
}
289289
}
290+
291+
/// Send a prompt to a session (for hotkey triggers)
292+
///
293+
/// # Errors
294+
///
295+
/// Returns an error if the session is not found or the request fails.
296+
pub async fn send_prompt(&mut self, session_name: &str, prompt: &str) -> anyhow::Result<()> {
297+
let response = self
298+
.send_request(Request::SendPrompt {
299+
session: session_name.to_string(),
300+
prompt: prompt.to_string(),
301+
})
302+
.await?;
303+
304+
match response {
305+
Response::Ok => Ok(()),
306+
Response::Error { code, message} => {
307+
anyhow::bail!("[{code}] {message}")
308+
}
309+
_ => anyhow::bail!("Unexpected response"),
310+
}
311+
}
290312
}
291313

292314
#[async_trait]

packages/multiplexer/src/api/handlers.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,32 @@ pub async fn handle_request(request: Request, manager: &SessionManager) -> Respo
170170
}
171171
}
172172
},
173+
174+
Request::SendPrompt { session, prompt } => {
175+
match manager.send_prompt_to_session(&session, &prompt).await {
176+
Ok(()) => {
177+
tracing::info!(session = %session, "Prompt sent to session");
178+
Response::Ok
179+
}
180+
Err(e) => {
181+
tracing::error!(session = %session, error = %e, "Failed to send prompt");
182+
Response::Error {
183+
code: "SEND_PROMPT_ERROR".to_string(),
184+
message: e.to_string(),
185+
}
186+
}
187+
}
188+
}
189+
190+
Request::GetSessionIdByName { name } => match manager.get_session(&name).await {
191+
Some(session) => Response::SessionId {
192+
session_id: session.id.to_string(),
193+
},
194+
None => Response::Error {
195+
code: "NOT_FOUND".to_string(),
196+
message: format!("Session not found: {name}"),
197+
},
198+
},
173199
}
174200
}
175201

packages/multiplexer/src/api/protocol.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ pub enum Request {
3939

4040
/// Get recent repositories
4141
GetRecentRepos,
42+
43+
/// Send a prompt to a session (for hotkey triggers)
44+
SendPrompt { session: String, prompt: String },
45+
46+
/// Get session ID by name (for hook scripts)
47+
GetSessionIdByName { name: String },
4248
}
4349

4450
/// Recent repository entry with timestamp
@@ -155,6 +161,12 @@ pub enum Response {
155161
/// Access mode updated successfully
156162
AccessModeUpdated,
157163

164+
/// Session ID returned
165+
SessionId { session_id: String },
166+
167+
/// Generic success response
168+
Ok,
169+
158170
/// Error response
159171
Error { code: String, message: String },
160172
}

packages/multiplexer/src/api/server.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,51 @@ pub async fn run_daemon_with_options(enable_proxy: bool) -> anyhow::Result<()> {
9090

9191
tracing::info!(socket = %socket_path.display(), "Daemon listening");
9292

93+
// Start hook listener for Claude status updates
94+
let hook_socket_path = paths::hooks_socket_path();
95+
let (hook_listener, mut hook_rx) = crate::hooks::HookListener::new(hook_socket_path);
96+
97+
let hook_manager = Arc::clone(&manager);
98+
tokio::spawn(async move {
99+
if let Err(e) = hook_listener.start().await {
100+
tracing::error!("Hook listener failed: {}", e);
101+
}
102+
});
103+
104+
// Process hook messages
105+
let process_manager = Arc::clone(&manager);
106+
tokio::spawn(async move {
107+
use crate::core::ClaudeWorkingStatus;
108+
use crate::hooks::HookEvent;
109+
110+
while let Some(msg) = hook_rx.recv().await {
111+
let new_status = match msg.event {
112+
HookEvent::UserPromptSubmit => ClaudeWorkingStatus::Working,
113+
HookEvent::PreToolUse { .. } => ClaudeWorkingStatus::Working,
114+
HookEvent::PermissionRequest => ClaudeWorkingStatus::WaitingApproval,
115+
HookEvent::Stop => ClaudeWorkingStatus::WaitingInput,
116+
HookEvent::IdlePrompt => ClaudeWorkingStatus::Idle,
117+
};
118+
119+
if let Err(e) = process_manager
120+
.update_claude_status(msg.session_id, new_status)
121+
.await
122+
{
123+
tracing::error!(
124+
session_id = %msg.session_id,
125+
error = %e,
126+
"Failed to update Claude status from hook"
127+
);
128+
}
129+
}
130+
});
131+
132+
// Start CI status poller for GitHub PR checks
133+
let ci_poller = crate::ci::CIPoller::new(Arc::clone(&manager));
134+
tokio::spawn(async move {
135+
ci_poller.start().await;
136+
});
137+
93138
// Accept connections
94139
loop {
95140
match listener.accept().await {

0 commit comments

Comments
 (0)