diff options
| author | Adam Malczewski <[email protected]> | 2026-05-19 23:20:41 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-19 23:20:41 +0900 |
| commit | a38d5b1279db6f9de5228c173019fc2ac08daec3 (patch) | |
| tree | 32c3a535d0b74872ef952b4a44d4d5ba2ec9d638 /packages/frontend/src/lib/components/ToolCallDisplay.svelte | |
| parent | 0ae805b28b5160b8d9fb43635fa172961f6550cc (diff) | |
| download | dispatch-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.svelte | 93 |
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> |
