Merge pull request #12525 from Kilo-Org/feature/charts

feat(charts/vscode): Added charting capabilities to kilo-ui storybook and VS Code extension
This commit is contained in:
cosi-conda
2026-08-03 09:39:56 -07:00
committed by GitHub
16 changed files with 694 additions and 6 deletions
+157
View File
@@ -0,0 +1,157 @@
---
name: chart
description: Use when the user asks to visualize data with charts, graphs, or plots using the `chart` tool (bar, line, scatter, pie, time series, etc.).
---
# Data Visualization
The `chart` tool is ALWAYS available in this environment. When the user asks to visualize data (charts, graphs, plots), you MUST call the `chart` tool. Never output the config as text, never say the tool is unavailable, never suggest external renderers. Always use the tool call — it is the only correct response for data visualization requests. Do NOT repeat or echo the config JSON in your text response.
Use the `chart` tool only when the user explicitly asks for a chart, graph, or plot. Only use these supported Chart.js v4 types: `bar`, `bubble`, `pie`, `doughnut`, `line`, `mixed`, `polarArea`, `radar`, `scatter`. For area charts, use `line` with `fill: true` on the dataset — do NOT use `area` as a type.
Use mermaid fenced code blocks (` ```mermaid `) when:
- The user asks for a diagram, flowchart, sequence diagram, ER diagram, or architecture diagram
- Visualizing relationships, processes, or structure — not data values
Mermaid is NOT a tool and is NOT Chart.js — never call the `chart` tool for mermaid diagrams. Just write the mermaid syntax directly in your text response inside a fenced code block. No tool call needed.
Do not use either for: code, text, or data that is already clear in prose or table form.
The `chart` tool input accepts:
- `title` (string) — short label shown in the tool header
- `description` (string, optional) — subtitle shown below the title
- `spec` (string) — a Chart.js config object as a JSON string
The `spec` field must be a Chart.js config JSON string with `type`, `data`, and optionally `options`. Examples:
Bar chart:
```json
{
"type": "bar",
"data": {
"labels": ["A", "B", "C"],
"datasets": [{ "label": "Value", "data": [10, 20, 15] }]
}
}
```
Area chart (line with fill):
```json
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": true }]
}
}
```
Line chart:
```json
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": false }]
}
}
```
Scatter plot:
```json
{
"type": "scatter",
"data": {
"datasets": [{
"label": "Points",
"data": [{ "x": 1, "y": 5 }, { "x": 2, "y": 8 }, { "x": 3, "y": 3 }]
}]
}
}
```
Time series:
```json
{
"type": "line",
"data": {
"labels": ["2024-01", "2024-02", "2024-03", "2024-04"],
"datasets": [{ "label": "Value", "data": [120, 145, 132, 178], "fill": true }]
}
}
```
Pie chart:
```json
{
"type": "pie",
"data": {
"labels": ["A", "B", "C"],
"datasets": [{ "data": [30, 50, 20] }]
}
}
```
Doughnut chart:
```json
{
"type": "doughnut",
"data": {
"labels": ["A", "B", "C"],
"datasets": [{ "data": [30, 50, 20] }]
}
}
```
Radar chart:
```json
{
"type": "radar",
"data": {
"labels": ["Speed", "Power", "Agility", "Stamina"],
"datasets": [{ "label": "Player", "data": [80, 60, 90, 70] }]
}
}
```
Bubble chart:
```json
{
"type": "bubble",
"data": {
"datasets": [{
"label": "Group A",
"data": [{ "x": 10, "y": 20, "r": 8 }, { "x": 15, "y": 10, "r": 5 }]
}]
}
}
```
Polar area chart:
```json
{
"type": "polarArea",
"data": {
"labels": ["A", "B", "C", "D"],
"datasets": [{ "data": [11, 16, 7, 14] }]
}
}
```
Mixed chart (bar + line):
```json
{
"type": "bar",
"data": {
"labels": ["Jan", "Feb", "Mar"],
"datasets": [
{ "type": "bar", "label": "Revenue", "data": [100, 120, 90] },
{ "type": "line", "label": "Trend", "data": [95, 115, 100] }
]
}
}
```
You may customize colors by setting `backgroundColor` and `borderColor` arrays on datasets. The renderer handles sizing — do not set width or height.
Only include `scales` in `options` for cartesian chart types: `bar`, `line`, `scatter`, `bubble`. Do NOT include `scales` for `pie`, `doughnut`, `polarArea`, `radar`, or `mixed` — it will cause them to fail.
+5
View File
@@ -362,6 +362,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.5",
"@solid-primitives/rootless": "1.5.2",
"chart.js": "4.5.1",
"diff": "catalog:",
"lucide-solid": "0.576.0",
"motion": "12.34.5",
@@ -1557,6 +1558,8 @@
"@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
@@ -2865,6 +2868,8 @@
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3b1eb6cf15d5c2dc66c769a91fcf09bcf25bc2e8d17696ad0e9ab4181ed3038c
size 658647
oid sha256:fd794db4d2c038984f9e753ba70b8b0473d97a16713a9ff7255f0d2cf505d41a
size 758306
+3 -1
View File
@@ -6,6 +6,7 @@
"exports": {
"./font": "./src/components/font.tsx",
"./button": "./src/components/button.tsx",
"./chart": "./src/components/chart.tsx",
"./code": "./src/components/code.tsx",
"./diff": "./src/components/diff.tsx",
"./diff-ssr": "./src/components/diff-ssr.tsx",
@@ -128,6 +129,7 @@
"motion": "12.34.5",
"motion-dom": "12.34.3",
"motion-utils": "12.29.2",
"strip-ansi": "7.1.2"
"strip-ansi": "7.1.2",
"chart.js": "4.5.1"
}
}
+14
View File
@@ -0,0 +1,14 @@
[data-component="chart-container"] {
padding: 12px;
}
[data-slot="chart-render"] {
width: 100%;
max-height: 300px;
}
[data-slot="chart-error"] {
padding: 12px;
color: var(--text-weak);
font-size: var(--kilo-font-size-12, 12px);
}
+146
View File
@@ -0,0 +1,146 @@
/** @jsxImportSource solid-js */
import { createEffect, createSignal, onCleanup } from "solid-js"
import { Chart, registerables } from "chart.js"
import { BasicTool } from "./basic-tool"
import type { ToolProps } from "./message-part"
import { busy } from "./tool-utils"
Chart.register(...registerables)
function getThemeColors() {
const style = getComputedStyle(document.documentElement)
const get = (v: string, fallback: string) => style.getPropertyValue(v).trim() || fallback
return {
text: get("--text-base", "#FAFAFA"),
textWeak: get("--text-weak", "#A3A3A3"),
border: get("--border-weak-base", "#FFFFFF1A"),
surface: get("--surface-raised-base", "#202020"),
series: [
get("--vscode-charts-blue", "#3B82F6"),
get("--vscode-charts-green", "#22C55E"),
get("--vscode-charts-purple", "#A855F7"),
get("--vscode-charts-orange", "#F97316"),
get("--vscode-charts-red", "#EF4444"),
get("--vscode-charts-yellow", "#EAB308"),
],
}
}
type ChartConfig = {
type: string
data: {
labels?: string[]
datasets: {
label?: string
data: number[] | { x: number | string; y: number; r?: number }[]
backgroundColor?: string | string[]
borderColor?: string | string[]
[key: string]: unknown
}[]
}
options?: Record<string, unknown>
}
export function ChartTool(props: ToolProps) {
const [canvas, setCanvas] = createSignal<HTMLCanvasElement>()
const [error, setError] = createSignal<string>()
let rendered = false
createEffect(() => {
const el = canvas()
const raw = props.output
if (!el || !raw || busy(props.status) || rendered) return
let config: ChartConfig
try {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== "object" || !parsed.type || !parsed.data) {
setError("Invalid chart config — must include type and data")
return
}
config = parsed
} catch (e) {
// output is not valid JSON — model likely passed an unsupported type
console.warn("[Kilo Chart]: could not parse output as JSON", e)
return
}
let chart: Chart | undefined
onCleanup(() => {
chart?.destroy()
rendered = false
})
if (!el.isConnected) return
const colors = getThemeColors()
const isPolar = config.type === "pie" || config.type === "doughnut" || config.type === "polarArea"
const datasets = config.data.datasets.map((dataset, i) => {
if (dataset.backgroundColor) return dataset
if (isPolar) {
const data = dataset.data as unknown[]
return {
...dataset,
backgroundColor: data.map((_, j) => colors.series[j % colors.series.length]),
borderColor: data.map((_, j) => colors.series[j % colors.series.length]),
}
}
return {
backgroundColor: colors.series[i % colors.series.length],
borderColor: colors.series[i % colors.series.length],
...dataset,
}
})
try {
chart = new Chart(el, {
type: config.type as any,
data: { ...config.data, datasets } as any,
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
labels: { color: colors.textWeak },
},
},
scales: {
x: {
ticks: { color: colors.textWeak },
grid: { color: colors.border },
border: { color: colors.border },
},
y: {
ticks: { color: colors.textWeak },
grid: { color: colors.border },
border: { color: colors.border },
},
},
...config.options,
},
})
rendered = true
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to render chart")
}
})
return (
<BasicTool
{...props}
icon="bullet-list"
trigger={{
title: props.input?.title ?? "Chart",
subtitle: props.input?.description ?? undefined,
args: [],
}}
defaultOpen={props.defaultOpen ?? true}
>
<div data-component="chart-container">
{error() ? <div data-slot="chart-error">{error()}</div> : <canvas ref={setCanvas} data-slot="chart-render" />}
</div>
</BasicTool>
)
}
@@ -3097,3 +3097,9 @@ ToolRegistry.register({
)
},
})
import { ChartTool } from "./chart"
ToolRegistry.register({
name: "chart",
render: ChartTool,
})
+1
View File
@@ -10,6 +10,7 @@
@import "../components/auto-approve-bar.css";
@import "../components/button.css";
@import "../components/card.css";
@import "../components/chart.css";
@import "../components/chat-input.css";
@import "../components/checkbox.css";
@import "../components/code.css";
@@ -1227,6 +1227,231 @@ export const SearchPreviews: Story = {
</div>
</div>
</section>
<section class="tool-call-lab-panel tool-call-lab-panel-wide">
<div class="tool-call-lab-panel-header">
<span class="tool-call-lab-panel-title">Chart renderers</span>
<span class="tool-call-lab-panel-note">
Chart.js chart tool rendered with production ChartTool component.
</span>
</div>
<div class="tool-call-lab-stack">
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Bar chart</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-bar-preview",
"chart",
{ title: "Bar chart" },
"Render chart",
JSON.stringify({
type: "bar",
data: {
labels: ["A", "B", "C", "D", "E"],
datasets: [{ label: "Value", data: [28, 55, 43, 91, 81] }],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Line plot</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-line-preview",
"chart",
{ title: "Line plot" },
"Render chart",
JSON.stringify({
type: "line",
data: {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"],
datasets: [{ label: "Value", data: [12, 28, 19, 45, 38, 62, 55, 74], fill: false }],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Scatter plot</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-scatter-preview",
"chart",
{ title: "Scatter plot" },
"Render chart",
JSON.stringify({
type: "scatter",
data: {
datasets: [
{
label: "Points",
data: [
{ x: 2, y: 14 },
{ x: 5, y: 38 },
{ x: 8, y: 22 },
{ x: 11, y: 61 },
{ x: 14, y: 44 },
{ x: 17, y: 73 },
{ x: 20, y: 55 },
{ x: 23, y: 88 },
{ x: 26, y: 67 },
{ x: 29, y: 95 },
],
},
],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Temporal (time series)</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-temporal-preview",
"chart",
{ title: "Time series" },
"Render chart",
JSON.stringify({
type: "line",
data: {
labels: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
],
datasets: [
{
label: "Value",
data: [120, 145, 132, 178, 163, 201, 194, 223, 215, 248, 237, 271],
fill: true,
},
],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Pie chart</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-pie-preview",
"chart",
{ title: "Pie chart" },
"Render chart",
JSON.stringify({
type: "pie",
data: {
labels: ["A", "B", "C", "D"],
datasets: [{ data: [30, 50, 15, 5] }],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Doughnut chart</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-doughnut-preview",
"chart",
{ title: "Doughnut chart" },
"Render chart",
JSON.stringify({
type: "doughnut",
data: {
labels: ["A", "B", "C", "D"],
datasets: [{ data: [40, 25, 20, 15] }],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Radar chart</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-radar-preview",
"chart",
{ title: "Radar chart" },
"Render chart",
JSON.stringify({
type: "radar",
data: {
labels: ["Speed", "Strength", "Agility", "Intelligence", "Endurance"],
datasets: [
{ label: "Player A", data: [80, 60, 75, 90, 70] },
{ label: "Player B", data: [55, 85, 60, 65, 80] },
],
},
}),
),
]}
/>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Bubble chart</span>
<AssistantMessage
message={base}
parts={[
done(
"chart-bubble-preview",
"chart",
{ title: "Bubble chart" },
"Render chart",
JSON.stringify({
type: "bubble",
data: {
datasets: [
{
label: "Dataset",
data: [
{ x: 5, y: 20, r: 10 },
{ x: 15, y: 35, r: 20 },
{ x: 25, y: 15, r: 8 },
{ x: 35, y: 50, r: 15 },
{ x: 45, y: 30, r: 25 },
],
},
],
},
}),
),
]}
/>
</div>
</div>
</section>
</div>
</SessionContext.Provider>
</StoryProviders>
@@ -0,0 +1,77 @@
// kilocode_change - new file
import { Effect, Schema } from "effect"
import * as Tool from "../../tool/tool"
const Parameters = Schema.Struct({
title: Schema.String.annotate({
description: "Short label for the chart shown in the tool header",
}),
description: Schema.optional(Schema.String).annotate({
description: "Optional subtitle shown below the title",
}),
spec: Schema.String.annotate({
description: "A Chart.js v4 configuration serialized as a JSON string. Pass the JSON object as a plain string value — do not nest objects, do not escape quotes manually. Example: '{\"type\":\"bar\",\"data\":{\"labels\":[\"A\",\"B\"],\"datasets\":[{\"data\":[1,2]}]}}'",
}),
})
type Meta = {
title: string
description?: string
error?: string
}
export const ChartTool = Tool.define(
"chart",
Effect.gen(function* () {
return {
description:
"Render a data visualization chart using a Chart.js v4 config. Use this when the user explicitly asks for a chart, graph, or plot. Supported types: bar, bubble, pie, doughnut, line (use fill:true for area charts), mixed, polarArea, radar, scatter. Do NOT use this for diagrams, flowcharts, sequence diagrams, or any mermaid content — write those as mermaid fenced code blocks in your text response instead. After calling this tool, do NOT output the JSON spec or raw data in your text response — the chart is the response.",
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
yield* ctx.metadata({
title: params.title,
metadata: { title: params.title, description: params.description } as Meta,
})
let spec: unknown
try {
spec = JSON.parse(params.spec)
} catch {
return {
title: params.title,
output: `Invalid chart spec: could not parse JSON. If you are trying to render a diagram or mermaid chart, do NOT use the chart tool — write a mermaid fenced code block in your text response instead.`,
metadata: { title: params.title, description: params.description, error: "invalid-json" } as Meta,
}
}
if (!spec || typeof spec !== "object" || !("type" in spec) || !("data" in spec)) {
return {
title: params.title,
output: `Invalid chart spec: must be a Chart.js v4 config with "type" and "data" fields. If you are trying to render a mermaid diagram, do NOT use the chart tool — write a mermaid fenced code block in your text response instead.`,
metadata: { title: params.title, description: params.description, error: "invalid-spec" } as Meta,
}
}
// "area" is not a Chart.js type — remap to line with fill
if ((spec as Record<string, unknown>).type === "area") {
const s = spec as Record<string, unknown>
s.type = "line"
const data = s.data as Record<string, unknown> | undefined
if (data && Array.isArray(data.datasets)) {
data.datasets = (data.datasets as Record<string, unknown>[]).map((ds) => ({
fill: true,
...ds,
}))
}
}
return {
title: params.title,
output: JSON.stringify(spec),
metadata: { title: params.title, description: params.description } as Meta,
}
}).pipe(Effect.orDie),
}
}),
)
@@ -3,6 +3,7 @@ import { RecallTool } from "../../tool/recall"
import { AgentManagerModelsTool } from "./agent-manager-models"
import { AgentManagerTool } from "./agent-manager"
import { BackgroundProcessTool } from "./background-process"
import { ChartTool } from "./chart"
import { GenerateImageTool } from "./generate-image"
import { InteractiveTerminalTool } from "./interactive-terminal"
import { NotebookEditTool, NotebookExecuteTool, NotebookReadTool } from "./notebook-host"
@@ -74,6 +75,7 @@ export namespace KiloToolRegistry {
const save = yield* MemorySaveTool
const manager = yield* AgentManagerTool.pipe(Effect.provideService(AgentManager.Service, host ?? unavailable))
const process = yield* BackgroundProcessTool
const chart = yield* ChartTool
const image = yield* GenerateImageTool
const terminal = yield* InteractiveTerminalTool
// The notify_user tool depends on KiloSessions.Service, which the tool-registry layer provides
@@ -83,13 +85,13 @@ export namespace KiloToolRegistry {
const notify = yield* NotifyUserTool.pipe(Effect.provideService(KiloSessions.Service, sessions))
const send = yield* SendFileTool
if (!notebook)
return { codebase, recall, managerModels, memory, save, manager, process, image, terminal, notify, send }
return { codebase, recall, managerModels, memory, save, manager, process, chart, image, terminal, notify, send }
const tools = yield* Effect.all({
notebookRead: NotebookReadTool,
notebookEdit: NotebookEditTool,
notebookExecute: NotebookExecuteTool,
}).pipe(Effect.provideService(Notebook.Service, notebook))
return { codebase, recall, managerModels, memory, save, manager, process, image, terminal, notify, send, ...tools }
return { codebase, recall, managerModels, memory, save, manager, process, chart, image, terminal, notify, send, ...tools }
})
}
@@ -104,6 +106,7 @@ export namespace KiloToolRegistry {
save: Tool.Info
manager: Tool.Info
process: Tool.Info
chart: Tool.Info
image: Tool.Info
terminal?: Tool.Info
notify: Tool.Info
@@ -124,6 +127,7 @@ export namespace KiloToolRegistry {
save: Tool.init(tools.save),
manager: Tool.init(tools.manager),
process: Tool.init(tools.process),
chart: Tool.init(tools.chart),
image: Tool.init(tools.image),
notify: Tool.init(tools.notify),
send: Tool.init(tools.send),
@@ -198,6 +202,7 @@ export namespace KiloToolRegistry {
save: Tool.Def
manager: Tool.Def
process: Tool.Def
chart: Tool.Def
image: Tool.Def
terminal?: Tool.Def
notify: Tool.Def
@@ -215,9 +220,9 @@ export namespace KiloToolRegistry {
tools.memory,
tools.save,
tools.recall,
...(Flag.KILO_CLIENT === "vscode" ? [tools.chart] : []),
...(Flag.KILO_CLIENT === "cli" || Flag.KILO_CLIENT === "vscode" ? [tools.process] : []),
...(Flag.KILO_CLIENT === "cli" && tools.terminal ? [tools.terminal] : []),
// Agent Manager tools are useful only when the extension can create and display their sessions.
...(Flag.KILO_CLIENT === "vscode" ? [tools.managerModels, tools.manager] : []),
...(Flag.KILO_CLIENT === "vscode" &&
cfg.experimental?.native_notebook_tools === true &&
@@ -0,0 +1,43 @@
import { expect, test } from "bun:test"
import { KiloToolRegistry } from "@/kilocode/tool/registry"
import type * as Tool from "@/tool/tool"
// Minimal stub — select() only reads .id from each Tool.Def
const stub = (id: string) => ({ id }) as unknown as Tool.Def
const tools = {
codebase: stub("codebase"),
recall: stub("recall"),
managerModels: stub("managerModels"),
memory: stub("memory"),
save: stub("save"),
manager: stub("manager"),
process: stub("process"),
chart: stub("chart"),
image: stub("image"),
notify: stub("notify"),
send: stub("send_file"),
}
function ids(client: string) {
const prev = process.env.KILO_CLIENT
try {
process.env.KILO_CLIENT = client
return KiloToolRegistry.extra(tools, {}).map((t) => t.id)
} finally {
if (prev === undefined) delete process.env.KILO_CLIENT
else process.env.KILO_CLIENT = prev
}
}
test("chart tool is included for vscode", () => {
expect(ids("vscode")).toContain("chart")
})
test("chart tool is excluded for cli", () => {
expect(ids("cli")).not.toContain("chart")
})
test("chart tool is excluded for jetbrains", () => {
expect(ids("jetbrains")).not.toContain("chart")
})
@@ -42,6 +42,7 @@ function infos() {
save: info("kilo_memory_save"),
manager: info("agent_manager"),
process: info("background_process"),
chart: info("chart"),
image: info("generate_image"),
notify: info("notify_user"),
send: info("send_file"),
@@ -339,6 +339,7 @@ describe("kilocode tool registry indexing", () => {
save: def("kilo_memory_save"),
manager: def("agent_manager"),
process: def("background_process"),
chart: def("chart"),
image: def("generate_image"),
terminal: def("interactive_terminal"),
notify: def("notify_user"),
@@ -398,6 +399,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_recall",
"kilo_memory_save",
"recall",
"chart",
"background_process",
"agent_manager_models",
"agent_manager",
@@ -415,6 +417,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_recall",
"kilo_memory_save",
"recall",
"chart",
"background_process",
"agent_manager_models",
"agent_manager",
@@ -428,6 +431,7 @@ describe("kilocode tool registry indexing", () => {
"kilo_memory_recall",
"kilo_memory_save",
"recall",
"chart",
"background_process",
"agent_manager_models",
"agent_manager",
@@ -54,6 +54,7 @@ function infos() {
save: info("kilo_memory_save"),
manager: info("agent_manager"),
process: info("background_process"),
chart: info("chart"),
image: info("generate_image"),
notify: info("notify_user"),
send: info("send_file"),
@@ -460,6 +460,7 @@ describe("send_file tool", () => {
save: tool,
manager: tool,
process: tool,
chart: tool,
image: tool,
notify: { id: "notify_user" } as Tool.Def,
send: tool,