From c1c3af8bf42e911d9d2a2cf06937fdf056d851d2 Mon Sep 17 00:00:00 2001 From: truffle Date: Sat, 25 Apr 2026 15:18:06 +0000 Subject: [PATCH 01/74] fix(cli): include working tree in WorktreeFamily.list for submodules Inside a git submodule `git worktree list --porcelain` reports the gitdir (`/.git/modules/`) rather than the actual working tree, so the worktree-family filter for the experimental session listing endpoint dropped every session whose directory was the real submodule path. The CLI `kilo session list` was unaffected because it filters on `project_id` only. Append `Instance.worktree` to the parsed dirs so the working tree is always in scope. Normal repos and linked worktrees already include it, so the [...new Set(...)] dedup keeps those cases unchanged. Closes #9267 --- .changeset/tui-submodule-session-list.md | 5 +++ .../opencode/src/kilocode/worktree-family.ts | 7 ++++ .../worktree-family-submodule.test.ts | 40 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 .changeset/tui-submodule-session-list.md create mode 100644 packages/opencode/test/kilocode/worktree-family-submodule.test.ts diff --git a/.changeset/tui-submodule-session-list.md b/.changeset/tui-submodule-session-list.md new file mode 100644 index 0000000000..fcb2ecbf3f --- /dev/null +++ b/.changeset/tui-submodule-session-list.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix empty TUI session list when launching kilo from inside a git submodule. `git worktree list --porcelain` reports the submodule's gitdir (`/.git/modules/`) instead of the working tree, so the worktree-family filter dropped every session whose directory was the actual submodule path. Include `Instance.worktree` in the returned set so submodule sessions stay in scope. diff --git a/packages/opencode/src/kilocode/worktree-family.ts b/packages/opencode/src/kilocode/worktree-family.ts index 519516b426..35fad2d360 100644 --- a/packages/opencode/src/kilocode/worktree-family.ts +++ b/packages/opencode/src/kilocode/worktree-family.ts @@ -25,6 +25,13 @@ export namespace WorktreeFamily { }) if (dirs.length > 0) { + // In a git submodule, `git worktree list --porcelain` reports the + // gitdir (`/.git/modules/`) instead of the actual working + // tree, so the parsed list never contains the directory sessions are + // recorded under. Including Instance.worktree keeps submodule sessions + // in scope without affecting normal repos (already present) or linked + // worktrees (also already present). + dirs.push(Filesystem.resolve(Instance.worktree)) return [...new Set(dirs)] } } diff --git a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts new file mode 100644 index 0000000000..a448eb0023 --- /dev/null +++ b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts @@ -0,0 +1,40 @@ +import { $ } from "bun" +import { afterEach, describe, expect, test } from "bun:test" +import * as fs from "fs/promises" +import path from "path" +import { Instance } from "../../src/project/instance" +import { WorktreeFamily } from "../../src/kilocode/worktree-family" +import { Log } from "../../src/util" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +afterEach(async () => { + await Instance.disposeAll() +}) + +describe("WorktreeFamily.list — git submodule", () => { + test("returns the submodule's working tree, not its gitdir", async () => { + await using parent = await tmpdir({ git: true }) + await using child = await tmpdir({ git: true }) + + // `protocol.file.allow=always` so the local clone is permitted, then commit + // the .gitmodules entry so the submodule is part of the parent's history. + await $`git -c protocol.file.allow=always submodule add ${child.path} sub`.cwd(parent.path).quiet() + await $`git commit -m "add submodule"`.cwd(parent.path).quiet() + + const submodule = path.join(parent.path, "sub") + const submoduleReal = await fs.realpath(submodule) + + await Instance.provide({ + directory: submodule, + fn: async () => { + const dirs = await WorktreeFamily.list() + // `git worktree list --porcelain` from inside a submodule reports the + // gitdir (`/.git/modules/sub`) as the worktree, so without the + // submodule guard the actual working tree is missing. + expect(dirs).toContain(submoduleReal) + }, + }) + }) +}) From 4c8a4064fcc54eaba3511874ee3d3ac75029948f Mon Sep 17 00:00:00 2001 From: truffle Date: Sat, 9 May 2026 05:07:42 +0000 Subject: [PATCH 02/74] test(cli): align Log import with @opencode-ai/core path after util refactor The src/util barrel was split into per-file modules in main; every other test in test/kilocode/ now imports Log from @opencode-ai/core/util/log directly. Match the pattern. --- .../opencode/test/kilocode/worktree-family-submodule.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts index a448eb0023..908a1f9693 100644 --- a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts +++ b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts @@ -4,7 +4,7 @@ import * as fs from "fs/promises" import path from "path" import { Instance } from "../../src/project/instance" import { WorktreeFamily } from "../../src/kilocode/worktree-family" -import { Log } from "../../src/util" +import * as Log from "@opencode-ai/core/util/log" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) From a1e487b044b94c07f3603fee9025b60afa1ee7a8 Mon Sep 17 00:00:00 2001 From: truffle Date: Sat, 9 May 2026 05:13:38 +0000 Subject: [PATCH 03/74] test(cli): switch to disposeAllInstances fixture helper after Instance API migration PR #25418 replaced Instance.disposeAll with the disposeAllInstances helper re-exported from test/fixture/fixture. Match the pattern used by the sister tests in test/kilocode/. --- .../opencode/test/kilocode/worktree-family-submodule.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts index 908a1f9693..42a81eafeb 100644 --- a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts +++ b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts @@ -5,12 +5,12 @@ import path from "path" import { Instance } from "../../src/project/instance" import { WorktreeFamily } from "../../src/kilocode/worktree-family" import * as Log from "@opencode-ai/core/util/log" -import { tmpdir } from "../fixture/fixture" +import { disposeAllInstances, tmpdir } from "../fixture/fixture" Log.init({ print: false }) afterEach(async () => { - await Instance.disposeAll() + await disposeAllInstances() }) describe("WorktreeFamily.list — git submodule", () => { From a7c12dca33736a88d82669261d71ba25ef28ba74 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 12 May 2026 20:32:34 -0600 Subject: [PATCH 04/74] fix(tui): handle newlines in DialogAlert messages The DialogAlert component was rendering '\n' literally instead of converting it to actual line breaks. This fix: - Imports For from solid-js for proper list rendering - Uses flexDirection='column' to stack lines vertically - Replaces escaped \n with actual newlines before splitting This affects the Teams tab message which displays: 'You're not a member of any teams.\nVisit https://app.kilo.ai...' --- packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index fb159115dc..ee017e660e 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -2,6 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { useKeyboard } from "@opentui/solid" +import { For } from "solid-js" export type DialogAlertProps = { title: string @@ -31,8 +32,10 @@ export function DialogAlert(props: DialogAlertProps) { esc - - {props.message} + + + {(line) => {line}} + Date: Tue, 12 May 2026 21:16:08 -0600 Subject: [PATCH 05/74] fix: add kilocode_change markers to dialog-alert.tsx for multi-line message support --- .../opencode/src/cli/cmd/tui/ui/dialog-alert.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index ee017e660e..7f8d4c2e06 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { useKeyboard } from "@opentui/solid" -import { For } from "solid-js" +import { For } from "solid-js" // kilocode_change export type DialogAlertProps = { title: string @@ -32,11 +32,13 @@ export function DialogAlert(props: DialogAlertProps) { esc - - - {(line) => {line}} - - + {/* kilocode_change start */} + + + {(line) => {line}} + + + {/* kilocode_change end */} Date: Fri, 15 May 2026 18:51:10 +0200 Subject: [PATCH 06/74] feat(cli): add balance command --- .changeset/fresh-balance-check.md | 5 ++ packages/opencode/src/index.ts | 2 + .../opencode/src/kilocode/cli/cmd/balance.ts | 60 +++++++++++++++++++ packages/opencode/src/kilocode/commands.ts | 2 + .../test/kilocode/cli/balance.test.ts | 45 ++++++++++++++ packages/opencode/test/kilocode/help.test.ts | 6 +- 6 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 .changeset/fresh-balance-check.md create mode 100644 packages/opencode/src/kilocode/cli/cmd/balance.ts create mode 100644 packages/opencode/test/kilocode/cli/balance.test.ts diff --git a/.changeset/fresh-balance-check.md b/.changeset/fresh-balance-check.md new file mode 100644 index 0000000000..e14f0d9847 --- /dev/null +++ b/.changeset/fresh-balance-check.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Add a `kilo balance` command for checking the active Kilo account or team balance. diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 116343fb45..0eac7ffe97 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -36,6 +36,7 @@ import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { RemoteCommand } from "./cli/cmd/remote" // kilocode_change import { RollCallCommand } from "./kilocode/cli/cmd/roll-call" // kilocode_change +import { BalanceCommand } from "./kilocode/cli/cmd/balance" // kilocode_change import { DevSetupCommand, DevAliasCommand } from "./kilocode/cli/dev-setup" // kilocode_change // kilocode_change start - Import telemetry, instance disposal, and legacy migration import { Telemetry } from "@kilocode/kilo-telemetry" @@ -231,6 +232,7 @@ let cli = yargs(args) // kilocode_change // .command(WebCommand) // kilocode_change (Disabled unsupported opencode web UI) .command(ModelsCommand) .command(RollCallCommand) // kilocode_change + .command(BalanceCommand) // kilocode_change .command(StatsCommand) .command(ExportCommand) .command(ImportCommand) diff --git a/packages/opencode/src/kilocode/cli/cmd/balance.ts b/packages/opencode/src/kilocode/cli/cmd/balance.ts new file mode 100644 index 0000000000..83085ad5a1 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/balance.ts @@ -0,0 +1,60 @@ +import type { Argv } from "yargs" +import { cmd } from "../../../cli/cmd/cmd" +import { UI } from "../../../cli/ui" +import { Auth } from "../../../auth" +import { fetchBalance, fetchProfile, type KilocodeBalance, type KilocodeProfile } from "@kilocode/kilo-gateway" + +interface Info { + email: string + team: string + organizationId: string | null + balance: number +} + +export function payload(input: { + profile: KilocodeProfile + balance: KilocodeBalance | null + organizationId?: string | null +}): Info { + const org = input.profile.organizations?.find((item) => item.id === input.organizationId) + return { + email: input.profile.email, + team: org?.name ?? "Personal", + organizationId: input.organizationId ?? null, + balance: input.balance?.balance ?? 0, + } +} + +export function format(info: Info): string { + return [`Account: ${info.email}`, `Team: ${info.team}`, `Balance: $${info.balance.toFixed(2)}`].join("\n") +} + +export const BalanceCommand = cmd({ + command: "balance", + describe: "show Kilo account balance", + builder: (yargs: Argv) => + yargs.option("json", { + describe: "output balance as JSON", + type: "boolean", + default: false, + }), + handler: async (args) => { + const auth = await Auth.get("kilo") + if (!auth || auth.type !== "oauth") { + UI.error("Not authenticated with Kilo Gateway") + process.exitCode = 1 + return + } + + const org = auth.accountId ?? null + const [profile, balance] = await Promise.all([fetchProfile(auth.access), fetchBalance(auth.access, org ?? undefined)]) + const info = payload({ profile, balance, organizationId: org }) + + if (args.json) { + console.log(JSON.stringify(info, null, 2)) + return + } + + UI.println(format(info)) + }, +}) diff --git a/packages/opencode/src/kilocode/commands.ts b/packages/opencode/src/kilocode/commands.ts index d0ca10ad03..05203cd47c 100644 --- a/packages/opencode/src/kilocode/commands.ts +++ b/packages/opencode/src/kilocode/commands.ts @@ -25,6 +25,7 @@ import { ConfigCommand as ConfigCLICommand } from "../cli/cmd/config" import { PluginCommand } from "../cli/cmd/plug" import { DevSetupCommand, DevAliasCommand } from "./cli/dev-setup" import { RollCallCommand } from "./cli/cmd/roll-call" +import { BalanceCommand } from "./cli/cmd/balance" import { HelpCommand } from "./help-command" import { InstallationBuildKind } from "@opencode-ai/core/installation/version" @@ -57,6 +58,7 @@ export const commands = [ ServeCommand, ModelsCommand, RollCallCommand, + BalanceCommand, StatsCommand, ExportCommand, ImportCommand, diff --git a/packages/opencode/test/kilocode/cli/balance.test.ts b/packages/opencode/test/kilocode/cli/balance.test.ts new file mode 100644 index 0000000000..e0e0655d81 --- /dev/null +++ b/packages/opencode/test/kilocode/cli/balance.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" + +import { format, payload } from "../../../src/kilocode/cli/cmd/balance" + +describe("balance CLI formatting", () => { + test("formats personal balance for human output", () => { + expect( + format({ + email: "one@example.com", + team: "Personal", + organizationId: null, + balance: 12.345, + }), + ).toBe("Account: one@example.com\nTeam: Personal\nBalance: $12.35") + }) + + test("formats team balance for human output", () => { + expect( + format({ + email: "one@example.com", + team: "Team One", + organizationId: "org-1", + balance: 7, + }), + ).toBe("Account: one@example.com\nTeam: Team One\nBalance: $7.00") + }) + + test("creates JSON payload", () => { + expect( + payload({ + profile: { + email: "one@example.com", + organizations: [{ id: "org-1", name: "Team One", role: "admin" }], + }, + balance: { balance: 3.5 }, + organizationId: "org-1", + }), + ).toEqual({ + email: "one@example.com", + team: "Team One", + organizationId: "org-1", + balance: 3.5, + }) + }) +}) diff --git a/packages/opencode/test/kilocode/help.test.ts b/packages/opencode/test/kilocode/help.test.ts index 7e8481db93..833735ce3e 100644 --- a/packages/opencode/test/kilocode/help.test.ts +++ b/packages/opencode/test/kilocode/help.test.ts @@ -23,6 +23,7 @@ import { ConfigCommand as ConfigCLICommand } from "../../src/cli/cmd/config" import { PluginCommand } from "../../src/cli/cmd/plug" import { DbCommand } from "../../src/cli/cmd/db" import { HelpCommand } from "../../src/kilocode/help-command" +import { BalanceCommand } from "../../src/kilocode/cli/cmd/balance" // Stand-in for TuiThreadCommand — the real one imports @opentui/solid which // doesn't resolve in the test environment. Only command/describe matter here. @@ -70,6 +71,7 @@ const commands = [ DbCommand, ConfigCLICommand, PluginCommand, + BalanceCommand, HelpCommand, CompletionStub, ] as any[] @@ -77,7 +79,7 @@ const commands = [ describe("kilo help --all (markdown)", () => { test("contains ## heading for each known top-level command", async () => { const output = await generateHelp({ all: true, format: "md", commands }) - for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) { + for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) { expect(output).toContain(`## kilo ${cmd}`) } }) @@ -99,7 +101,7 @@ describe("kilo help --all (text)", () => { test("still contains each command name", async () => { const output = await generateHelp({ all: true, format: "text", commands }) - for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent"]) { + for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) { expect(output).toContain(`kilo ${cmd}`) } }) From 58f9754a37e390ff5670b5bb8ba18eba6b54b336 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Fri, 15 May 2026 20:37:49 +0200 Subject: [PATCH 07/74] fix(cli): handle balance command errors --- .../opencode/src/kilocode/cli/cmd/balance.ts | 66 ++++++++++++++----- .../test/kilocode/cli/balance.test.ts | 44 ++++++++++++- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/kilocode/cli/cmd/balance.ts b/packages/opencode/src/kilocode/cli/cmd/balance.ts index 83085ad5a1..a93085b65f 100644 --- a/packages/opencode/src/kilocode/cli/cmd/balance.ts +++ b/packages/opencode/src/kilocode/cli/cmd/balance.ts @@ -1,7 +1,7 @@ import type { Argv } from "yargs" import { cmd } from "../../../cli/cmd/cmd" import { UI } from "../../../cli/ui" -import { Auth } from "../../../auth" +import { Auth, type Info as AuthInfo } from "../../../auth" import { fetchBalance, fetchProfile, type KilocodeBalance, type KilocodeProfile } from "@kilocode/kilo-gateway" interface Info { @@ -29,6 +29,15 @@ export function format(info: Info): string { return [`Account: ${info.email}`, `Team: ${info.team}`, `Balance: $${info.balance.toFixed(2)}`].join("\n") } +interface Args { + json: boolean + getAuth?: (providerID: string) => Promise + getProfile?: (token: string) => Promise + getBalance?: (token: string, organizationId?: string) => Promise + error?: (msg: string) => void + exit?: (code: number) => void +} + export const BalanceCommand = cmd({ command: "balance", describe: "show Kilo account balance", @@ -39,22 +48,43 @@ export const BalanceCommand = cmd({ default: false, }), handler: async (args) => { - const auth = await Auth.get("kilo") - if (!auth || auth.type !== "oauth") { - UI.error("Not authenticated with Kilo Gateway") - process.exitCode = 1 - return - } - - const org = auth.accountId ?? null - const [profile, balance] = await Promise.all([fetchProfile(auth.access), fetchBalance(auth.access, org ?? undefined)]) - const info = payload({ profile, balance, organizationId: org }) - - if (args.json) { - console.log(JSON.stringify(info, null, 2)) - return - } - - UI.println(format(info)) + await handle({ json: args.json }) }, }) + +export async function handle(args: Args) { + const auth = await (args.getAuth ?? Auth.get)("kilo") + const error = args.error ?? UI.error + const exit = args.exit ?? ((code: number) => (process.exitCode = code)) + + if (!auth || auth.type !== "oauth") { + error("Not authenticated with Kilo Gateway") + exit(1) + return + } + + const org = auth.accountId ?? null + const result = await (async () => { + try { + return await Promise.all([ + (args.getProfile ?? fetchProfile)(auth.access), + (args.getBalance ?? fetchBalance)(auth.access, org ?? undefined), + ] as const) + } catch (err) { + error(err instanceof Error ? err.message : String(err)) + exit(1) + return undefined + } + })() + if (!result) return + + const [profile, balance] = result + const info = payload({ profile, balance, organizationId: org }) + + if (args.json) { + console.log(JSON.stringify(info, null, 2)) + return + } + + process.stdout.write(format(info) + "\n") +} diff --git a/packages/opencode/test/kilocode/cli/balance.test.ts b/packages/opencode/test/kilocode/cli/balance.test.ts index e0e0655d81..15bc4f576d 100644 --- a/packages/opencode/test/kilocode/cli/balance.test.ts +++ b/packages/opencode/test/kilocode/cli/balance.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { format, payload } from "../../../src/kilocode/cli/cmd/balance" +import { format, handle, payload } from "../../../src/kilocode/cli/cmd/balance" describe("balance CLI formatting", () => { test("formats personal balance for human output", () => { @@ -42,4 +42,46 @@ describe("balance CLI formatting", () => { balance: 3.5, }) }) + + test("writes human output to stdout", async () => { + const logs: string[] = [] + const write = process.stdout.write + + process.stdout.write = ((chunk: string | Uint8Array) => { + logs.push(String(chunk)) + return true + }) as typeof process.stdout.write + + try { + await handle({ + json: false, + getAuth: async () => ({ type: "oauth", refresh: "refresh", access: "token", expires: 1 }), + getProfile: async () => ({ email: "one@example.com" }), + getBalance: async () => ({ balance: 4 }), + }) + } finally { + process.stdout.write = write + } + + expect(logs.join("")).toBe("Account: one@example.com\nTeam: Personal\nBalance: $4.00\n") + }) + + test("handles profile fetch errors without throwing", async () => { + const errors: string[] = [] + const codes: number[] = [] + + await handle({ + json: false, + error: (msg) => errors.push(msg), + exit: (code) => codes.push(code), + getAuth: async () => ({ type: "oauth", refresh: "refresh", access: "token", expires: 1 }), + getProfile: async () => { + throw new Error("Invalid token") + }, + getBalance: async () => ({ balance: 4 }), + }) + + expect(errors).toEqual(["Invalid token"]) + expect(codes).toEqual([1]) + }) }) From 8ba95b1f23fb1c73d5ce12d1031070c4c4332815 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Fri, 15 May 2026 21:03:47 -0600 Subject: [PATCH 08/74] feat(cli): show running spinner in subagent footer Add an animated spinner to the subagent session footer to indicate when a subagent is actively working, matching the running status indicator shown in the main session prompt. --- .../tui/routes/session/subagent-footer.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx index 18e8202812..d037514c0c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx @@ -3,6 +3,8 @@ import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" +import { Spinner } from "@tui/component/spinner" // kilocode_change +import { useLocal } from "@tui/context/local" // kilocode_change import type { AssistantMessage } from "@kilocode/sdk/v2" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "../../context/keybind" @@ -12,9 +14,24 @@ import { useTerminalDimensions } from "@opentui/solid" export function SubagentFooter() { const route = useRouteData("session") const sync = useSync() + const local = useLocal() // kilocode_change const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const session = createMemo(() => sync.session.get(route.sessionID)) + // kilocode_change start + const lastAssistant = createMemo(() => messages().findLast((m) => m.role === "assistant")) + + const isRunning = createMemo(() => { + const status = sync.data.session_status?.[route.sessionID] + if (status?.type === "busy") return true + const last = lastAssistant() + if (last && !last.time.completed) return true + return false + }) + + const agentColor = createMemo(() => local.agent.color(lastAssistant()?.agent ?? "")) + // kilocode_change end + const subagentInfo = createMemo(() => { const s = session() if (!s) return { label: "Subagent", index: 0, total: 0 } @@ -84,6 +101,11 @@ export function SubagentFooter() { ({subagentInfo().index} of {subagentInfo().total}) + {/* kilocode_change start */} + + + + {/* kilocode_change end */} {(item) => ( From c265fa4c4ef18204f8e2741c66953c24bf012f2a Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sat, 16 May 2026 10:20:02 -0600 Subject: [PATCH 09/74] chore: add changeset for subagent footer spinner --- .changeset/subagent-footer-spinner.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/subagent-footer-spinner.md diff --git a/.changeset/subagent-footer-spinner.md b/.changeset/subagent-footer-spinner.md new file mode 100644 index 0000000000..d5ee705744 --- /dev/null +++ b/.changeset/subagent-footer-spinner.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Show running spinner in subagent footer to indicate when subagent is processing From 0231a1f3c1b2b9148e5072748b71c9321612ed1b Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 16:21:03 +0000 Subject: [PATCH 10/74] fix(cli): rename balance command to profile --- .changeset/fresh-balance-check.md | 2 +- packages/opencode/src/index.ts | 4 ++-- .../cli/cmd/{balance.ts => profile.ts} | 18 +++++++++++++----- packages/opencode/src/kilocode/commands.ts | 4 ++-- .../cli/{balance.test.ts => profile.test.ts} | 18 +++++++++++------- packages/opencode/test/kilocode/help.test.ts | 8 ++++---- 6 files changed, 33 insertions(+), 21 deletions(-) rename packages/opencode/src/kilocode/cli/cmd/{balance.ts => profile.ts} (85%) rename packages/opencode/test/kilocode/cli/{balance.test.ts => profile.test.ts} (78%) diff --git a/.changeset/fresh-balance-check.md b/.changeset/fresh-balance-check.md index e14f0d9847..be117b3cc1 100644 --- a/.changeset/fresh-balance-check.md +++ b/.changeset/fresh-balance-check.md @@ -2,4 +2,4 @@ "@kilocode/cli": minor --- -Add a `kilo balance` command for checking the active Kilo account or team balance. +Add a `kilo profile` command for checking the active Kilo account or team balance. diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 0eac7ffe97..d53cc17d5d 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -36,7 +36,7 @@ import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { RemoteCommand } from "./cli/cmd/remote" // kilocode_change import { RollCallCommand } from "./kilocode/cli/cmd/roll-call" // kilocode_change -import { BalanceCommand } from "./kilocode/cli/cmd/balance" // kilocode_change +import { ProfileCommand } from "./kilocode/cli/cmd/profile" // kilocode_change import { DevSetupCommand, DevAliasCommand } from "./kilocode/cli/dev-setup" // kilocode_change // kilocode_change start - Import telemetry, instance disposal, and legacy migration import { Telemetry } from "@kilocode/kilo-telemetry" @@ -232,7 +232,7 @@ let cli = yargs(args) // kilocode_change // .command(WebCommand) // kilocode_change (Disabled unsupported opencode web UI) .command(ModelsCommand) .command(RollCallCommand) // kilocode_change - .command(BalanceCommand) // kilocode_change + .command(ProfileCommand) // kilocode_change .command(StatsCommand) .command(ExportCommand) .command(ImportCommand) diff --git a/packages/opencode/src/kilocode/cli/cmd/balance.ts b/packages/opencode/src/kilocode/cli/cmd/profile.ts similarity index 85% rename from packages/opencode/src/kilocode/cli/cmd/balance.ts rename to packages/opencode/src/kilocode/cli/cmd/profile.ts index a93085b65f..8feef9e687 100644 --- a/packages/opencode/src/kilocode/cli/cmd/balance.ts +++ b/packages/opencode/src/kilocode/cli/cmd/profile.ts @@ -5,6 +5,7 @@ import { Auth, type Info as AuthInfo } from "../../../auth" import { fetchBalance, fetchProfile, type KilocodeBalance, type KilocodeProfile } from "@kilocode/kilo-gateway" interface Info { + name: string | null email: string team: string organizationId: string | null @@ -18,6 +19,7 @@ export function payload(input: { }): Info { const org = input.profile.organizations?.find((item) => item.id === input.organizationId) return { + name: input.profile.name ?? null, email: input.profile.email, team: org?.name ?? "Personal", organizationId: input.organizationId ?? null, @@ -26,7 +28,13 @@ export function payload(input: { } export function format(info: Info): string { - return [`Account: ${info.email}`, `Team: ${info.team}`, `Balance: $${info.balance.toFixed(2)}`].join("\n") + const lines = [ + ...(info.name ? [`Name: ${info.name}`] : []), + `Email: ${info.email}`, + `Team: ${info.team}`, + `Balance: $${info.balance.toFixed(2)}`, + ] + return lines.join("\n") } interface Args { @@ -38,12 +46,12 @@ interface Args { exit?: (code: number) => void } -export const BalanceCommand = cmd({ - command: "balance", - describe: "show Kilo account balance", +export const ProfileCommand = cmd({ + command: "profile", + describe: "show Kilo account profile", builder: (yargs: Argv) => yargs.option("json", { - describe: "output balance as JSON", + describe: "output profile as JSON", type: "boolean", default: false, }), diff --git a/packages/opencode/src/kilocode/commands.ts b/packages/opencode/src/kilocode/commands.ts index 05203cd47c..98c8653c7d 100644 --- a/packages/opencode/src/kilocode/commands.ts +++ b/packages/opencode/src/kilocode/commands.ts @@ -25,7 +25,7 @@ import { ConfigCommand as ConfigCLICommand } from "../cli/cmd/config" import { PluginCommand } from "../cli/cmd/plug" import { DevSetupCommand, DevAliasCommand } from "./cli/dev-setup" import { RollCallCommand } from "./cli/cmd/roll-call" -import { BalanceCommand } from "./cli/cmd/balance" +import { ProfileCommand } from "./cli/cmd/profile" import { HelpCommand } from "./help-command" import { InstallationBuildKind } from "@opencode-ai/core/installation/version" @@ -58,7 +58,7 @@ export const commands = [ ServeCommand, ModelsCommand, RollCallCommand, - BalanceCommand, + ProfileCommand, StatsCommand, ExportCommand, ImportCommand, diff --git a/packages/opencode/test/kilocode/cli/balance.test.ts b/packages/opencode/test/kilocode/cli/profile.test.ts similarity index 78% rename from packages/opencode/test/kilocode/cli/balance.test.ts rename to packages/opencode/test/kilocode/cli/profile.test.ts index 15bc4f576d..5d1f0a2f30 100644 --- a/packages/opencode/test/kilocode/cli/balance.test.ts +++ b/packages/opencode/test/kilocode/cli/profile.test.ts @@ -1,34 +1,37 @@ import { describe, expect, test } from "bun:test" -import { format, handle, payload } from "../../../src/kilocode/cli/cmd/balance" +import { format, handle, payload } from "../../../src/kilocode/cli/cmd/profile" -describe("balance CLI formatting", () => { +describe("profile CLI formatting", () => { test("formats personal balance for human output", () => { expect( format({ + name: null, email: "one@example.com", team: "Personal", organizationId: null, balance: 12.345, }), - ).toBe("Account: one@example.com\nTeam: Personal\nBalance: $12.35") + ).toBe("Email: one@example.com\nTeam: Personal\nBalance: $12.35") }) - test("formats team balance for human output", () => { + test("formats profile name for human output", () => { expect( format({ + name: "User One", email: "one@example.com", team: "Team One", organizationId: "org-1", balance: 7, }), - ).toBe("Account: one@example.com\nTeam: Team One\nBalance: $7.00") + ).toBe("Name: User One\nEmail: one@example.com\nTeam: Team One\nBalance: $7.00") }) test("creates JSON payload", () => { expect( payload({ profile: { + name: "User One", email: "one@example.com", organizations: [{ id: "org-1", name: "Team One", role: "admin" }], }, @@ -36,6 +39,7 @@ describe("balance CLI formatting", () => { organizationId: "org-1", }), ).toEqual({ + name: "User One", email: "one@example.com", team: "Team One", organizationId: "org-1", @@ -56,14 +60,14 @@ describe("balance CLI formatting", () => { await handle({ json: false, getAuth: async () => ({ type: "oauth", refresh: "refresh", access: "token", expires: 1 }), - getProfile: async () => ({ email: "one@example.com" }), + getProfile: async () => ({ email: "one@example.com", name: "User One" }), getBalance: async () => ({ balance: 4 }), }) } finally { process.stdout.write = write } - expect(logs.join("")).toBe("Account: one@example.com\nTeam: Personal\nBalance: $4.00\n") + expect(logs.join("")).toBe("Name: User One\nEmail: one@example.com\nTeam: Personal\nBalance: $4.00\n") }) test("handles profile fetch errors without throwing", async () => { diff --git a/packages/opencode/test/kilocode/help.test.ts b/packages/opencode/test/kilocode/help.test.ts index 833735ce3e..3e6df088f5 100644 --- a/packages/opencode/test/kilocode/help.test.ts +++ b/packages/opencode/test/kilocode/help.test.ts @@ -23,7 +23,7 @@ import { ConfigCommand as ConfigCLICommand } from "../../src/cli/cmd/config" import { PluginCommand } from "../../src/cli/cmd/plug" import { DbCommand } from "../../src/cli/cmd/db" import { HelpCommand } from "../../src/kilocode/help-command" -import { BalanceCommand } from "../../src/kilocode/cli/cmd/balance" +import { ProfileCommand } from "../../src/kilocode/cli/cmd/profile" // Stand-in for TuiThreadCommand — the real one imports @opentui/solid which // doesn't resolve in the test environment. Only command/describe matter here. @@ -71,7 +71,7 @@ const commands = [ DbCommand, ConfigCLICommand, PluginCommand, - BalanceCommand, + ProfileCommand, HelpCommand, CompletionStub, ] as any[] @@ -79,7 +79,7 @@ const commands = [ describe("kilo help --all (markdown)", () => { test("contains ## heading for each known top-level command", async () => { const output = await generateHelp({ all: true, format: "md", commands }) - for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) { + for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "profile"]) { expect(output).toContain(`## kilo ${cmd}`) } }) @@ -101,7 +101,7 @@ describe("kilo help --all (text)", () => { test("still contains each command name", async () => { const output = await generateHelp({ all: true, format: "text", commands }) - for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "balance"]) { + for (const cmd of ["run", "auth", "debug", "mcp", "session", "agent", "profile"]) { expect(output).toContain(`kilo ${cmd}`) } }) From b590f8c25f1af82e7df854b5b969ae8749118bba Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sat, 16 May 2026 10:23:13 -0600 Subject: [PATCH 11/74] chore: add changeset for dialog alert newlines --- .changeset/dialog-alert-newlines.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dialog-alert-newlines.md diff --git a/.changeset/dialog-alert-newlines.md b/.changeset/dialog-alert-newlines.md new file mode 100644 index 0000000000..782b28c611 --- /dev/null +++ b/.changeset/dialog-alert-newlines.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Handle newlines in DialogAlert messages From aca8aeb2b91679b52937562d45986562440ac1de Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 19 May 2026 20:29:15 -0600 Subject: [PATCH 12/74] fix(cli): toggle export dialog checkboxes on mouse click The export options dialog checkbox rows only set visual focus on mouse click but don't toggle the checkbox state. Update onMouseUp handlers to both set focus and toggle the corresponding boolean value. --- .changeset/export-dialog-checkbox-click.md | 5 +++++ .../cli/cmd/tui/ui/dialog-export-options.tsx | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .changeset/export-dialog-checkbox-click.md diff --git a/.changeset/export-dialog-checkbox-click.md b/.changeset/export-dialog-checkbox-click.md new file mode 100644 index 0000000000..50a8d56c81 --- /dev/null +++ b/.changeset/export-dialog-checkbox-click.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Toggle export dialog checkboxes on mouse click diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx index b9362db46b..4f7f053efa 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx @@ -120,7 +120,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { gap={2} paddingLeft={1} backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined} - onMouseUp={() => setStore("active", "thinking")} + onMouseUp={() => { // kilocode_change start + setStore("active", "thinking") + setStore("thinking", !store.thinking) + } /* kilocode_change end */} > {store.thinking ? "[x]" : "[ ]"} @@ -132,7 +135,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { gap={2} paddingLeft={1} backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined} - onMouseUp={() => setStore("active", "toolDetails")} + onMouseUp={() => { // kilocode_change start + setStore("active", "toolDetails") + setStore("toolDetails", !store.toolDetails) + } /* kilocode_change end */} > {store.toolDetails ? "[x]" : "[ ]"} @@ -144,7 +150,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { gap={2} paddingLeft={1} backgroundColor={store.active === "assistantMetadata" ? theme.backgroundElement : undefined} - onMouseUp={() => setStore("active", "assistantMetadata")} + onMouseUp={() => { // kilocode_change start + setStore("active", "assistantMetadata") + setStore("assistantMetadata", !store.assistantMetadata) + } /* kilocode_change end */} > {store.assistantMetadata ? "[x]" : "[ ]"} @@ -156,7 +165,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) { gap={2} paddingLeft={1} backgroundColor={store.active === "openWithoutSaving" ? theme.backgroundElement : undefined} - onMouseUp={() => setStore("active", "openWithoutSaving")} + onMouseUp={() => { // kilocode_change start + setStore("active", "openWithoutSaving") + setStore("openWithoutSaving", !store.openWithoutSaving) + } /* kilocode_change end */} > {store.openWithoutSaving ? "[x]" : "[ ]"} From 3648e2e132da1cd1e346742acd9eadad9ed47c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Wed, 20 May 2026 13:43:02 -0300 Subject: [PATCH 13/74] wip: kilo console --- .kilo/plans/1779210897645-curious-wolf.md | 139 + .kilo/plans/webconfig.md | 741 ++++++ .opencode/opencode.jsonc | 1 + bun.lock | 21 + packages/core/src/kilocode/global.ts | 5 +- packages/kilo-config-ui/index.html | 12 + packages/kilo-config-ui/package.json | 27 + packages/kilo-config-ui/src/App.tsx | 17 + packages/kilo-config-ui/src/client.ts | 272 ++ .../src/components/ConfirmDialog.tsx | 42 + .../src/components/app-header/AppHeader.tsx | 26 + .../src/components/app-sidebar/AppSidebar.tsx | 54 + .../src/context/ConfigProvider.tsx | 169 ++ .../kilo-config-ui/src/context/config.tsx | 30 + packages/kilo-config-ui/src/index.tsx | 36 + .../src/layouts/ConfigLayout.tsx | 54 + .../src/layouts/ConsoleLayout.tsx | 21 + .../src/routes/config/AgentsRoute.tsx | 252 ++ .../src/routes/config/CliUiRoute.tsx | 26 + .../src/routes/config/ConfigPage.tsx | 41 + .../src/routes/config/ConfigRoute.tsx | 9 + .../src/routes/config/ConfigSidebar.tsx | 70 + .../src/routes/config/FormattersRoute.tsx | 130 + .../src/routes/config/KeybindsRoute.tsx | 54 + .../src/routes/config/McpRoute.tsx | 69 + .../src/routes/config/ModelsRoute.tsx | 441 ++++ .../src/routes/config/OverviewRoute.tsx | 73 + .../src/routes/config/PermissionsRoute.tsx | 66 + .../src/routes/config/ProvidersRoute.tsx | 400 +++ .../src/routes/config/RulesRoute.tsx | 71 + .../src/routes/config/ServersRoute.tsx | 128 + .../src/routes/config/SourcesRoute.tsx | 50 + .../src/routes/config/ToolsRoute.tsx | 39 + .../src/routes/config/sections.tsx | 119 + .../src/routes/config/state/agents.ts | 193 ++ .../src/routes/config/state/formatters.ts | 59 + .../src/routes/config/state/keybinds.ts | 32 + .../src/routes/config/state/mcp.ts | 50 + .../src/routes/config/state/models.ts | 248 ++ .../src/routes/config/state/permissions.ts | 70 + .../src/routes/config/state/providers.ts | 416 +++ .../src/routes/config/state/ui.ts | 32 + .../src/routes/profile/ProfileRoute.tsx | 9 + .../src/routes/projects/ProjectsRoute.tsx | 183 ++ .../kilo-config-ui/src/shared/navigation.ts | 20 + packages/kilo-config-ui/src/shared/utils.ts | 99 + packages/kilo-config-ui/src/styles.css | 1836 +++++++++++++ packages/kilo-config-ui/src/vite-env.d.ts | 3 + packages/kilo-config-ui/tsconfig.json | 19 + packages/kilo-config-ui/vite.config.ts | 20 + .../code-with-ai/platforms/cli-reference.md | 38 +- packages/kilo-gateway/test/api/models.test.ts | 22 +- packages/opencode/src/cli/cmd/run.ts | 10 + packages/opencode/src/cli/cmd/tui/attach.ts | 5 +- packages/opencode/src/cli/cmd/tui/thread.ts | 76 +- packages/opencode/src/index.ts | 2 + .../opencode/src/kilocode/agent/builder.ts | 109 + .../opencode/src/kilocode/cli/cmd/daemon.ts | 117 + .../kilocode/components/model-info-panel.tsx | 7 +- .../src/kilocode/config/model-state.ts | 97 + .../opencode/src/kilocode/config/overlay.ts | 271 ++ .../opencode/src/kilocode/config/sources.ts | 315 +++ .../opencode/src/kilocode/daemon/client.ts | 49 + .../opencode/src/kilocode/daemon/daemon.ts | 359 +++ .../opencode/src/kilocode/server/instance.ts | 15 +- .../kilocode/server/routes/agent-builder.ts | 64 + .../server/routes/config-model-state.ts | 47 + .../kilocode/server/routes/config-overlay.ts | 106 + .../kilocode/server/routes/config-rules.ts | 113 + .../kilocode/server/routes/config-sources.ts | 73 + .../src/kilocode/server/routes/tui-config.ts | 70 + packages/opencode/src/kilocode/tui/config.ts | 124 + packages/opencode/src/plugin/codex.ts | 8 +- packages/opencode/src/project/bootstrap.ts | 13 +- packages/opencode/src/provider/provider.ts | 3 +- .../src/server/routes/instance/index.ts | 5 +- packages/opencode/src/session/compaction.ts | 11 +- packages/opencode/src/tool/bash.ts | 1 - packages/opencode/src/tool/webfetch.ts | 3 +- packages/opencode/test/cli/tui/thread.test.ts | 8 + .../test/kilocode/codex-auth-refresh.test.ts | 3 +- .../opencode/test/kilocode/daemon.test.ts | 139 + .../opencode/test/kilocode/encoding.test.ts | 31 +- .../provider-list-failed-state.test.ts | 14 +- .../kilocode/server/agent-builder.test.ts | 113 + .../server/config-model-state.test.ts | 79 + .../kilocode/server/config-overlay.test.ts | 146 ++ .../test/kilocode/server/config-rules.test.ts | 72 + .../kilocode/server/config-sources.test.ts | 153 ++ .../test/kilocode/server/tui-config.test.ts | 64 + .../session/instruction-substitution.test.ts | 6 +- .../opencode/test/kilocode/util/url.test.ts | 4 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 638 ++++- packages/sdk/js/src/v2/gen/types.gen.ts | 865 ++++++ packages/sdk/openapi.json | 2328 +++++++++++++++++ .../transforms/transform-package-json.test.ts | 6 +- .../transforms/transform-package-json.ts | 7 +- 97 files changed, 13707 insertions(+), 96 deletions(-) create mode 100644 .kilo/plans/1779210897645-curious-wolf.md create mode 100644 .kilo/plans/webconfig.md create mode 100644 packages/kilo-config-ui/index.html create mode 100644 packages/kilo-config-ui/package.json create mode 100644 packages/kilo-config-ui/src/App.tsx create mode 100644 packages/kilo-config-ui/src/client.ts create mode 100644 packages/kilo-config-ui/src/components/ConfirmDialog.tsx create mode 100644 packages/kilo-config-ui/src/components/app-header/AppHeader.tsx create mode 100644 packages/kilo-config-ui/src/components/app-sidebar/AppSidebar.tsx create mode 100644 packages/kilo-config-ui/src/context/ConfigProvider.tsx create mode 100644 packages/kilo-config-ui/src/context/config.tsx create mode 100644 packages/kilo-config-ui/src/index.tsx create mode 100644 packages/kilo-config-ui/src/layouts/ConfigLayout.tsx create mode 100644 packages/kilo-config-ui/src/layouts/ConsoleLayout.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/AgentsRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/CliUiRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ConfigPage.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ConfigRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ConfigSidebar.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/FormattersRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/KeybindsRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/McpRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ModelsRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/OverviewRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/PermissionsRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ProvidersRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/RulesRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ServersRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/SourcesRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/ToolsRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/sections.tsx create mode 100644 packages/kilo-config-ui/src/routes/config/state/agents.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/formatters.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/keybinds.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/mcp.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/models.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/permissions.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/providers.ts create mode 100644 packages/kilo-config-ui/src/routes/config/state/ui.ts create mode 100644 packages/kilo-config-ui/src/routes/profile/ProfileRoute.tsx create mode 100644 packages/kilo-config-ui/src/routes/projects/ProjectsRoute.tsx create mode 100644 packages/kilo-config-ui/src/shared/navigation.ts create mode 100644 packages/kilo-config-ui/src/shared/utils.ts create mode 100644 packages/kilo-config-ui/src/styles.css create mode 100644 packages/kilo-config-ui/src/vite-env.d.ts create mode 100644 packages/kilo-config-ui/tsconfig.json create mode 100644 packages/kilo-config-ui/vite.config.ts create mode 100644 packages/opencode/src/kilocode/agent/builder.ts create mode 100644 packages/opencode/src/kilocode/cli/cmd/daemon.ts create mode 100644 packages/opencode/src/kilocode/config/model-state.ts create mode 100644 packages/opencode/src/kilocode/config/overlay.ts create mode 100644 packages/opencode/src/kilocode/config/sources.ts create mode 100644 packages/opencode/src/kilocode/daemon/client.ts create mode 100644 packages/opencode/src/kilocode/daemon/daemon.ts create mode 100644 packages/opencode/src/kilocode/server/routes/agent-builder.ts create mode 100644 packages/opencode/src/kilocode/server/routes/config-model-state.ts create mode 100644 packages/opencode/src/kilocode/server/routes/config-overlay.ts create mode 100644 packages/opencode/src/kilocode/server/routes/config-rules.ts create mode 100644 packages/opencode/src/kilocode/server/routes/config-sources.ts create mode 100644 packages/opencode/src/kilocode/server/routes/tui-config.ts create mode 100644 packages/opencode/src/kilocode/tui/config.ts create mode 100644 packages/opencode/test/kilocode/daemon.test.ts create mode 100644 packages/opencode/test/kilocode/server/agent-builder.test.ts create mode 100644 packages/opencode/test/kilocode/server/config-model-state.test.ts create mode 100644 packages/opencode/test/kilocode/server/config-overlay.test.ts create mode 100644 packages/opencode/test/kilocode/server/config-rules.test.ts create mode 100644 packages/opencode/test/kilocode/server/config-sources.test.ts create mode 100644 packages/opencode/test/kilocode/server/tui-config.test.ts diff --git a/.kilo/plans/1779210897645-curious-wolf.md b/.kilo/plans/1779210897645-curious-wolf.md new file mode 100644 index 0000000000..ce22979af4 --- /dev/null +++ b/.kilo/plans/1779210897645-curious-wolf.md @@ -0,0 +1,139 @@ +# Plan: Config UI multinivel + +## Objetivo +Refactorizar `packages/kilo-config-ui/` e implementar el backend necesario para representar la configuración multinivel de Kilo con una estructura simple: configuración global, configuración de proyecto, valores heredados y sobrescrituras locales visibles. + +## Correcciones de base +- Usar nombres Kilo actuales: `kilo.json` / `kilo.jsonc`, `KILO_CONFIG`, `KILO_CONFIG_CONTENT`, `KILO_CONFIG_DIR`, `KILO_DISABLE_PROJECT_CONFIG`, `.kilo/` como directorio moderno, y `.kilocode/` / `.opencode/` como legacy. +- Mantener la lógica Kilo en rutas y módulos Kilo-owned bajo `packages/opencode/src/kilocode/` siempre que sea posible. +- Evitar que el frontend calcule precedencia. El backend debe devolver valor efectivo, valor global editable, valor local editable y metadatos de origen. +- No copiar configuraciones heredadas al archivo local al guardar. Las mutaciones del proyecto deben escribir solo el parche local necesario. + +## Estado Actual +- `packages/kilo-config-ui` ya tiene rutas `/config/*`, `/projects`, un `ConfigProvider`, y consume `@kilocode/sdk/v2/client`. +- El backend ya expone rutas Kilo-owned bajo `/config/sources`, `/config/effective`, `/profiles`, `/agent-builder` y `/tui`. +- La UI actual usa `snap.effective` para construir parches en secciones como MCP, permisos y providers. Eso puede persistir valores heredados en el scope local. +- `Config.update` y `Config.updateGlobal` ya escriben parches, pero el contrato actual no devuelve suficiente información para mostrar herencia ni resetear colecciones con precisión. + +## Decisiones De Alcance +- Implementar una primera versión enfocada en modelos, MCP, permisos, agents, formatters/LSP, sources y rules de proyecto. +- Tratar providers como global-only en la UI. En vistas de proyecto se mostrarán como heredados/read-only con acceso a settings globales. +- Añadir rutas canónicas `/settings/*` para global y `/projects/:id/settings/*` para proyecto. Reutilizar los mismos componentes con contexto de scope. +- Mantener `/projects` como índice de proyectos. Cada card debe enlazar al settings del proyecto usando el `id` en la URL y el `worktree` como directorio de instancia. +- Dejar para una fase posterior el simulador real de tokens MCP y métricas avanzadas. En esta fase se mostrará estado, número de servidores/herramientas si ya está disponible, y advertencias de complejidad. + +## Backend + +1. Crear `packages/opencode/src/kilocode/config/overlay.ts`. +- Definir schemas Zod para `Scope`, `Origin`, `ResolvedField`, `ResolvedCollectionItem`, `OverlayResult` y `OverlayPatch`. +- `Origin` inicial: `project`, `global`, `system`, `default`. +- `ResolvedField` debe incluir `key`, `value`, `global`, `local`, `source`, `inherited`, `overridden`, `editable`, `path` opcional y `reason` opcional. +- `OverlayResult` debe incluir `scope`, `effective`, `global`, `project`, `sources`, `targets`, `fields` y `collections`. + +2. Leer capas editables sin duplicar todo el motor de config. +- `effective`: usar `Config.Service.get()` y aplicar preview de profile con `KilocodeEffectiveConfig.profile(...)` cuando aplique. +- `global`: usar `Config.Service.getGlobal()` para la config global editable del usuario. +- `project`: agregar helper Kilo-owned para leer y fusionar solo config de proyecto editable usando la misma familia de archivos `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` y directorios `.kilo`, `.kilocode`, `.opencode`. +- `sources`: reutilizar `KilocodeConfigSources.list(...)`. +- `targets`: calcular el archivo global editable y reutilizar/exportar helper Kilo-owned equivalente a `projectConfigUpdateTarget` para mostrar dónde se escribirá. + +3. Resolver metadatos de campos. +- Implementar helpers `hasPath`, `getPath`, `setPath`, `unsetPath` en Kilo-owned code. +- Para campos escalares iniciales: `model`, `small_model`, `default_agent`, `snapshot`, `share`, `autoupdate`, `disabled_providers`, `enabled_providers`, `watcher.ignore`, `instructions`. +- Para colecciones iniciales: `mcp`, `permission`, `agent`, `formatter`, `lsp`, `provider`. +- En scope `project`, un valor es `inherited` cuando no existe localmente y sí existe globalmente. Es `overridden` cuando existe localmente. Es `system` cuando el efectivo existe pero no aparece en capas editables. + +4. Agregar ruta Hono Kilo-owned. +- Opción preferida: nueva ruta en `packages/opencode/src/kilocode/server/routes/config-overlay.ts` registrada bajo `/config/overlay` desde `packages/opencode/src/kilocode/server/instance.ts`. +- `GET /config/overlay?scope=global|project&profile=...` devuelve `OverlayResult`. +- `PATCH /config/overlay` acepta `{ scope, set?: Record, unset?: string[][] }`. +- Para `set`, validar contra `Config.Info.zod` cuando sea posible y delegar a `Config.updateGlobal` o `Config.update`. +- Para `unset`, aplicar null/delete sentinels contra el archivo target con `jsonc-parser`, validar el resultado completo, invalidar instancia y emitir el evento ya existente de config actualizada o dispose según corresponda. + +5. Evitar cambios compartidos innecesarios. +- Mantener toda la lógica en `src/kilocode/`. +- Si se toca un archivo shared, limitarlo a registro/import mínimo con `kilocode_change` estrecho. +- Regenerar SDK después de agregar endpoints con `./script/generate.ts` desde root. + +6. Agregar rules de proyecto. +- Crear helper/ruta Kilo-owned para `GET /config/rules?scope=project` y `PUT /config/rules`. +- V1 editará `AGENTS.md` del worktree de proyecto. Si no existe, crearlo al guardar. +- Exponer archivos encontrados (`AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`) como lectura contextual, pero solo editar `AGENTS.md` inicialmente. + +## Frontend + +1. Reorganizar routing. +- Cambiar rutas canónicas globales a `/settings`, `/settings/models`, `/settings/agents`, `/settings/mcp`, `/settings/permissions`, `/settings/providers`, `/settings/sources`, `/settings/servers`, `/settings/ui`, `/settings/keybinds`. +- Agregar rutas de proyecto `/projects/:id`, `/projects/:id/settings`, `/projects/:id/settings/models`, `/projects/:id/settings/agents`, `/projects/:id/settings/mcp`, `/projects/:id/settings/permissions`, `/projects/:id/settings/rules`, `/projects/:id/settings/formatters`, `/projects/:id/settings/sources`. +- Resolver `:id` con `project.list()` y usar `Project.worktree` como `directory` del SDK. Conservar `?directory=` como fallback para deep links. + +2. Refactorizar `ConfigProvider`. +- Reemplazar `scope` tomado de querystring por contexto derivado de la ruta. +- Cargar `Snapshot` desde el nuevo `config.overlay`, además de health, providers, auth methods, profiles, TUI, tools, MCP, LSP, formatter y agents. +- Añadir `patch(set, unset)` para escribir overrides sin partir de `effective`. +- Añadir refresco por `window.focus`, `visibilitychange` y, si es viable, streaming con `fetch` a `/global/event` o `/event` usando headers de auth. Evitar `EventSource` porque no permite headers personalizados. + +3. Crear componentes compartidos de herencia. +- `ScopeHeader`: muestra Global/Project, nombre del proyecto, directorio, profile y health. +- `SourceBadge`: `global`, `project`, `system`, `default`, `inherited`, `local override`. +- `OverrideControl`: acciones `Override`, `Revert to global`, `Disable locally` cuando aplique. +- `ResolvedFieldRow`: layout para campos escalares con opacidad reducida si son heredados. +- `CollectionSection`: separa `Project local`, `Inherited global` y `System/read-only`. + +4. Refactorizar secciones existentes. +- `ModelsRoute`: mostrar `model` y `small_model` como campos resueltos arriba del catálogo. `Default` y `Small` deben escribir solo `{ model: id }` o `{ small_model: id }`. Reset local usa `unset: [["model"]]` o `[["small_model"]]`. +- `McpRoute`: construir lista desde `overlay.collections.mcp`, no desde `effective`. Agregar MCP escribe `{ mcp: { [id]: cfg } }`. Deshabilitar heredado escribe `{ mcp: { [id]: { enabled: false } } }`. Revertir usa `unset: [["mcp", id]]`. +- `PermissionsRoute`: mostrar reglas heredadas atenuadas y reglas locales activas. Agregar regla debe escribir solo la herramienta/patrón afectado. Reset por regla usa `unset` sobre la ruta específica. +- `ProvidersRoute`: solo editable en `/settings/providers`. En proyecto, mostrar resumen read-only de providers heredados y CTA a global settings. +- `AgentsRoute`: mantener el builder, pero separar agentes cargados en locales/heredados cuando el overlay pueda clasificar `agent`. Agregar acciones de clonación/desactivación solo si el backend expone paths/origen suficiente; si no, mostrar read-only heredado y permitir crear uno local nuevo. +- `FormattersRoute`: usar overlay para distinguir formatters/LSP locales de heredados. +- `SourcesRoute`: reutilizar `sources` del overlay y destacar el target de escritura del scope actual. +- Nueva `RulesRoute`: editor de `AGENTS.md` para el proyecto, con advertencia de que el archivo vive en el repo. + +5. Simplificar navegación y copy. +- Cambiar labels de “Config” a “Settings” en la shell. +- El sidebar debe recibir el scope y generar `href` según global/proyecto. +- `ProjectsRoute` debe enlazar cada proyecto a su settings en vez de ser solo informativo. +- Evitar controles globales peligrosos dentro de project settings. + +6. Estilos. +- Añadir clases para inherited/overridden/read-only sin reescribir todo el CSS. +- Verificar desktop y mobile: sidebar colapsable o apilado en ancho pequeño, listas con overflow horizontal solo cuando sea inevitable. + +## Pruebas + +1. Backend unit/integration en `packages/opencode/test/kilocode/server/config-overlay.test.ts`. +- Hereda `model` global cuando no hay valor local. +- Marca `model` como `project` cuando hay override local. +- Reset local elimina la clave local y vuelve a exponer el global. +- Agregar MCP en proyecto no copia servidores globales al archivo local. +- Deshabilitar MCP heredado escribe solo `{ enabled: false }` para ese servidor. +- Agregar permiso local no copia todo `effective.permission`. +- Sources/overlay no exponen valores secretos de env o inline config. + +2. Rules tests en `packages/opencode/test/kilocode/server/config-rules.test.ts`. +- Lista archivos de reglas existentes. +- Crea/actualiza `AGENTS.md` en el worktree de proyecto. +- Rechaza escritura global en V1 si la ruta se limita a proyecto. + +3. Frontend validation. +- `bun run --cwd packages/kilo-config-ui typecheck`. +- `bun run --cwd packages/kilo-config-ui build`. + +4. CLI/backend validation. +- Desde `packages/opencode/`: `bun run typecheck`. +- Desde `packages/opencode/`: targeted `bun test ./test/kilocode/server/config-overlay.test.ts ./test/kilocode/server/config-rules.test.ts ./test/kilocode/project-config-update.test.ts ./test/kilocode/profile-overlay.test.ts`. +- Desde root, después de tocar shared `packages/opencode/`: `bun run script/check-opencode-annotations.ts`. + +5. SDK/codegen. +- Ejecutar `./script/generate.ts` desde root después de agregar rutas OpenAPI. +- Confirmar que `packages/sdk/js/src/v2/gen/*` y tipos usados por `kilo-config-ui` quedan actualizados. + +## Cambioset +- Si la UI/backend queda expuesta al usuario final en esta implementación, agregar changeset patch para `@kilocode/cli` describiendo: “Support project-aware settings with inherited global config and local overrides.” + +## Riesgos Y Mitigaciones +- Riesgo: duplicar valores heredados en `kilo.json` local. Mitigación: toda mutación usa `set/unset` local mínimo y tests específicos para MCP/permisos. +- Riesgo: divergencia con el motor real de configuración. Mitigación: usar `Config.Service.get()` para efectivo y limitar el overlay a metadatos editables, sin reemplazar el loader real. +- Riesgo: tocar shared upstream code. Mitigación: rutas, schemas y helpers en `src/kilocode/`; shared solo para registro si es inevitable. +- Riesgo: SSE con auth en navegador. Mitigación: usar `fetch` streaming con headers o caer a refetch por focus/visibility sin bloquear la funcionalidad principal. diff --git a/.kilo/plans/webconfig.md b/.kilo/plans/webconfig.md new file mode 100644 index 0000000000..a5d26ff6e2 --- /dev/null +++ b/.kilo/plans/webconfig.md @@ -0,0 +1,741 @@ +# Web Config Dashboard Plan + +## Goal + +Build a daemon-backed Kilo configuration dashboard that becomes the advanced replacement for JSON-based CLI configuration and eventually supersedes the VS Code settings panel. + +The core product shape: + +- `kilo` starts or attaches to a local background daemon. +- The daemon owns the HTTP/SSE API and serves a local web dashboard. +- The dashboard configures global, project, and profile-scoped Kilo behavior. +- The UI uses `@kilocode/kilo-ui` with a standalone SolidJS app. +- Existing config files remain the canonical storage format, but users mostly interact visually. + +## Key Findings + +Current CLI config already supports most primitives, but not the product model: + +- Main CLI config is in `packages/opencode/src/config/config.ts`. +- Project/global updates already exist through `Config.update()` and `Config.updateGlobal()`. +- Existing APIs expose resolved config, but not enough provenance/source information for a serious UI. +- TUI config is separate in `tui.json[c]`, not `kilo.json[c]`. +- Existing VS Code settings UI is SolidJS and uses `kilo-ui`, but it is tightly coupled to VS Code webview messaging and VS Code settings storage. +- Current default TUI and `kilo run` do not use a persistent server; they use in-process or worker-local server transports. +- `kilo serve` already exposes HTTP/SSE and can serve static UI assets, but it is foreground and unauthenticated unless `KILO_SERVER_PASSWORD` is set. + +## Implementation Checklist + +This plan will be implemented one checkpoint at a time. Each checkpoint should leave the repo in a manually testable state before moving to the next one. + +Manual tests use port `4097` because `4096` is often occupied by the VS Code extension's background `kilo serve --port 0` process. If `4097` is also occupied, use another free localhost port and replace the port in the commands below. + +### Checkpoint 0: Existing Server Probe + +Status: Complete before this plan started. + +- [x] Confirm the server already exposes `GET /global/health` for daemon attach/status probing. +- [x] Treat this as the base health contract for future daemon status checks. + +Manual test: + +```bash +bun run --conditions=browser ./src/index.ts serve --hostname 127.0.0.1 --port 4097 +curl http://127.0.0.1:4097/global/health +``` + +Expected result: + +```json +{"healthy":true,"version":""} +``` + +### Checkpoint 1: TUI Config HTTP API + +Status: Complete. + +- [x] Add Kilo-owned helpers to read effective TUI config for a requested instance directory. +- [x] Add Kilo-owned helpers to patch global or project `tui.json[c]` with sparse updates. +- [x] Add `GET /tui/config` for the effective TUI config. +- [x] Add `PATCH /tui/config?scope=project|global` for TUI config writes. +- [x] Add focused tests for reading and updating project TUI config. +- [x] Regenerate SDK output after adding the server endpoint. + +Manual test: + +```bash +bun run --conditions=browser ./src/index.ts serve --hostname 127.0.0.1 --port 4097 +curl -H "x-kilo-directory: $PWD" http://127.0.0.1:4097/tui/config +curl -X PATCH -H "content-type: application/json" -H "x-kilo-directory: $PWD" "http://127.0.0.1:4097/tui/config?scope=project" --data '{"theme":"dracula"}' +``` + +Expected result: + +- The first request returns effective TUI settings. +- The patch request creates or updates `.kilo/tui.json` for the project. +- A follow-up `GET /tui/config` includes `"theme":"dracula"`. + +### Checkpoint 2: Config Source Inventory API + +Status: Complete. + +- [x] Add read-only config source inventory for global, project, config-dir, env, managed, and cloud sources. +- [x] Expose source path, scope, existence, editability, and precedence metadata. +- [x] Do not expose secrets from provider options or auth storage. +- [x] Add tests for source ordering and project directory behavior. + +Manual test: + +```bash +curl -H "x-kilo-directory: $PWD" http://127.0.0.1:4097/config/sources +``` + +Expected result: + +- The response lists discovered config files/directories in precedence order. +- Read-only or managed sources are marked non-editable. + +### Checkpoint 3: Profile Storage API + +Status: Complete. + +- [x] Define profile metadata schemas. +- [x] Add global profile list/create/update/delete endpoints. +- [x] Add project profile list/create/update/delete endpoints. +- [x] Add active profile selection metadata without changing runtime config precedence yet. +- [x] Add tests for profile file creation and validation. + +Manual test: + +```bash +TMPDIR=$(mktemp -d) +curl -H "x-kilo-directory: $TMPDIR" http://127.0.0.1:4097/profiles +curl -X POST -H "content-type: application/json" -H "x-kilo-directory: $TMPDIR" http://127.0.0.1:4097/profiles --data '{"scope":"project","id":"work","name":"Work"}' +curl -X POST -H "x-kilo-directory: $TMPDIR" "http://127.0.0.1:4097/profiles/work/activate?scope=project" +cat "$TMPDIR/.kilo/profiles/index.jsonc" +``` + +Expected result: + +- A new project profile appears in the profile list. +- Profile metadata is persisted under `$TMPDIR/.kilo/profiles/index.jsonc`. +- Empty `$TMPDIR/.kilo/profiles/work/kilo.jsonc` and `tui.jsonc` files are created. + +### Checkpoint 4: Profile Overlay Runtime + +Status: Complete. + +- [x] Load active global profile overlays after global base config. +- [x] Load active project profile overlays after project base config. +- [x] Keep env, cloud, managed, and runtime overlays at their current special precedence. +- [x] Add tests for effective config with global and project profile overlays. + +Manual test: + +```bash +curl -H "x-kilo-directory: $PWD" "http://127.0.0.1:4097/config/effective?profile=work" +``` + +Expected result: + +- Effective config includes profile values in the documented precedence order. +- Base project config still overrides global base config. + +### Checkpoint 5: Dashboard Scaffold + +Status: Complete. + +- [x] Create `packages/kilo-config-ui` as a SolidJS/Vite app. +- [x] Reuse `@kilocode/kilo-ui` and the standalone `kilo` theme. +- [x] Add SDK/HTTP client bootstrap against the local daemon/server. +- [x] Add dashboard shell, diagnostics, scope selector, and read-only config summary. + +Manual test: + +```bash +# Terminal 1 +cd packages/opencode +bun run --conditions=browser ./src/index.ts serve --hostname 127.0.0.1 --port 4097 + +# Terminal 2 +cd packages/kilo-config-ui +bun run dev +open "http://127.0.0.1:3017?server=http://127.0.0.1:4097&directory=$PWD" +``` + +Expected result: + +- The dashboard loads in a browser. +- It can show server health and effective config for the selected directory. + +### Checkpoint 6: Providers And Models UI + +Status: Complete. + +- [x] Add provider list and connection state UI. +- [x] Add provider enable/disable controls. +- [x] Add model browser with search and filters. +- [x] Add default model and small model controls. +- [x] Store favorites/groups/tags in profile metadata. + +Manual test: + +```bash +cd packages/kilo-config-ui +bun run dev +open "http://127.0.0.1:3017?server=http://127.0.0.1:4097&directory=$PWD" +``` + +Expected result: + +- Provider and model changes are visible in config files or profile metadata. +- The CLI model picker observes default model changes after reload. + +### Checkpoint 7: Advanced Config UI + +Status: Complete. + +- [x] Add MCP server editor. +- [x] Add built-in and MCP tool inventory. +- [x] Add visual permission rule builder that preserves rule order. +- [x] Add TUI keybind editor with duplicate detection. +- [x] Add formatter and LSP configuration pages. + +Manual test: + +```bash +cd packages/kilo-config-ui +bun run dev +open "http://127.0.0.1:3017?server=http://127.0.0.1:4097&directory=$PWD" +``` + +Expected result: + +- Edits produce sparse config patches. +- Existing JSONC comments and unrelated fields are preserved where practical. + +### Checkpoint 8: Agent Builder + +Status: Complete. + +- [x] Add primary/subagent editor. +- [x] Save agents as canonical `agent/*.md` files. +- [x] Add prompt snippet insertion. +- [x] Compose model, provider, tools, MCP tools, and permissions visually. +- [x] Add generated markdown preview and validation. + +Manual test: + +```bash +# Terminal 1 +cd packages/opencode +bun run --conditions=browser ./src/index.ts serve --hostname 127.0.0.1 --port 4097 + +# Terminal 2 +cd packages/kilo-config-ui +bun run dev +open "http://127.0.0.1:3017?server=http://127.0.0.1:4097&directory=$PWD" +``` + +Expected result: + +- A created agent appears in the CLI agent selector. +- A created subagent appears as a Task-tool selectable subagent. + +### Checkpoint 9: Daemon Manager + +Status: Complete. + +- [x] Add daemon state file and lock handling. +- [x] Add authenticated daemon startup with random local token. +- [x] Add daemon health/version probing. +- [x] Add `kilo daemon status/start/stop/restart` commands. +- [x] Keep daemon usage opt-in while dashboard and APIs stabilize. + +Manual test: + +```bash +kilo daemon start +kilo daemon status +kilo daemon stop +``` + +Expected result: + +- Daemon starts in the background, reports health/version/port, and stops cleanly. + +### Checkpoint 10: Default Daemon And VS Code Replacement + +Status: CLI default complete; VS Code replacement deferred. + +- [x] Add TUI/run attach mode against the daemon. +- [x] Add fallback for daemon startup or attach failures. +- [x] Add `KILO_NO_DAEMON=1` escape hatch. +- [ ] Make VS Code open or embed the new dashboard. +- [ ] Deprecate duplicated settings UI after parity is reached. + +Manual test: + +```bash +kilo +kilo run "say hello" +``` + +Expected result: + +- CLI commands attach to the daemon when enabled. +- Users can still bypass daemon mode when needed. + +## Architecture + +### Daemon Layer + +Add a Kilo-owned daemon manager under something like: + +`packages/opencode/src/kilocode/daemon/` + +Responsibilities: + +- Start daemon on first CLI invocation. +- Reuse existing daemon if healthy. +- Store daemon metadata under Kilo global state/config, for example: +- `pid` +- `port` +- `hostname` +- `auth token` +- `version` +- `startedAt` +- `log path` +- Use a lock file to avoid concurrent CLI calls spawning multiple daemons. +- Detect stale daemons by pid, health endpoint, and version mismatch. +- Restart on upgrade or corrupt state. +- Keep `kilo serve` as explicit foreground mode for servers/headless use. + +New CLI commands: + +- `kilo daemon status` +- `kilo daemon start` +- `kilo daemon stop` +- `kilo daemon restart` +- `kilo dashboard` or `kilo config ui` + +Security requirements: + +- Default daemon binds to `127.0.0.1`. +- Generate a random local token/password on first start. +- Never expose unauthenticated config/session/tool APIs. +- Prefer a one-time browser launch token that sets an `HttpOnly; SameSite=Strict` cookie, then redirects to a clean dashboard URL. +- Require explicit user config for external host binding. + +### Server/API Layer + +Use the existing server as the foundation, but add missing configuration APIs. + +Existing useful APIs: + +- `GET /config` +- `PATCH /config` +- `GET /global/config` +- `PATCH /global/config` +- provider list/auth endpoints +- config warning endpoints + +Needed new APIs: + +- `GET /global/health` +- `POST /global/shutdown` or daemon-local shutdown equivalent +- `GET /config/sources?directory=...` +- `GET /config/effective?directory=...&profile=...` +- `GET/PATCH /tui/config` +- `GET /profiles` +- `POST /profiles` +- `PATCH /profiles/:id` +- `DELETE /profiles/:id` +- `POST /profiles/:id/activate` +- `GET /tools` +- `GET /mcp/tools` +- `POST /providers/custom/models` +- `GET /config/schema` or equivalent metadata for UI form generation + +Important: the dashboard should write sparse patches, not the fully resolved config. Resolved config contains inherited values and internal data that should not be written back wholesale. + +### Profile System + +Recommended model: profiles are named config overlays, not a giant new top-level object in `kilo.json`. + +Proposed storage: + +- Global profiles: +- `~/.config/kilo/profiles//kilo.jsonc` +- `~/.config/kilo/profiles//tui.jsonc` +- `~/.config/kilo/profiles//agent/*.md` +- `~/.config/kilo/profiles//command/*.md` +- Project profiles: +- `.kilo/profiles//kilo.jsonc` +- `.kilo/profiles//tui.jsonc` +- `.kilo/profiles//agent/*.md` +- `.kilo/profiles//command/*.md` +- Profile metadata: +- `~/.config/kilo/profiles/index.jsonc` +- `.kilo/profiles/index.jsonc` + +Profile metadata can store UI-only fields: + +- display name +- description +- color/icon +- tags +- model favorites +- model groups +- last active profile +- profile templates +- dashboard ordering + +Proposed precedence: + +- global base config +- active global profile +- project base config +- active project profile +- env/content/cloud/managed overlays keep their current special precedence + +This gives users a global “Work” profile, a global “Personal” profile, and optional project-specific variants without replacing existing config semantics. + +### New Dashboard Package + +Create a new workspace package: + +`packages/kilo-config-ui` + +Use: + +- SolidJS +- Vite +- `@kilocode/kilo-ui` +- `@kilocode/sdk` +- `ThemeProvider defaultTheme="kilo"` +- Storybook/visual tests later using existing Kilo UI patterns + +Do not reuse the VS Code `KiloProvider.ts` bridge. Instead, build a direct SDK/HTTP data layer. + +Recommended app layout: + +- Dashboard overview +- Profiles +- Providers +- Models +- Agents +- Tools +- MCP +- TUI +- Formatters/LSP +- Permissions +- Prompt snippets/commands +- Import/export +- Diagnostics/warnings + +### Settings UI Reuse Strategy + +Reuse from VS Code settings where practical: + +- provider catalog logic +- provider visibility logic +- custom provider validation +- custom provider model card/form ideas +- model selector concepts +- `SettingsRow`-style layout patterns + +Do not directly reuse: + +- VS Code message transport +- VS Code settings persistence +- VS Code-specific CSS variable assumptions +- `KiloProvider.ts` +- sidebar/editor webview shell + +Long-term, the VS Code extension should either: + +- open the daemon dashboard, or +- embed the same `packages/kilo-config-ui` screens with a VS Code adapter. + +The dashboard should become the source of truth to avoid maintaining two settings products. + +## Feature Plan + +### Providers + +Capabilities: + +- list connected, available, disabled, env-sourced, and custom providers +- configure API keys without committing secrets to project config +- support OAuth providers +- create OpenAI-compatible custom providers +- fetch models from custom provider endpoints +- enable/disable providers +- show source/provenance: env, auth store, global config, project config, profile, managed config + +Important rule: secrets should prefer auth storage or env vars, not project files. + +### Models + +Capabilities: + +- browse all available models +- search/filter by provider, capability, context size, cost, tags +- set default model and small model +- set per-agent model +- mark favorites +- create model groups +- tag models +- hide deprecated/unwanted models +- preview final `provider/model` IDs + +Favorites/groups/tags should probably live in profile metadata, not `Config.Info`, unless runtime behavior needs them. + +### Tools + +Capabilities: + +- list built-in tools +- show descriptions +- show current permission status +- show whether tool is available to current agent/profile +- later: show MCP tools alongside built-in tools + +### MCP + +Capabilities: + +- add local MCP server +- add remote MCP server +- configure env vars, headers, OAuth, timeout +- enable/disable servers +- inspect tools exposed by each MCP +- configure MCP tool permissions using existing permission keys like `server_tool` + +### TUI Configuration + +Capabilities: + +- edit `tui.json[c]` +- theme selector +- keybind editor +- conflict detection for duplicate keybinds +- scroll speed/acceleration +- diff style +- mouse mode +- plugin enablement + +Important: do not write TUI settings into `kilo.json`. + +### Formatters/LSP + +Capabilities: + +- configure formatter commands +- map formatters to extensions +- enable/disable formatters +- configure LSP commands +- map LSP servers to extensions +- validate command arrays visually + +### Permissions + +Capabilities: + +- visual permission rule builder +- preserve object/rule order +- support scalar and pattern forms +- support `allow`, `ask`, `deny`, `null` +- show final effective permission by scope +- warn when broad rules override specific rules +- support agent-level permissions + +Important: permission ordering matters. The UI must preserve order and explain precedence. + +### Agent Builder + +This is the differentiating feature. + +Capabilities: + +- create primary agents and subagents +- configure: +- name +- description +- mode: primary/subagent/all +- model +- provider/model variant +- temperature/top-p/options +- max steps +- color +- permissions +- enabled tools +- MCP tools +- prompt +- prompt snippets +- allowed subagents +- save as canonical `agent/*.md` where possible +- support import/export as markdown agent files +- preview generated frontmatter/body +- validate before saving + +Later advanced workflow feature: + +- visual graph of agents/subagents +- define handoff rules +- define which subagent can call which subagent +- define shared prompt snippets +- define per-agent MCP/tool permissions +- package/share an agent workflow as a profile template + +## Implementation Phases + +### 1. Spec And Contracts + +Deliverables: + +- profile storage spec +- daemon state file spec +- dashboard route names +- config provenance response shape +- auth/security design +- migration behavior +- UI information architecture + +No risky code yet. + +### 2. Config API Foundation + +Deliverables: + +- config source/provenance API +- TUI config read/write API +- profile read/write/activation APIs +- validation API for pending config patches +- tests for global/project/profile precedence + +Most logic should live under `packages/opencode/src/kilocode/`. + +### 3. Daemon Foundation + +Deliverables: + +- daemon manager +- lock/state handling +- authenticated local daemon startup +- health/version checks +- status/start/stop commands +- optional attach mode for CLI clients + +Rollout should be opt-in or experimental first, not forced as default immediately. + +### 4. Dashboard Scaffold + +Deliverables: + +- `packages/kilo-config-ui` +- Solid/Vite app +- Kilo UI theme +- SDK client +- auth bootstrap +- dashboard shell +- config warning display +- global/project/profile scope selector + +### 5. Providers And Models + +Deliverables: + +- provider cards +- auth/connect/disconnect +- custom provider flow +- model browser +- default/small model controls +- favorites/groups/tags metadata + +### 6. Profiles + +Deliverables: + +- profile list/create/duplicate/delete +- global/project activation +- effective config preview +- diff against base/global/project +- import/export profile bundle + +### 7. Advanced Config Pages + +Deliverables: + +- MCP page +- tools page +- permissions editor +- TUI keybind editor +- formatters/LSP page + +### 8. Agent Builder + +Deliverables: + +- visual agent editor +- subagent mode support +- prompt editor/snippet insertion +- permission/tool/MCP composition +- markdown agent import/export +- validation and preview + +### 9. VS Code Replacement Path + +Deliverables: + +- extension opens or embeds new dashboard +- old settings panel becomes compatibility/deprecated path +- shared config UI components replace duplicated VS Code settings logic +- VS Code-only settings either move into CLI config or remain in a small VS Code-specific section + +### 10. Default Daemon Rollout + +Deliverables: + +- migrate TUI/run to attach to daemon +- keep fallback to in-process/worker mode +- detect daemon failures cleanly +- document escape hatch like `KILO_NO_DAEMON=1` +- make daemon default only after stability + +## Merge-Minimizing Strategy + +Because `packages/opencode` is shared with upstream OpenCode: + +- Put daemon/profile/dashboard-specific logic in Kilo-owned paths like `src/kilocode/daemon`, `src/kilocode/profile`, `src/kilocode/config-ui`. +- Keep shared file changes minimal: +- CLI command registration +- server route hook +- config loader hook for profile overlays +- static UI route registration +- Mark unavoidable shared-file additions with narrow `kilocode_change` comments. +- Put tests under `packages/opencode/test/kilocode/`. +- Run `bun run script/check-opencode-annotations.ts` after implementation work touches shared opencode files. + +## Main Risks + +- Daemon security is the highest-risk area; config and session APIs cannot be exposed unauthenticated. +- Profile precedence can become confusing if not defined before coding. +- Writing resolved config back to files would corrupt user intent; only sparse patches should be written. +- TUI config is separate from main config and needs dedicated API/storage. +- VS Code settings currently mix CLI config and VS Code-local settings; not everything can move 1:1. +- Agent builder can become too broad; it should start by generating existing agent markdown files before inventing a new workflow runtime. +- Default daemon behavior is a breaking UX shift; it should be phased in behind explicit commands/flags first. + +## Recommended First Iteration Scope + +Start with the foundation, not the full UI: + +- Define profile storage and precedence. +- Define daemon auth/state/health lifecycle. +- Add config provenance and TUI config APIs. +- Scaffold the dashboard with overview, config warnings, scope selector, and provider/model read-only views. +- Only then add write flows and agent builder. + +Implementation is proceeding checkpoint by checkpoint. Continue only after the current checkpoint is manually validated or revised. diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 30c4d882b9..f126a43c0f 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -18,4 +18,5 @@ "github-triage": false, "github-pr-search": false, }, + "disabled_providers": [], } diff --git a/bun.lock b/bun.lock index 76cd6a758f..290194e192 100644 --- a/bun.lock +++ b/bun.lock @@ -66,6 +66,25 @@ "@types/semver": "catalog:", }, }, + "packages/kilo-config-ui": { + "name": "@kilocode/kilo-config-ui", + "version": "7.2.52", + "dependencies": { + "@kilocode/kilo-ui": "workspace:*", + "@kilocode/sdk": "workspace:*", + "@opencode-ai/ui": "workspace:*", + "@solidjs/router": "catalog:", + "solid-js": "catalog:", + }, + "devDependencies": { + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-solid": "catalog:", + }, + }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", "version": "7.2.52", @@ -1278,6 +1297,8 @@ "@kilocode/cli": ["@kilocode/cli@workspace:packages/opencode"], + "@kilocode/kilo-config-ui": ["@kilocode/kilo-config-ui@workspace:packages/kilo-config-ui"], + "@kilocode/kilo-docs": ["@kilocode/kilo-docs@workspace:packages/kilo-docs"], "@kilocode/kilo-gateway": ["@kilocode/kilo-gateway@workspace:packages/kilo-gateway"], diff --git a/packages/core/src/kilocode/global.ts b/packages/core/src/kilocode/global.ts index eb5b5d06b5..b57d06f7ae 100644 --- a/packages/core/src/kilocode/global.ts +++ b/packages/core/src/kilocode/global.ts @@ -12,7 +12,10 @@ import fs from "fs/promises" */ export async function ensureRealDir(p: string) { await fs.mkdir(p, { recursive: true }) - const ok = await fs.stat(p).then(() => true).catch(() => false) + const ok = await fs + .stat(p) + .then(() => true) + .catch(() => false) if (!ok) { await fs.rm(p, { force: true }) await fs.mkdir(p, { recursive: true }) diff --git a/packages/kilo-config-ui/index.html b/packages/kilo-config-ui/index.html new file mode 100644 index 0000000000..8986736ee8 --- /dev/null +++ b/packages/kilo-config-ui/index.html @@ -0,0 +1,12 @@ + + + + + + Kilo Config Dashboard + + +
+ + + diff --git a/packages/kilo-config-ui/package.json b/packages/kilo-config-ui/package.json new file mode 100644 index 0000000000..13a867363d --- /dev/null +++ b/packages/kilo-config-ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@kilocode/kilo-config-ui", + "version": "7.2.52", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 3017", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 3018", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@kilocode/kilo-ui": "workspace:*", + "@kilocode/sdk": "workspace:*", + "@solidjs/router": "catalog:", + "@opencode-ai/ui": "workspace:*", + "solid-js": "catalog:" + }, + "devDependencies": { + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-solid": "catalog:" + } +} diff --git a/packages/kilo-config-ui/src/App.tsx b/packages/kilo-config-ui/src/App.tsx new file mode 100644 index 0000000000..0e7376e754 --- /dev/null +++ b/packages/kilo-config-ui/src/App.tsx @@ -0,0 +1,17 @@ +import { createMemo } from "solid-js" +import type { JSX } from "solid-js" +import { useLocation } from "@solidjs/router" +import { ThemeProvider } from "@kilocode/kilo-ui/theme" +import { ConsoleLayout } from "./layouts/ConsoleLayout" +import { path as route } from "./shared/navigation" + +export default function App(props: { children?: JSX.Element }) { + const loc = useLocation() + const current = createMemo(() => route(loc.pathname)) + + return ( + + {props.children} + + ) +} diff --git a/packages/kilo-config-ui/src/client.ts b/packages/kilo-config-ui/src/client.ts new file mode 100644 index 0000000000..8ff7773b6b --- /dev/null +++ b/packages/kilo-config-ui/src/client.ts @@ -0,0 +1,272 @@ +import { createKiloClient, type Config as EffectiveConfig } from "@kilocode/sdk/v2/client" +import type { + AgentBuilderPreviewResponse, + AgentBuilderSaveResponse, + Auth, + AppAgentsResponse, + ConfigOverlayResponse, + ConfigModelStateResponse, + ConfigRulesResponse, + ConfigSourcesResponse, + FormatterStatusResponse, + GlobalHealthResponse, + LspStatusResponse, + McpStatusResponse, + Project as KiloProject, + ProviderAuthAuthorization, + ProviderAuthResponse, + ProviderListResponse, + ToolIdsResponse, + TuiConfigGetResponse, +} from "@kilocode/sdk/v2/client" + +export type Scope = "global" | "project" + +export type Query = { + url: string + dir: string + scope: Scope +} + +export type ProjectQuery = Pick + +export type ProjectItem = KiloProject + +export type Snapshot = { + health: GlobalHealthResponse + effective: EffectiveConfig + overlay: ConfigOverlayResponse + sources: ConfigSourcesResponse + rules?: ConfigRulesResponse + modelState: ConfigModelStateResponse + providers: ProviderListResponse + authMethods: ProviderAuthResponse + tui: TuiConfigGetResponse + tools: ToolIdsResponse + mcp: McpStatusResponse + lsp: LspStatusResponse + formatter: FormatterStatusResponse + agents: AppAgentsResponse +} + +export type ConfigPatch = Partial + +export type ConfigUnset = string[][] +export type ModelRef = ConfigModelStateResponse["favorite"][number] + +export type TuiPatch = Partial + +export type AgentPayload = { + scope: Scope + id: string + description?: string + mode: "primary" | "subagent" | "all" + model?: string + color?: string + steps?: number + tools?: string[] + permission?: Record + prompt: string +} + +type Result = { + data: T | undefined + error?: unknown +} + +const ports = Array.from({ length: 20 }, (_, index) => 4097 + index) +const key = "kilo.config.server" +const auth = `Basic ${btoa("kilo:kilo")}` + +const fetcher = window.fetch.bind(window) as typeof fetch + +function client(input: ProjectQuery) { + return createKiloClient({ + baseUrl: input.url, + directory: value(input.dir), + headers: { + Authorization: auth, + }, + fetch: fetcher, + }) +} + +function value(input: string) { + const trimmed = input.trim() + if (trimmed) return trimmed + return undefined +} + +function message(input: unknown) { + if (input instanceof Error) return input.message + if (typeof input === "string") return input + if (input === undefined || input === null) return "Unknown error" + return JSON.stringify(input) +} + +function demand(label: string, result: Result) { + if (result.error) throw new Error(`${label}: ${message(result.error)}`) + if (result.data === undefined) throw new Error(`${label}: empty response`) + return result.data +} + +async function probe(url: string) { + const ctl = new AbortController() + const timer = window.setTimeout(() => ctl.abort(), 400) + return await fetcher(`${url}/global/health`, { headers: { Authorization: auth }, signal: ctl.signal }) + .then((res) => (res.ok ? url : undefined)) + .catch(() => undefined) + .finally(() => window.clearTimeout(timer)) +} + +export function loadCached() { + return window.localStorage.getItem(key) ?? "" +} + +export function saveCached(url: string) { + window.localStorage.setItem(key, url) +} + +export function forgetCached() { + window.localStorage.removeItem(key) +} + +export async function healthy(url: string) { + return (await probe(url)) !== undefined +} + +export async function discover() { + const urls = ports.flatMap((port) => [`http://127.0.0.1:${port}`, `http://localhost:${port}`]) + const hit = await Promise.any( + urls.map((url) => + probe(url).then((value) => { + if (value) return value + throw new Error(`${url} unavailable`) + }), + ), + ).catch(() => undefined) + return hit +} + +export async function load(input: Query): Promise { + const sdk = client(input) + const [health, overlay, modelState, providers, authMethods, tui, tools, mcp, lsp, formatter, agents, rules] = + await Promise.all([ + sdk.global.health(), + sdk.config.overlay({ scope: input.scope }), + sdk.config.modelState(), + sdk.provider.list(), + sdk.provider.auth(), + sdk.tui.config.get(), + sdk.tool.ids(), + sdk.mcp.status(), + sdk.lsp.status(), + sdk.formatter.status(), + sdk.app.agents(), + input.scope === "project" ? sdk.config.rules() : Promise.resolve({ data: undefined }), + ]) + const resolved = demand("Config overlay", overlay) + + return { + health: demand("Health", health), + effective: resolved.effective, + overlay: resolved, + sources: { sources: resolved.sources }, + rules: input.scope === "project" ? demand("Rules", rules) : undefined, + modelState: demand("Model state", modelState), + providers: demand("Providers", providers), + authMethods: demand("Provider auth methods", authMethods), + tui: demand("TUI config", tui), + tools: demand("Tools", tools), + mcp: demand("MCP status", mcp), + lsp: demand("LSP status", lsp), + formatter: demand("Formatter status", formatter), + agents: demand("Agents", agents), + } +} + +export async function loadProjects(input: ProjectQuery): Promise { + const sdk = client(input) + const dir = value(input.dir) + const result = await sdk.project.list(dir ? { directory: dir } : undefined) + return demand("Projects", result) +} + +export async function saveConfig(input: Query, patch: Partial) { + const sdk = client(input) + const result = await sdk.config.overlayUpdate({ scope: input.scope, set: patch }) + return demand("Update config", result) +} + +export async function unsetConfig(input: Query, unset: ConfigUnset) { + const sdk = client(input) + const result = await sdk.config.overlayUpdate({ scope: input.scope, unset }) + return demand("Update config", result) +} + +export async function saveRules(input: Query, content: string) { + const sdk = client(input) + const result = await sdk.config.rulesUpdate({ content }) + return demand("Update rules", result) +} + +export async function saveModelState(input: Query, favorite: ModelRef[]) { + const sdk = client(input) + const result = await sdk.config.modelStateUpdate({ favorite }) + return demand("Update model state", result) +} + +export async function connectProvider(input: Query, id: string, key: string, metadata?: Record) { + const sdk = client(input) + const auth: Auth = metadata ? { type: "api", key, metadata } : { type: "api", key } + const result = await sdk.auth.set({ providerID: id, auth }) + demand("Connect provider", result) + await sdk.global.dispose() +} + +export async function authorizeProvider( + input: Query, + id: string, + method: number, + inputs?: Record, +): Promise { + const sdk = client(input) + const result = await sdk.provider.oauth.authorize({ providerID: id, method, inputs }) + return demand("Authorize provider", result) +} + +export async function completeProvider(input: Query, id: string, method: number, code?: string) { + const sdk = client(input) + const result = await sdk.provider.oauth.callback({ providerID: id, method, code }) + demand("Complete provider authorization", result) + await sdk.global.dispose() +} + +export async function saveTui(input: Query, patch: TuiPatch) { + const sdk = client(input) + const result = await sdk.tui.config.update({ scope: input.scope, ...patch }) + return demand("Update TUI config", result) +} + +export async function previewAgent(input: Query, payload: AgentPayload): Promise { + const sdk = client(input) + const result = await sdk.agentBuilder.preview(payload) + return demand("Preview agent", result) +} + +export async function saveAgent(input: Query, payload: AgentPayload): Promise { + const sdk = client(input) + const result = await sdk.agentBuilder.save({ + path_id: payload.id, + scope: payload.scope, + description: payload.description, + mode: payload.mode, + model: payload.model, + color: payload.color, + steps: payload.steps, + tools: payload.tools, + permission: payload.permission, + prompt: payload.prompt, + }) + return demand("Save agent", result) +} diff --git a/packages/kilo-config-ui/src/components/ConfirmDialog.tsx b/packages/kilo-config-ui/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000000..f605b786a9 --- /dev/null +++ b/packages/kilo-config-ui/src/components/ConfirmDialog.tsx @@ -0,0 +1,42 @@ +import { Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Icon } from "@kilocode/kilo-ui/icon" + +type Props = { + open: boolean + title: string + message?: string + confirm?: string + cancel?: string + busy?: boolean + onCancel: () => void + onConfirm: () => void +} + +export function ConfirmDialog(props: Props) { + return ( + +
+
+
+ +
+

{props.title}

+ {(text) =>

{text()}

}
+
+
+
+ + +
+
+
+
+ ) +} diff --git a/packages/kilo-config-ui/src/components/app-header/AppHeader.tsx b/packages/kilo-config-ui/src/components/app-header/AppHeader.tsx new file mode 100644 index 0000000000..36f50ba7c5 --- /dev/null +++ b/packages/kilo-config-ui/src/components/app-header/AppHeader.tsx @@ -0,0 +1,26 @@ +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Mark } from "@kilocode/kilo-ui/logo" + +export function AppHeader() { + return ( +
+ + + + + Kilo Console + + + + + +
+ ) +} diff --git a/packages/kilo-config-ui/src/components/app-sidebar/AppSidebar.tsx b/packages/kilo-config-ui/src/components/app-sidebar/AppSidebar.tsx new file mode 100644 index 0000000000..7dcd78aa11 --- /dev/null +++ b/packages/kilo-config-ui/src/components/app-sidebar/AppSidebar.tsx @@ -0,0 +1,54 @@ +import { A, useLocation } from "@solidjs/router" +import { For } from "solid-js" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { projects, type Path } from "../../shared/navigation" + +type Props = { + path: Path +} + +export function AppSidebar(props: Props) { + const loc = useLocation() + const search = () => { + const params = new URLSearchParams(loc.search) + params.delete("directory") + const query = params.toString() + return query ? `?${query}` : "" + } + const settings = () => { + const suffix = search() + return `/settings${suffix}` + } + + return ( + + ) +} diff --git a/packages/kilo-config-ui/src/context/ConfigProvider.tsx b/packages/kilo-config-ui/src/context/ConfigProvider.tsx new file mode 100644 index 0000000000..8e20de3ddc --- /dev/null +++ b/packages/kilo-config-ui/src/context/ConfigProvider.tsx @@ -0,0 +1,169 @@ +import { createEffect, createMemo, createResource, createSignal } from "solid-js" +import type { JSX } from "solid-js" +import { + discover, + forgetCached, + healthy, + load, + loadCached, + loadProjects, + saveCached, + saveConfig, + saveRules, + saveTui, + unsetConfig, + type ConfigPatch, + type ConfigUnset, + type Query, + type Scope, + type TuiPatch, +} from "../client" +import { ConfigContext, type Task } from "./config" +import { clean, errMsg } from "../shared/utils" +import { useLocation, useParams } from "@solidjs/router" + +const params = new URLSearchParams(window.location.search) +const ui = new Set(["3017", "3018"]) + +function shouldDiscover(input = params) { + if (input.get("server")) return false + return ui.has(window.location.port) +} + +function base(input = params) { + const param = input.get("server") + if (param) return param + const cached = shouldDiscover(input) ? loadCached() : "" + if (cached) return cached + if (shouldDiscover(input)) return "" + return window.location.origin +} + +export function ConfigProvider(props: { children?: JSX.Element }) { + const loc = useLocation() + const params = useParams() + const search = createMemo(() => new URLSearchParams(loc.search)) + const discoverable = () => shouldDiscover(search()) + const fallback = () => base(search()) + const [url, setUrl] = createSignal(fallback()) + const scope = createMemo(() => (loc.pathname.startsWith("/projects/") ? "project" : "global")) + const [saving, setSaving] = createSignal() + const [failure, setFailure] = createSignal() + const needs = createMemo(() => scope() === "project") + const projects = createMemo(() => { + const target = clean(url()) || fallback() + if (!target || !needs()) return undefined + return { url: target, dir: "" } + }) + const [items] = createResource(projects, loadProjects) + const resolved = createMemo(() => { + if (!needs()) return "" + return items()?.find((item) => item.id === params.project)?.worktree ?? "" + }) + + const query = createMemo(() => { + const target = clean(url()) || fallback() + if (!target) return undefined + if (needs() && !resolved()) return undefined + return { url: target, dir: resolved(), scope: scope() } + }) + const [data, { refetch }] = createResource(query, load) + + function target() { + const item = query() + if (!item) throw new Error("Kilo server discovery is still running") + return item + } + + createEffect(() => { + if (!needs() || items.loading || items.error || !items()) return + if (!resolved()) setFailure(`Project not found: ${params.project}`) + }) + + createEffect(() => { + const next = search().get("server") + if (next && next !== url()) setUrl(next) + }) + + createEffect(() => { + if (!discoverable()) return + const cached = loadCached() + void Promise.resolve(cached ? healthy(cached) : false) + .then((ok) => { + if (ok) return cached + forgetCached() + return discover() + }) + .then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + }) + + createEffect(() => { + const snap = data() + const item = query() + if (!snap || !item || !discoverable()) return + saveCached(item.url) + }) + + createEffect(() => { + if (!data.error || !discoverable()) return + const cached = loadCached() + if (!cached || cached !== url()) return + forgetCached() + setUrl("") + void discover().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + }) + + function fail(message: string) { + setFailure(message) + } + + function run(label: string, job: () => Promise, task?: Task) { + setSaving(label) + setFailure(undefined) + void job() + .then(() => (task?.refetch === false ? undefined : refetch())) + .then(() => undefined) + .catch((err: unknown) => setFailure(errMsg(err))) + .finally(() => setSaving(undefined)) + } + + function save(patch: Partial) { + run("Saving config", () => saveConfig(target(), patch)) + } + + function unset(paths: ConfigUnset) { + run("Saving config", () => unsetConfig(target(), paths)) + } + + function rules(content: string) { + run("Saving rules", () => saveRules(target(), content)) + } + + function tui(patch: TuiPatch) { + run("Saving TUI config", () => saveTui(target(), patch)) + } + + const ctx = { + data, + query, + saving, + failure, + target, + fail, + run, + save, + unset, + rules, + tui, + } + + return {props.children} +} diff --git a/packages/kilo-config-ui/src/context/config.tsx b/packages/kilo-config-ui/src/context/config.tsx new file mode 100644 index 0000000000..cf25af56a7 --- /dev/null +++ b/packages/kilo-config-ui/src/context/config.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext } from "solid-js" +import type { Accessor, Resource } from "solid-js" +import type { Query, Snapshot, ConfigPatch, ConfigUnset, TuiPatch } from "../client" + +export type Task = { + refetch?: boolean +} + +export type Ctx = { + data: Resource + query: Accessor + saving: Accessor + failure: Accessor + + target: () => Query + fail: (message: string) => void + run: (label: string, job: () => Promise, task?: Task) => void + save: (patch: Partial) => void + unset: (paths: ConfigUnset) => void + rules: (content: string) => void + tui: (patch: TuiPatch) => void +} + +export const ConfigContext = createContext() + +export function useConfig() { + const ctx = useContext(ConfigContext) + if (!ctx) throw new Error("useConfig must be used within ConfigLayout") + return ctx +} diff --git a/packages/kilo-config-ui/src/index.tsx b/packages/kilo-config-ui/src/index.tsx new file mode 100644 index 0000000000..f1102c6f30 --- /dev/null +++ b/packages/kilo-config-ui/src/index.tsx @@ -0,0 +1,36 @@ +import "@kilocode/kilo-ui/styles" +import { Router, Route } from "@solidjs/router" +import { render } from "solid-js/web" +import App from "./App" +import "./styles.css" +import { ProjectsRoute } from "./routes/projects/ProjectsRoute" +import { ProfileRoute } from "./routes/profile/ProfileRoute" +import { ConfigLayout } from "./layouts/ConfigLayout" +import { configSections } from "./routes/config/sections" + +const root = document.getElementById("root") +if (!root) throw new Error("Missing root element") + +function routes() { + return configSections.map((item) => ) +} + +render( + () => ( + + + + {routes()} + + + + {routes()} + + + {routes()} + + + + ), + root, +) diff --git a/packages/kilo-config-ui/src/layouts/ConfigLayout.tsx b/packages/kilo-config-ui/src/layouts/ConfigLayout.tsx new file mode 100644 index 0000000000..3194ebfccc --- /dev/null +++ b/packages/kilo-config-ui/src/layouts/ConfigLayout.tsx @@ -0,0 +1,54 @@ +import { Show } from "solid-js" +import type { JSX } from "solid-js" +import { Card } from "@kilocode/kilo-ui/card" +import { ConfigProvider } from "../context/ConfigProvider" +import { useConfig } from "../context/config" +import { ConfigSidebar } from "../routes/config/ConfigSidebar" +import { errMsg } from "../shared/utils" + +export function ConfigLayout(props: { children?: JSX.Element }) { + return ( + + {props.children} + + ) +} + +function ConfigContent(props: { children?: JSX.Element }) { + const ctx = useConfig() + + return ( +
+ +
+ + {(item) => ( + + )} + + + {(item) => ( + + )} + + + + + + + + {props.children} +
+
+ ) +} diff --git a/packages/kilo-config-ui/src/layouts/ConsoleLayout.tsx b/packages/kilo-config-ui/src/layouts/ConsoleLayout.tsx new file mode 100644 index 0000000000..fef8dbedd3 --- /dev/null +++ b/packages/kilo-config-ui/src/layouts/ConsoleLayout.tsx @@ -0,0 +1,21 @@ +import type { JSX } from "solid-js" +import { AppHeader } from "../components/app-header/AppHeader" +import { AppSidebar } from "../components/app-sidebar/AppSidebar" +import type { Path } from "../shared/navigation" + +type Props = { + children: JSX.Element + path: Path +} + +export function ConsoleLayout(props: Props) { + return ( +
+ +
+ +
{props.children}
+
+
+ ) +} diff --git a/packages/kilo-config-ui/src/routes/config/AgentsRoute.tsx b/packages/kilo-config-ui/src/routes/config/AgentsRoute.tsx new file mode 100644 index 0000000000..0014dab74e --- /dev/null +++ b/packages/kilo-config-ui/src/routes/config/AgentsRoute.tsx @@ -0,0 +1,252 @@ +import { For, Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Tag } from "@kilocode/kilo-ui/tag" +import { toMode, toAction } from "../../shared/utils" +import { ConfigPage, SourceBadge } from "./ConfigPage" +import { snippets, useAgentBuilder } from "./state/agents" + +export function AgentsRoute() { + const state = useAgentBuilder() + + return ( + + {(data) => ( + {data().agents.length}}> +
+
+
+ + + + + + + + +