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] 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}}> +
+
+
+ + + + + + + + +