mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
Merge pull request #6868 from Kilo-Org/mark/permission-dock-replace-pattern
refactor: show permissions in dock above chatbox instead of inline
This commit is contained in:
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6cbba3354994879f21489539615bbeeb71cb944980c642d502fdab3c4500e73
|
||||
size 11000
|
||||
oid sha256:59b9045133b989b85e3748e4fe509b317e21761280f26b3ee46f837bccf3a1ad
|
||||
size 14489
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bcb19f99cbd4cbaf403e2224139ff47318c2618457975d4f957b481962031836
|
||||
size 9735
|
||||
oid sha256:ce43eb9d6415fdb082ff03ab15080219c6275f7ae1bd09689f7d020a5511347c
|
||||
size 15274
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2a225a66c7981745089a46f20841d5f214d23d905b8b31194cfba9f278a3763d
|
||||
size 10965
|
||||
oid sha256:cae28aa54297014cb01b26120197b3bcab0b0a250c21d90c1050de3e09ee622b
|
||||
size 10743
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6fcd2e69976f2c9a02b24e721e96638ec20a6711d3853d0795e2913315ca0b5
|
||||
size 14341
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ed8885168e17a44fee3cd34e32143a6514fce09c3cd143dcf3be3088164d696
|
||||
size 16827
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83076ce468f046d5a884d8abf38189718641803cc966cae255e75e238894ae72
|
||||
size 30673
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9777ee1cb00c3632e71c64d2518f86aab1e4199d52e95b36c0d6344ac042994a
|
||||
size 14844
|
||||
oid sha256:26b18e59c4eccd851e4cb11f62f4b7b09e0fce56e9922b1ec51a94a4d9aeae67
|
||||
size 17664
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
* Unlike the upstream AssistantParts, this renders each read/glob/grep/list tool
|
||||
* individually for maximum verbosity in the VS Code sidebar context.
|
||||
*
|
||||
* Permissions and questions with a tool context are rendered inline with their
|
||||
* tool call rather than in the bottom dock.
|
||||
* Questions with a tool context are rendered inline with their tool call.
|
||||
* Permissions are rendered in the bottom dock (PermissionDock).
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createMemo } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import type {
|
||||
AssistantMessage as SDKAssistantMessage,
|
||||
Part as SDKPart,
|
||||
@@ -20,24 +19,20 @@ import type {
|
||||
} from "@kilocode/sdk/v2"
|
||||
import { useData } from "@kilocode/kilo-ui/context/data"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { QuestionDock } from "./QuestionDock"
|
||||
|
||||
// Tools that the upstream message-part renderer suppresses (returns null for).
|
||||
// We render these ourselves via ToolRegistry when they have a pending permission
|
||||
// or when they complete, so the user can see what the AI set up.
|
||||
// We also use this set in ChatView to know NOT to block the prompt input for
|
||||
// these tool permissions (since they're shown inline in the message stream).
|
||||
// We render these ourselves via ToolRegistry when they complete,
|
||||
// so the user can see what the AI set up.
|
||||
export const UPSTREAM_SUPPRESSED_TOOLS = new Set(["todowrite", "todoread"])
|
||||
|
||||
function isRenderable(part: SDKPart, pendingPermissionCallIDs: Set<string>): boolean {
|
||||
function isRenderable(part: SDKPart): boolean {
|
||||
if (part.type === "tool") {
|
||||
const tool = (part as SDKPart & { tool: string }).tool
|
||||
const state = (part as SDKPart & { state: { status: string } }).state
|
||||
if (UPSTREAM_SUPPRESSED_TOOLS.has(tool)) {
|
||||
const callID = (part as SDKPart & { callID: string }).callID
|
||||
// Show todo parts when waiting for permission (inline) or when completed (to show what happened)
|
||||
return pendingPermissionCallIDs.has(callID) || state.status === "completed"
|
||||
// Show todo parts only when completed (permissions are now in the dock)
|
||||
return state.status === "completed"
|
||||
}
|
||||
if (tool === "question" && (state.status === "pending" || state.status === "running")) return false
|
||||
return true
|
||||
@@ -76,52 +71,29 @@ function TodoToolCard(props: { part: ToolPart }) {
|
||||
export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
const data = useData()
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
|
||||
const id = () => session.currentSessionID()
|
||||
const permissions = () => session.permissions().filter((p) => p.sessionID === id() && p.tool)
|
||||
const questions = () => session.questions().filter((q) => q.sessionID === id() && q.tool)
|
||||
|
||||
const pendingPermissionCallIDs = createMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
for (const p of permissions()) {
|
||||
if (p.tool?.messageID === props.message.id) ids.add(p.tool.callID)
|
||||
}
|
||||
return ids
|
||||
})
|
||||
const questions = () => session.questions().filter((q) => q.sessionID === session.currentSessionID() && q.tool)
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const stored = data.store.part?.[props.message.id]
|
||||
if (!stored) return []
|
||||
return (stored as SDKPart[]).filter((part) => isRenderable(part, pendingPermissionCallIDs()))
|
||||
return (stored as SDKPart[]).filter((part) => isRenderable(part))
|
||||
})
|
||||
|
||||
const permissionForPart = (part: SDKPart) => {
|
||||
if (part.type !== "tool") return undefined
|
||||
const callID = (part as SDKPart & { callID: string }).callID
|
||||
return permissions().find((p) => p.tool!.callID === callID && p.tool!.messageID === props.message.id)
|
||||
}
|
||||
|
||||
// Questions linked to this message (rendered after the last part)
|
||||
const questionForMessage = () => questions().find((q) => q.tool!.messageID === props.message.id)
|
||||
|
||||
const decide = (permissionId: string, response: "once" | "always" | "reject") => {
|
||||
if (session.respondingPermissions().has(permissionId)) return
|
||||
session.respondToPermission(permissionId, response)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<For each={parts()}>
|
||||
{(part) => {
|
||||
const perm = () => permissionForPart(part)
|
||||
// Upstream PART_MAPPING["tool"] returns null for todowrite/todoread,
|
||||
// so we detect them here and render via ToolRegistry directly.
|
||||
const isUpstreamSuppressed =
|
||||
part.type === "tool" && UPSTREAM_SUPPRESSED_TOOLS.has((part as SDKPart & { tool: string }).tool)
|
||||
return (
|
||||
<Show when={isUpstreamSuppressed || PART_MAPPING[part.type]}>
|
||||
<div data-component="tool-part-wrapper" data-permission={!!perm()} data-part-type={part.type}>
|
||||
<div data-component="tool-part-wrapper" data-part-type={part.type}>
|
||||
<Show
|
||||
when={isUpstreamSuppressed}
|
||||
fallback={
|
||||
@@ -135,59 +107,6 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
>
|
||||
<TodoToolCard part={part as unknown as ToolPart} />
|
||||
</Show>
|
||||
<Show when={perm()} keyed>
|
||||
{(p) => {
|
||||
const isTodoPerm = UPSTREAM_SUPPRESSED_TOOLS.has(p.toolName)
|
||||
// For todo tools: show the friendly operation description instead of raw patterns like '*'
|
||||
// For other tools: show patterns only if they're meaningful (not just '*')
|
||||
const meaningfulPatterns = p.patterns.filter((pat) => pat !== "*")
|
||||
return (
|
||||
<div data-component="permission-prompt" onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<Show when={!isTodoPerm && meaningfulPatterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={meaningfulPatterns}>
|
||||
{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isTodoPerm}>
|
||||
<p data-slot="permission-description">
|
||||
{p.toolName === "todowrite"
|
||||
? language.t("settings.permissions.tool.todowrite.description")
|
||||
: language.t("settings.permissions.tool.todoread.description")}
|
||||
</p>
|
||||
</Show>
|
||||
<div data-slot="permission-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "reject")}
|
||||
disabled={session.respondingPermissions().has(p.id)}
|
||||
>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "always")}
|
||||
disabled={session.respondingPermissions().has(p.id)}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "once")}
|
||||
disabled={session.respondingPermissions().has(p.id)}
|
||||
>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">{language.t("ui.permission.sessionHint")}</p>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
* Main chat container that combines all chat components
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createEffect, on, onCleanup, onMount } from "solid-js"
|
||||
import { Component, Show, createEffect, on, onCleanup, onMount } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { TaskHeader } from "./TaskHeader"
|
||||
import { MessageList } from "./MessageList"
|
||||
import { PromptInput } from "./PromptInput"
|
||||
import { QuestionDock } from "./QuestionDock"
|
||||
import { UPSTREAM_SUPPRESSED_TOOLS } from "./AssistantMessage"
|
||||
import { PermissionDock } from "./PermissionDock"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useWorktreeMode } from "../../context/worktree-mode"
|
||||
import type { PermissionRequest } from "../../types/messages"
|
||||
|
||||
interface ChatViewProps {
|
||||
onSelectSession?: (id: string) => void
|
||||
@@ -41,16 +39,14 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
const allPermissions = () => session.permissions()
|
||||
const allQuestions = () => session.questions()
|
||||
|
||||
// Bottom-dock permission: prefer current-session non-tool permissions,
|
||||
// Bottom-dock permission: prefer current-session permissions,
|
||||
// then fall back to any pending permission (including child sessions).
|
||||
const questionRequest = () =>
|
||||
allQuestions().find((q) => q.sessionID === id() && !q.tool) ?? allQuestions().find((q) => !q.tool) ?? allQuestions()[0]
|
||||
const permissionRequest = () =>
|
||||
allPermissions().find((p) => p.sessionID === id() && !p.tool) ?? allPermissions().find((p) => !p.tool) ?? allPermissions()[0]
|
||||
// Only block the prompt when there's a non-inline permission or any question pending
|
||||
// (todo permissions are shown inline, not in the bottom dock)
|
||||
const isInlinePermission = (p: PermissionRequest) => p.tool && UPSTREAM_SUPPRESSED_TOOLS.has(p.toolName)
|
||||
const blocked = () => allPermissions().some((p) => !isInlinePermission(p)) || allQuestions().length > 0
|
||||
allQuestions().find((q) => q.sessionID === id() && !q.tool) ??
|
||||
allQuestions().find((q) => !q.tool) ??
|
||||
allQuestions()[0]
|
||||
const permissionRequest = () => allPermissions().find((p) => p.sessionID === id()) ?? allPermissions()[0]
|
||||
const blocked = () => allPermissions().length > 0 || allQuestions().length > 0
|
||||
|
||||
// When a bottom-dock permission/question disappears while the session is busy,
|
||||
// the scroll container grows taller. Dispatch a custom event so MessageList can
|
||||
@@ -95,60 +91,13 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
{(req) => <QuestionDock request={req} />}
|
||||
</Show>
|
||||
<Show when={permissionRequest()} keyed>
|
||||
{(perm) => {
|
||||
const fromChild = () => perm.sessionID !== id()
|
||||
const subtitle = () => fromChild() ? `${perm.toolName} (subagent)` : perm.toolName
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-permission="true">
|
||||
<BasicTool
|
||||
icon="checklist"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: language.t("notification.permission.title"),
|
||||
subtitle: subtitle(),
|
||||
}}
|
||||
>
|
||||
<Show when={perm.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={perm.patterns}>
|
||||
{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => decide("reject")}
|
||||
disabled={session.respondingPermissions().has(perm.id)}
|
||||
>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => decide("always")}
|
||||
disabled={session.respondingPermissions().has(perm.id)}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => decide("once")}
|
||||
disabled={session.respondingPermissions().has(perm.id)}
|
||||
>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">{language.t("ui.permission.sessionHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
{(perm) => (
|
||||
<PermissionDock
|
||||
request={perm}
|
||||
responding={session.respondingPermissions().has(perm.id)}
|
||||
onDecide={decide}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={hasMessages() && idle() && !blocked()}>
|
||||
<div class="new-task-button-wrapper">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* PermissionDock component
|
||||
* Displays permission requests from the AI assistant in the dock above the prompt input.
|
||||
* Uses kilo-ui's DockPrompt component for proper surface styling.
|
||||
* Modeled after the app's SessionPermissionDock.
|
||||
*/
|
||||
|
||||
import { Component, For, Show } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { DockPrompt } from "@kilocode/kilo-ui/dock-prompt"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import type { PermissionRequest } from "../../types/messages"
|
||||
|
||||
export const PermissionDock: Component<{
|
||||
request: PermissionRequest
|
||||
responding: boolean
|
||||
onDecide: (response: "once" | "always" | "reject") => void
|
||||
}> = (props) => {
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
|
||||
const fromChild = () => props.request.sessionID !== session.currentSessionID()
|
||||
|
||||
const toolDescription = () => {
|
||||
const key = `settings.permissions.tool.${props.request.toolName}.description`
|
||||
const value = language.t(key as Parameters<typeof language.t>[0])
|
||||
if (value === key) return ""
|
||||
return value
|
||||
}
|
||||
|
||||
const subtitle = () => (fromChild() ? `${props.request.toolName} (subagent)` : props.request.toolName)
|
||||
|
||||
return (
|
||||
<DockPrompt
|
||||
kind="permission"
|
||||
header={
|
||||
<div data-slot="permission-row" data-variant="header">
|
||||
<span data-slot="permission-icon">
|
||||
<Icon name="warning" size="normal" />
|
||||
</span>
|
||||
<div data-slot="permission-header-title">
|
||||
{language.t("notification.permission.title")}
|
||||
<span data-slot="permission-header-subtitle">{subtitle()}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<div />
|
||||
<div data-slot="permission-footer-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => props.onDecide("reject")} disabled={props.responding}>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => props.onDecide("always")}
|
||||
disabled={props.responding}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Show when={toolDescription()}>
|
||||
<div data-slot="permission-row">
|
||||
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||
<div data-slot="permission-hint">{toolDescription()}</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.request.patterns.length > 0}>
|
||||
<div data-slot="permission-row">
|
||||
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||
<div data-slot="permission-patterns">
|
||||
<For each={props.request.patterns}>
|
||||
{(pattern) => <code data-slot="permission-pattern">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</DockPrompt>
|
||||
)
|
||||
}
|
||||
@@ -7,10 +7,9 @@
|
||||
* Call registerExpandedTaskTool() once at app startup to activate.
|
||||
*/
|
||||
|
||||
import { Component, createEffect, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Component, createEffect, createMemo, For, Show } from "solid-js"
|
||||
import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useData } from "@kilocode/kilo-ui/context/data"
|
||||
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
|
||||
@@ -64,36 +63,6 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
|
||||
return getSessionToolParts(data.store, id)
|
||||
})
|
||||
|
||||
// Permission from child session
|
||||
const childPermission = createMemo(() => {
|
||||
const id = childSessionId()
|
||||
if (!id) return undefined
|
||||
const perms = data.store.permission?.[id] as unknown[]
|
||||
return (perms as PermissionRequest[] | undefined)?.[0]
|
||||
})
|
||||
|
||||
const childToolPart = createMemo(() => {
|
||||
const perm = childPermission()
|
||||
if (!perm || !perm.tool) return undefined
|
||||
const id = childSessionId()
|
||||
if (!id) return undefined
|
||||
const messages = (data.store.message?.[id] as SDKMessage[] | undefined) ?? []
|
||||
const msg = [...messages].reverse().find((m: SDKMessage) => m.id === perm.tool!.messageID)
|
||||
if (!msg) return undefined
|
||||
const parts = (data.store.part?.[msg.id] as ToolPart[] | undefined) ?? []
|
||||
return parts.find((p) => p.type === "tool" && p.callID === perm.tool!.callID) as ToolPart | undefined
|
||||
})
|
||||
|
||||
const respond = (response: "once" | "always" | "reject") => {
|
||||
const perm = childPermission()
|
||||
if (!perm || !data.respondToPermission) return
|
||||
data.respondToPermission({
|
||||
sessionID: perm.sessionID,
|
||||
permissionID: perm.id,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
const autoScroll = createAutoScroll({
|
||||
working: running,
|
||||
overflowAnchor: "auto",
|
||||
@@ -112,100 +81,40 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
|
||||
// Render the child tool card that triggered a permission (shown when permission is pending)
|
||||
const renderChildToolCard = () => {
|
||||
const part = childToolPart()
|
||||
if (!part) return null
|
||||
const render = ToolRegistry.render(part.tool)
|
||||
if (!render) return null
|
||||
const metadata = (part.state as { metadata?: Record<string, unknown> })?.metadata ?? {}
|
||||
const input = part.state?.input ?? {}
|
||||
const output = (part.state as { output?: string })?.output
|
||||
return render({
|
||||
input: input as Record<string, unknown>,
|
||||
tool: part.tool,
|
||||
metadata: metadata as Record<string, unknown>,
|
||||
output,
|
||||
status: part.state.status,
|
||||
defaultOpen: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-permission={!!childPermission()}>
|
||||
<Switch>
|
||||
{/* Branch 1: pending permission from child session */}
|
||||
<Match when={childPermission()}>
|
||||
<>
|
||||
<Show when={childToolPart()} fallback={<BasicTool icon="task" defaultOpen trigger={trigger()} />}>
|
||||
{renderChildToolCard()}
|
||||
</Show>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
||||
{i18n.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
||||
{i18n.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
||||
{i18n.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">{i18n.t("ui.permission.sessionHint")}</p>
|
||||
</div>
|
||||
</>
|
||||
</Match>
|
||||
|
||||
{/* Branch 2: normal — compact list of child tool calls */}
|
||||
<Match when={true}>
|
||||
<BasicTool icon="task" status={props.status} trigger={trigger()} defaultOpen>
|
||||
<div
|
||||
ref={autoScroll.scrollRef}
|
||||
onScroll={autoScroll.handleScroll}
|
||||
data-component="tool-output"
|
||||
data-scrollable
|
||||
>
|
||||
<div ref={autoScroll.contentRef} data-component="task-tools">
|
||||
<For each={childToolParts()}>
|
||||
{(item) => {
|
||||
const info = createMemo(() => getToolInfo(item.tool, item.state?.input))
|
||||
const subtitle = createMemo(() => {
|
||||
if (info().subtitle) return info().subtitle
|
||||
const state = item.state as { status: string; title?: string }
|
||||
if (state.status === "completed" || state.status === "running") {
|
||||
return state.title
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
return (
|
||||
<div data-slot="task-tool-item">
|
||||
<Icon name={info().icon} size="small" />
|
||||
<span data-slot="task-tool-title">{info().title}</span>
|
||||
<Show when={subtitle()}>
|
||||
<span data-slot="task-tool-subtitle">{subtitle()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</BasicTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div data-component="tool-part-wrapper">
|
||||
<BasicTool icon="task" status={props.status} trigger={trigger()} defaultOpen>
|
||||
<div ref={autoScroll.scrollRef} onScroll={autoScroll.handleScroll} data-component="tool-output" data-scrollable>
|
||||
<div ref={autoScroll.contentRef} data-component="task-tools">
|
||||
<For each={childToolParts()}>
|
||||
{(item) => {
|
||||
const info = createMemo(() => getToolInfo(item.tool, item.state?.input))
|
||||
const subtitle = createMemo(() => {
|
||||
if (info().subtitle) return info().subtitle
|
||||
const state = item.state as { status: string; title?: string }
|
||||
if (state.status === "completed" || state.status === "running") {
|
||||
return state.title
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
return (
|
||||
<div data-slot="task-tool-item">
|
||||
<Icon name={info().icon} size="small" />
|
||||
<span data-slot="task-tool-title">{info().title}</span>
|
||||
<Show when={subtitle()}>
|
||||
<span data-slot="task-tool-subtitle">{subtitle()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</BasicTool>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Minimal PermissionRequest shape we need
|
||||
interface PermissionRequest {
|
||||
id: string
|
||||
sessionID: string
|
||||
tool?: { messageID: string; callID: string }
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the upstream "task" tool registration with the v1.0.25-style renderer.
|
||||
* Must be called once at app startup.
|
||||
|
||||
@@ -8,18 +8,10 @@
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "storybook-solidjs-vite"
|
||||
import { For, Show } from "solid-js"
|
||||
import { Part } from "@kilocode/kilo-ui/message-part"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import type {
|
||||
AssistantMessage as SDKAssistantMessage,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
Message as SDKMessage,
|
||||
} from "@kilocode/sdk/v2"
|
||||
import type { AssistantMessage as SDKAssistantMessage, TextPart, ToolPart } from "@kilocode/sdk/v2"
|
||||
import { StoryProviders, defaultMockData, mockSessionValue } from "./StoryProviders"
|
||||
import { AssistantMessage } from "../components/chat/AssistantMessage"
|
||||
import { ChatView } from "../components/chat/ChatView"
|
||||
import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides"
|
||||
import { SessionContext } from "../context/session"
|
||||
import type { PermissionRequest, QuestionRequest } from "../types/messages"
|
||||
@@ -336,80 +328,120 @@ export default meta
|
||||
type Story = StoryObj
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Tool with inline permission (glob)
|
||||
// 1. Permission dock — glob (above chatbox)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const GlobWithPermission: Story = {
|
||||
name: "Glob + Inline Permission",
|
||||
name: "Permission Dock — glob above chatbox",
|
||||
render: () => {
|
||||
const perms = [globPermission]
|
||||
const data = dataWith([globPending], perms)
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
<StoryProviders permissions={perms} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "300px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Tool with inline permission (bash)
|
||||
// 2. Permission dock — bash (above chatbox)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const BashWithPermission: Story = {
|
||||
name: "Bash + Inline Permission",
|
||||
name: "Permission Dock — bash above chatbox",
|
||||
render: () => {
|
||||
const perms = [bashPermission]
|
||||
const data = dataWith([bashPending], perms)
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
<StoryProviders permissions={perms} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "300px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Permission dock (non-tool bottom prompt)
|
||||
// 3. Permission dock — write with file patterns (above chatbox)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PermissionDock: Story = {
|
||||
name: "Permission Dock",
|
||||
export const PermissionDockWrite: Story = {
|
||||
name: "Permission Dock — write with patterns",
|
||||
render: () => {
|
||||
const perm = dockPermission
|
||||
const perms = [dockPermission]
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders sessionID={SESSION_ID}>
|
||||
<div data-component="tool-part-wrapper" data-permission="true">
|
||||
<BasicTool
|
||||
icon="checklist"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: "Permission required",
|
||||
subtitle: perm.toolName,
|
||||
}}
|
||||
>
|
||||
<Show when={perm.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={perm.patterns}>{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}</For>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small">
|
||||
Deny
|
||||
</Button>
|
||||
<Button variant="secondary" size="small">
|
||||
Always Allow
|
||||
</Button>
|
||||
<Button variant="primary" size="small">
|
||||
Allow Once
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">Approval applies only to the current session</p>
|
||||
<StoryProviders permissions={perms} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "350px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3b. Permission dock — todowrite (above chatbox)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PermissionDockTodo: Story = {
|
||||
name: "Permission Dock — todowrite above chatbox",
|
||||
render: () => {
|
||||
const perms = [todoWritePermission]
|
||||
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>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "300px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3c. Question dock above chatbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const QuestionAboveChatbox: Story = {
|
||||
name: "Question Dock — above chatbox",
|
||||
render: () => {
|
||||
const qs = [questionRequest]
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", questions: qs }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders questions={qs} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "500px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
@@ -550,17 +582,25 @@ export const QuestionDismissed: Story = {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 10. TodoWrite with inline permission (pending — shows todo list + permission prompt)
|
||||
// 10. TodoWrite with permission in dock (pending — permission shown above chatbox)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const TodoWriteWithPermission: Story = {
|
||||
name: "TodoWrite + Inline Permission",
|
||||
name: "TodoWrite + Permission in Dock",
|
||||
render: () => {
|
||||
const perms = [todoWritePermission]
|
||||
const data = dataWith([todoWritePending], perms)
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "350px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1094,56 +1094,64 @@
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Permission Dock (patterns inside BasicTool)
|
||||
Permission Dock (VS Code overrides)
|
||||
============================================ */
|
||||
|
||||
.permission-dock-patterns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.permission-dock-pattern {
|
||||
font-size: 12px;
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
color: var(--vscode-foreground);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Permission Prompt (inline with tool cards) */
|
||||
[data-component="tool-part-wrapper"][data-permission="true"] {
|
||||
/* When wrapping a Part + permission, the outer wrapper provides the card border.
|
||||
Remove the inner Part's tool-part-wrapper border so we don't get double borders. */
|
||||
> [data-component="tool-part-wrapper"] {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
[data-component="dock-prompt"][data-kind="permission"] {
|
||||
[data-slot="permission-body"] {
|
||||
padding: 10px 12px 0;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] {
|
||||
border-top: 1px solid var(--border-weak-base, var(--vscode-panel-border));
|
||||
padding: 8px 12px;
|
||||
}
|
||||
[data-slot="permission-header-title"] {
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] [data-slot="permission-description"] {
|
||||
font-size: 12px;
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
[data-slot="permission-header-subtitle"] {
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-regular, 400);
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] [data-slot="permission-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
[data-slot="permission-hint"] {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] [data-slot="permission-hint"] {
|
||||
font-size: 11px;
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
margin: 4px 0 0;
|
||||
[data-slot="permission-patterns"] {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
max-height: 120px;
|
||||
|
||||
code {
|
||||
font-size: 12px;
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
color: var(--vscode-foreground);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="permission-footer"] {
|
||||
padding: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-slot="permission-footer-actions"] > [data-component="button"][data-variant="primary"] {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
border: 1px solid var(--vscode-button-border, transparent);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="permission-footer-actions"] > [data-component="button"][data-variant="ghost"] {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog Confirmation */
|
||||
|
||||
Reference in New Issue
Block a user