feat(v2): add permission mode passthrough and fix agent stop-on-close

- Add permission_mode field to AgentQueryOptions (Rust, sidecar, bridge)
  flowing from controller through sidecar to SDK; defaults to
  bypassPermissions, supports default mode
- Fix AgentPane onDestroy bug: remove stopAgent() from onDestroy (fires
  on layout remounts), move stop-on-close to TilingGrid onClose handler
- Bundle SDK into sidecar via esbuild (remove --external flag)
This commit is contained in:
Hibryda 2026-03-06 23:33:51 +01:00
parent af0eb362e6
commit d5eb08ed42
7 changed files with 26 additions and 14 deletions

View file

@ -18,6 +18,7 @@ pub struct AgentQueryOptions {
pub max_turns: Option<u32>, pub max_turns: Option<u32>,
pub max_budget_usd: Option<f64>, pub max_budget_usd: Option<f64>,
pub resume_session_id: Option<String>, pub resume_session_id: Option<String>,
pub permission_mode: Option<String>,
} }
/// Directories to search for sidecar scripts. /// Directories to search for sidecar scripts.
@ -168,6 +169,7 @@ impl SidecarManager {
"maxTurns": options.max_turns, "maxTurns": options.max_turns,
"maxBudgetUsd": options.max_budget_usd, "maxBudgetUsd": options.max_budget_usd,
"resumeSessionId": options.resume_session_id, "resumeSessionId": options.resume_session_id,
"permissionMode": options.permission_mode,
}); });
self.send_message(&msg) self.send_message(&msg)

View file

@ -12,7 +12,7 @@
"tauri:dev": "cargo tauri dev", "tauri:dev": "cargo tauri dev",
"tauri:build": "cargo tauri build", "tauri:build": "cargo tauri build",
"test": "vitest run", "test": "vitest run",
"build:sidecar": "esbuild sidecar/agent-runner.ts --bundle --platform=node --format=esm --outfile=sidecar/dist/agent-runner.mjs --external:@anthropic-ai/claude-agent-sdk" "build:sidecar": "esbuild sidecar/agent-runner.ts --bundle --platform=node --format=esm --outfile=sidecar/dist/agent-runner.mjs"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",

View file

@ -27,6 +27,7 @@ interface QueryMessage {
maxTurns?: number; maxTurns?: number;
maxBudgetUsd?: number; maxBudgetUsd?: number;
resumeSessionId?: string; resumeSessionId?: string;
permissionMode?: string;
} }
interface StopMessage { interface StopMessage {
@ -51,7 +52,7 @@ function handleMessage(msg: Record<string, unknown>) {
} }
async function handleQuery(msg: QueryMessage) { async function handleQuery(msg: QueryMessage) {
const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId } = msg; const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId, permissionMode } = msg;
if (sessions.has(sessionId)) { if (sessions.has(sessionId)) {
send({ type: "error", sessionId, message: "Session already running" }); send({ type: "error", sessionId, message: "Session already running" });
@ -84,8 +85,8 @@ async function handleQuery(msg: QueryMessage) {
"Bash", "Read", "Write", "Edit", "Glob", "Grep", "Bash", "Read", "Write", "Edit", "Glob", "Grep",
"WebSearch", "WebFetch", "TodoWrite", "NotebookEdit", "WebSearch", "WebFetch", "TodoWrite", "NotebookEdit",
], ],
permissionMode: "bypassPermissions", permissionMode: (permissionMode ?? "bypassPermissions") as "bypassPermissions" | "default",
allowDangerouslySkipPermissions: true, allowDangerouslySkipPermissions: (permissionMode ?? "bypassPermissions") === "bypassPermissions",
}, },
}); });

View file

@ -36,6 +36,7 @@ interface QueryMessage {
maxTurns?: number; maxTurns?: number;
maxBudgetUsd?: number; maxBudgetUsd?: number;
resumeSessionId?: string; resumeSessionId?: string;
permissionMode?: string;
} }
interface StopMessage { interface StopMessage {
@ -60,7 +61,7 @@ function handleMessage(msg: Record<string, unknown>) {
} }
async function handleQuery(msg: QueryMessage) { async function handleQuery(msg: QueryMessage) {
const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId } = msg; const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId, permissionMode } = msg;
if (sessions.has(sessionId)) { if (sessions.has(sessionId)) {
send({ type: 'error', sessionId, message: 'Session already running' }); send({ type: 'error', sessionId, message: 'Session already running' });
@ -93,8 +94,8 @@ async function handleQuery(msg: QueryMessage) {
'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
'WebSearch', 'WebFetch', 'TodoWrite', 'NotebookEdit', 'WebSearch', 'WebFetch', 'TodoWrite', 'NotebookEdit',
], ],
permissionMode: 'bypassPermissions', permissionMode: (permissionMode ?? 'bypassPermissions') as 'bypassPermissions' | 'default',
allowDangerouslySkipPermissions: true, allowDangerouslySkipPermissions: (permissionMode ?? 'bypassPermissions') === 'bypassPermissions',
}, },
}); });

View file

@ -11,6 +11,7 @@ export interface AgentQueryOptions {
max_turns?: number; max_turns?: number;
max_budget_usd?: number; max_budget_usd?: number;
resume_session_id?: string; resume_session_id?: string;
permission_mode?: string;
remote_machine_id?: string; remote_machine_id?: string;
} }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount } from 'svelte';
import { marked, Renderer } from 'marked'; import { marked, Renderer } from 'marked';
import { queryAgent, stopAgent, isAgentReady, restartAgent } from '../../adapters/agent-bridge'; import { queryAgent, stopAgent, isAgentReady, restartAgent } from '../../adapters/agent-bridge';
import { import {
@ -68,11 +68,8 @@
} }
}); });
onDestroy(() => { // NOTE: Do NOT stop agents in onDestroy — it fires on layout changes/remounts,
if (session?.status === 'running' || session?.status === 'starting') { // not just explicit close. Stop-on-close is handled by TilingGrid.
stopAgent(sessionId).catch(() => {});
}
});
let followUpPrompt = $state(''); let followUpPrompt = $state('');

View file

@ -14,6 +14,8 @@
} from '../../stores/layout.svelte'; } from '../../stores/layout.svelte';
import { detachPane } from '../../utils/detach'; import { detachPane } from '../../utils/detach';
import { isDetachedMode } from '../../utils/detach'; import { isDetachedMode } from '../../utils/detach';
import { stopAgent } from '../../adapters/agent-bridge';
import { getAgentSession } from '../../stores/agents.svelte';
let gridTemplate = $derived(getGridTemplate()); let gridTemplate = $derived(getGridTemplate());
let panes = $derived(getPanes()); let panes = $derived(getPanes());
@ -162,7 +164,15 @@
<PaneContainer <PaneContainer
title={pane.title} title={pane.title}
status={pane.focused ? 'running' : 'idle'} status={pane.focused ? 'running' : 'idle'}
onClose={() => removePane(pane.id)} onClose={() => {
if (pane.type === 'agent') {
const s = getAgentSession(pane.id);
if (s?.status === 'running' || s?.status === 'starting') {
stopAgent(pane.id).catch(() => {});
}
}
removePane(pane.id);
}}
onDetach={detached ? undefined : () => handleDetach(pane)} onDetach={detached ? undefined : () => handleDetach(pane)}
> >
{#if pane.type === 'terminal'} {#if pane.type === 'terminal'}