mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
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:
@@ -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.
|
||||
@@ -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=="],
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3b1eb6cf15d5c2dc66c769a91fcf09bcf25bc2e8d17696ad0e9ab4181ed3038c
|
||||
size 658647
|
||||
oid sha256:fd794db4d2c038984f9e753ba70b8b0473d97a16713a9ff7255f0d2cf505d41a
|
||||
size 758306
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user