mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Pre-populate permission dock toggles from config (#7217)
* feat(vscode): pre-populate permission dock toggles from saved config * chore: update kilo-vscode visual regression baselines --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot]
parent
3f5d0893bb
commit
b136dbe9cc
@@ -259,6 +259,24 @@ test.describe("Permission Dock Dropdown — many rules", () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config pre-populated — rules show saved allow/deny state from config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Permission Dock Dropdown — config pre-populated", () => {
|
||||
const STORY_ID = "composite-webview--permission-dock-config-preloaded"
|
||||
|
||||
test("rules expanded — pre-populated from config (mixed allow/deny/pending)", async ({ page }) => {
|
||||
await page.goto(storyUrl(STORY_ID), { waitUntil: "load" })
|
||||
await disableAnimations(page)
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
await openDropdown(page)
|
||||
|
||||
const root = page.locator("#storybook-root")
|
||||
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "config-preloaded-expanded.png"])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Subagent permission — shows "(subagent)" in subtitle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc81b3e1762b6a8f2b1b9708ca548042ae019cd4fb0d34ccbe53b3ee204047e6
|
||||
size 18145
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { savedRuleStates } from "../../webview-ui/src/components/chat/permission-dock-utils"
|
||||
|
||||
describe("savedRuleStates", () => {
|
||||
it("returns empty map when rule is undefined", () => {
|
||||
expect(savedRuleStates(["npm *", "git *"], undefined)).toEqual({})
|
||||
})
|
||||
|
||||
it("returns empty map when rules array is empty", () => {
|
||||
expect(savedRuleStates([], { "npm *": "allow" })).toEqual({})
|
||||
})
|
||||
|
||||
it("populates approved entries from config", () => {
|
||||
const result = savedRuleStates(["npm *", "git *", "rm *"], { "npm *": "allow", "rm *": "allow" })
|
||||
expect(result).toEqual({ 0: "approved", 2: "approved" })
|
||||
})
|
||||
|
||||
it("populates denied entries from config", () => {
|
||||
const result = savedRuleStates(["npm *", "rm *"], { "rm *": "deny" })
|
||||
expect(result).toEqual({ 1: "denied" })
|
||||
})
|
||||
|
||||
it("populates mixed approved and denied", () => {
|
||||
const result = savedRuleStates(["npm *", "git *", "rm *"], {
|
||||
"npm *": "allow",
|
||||
"git *": "deny",
|
||||
})
|
||||
expect(result).toEqual({ 0: "approved", 1: "denied" })
|
||||
})
|
||||
|
||||
it("skips ask entries (they stay pending)", () => {
|
||||
const result = savedRuleStates(["npm *", "git *"], { "npm *": "ask", "git *": "allow" })
|
||||
expect(result).toEqual({ 1: "approved" })
|
||||
})
|
||||
|
||||
it("handles scalar rule with wildcard in rules array", () => {
|
||||
const result = savedRuleStates(["*"], "allow")
|
||||
expect(result).toEqual({ 0: "approved" })
|
||||
})
|
||||
|
||||
it("handles scalar rule with non-wildcard patterns (all pending)", () => {
|
||||
const result = savedRuleStates(["npm *", "git *"], "allow")
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it("returns empty map for scalar ask with wildcard", () => {
|
||||
const result = savedRuleStates(["*"], "ask")
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it("returns denied for scalar deny with wildcard", () => {
|
||||
const result = savedRuleStates(["*"], "deny")
|
||||
expect(result).toEqual({ 0: "denied" })
|
||||
})
|
||||
|
||||
it("returns pending for patterns not in config object", () => {
|
||||
const result = savedRuleStates(["npm *", "git *"], { "npm *": "allow" })
|
||||
expect(result).toEqual({ 0: "approved" })
|
||||
})
|
||||
|
||||
it("returns empty map for empty config object", () => {
|
||||
const result = savedRuleStates(["npm *"], {})
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1cf3791372e3e69fd759f2596e72211c982e8694990103c178f1aed0c59229f1
|
||||
size 16139
|
||||
@@ -17,10 +17,10 @@ import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { savedRuleStates, type RuleDecision } from "./permission-dock-utils"
|
||||
import type { PermissionRequest } from "../../types/messages"
|
||||
|
||||
type RuleDecision = "approved" | "denied" | "pending"
|
||||
|
||||
let rulesExpandedPreference = false
|
||||
|
||||
export const PermissionDock: Component<{
|
||||
@@ -30,6 +30,7 @@ export const PermissionDock: Component<{
|
||||
}> = (props) => {
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
const { config } = useConfig()
|
||||
|
||||
const fromChild = () => props.request.sessionID !== session.currentSessionID()
|
||||
// Bash sends fine-grained rules via metadata.rules; other tools use the always array.
|
||||
@@ -42,7 +43,11 @@ export const PermissionDock: Component<{
|
||||
return typeof cmd === "string" ? cmd : undefined
|
||||
}
|
||||
|
||||
const [decisions, setDecisions] = createSignal<Record<number, RuleDecision>>({})
|
||||
// Pre-populate toggle states from existing config rules so previously
|
||||
// approved/denied patterns show their saved state immediately.
|
||||
const saved = config().permission?.[props.request.toolName]
|
||||
const loadState = savedRuleStates(rules(), saved)
|
||||
const [decisions, setDecisions] = createSignal<Record<number, RuleDecision>>(loadState)
|
||||
const [expanded, setExpanded] = createSignal(rulesExpandedPreference)
|
||||
|
||||
const hasRules = () => rules().length > 0
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { PermissionRule } from "../../types/messages"
|
||||
|
||||
export type RuleDecision = "approved" | "denied" | "pending"
|
||||
|
||||
/**
|
||||
* Check which rules are already saved in the user's config and return
|
||||
* their initial toggle states (approved/denied). Rules not found in
|
||||
* the config are omitted (they default to "pending").
|
||||
*/
|
||||
export function savedRuleStates(rules: string[], saved: PermissionRule | undefined): Record<number, RuleDecision> {
|
||||
const result: Record<number, RuleDecision> = {}
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const pattern = rules[i]
|
||||
const action = typeof saved === "string" ? (pattern === "*" ? saved : undefined) : saved?.[pattern]
|
||||
if (action === "allow") result[i] = "approved"
|
||||
if (action === "deny") result[i] = "denied"
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function stripNulls(obj: Config): Config {
|
||||
return result as Config
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextValue>()
|
||||
export const ConfigContext = createContext<ConfigContextValue>()
|
||||
|
||||
export const ConfigProvider: ParentComponent = (props) => {
|
||||
const vscode = useVSCode()
|
||||
|
||||
@@ -14,7 +14,7 @@ import { VSCodeProvider } from "../context/vscode"
|
||||
import { ServerProvider } from "../context/server"
|
||||
import { ProviderContext } from "../context/provider"
|
||||
import { flattenModels, findModel as _findModel } from "../context/provider-utils"
|
||||
import { ConfigProvider } from "../context/config"
|
||||
import { ConfigProvider, ConfigContext } from "../context/config"
|
||||
import { DataProvider } from "@kilocode/kilo-ui/context/data"
|
||||
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
|
||||
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
|
||||
@@ -32,7 +32,7 @@ import { dict as appEn } from "../i18n/en"
|
||||
import { dict as amEn } from "../../agent-manager/i18n/en"
|
||||
import { dict as kiloEn } from "@kilocode/kilo-i18n/en"
|
||||
import { resolveTemplate } from "../context/language-utils"
|
||||
import type { PermissionRequest, QuestionRequest } from "../types/messages"
|
||||
import type { Config, PermissionRequest, QuestionRequest } from "../types/messages"
|
||||
|
||||
// Merged English dictionary (same merge order as the real LanguageProvider)
|
||||
const dict: Record<string, string> = { ...appEn, ...amEn, ...uiEn, ...kiloEn }
|
||||
@@ -189,10 +189,25 @@ interface StoryProvidersProps {
|
||||
questions?: QuestionRequest[]
|
||||
status?: string
|
||||
sessionID?: string
|
||||
/** When provided, injects a mock ConfigContext with this config instead of the real ConfigProvider. */
|
||||
config?: Config
|
||||
/** When true, renders children without the default 12px padding wrapper */
|
||||
noPadding?: boolean
|
||||
}
|
||||
|
||||
/** Wraps children with either a mock ConfigContext (when config prop is given) or the real ConfigProvider. */
|
||||
const ConfigWrapper: ParentComponent<{ config?: Config }> = (props) => {
|
||||
if (props.config) {
|
||||
const value = {
|
||||
config: () => props.config!,
|
||||
loading: () => false,
|
||||
updateConfig: noop,
|
||||
}
|
||||
return <ConfigContext.Provider value={value}>{props.children}</ConfigContext.Provider>
|
||||
}
|
||||
return <ConfigProvider>{props.children}</ConfigProvider>
|
||||
}
|
||||
|
||||
export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
|
||||
const data = () => props.data ?? defaultMockData
|
||||
const session = mockSessionValue({
|
||||
@@ -206,7 +221,7 @@ export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
|
||||
return (
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<ConfigProvider>
|
||||
<ConfigWrapper config={props.config}>
|
||||
<MockProviderProvider>
|
||||
<DialogProvider>
|
||||
<LanguageContext.Provider
|
||||
@@ -239,7 +254,7 @@ export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
|
||||
</LanguageContext.Provider>
|
||||
</DialogProvider>
|
||||
</MockProviderProvider>
|
||||
</ConfigProvider>
|
||||
</ConfigWrapper>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
)
|
||||
|
||||
@@ -782,6 +782,58 @@ export const PermissionDockSubagent: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 17. Permission dock — bash with pre-populated config rules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const bashConfigPermission: PermissionRequest = {
|
||||
id: "perm-bash-config-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "bash",
|
||||
patterns: ["npm install"],
|
||||
always: ["npm install *"],
|
||||
args: {
|
||||
command: "npm install",
|
||||
rules: ["npm *", "npm install", "npm run *", "npm test", "git *", "npx *"],
|
||||
},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-bash-config-001" },
|
||||
}
|
||||
|
||||
export const PermissionDockConfigPreloaded: Story = {
|
||||
name: "Permission Dock — config pre-populated",
|
||||
render: () => {
|
||||
const perms = [bashConfigPermission]
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders
|
||||
permissions={perms}
|
||||
sessionID={SESSION_ID}
|
||||
status="busy"
|
||||
noPadding
|
||||
config={{
|
||||
permission: {
|
||||
bash: {
|
||||
"*": "ask",
|
||||
"npm *": "allow",
|
||||
"npm install": "allow",
|
||||
"git *": "deny",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "400px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 17. MCP tool cards — collapsed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user