mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(tui): show why a tool call was auto-approved or denied
Ports the auto-approval provenance explanation already shown in kilo-ui/vscode to the TUI. Adds a Kilo-owned tool-approval.tsx with a plain-text description helper (describeApproval) and a shared ApprovalNote row component, then wires a single call into InlineTool/ InlineToolRow (Shell, Read, Grep, Glob, WebFetch, etc.) and BlockTool (Write, Edit, ApplyPatch, Task), showing a muted line under completed/ failed tool calls. Todo writes are excluded via a hideApproval prop, mirroring the kilo-ui behavior that treats them as orchestration rather than an auditable action.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import { Show } from "solid-js"
|
||||
import type { PermissionProvenance } from "@/kilocode/permission/provenance"
|
||||
import type { ToolState } from "@kilocode/sdk/v2"
|
||||
|
||||
/** `state.metadata` off any tool state, including the pending variant that lacks the field. */
|
||||
export function stateMetadata(state: ToolState | undefined) {
|
||||
return state && "metadata" in state ? state.metadata : undefined
|
||||
}
|
||||
|
||||
const SOURCES = ["agent", "global", "project", "yolo", "session", "manual", "default"] as const
|
||||
|
||||
/** Read the approval/denial provenance off a tool part's metadata, if present. */
|
||||
export function toolApprovalFrom(metadata: Record<string, unknown> | undefined) {
|
||||
const value = metadata?.approval
|
||||
if (!value || typeof value !== "object") return undefined
|
||||
const approval = value as PermissionProvenance.Approval
|
||||
return (SOURCES as readonly string[]).includes(approval.source) ? approval : undefined
|
||||
}
|
||||
|
||||
function sourceLabel(approval: PermissionProvenance.Approval): string | undefined {
|
||||
switch (approval.source) {
|
||||
case "agent":
|
||||
return approval.agent ? `by the ${approval.agent} agent` : "by the agent"
|
||||
case "global":
|
||||
return "by your global config"
|
||||
case "project":
|
||||
return "by the project config"
|
||||
case "yolo":
|
||||
return "by auto-approve (YOLO) mode"
|
||||
case "session":
|
||||
return "by a session auto-approve rule"
|
||||
case "default":
|
||||
return "by default"
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** A short "why" line describing an auto-approval or denial, for the TUI's plain-text rows. */
|
||||
export function describeApproval(metadata: Record<string, unknown> | undefined): string | undefined {
|
||||
const approval = toolApprovalFrom(metadata)
|
||||
if (!approval) return undefined
|
||||
const manual = approval.source === "manual"
|
||||
const decision = manual ? "approved by you" : approval.rule?.action === "deny" ? "denied" : "auto-approved"
|
||||
if (manual) return decision
|
||||
const source = sourceLabel(approval)
|
||||
const rule = approval.rule
|
||||
// The catch-all "*"/"*" rule carries no useful detail; let the source alone explain it.
|
||||
const ruleText =
|
||||
rule && !(rule.permission === "*" && rule.pattern === "*") ? ` (matched ${rule.permission} \`${rule.pattern}\`)` : ""
|
||||
return source ? `${decision} ${source}${ruleText}` : decision
|
||||
}
|
||||
|
||||
/** The muted "why" row rendered under a completed/failed inline or block tool. */
|
||||
export function ApprovalNote(props: { note: string | undefined; color?: RGBA; paddingLeft: number }) {
|
||||
return (
|
||||
<Show when={props.note}>
|
||||
<box paddingLeft={props.paddingLeft}>
|
||||
<text fg={props.color}>{props.note}</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -63,6 +63,7 @@ import { Toast, useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
import { ApprovalNote, describeApproval, stateMetadata } from "../../kilocode/tool-approval" // kilocode_change
|
||||
import { useEpilogue } from "../../context/epilogue"
|
||||
import { normalizePath } from "../../util/path"
|
||||
import { PermissionPrompt } from "./permission"
|
||||
@@ -2204,6 +2205,8 @@ function InlineTool(props: {
|
||||
|
||||
const failed = createMemo(() => Boolean(error() && !denied()))
|
||||
const clickable = createMemo(() => Boolean(props.onClick || failed()))
|
||||
// kilocode_change - explain why the call was auto-approved or denied
|
||||
const approvalNote = createMemo(() => describeApproval(stateMetadata(props.part.state)))
|
||||
const fg = createMemo(() => {
|
||||
if (props.color) return props.color
|
||||
if (permission()) return theme.warning
|
||||
@@ -2228,6 +2231,8 @@ function InlineTool(props: {
|
||||
failure={props.failure}
|
||||
spinner={props.spinner}
|
||||
separate={props.separate}
|
||||
note={approvalNote()} // kilocode_change
|
||||
noteColor={theme.textMuted} // kilocode_change
|
||||
onMouseOver={() => clickable() && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
@@ -2258,6 +2263,8 @@ export function InlineToolRow(props: {
|
||||
failure?: string
|
||||
spinner?: boolean
|
||||
separate?: boolean
|
||||
note?: string // kilocode_change - why the call was auto-approved or denied
|
||||
noteColor?: RGBA // kilocode_change
|
||||
children: JSX.Element
|
||||
onMouseOver?: () => void
|
||||
onMouseOut?: () => void
|
||||
@@ -2320,6 +2327,12 @@ export function InlineToolRow(props: {
|
||||
<text fg={props.errorColor}>{props.error}</text>
|
||||
</box>
|
||||
</Show>
|
||||
{/* kilocode_change - explain why the call was auto-approved or denied */}
|
||||
<ApprovalNote
|
||||
note={props.note && (props.complete || props.failed) ? props.note : undefined}
|
||||
color={props.noteColor}
|
||||
paddingLeft={INLINE_TOOL_ICON_WIDTH}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -2330,11 +2343,14 @@ function BlockTool(props: {
|
||||
onClick?: () => void
|
||||
part?: ToolPart
|
||||
spinner?: boolean
|
||||
hideApproval?: boolean // kilocode_change - suppress the auto-approval note (e.g. todowrite)
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
||||
// kilocode_change - explain why the call was auto-approved or denied
|
||||
const approvalNote = createMemo(() => (props.hideApproval ? undefined : describeApproval(stateMetadata(props.part?.state))))
|
||||
return (
|
||||
<box
|
||||
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
|
||||
@@ -2368,6 +2384,8 @@ function BlockTool(props: {
|
||||
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
||||
</Show>
|
||||
{props.children}
|
||||
{/* kilocode_change - explain why the call was auto-approved or denied */}
|
||||
<ApprovalNote note={approvalNote()} color={theme.textMuted} paddingLeft={3} />
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
@@ -2825,7 +2843,8 @@ function TodoWrite(props: ToolProps) {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={parseTodos(props.metadata.todos).length}>
|
||||
<BlockTool title="# Todos" part={props.part}>
|
||||
{/* kilocode_change - todo writes are orchestration, not a mutating action to explain */}
|
||||
<BlockTool title="# Todos" part={props.part} hideApproval>
|
||||
<box>
|
||||
<For each={todos()}>{(todo) => <TodoItem status={todo.status} content={todo.content} />}</For>
|
||||
</box>
|
||||
|
||||
Reference in New Issue
Block a user