diff --git a/ui/src/ui/views/nodes-exec-approvals.ts b/ui/src/ui/views/nodes-exec-approvals.ts new file mode 100644 index 0000000000..f968006345 --- /dev/null +++ b/ui/src/ui/views/nodes-exec-approvals.ts @@ -0,0 +1,651 @@ +import { html, nothing } from "lit"; +import type { + ExecApprovalsAllowlistEntry, + ExecApprovalsFile, +} from "../controllers/exec-approvals.ts"; +import type { NodesProps } from "./nodes.ts"; +import { clampText, formatRelativeTimestamp } from "../format.ts"; + +type ExecSecurity = "deny" | "allowlist" | "full"; +type ExecAsk = "off" | "on-miss" | "always"; + +type ExecApprovalsResolvedDefaults = { + security: ExecSecurity; + ask: ExecAsk; + askFallback: ExecSecurity; + autoAllowSkills: boolean; +}; + +type ExecApprovalsAgentOption = { + id: string; + name?: string; + isDefault?: boolean; +}; + +type ExecApprovalsTargetNode = { + id: string; + label: string; +}; + +type ExecApprovalsState = { + ready: boolean; + disabled: boolean; + dirty: boolean; + loading: boolean; + saving: boolean; + form: ExecApprovalsFile | null; + defaults: ExecApprovalsResolvedDefaults; + selectedScope: string; + selectedAgent: Record | null; + agents: ExecApprovalsAgentOption[]; + allowlist: ExecApprovalsAllowlistEntry[]; + target: "gateway" | "node"; + targetNodeId: string | null; + targetNodes: ExecApprovalsTargetNode[]; + onSelectScope: (agentId: string) => void; + onSelectTarget: (kind: "gateway" | "node", nodeId: string | null) => void; + onPatch: (path: Array, value: unknown) => void; + onRemove: (path: Array) => void; + onLoad: () => void; + onSave: () => void; +}; + +const EXEC_APPROVALS_DEFAULT_SCOPE = "__defaults__"; + +const SECURITY_OPTIONS: Array<{ value: ExecSecurity; label: string }> = [ + { value: "deny", label: "Deny" }, + { value: "allowlist", label: "Allowlist" }, + { value: "full", label: "Full" }, +]; + +const ASK_OPTIONS: Array<{ value: ExecAsk; label: string }> = [ + { value: "off", label: "Off" }, + { value: "on-miss", label: "On miss" }, + { value: "always", label: "Always" }, +]; + +function normalizeSecurity(value?: string): ExecSecurity { + if (value === "allowlist" || value === "full" || value === "deny") { + return value; + } + return "deny"; +} + +function normalizeAsk(value?: string): ExecAsk { + if (value === "always" || value === "off" || value === "on-miss") { + return value; + } + return "on-miss"; +} + +function resolveExecApprovalsDefaults( + form: ExecApprovalsFile | null, +): ExecApprovalsResolvedDefaults { + const defaults = form?.defaults ?? {}; + return { + security: normalizeSecurity(defaults.security), + ask: normalizeAsk(defaults.ask), + askFallback: normalizeSecurity(defaults.askFallback ?? "deny"), + autoAllowSkills: Boolean(defaults.autoAllowSkills ?? false), + }; +} + +function resolveConfigAgents(config: Record | null): ExecApprovalsAgentOption[] { + const agentsNode = (config?.agents ?? {}) as Record; + const list = Array.isArray(agentsNode.list) ? agentsNode.list : []; + const agents: ExecApprovalsAgentOption[] = []; + list.forEach((entry) => { + if (!entry || typeof entry !== "object") { + return; + } + const record = entry as Record; + const id = typeof record.id === "string" ? record.id.trim() : ""; + if (!id) { + return; + } + const name = typeof record.name === "string" ? record.name.trim() : undefined; + const isDefault = record.default === true; + agents.push({ id, name: name || undefined, isDefault }); + }); + return agents; +} + +function resolveExecApprovalsAgents( + config: Record | null, + form: ExecApprovalsFile | null, +): ExecApprovalsAgentOption[] { + const configAgents = resolveConfigAgents(config); + const approvalsAgents = Object.keys(form?.agents ?? {}); + const merged = new Map(); + configAgents.forEach((agent) => merged.set(agent.id, agent)); + approvalsAgents.forEach((id) => { + if (merged.has(id)) { + return; + } + merged.set(id, { id }); + }); + const agents = Array.from(merged.values()); + if (agents.length === 0) { + agents.push({ id: "main", isDefault: true }); + } + agents.sort((a, b) => { + if (a.isDefault && !b.isDefault) { + return -1; + } + if (!a.isDefault && b.isDefault) { + return 1; + } + const aLabel = a.name?.trim() ? a.name : a.id; + const bLabel = b.name?.trim() ? b.name : b.id; + return aLabel.localeCompare(bLabel); + }); + return agents; +} + +function resolveExecApprovalsScope( + selected: string | null, + agents: ExecApprovalsAgentOption[], +): string { + if (selected === EXEC_APPROVALS_DEFAULT_SCOPE) { + return EXEC_APPROVALS_DEFAULT_SCOPE; + } + if (selected && agents.some((agent) => agent.id === selected)) { + return selected; + } + return EXEC_APPROVALS_DEFAULT_SCOPE; +} + +export function resolveExecApprovalsState(props: NodesProps): ExecApprovalsState { + const form = props.execApprovalsForm ?? props.execApprovalsSnapshot?.file ?? null; + const ready = Boolean(form); + const defaults = resolveExecApprovalsDefaults(form); + const agents = resolveExecApprovalsAgents(props.configForm, form); + const targetNodes = resolveExecApprovalsNodes(props.nodes); + const target = props.execApprovalsTarget; + let targetNodeId = + target === "node" && props.execApprovalsTargetNodeId ? props.execApprovalsTargetNodeId : null; + if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) { + targetNodeId = null; + } + const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents); + const selectedAgent = + selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE + ? (((form?.agents ?? {})[selectedScope] as Record | undefined) ?? null) + : null; + const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist) + ? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? []) + : []; + return { + ready, + disabled: props.execApprovalsSaving || props.execApprovalsLoading, + dirty: props.execApprovalsDirty, + loading: props.execApprovalsLoading, + saving: props.execApprovalsSaving, + form, + defaults, + selectedScope, + selectedAgent, + agents, + allowlist, + target, + targetNodeId, + targetNodes, + onSelectScope: props.onExecApprovalsSelectAgent, + onSelectTarget: props.onExecApprovalsTargetChange, + onPatch: props.onExecApprovalsPatch, + onRemove: props.onExecApprovalsRemove, + onLoad: props.onLoadExecApprovals, + onSave: props.onSaveExecApprovals, + }; +} + +export function renderExecApprovals(state: ExecApprovalsState) { + const ready = state.ready; + const targetReady = state.target !== "node" || Boolean(state.targetNodeId); + return html` +
+
+
+
Exec approvals
+
+ Allowlist and approval policy for exec host=gateway/node. +
+
+ +
+ + ${renderExecApprovalsTarget(state)} + + ${ + !ready + ? html`
+
Load exec approvals to edit allowlists.
+ +
` + : html` + ${renderExecApprovalsTabs(state)} + ${renderExecApprovalsPolicy(state)} + ${ + state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE + ? nothing + : renderExecApprovalsAllowlist(state) + } + ` + } +
+ `; +} + +function renderExecApprovalsTarget(state: ExecApprovalsState) { + const hasNodes = state.targetNodes.length > 0; + const nodeValue = state.targetNodeId ?? ""; + return html` +
+
+
+
Target
+
+ Gateway edits local approvals; node edits the selected node. +
+
+
+ + ${ + state.target === "node" + ? html` + + ` + : nothing + } +
+
+ ${ + state.target === "node" && !hasNodes + ? html` +
No nodes advertise exec approvals yet.
+ ` + : nothing + } +
+ `; +} + +function renderExecApprovalsTabs(state: ExecApprovalsState) { + return html` +
+ Scope +
+ + ${state.agents.map((agent) => { + const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id; + return html` + + `; + })} +
+
+ `; +} + +function renderExecApprovalsPolicy(state: ExecApprovalsState) { + const isDefaults = state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE; + const defaults = state.defaults; + const agent = state.selectedAgent ?? {}; + const basePath = isDefaults ? ["defaults"] : ["agents", state.selectedScope]; + const agentSecurity = typeof agent.security === "string" ? agent.security : undefined; + const agentAsk = typeof agent.ask === "string" ? agent.ask : undefined; + const agentAskFallback = typeof agent.askFallback === "string" ? agent.askFallback : undefined; + const securityValue = isDefaults ? defaults.security : (agentSecurity ?? "__default__"); + const askValue = isDefaults ? defaults.ask : (agentAsk ?? "__default__"); + const askFallbackValue = isDefaults ? defaults.askFallback : (agentAskFallback ?? "__default__"); + const autoOverride = + typeof agent.autoAllowSkills === "boolean" ? agent.autoAllowSkills : undefined; + const autoEffective = autoOverride ?? defaults.autoAllowSkills; + const autoIsDefault = autoOverride == null; + + return html` +
+
+
+
Security
+
+ ${isDefaults ? "Default security mode." : `Default: ${defaults.security}.`} +
+
+
+ +
+
+ +
+
+
Ask
+
+ ${isDefaults ? "Default prompt policy." : `Default: ${defaults.ask}.`} +
+
+
+ +
+
+ +
+
+
Ask fallback
+
+ ${ + isDefaults + ? "Applied when the UI prompt is unavailable." + : `Default: ${defaults.askFallback}.` + } +
+
+
+ +
+
+ +
+
+
Auto-allow skill CLIs
+
+ ${ + isDefaults + ? "Allow skill executables listed by the Gateway." + : autoIsDefault + ? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).` + : `Override (${autoEffective ? "on" : "off"}).` + } +
+
+
+ + ${ + !isDefaults && !autoIsDefault + ? html`` + : nothing + } +
+
+
+ `; +} + +function renderExecApprovalsAllowlist(state: ExecApprovalsState) { + const allowlistPath = ["agents", state.selectedScope, "allowlist"]; + const entries = state.allowlist; + return html` +
+
+
Allowlist
+
Case-insensitive glob patterns.
+
+ +
+
+ ${ + entries.length === 0 + ? html` +
No allowlist entries yet.
+ ` + : entries.map((entry, index) => renderAllowlistEntry(state, entry, index)) + } +
+ `; +} + +function renderAllowlistEntry( + state: ExecApprovalsState, + entry: ExecApprovalsAllowlistEntry, + index: number, +) { + const lastUsed = entry.lastUsedAt ? formatRelativeTimestamp(entry.lastUsedAt) : "never"; + const lastCommand = entry.lastUsedCommand ? clampText(entry.lastUsedCommand, 120) : null; + const lastPath = entry.lastResolvedPath ? clampText(entry.lastResolvedPath, 120) : null; + return html` +
+
+
${entry.pattern?.trim() ? entry.pattern : "New pattern"}
+
Last used: ${lastUsed}
+ ${lastCommand ? html`
${lastCommand}
` : nothing} + ${lastPath ? html`
${lastPath}
` : nothing} +
+
+ + +
+
+ `; +} + +function resolveExecApprovalsNodes( + nodes: Array>, +): ExecApprovalsTargetNode[] { + const list: ExecApprovalsTargetNode[] = []; + for (const node of nodes) { + const commands = Array.isArray(node.commands) ? node.commands : []; + const supports = commands.some( + (cmd) => + String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set", + ); + if (!supports) { + continue; + } + const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : ""; + if (!nodeId) { + continue; + } + const displayName = + typeof node.displayName === "string" && node.displayName.trim() + ? node.displayName.trim() + : nodeId; + list.push({ + id: nodeId, + label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`, + }); + } + list.sort((a, b) => a.label.localeCompare(b.label)); + return list; +} diff --git a/ui/src/ui/views/nodes.ts b/ui/src/ui/views/nodes.ts index 64bb383024..8cb5a81307 100644 --- a/ui/src/ui/views/nodes.ts +++ b/ui/src/ui/views/nodes.ts @@ -5,13 +5,9 @@ import type { PairedDevice, PendingDevice, } from "../controllers/devices.ts"; -import type { - ExecApprovalsAllowlistEntry, - ExecApprovalsFile, - ExecApprovalsSnapshot, -} from "../controllers/exec-approvals.ts"; -import { clampText, formatRelativeTimestamp, formatList } from "../format.ts"; - +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "../controllers/exec-approvals.ts"; +import { formatRelativeTimestamp, formatList } from "../format.ts"; +import { renderExecApprovals, resolveExecApprovalsState } from "./nodes-exec-approvals.ts"; export type NodesProps = { loading: boolean; nodes: Array>; @@ -248,64 +244,6 @@ type BindingState = { formMode: "form" | "raw"; }; -type ExecSecurity = "deny" | "allowlist" | "full"; -type ExecAsk = "off" | "on-miss" | "always"; - -type ExecApprovalsResolvedDefaults = { - security: ExecSecurity; - ask: ExecAsk; - askFallback: ExecSecurity; - autoAllowSkills: boolean; -}; - -type ExecApprovalsAgentOption = { - id: string; - name?: string; - isDefault?: boolean; -}; - -type ExecApprovalsTargetNode = { - id: string; - label: string; -}; - -type ExecApprovalsState = { - ready: boolean; - disabled: boolean; - dirty: boolean; - loading: boolean; - saving: boolean; - form: ExecApprovalsFile | null; - defaults: ExecApprovalsResolvedDefaults; - selectedScope: string; - selectedAgent: Record | null; - agents: ExecApprovalsAgentOption[]; - allowlist: ExecApprovalsAllowlistEntry[]; - target: "gateway" | "node"; - targetNodeId: string | null; - targetNodes: ExecApprovalsTargetNode[]; - onSelectScope: (agentId: string) => void; - onSelectTarget: (kind: "gateway" | "node", nodeId: string | null) => void; - onPatch: (path: Array, value: unknown) => void; - onRemove: (path: Array) => void; - onLoad: () => void; - onSave: () => void; -}; - -const EXEC_APPROVALS_DEFAULT_SCOPE = "__defaults__"; - -const SECURITY_OPTIONS: Array<{ value: ExecSecurity; label: string }> = [ - { value: "deny", label: "Deny" }, - { value: "allowlist", label: "Allowlist" }, - { value: "full", label: "Full" }, -]; - -const ASK_OPTIONS: Array<{ value: ExecAsk; label: string }> = [ - { value: "off", label: "Off" }, - { value: "on-miss", label: "On miss" }, - { value: "always", label: "Always" }, -]; - function resolveBindingsState(props: NodesProps): BindingState { const config = props.configForm; const nodes = resolveExecNodes(props.nodes); @@ -329,141 +267,6 @@ function resolveBindingsState(props: NodesProps): BindingState { }; } -function normalizeSecurity(value?: string): ExecSecurity { - if (value === "allowlist" || value === "full" || value === "deny") { - return value; - } - return "deny"; -} - -function normalizeAsk(value?: string): ExecAsk { - if (value === "always" || value === "off" || value === "on-miss") { - return value; - } - return "on-miss"; -} - -function resolveExecApprovalsDefaults( - form: ExecApprovalsFile | null, -): ExecApprovalsResolvedDefaults { - const defaults = form?.defaults ?? {}; - return { - security: normalizeSecurity(defaults.security), - ask: normalizeAsk(defaults.ask), - askFallback: normalizeSecurity(defaults.askFallback ?? "deny"), - autoAllowSkills: Boolean(defaults.autoAllowSkills ?? false), - }; -} - -function resolveConfigAgents(config: Record | null): ExecApprovalsAgentOption[] { - const agentsNode = (config?.agents ?? {}) as Record; - const list = Array.isArray(agentsNode.list) ? agentsNode.list : []; - const agents: ExecApprovalsAgentOption[] = []; - list.forEach((entry) => { - if (!entry || typeof entry !== "object") { - return; - } - const record = entry as Record; - const id = typeof record.id === "string" ? record.id.trim() : ""; - if (!id) { - return; - } - const name = typeof record.name === "string" ? record.name.trim() : undefined; - const isDefault = record.default === true; - agents.push({ id, name: name || undefined, isDefault }); - }); - return agents; -} - -function resolveExecApprovalsAgents( - config: Record | null, - form: ExecApprovalsFile | null, -): ExecApprovalsAgentOption[] { - const configAgents = resolveConfigAgents(config); - const approvalsAgents = Object.keys(form?.agents ?? {}); - const merged = new Map(); - configAgents.forEach((agent) => merged.set(agent.id, agent)); - approvalsAgents.forEach((id) => { - if (merged.has(id)) { - return; - } - merged.set(id, { id }); - }); - const agents = Array.from(merged.values()); - if (agents.length === 0) { - agents.push({ id: "main", isDefault: true }); - } - agents.sort((a, b) => { - if (a.isDefault && !b.isDefault) { - return -1; - } - if (!a.isDefault && b.isDefault) { - return 1; - } - const aLabel = a.name?.trim() ? a.name : a.id; - const bLabel = b.name?.trim() ? b.name : b.id; - return aLabel.localeCompare(bLabel); - }); - return agents; -} - -function resolveExecApprovalsScope( - selected: string | null, - agents: ExecApprovalsAgentOption[], -): string { - if (selected === EXEC_APPROVALS_DEFAULT_SCOPE) { - return EXEC_APPROVALS_DEFAULT_SCOPE; - } - if (selected && agents.some((agent) => agent.id === selected)) { - return selected; - } - return EXEC_APPROVALS_DEFAULT_SCOPE; -} - -function resolveExecApprovalsState(props: NodesProps): ExecApprovalsState { - const form = props.execApprovalsForm ?? props.execApprovalsSnapshot?.file ?? null; - const ready = Boolean(form); - const defaults = resolveExecApprovalsDefaults(form); - const agents = resolveExecApprovalsAgents(props.configForm, form); - const targetNodes = resolveExecApprovalsNodes(props.nodes); - const target = props.execApprovalsTarget; - let targetNodeId = - target === "node" && props.execApprovalsTargetNodeId ? props.execApprovalsTargetNodeId : null; - if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) { - targetNodeId = null; - } - const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents); - const selectedAgent = - selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE - ? (((form?.agents ?? {})[selectedScope] as Record | undefined) ?? null) - : null; - const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist) - ? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? []) - : []; - return { - ready, - disabled: props.execApprovalsSaving || props.execApprovalsLoading, - dirty: props.execApprovalsDirty, - loading: props.execApprovalsLoading, - saving: props.execApprovalsSaving, - form, - defaults, - selectedScope, - selectedAgent, - agents, - allowlist, - target, - targetNodeId, - targetNodes, - onSelectScope: props.onExecApprovalsSelectAgent, - onSelectTarget: props.onExecApprovalsTargetChange, - onPatch: props.onExecApprovalsPatch, - onRemove: props.onExecApprovalsRemove, - onLoad: props.onLoadExecApprovals, - onSave: props.onSaveExecApprovals, - }; -} - function renderBindings(state: BindingState) { const supportsBinding = state.nodes.length > 0; const defaultValue = state.defaultBinding ?? ""; @@ -557,427 +360,6 @@ function renderBindings(state: BindingState) { `; } -function renderExecApprovals(state: ExecApprovalsState) { - const ready = state.ready; - const targetReady = state.target !== "node" || Boolean(state.targetNodeId); - return html` -
-
-
-
Exec approvals
-
- Allowlist and approval policy for exec host=gateway/node. -
-
- -
- - ${renderExecApprovalsTarget(state)} - - ${ - !ready - ? html`
-
Load exec approvals to edit allowlists.
- -
` - : html` - ${renderExecApprovalsTabs(state)} - ${renderExecApprovalsPolicy(state)} - ${ - state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE - ? nothing - : renderExecApprovalsAllowlist(state) - } - ` - } -
- `; -} - -function renderExecApprovalsTarget(state: ExecApprovalsState) { - const hasNodes = state.targetNodes.length > 0; - const nodeValue = state.targetNodeId ?? ""; - return html` -
-
-
-
Target
-
- Gateway edits local approvals; node edits the selected node. -
-
-
- - ${ - state.target === "node" - ? html` - - ` - : nothing - } -
-
- ${ - state.target === "node" && !hasNodes - ? html` -
No nodes advertise exec approvals yet.
- ` - : nothing - } -
- `; -} - -function renderExecApprovalsTabs(state: ExecApprovalsState) { - return html` -
- Scope -
- - ${state.agents.map((agent) => { - const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id; - return html` - - `; - })} -
-
- `; -} - -function renderExecApprovalsPolicy(state: ExecApprovalsState) { - const isDefaults = state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE; - const defaults = state.defaults; - const agent = state.selectedAgent ?? {}; - const basePath = isDefaults ? ["defaults"] : ["agents", state.selectedScope]; - const agentSecurity = typeof agent.security === "string" ? agent.security : undefined; - const agentAsk = typeof agent.ask === "string" ? agent.ask : undefined; - const agentAskFallback = typeof agent.askFallback === "string" ? agent.askFallback : undefined; - const securityValue = isDefaults ? defaults.security : (agentSecurity ?? "__default__"); - const askValue = isDefaults ? defaults.ask : (agentAsk ?? "__default__"); - const askFallbackValue = isDefaults ? defaults.askFallback : (agentAskFallback ?? "__default__"); - const autoOverride = - typeof agent.autoAllowSkills === "boolean" ? agent.autoAllowSkills : undefined; - const autoEffective = autoOverride ?? defaults.autoAllowSkills; - const autoIsDefault = autoOverride == null; - - return html` -
-
-
-
Security
-
- ${isDefaults ? "Default security mode." : `Default: ${defaults.security}.`} -
-
-
- -
-
- -
-
-
Ask
-
- ${isDefaults ? "Default prompt policy." : `Default: ${defaults.ask}.`} -
-
-
- -
-
- -
-
-
Ask fallback
-
- ${ - isDefaults - ? "Applied when the UI prompt is unavailable." - : `Default: ${defaults.askFallback}.` - } -
-
-
- -
-
- -
-
-
Auto-allow skill CLIs
-
- ${ - isDefaults - ? "Allow skill executables listed by the Gateway." - : autoIsDefault - ? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).` - : `Override (${autoEffective ? "on" : "off"}).` - } -
-
-
- - ${ - !isDefaults && !autoIsDefault - ? html`` - : nothing - } -
-
-
- `; -} - -function renderExecApprovalsAllowlist(state: ExecApprovalsState) { - const allowlistPath = ["agents", state.selectedScope, "allowlist"]; - const entries = state.allowlist; - return html` -
-
-
Allowlist
-
Case-insensitive glob patterns.
-
- -
-
- ${ - entries.length === 0 - ? html` -
No allowlist entries yet.
- ` - : entries.map((entry, index) => renderAllowlistEntry(state, entry, index)) - } -
- `; -} - -function renderAllowlistEntry( - state: ExecApprovalsState, - entry: ExecApprovalsAllowlistEntry, - index: number, -) { - const lastUsed = entry.lastUsedAt ? formatRelativeTimestamp(entry.lastUsedAt) : "never"; - const lastCommand = entry.lastUsedCommand ? clampText(entry.lastUsedCommand, 120) : null; - const lastPath = entry.lastResolvedPath ? clampText(entry.lastResolvedPath, 120) : null; - return html` -
-
-
${entry.pattern?.trim() ? entry.pattern : "New pattern"}
-
Last used: ${lastUsed}
- ${lastCommand ? html`
${lastCommand}
` : nothing} - ${lastPath ? html`
${lastPath}
` : nothing} -
-
- - -
-
- `; -} - function renderAgentBinding(agent: BindingAgent, state: BindingState) { const bindingValue = agent.binding ?? "__default__"; const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id; @@ -1050,36 +432,6 @@ function resolveExecNodes(nodes: Array>): BindingNode[] return list; } -function resolveExecApprovalsNodes( - nodes: Array>, -): ExecApprovalsTargetNode[] { - const list: ExecApprovalsTargetNode[] = []; - for (const node of nodes) { - const commands = Array.isArray(node.commands) ? node.commands : []; - const supports = commands.some( - (cmd) => - String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set", - ); - if (!supports) { - continue; - } - const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : ""; - if (!nodeId) { - continue; - } - const displayName = - typeof node.displayName === "string" && node.displayName.trim() - ? node.displayName.trim() - : nodeId; - list.push({ - id: nodeId, - label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`, - }); - } - list.sort((a, b) => a.label.localeCompare(b.label)); - return list; -} - function resolveAgentBindings(config: Record | null): { defaultBinding?: string | null; agents: BindingAgent[];