From 8737bdfb19cad96cd0d7287ced57b22e2bbc719e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 27 Mar 2026 10:48:01 -0300 Subject: [PATCH 001/166] feat: KiloClaw Chat on TUI --- bun.lock | 7 + .../kilo-docs/pages/customize/agents-md.md | 2 +- packages/kilo-gateway/src/server/routes.ts | 97 ++++++++ packages/opencode/package.json | 1 + packages/opencode/src/cli/cmd/pr.ts | 3 +- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/cli/cmd/tui/app.tsx | 12 + .../src/cli/cmd/tui/context/route.tsx | 21 +- packages/opencode/src/kilocode/claw/chat.tsx | 216 ++++++++++++++++++ packages/opencode/src/kilocode/claw/client.ts | 152 ++++++++++++ packages/opencode/src/kilocode/claw/hooks.ts | 141 ++++++++++++ .../opencode/src/kilocode/claw/sidebar.tsx | 150 ++++++++++++ packages/opencode/src/kilocode/claw/types.ts | 34 +++ packages/opencode/src/kilocode/claw/view.tsx | 86 +++++++ .../kilocode/components/dialog-claw-setup.tsx | 66 ++++++ .../components/dialog-claw-upgrade.tsx | 54 +++++ .../opencode/src/kilocode/kilo-commands.tsx | 38 +++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 69 ++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 41 ++++ packages/sdk/openapi.json | 176 +++++++++++++- 20 files changed, 1360 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/src/kilocode/claw/chat.tsx create mode 100644 packages/opencode/src/kilocode/claw/client.ts create mode 100644 packages/opencode/src/kilocode/claw/hooks.ts create mode 100644 packages/opencode/src/kilocode/claw/sidebar.tsx create mode 100644 packages/opencode/src/kilocode/claw/types.ts create mode 100644 packages/opencode/src/kilocode/claw/view.tsx create mode 100644 packages/opencode/src/kilocode/components/dialog-claw-setup.tsx create mode 100644 packages/opencode/src/kilocode/components/dialog-claw-upgrade.tsx diff --git a/bun.lock b/bun.lock index 10c966918b..fd3e6acabc 100644 --- a/bun.lock +++ b/bun.lock @@ -403,6 +403,7 @@ "remeda": "catalog:", "simple-git": "3.31.1", "solid-js": "catalog:", + "stream-chat": "9.38.0", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", "turndown": "7.2.0", @@ -1983,6 +1984,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], @@ -3261,6 +3264,8 @@ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -3977,6 +3982,8 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="], + "stream-chat": ["stream-chat@9.38.0", "", { "dependencies": { "@types/jsonwebtoken": "^9.0.8", "@types/ws": "^8.5.14", "axios": "^1.12.2", "base64-js": "^1.5.1", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "jsonwebtoken": "^9.0.3", "linkifyjs": "^4.3.2", "ws": "^8.18.1" } }, "sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg=="], + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/packages/kilo-docs/pages/customize/agents-md.md b/packages/kilo-docs/pages/customize/agents-md.md index d451b5e7a7..cd34077e7a 100644 --- a/packages/kilo-docs/pages/customize/agents-md.md +++ b/packages/kilo-docs/pages/customize/agents-md.md @@ -18,7 +18,7 @@ If you'd like to migrate your memory bank content to AGENTS.md: 1. Examine the contents in `.kilocode/rules/memory-bank/` 2. Move that content into your project's `AGENTS.md` file (or ask Kilo to do it for you) -{% /callout %} + {% /callout %} ## What is AGENTS.md? diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 79a27b7f5f..7dba761903 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -496,6 +496,103 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { } }, ) + .get( + "/claw/status", + describeRoute({ + summary: "Get KiloClaw instance status", + description: "Fetch the user's KiloClaw instance status via the KiloClaw worker", + operationId: "kilo.claw.status", + responses: { + 200: { + description: "Instance status", + content: { + "application/json": { + schema: resolver(z.unknown()), + }, + }, + }, + ...errors(401, 502), + }, + }), + async (c: any) => { + try { + const auth = await Auth.get("kilo") + if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined + if (!token) return c.json({ error: "No valid token found" }, 401) + + const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + + if (!response.ok) { + const text = await response.text() + return c.json({ error: `KiloClaw request failed: ${response.status} ${text}` }, response.status as any) + } + + return c.json(await response.json()) + } catch (err: any) { + console.error("[Kilo Gateway] claw/status: error", err?.message ?? err) + return c.json({ error: "Failed to reach KiloClaw" }, 502) + } + }, + ) + .get( + "/claw/chat-credentials", + describeRoute({ + summary: "Get KiloClaw chat credentials", + description: "Fetch Stream Chat credentials for the user's KiloClaw instance", + operationId: "kilo.claw.chatCredentials", + responses: { + 200: { + description: "Stream Chat credentials or null", + content: { + "application/json": { + schema: resolver( + z + .object({ + apiKey: z.string(), + userId: z.string(), + userToken: z.string(), + channelId: z.string(), + }) + .nullable(), + ), + }, + }, + }, + ...errors(401, 502), + }, + }), + async (c: any) => { + try { + const auth = await Auth.get("kilo") + if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined + if (!token) return c.json({ error: "No valid token found" }, 401) + + const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/chat-credentials`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + + if (!response.ok) { + const text = await response.text() + return c.json({ error: `KiloClaw request failed: ${response.status} ${text}` }, response.status as any) + } + + return c.json(await response.json()) + } catch (err: any) { + console.error("[Kilo Gateway] claw/chat-credentials: error", err?.message ?? err) + return c.json({ error: "Failed to reach KiloClaw" }, 502) + } + }, + ) .get( "/cloud-sessions", describeRoute({ diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8180756d0b..58550a202d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -128,6 +128,7 @@ "remeda": "catalog:", "simple-git": "3.31.1", "solid-js": "catalog:", + "stream-chat": "9.38.0", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", "turndown": "7.2.0", diff --git a/packages/opencode/src/cli/cmd/pr.ts b/packages/opencode/src/cli/cmd/pr.ts index 170c4671b6..6f1168c98c 100644 --- a/packages/opencode/src/cli/cmd/pr.ts +++ b/packages/opencode/src/cli/cmd/pr.ts @@ -97,7 +97,8 @@ export const PrCommand = cmd({ // Launch opencode TUI with session ID if available const { spawn } = await import("child_process") const opencodeArgs = sessionId ? ["-s", sessionId] : [] - const opencodeProcess = spawn(bin, opencodeArgs, { // kilocode_change + const opencodeProcess = spawn(bin, opencodeArgs, { + // kilocode_change stdio: "inherit", cwd: process.cwd(), windowsHide: true, // kilocode_change - prevent CMD window flash on Windows diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 1cdd7a9e58..556150729c 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -317,7 +317,7 @@ export const RunCommand = cmd({ describe: "auto-approve all permissions (for autonomous/pipeline usage)", default: false, }) - // kilocode_change end + // kilocode_change end ) }, handler: async (args) => { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 2fdc41b393..843b4a4185 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -43,6 +43,7 @@ import open from "open" import { writeHeapSnapshot } from "v8" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { registerKiloCommands } from "@/kilocode/kilo-commands" // kilocode_change +import { KiloClawView } from "@/kilocode/claw/view" // kilocode_change import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui" // kilocode_change import { TuiConfigProvider } from "./context/tui-config" import { TuiConfig } from "@/config/tui" @@ -301,6 +302,12 @@ function App() { const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title renderer.setTerminalTitle(`${titleDefault} | ${title}`) // kilocode_change } + + // kilocode_change start + if (route.data.type === "kiloclaw") { + renderer.setTerminalTitle(`${titleDefault} | KiloClaw`) + } + // kilocode_change end }) const args = useArgs() @@ -812,6 +819,11 @@ function App() { + {/* kilocode_change start */} + + + + {/* kilocode_change end */} ) diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 0656583877..d9a4cc164d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -13,7 +13,13 @@ export type SessionRoute = { initialPrompt?: PromptInfo } -export type Route = HomeRoute | SessionRoute +// kilocode_change start +export type KiloClawRoute = { + type: "kiloclaw" +} +// kilocode_change end + +export type Route = HomeRoute | SessionRoute | KiloClawRoute // kilocode_change export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ name: "Route", @@ -26,14 +32,27 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ }, ) + // kilocode_change start + let previous: Route | undefined + // kilocode_change end + return { get data() { return store }, navigate(route: Route) { console.log("navigate", route) + previous = { ...store } // kilocode_change setStore(route) }, + // kilocode_change start + back() { + const target = previous ?? ({ type: "home" } as const) + previous = undefined + console.log("navigate", target) + setStore(target) + }, + // kilocode_change end } }, }) diff --git a/packages/opencode/src/kilocode/claw/chat.tsx b/packages/opencode/src/kilocode/claw/chat.tsx new file mode 100644 index 0000000000..7a479e0816 --- /dev/null +++ b/packages/opencode/src/kilocode/claw/chat.tsx @@ -0,0 +1,216 @@ +// kilocode_change - new file + +/** + * KiloClaw chat panel + * + * Renders a scrollable message list and a textarea input for chatting + * with the KiloClaw bot via Stream Chat. + * + * Visual style mirrors the session chat TUI (routes/session/index.tsx). + */ + +import { createEffect, createMemo, For, Show } from "solid-js" +import { type KeyBinding, type MouseEvent, type TextareaRenderable } from "@opentui/core" +import { useRenderer } from "@opentui/solid" +import { useTheme } from "@tui/context/theme" +import { SplitBorder, EmptyBorder } from "@tui/component/border" +import { useKV } from "@tui/context/kv" +import type { ChatMessage } from "./types" + +function UserMessageRow(props: { msg: ChatMessage; index: number }) { + const { theme } = useTheme() + return ( + + + + {props.msg.text} + + + + ) +} + +function BotMessageRow(props: { msg: ChatMessage; index: number }) { + const { theme, syntax } = useTheme() + const empty = () => !props.msg.text || !props.msg.text.trim() + return ( + + + Thinking...}> + + + + + ) +} + +export function ClawChat(props: { + messages: ChatMessage[] + online: boolean + connected: boolean + loading: boolean + error: string | null + disabled: boolean + onSend: (text: string) => void +}) { + const { theme } = useTheme() + const renderer = useRenderer() + const kv = useKV() + const [showScrollbar] = kv.signal("scrollbar_visible", true) + let input: TextareaRenderable + + const active = createMemo(() => !props.disabled && props.connected) + + const placeholder = createMemo(() => { + if (props.error) return props.error + if (props.loading) return "Connecting..." + if (!props.connected) return "Chat unavailable" + if (props.disabled) return "Instance is stopped" + return "" + }) + + const submit = () => { + if (!input) return + const text = input.plainText.trim() + if (!text) return + if (!active()) return + props.onSend(text) + input.clear() + } + + createEffect(() => { + if (active()) input?.focus() + }) + + // Stream Chat WebSocket events fire outside OpenTUI's render cycle. + // Track props driven by external callbacks and explicitly request a repaint. + createEffect(() => { + props.messages.length + props.online + renderer.requestRender() + }) + + return ( + + {/* Messages */} + + + + No messages yet. Say hello! + + + + + + Chat not available. Your instance may need to be provisioned or upgraded. + + + + + {(msg, index) => ( + }> + + + )} + + + + {/* Input area */} + + + + {placeholder()}}> +