Merge pull request #219 from Kilo-Org/mark/phase13

Permissions dialog with kilo-ai
This commit is contained in:
Mark IJbema
2026-02-11 10:02:39 +01:00
committed by GitHub
7 changed files with 72 additions and 167 deletions
-3
View File
@@ -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",
+2
View File
@@ -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.
-3
View File
@@ -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
============================================ */