mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-28 02:42:15 +08:00
Merge pull request #219 from Kilo-Org/mark/phase13
Permissions dialog with kilo-ai
This commit is contained in:
@@ -340,9 +340,6 @@
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"eventsource": "^2.0.2",
|
||||
"lucide-solid": "^0.563.0",
|
||||
"solid-js": "^1.9.11",
|
||||
|
||||
@@ -40,6 +40,8 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
|
||||
- Global styles imported via `import "@kilocode/kilo-ui/styles"` in [`index.tsx`](webview-ui/src/index.tsx:2)
|
||||
- [`chat.css`](webview-ui/src/styles/chat.css) is being progressively migrated — when replacing a component with kilo-ui, remove the corresponding CSS rules from it
|
||||
- See [`docs/ui-implementation-plan.md`](docs/ui-implementation-plan.md) for the full migration plan and phased rollout
|
||||
- **Check the desktop app first**: [`packages/app/src/`](../../packages/app/src/) is the reference implementation for how kilo-ui components are composed together. Always check how the app uses a component before implementing it in the webview — don't just look at the component API in isolation.
|
||||
- **`data-component` and `data-slot` attributes carry CSS styling** — kilo-ui uses `[data-component]` and `[data-slot]` attribute selectors, not class names. When the app uses e.g. `data-component="permission-prompt"` and `data-slot="permission-actions"`, these get kilo-ui styling for free.
|
||||
|
||||
## Debugging
|
||||
|
||||
|
||||
@@ -16,8 +16,18 @@ Batch approval UI for file read operations.
|
||||
|
||||
**Reimplement?** Partial (UI can stay; semantics need remapping).
|
||||
|
||||
- Kilo CLI uses a permission-request queue (asked/replied) model; the extension host must translate Kilo CLI permission events into Kilo’s existing approval UX per [`docs/opencode-core/opencode-migration-plan.md`](docs/opencode-core/opencode-migration-plan.md:1).
|
||||
- If Kilo CLI permission prompts are per-tool-call (not “batch per-file”), you may need to:
|
||||
- Kilo CLI uses a permission-request queue (asked/replied) model; the extension host must translate Kilo CLI permission events into Kilo's existing approval UX per [`docs/opencode-core/opencode-migration-plan.md`](docs/opencode-core/opencode-migration-plan.md:1).
|
||||
- If Kilo CLI permission prompts are per-tool-call (not "batch per-file"), you may need to:
|
||||
- either keep a batch UI but respond to permissions one-by-one, or
|
||||
- simplify the UI to match Kilo CLI’s permission granularity.
|
||||
- simplify the UI to match Kilo CLI's permission granularity.
|
||||
- Kilo CLI UI reference: permission prompt actions exist in [`packages/ui/src/components/message-part.tsx`](https://github.com/Kilo-Org/kilo/blob/main/packages/ui/src/components/message-part.tsx:1).
|
||||
|
||||
## TODO: Render permissions inline instead of in a modal Dialog
|
||||
|
||||
Both the desktop app ([`packages/app/src/pages/session.tsx:2727-2774`](../../packages/app/src/pages/session.tsx:2727)) and the old extension render permission prompts **inline in the prompt area**, replacing the text input while a permission is pending. The current vscode rebuild uses a modal `Dialog` wrapper instead.
|
||||
|
||||
This should be changed to match the inline pattern:
|
||||
|
||||
- Render the `BasicTool` + `permission-prompt` block in the prompt dock area (where `PromptInput` sits), conditionally replacing the input when `permissions().length > 0`.
|
||||
- Remove the `Dialog` / `useDialog()` wrapper.
|
||||
- This also enables the prompt area to naturally block user input while permissions are pending, matching the app's `blocked()` memo pattern.
|
||||
|
||||
@@ -777,7 +777,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica
|
||||
| Session history | List renders, selection works, navigation back to chat |
|
||||
| Login flow | Device auth card renders, QR code displays, success/error states |
|
||||
| Theme switching | Change VS Code theme → webview updates automatically |
|
||||
| Sidebar ↔ tab | Both webview hosts render identically |
|
||||
| Sidebar ↔ tab | Both webview hosts render identically |
|
||||
| Markdown content | Headers, lists, code blocks, links render correctly |
|
||||
| Long conversations | Auto-scroll works, performance is acceptable |
|
||||
|
||||
@@ -790,7 +790,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica
|
||||
| Phase 0: Foundation | ✅ Complete | 2026-02-10 | Build system, CSP, providers, styles wired up. `I18nProvider` locale corrected to accessor. Font loaders (`.woff`, `.woff2`, `.ttf`) added to esbuild. |
|
||||
| Phase 1.1: Buttons | ✅ Complete | 2026-02-10 | All buttons in PromptInput, PermissionDialog, ProfileView, DeviceAuthCard, Settings replaced with `Button`. `IconButton` not used — requires named `icon` prop incompatible with inline SVGs. Settings tab sidebar buttons left as native `<button>` (complex active state styling). |
|
||||
| Phase 1.2: Spinners | ✅ Complete | 2026-02-10 | Emoji loading indicators (`⏳`, `⚙️`) in Message.tsx and DeviceAuthCard.tsx replaced with `Spinner`. Status icons (`✓`, `✕`) kept as-is. |
|
||||
| Phase 1.3: Permission Dialog | ⬚ Not started | — | |
|
||||
| Phase 1.3: Permission Dialog | ✅ Complete | 2026-02-11 | Custom overlay replaced with `Dialog` via `useDialog()` hook + `BasicTool` for collapsible header + `data-component="permission-prompt"` for actions. Removed 7 custom CSS classes. Direct `@kobalte/core` dep removed to avoid dual-package context mismatch. |
|
||||
| Phase 1.4: Cards | ⬚ Not started | — | |
|
||||
| Phase 1.5: Icons | ⬚ Not started | — | Requires adding 6 missing icons to kilo-ui first |
|
||||
| Phase 2: Settings UI | ⬚ Not started | — | |
|
||||
@@ -804,3 +804,4 @@ Each phase has specific acceptance criteria (listed above). The general verifica
|
||||
3. **Font loaders added to esbuild**: The plan didn't mention font file handling, but kilo-ui's katex dependency bundles `.woff`, `.woff2`, and `.ttf` fonts that needed `file` loaders in esbuild.
|
||||
4. **`Spinner` has no `size` prop**: The plan assumed `Spinner` would accept a `size` prop. The actual component is sized via CSS `style` prop (`width`/`height`).
|
||||
5. **Settings tab sidebar buttons**: Left as native `<button>` elements because they have complex active/hover state styling that would need significant rework to use `Button`.
|
||||
6. **Permission dialog vs inline prompt**: The desktop app renders permissions inline in the prompt dock area (replacing the input). The vscode webview keeps a `Dialog` wrapper because the layout differs. The inner content uses the same `BasicTool` + `data-component="permission-prompt"` pattern as the app. The args are shown as raw JSON rather than `perm.patterns` because the vscode permission data shape differs from the app's SDK format.
|
||||
|
||||
@@ -164,9 +164,6 @@
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"eventsource": "^2.0.2",
|
||||
"lucide-solid": "^0.563.0",
|
||||
"solid-js": "^1.9.11"
|
||||
|
||||
@@ -1,64 +1,59 @@
|
||||
/**
|
||||
* PermissionDialog component
|
||||
* Displays permission requests from the assistant and allows user to respond
|
||||
* Displays permission requests from the assistant and allows user to respond.
|
||||
*
|
||||
* Uses the same component pattern as the desktop app (packages/app/src/pages/session.tsx):
|
||||
* BasicTool for the collapsible tool header, and data-component="permission-prompt"
|
||||
* with data-slot="permission-actions" for the button row (styled by kilo-ui CSS).
|
||||
*/
|
||||
|
||||
import { Component, Show, For, createSignal } from "solid-js"
|
||||
import { Component, For, Show, createEffect, on } from "solid-js"
|
||||
import { Dialog } from "@kilocode/kilo-ui/dialog"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { useSession } from "../../context/session"
|
||||
import type { PermissionRequest } from "../../types/messages"
|
||||
|
||||
interface PermissionDialogProps {
|
||||
interface PermissionItemProps {
|
||||
permission: PermissionRequest
|
||||
}
|
||||
|
||||
const PermissionItem: Component<PermissionDialogProps> = (props) => {
|
||||
const PermissionItem: Component<PermissionItemProps> = (props) => {
|
||||
const session = useSession()
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
|
||||
const handleResponse = (response: "once" | "always" | "reject") => {
|
||||
session.respondToPermission(props.permission.id, response)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="permission-item">
|
||||
<div class="permission-header">
|
||||
<span class="permission-icon">🔐</span>
|
||||
<span class="permission-title">Permission Required</span>
|
||||
</div>
|
||||
|
||||
<div class="permission-content">
|
||||
<p class="permission-message">
|
||||
{props.permission.message || `The assistant wants to use: ${props.permission.toolName}`}
|
||||
</p>
|
||||
|
||||
<Button variant="ghost" size="small" onClick={() => setExpanded(!expanded())}>
|
||||
{expanded() ? "Hide details" : "Show details"}
|
||||
</Button>
|
||||
|
||||
<Show when={expanded()}>
|
||||
<div data-component="tool-part-wrapper" data-permission="true">
|
||||
<BasicTool
|
||||
icon="checklist"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: props.permission.toolName,
|
||||
}}
|
||||
>
|
||||
<Show when={props.permission.args}>
|
||||
<div class="permission-details">
|
||||
<div class="permission-tool">
|
||||
<strong>Tool:</strong> {props.permission.toolName}
|
||||
</div>
|
||||
<div class="permission-args">
|
||||
<strong>Arguments:</strong>
|
||||
<pre>{JSON.stringify(props.permission.args, null, 2)}</pre>
|
||||
</div>
|
||||
<pre>{JSON.stringify(props.permission.args, null, 2)}</pre>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => handleResponse("reject")}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button variant="secondary" size="small" onClick={() => handleResponse("once")}>
|
||||
Allow Once
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => handleResponse("always")}>
|
||||
Always Allow
|
||||
</Button>
|
||||
</BasicTool>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => handleResponse("reject")}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button variant="secondary" size="small" onClick={() => handleResponse("always")}>
|
||||
Always Allow
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => handleResponse("once")}>
|
||||
Allow Once
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -66,17 +61,27 @@ const PermissionItem: Component<PermissionDialogProps> = (props) => {
|
||||
|
||||
export const PermissionDialog: Component = () => {
|
||||
const session = useSession()
|
||||
const dialog = useDialog()
|
||||
|
||||
const permissions = () => session.permissions()
|
||||
const hasPermissions = () => permissions().length > 0
|
||||
|
||||
return (
|
||||
<Show when={hasPermissions()}>
|
||||
<div class="permission-overlay">
|
||||
<div class="permission-dialog">
|
||||
<For each={permissions()}>{(permission) => <PermissionItem permission={permission} />}</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
// Reactively show/hide the dialog based on pending permissions.
|
||||
// useDialog().show() internally wraps the Dialog in a Kobalte root + portal
|
||||
// from the same package copy, avoiding the dual-package context mismatch.
|
||||
createEffect(
|
||||
on(hasPermissions, (has) => {
|
||||
if (has) {
|
||||
dialog.show(() => (
|
||||
<Dialog title="Permission Required" fit action={<span />}>
|
||||
<For each={permissions()}>{(permission) => <PermissionItem permission={permission} />}</For>
|
||||
</Dialog>
|
||||
))
|
||||
} else {
|
||||
dialog.close()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -376,126 +376,19 @@
|
||||
Permission Dialog
|
||||
============================================ */
|
||||
|
||||
.permission-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.permission-dialog {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permission-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.permission-item + .permission-item {
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.permission-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.permission-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.permission-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.permission-message {
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.permission-details-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-details-toggle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.permission-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--vscode-input-background);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-details pre {
|
||||
margin: 4px 0 0 0;
|
||||
margin: 0;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.permission-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.permission-button {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.permission-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.permission-button-reject {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.permission-button-once {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.permission-button-always {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Session List
|
||||
============================================ */
|
||||
|
||||
Reference in New Issue
Block a user