summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/ToolCallDisplay.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-19 23:20:41 +0900
committerAdam Malczewski <[email protected]>2026-05-19 23:20:41 +0900
commita38d5b1279db6f9de5228c173019fc2ac08daec3 (patch)
tree32c3a535d0b74872ef952b4a44d4d5ba2ec9d638 /packages/frontend/src/lib/components/ToolCallDisplay.svelte
parent0ae805b28b5160b8d9fb43635fa172961f6550cc (diff)
downloaddispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.tar.gz
dispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.zip
feat: Phase 2 — shell permissions, tree-sitter analysis, permission UI
Permission engine: - Rule-based engine: wildcard matching, last-match-wins, reject cascade - PermissionService with pending/approved state, PermissionChecker interface - dispatch.yaml config loader with per-permission pattern rules Shell tool: - run_shell tool with child_process spawn, timeout, streaming output - Tree-sitter static analysis (web-tree-sitter + tree-sitter-bash WASM) - BashArity command normalization for 'always allow' patterns - FILE_COMMANDS set: rm, cp, mv, mkdir, ls, find, grep, cat, etc. Agent loop refactored: - Removed maxSteps, manual step loop with tool execution - Permission checks on shell commands (external_directory only) - Permission checks on file tools outside workspace boundary - Symlink bypass fix (realpathSync), .. false positive fix - Shell output streaming via Promise.race + setImmediate polling API layer: - PermissionManager wraps PermissionService, broadcasts via WebSocket - WebSocket handles permission-reply messages from frontend - Config loaded from dispatch.yaml, converted to ruleset Frontend: - Permission prompt modal (native dialog, focus trap, ARIA) - Always-allow confirmation flow with pattern preview - Shell output display (live streaming + final parsed result) - Permission log panel (fixed bottom-right overlay) - Exit code badge (green 0, red non-zero) 134 tests, typecheck clean on all 3 packages
Diffstat (limited to 'packages/frontend/src/lib/components/ToolCallDisplay.svelte')
-rw-r--r--packages/frontend/src/lib/components/ToolCallDisplay.svelte93
1 files changed, 83 insertions, 10 deletions
diff --git a/packages/frontend/src/lib/components/ToolCallDisplay.svelte b/packages/frontend/src/lib/components/ToolCallDisplay.svelte
index 83d21ba..070a8e3 100644
--- a/packages/frontend/src/lib/components/ToolCallDisplay.svelte
+++ b/packages/frontend/src/lib/components/ToolCallDisplay.svelte
@@ -8,6 +8,39 @@ let isExpanded = $state(toolCall.isExpanded);
function toggle() {
isExpanded = !isExpanded;
}
+
+interface ShellResult {
+ stdout: string;
+ stderr: string;
+ exitCode: number;
+}
+
+function parseShellResult(result: string): ShellResult | null {
+ try {
+ const parsed = JSON.parse(result) as unknown;
+ if (
+ parsed !== null &&
+ typeof parsed === "object" &&
+ "stdout" in parsed &&
+ "stderr" in parsed &&
+ "exitCode" in parsed
+ ) {
+ return {
+ stdout: String((parsed as Record<string, unknown>).stdout ?? ""),
+ stderr: String((parsed as Record<string, unknown>).stderr ?? ""),
+ exitCode: Number((parsed as Record<string, unknown>).exitCode ?? 0),
+ };
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+const isShell = $derived(toolCall.name === "run_shell");
+const shellResult = $derived(
+ isShell && toolCall.result !== undefined ? parseShellResult(toolCall.result) : null,
+);
</script>
<div class="collapse collapse-arrow bg-base-200 my-1 rounded-lg border border-base-300 {isExpanded ? 'collapse-open' : ''}">
@@ -20,7 +53,11 @@ function toggle() {
<span class="badge badge-neutral badge-sm">tool</span>
<span class="font-mono">{toolCall.name}</span>
{#if toolCall.result !== undefined}
- {#if toolCall.isError}
+ {#if isShell && shellResult !== null}
+ <span class="badge badge-sm ml-auto {shellResult.exitCode === 0 ? 'badge-success' : 'badge-error'}">
+ exit {shellResult.exitCode}
+ </span>
+ {:else if toolCall.isError}
<span class="badge badge-error badge-sm ml-auto">error</span>
{:else}
<span class="badge badge-success badge-sm ml-auto">done</span>
@@ -36,15 +73,51 @@ function toggle() {
<p class="font-semibold text-base-content/70 mb-1">Arguments</p>
<pre class="bg-base-300 rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all">{JSON.stringify(toolCall.arguments, null, 2)}</pre>
</div>
- {#if toolCall.result !== undefined}
- <div class="mt-2">
- <p class="font-semibold text-base-content/70 mb-1">Result</p>
- <pre
- class="rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all {toolCall.isError
- ? 'bg-error/20 text-error'
- : 'bg-base-300'}">{toolCall.result}</pre>
- </div>
- {/if}
+ {#if isShell && toolCall.result !== undefined}
+ {#if shellResult !== null}
+ <div class="mt-2">
+ <p class="font-semibold text-base-content/70 mb-1">stdout:</p>
+ <pre class="bg-base-300 rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all font-mono">{shellResult.stdout || "(empty)"}</pre>
+ </div>
+ {#if shellResult.stderr}
+ <div class="mt-2">
+ <p class="font-semibold text-error/80 mb-1">stderr:</p>
+ <pre class="bg-error/10 text-error rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all font-mono">{shellResult.stderr}</pre>
+ </div>
+ {/if}
+ <div class="mt-2 flex items-center gap-2">
+ <span class="font-semibold text-base-content/70">exit code:</span>
+ <span class="badge badge-sm {shellResult.exitCode === 0 ? 'badge-success' : 'badge-error'}">{shellResult.exitCode}</span>
+ </div>
+ {:else}
+ <div class="mt-2">
+ <p class="font-semibold text-base-content/70 mb-1">Result</p>
+ <pre class="rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all {toolCall.isError ? 'bg-error/20 text-error' : 'bg-base-300'}">{toolCall.result}</pre>
+ </div>
+ {/if}
+ {:else if isShell && toolCall.shellOutput}
+ {#if toolCall.shellOutput.stdout}
+ <div class="mt-2">
+ <p class="font-semibold text-base-content/70 mb-1">stdout</p>
+ <pre class="bg-base-300 rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all text-xs">{toolCall.shellOutput.stdout}</pre>
+ </div>
+ {/if}
+ {#if toolCall.shellOutput.stderr}
+ <div class="mt-2">
+ <p class="font-semibold text-error/70 mb-1">stderr</p>
+ <pre class="bg-error/10 rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all text-xs text-error">{toolCall.shellOutput.stderr}</pre>
+ </div>
+ {/if}
+ <span class="text-xs text-base-content/50 italic">Running...</span>
+ {:else if toolCall.result !== undefined}
+ <div class="mt-2">
+ <p class="font-semibold text-base-content/70 mb-1">Result</p>
+ <pre
+ class="rounded p-2 overflow-auto max-h-40 whitespace-pre-wrap break-all {toolCall.isError
+ ? 'bg-error/20 text-error'
+ : 'bg-base-300'}">{toolCall.result}</pre>
+ </div>
+ {/if}
</div>
{/if}
</div>