mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #234 from Kilo-Org/mark/phase3
feat: Markdown rendering, BasicTool for tools, Collapsible for reasoning (Phase 3)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -829,7 +829,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:`,
|
||||
|
||||
@@ -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 = () => {
|
||||
<ThemeProvider defaultTheme="kilo-vscode">
|
||||
<I18nProvider value={{ locale: () => "en", t: (key) => key }}>
|
||||
<DialogProvider>
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<ProviderProvider>
|
||||
<SessionProvider>
|
||||
<AppContent />
|
||||
</SessionProvider>
|
||||
</ProviderProvider>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
<MarkedProvider>
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<ProviderProvider>
|
||||
<SessionProvider>
|
||||
<AppContent />
|
||||
</SessionProvider>
|
||||
</ProviderProvider>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -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 <div class="message-text">{props.part.text}</div>
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div class="message-reasoning">
|
||||
<button class="reasoning-header" onClick={() => setExpanded(!expanded())} aria-expanded={expanded()}>
|
||||
<span class="reasoning-icon">💭</span>
|
||||
<span class="reasoning-label">Thinking...</span>
|
||||
<span class="reasoning-toggle">{expanded() ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
<Show when={expanded()}>
|
||||
<div class="reasoning-content">{props.part.text}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.role === "assistant"} fallback={<div class="message-text">{props.part.text}</div>}>
|
||||
<Markdown text={props.part.text} />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Collapsible variant="ghost">
|
||||
<Collapsible.Trigger>
|
||||
<span>💭 Thinking...</span>
|
||||
<Collapsible.Arrow />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div class="message-text" style={{ "font-style": "italic", opacity: 0.8 }}>
|
||||
{props.part.text}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
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 <Spinner style={{ width: "14px", height: "14px" }} />
|
||||
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 (
|
||||
<div class={`message-tool ${getStatusClass()}`}>
|
||||
<button class="tool-header" onClick={() => setExpanded(!expanded())} aria-expanded={expanded()}>
|
||||
<span class="tool-icon">{getStatusIcon()}</span>
|
||||
<span class="tool-name">{getTitle()}</span>
|
||||
<span class="tool-toggle">{expanded() ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
<Show when={expanded()}>
|
||||
<div class="tool-details">
|
||||
<div class="tool-input">
|
||||
<div data-component="tool-part-wrapper">
|
||||
<BasicTool
|
||||
icon={getStatusIcon()}
|
||||
defaultOpen={false}
|
||||
trigger={{
|
||||
title: getTitle(),
|
||||
subtitle: props.part.tool !== getTitle() ? props.part.tool : undefined,
|
||||
action: isActive() ? ((<Spinner style={{ width: "14px", height: "14px" }} />) as JSX.Element) : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "8px", "font-size": "12px" }}>
|
||||
<div>
|
||||
<strong>Input:</strong>
|
||||
<pre>{JSON.stringify(props.part.state.input, null, 2)}</pre>
|
||||
<pre style={{ margin: "4px 0 8px", "white-space": "pre-wrap", "word-break": "break-word" }}>
|
||||
{JSON.stringify(props.part.state.input, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<Show when={props.part.state.status === "completed" && "output" in props.part.state}>
|
||||
<div class="tool-output">
|
||||
<div>
|
||||
<strong>Output:</strong>
|
||||
<pre>{(props.part.state as { output: string }).output}</pre>
|
||||
<pre style={{ margin: "4px 0 0", "white-space": "pre-wrap", "word-break": "break-word" }}>
|
||||
{(props.part.state as { output: string }).output}
|
||||
</pre>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.part.state.status === "error" && "error" in props.part.state}>
|
||||
<div class="tool-error">
|
||||
<div style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
<strong>Error:</strong>
|
||||
<pre>{(props.part.state as { error: string }).error}</pre>
|
||||
<pre style={{ margin: "4px 0 0", "white-space": "pre-wrap", "word-break": "break-word" }}>
|
||||
{(props.part.state as { error: string }).error}
|
||||
</pre>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<Show when={props.part.type === "text"}>
|
||||
<TextPartView part={props.part as TextPart} />
|
||||
<TextPartView part={props.part as TextPart} role={props.role} />
|
||||
</Show>
|
||||
<Show when={props.part.type === "tool"}>
|
||||
<ToolPartView part={props.part as ToolPart} />
|
||||
@@ -134,11 +151,16 @@ export const Message: Component<MessageProps> = (props) => {
|
||||
when={hasParts()}
|
||||
fallback={
|
||||
<Show when={props.message.content}>
|
||||
<div class="message-text">{props.message.content}</div>
|
||||
<Show
|
||||
when={props.message.role === "assistant"}
|
||||
fallback={<div class="message-text">{props.message.content}</div>}
|
||||
>
|
||||
<Markdown text={props.message.content!} />
|
||||
</Show>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<For each={parts()}>{(part) => <PartView part={part} />}</For>
|
||||
<For each={parts()}>{(part) => <PartView part={part} role={props.message.role} />}</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
============================================ */
|
||||
|
||||
Reference in New Issue
Block a user