Merge branch 'main' into kirillk/model-picker

This commit is contained in:
Kirill Kalishev
2026-03-17 15:07:42 -04:00
committed by GitHub
58 changed files with 561 additions and 123 deletions
+1
View File
@@ -1868,6 +1868,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
sessionID: perm.sessionID,
toolName: perm.permission,
patterns: perm.patterns,
always: perm.always,
args: perm.metadata,
message: `Permission required: ${perm.permission}`,
tool: perm.tool,
@@ -167,6 +167,7 @@ export type WebviewMessage =
sessionID: string
toolName: string
patterns: string[]
always: string[]
args: Record<string, unknown>
message: string
tool?: { messageID: string; callID: string }
@@ -240,6 +241,7 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
sessionID: event.properties.sessionID,
toolName: event.properties.permission,
patterns: event.properties.patterns ?? [],
always: event.properties.always ?? [],
args: event.properties.metadata,
message: `Permission required: ${event.properties.permission}`,
tool: event.properties.tool,
@@ -160,22 +160,6 @@ test.describe("Permission Dock Dropdown — write", () => {
const root = page.locator("#storybook-root")
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-expanded-pending.png"])
})
test("rules expanded — mixed decisions", async ({ page }) => {
await page.goto(storyUrl(STORY_ID), { waitUntil: "load" })
await disableAnimations(page)
await page.waitForSelector("#storybook-root *", { state: "attached" })
await openDropdown(page)
// Approve first rule, deny second (dispatchEvent bypasses tooltip overlays)
const approveButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="approve"]')
const denyButtons = page.locator('[data-slot="permission-rule-toggle"][data-variant="deny"]')
await approveButtons.first().dispatchEvent("click")
await denyButtons.nth(1).dispatchEvent("click")
const root = page.locator("#storybook-root")
await expect(root).toHaveScreenshot(["permission-dock-dropdown", "write-rules-mixed.png"])
})
})
// ---------------------------------------------------------------------------
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:321afc41e5908e73656b1a8d5e3c32f23f54d10dee12655506b4b58348230305
size 13718
oid sha256:3ef8ec5886054bf0e7ae288c66ce5175204e74728c3e37f3669c551be98755b7
size 12476
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:666d73c69b19e8c79815129b9d17747dbf755c316f3e6da0cba6977966b9cccc
size 13906
oid sha256:54ff3bd3916245aa1dc65384db4a0b5908b4d78cc3700157112f1be63ce054f9
size 12663
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0608142ccb5bab0468ae44060bed17c5ba98b8ee2d5a477627644d1c91e760a
size 13952
oid sha256:9edcfe9f76314110e9c1795ee0008ca35187ce28a19c86cfcf1f8bf499fe151c
size 12714
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7dd67617fdfc9ea6c4b0f54895d930d76c5c712b158e4be93967a3a3239a53b2
size 14143
oid sha256:4e314513c9b4e964ea3b9a7cdbfc561e57eab2fa867aadfd370d1c0ad68c11b8
size 12903
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43b16bc81164f310ae40f7ebe6459281f374be323dfc07e3bef6b2d1608b6637
size 20835
oid sha256:a3b9925b5424dd81e0fd60dc3cc4307730abdc010cccd73e7243292880dd6b09
size 16833
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee48a78cd2fe43c34d0014262ac794e48e5405d2116140d08219fc8e1621be0c
size 18516
oid sha256:ef1d55223be06fd9d16566bc80a525c84c933a337ee4ccf2c41809e4984571b6
size 17276
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f69b6545bd98003edf8c7208eb655066a24847e055cf0a5492c99ff67bbbab38
size 14621
oid sha256:5fb585654180101c29bd4e7efbc7129bf29e8681bc764cdae0d8233fe302a29e
size 12711
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a7db3c77ca7dee3a74fe3fd08b45e8d32a00193d1fb13ec7b4e2b1671dac7a3
size 14831
oid sha256:c0702dda15977143eeac1557040a583946f2fed4b1610c97a772cff4ae3f2482
size 12842
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4aa38c3ade48132b3f0083438ef71c0e34f180db3bbd4d70455d6bb34a1225c
size 19202
oid sha256:9784fcf6612167075eb796887c9ac6ad8ab1596ef03dd0ace2b159f82dcb3675
size 17978
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e46b79adada26121c948ce33b78ba78c8b0b13dc153708b60a56f1cfc6034503
size 20095
oid sha256:606cc8d90900a26682852c9eb14d24305f31672ecd40265954b7b85cca7d3c48
size 18850
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:699b49cbb768c3a9cea64878a01b5507a1f304a14f12363f2329ad07c8cc35cb
size 15082
oid sha256:ab71bff42d0bd60c84f75cc04d91fdae8516667913152a0ae4e792268368c9d1
size 13844
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60e6d4b1d0e860195c26752e50a97e6d22d7911d029e7d08d24f46b327b21a01
size 14075
oid sha256:8d37fd5942ec471c7f534b7b65ac568cdde7cd1b7f3542c50a72500ac3823cae
size 12827
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ff80ac6cd76dbb278e6483c2e5c352095fc893ae5f5d5cb2eb70ded640bfc0e
size 16913
oid sha256:04377b0ddbaa0e2f4f126adff431db902fbff65ce653f2bc59434d150a833578
size 15638
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6531e4d0e975a29d538bc7b609a987071afc0997b396fd06e52cbbf2b7ef2e26
size 17467
@@ -284,6 +284,7 @@ describe("mapSSEEventToWebviewMessage", () => {
expect(msg.permission.args).toEqual({ path: "/foo" })
expect(msg.permission.message).toBe("Permission required: read_file")
expect(msg.permission.patterns).toEqual(["**/*.ts"])
expect(msg.permission.always).toEqual([])
}
})
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de6be977bd3805fdbbbb1fdaf3498201c1097adea26ebe460c56b681d1828e39
size 9457
oid sha256:c5ada07e94ff10490732ba4b27d8e204102816aa00e472324a31ee9f8c7e8484
size 9470
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3164d510232a0457cf935a3032f736825486f133e89994e544c678ccc7e6849a
size 30652
oid sha256:0bbec9564c9fa46fce155ada8e176641e540c912461102b7b424d317b3283c38
size 31033
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc40d7bbc5f3eaee3b48b4f90046a10e54db3288e71214eec190e4b0fa61cc16
size 22777
oid sha256:48ebccc3ab71403efa74803f140b17d97a000989b64dd42c65d8f9774d059cb8
size 23139
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3da4a2d29601e45eb4b5fe007a62a203c2b5151b2d74437d22630d2ddc8582a6
size 28723
oid sha256:371621a81d9c42d1dc327492fb2e3d2516857dfa189adf3c924ac6951d33e910
size 29073
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc5f6d5b31970c33aaf062756bbdd73637db17f1bc74c1558e9a81c33c5d602d
size 4582
oid sha256:742b7939e4b5a803413aad42418fe60f8d138bf6153087baae8cc22fba4202ae
size 4755
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cd828877cfcbb7d3f3af475c2ca5f92b2fd627ae41f1a03725ab91a948b40833
size 4704
oid sha256:5ab73f8c0585e9f889c51073428aa0e7082d7aecdc3d5b0c459720c1b860c436
size 4890
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf635f68a3a3ea84caa026f3be0481f21a2a7424acd8d2fdc138f8018f843af8
oid sha256:0e29bb3e5a3d408f6a8edabbf12a59a53c3e39e7fd3efa7cadc0e3c7c380e5cc
size 18908
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb8dd7f0ecbbeed00e1321c050964b54f832a571c8acbc37269de205bd64a483
size 13788
oid sha256:c6226c4c30415c1e8d1103003464f3a7c86df362bb7b49d2d37400acfd4c1372
size 12528
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:484ec50824ececf29d8b1464fb94ef48637d4f97b9a3c68dcf3ae5d8f157dcf6
size 15502
oid sha256:fb035f3cea015f0a5e693ac3bef31848c5c596642948ddd3f524699cd3637b23
size 14245
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:22c9bc250a347451c3a194f3ec701bfaf9db67f3104d12bde92adffc08bcd30b
size 6263
oid sha256:61c9ddc7067281976b9183e0696952d1ed2f3453c67f54b3c8766ddf1f9b3ba5
size 6402
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f7fd08433524c113a2d7b3fed8bef5c023d9a451621f658b836c7967ca954aa
size 8310
oid sha256:52f85f9f63eccd6217f37616c525df311e4d0a558547ae127366a7388e859dee
size 8395
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab9edfc18d0d7094f7c5410d7b08e0012c2128f1638b7090aad9301f127473ed
size 17425
oid sha256:1cf3791372e3e69fd759f2596e72211c982e8694990103c178f1aed0c59229f1
size 16139
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05783f1a5b4c8631dee2582b243a724eb4fb811cde894f86ce3575e2da1a997e
size 17416
oid sha256:1f3dc016ba452b8eaf69c5f4532d0edc4915c43c345a62029fcbaad46f8ac902
size 16140
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24364efcbb54ed842a9af0c630e6b2a4f5df37d8cabb651e18dcbebda37a26c5
size 17221
oid sha256:469f10e66c7505ff9034663696fdefc400f21803612fb5df8a58f4f991155ac3
size 15945
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8e117613ac4378e82739c266c2e2f818e9ed79bc0e84e0ebe7b8f61c02b88c0
size 14836
oid sha256:36b72898b310b3c4e57fbc506e5ee663d74ed64ed2967d8a9d1201da50904bf0
size 13583
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2259e821d911a5455a4bdd72716d5ae1de1a5fd5b6aaad666d41846edac716cc
size 15079
oid sha256:6a1ebf498979d1e4371169657582cfb4fe0cf50f2dce9a666c8b3f66355057e1
size 13824
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3fc2414e502c0f064fc8d15e5f14ccdc5c250ded619b8362472d2a41fb522de9
size 15023
oid sha256:cce4f8672aba4f77ed12ecd59fa0b4846ee6673d59e91de748e5f3333f124292
size 13766
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:acb0efba94927a8ada6a16a66bd82160a1f41d320a2f512114da54aaa2cc1574
size 16143
oid sha256:2fc8f143da673f83abe1def14f87aa325ecfb8127de9c5f66c97096e43dc1f28
size 14876
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0dcef8172fb810da542e5d4f364a60fa8cb0d0d3a212ad939e0117ca91b5b57c
size 30461
oid sha256:b3f472c100d4432064af12da764bd44baa3295ad4b20c6158cc3fa467667797a
size 30794
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e3d24b86091c0041a1e7426209a1d1d826b656f04b9fed1d662e3d5b67aa95f
size 6185
oid sha256:74d53efec74b56fcd689b1b2de4facb0570306995a6f5451d38e8303b65bbcc6
size 6187
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c21917a9f4f80354ac18a7c4ced1f204a1b28bed4ac5fb0cf34e339dbdc7d5da
size 15929
oid sha256:38d2cef0a125b5b6bba81af716c1accd522a60e0c915c8f9a7be502382bfcb2b
size 14661
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2c75a7bb06035f07f364c7fb7a1eb0b813663658d018e4eea881c48cc0e276c
size 8698
oid sha256:ce16f0acdd7d82bb2b1bfd993e015cf89f1d20d50d2c045a6ad08fd4a6382c50
size 8931
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d027fc81aa110b8a3fa94816c18ddd7d104ed1fc103abd234c39e805637afce
size 12263
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a69ac58dc72885950a702661b75bc1989a28e8b07b3136037255891b7e5a3b06
size 12839
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1edca81fb2c946f87df5436684418e63b7a9c761079e44f024971a81316b8263
size 6549
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce4ba314abb23bb43694d512ade273d99760f250b061e235899d86e170bb5f9a
size 52892
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:631d41ecf6216249943722a797f75c3aefb2f84ebdb7ee80529662a26fa35af5
size 50206
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c925148c349c6ab1f14190a2a379aa24869de35e5b77455fa15d8868eb08343
size 5943
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30346f68598419d1fe80a3f33d3326cc844a30356ebedb022a841be4096514c6
size 53909
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e654374e5c12a4a184a0d27709cd773684e31e08a3d7b6305358849cf30c75be
size 50986
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9399ad494d6bc3302cc01f6f815763662c43d1eafe0b2ecff4b8eacb5dc5e8ef
size 10770
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7e17426d82ff00ac8194963a3c3be946994018e2f6c9553dfaee237cb043fd9
size 11324
@@ -4,7 +4,9 @@
* Uses kilo-ui's DockPrompt component for proper surface styling.
*
* Per-rule toggles allow users to approve/deny individual permission rules for future requests.
* The command buttons (Deny / Allow Always / Allow Once) control the current command.
* For bash, the hierarchical rules from metadata.rules are shown.
* For other tools, the always array is shown so users can configure per-tool permissions.
* The command buttons (Deny / Allow Once) control the current command.
* When all rules are toggled ✓, the command auto-runs.
*/
@@ -24,14 +26,14 @@ let rulesExpandedPreference = false
export const PermissionDock: Component<{
request: PermissionRequest
responding: boolean
onDecide: (response: "once" | "always" | "reject", approvedAlways: string[], deniedAlways: string[]) => void
onDecide: (response: "once" | "reject", approvedAlways: string[], deniedAlways: string[]) => void
}> = (props) => {
const session = useSession()
const language = useLanguage()
const fromChild = () => props.request.sessionID !== session.currentSessionID()
// Bash sends fine-grained rules via metadata.rules; other tools have no dropdown.
const rules = () => props.request.args?.rules ?? []
// Bash sends fine-grained rules via metadata.rules; other tools use the always array.
const rules = () => props.request.args?.rules ?? props.request.always ?? []
// Rules like "git *" or "git log *" — strip the trailing wildcard for display.
// A bare "*" (global wildcard) becomes empty so only the tool name shows.
const label = (rule: string) => (rule === "*" ? "" : rule.replace(/ \*$/, ""))
@@ -43,7 +45,6 @@ export const PermissionDock: Component<{
const [decisions, setDecisions] = createSignal<Record<number, RuleDecision>>({})
const [expanded, setExpanded] = createSignal(rulesExpandedPreference)
const hasDenied = () => Object.values(decisions()).some((d) => d === "denied")
const hasRules = () => rules().length > 0
const toggleExpanded = () => {
@@ -191,17 +192,6 @@ export const PermissionDock: Component<{
>
{language.t("ui.permission.allowOnce")}
</Button>
<Button
variant="secondary"
size="small"
onClick={() => {
const { approved, denied } = collectRules()
props.onDecide("always", approved, denied)
}}
disabled={props.responding || hasDenied()}
>
{language.t("ui.permission.allowAlways")}
</Button>
<Button
variant="ghost"
size="small"
@@ -162,7 +162,8 @@ const globPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "glob",
patterns: ["**/*.md"],
args: { pattern: "**/*.md", rules: ["**/*.md"] },
always: ["*"],
args: { pattern: "**/*.md" },
tool: { messageID: ASST_MSG_ID, callID: "call-glob-001" },
}
@@ -171,6 +172,7 @@ const bashPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "bash",
patterns: ["bun test"],
always: ["bun *"],
args: { command: "bun test", rules: ["bun *", "bun test"] },
tool: { messageID: ASST_MSG_ID, callID: "call-bash-001" },
}
@@ -180,7 +182,8 @@ const dockPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "write",
patterns: ["src/main.tsx", "src/utils.ts"],
args: { rules: ["src/main.tsx", "src/utils.ts"] },
always: ["*"],
args: {},
// No `tool` field — this is a non-tool (dock) permission
}
@@ -292,7 +295,8 @@ const todoWritePermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "todowrite",
patterns: ["*"],
args: { rules: ["*"] },
always: ["*"],
args: {},
tool: { messageID: ASST_MSG_ID, callID: "call-todo-001" },
}
@@ -612,7 +616,8 @@ const editPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "edit",
patterns: ["src/components/App.tsx", "src/utils/helpers.ts"],
args: { rules: ["src/components/App.tsx", "src/utils/helpers.ts"] },
always: ["*"],
args: {},
tool: { messageID: ASST_MSG_ID, callID: "call-edit-001" },
}
@@ -645,7 +650,8 @@ const websearchPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "websearch",
patterns: ["*"],
args: { rules: ["*"] },
always: ["*"],
args: {},
tool: { messageID: ASST_MSG_ID, callID: "call-websearch-001" },
}
@@ -677,8 +683,9 @@ const externalDirPermission: PermissionRequest = {
id: "perm-extdir-001",
sessionID: SESSION_ID,
toolName: "external_directory",
patterns: ["/home/user/other-project/config.json"],
args: { rules: ["/home/user/other-project/config.json"] },
patterns: ["/home/user/other-project/*"],
always: ["/home/user/other-project/*"],
args: { filepath: "/home/user/other-project/config.json" },
tool: { messageID: ASST_MSG_ID, callID: "call-extdir-001" },
}
@@ -711,6 +718,7 @@ const bashManyRulesPermission: PermissionRequest = {
sessionID: SESSION_ID,
toolName: "bash",
patterns: ["npm install"],
always: ["npm install *"],
args: {
command: "npm install",
rules: ["npm *", "npm install", "npm run *", "npm test", "npm run build", "npx *"],
@@ -747,6 +755,7 @@ const subagentPermission: PermissionRequest = {
sessionID: "child-session-001",
toolName: "bash",
patterns: ["git status"],
always: ["git status *"],
args: { command: "git status", rules: ["git *", "git status"] },
tool: { messageID: ASST_MSG_ID, callID: "call-subagent-001" },
}
@@ -10,7 +10,12 @@ import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { StoryProviders } from "./StoryProviders"
import { MarketplaceListView } from "../components/marketplace/MarketplaceListView"
import { ItemCard } from "../components/marketplace/ItemCard"
import type { SkillMarketplaceItem, MarketplaceInstalledMetadata } from "../types/marketplace"
import type {
SkillMarketplaceItem,
McpMarketplaceItem,
ModeMarketplaceItem,
MarketplaceInstalledMetadata,
} from "../types/marketplace"
import "../components/marketplace/marketplace.css"
const meta: Meta = {
@@ -98,13 +103,160 @@ const MOCK_SKILLS: SkillMarketplaceItem[] = [
},
]
const MOCK_MCPS: McpMarketplaceItem[] = [
{
type: "mcp",
id: "github-mcp",
name: "GitHub",
description:
"Interact with GitHub repositories, issues, and pull requests. Search code, manage branches, and automate workflows.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
content:
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } }',
parameters: [{ name: "GitHub Token", key: "GITHUB_TOKEN", placeholder: "ghp_xxxxxxxxxxxx" }],
author: "Anthropic",
tags: ["version-control", "development"],
},
{
type: "mcp",
id: "postgres-mcp",
name: "PostgreSQL",
description: "Query and manage PostgreSQL databases. Run SQL, inspect schemas, and manage connections.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
content: [
{
name: "npx",
content:
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres", "${CONNECTION_STRING}"] }',
parameters: [
{ name: "Connection String", key: "CONNECTION_STRING", placeholder: "postgresql://user:pass@localhost/db" },
],
},
{
name: "Docker",
content:
'{ "command": "docker", "args": ["run", "--rm", "-e", "CONNECTION_STRING=${CONNECTION_STRING}", "mcp/postgres"] }',
parameters: [
{ name: "Connection String", key: "CONNECTION_STRING", placeholder: "postgresql://user:pass@localhost/db" },
],
prerequisites: ["Docker must be installed and running"],
},
],
author: "Anthropic",
tags: ["database", "sql"],
},
{
type: "mcp",
id: "filesystem-mcp",
name: "Filesystem",
description: "Read, write, and manage files on the local filesystem with configurable access controls.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "${ALLOWED_DIR}"] }',
parameters: [{ name: "Allowed Directory", key: "ALLOWED_DIR", placeholder: "/path/to/directory" }],
tags: ["filesystem", "development"],
},
{
type: "mcp",
id: "slack-mcp",
name: "Slack",
description: "Send and receive messages, manage channels, and search conversations in Slack workspaces.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/slack",
content:
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-slack"], "env": { "SLACK_TOKEN": "${SLACK_TOKEN}" } }',
parameters: [{ name: "Slack Bot Token", key: "SLACK_TOKEN", placeholder: "xoxb-xxxxxxxxxxxx" }],
author: "Anthropic",
tags: ["communication", "productivity"],
},
{
type: "mcp",
id: "brave-search-mcp",
name: "Brave Search",
description: "Search the web using the Brave Search API for real-time information retrieval.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
content:
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "${BRAVE_API_KEY}" } }',
parameters: [{ name: "API Key", key: "BRAVE_API_KEY", placeholder: "BSA-xxxxxxxxxxxx" }],
tags: ["search", "web"],
},
{
type: "mcp",
id: "puppeteer-mcp",
name: "Puppeteer",
description: "Automate browser interactions, take screenshots, and scrape web pages using headless Chrome.",
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer",
content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-puppeteer"] }',
prerequisites: ["Chrome or Chromium must be installed"],
tags: ["browser", "automation", "web"],
},
]
const MOCK_MODES: ModeMarketplaceItem[] = [
{
type: "mode",
id: "architect",
name: "Architect",
description:
"High-level system design and planning. Focuses on architecture decisions, component boundaries, and technical specifications without writing implementation code.",
content: "name: Architect\nroleDefinition: You are a software architect...\ngroups:\n - read\n",
author: "Kilo",
tags: ["planning", "design"],
},
{
type: "mode",
id: "reviewer",
name: "Code Reviewer",
description:
"Reviews code for bugs, security issues, and best practices. Provides actionable feedback with specific line references.",
content: "name: Code Reviewer\nroleDefinition: You are a code reviewer...\ngroups:\n - read\n",
author: "Kilo",
tags: ["review", "quality"],
},
{
type: "mode",
id: "docs-writer",
name: "Documentation Writer",
description: "Generates and maintains documentation including READMEs, API docs, and inline code comments.",
content: "name: Documentation Writer\nroleDefinition: You write documentation...\ngroups:\n - read\n - edit\n",
tags: ["documentation", "writing"],
},
{
type: "mode",
id: "tdd",
name: "Test-Driven Developer",
description:
"Follows strict TDD methodology: write failing tests first, implement minimum code to pass, then refactor.",
content:
"name: Test-Driven Developer\nroleDefinition: You follow TDD...\ngroups:\n - read\n - edit\n - command\n",
author: "Community",
tags: ["testing", "methodology"],
},
{
type: "mode",
id: "debug",
name: "Debugger",
description: "Systematically diagnoses and fixes bugs. Uses logs, stack traces, and bisection to isolate issues.",
content: "name: Debugger\nroleDefinition: You are a debugger...\ngroups:\n - read\n - command\n",
tags: ["debugging", "troubleshooting"],
},
]
const EMPTY_METADATA: MarketplaceInstalledMetadata = { project: {}, global: {} }
const PARTIAL_INSTALLED: MarketplaceInstalledMetadata = {
const PARTIAL_INSTALLED_SKILLS: MarketplaceInstalledMetadata = {
project: { "nextjs-developer": { type: "skill" } },
global: { "python-data-science": { type: "skill" } },
}
const PARTIAL_INSTALLED_MCPS: MarketplaceInstalledMetadata = {
project: { "github-mcp": { type: "mcp" } },
global: { "postgres-mcp": { type: "mcp" } },
}
const PARTIAL_INSTALLED_MODES: MarketplaceInstalledMetadata = {
project: { architect: { type: "mode" } },
global: { reviewer: { type: "mode" } },
}
const noop = () => {}
// ---------------------------------------------------------------------------
@@ -138,7 +290,7 @@ export const SkillsTabWithInstalled: Story = {
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={MOCK_SKILLS}
metadata={PARTIAL_INSTALLED}
metadata={PARTIAL_INSTALLED_SKILLS}
fetching={false}
type="skill"
searchPlaceholder="Search skills..."
@@ -196,7 +348,7 @@ export const InstalledSkillCard: Story = {
<div style={{ width: "420px", padding: "12px" }}>
<ItemCard
item={MOCK_SKILLS[0]}
metadata={PARTIAL_INSTALLED}
metadata={PARTIAL_INSTALLED_SKILLS}
displayName={MOCK_SKILLS[0].displayName}
linkUrl={MOCK_SKILLS[0].githubUrl}
onInstall={noop}
@@ -206,3 +358,187 @@ export const InstalledSkillCard: Story = {
</StoryProviders>
),
}
// ---------------------------------------------------------------------------
// MCP Stories
// ---------------------------------------------------------------------------
export const McpTabWithItems: Story = {
name: "MCP tab — with items",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={MOCK_MCPS}
metadata={EMPTY_METADATA}
fetching={false}
type="mcp"
searchPlaceholder="Search MCP servers..."
emptyMessage="No MCP servers found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const McpTabWithInstalled: Story = {
name: "MCP tab — some installed",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={MOCK_MCPS}
metadata={PARTIAL_INSTALLED_MCPS}
fetching={false}
type="mcp"
searchPlaceholder="Search MCP servers..."
emptyMessage="No MCP servers found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const McpTabEmpty: Story = {
name: "MCP tab — empty state",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "400px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={[]}
metadata={EMPTY_METADATA}
fetching={false}
type="mcp"
searchPlaceholder="Search MCP servers..."
emptyMessage="No MCP servers found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const SingleMcpCard: Story = {
name: "ItemCard — single MCP not installed",
render: () => (
<StoryProviders>
<div style={{ width: "420px", padding: "12px" }}>
<ItemCard
item={MOCK_MCPS[0]}
metadata={EMPTY_METADATA}
linkUrl={MOCK_MCPS[0].url}
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const InstalledMcpCard: Story = {
name: "ItemCard — installed MCP",
render: () => (
<StoryProviders>
<div style={{ width: "420px", padding: "12px" }}>
<ItemCard
item={MOCK_MCPS[0]}
metadata={PARTIAL_INSTALLED_MCPS}
linkUrl={MOCK_MCPS[0].url}
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
// ---------------------------------------------------------------------------
// Mode Stories
// ---------------------------------------------------------------------------
export const ModesTabWithItems: Story = {
name: "Modes tab — with items",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={MOCK_MODES}
metadata={EMPTY_METADATA}
fetching={false}
type="mode"
searchPlaceholder="Search modes..."
emptyMessage="No modes found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const ModesTabWithInstalled: Story = {
name: "Modes tab — some installed",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={MOCK_MODES}
metadata={PARTIAL_INSTALLED_MODES}
fetching={false}
type="mode"
searchPlaceholder="Search modes..."
emptyMessage="No modes found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const ModesTabEmpty: Story = {
name: "Modes tab — empty state",
render: () => (
<StoryProviders>
<div style={{ width: "420px", height: "400px", overflow: "auto", padding: "12px" }}>
<MarketplaceListView
items={[]}
metadata={EMPTY_METADATA}
fetching={false}
type="mode"
searchPlaceholder="Search modes..."
emptyMessage="No modes found"
onInstall={noop}
onRemove={noop}
/>
</div>
</StoryProviders>
),
}
export const SingleModeCard: Story = {
name: "ItemCard — single mode not installed",
render: () => (
<StoryProviders>
<div style={{ width: "420px", padding: "12px" }}>
<ItemCard item={MOCK_MODES[0]} metadata={EMPTY_METADATA} onInstall={noop} onRemove={noop} />
</div>
</StoryProviders>
),
}
export const InstalledModeCard: Story = {
name: "ItemCard — installed mode",
render: () => (
<StoryProviders>
<div style={{ width: "420px", padding: "12px" }}>
<ItemCard item={MOCK_MODES[0]} metadata={PARTIAL_INSTALLED_MODES} onInstall={noop} onRemove={noop} />
</div>
</StoryProviders>
),
}
@@ -157,6 +157,7 @@ export interface PermissionRequest {
sessionID: string
toolName: string
patterns: string[]
always: string[]
args: Record<string, unknown> & { rules?: string[] }
message?: string
tool?: { messageID: string; callID: string }
+4 -1
View File
@@ -1563,7 +1563,10 @@ export namespace Config {
})
}
export async function updateGlobal(config: Info) {
// kilocode_change start — add dispose option to skip Instance.disposeAll for permission-only changes
export async function updateGlobal(config: Info, options?: { dispose?: boolean }) {
const dispose = options?.dispose ?? true
// kilocode_change end
const filepath = globalConfigFile()
const before = await Filesystem.readText(filepath).catch((err: any) => {
if (err.code === "ENOENT") return "{}"
+29 -5
View File
@@ -66,12 +66,36 @@ export namespace PermissionNext {
}
// kilocode_change start — inverse of fromConfig: convert rules back to config format
/**
* Permissions typed as PermissionAction in the config schema (scalar-only).
* These must be serialized as "allow"/"deny"/"ask", not as { "*": "allow" }.
*/
const SCALAR_ONLY_PERMISSIONS = new Set([
"todowrite",
"todoread",
"question",
"webfetch",
"websearch",
"codesearch",
"doom_loop",
])
export function toConfig(rules: Ruleset): Config.Permission {
const result: Config.Permission = {}
for (const rule of rules) {
const existing = result[rule.permission]
// Scalar-only permissions (e.g. websearch, todowrite, doom_loop) only
// accept PermissionAction ("allow"/"deny"/"ask"), not object form.
// Use scalar format for "*"; skip non-wildcard patterns (they can't be
// represented in the config schema — they only work in-memory).
if (SCALAR_ONLY_PERMISSIONS.has(rule.permission)) {
if (rule.pattern === "*") result[rule.permission] = rule.action
continue
}
if (existing === undefined) {
// Always use object format to avoid replacing existing granular rules
// Use object format to avoid replacing existing granular rules
// when merged via updateGlobal (e.g. { read: "allow" } would wipe
// { read: { "*": "ask", "src/*": "allow" } })
result[rule.permission] = { [rule.pattern]: rule.action }
@@ -194,15 +218,15 @@ export namespace PermissionNext {
const existing = s.pending[input.requestID]
if (!existing) throw new NotFoundError({ message: `Permission request ${input.requestID} not found` })
const validRules = new Set(existing.info.metadata?.rules ?? [])
// Combine metadata.rules (bash hierarchy) and always (all tools).
// Set preserves insertion order and deduplicates.
const validRules = new Set([...(existing.info.metadata?.rules ?? []), ...existing.info.always])
const permission = existing.info.permission
// Build rules in metadata.rules order so broader patterns come before
// specific ones, preserving intended precedence for evaluate(findLast).
const approvedSet = new Set(input.approvedAlways ?? [])
const deniedSet = new Set(input.deniedAlways ?? [])
const newRules: Ruleset = []
for (const pattern of existing.info.metadata?.rules ?? []) {
for (const pattern of validRules) {
if (approvedSet.has(pattern)) newRules.push({ permission, pattern, action: "allow" })
if (deniedSet.has(pattern)) newRules.push({ permission, pattern, action: "deny" })
}
@@ -82,7 +82,7 @@ describe("saveAlwaysRules", () => {
})
})
test("ignores patterns not in metadata.rules", async () => {
test("ignores patterns not in metadata.rules or always", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
@@ -97,7 +97,7 @@ describe("saveAlwaysRules", () => {
ruleset: [],
})
// "curl" is not in metadata.rules — should be silently ignored
// "curl" is not in metadata.rules or always — should be silently ignored
await PermissionNext.saveAlwaysRules({
requestID: "permission_3",
approvedAlways: ["npm install", "curl http://evil.com"],
@@ -133,6 +133,40 @@ describe("saveAlwaysRules", () => {
})
})
test("accepts patterns from always array (non-bash tools)", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const askPromise = PermissionNext.ask({
id: "permission_nonbash",
sessionID: "session_test",
permission: "read",
patterns: ["src/main.ts"],
metadata: {},
always: ["*"],
ruleset: [],
})
// "*" is in always — should be accepted even without metadata.rules
await PermissionNext.saveAlwaysRules({ requestID: "permission_nonbash", approvedAlways: ["*"] })
await PermissionNext.reply({ requestID: "permission_nonbash", reply: "once" })
await expect(askPromise).resolves.toBeUndefined()
// "*" wildcard should auto-allow any read
const result = await PermissionNext.ask({
sessionID: "session_test",
permission: "read",
patterns: ["any/file.ts"],
metadata: {},
always: [],
ruleset: [],
})
expect(result).toBeUndefined()
},
})
})
test("accepts hierarchy patterns from metadata.rules", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
@@ -60,3 +60,29 @@ test("toConfig - roundtrip with fromConfig (object)", () => {
const result = PermissionNext.toConfig(rules)
expect(result).toEqual(config)
})
test("toConfig - scalar-only permission uses scalar format", () => {
const result = PermissionNext.toConfig([{ permission: "websearch", pattern: "*", action: "allow" }])
expect(result).toEqual({ websearch: "allow" })
})
test("toConfig - scalar-only permission with non-wildcard pattern is skipped", () => {
// doom_loop uses always: [toolName], so pattern can be "bash" etc.
// Non-wildcard patterns for scalar-only permissions can't be represented
// in the config schema — they only work in-memory (known limitation).
const result = PermissionNext.toConfig([{ permission: "doom_loop", pattern: "bash", action: "allow" }])
expect(result).toEqual({})
})
test("toConfig - mixed scalar-only and rule-capable permissions", () => {
const result = PermissionNext.toConfig([
{ permission: "websearch", pattern: "*", action: "allow" },
{ permission: "todowrite", pattern: "*", action: "allow" },
{ permission: "bash", pattern: "npm *", action: "allow" },
])
expect(result).toEqual({
websearch: "allow",
todowrite: "allow",
bash: { "npm *": "allow" },
})
})