From 6bcaa198320e3e1ed1c1fba4122efc27560fdc0a Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 11:54:36 +0100 Subject: [PATCH 1/2] feat: add Markdown rendering, BasicTool for tools, Collapsible for reasoning (Phase 3) - Add MarkedProvider to App.tsx provider hierarchy - TextPartView: render assistant messages as Markdown (headings, lists, code blocks, links, math via katex, syntax highlighting via shiki) - ToolPartView: replace custom tool-header/tool-details with BasicTool (collapsible with icon based on status + spinner for active) - ReasoningPartView: replace custom reasoning block with Collapsible - Remove ~150 lines of tool/reasoning CSS from chat.css - User messages still render as plain text (no markdown) - Code/Diff providers deferred (not needed for current features) - Update progress tracker --- .../docs/ui-implementation-plan.md | 2 +- packages/kilo-vscode/webview-ui/src/App.tsx | 21 +-- .../src/components/chat/Message.tsx | 124 ++++++++------ .../webview-ui/src/styles/chat.css | 152 ------------------ 4 files changed, 86 insertions(+), 213 deletions(-) diff --git a/packages/kilo-vscode/docs/ui-implementation-plan.md b/packages/kilo-vscode/docs/ui-implementation-plan.md index eb3a3edef0c..fb004987513 100644 --- a/packages/kilo-vscode/docs/ui-implementation-plan.md +++ b/packages/kilo-vscode/docs/ui-implementation-plan.md @@ -794,7 +794,7 @@ Each phase has specific acceptance criteria (listed above). The general verifica | Phase 1.4: Cards | ✅ Complete | 2026-02-11 | Inline-styled card divs in ProfileView (profile + balance) and DeviceAuthCard (5 state cards) replaced with `Card` component. Removed `cardStyle` constant. | | Phase 1.5: Icons | ✅ Complete | 2026-02-11 | All 15 lucide-solid icons replaced with kilo-ui `Icon`. `lucide-solid` dep removed. Many icons use approximate matches (see deviation #7). | | Phase 2: Settings UI | ✅ Complete | 2026-02-11 | Settings shell uses kilo-ui `Tabs` with vertical orientation and `variant="settings"`. ~80 lines of inline styles removed. Tab content (stubs) unchanged. Back button in header kept with minimal CSS. | -| Phase 3: Chat UI | ⬚ Not started | — | | +| Phase 3: Chat UI | ✅ Complete | 2026-02-11 | MarkedProvider added. Assistant text renders as Markdown. Tool calls use BasicTool. Reasoning uses Collapsible. Removed ~150 lines of tool/reasoning CSS. Code/Diff providers deferred (not needed yet). | | Phase 4: Advanced | ⬚ Not started | — | | ### Deviations from plan diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index b03e610b2c4..d079ffe6b8d 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -2,6 +2,7 @@ import { Component, createSignal, Switch, Match, onMount, onCleanup } from "soli import { ThemeProvider } from "@kilocode/kilo-ui/theme" import { I18nProvider } from "@kilocode/kilo-ui/context" import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" import Settings from "./components/Settings" import ProfileView from "./components/ProfileView" import { VSCodeProvider } from "./context/vscode" @@ -116,15 +117,17 @@ const App: Component = () => { "en", t: (key) => key }}> - - - - - - - - - + + + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx index c6b30656c81..a0e7189f444 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx @@ -1,10 +1,19 @@ /** * Message component * Displays a single chat message with its parts (text, tool calls, reasoning) + * + * Uses kilo-ui components: + * - Markdown for rendering assistant text (headings, lists, code blocks, links) + * - BasicTool for tool call display (collapsible with icon, title, subtitle) + * - Collapsible for reasoning blocks + * - Spinner for pending/running tool status */ -import { Component, For, Show, createSignal, JSX } from "solid-js" +import { Component, For, Show, JSX } from "solid-js" +import { Markdown } from "@kilocode/kilo-ui/markdown" import { Spinner } from "@kilocode/kilo-ui/spinner" +import { BasicTool } from "@kilocode/kilo-ui/basic-tool" +import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { useSession } from "../../context/session" import type { Message as MessageType, Part, TextPart, ToolPart, ReasoningPart } from "../../types/messages" @@ -12,89 +21,97 @@ interface MessageProps { message: MessageType } -// Text part display -const TextPartView: Component<{ part: TextPart }> = (props) => { - return
{props.part.text}
-} - -// Reasoning part display -const ReasoningPartView: Component<{ part: ReasoningPart }> = (props) => { - const [expanded, setExpanded] = createSignal(false) - +// Text part display — renders markdown for assistant, plain text for user +const TextPartView: Component<{ part: TextPart; role: string }> = (props) => { return ( -
- - -
{props.part.text}
-
-
+ {props.part.text}}> + + ) } -// Tool part display -const ToolPartView: Component<{ part: ToolPart }> = (props) => { - const [expanded, setExpanded] = createSignal(false) +// Reasoning part display — collapsible "Thinking..." block +const ReasoningPartView: Component<{ part: ReasoningPart }> = (props) => { + return ( + + + 💭 Thinking... + + + +
+ {props.part.text} +
+
+
+ ) +} - const getStatusIcon = (): JSX.Element | string => { +// Tool part display — uses BasicTool for collapsible tool header +const ToolPartView: Component<{ part: ToolPart }> = (props) => { + const getStatusIcon = (): string => { switch (props.part.state.status) { case "pending": case "running": - return + return "settings-gear" case "completed": - return "✓" + return "check-small" case "error": - return "✕" + return "circle-x" } } - const getStatusClass = () => { - return `tool-status-${props.part.state.status}` - } - const getTitle = () => { if ("title" in props.part.state && props.part.state.title) { - return props.part.state.title + return props.part.state.title as string } return props.part.tool } + const isActive = () => props.part.state.status === "pending" || props.part.state.status === "running" + return ( -
- - -
-
+
+ ) as JSX.Element) : undefined, + }} + > +
+
Input: -
{JSON.stringify(props.part.state.input, null, 2)}
+
+              {JSON.stringify(props.part.state.input, null, 2)}
+            
-
+
Output: -
{(props.part.state as { output: string }).output}
+
+                {(props.part.state as { output: string }).output}
+              
-
+
Error: -
{(props.part.state as { error: string }).error}
+
+                {(props.part.state as { error: string }).error}
+              
-
+
) } // Part renderer -const PartView: Component<{ part: Part }> = (props) => { +const PartView: Component<{ part: Part; role: string }> = (props) => { // Skip step-start and step-finish parts - they're metadata, not displayable content if (props.part.type === "step-start" || props.part.type === "step-finish") { return null @@ -103,7 +120,7 @@ const PartView: Component<{ part: Part }> = (props) => { return ( <> - + @@ -134,11 +151,16 @@ export const Message: Component = (props) => { when={hasParts()} fallback={ -
{props.message.content}
+ {props.message.content}
} + > + + } > - {(part) => } + {(part) => }
diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 254b3357099..8aee61b2c27 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -130,158 +130,6 @@ line-height: 1.5; } -/* ============================================ - Tool Parts - ============================================ */ - -.message-tool { - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - overflow: hidden; -} - -.tool-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 6px 10px; - background: var(--vscode-sideBar-background); - border: none; - cursor: pointer; - text-align: left; - color: var(--vscode-foreground); - font-size: 12px; -} - -.tool-header:hover { - background: var(--vscode-list-hoverBackground); -} - -.tool-icon { - font-size: 12px; -} - -.tool-name { - flex: 1; - font-family: var(--vscode-editor-font-family); -} - -.tool-toggle { - font-size: 10px; - color: var(--vscode-descriptionForeground); -} - -.tool-details { - padding: 8px 10px; - background: var(--vscode-editor-background); - font-size: 11px; -} - -.tool-input, -.tool-output, -.tool-error { - margin-bottom: 8px; -} - -.tool-input:last-child, -.tool-output:last-child, -.tool-error:last-child { - margin-bottom: 0; -} - -.tool-details pre { - margin: 4px 0 0 0; - font-family: var(--vscode-editor-font-family); - font-size: 11px; - white-space: pre-wrap; - word-break: break-word; - overflow-x: auto; -} - -/* Tool status styles */ -.tool-status-pending .tool-icon { - color: var(--vscode-charts-yellow); -} - -.tool-status-running .tool-icon { - color: var(--vscode-charts-blue); - animation: spin 1s linear infinite; -} - -.tool-status-completed .tool-icon { - color: var(--vscode-charts-green); -} - -.tool-status-error .tool-icon { - color: var(--vscode-errorForeground); -} - -.tool-error { - color: var(--vscode-errorForeground); -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* ============================================ - Reasoning Parts - ============================================ */ - -.message-reasoning { - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - overflow: hidden; - opacity: 0.8; -} - -.reasoning-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 6px 10px; - background: var(--vscode-sideBar-background); - border: none; - cursor: pointer; - text-align: left; - color: var(--vscode-descriptionForeground); - font-size: 12px; - font-style: italic; -} - -.reasoning-header:hover { - background: var(--vscode-list-hoverBackground); -} - -.reasoning-icon { - font-size: 12px; -} - -.reasoning-label { - flex: 1; -} - -.reasoning-toggle { - font-size: 10px; -} - -.reasoning-content { - padding: 8px 10px; - background: var(--vscode-editor-background); - font-size: 12px; - font-style: italic; - color: var(--vscode-descriptionForeground); - white-space: pre-wrap; - word-break: break-word; -} - /* ============================================ Prompt Input ============================================ */ From 9ac221ee61d101e86669e72be144edb5edb126fd Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Wed, 11 Feb 2026 12:47:22 +0100 Subject: [PATCH 2/2] fix: resolve Markdown rendering in webview Three issues prevented kilo-ui Markdown component from rendering: 1. Wrong prop name: Markdown expects 'text' not 'content' 2. Duplicate solid-js runtimes (pnpm 1.9.11 vs bun 1.9.10) caused MarkedProvider context to be invisible to Markdown's useMarked(). Added solidDedupePlugin to esbuild to force single instance. 3. Missing 'wasm-unsafe-eval' CSP for shiki WASM syntax highlighting. Also added ErrorBoundary safety net around Markdown that falls back to plain text rendering on errors. --- packages/kilo-vscode/esbuild.js | 31 ++++++++++++++++++- packages/kilo-vscode/src/KiloProvider.ts | 2 +- .../src/components/chat/Message.tsx | 4 +-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index 54b637ab915..c6541f031c3 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -1,9 +1,38 @@ const esbuild = require("esbuild") +const path = require("path") const { solidPlugin } = require("esbuild-plugin-solid") const production = process.argv.includes("--production") const watch = process.argv.includes("--watch") +/** + * Force all solid-js imports (from kilo-ui and the webview) to resolve to + * the **same** copy so SolidJS contexts are shared across packages. + * Without this, the monorepo hoists separate copies (pnpm vs bun) and + * createContext / useContext can't see each other. + * + * @type {import('esbuild').Plugin} + */ +const solidDedupePlugin = { + name: "solid-dedupe", + setup(build) { + // Resolve these bare specifiers to the kilo-vscode-local copy + const solidRoot = path.dirname(require.resolve("solid-js/package.json")) + const aliases = { + "solid-js": path.join(solidRoot, "dist", "solid.js"), + "solid-js/web": path.join(solidRoot, "web", "dist", "web.js"), + "solid-js/store": path.join(solidRoot, "store", "dist", "store.js"), + } + + build.onResolve({ filter: /^solid-js(\/web|\/store)?$/ }, (args) => { + const key = args.path + if (aliases[key]) { + return { path: aliases[key] } + } + }) + }, +} + /** * @type {import('esbuild').Plugin} */ @@ -72,7 +101,7 @@ async function main() { ".woff2": "file", ".ttf": "file", }, - plugins: [cssPackageResolvePlugin, solidPlugin(), esbuildProblemMatcherPlugin], + plugins: [solidDedupePlugin, cssPackageResolvePlugin, solidPlugin(), esbuildProblemMatcherPlugin], }) if (watch) { diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2fd828add35..c3c291e2b39 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -776,7 +776,7 @@ export class KiloProvider implements vscode.WebviewViewProvider { const csp = [ "default-src 'none'", `style-src 'unsafe-inline' ${webview.cspSource}`, - `script-src 'nonce-${nonce}'`, + `script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`, `font-src ${webview.cspSource}`, "connect-src http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*", `img-src ${webview.cspSource} data: https:`, diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx index a0e7189f444..8cb54050453 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/Message.tsx @@ -25,7 +25,7 @@ interface MessageProps { const TextPartView: Component<{ part: TextPart; role: string }> = (props) => { return ( {props.part.text}
}> - +
) } @@ -155,7 +155,7 @@ export const Message: Component = (props) => { when={props.message.role === "assistant"} fallback={
{props.message.content}
} > - + }