generated from nhcarrigan/template
feat: add worktree isolation support and hook event display
Closes #152, closes #150 - Add use_worktree bool to ClaudeStartOptions and HikariConfig (Rust + TS) - Pass --worktree flag to claude in both WSL and non-WSL command paths - Detect WorktreeCreate/WorktreeRemove hook events in stderr handler - Emit worktree events with distinct line_type instead of error - Add worktree line type to TerminalLine union, Terminal rendering, and tests - Display worktree events in green with [worktree] prefix - Add worktree isolation toggle to Agent Settings section of ConfigSidebar - Thread use_worktree through all seven start_claude call sites
This commit is contained in:
@@ -25,6 +25,9 @@ pub struct ClaudeStartOptions {
|
||||
|
||||
#[serde(default)]
|
||||
pub resume_session_id: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub use_worktree: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -113,6 +116,9 @@ pub struct HikariConfig {
|
||||
|
||||
#[serde(default = "default_discord_rpc_enabled")]
|
||||
pub discord_rpc_enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub use_worktree: bool,
|
||||
}
|
||||
|
||||
impl Default for HikariConfig {
|
||||
@@ -145,6 +151,7 @@ impl Default for HikariConfig {
|
||||
budget_action: BudgetAction::Warn,
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
use_worktree: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +291,7 @@ mod tests {
|
||||
budget_action: BudgetAction::Block,
|
||||
budget_warning_threshold: 0.75,
|
||||
discord_rpc_enabled: true,
|
||||
use_worktree: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
|
||||
@@ -265,6 +265,11 @@ impl WslBridge {
|
||||
}
|
||||
}
|
||||
|
||||
// Add worktree flag if requested
|
||||
if options.use_worktree {
|
||||
cmd.arg("--worktree");
|
||||
}
|
||||
|
||||
cmd.current_dir(working_dir);
|
||||
|
||||
// Set API key as environment variable if specified
|
||||
@@ -351,6 +356,11 @@ impl WslBridge {
|
||||
}
|
||||
}
|
||||
|
||||
// Add worktree flag if requested
|
||||
if options.use_worktree {
|
||||
claude_cmd.push_str(" --worktree");
|
||||
}
|
||||
|
||||
// Use bash -lc to load login profile (ensures PATH includes claude)
|
||||
cmd.args(["-e", "bash", "-lc", &claude_cmd]);
|
||||
|
||||
@@ -751,11 +761,18 @@ fn handle_stderr(
|
||||
}
|
||||
}
|
||||
|
||||
// Still emit the stderr line as output
|
||||
// Worktree hook events are informational — emit with a distinct type
|
||||
let is_worktree_event = line.contains("[WorktreeCreate Hook]")
|
||||
|| line.contains("[WorktreeRemove Hook]");
|
||||
|
||||
let _ = app.emit(
|
||||
"claude:output",
|
||||
OutputEvent {
|
||||
line_type: "error".to_string(),
|
||||
line_type: if is_worktree_event {
|
||||
"worktree".to_string()
|
||||
} else {
|
||||
"error".to_string()
|
||||
},
|
||||
content: line,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
|
||||
@@ -61,6 +61,7 @@ async function changeDirectory(path: string): Promise<void> {
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -135,6 +136,7 @@ async function startNewConversation(): Promise<void> {
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
});
|
||||
|
||||
let showCustomThemeEditor = $state(false);
|
||||
@@ -473,6 +474,21 @@
|
||||
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Worktree Isolation -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={config.use_worktree}
|
||||
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
||||
/>
|
||||
<span class="text-sm text-[var(--text-primary)]">Worktree isolation</span>
|
||||
</label>
|
||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
||||
Launch sessions with <code class="font-mono">--worktree</code> for isolated git worktree environments
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Greeting Section -->
|
||||
|
||||
@@ -362,6 +362,7 @@ User: ${formattedMessage}`;
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: newGrantedTools,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
});
|
||||
|
||||
let streamerModeActive = $state(false);
|
||||
@@ -178,6 +179,7 @@
|
||||
custom_instructions: currentConfig.custom_instructions || null,
|
||||
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: currentConfig.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -289,6 +291,7 @@
|
||||
custom_instructions: currentConfig.custom_instructions || null,
|
||||
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: currentConfig.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
return "terminal-rate-limit";
|
||||
case "compact-prompt":
|
||||
return "terminal-compact-prompt";
|
||||
case "worktree":
|
||||
return "terminal-worktree";
|
||||
default:
|
||||
return "terminal-default";
|
||||
}
|
||||
@@ -117,6 +119,8 @@
|
||||
return "[error]";
|
||||
case "rate-limit":
|
||||
return "[rate-limit]";
|
||||
case "worktree":
|
||||
return "[worktree]";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -386,6 +390,10 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.terminal-worktree {
|
||||
color: var(--terminal-worktree, #34d399);
|
||||
}
|
||||
|
||||
.compact-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -40,6 +40,8 @@ function getLineClass(type: string): string {
|
||||
return "terminal-rate-limit";
|
||||
case "compact-prompt":
|
||||
return "terminal-compact-prompt";
|
||||
case "worktree":
|
||||
return "terminal-worktree";
|
||||
default:
|
||||
return "terminal-default";
|
||||
}
|
||||
@@ -59,6 +61,8 @@ function getLinePrefix(type: string): string {
|
||||
return "[error]";
|
||||
case "rate-limit":
|
||||
return "[rate-limit]";
|
||||
case "worktree":
|
||||
return "[worktree]";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -116,6 +120,10 @@ describe("getLineClass", () => {
|
||||
expect(getLineClass("compact-prompt")).toBe("terminal-compact-prompt");
|
||||
});
|
||||
|
||||
it("returns terminal-worktree for worktree lines", () => {
|
||||
expect(getLineClass("worktree")).toBe("terminal-worktree");
|
||||
});
|
||||
|
||||
it("returns terminal-default for unknown line types", () => {
|
||||
expect(getLineClass("unknown")).toBe("terminal-default");
|
||||
expect(getLineClass("")).toBe("terminal-default");
|
||||
@@ -152,6 +160,10 @@ describe("getLinePrefix", () => {
|
||||
expect(getLinePrefix("compact-prompt")).toBe("");
|
||||
});
|
||||
|
||||
it("returns [worktree] for worktree lines", () => {
|
||||
expect(getLinePrefix("worktree")).toBe("[worktree]");
|
||||
});
|
||||
|
||||
it("returns empty string for thinking lines (no prefix)", () => {
|
||||
expect(getLinePrefix("thinking")).toBe("");
|
||||
});
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: grantedToolsList,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ describe("config store", () => {
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
};
|
||||
|
||||
expect(config.model).toBe("claude-sonnet-4");
|
||||
@@ -240,6 +241,7 @@ describe("config store", () => {
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
};
|
||||
|
||||
expect(config.model).toBeNull();
|
||||
@@ -785,6 +787,7 @@ describe("config store", () => {
|
||||
budget_warning_threshold: 0.9,
|
||||
discord_rpc_enabled: false,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
};
|
||||
|
||||
const mockInvokeImpl = vi.mocked(invoke);
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface HikariConfig {
|
||||
discord_rpc_enabled: boolean;
|
||||
// Thinking blocks settings
|
||||
show_thinking_blocks: boolean;
|
||||
// Worktree isolation
|
||||
use_worktree: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig: HikariConfig = {
|
||||
@@ -87,6 +89,7 @@ const defaultConfig: HikariConfig = {
|
||||
budget_warning_threshold: 0.8,
|
||||
discord_rpc_enabled: true,
|
||||
show_thinking_blocks: true,
|
||||
use_worktree: false,
|
||||
};
|
||||
|
||||
function createConfigStore() {
|
||||
|
||||
+4
-2
@@ -331,7 +331,8 @@ export async function initializeTauriListeners() {
|
||||
| "error"
|
||||
| "thinking"
|
||||
| "rate-limit"
|
||||
| "compact-prompt",
|
||||
| "compact-prompt"
|
||||
| "worktree",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
@@ -348,7 +349,8 @@ export async function initializeTauriListeners() {
|
||||
| "error"
|
||||
| "thinking"
|
||||
| "rate-limit"
|
||||
| "compact-prompt",
|
||||
| "compact-prompt"
|
||||
| "worktree",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
|
||||
@@ -8,7 +8,8 @@ export interface TerminalLine {
|
||||
| "error"
|
||||
| "thinking"
|
||||
| "rate-limit"
|
||||
| "compact-prompt";
|
||||
| "compact-prompt"
|
||||
| "worktree";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
toolName?: string;
|
||||
|
||||
Reference in New Issue
Block a user