diff --git a/.changeset/agent-manager-pr-status-icon.md b/.changeset/agent-manager-pr-status-icon.md new file mode 100644 index 0000000000..bdb2257257 --- /dev/null +++ b/.changeset/agent-manager-pr-status-icon.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Color Agent Manager PR badges by pull request state (open, draft, merged, closed) and show CI and review status as a separate icon, so a failing check is no longer mistaken for a closed PR. diff --git a/.changeset/prefer-kilo-config.md b/.changeset/prefer-kilo-config.md new file mode 100644 index 0000000000..6fe9a3d09c --- /dev/null +++ b/.changeset/prefer-kilo-config.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Stop loading `.opencode` config directories and use `.kilo` instead, while retaining `.kilocode` as a legacy fallback. diff --git a/.changeset/release-disconnected-streams.md b/.changeset/release-disconnected-streams.md new file mode 100644 index 0000000000..651417c7aa --- /dev/null +++ b/.changeset/release-disconnected-streams.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Release disconnected event streams so long-running servers do not retain queued session diffs. diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md index e0f87943db..ae789ea681 100644 --- a/packages/kilo-docs/pages/automate/extending/plugins.md +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -58,7 +58,7 @@ Config files live in the same locations as the rest of your CLI configuration Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory: - Global: `~/.config/kilo/plugin/` -- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/` +- Project: `.kilo/plugin/` or legacy `.kilocode/plugin/` Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file. @@ -86,7 +86,7 @@ kilo plugin my-plugin --global kilo plugin my-plugin --force ``` -The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (currently `.opencode/opencode.jsonc` / `.opencode/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments. +The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (`.kilo/opencode.jsonc` / `.kilo/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments. ### How plugins are installed @@ -446,7 +446,7 @@ If a custom tool uses the same name as a built-in tool, **the custom tool wins** ### Alternative: standalone tool files -For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`. +For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); use `.kilo/`, or legacy `.kilocode/`, instead of `.opencode/`. --- diff --git a/packages/kilo-docs/pages/automate/integrations.md b/packages/kilo-docs/pages/automate/integrations.md index 8cfd5d2e2f..fb2146f314 100644 --- a/packages/kilo-docs/pages/automate/integrations.md +++ b/packages/kilo-docs/pages/automate/integrations.md @@ -20,7 +20,7 @@ Kilo Integrations lets you connect GitHub or GitLab for repository workflows and - **Connect GitHub, GitLab, or DoltHub to Kilo Code** in a few clicks - **Enable advanced features** like Cloud Agents, Code Reviews, and Kilo Deploy - **Authorize GitHub or GitLab repository access** so Kilo can analyze and work with your code -- **Query Dolt-versioned data** from your workspace through Kilo Connect +- **Query Dolt-versioned data** and authorize DoltHub access for Gas Town Wasteland ## Prerequisites @@ -114,15 +114,21 @@ For self-hosted GitLab instances using OAuth, you need to register an OAuth appl ## Connecting DoltHub -DoltHub is available through [Kilo Connect](/docs/code-with-ai/platforms/kilo-connect) for teams that work with Dolt-versioned data. +Use the DoltHub integration page to connect your DoltHub account. Kilo uses this OAuth connection for [Kilo Connect](/docs/code-with-ai/platforms/kilo-connect) and as the default auth method for [Gas Town Wasteland](/docs/code-with-ai/gastown/wasteland). 1. Go to the **Integrations** page: - **Personal**: [app.kilo.ai/integrations/dolthub](https://app.kilo.ai/integrations/dolthub) - **Organization**: Your organization → Integrations → DoltHub 2. Click **Connect DoltHub**. -3. Authorize the connection with DoltHub. +3. Approve Kilo on DoltHub. 4. Return to Kilo and confirm DoltHub shows a **Connected** status. +{% image src="/docs/img/integrations/dolthub/connect.png" alt="DoltHub integration page before OAuth connection" width="900" caption="DoltHub integration page in Kilo" /%} + +{% image src="/docs/img/integrations/dolthub/authorize.png" alt="DoltHub OAuth authorization screen for Kilo" width="700" caption="DoltHub OAuth authorization screen" /%} + +{% image src="/docs/img/integrations/dolthub/connected.png" alt="Connected DoltHub integration page showing api_read_write permission" width="900" caption="Connected DoltHub integration" /%} + To remove the connection, click **Disconnect** from the DoltHub integration page. --- @@ -153,6 +159,7 @@ Once your integrations are connected, the following features are enabled in Kilo - Query Dolt-versioned databases from your workspace - Use DoltHub alongside GitHub or GitLab when a workflow also needs repository access +- Authorize Gas Town Wasteland to fork commons databases, push claims and evidence, and manage DoltHub PRs. Wasteland also supports an advanced API token option when OAuth is not available. ### Upcoming: @@ -185,6 +192,7 @@ From the **Integrations** page: From the **Integrations** page, open DoltHub to: - View the connected status +- View granted permissions - Disconnect DoltHub from Kilo --- diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md index edafc39180..254cb83f0c 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md @@ -26,7 +26,7 @@ Running your own wasteland means creating a new DoltHub database and configuring 1. Navigate to your town's **Settings** → **Wasteland** tab 2. Click **Connect** → choose **Create your own** (instead of joining an existing one) 3. Enter your wasteland name and upstream (e.g., `my-org/wl-internal`) -4. Provide your DoltHub PAT — this must have write access to the upstream repo +4. Authorize DoltHub — OAuth through Kilo Integrations is the default. The advanced API token option is also supported, but it must have write access to the upstream repo. 5. Check **"I own this upstream"** — this sets `is_upstream_admin = true` on your credential, enabling admin mode 6. Click **Connect** @@ -297,9 +297,9 @@ The wasteland settings page exposes additional controls when your credential has ### Test admin access -The **Test admin access** button probes DoltHub by attempting a no-op write against a scratch branch on the upstream. If the probe succeeds, you see a green "Admin access verified" badge. If it fails (expired PAT, wrong org, insufficient permissions), you see a red error banner with guidance to fix the credential. +The **Test admin access** button checks whether Kilo can write to a scratch branch on the upstream. If it works, you see a green "Admin access verified" badge. If it fails, DoltHub OAuth may be disconnected, the API token may be invalid, the wrong org may be connected, or write access may be missing. -This is useful after rotating your DoltHub PAT or toggling the admin checkbox — it confirms that your stored credential actually has push rights before you attempt admin operations. +Use this after reconnecting DoltHub OAuth, updating an API token, or toggling the admin checkbox. It confirms Kilo can push before you run admin actions. ### Pending PRs diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md index 7c2470be37..7b93d2e526 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md @@ -43,7 +43,7 @@ A **rig identity** is how your Gas Town is addressed on the Wasteland. It's comp -When connecting your Gas Town to a Wasteland, you provide a DoltHub personal access token (PAT) so the Mayor can push claims and evidence on your behalf. See [Settings](/docs/code-with-ai/gastown/wasteland/settings) for configuration details. +When you connect Gas Town to a Wasteland, authorize DoltHub through Kilo Integrations. The Mayor uses that OAuth connection to push claims and evidence. If OAuth does not work for your setup, the connection dialog also supports a DoltHub API token in **Advanced**. See [Settings](/docs/code-with-ai/gastown/wasteland/settings) for setup details. ## Wanted Board @@ -171,4 +171,4 @@ In **PR mode** (the default), the flow looks like this: In **direct mode** (enabled when `is_upstream_admin = true`), changes push directly to the upstream commons without creating a PR. This is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default — the `--direct` flag must be passed explicitly, and is silently downgraded to PR mode if the caller's credential isn't marked as admin. -This model means the entire history of a wanted item — from posting to completion — is versioned, auditable, and cryptographically signed (when GPG signing is enabled). See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the end-to-end walkthrough and [Administration](/docs/code-with-ai/gastown/wasteland/admin) for running your own instance. \ No newline at end of file +This model means the entire history of a wanted item — from posting to completion — is versioned, auditable, and cryptographically signed (when GPG signing is enabled). See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the end-to-end walkthrough and [Administration](/docs/code-with-ai/gastown/wasteland/admin) for running your own instance. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md index 32f243ae81..432df0892a 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md @@ -8,8 +8,8 @@ noindex: true Get your Gas Town connected to the Commons wasteland and working on your first wanted item in a few minutes. -{% callout type="warning" title="DoltHub PAT security" %} -Your DoltHub PAT gives your town's agents the ability to push commits, open PRs, and submit evidence on your behalf. Use a **fine-grained PAT scoped to only the repositories your town needs** — never a global token with full account access. See [DoltHub credentials](https://www.dolthub.com/settings/credentials) to create one. +{% callout type="info" title="Connect DoltHub first" %} +Connect DoltHub through [Kilo Integrations](https://app.kilo.ai/integrations/dolthub) before joining a wasteland. Kilo uses DoltHub OAuth by default. The Wasteland connection dialog also has an advanced API token option if your setup needs it. {% /callout %} ## 1. Before You Start @@ -25,15 +25,14 @@ Make sure you have the prerequisites: Open your town's **Settings** → **Wasteland** tab and click **Connect**. 1. **Choose an upstream** — The default is `hop/wl-commons`, the reference commons. This is where the shared Wanted Board lives. -2. **Enter your DoltHub PAT** — Create a token at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials). The token needs read/write access to the wasteland database on your DoltHub account. Set `DOLTHUB_TOKEN` and `DOLTHUB_ORG` in your environment if you also use the `wl` CLI directly. +2. **Authorize DoltHub** — If DoltHub isn't connected yet, open [Kilo Integrations](https://app.kilo.ai/integrations/dolthub) and click **Connect DoltHub**. Use the advanced API token option only if you can't use OAuth. 3. **Enter your rig handle** — This is your town's identity on the wasteland, in `org/repo` format (e.g., `kilo/main`). It's set once when you join — choose carefully, as it's sticky by design. Behind the scenes this is equivalent to `wl join --handle `. 4. Click **Connect**. Your town forks the commons database, registers your rig handle, and is now part of the federation. - -{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="The Wasteland connect dialog in Gas Town settings" %} -{% image src="/docs/img/gastown/wasteland/gt-wasteland-connect-dialog.png" alt="Wasteland connect dialog" /%} +{% browserFrame url="app.kilo.ai/integrations/dolthub" caption="DoltHub connection in Kilo Integrations" %} +{% image src="/docs/img/integrations/dolthub/connect.png" alt="DoltHub integration page with Connect DoltHub button" /%} {% /browserFrame %} {% callout type="info" %} diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md index 851700a4a1..69127f63c1 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md @@ -1,12 +1,12 @@ --- title: "Wasteland Settings" -description: "Wasteland connection settings in the Gas Town dashboard, DoltHub token setup, and rig identity" +description: "Wasteland connection settings in the Gas Town dashboard, DoltHub credentials, and rig identity" noindex: true --- # {% $markdoc.frontmatter.title %} -Reference for every Wasteland-related setting in your Gas Town dashboard, from upstream selection to DoltHub PAT management. +Reference for every Wasteland setting in your Gas Town dashboard, from upstream selection to DoltHub credentials. Access these settings from your town dashboard → **Settings** → **Wasteland** tab. @@ -72,46 +72,66 @@ If you absolutely need a different handle, disconnect from the wasteland and rec Think of your rig handle like a GitHub username — you set it once and it follows you everywhere on that wasteland. Choose something stable and recognizable. {% /callout %} -## DoltHub PAT +## DoltHub credentials -Your DoltHub Personal Access Token lets your town's agents interact with the wasteland on your behalf — forking the commons, pushing claims, and submitting evidence via DoltHub pull requests. +Your DoltHub credentials let your town's agents use the wasteland. They can fork the commons, push claims, and submit evidence through DoltHub pull requests. -### Creating a PAT +{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="DoltHub credentials dialog with OAuth default and advanced API token option" %} +{% image src="/docs/img/gastown/wasteland/dolthub-credentials-dialog.png" alt="DoltHub credentials dialog showing connected account and advanced API token fields" /%} +{% /browserFrame %} -1. Go to [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) -2. Create a new token -3. Scope the token to the repositories your town needs access to +### Default: OAuth through Kilo Integrations -### Required scope +1. Go to [Kilo Integrations → DoltHub](https://app.kilo.ai/integrations/dolthub) +2. Click **Connect DoltHub** +3. Approve Kilo on DoltHub +4. Return to Kilo and confirm DoltHub shows **Connected** -The PAT needs read/write access to the wasteland database on your DoltHub account. At minimum, it must be able to: +When you join a wasteland, choose **Use your connected DoltHub account**. Confirm your DoltHub username so Kilo can label commits and contribution branches correctly. + +### Advanced: API token + +If OAuth is not available for your setup, expand **Advanced — Paste an API token** in the Wasteland connection dialog. + +1. Create a DoltHub API token from [dolthub.com/settings/tokens](https://www.dolthub.com/settings/tokens) +2. Paste the token into **DoltHub API token** +3. Enter your DoltHub username or organization + +Use this only when you need a token-based setup. For normal setup, use the connected DoltHub account. + +### Required access + +Your DoltHub credentials need read and write access. Kilo uses them to: - Fork the upstream commons database - Push branches to your fork - Open and update pull requests on the upstream -{% callout type="warning" title="Security: agents act on your behalf" %} -Your DoltHub PAT gives your town's agents the ability to push commits and open PRs under your DoltHub account. **Use a fine-grained PAT scoped to only the repositories your town needs.** Never use a global token with full account access. Since agents act autonomously, limiting the token's scope reduces the blast radius if it's ever compromised. +{% callout type="info" title="Use OAuth by default" %} +Kilo uses DoltHub OAuth for the normal Gas Town Wasteland flow. Use the advanced API token option only for setups that cannot use OAuth. {% /callout %} -### Rotating your PAT +### Reconnecting or rotating credentials -To rotate an expired or compromised token: +To reconnect DoltHub OAuth: -1. Generate a new PAT at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) -2. Go to **Settings** → **Wasteland** tab -3. Update the PAT field with the new token -4. Revoke the old token on DoltHub +1. Go to [Kilo Integrations → DoltHub](https://app.kilo.ai/integrations/dolthub) +2. Click **Disconnect** +3. Click **Connect DoltHub** +4. Approve Kilo again on DoltHub -Your town's active connections will pick up the new token immediately — no reconnect needed. +If you used the advanced API token option, create a new token on DoltHub and paste it into the Wasteland connection dialog. -### If your PAT is invalid +Your town uses the updated DoltHub credentials automatically. You don't need to reconnect the wasteland. -If the Mayor reports DoltHub authentication errors, the PAT may have expired or been revoked. Check: +### If DoltHub auth fails -- The token hasn't been revoked on [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) -- The token's scope still covers the wasteland database -- The token hasn't hit a rate limit +If the Mayor reports DoltHub authentication errors, check: + +- DoltHub shows **Connected** in [Kilo Integrations](https://app.kilo.ai/integrations/dolthub) +- The connected DoltHub account can access the wasteland database +- The advanced API token is valid, if you used one +- DoltHub isn't rate limiting API requests See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland) for detailed diagnosis steps. @@ -139,7 +159,7 @@ The auto-submit flow: 4. Evidence is pushed to your wasteland fork and proposed upstream as a DoltHub PR {% callout type="info" %} -If the auto-submit fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. +If the auto-submit fails, DoltHub may be unavailable, OAuth may be disconnected, or the advanced API token may be invalid. The Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the auth issue is fixed. {% /callout %} ## Wasteland Admin Settings diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md index 1f367d00d9..7535d58a1c 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md @@ -131,7 +131,7 @@ When working through Gas Town, the Mayor automatically packages the PR URL from The DoltHub PR created during the claim step now contains both the claim commit and the evidence commit. Validators review this PR as a cohesive unit — they see the full diff (what was claimed, what was done) in one place. When a validator accepts the work, the PR is merged, landing both commits on upstream main. -If evidence submission fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. +If evidence submission fails, DoltHub may be unavailable, OAuth may be disconnected, or the advanced API token may be invalid. The Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the auth issue is fixed. {% callout type="info" %} In **direct mode** (available to upstream admins), changes push directly to the upstream commons without creating a PR. This skips the review gate and is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default. @@ -214,7 +214,7 @@ If a validator **rejects** your evidence, the item goes back to `claimed` (not ` | Problem | What to do | |---|---| | Claim fails — another rig got there first | The Mayor will suggest the next available item. Try claiming a different one. | -| Evidence auto-submit fails | Check your DoltHub PAT is valid. The Mayor will retry. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Evidence auto-submit fails | Check DoltHub OAuth in [Kilo Integrations](https://app.kilo.ai/integrations/dolthub), or check the API token in the Wasteland connection dialog if you used the advanced option. The Mayor will retry. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | | Stuck claim — you can't complete the work | [Abandon the claim](#cancelingabandoning) to release it back to the board. | | Item stays in `in_review` forever | Validators may be backlogged. You can check the PR status on DoltHub directly. | | `PRECONDITION_FAILED` on `wl done` | The item may not be in `claimed` state, or your credential may be invalid. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md index 6c6e5cb652..1699cb4455 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md @@ -335,8 +335,8 @@ The Kilo CLI is a fork of [OpenCode](https://opencode.ai) and supports the same | Scope | Path | |---|---| -| **Global** | `~/.config/kilo/opencode.json` or `opencode.jsonc` (Windows: config dir may vary; same filenames) | -| **Project** | `./opencode.json` or `./.opencode/` in project root | +| **Global** | `~/.config/kilo/kilo.json[c]` or legacy `opencode.json[c]` (Windows config dir may vary) | +| **Project** | `./kilo.json[c]`, legacy `./opencode.json[c]`, or config inside `./.kilo/` (legacy `./.kilocode/` is also read) | Project-level configuration takes precedence over global settings. diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md b/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md index 0ce89b82ad..eacd668954 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md @@ -16,7 +16,7 @@ description: "Use Kilo Code from Slack, GitHub, and Linear, and connect DoltHub | [Slack](/docs/code-with-ai/platforms/slack) | `@Kilo` in any channel or DM | Ask questions, implement fixes, debug issues | | [GitHub](/docs/code-with-ai/platforms/github) | `@kilocode-bot` on issues and PRs | Fix issues, review code, cross-repo changes | | [Linear](/docs/code-with-ai/platforms/linear) | `@kilo` on any issue | Implement fixes, investigate bugs, cross-repo changes | -| DoltHub | [Connect DoltHub](https://app.kilo.ai/integrations/dolthub) from Integrations | Query Dolt-versioned data from your workspace | +| DoltHub | [Connect DoltHub](https://app.kilo.ai/integrations/dolthub) from Integrations | Query Dolt-versioned data and authorize DoltHub access for Gas Town Wasteland | --- diff --git a/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md b/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md index 02963d704f..ce188c485a 100644 --- a/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md +++ b/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md @@ -262,7 +262,7 @@ Later sources override earlier values during instance config load: | 11 | macOS managed preferences | | 12 | Runtime flag-derived permission, tool, compaction, and plugin behavior | -Global config files load from `${Global.Path.config}`. Project updates prefer existing config files found in ancestor `.kilo`, `.kilocode`, or `.opencode` directories, then existing project root config files, then create `.kilo/kilo.json`. Global indexing settings can carry provider and storage defaults, but global `indexing.enabled` is stripped so project enablement remains local in effective instance config. +Global config files load from `${Global.Path.config}`. Project updates prefer existing config files found in ancestor `.kilo` or legacy `.kilocode` directories, then existing project root config files, then create `.kilo/kilo.json`. Global indexing settings can carry provider and storage defaults, but global `indexing.enabled` is stripped so project enablement remains local in effective instance config. Signed-in organization modes become normal agent configuration during load. They override migrated legacy modes and remain overridable by later config sources in table. diff --git a/packages/kilo-docs/pages/customize/custom-modes.md b/packages/kilo-docs/pages/customize/custom-modes.md index 95f82ce3da..fb68c4c276 100644 --- a/packages/kilo-docs/pages/customize/custom-modes.md +++ b/packages/kilo-docs/pages/customize/custom-modes.md @@ -76,7 +76,7 @@ Create `.md` files in any of these directories: ``` .kilo/agents/my-agent.md .kilo/agent/my-agent.md -.opencode/agents/my-agent.md +.kilocode/agents/my-agent.md ``` For global agents, place files in your global config directory: @@ -191,7 +191,7 @@ Agent configurations merge from lowest to highest priority: 1. Built-in (native) agent defaults 2. Global config (`~/.config/kilo/kilo.jsonc`) 3. Project config (`kilo.jsonc` at project root) -4. `.kilo/` / `.opencode/` directory configs and agent `.md` files +4. `.kilo/` / legacy `.kilocode/` directory configs and agent `.md` files 5. Environment variable overrides (`KILO_CONFIG_CONTENT`) When the same agent name appears at multiple levels, properties are merged (not replaced wholesale), so you can override just a model or temperature without redefining the entire agent. @@ -312,7 +312,7 @@ Create `.md` files in any of these directories: ``` .kilo/agents/my-agent.md .kilo/agent/my-agent.md -.opencode/agents/my-agent.md +.kilocode/agents/my-agent.md ``` For global agents, place files in your global config directory: @@ -427,7 +427,7 @@ Agent configurations merge from lowest to highest priority: 1. Built-in (native) agent defaults 2. Global config (`~/.config/kilo/kilo.jsonc`) 3. Project config (`kilo.jsonc` at project root) -4. `.kilo/` / `.opencode/` directory configs and agent `.md` files +4. `.kilo/` / legacy `.kilocode/` directory configs and agent `.md` files 5. Environment variable overrides (`KILO_CONFIG_CONTENT`) When the same agent name appears at multiple levels, properties are merged (not replaced wholesale), so you can override just a model or temperature without redefining the entire agent. @@ -579,7 +579,7 @@ When importing modes, you can change the slug in the exported YAML file before i Custom agents are defined as Markdown files with optional YAML frontmatter. You can place them in: -- **Project agents:** `.kilo/agents/*.md` (or `.opencode/agents/*.md`) +- **Project agents:** `.kilo/agents/*.md` (or legacy `.kilocode/agents/*.md`) - **Global agents:** `~/.config/kilo/agents/*.md` ### Agent File Format @@ -1256,7 +1256,7 @@ customModes: ### Common Issues -- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.opencode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker. +- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.kilocode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker. - **Permission errors:** Permission rules are evaluated last-match-wins. If an agent can't use a tool you expect, check that an `allow` rule appears after any `deny` rules for that permission. - **YAML frontmatter parse errors:** Ensure the frontmatter block starts and ends with `---` on its own line. Validate that YAML keys match expected property names (e.g., `top_p` not `topP`). - **Agent overrides not working:** Config merges from global to project level. If a global config sets a property, your project config can override it, but both must use the same agent name. @@ -1273,7 +1273,7 @@ customModes: ### Common Issues -- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.opencode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker. +- **Agent not appearing:** Ensure the `.md` file is in a recognized directory (`.kilo/agents/`, `.kilo/agent/`, `.kilocode/agents/`). Check that the `mode` property is `primary` or `all` if you expect it in the agent picker. - **Permission errors:** Permission rules are evaluated last-match-wins. If an agent can't use a tool you expect, check that an `allow` rule appears after any `deny` rules for that permission. - **YAML frontmatter parse errors:** Ensure the frontmatter block starts and ends with `---` on its own line. Validate that YAML keys match expected property names (e.g., `top_p` not `topP`). - **Agent overrides not working:** Config merges from global to project level. If a global config sets a property, your project config can override it, but both must use the same agent name. diff --git a/packages/kilo-docs/public/img/gastown/wasteland/dolthub-credentials-dialog.png b/packages/kilo-docs/public/img/gastown/wasteland/dolthub-credentials-dialog.png new file mode 100644 index 0000000000..bcff80f5d6 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/dolthub-credentials-dialog.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png deleted file mode 100644 index 1d2a1008d8..0000000000 Binary files a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png and /dev/null differ diff --git a/packages/kilo-docs/public/img/integrations/dolthub/authorize.png b/packages/kilo-docs/public/img/integrations/dolthub/authorize.png new file mode 100644 index 0000000000..ae5ed80dd0 Binary files /dev/null and b/packages/kilo-docs/public/img/integrations/dolthub/authorize.png differ diff --git a/packages/kilo-docs/public/img/integrations/dolthub/connect.png b/packages/kilo-docs/public/img/integrations/dolthub/connect.png new file mode 100644 index 0000000000..8a6492928d Binary files /dev/null and b/packages/kilo-docs/public/img/integrations/dolthub/connect.png differ diff --git a/packages/kilo-docs/public/img/integrations/dolthub/connected.png b/packages/kilo-docs/public/img/integrations/dolthub/connected.png new file mode 100644 index 0000000000..70398b7aa6 Binary files /dev/null and b/packages/kilo-docs/public/img/integrations/dolthub/connected.png differ diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png index 9607336064..42e7b3b4b6 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da91a2dd21f1b647d3d7e95756755d5e8357bbba7f964f328d1f58363990e2cd -size 12377 +oid sha256:e2b861de27fb8e6d64becd50bb4b3b05d748fba74b98aafaf5cb2a182ff74655 +size 26259 diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 303661b74a..a349d0fe20 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -2,7 +2,7 @@ * Sidebar worktree item with inline delete confirmation, HoverCard, rename, and stats. * Extracted from AgentManagerApp for reuse and visual-regression testing via Storybook. */ -import { Component, For, Show, createSignal } from "solid-js" +import { Component, For, Match, Show, Switch, createSignal } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" @@ -84,17 +84,43 @@ const MAX_SHORTCUT = 9 const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats => !!s && (s.files > 0 || s.additions > 0 || s.deletions > 0 || s.ahead > 0 || s.behind > 0) -/** Returns the accent color for a PR badge based on state priority. */ +/** + * Accent color for a PR badge, derived from the PR's lifecycle state + * (open/draft/merged/closed). The one exception is an open PR with checks still + * running, which uses amber and pulses its background (see prChecksRunning) — a + * transient, unambiguous signal since no lifecycle state uses amber. Terminal CI + * and review results are conveyed by a separate status icon (see prBadgeIndicator) + * so a failing check is not mistaken for a closed PR. + */ export function prAccentColor(pr: PRStatus): string { if (pr.state === "draft") return "var(--text-weaker)" if (pr.state === "merged") return "#a78bfa" if (pr.state === "closed") return "#f87171" - if (pr.checks.status === "failure") return "#ef4444" - if (pr.review === "changes_requested") return "#fbbf24" if (pr.checks.status === "pending") return "#fbbf24" return "#34d399" } +/** True while an open PR's checks are still running — drives the pulsing amber badge. */ +export function prChecksRunning(pr: PRStatus): boolean { + return pr.state === "open" && pr.checks.status === "pending" +} + +export type PRBadgeIndicator = "failure" | "changes" | "approved" | "none" + +/** + * Terminal CI/review status shown as an icon overlaid on the PR badge, independent + * of the badge's state-based accent color. Running checks are not represented here — + * they are shown by the pulsing amber background instead. Terminal PRs (merged/closed) + * show no indicator since their checks are no longer actionable. + */ +export function prBadgeIndicator(pr: PRStatus): PRBadgeIndicator { + if (pr.state === "merged" || pr.state === "closed") return "none" + if (pr.checks.status === "failure") return "failure" + if (pr.review === "changes_requested") return "changes" + if (pr.review === "approved") return "approved" + return "none" +} + function prStateLabel(state: PRStatus["state"]): string { if (state === "draft") return "Draft" if (state === "merged") return "Merged" @@ -315,14 +341,30 @@ export const WorktreeItem: Component = (props) => { > {(pr) => { const accent = () => prAccentColor(pr()) + const indicator = () => prBadgeIndicator(pr()) return ( - + }> + + + + + + + + + + #{pr().number} ) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 35dae03902..260baec350 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -710,6 +710,19 @@ button.am-section-toggle:hover .am-section-label { .am-pr-badge:hover .am-pr-badge-number { color: var(--pr-accent); } +/* Status icon overlaid on the badge — colored by CI/review result, independent + of the badge's PR-state accent color. */ +.am-pr-badge-status[data-status="failure"] { + color: #ef4444; +} +.am-pr-badge-status[data-status="changes"] { + color: #fbbf24; +} +.am-pr-badge-status[data-status="approved"] { + color: #34d399; +} + +/* Checks still running — pulse the whole amber badge. */ .am-pr-badge[data-pending] { animation: am-pr-pulse 1.5s ease-in-out infinite; } diff --git a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx index 9d8fd984c2..cf348a4d33 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx @@ -573,10 +573,11 @@ export const WithPRBadges: Story = { name: "Section — worktrees with PR badges", render: () => ( -
+
- +
+ {/* Open + passing + approved → green badge, green check */} + {/* Open + failing checks → green badge, red ✗ (no longer confusable with closed) */} + {/* Open + changes requested → green badge, amber warning */} + {/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */} + + {/* Draft → gray badge */} + + {/* Merged → purple badge, no status icon */} + + {/* Closed → red badge, no status icon (distinct from a failing open PR) */} +
diff --git a/packages/opencode/specs/tui-plugins.md b/packages/opencode/specs/tui-plugins.md index 89ebc1ffbc..fc4c7894eb 100644 --- a/packages/opencode/specs/tui-plugins.md +++ b/packages/opencode/specs/tui-plugins.md @@ -195,8 +195,8 @@ npm plugins can declare a version compatibility range in `package.json` using th - Alias: `kilo plug `. - `-g` / `--global` writes into the global config dir. - Local installs resolve target dir inside `patchPluginConfig`. -- For local scope, path is `/.opencode` only when VCS is git and `worktree !== "/"`; otherwise `/.opencode`. -- Root-worktree fallback (`worktree === "/"` uses `/.opencode`) is covered by regression tests. +- For local scope, path is `/.kilo` only when VCS is git and `worktree !== "/"`; otherwise `/.kilo`. +- Root-worktree fallback (`worktree === "/"` uses `/.kilo`) is covered by regression tests. - `patchPluginConfig` applies all detected targets (`server` and/or `tui`) in one call. - `patchPluginConfig` returns structured result unions (`ok`, `code`, fields by error kind) instead of custom thrown errors. - `patchPluginConfig` serializes per-target config writes with `Flock.acquire(...)`. @@ -213,7 +213,7 @@ npm plugins can declare a version compatibility range in `package.json` using th - There is no uninstall, list, or update CLI command for external plugins. - Local file plugins are configured directly in `tui.json`. -When `plugin` entries exist in a writable `.opencode` dir or `KILO_CONFIG_DIR`, Kilo installs `@kilocode/plugin` into that dir and writes: +When `plugin` entries exist in a writable `.kilo` or legacy `.kilocode` directory, or `KILO_CONFIG_DIR`, Kilo installs `@kilocode/plugin` into that directory and writes: - `package.json` - `bun.lock` @@ -413,7 +413,7 @@ Theme install behavior: - If the theme name already exists, install is skipped unless plugin metadata state is `updated`. - On `updated`, host skips rewrite when tracked `mtime`/`size` is unchanged. - When a theme already exists and state is not `updated`, host can still persist theme metadata when destination already exists. -- Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source. +- Local plugins persist installed themes under `.kilo/themes`, or under `themes` beside a config source in `.kilo` or legacy `.kilocode`. - Global plugins persist installed themes under the global `themes` dir. - Invalid or unreadable theme files are ignored. @@ -538,7 +538,4 @@ The plugin manager is exposed as a command with title `Plugins` and value `plugi ## Current in-repo examples -- Local smoke plugin: `.opencode/plugins/tui-smoke.tsx` -- Local vim plugin: `.opencode/plugins/tui-vim.tsx` -- Local smoke config: `.opencode/tui.json` -- Local smoke theme: `.opencode/plugins/smoke-theme.json` +The upstream `.opencode` smoke fixtures remain in the repository for OpenCode compatibility, but Kilo does not auto-load them. Kilo-local examples must live under `.kilo` or legacy `.kilocode`. diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index a18a49de4b..9ff46f91d9 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -397,25 +397,26 @@ export const McpLogoutCommand = effectCmd({ }) async function resolveConfigPath(baseDir: string, global = false) { - // kilocode_change start - prefer kilo.json/.kilo over opencode.json/.opencode - // Check for existing config files (prefer .jsonc over .json, check .kilo/ and .opencode/ subdirectory too) - const candidates = [ - path.join(baseDir, "kilo.json"), + // kilocode_change start - prefer supported Kilo config directories over root files + const roots = [ path.join(baseDir, "kilo.jsonc"), - path.join(baseDir, "opencode.json"), + path.join(baseDir, "kilo.json"), path.join(baseDir, "opencode.jsonc"), + path.join(baseDir, "opencode.json"), ] - - if (!global) { - candidates.push( - path.join(baseDir, ".kilo", "kilo.json"), - path.join(baseDir, ".kilo", "kilo.jsonc"), - path.join(baseDir, ".kilo", "opencode.json"), - path.join(baseDir, ".kilo", "opencode.jsonc"), - path.join(baseDir, ".opencode", "opencode.json"), - path.join(baseDir, ".opencode", "opencode.jsonc"), - ) - } + const candidates = global + ? roots + : [ + path.join(baseDir, ".kilo", "kilo.jsonc"), + path.join(baseDir, ".kilo", "kilo.json"), + path.join(baseDir, ".kilo", "opencode.jsonc"), + path.join(baseDir, ".kilo", "opencode.json"), + path.join(baseDir, ".kilocode", "kilo.jsonc"), + path.join(baseDir, ".kilocode", "kilo.json"), + path.join(baseDir, ".kilocode", "opencode.jsonc"), + path.join(baseDir, ".kilocode", "opencode.json"), + ...roots, + ] for (const candidate of candidates) { if (await Filesystem.exists(candidate)) { diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index d9ff1c293b..4363e249fa 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -191,8 +191,10 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: acc.plugin_origins = plugins }) - // Every config dir we may read from: global config dir, any `.opencode` + // kilocode_change start - discover canonical and legacy Kilo config directories + // Every config dir we may read from: global config, .kilo and legacy .kilocode // folders between cwd and home, and KILO_CONFIG_DIR. + // kilocode_change end const directories = yield* ConfigPaths.directories(ctx.directory) yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory })) @@ -220,18 +222,16 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: yield* mergeFile(acc, file) } - // 4. `.opencode` directories (and KILO_CONFIG_DIR) discovered while - // walking up the tree. Also returned below so callers can install plugin - // dependencies from each location. - // kilocode_change start - also load tui.json from .kilo/.kilocode + // kilocode_change start - load tui.json from supported Kilo config directories + // 4. `.kilo` and legacy `.kilocode` directories (and KILO_CONFIG_DIR) + // discovered while walking up the tree. Also returned below so callers can + // install plugin dependencies from each location. const dirs = unique(directories).filter( - (dir) => - dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir.endsWith(".opencode") || dir === Flag.KILO_CONFIG_DIR, + (dir) => dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir === Flag.KILO_CONFIG_DIR, ) // kilocode_change end for (const dir of dirs) { - // if (!dir.endsWith(".opencode") && dir !== Flag.KILO_CONFIG_DIR) continue // kilocode_change for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { yield* mergeFile(acc, file) } diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 8f2c55477f..9299d901c4 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -508,7 +508,7 @@ async function getCustomThemes() { Global.Path.config, ...(await Array.fromAsync( Filesystem.up({ - targets: [".kilo", ".opencode"], // kilocode_change + targets: [".kilocode", ".kilo"], // kilocode_change start: process.cwd(), }), )), diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx index 7de14fa8b7..a608005cfb 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx @@ -135,10 +135,12 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) { } const tip = createMemo(() => { if (props.connected === false) return NO_MODELS_TIP - const tips = KILO_TIPS.flatMap((item) => { // kilocode_change + // kilocode_change start + const tips = KILO_TIPS.flatMap((item) => { const value = typeof item === "string" ? item : item(shortcuts) return value ? [value] : [] }) + // kilocode_change end return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP }, NO_MODELS_TIP) // Solid can expose a memo's initial value while a pure computation is pending. @@ -219,10 +221,10 @@ const TIPS: Tip[] = [ "Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section", "Set any keybind to {highlight}none{/highlight} to disable it completely", "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} to define reusable custom prompts", + "Add {highlight}.md{/highlight} files to {highlight}.kilo/commands/{/highlight} to define reusable custom prompts", // kilocode_change "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input", "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas", + "Add {highlight}.md{/highlight} files to {highlight}.kilo/agents/{/highlight} for specialized AI personas", // kilocode_change "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools", 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions', 'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands', @@ -231,9 +233,9 @@ const TIPS: Tip[] = [ 'Set {highlight}"formatter": false{/highlight} in config to disable formatters enabled by another config layer', "Define custom formatter commands with file extensions in config", 'Set {highlight}"lsp": true{/highlight} in config to enable built-in LSP servers for code analysis', - "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tools/{/highlight} to define new LLM tools", + "Create {highlight}.ts{/highlight} files in {highlight}.kilo/tools/{/highlight} to define new LLM tools", // kilocode_change "Tool definitions can invoke scripts written in Python, Go, etc", - "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks", + "Add {highlight}.ts{/highlight} files to {highlight}.kilo/plugins/{/highlight} for event hooks", // kilocode_change "Use plugins to send OS notifications when sessions complete", "Create a plugin to prevent OpenCode from reading sensitive files", "Use {highlight}opencode run{/highlight} for non-interactive scripting", @@ -250,7 +252,7 @@ const TIPS: Tip[] = [ "Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs", "Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews", 'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors', - "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory", + "Create JSON theme files in {highlight}.kilo/themes/{/highlight} directory", // kilocode_change "Themes support dark/light variants for both modes", "Use numeric xterm color codes 0-255 in custom theme JSON", "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config", diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index b33efd469b..1ef62bd496 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -254,10 +254,13 @@ function createThemeInstaller( const src = Filesystem.resolveFilePath(root, file) const name = path.basename(src, path.extname(src)) const source_dir = path.dirname(meta.source) + // kilocode_change start - install local themes into supported Kilo config directories + const base = path.basename(source_dir) const local_dir = - path.basename(source_dir) === ".opencode" + base === ".kilo" || base === ".kilocode" ? path.join(source_dir, "themes") - : path.join(source_dir, ".opencode", "themes") + : path.join(source_dir, ".kilo", "themes") + // kilocode_change end const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes") const dest = path.join(dest_dir, `${name}.json`) const stat = await Filesystem.statAsync(src) @@ -843,7 +846,7 @@ function defaultPluginOrigin(state: RuntimeState, spec: string): ConfigPlugin.Or return { spec, scope: "local", - source: state.api.state.path.config || path.join(state.directory, ".opencode", "tui.json"), + source: state.api.state.path.config || path.join(state.directory, ".kilo", "tui.json"), // kilocode_change } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 1b07116cfe..be259ff3d9 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -871,7 +871,7 @@ export const layer = Layer.effect( const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree) const primary = Flag.KILO_DISABLE_PROJECT_CONFIG ? [] - : yield* primaryPaths(ctx.directory, ctx.worktree, [".kilocode", ".kilo", ".opencode"]) + : yield* primaryPaths(ctx.directory, ctx.worktree, [".kilocode", ".kilo"]) // Load primary fallbacks before active-worktree config, then track them as local. directories.splice(1, 0, ...primary) const primarySet = new Set(primary) @@ -940,7 +940,7 @@ export const layer = Layer.effect( result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir, warnings))) result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings))) // kilocode_change end - // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load + // kilocode_change - Auto-discovered plugins under config directories are already local files, so ConfigPlugin.load // returns normalized Specs and we only need to attach origin metadata here. const list = yield* Effect.promise(() => ConfigPlugin.load(dir)) yield* mergePluginOrigins(dir, list, scope) // kilocode_change diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index c1ced22475..8c28a4d07f 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -26,13 +26,13 @@ export const directories = Effect.fn("ConfigPaths.directories")(function* (direc Global.Path.config, ...(!Flag.KILO_DISABLE_PROJECT_CONFIG ? yield* afs.up({ - targets: [".kilocode", ".kilo", ".opencode"], // kilocode_change + targets: [".kilocode", ".kilo"], // kilocode_change start: directory, stop: worktree, }) : []), ...(yield* afs.up({ - targets: [".kilocode", ".kilo", ".opencode"], // kilocode_change + targets: [".kilocode", ".kilo"], // kilocode_change start: Global.Path.home, stop: Global.Path.home, })), diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index ee5283f1a8..5b22ef9bec 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -441,7 +441,7 @@ export const layer: Layer.Layer files.map((file) => path.join(dir, file))), ...roots] return candidates.find((file) => existsSync(file)) ?? path.join(input.directory, ".kilo", "kilo.jsonc") @@ -182,12 +182,7 @@ export namespace KilocodeConfigOverlay { } function globalDirs() { - return [ - Global.Path.config, - path.join(Global.Path.home, ".kilocode"), - path.join(Global.Path.home, ".kilo"), - path.join(Global.Path.home, ".opencode"), - ] + return [Global.Path.config, path.join(Global.Path.home, ".kilocode"), path.join(Global.Path.home, ".kilo")] } async function withAgents(input: Config.Info, dirs: string[]): Promise { diff --git a/packages/opencode/src/kilocode/config/sources.ts b/packages/opencode/src/kilocode/config/sources.ts index 30706f9fe8..1dedcb0dca 100644 --- a/packages/opencode/src/kilocode/config/sources.ts +++ b/packages/opencode/src/kilocode/config/sources.ts @@ -56,7 +56,7 @@ export namespace KilocodeConfigSources { type Pending = Omit - const roots = [".kilocode", ".kilo", ".opencode"] as const + const roots = [".kilocode", ".kilo"] as const const global = ["config.json", "kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc"] as const export async function list(input: Input): Promise { diff --git a/packages/opencode/src/kilocode/docs/migration.md b/packages/opencode/src/kilocode/docs/migration.md index a3537a8961..ef12e34a12 100644 --- a/packages/opencode/src/kilocode/docs/migration.md +++ b/packages/opencode/src/kilocode/docs/migration.md @@ -192,7 +192,7 @@ Kilocode skills are automatically discovered and made available in Opencode. Thi ## How It Works -Opencode scans additional directories for skills alongside its native `.opencode/skill/` locations. The `KilocodePaths.skillDirectories()` function provides these paths. +Kilo scans canonical `.kilo/skill/` and `.kilo/skills/` directories alongside legacy `.kilocode/` equivalents. The `KilocodePaths.skillDirectories()` function provides these paths. ## Source Locations @@ -249,16 +249,7 @@ Detailed instructions for the agent... ## Priority / Override Behavior -When the same skill name exists in multiple locations, **last one wins**: - -1. `.claude/skills/` (lowest priority) -2. `.kilocode/skills/` (walk-up) -3. `~/.kilocode/skills/` -4. VSCode extension storage -5. `.opencode/skill/` (walk-up) -6. `~/.opencode/skill/` (highest priority) - -This means Opencode native skills take precedence over Kilocode skills with the same name. +When the same skill name exists in multiple locations, **last one wins**. Canonical `.kilo/` skills take precedence over legacy `.kilocode/` skills at the same project level. Explicit external skill paths and compatibility directories are loaded according to the configured scan order. ## Mode-Specific Skills @@ -286,8 +277,8 @@ Skills can be symlinked from a shared location: ``` .agents/skills/shared-skill/ # Actual skill -.kilocode/skills/shared-skill -> ... # Symlink -.opencode/skill/shared-skill -> ... # Symlink +.kilocode/skills/shared-skill -> ... # Legacy symlink +.kilo/skill/shared-skill -> ... # Canonical symlink ``` The scanner follows symlinks, so a skill installed once can be available to both Kilo VSCode and Opencode CLI. diff --git a/packages/opencode/src/kilocode/docs/rules-migration.md b/packages/opencode/src/kilocode/docs/rules-migration.md index ebec954562..84fee0e642 100644 --- a/packages/opencode/src/kilocode/docs/rules-migration.md +++ b/packages/opencode/src/kilocode/docs/rules-migration.md @@ -70,7 +70,7 @@ The migrator reads rules from these locations: - `AGENTS.md` in project root - `CLAUDE.md` in project root -- `~/.opencode/AGENTS.md` (global) +- `~/.config/kilo/AGENTS.md` (global) ## Not Migrated diff --git a/packages/opencode/src/kilocode/permission/config-paths.ts b/packages/opencode/src/kilocode/permission/config-paths.ts index 4946ec92a8..c120bc4330 100644 --- a/packages/opencode/src/kilocode/permission/config-paths.ts +++ b/packages/opencode/src/kilocode/permission/config-paths.ts @@ -5,9 +5,9 @@ import { KilocodePaths } from "@/kilocode/paths" export namespace ConfigProtection { /** * Config directory prefixes (relative paths, forward-slash normalized). - * Matches .kilo/, .kilocode/, .opencode/ at any depth within the project. + * Matches .kilo/ and legacy .kilocode/ at any depth within the project. */ - const CONFIG_DIRS = [".kilo/", ".kilocode/", ".opencode/"] + const CONFIG_DIRS = [".kilo/", ".kilocode/"] /** * Subdirectories under CONFIG_DIRS that are NOT config files (e.g. plan files). diff --git a/packages/opencode/src/kilocode/server/sse.ts b/packages/opencode/src/kilocode/server/sse.ts new file mode 100644 index 0000000000..752d9db5bd --- /dev/null +++ b/packages/opencode/src/kilocode/server/sse.ts @@ -0,0 +1,51 @@ +import { Effect } from "effect" +import type { HttpServerRequest } from "effect/unstable/http" + +type Socket = { + readonly destroyed: boolean + once(event: "close", handler: () => void): unknown + off(event: "close", handler: () => void): unknown +} + +type Incoming = { + readonly aborted: boolean + readonly socket: Socket + once(event: "aborted", handler: () => void): unknown + off(event: "aborted", handler: () => void): unknown +} + +function isIncoming(source: object): source is Incoming { + return "aborted" in source && "socket" in source +} + +function aborted(signal: AbortSignal) { + return Effect.callback((resume) => { + if (signal.aborted) return resume(Effect.void) + const handler = () => resume(Effect.void) + signal.addEventListener("abort", handler, { once: true }) + return Effect.sync(() => signal.removeEventListener("abort", handler)) + }) +} + +// The stream scope owns the GlobalBus subscription, but some client disconnects do not +// interrupt that scope through the current Node HTTP adapter. Every orphaned stream then +// keeps an unbounded event queue alive. Since session.diff events can contain complete, +// multi-megabyte patches, extension reconnects can retain tens of gigabytes in those queues. +// Convert both Web and Node transport disconnects into an Effect that can interrupt the +// stream and run its acquireRelease finalizers immediately. +export function disconnect(request: HttpServerRequest.HttpServerRequest) { + const source = request.source + if (source instanceof Request) return aborted(source.signal) + if (!isIncoming(source)) return Effect.never + + return Effect.callback((resume) => { + if (source.aborted || source.socket.destroyed) return resume(Effect.void) + const handler = () => resume(Effect.void) + source.once("aborted", handler) + source.socket.once("close", handler) + return Effect.sync(() => { + source.off("aborted", handler) + source.socket.off("close", handler) + }) + }) +} diff --git a/packages/opencode/src/kilocode/skills/kilo-config.md b/packages/opencode/src/kilocode/skills/kilo-config.md index a1a26eaaca..341cbfa898 100644 --- a/packages/opencode/src/kilocode/skills/kilo-config.md +++ b/packages/opencode/src/kilocode/skills/kilo-config.md @@ -2,11 +2,11 @@ All config lives in `kilo.json` (or `kilo.jsonc`). Precedence low-to-high: remote well-known, global (`~/.config/kilo/kilo.json`), env `KILO_CONFIG`, project `./kilo.json`, `.kilo/kilo.json`, `KILO_CONFIG_CONTENT`, managed (see Config File Locations). Deep-merged; later wins. -This also covers where Kilo looks for config files, commands, agents, and skills across project, global, and legacy paths such as `.kilo/`, `.kilocode/`, `.opencode/`, and `~/.config/kilo/`, plus Agent Manager setup/run scripts in the VS Code extension. +This also covers where Kilo looks for config files, commands, agents, and skills across project, global, and legacy paths such as `.kilo/`, `.kilocode/`, and `~/.config/kilo/`, plus Agent Manager setup/run scripts in the VS Code extension. ## Commands (`.kilo/command/*.md`) -Markdown files with YAML frontmatter. The filename (minus `.md`) becomes the command name invoked via `/name`. Commands can live in `.kilo/`, `.kilocode/`, `.opencode/`, and global config roots, with both `command/` and `commands/` directory names supported. See Config File Locations for the full search order. +Markdown files with YAML frontmatter. The filename (minus `.md`) becomes the command name invoked via `/name`. Commands can live in `.kilo/`, legacy `.kilocode/`, and global config roots, with both `command/` and `commands/` directory names supported. See Config File Locations for the full search order. ```yaml --- @@ -29,9 +29,8 @@ When asked where `/name` lives, do not search only the repo root. Search these r 1. `~/.config/kilo/` 2. `~/.kilo/` 3. `~/.kilocode/` -4. `~/.opencode/` -5. The `KILO_CONFIG_DIR` directory (if the env var is set) -6. project `.kilo/`, `.kilocode/`, and `.opencode/` directories from the current working directory up to the worktree root +4. The `KILO_CONFIG_DIR` directory (if the env var is set) +5. project `.kilo/` and `.kilocode/` directories from the current working directory up to the worktree root Use exact patterns first: @@ -42,7 +41,7 @@ If found, return the full path. If not found in those roots, explain that the co ## Agents (`.kilo/agent/*.md`) -Also loaded from `.kilocode/` and `.opencode/` directories (legacy), and plural `agents/` variants. +Also loaded from legacy `.kilocode/` directories and plural `agents/` variants. ```yaml --- @@ -257,7 +256,7 @@ Additional skill directories and remote URLs: } ``` -Skills are markdown files at `skills//SKILL.md` (or `skill//SKILL.md`) with `name` and `description` in frontmatter. Discovered inside `.kilo/`, `.kilocode/`, and `.opencode/` directories. +Skills are markdown files at `skills//SKILL.md` (or `skill//SKILL.md`) with `name` and `description` in frontmatter. Discovered inside `.kilo/` and legacy `.kilocode/` directories. ## Other Top-Level Fields @@ -340,14 +339,14 @@ Notification settings are managed through `kilo console` under **Settings > CLI | Global | `~/.config/kilo/kilo.json`, `~/.config/kilo/kilo.jsonc`, `~/.config/kilo/opencode.json` (legacy), `~/.config/kilo/opencode.jsonc` (legacy), `~/.config/kilo/config.json` (legacy) | | Managed | Linux: `/etc/kilo/`, macOS: `/Library/Application Support/kilo/`, Windows: `%ProgramData%\kilo\` — loads `kilo.json`, `kilo.jsonc`, `opencode.json`, `opencode.jsonc` (enterprise, highest priority) | -Each config directory (`.kilo/`, `.kilocode/`, `.opencode/`) can also contain `kilo.json`, `kilo.jsonc`, `opencode.json`, or `opencode.jsonc`. +Each config directory (`.kilo/` and legacy `.kilocode/`) can also contain `kilo.json`, `kilo.jsonc`, `opencode.json`, or `opencode.jsonc`. ### Config directories -Three directory names are scanned: `.kilo` (modern), `.kilocode` (legacy), `.opencode` (legacy). All three are checked at each level: +Two directory names are scanned: `.kilo` (canonical) and `.kilocode` (legacy fallback). Both are checked at each level, and `.kilo` wins when both define the same entry. `.opencode` directories are not loaded. -- **Project**: walks up from CWD to the git worktree root, checking for all three at each directory level -- **Home**: `~/.kilo/`, `~/.kilocode/`, `~/.opencode/` +- **Project**: walks up from CWD to the git worktree root, checking both directories at each level +- **Home**: `~/.kilo/` and `~/.kilocode/` - **XDG global**: `~/.config/kilo/` (always loaded, lowest file-based precedence) ### Commands, agents, modes, plugins @@ -361,7 +360,7 @@ Glob patterns run inside every discovered config directory (including legacy): | Mode | `{mode,modes}/*.md` | | Plugin | `{plugin,plugins}/*.{ts,js}` | -Example: `~/.config/kilo/command/*.md` (modern global), `~/.kilocode/command/*.md` (legacy global), `.opencode/commands/*.md` (legacy project) all load commands. +Example: `~/.config/kilo/command/*.md` (global), `~/.kilocode/command/*.md` (legacy home), and `.kilo/commands/*.md` (project) all load commands. ### Skills and instructions diff --git a/packages/opencode/src/kilocode/tui/config.ts b/packages/opencode/src/kilocode/tui/config.ts index a5d8beeeec..5d61466649 100644 --- a/packages/opencode/src/kilocode/tui/config.ts +++ b/packages/opencode/src/kilocode/tui/config.ts @@ -23,7 +23,7 @@ export namespace KilocodeTuiConfig { export type Editable = Omit & { keybinds?: Record } const files = ["tui.jsonc", "tui.json"] as const - const dirs = [".kilo", ".kilocode", ".opencode"] as const + const dirs = [".kilo", ".kilocode"] as const export async function get(input: { directory: string }) { const cfg = await Effect.runPromise( diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index f07b78bc36..f40916aa5a 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -334,7 +334,7 @@ function patchDir(input: PatchInput) { if (input.global) return input.config ?? Global.Path.config const git = input.vcs === "git" && input.worktree !== "/" const root = git ? input.worktree : input.directory - return path.join(root, ".opencode") + return path.join(root, ".kilo") // kilocode_change } function patchName(kind: Kind): "opencode" | "tui" { diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index c9f202fd9b..819766ac45 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -3,6 +3,7 @@ import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global" import { EffectBridge } from "@/effect/bridge" import { Bus } from "@/bus" import { Installation } from "@/installation" +import { disconnect } from "@/kilocode/server/sse" // kilocode_change import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle" import { InstallationVersion } from "@opencode-ai/core/installation/version" import * as Log from "@opencode-ai/core/util/log" @@ -33,7 +34,9 @@ function parseBody(body: string) { } } -function eventResponse() { +// kilocode_change start +function eventResponse(request: HttpServerRequest.HttpServerRequest) { + // kilocode_change end log.info("global event connected") const events = Stream.callback((queue) => { const handler = (event: GlobalBusEvent) => Queue.offerUnsafe(queue, event) @@ -53,6 +56,11 @@ function eventResponse() { Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()), Stream.encodeText, + // kilocode_change start - prevent disconnected SSE clients from retaining full diff payloads + // Explicit interruption closes the stream scope, unregisters its GlobalBus listener, and + // releases the unbounded callback queue even when transport cancellation is not propagated. + Stream.interruptWhen(disconnect(request)), + // kilocode_change end Stream.ensuring(Effect.sync(() => log.info("global event disconnected"))), ), { @@ -77,7 +85,8 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl }) const event = Effect.fn("GlobalHttpApi.event")(function* () { - return eventResponse() + const request = yield* HttpServerRequest.HttpServerRequest // kilocode_change + return eventResponse(request) // kilocode_change }) const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () { diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 6ffedd7e0a..152693c077 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -596,7 +596,7 @@ it.instance( () => Effect.gen(function* () { const test = yield* TestInstance - const skillDir = path.join(test.directory, ".opencode", "skill", "perm-skill") + const skillDir = path.join(test.directory, ".kilo", "skill", "perm-skill") // kilocode_change yield* Effect.promise(() => Bun.write( path.join(skillDir, "SKILL.md"), diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 0aed67e63d..df7d3b9b0d 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -168,9 +168,9 @@ async function load(): Promise { const invalidThemePath = path.join(dir, invalidThemeFile) const globalThemePath = path.join(dir, globalThemeFile) const preloadedThemePath = path.join(dir, preloadedThemeFile) - const localDest = path.join(dir, ".opencode", "themes", localThemeFile) + const localDest = path.join(dir, ".kilo", "themes", localThemeFile) // kilocode_change const globalDest = path.join(Global.Path.config, "themes", globalThemeFile) - const preloadedDest = path.join(dir, ".opencode", "themes", preloadedThemeFile) + const preloadedDest = path.join(dir, ".kilo", "themes", preloadedThemeFile) // kilocode_change const fnMarker = path.join(dir, "function-called.txt") const localMarker = path.join(dir, "local-called.json") const invalidMarker = path.join(dir, "invalid-called.json") @@ -549,7 +549,7 @@ export default { .then(() => true) .catch(() => false) const leaked_global_to_local = await fs - .stat(path.join(tmp.path, ".opencode", "themes", tmp.extra.globalThemeFile)) + .stat(path.join(tmp.path, ".kilo", "themes", tmp.extra.globalThemeFile)) // kilocode_change .then(() => true) .catch(() => false) @@ -1231,7 +1231,7 @@ test("updates installed theme when plugin metadata changes", async () => { const spec = pathToFileURL(pluginPath).href const themeFile = "theme-update.json" const themePath = path.join(dir, themeFile) - const dest = path.join(dir, ".opencode", "themes", themeFile) + const dest = path.join(dir, ".kilo", "themes", themeFile) // kilocode_change const themeName = themeFile.replace(/\.json$/, "") const configPath = path.join(dir, "tui.json") diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index b298f51a79..4b22d4bb87 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -817,7 +817,7 @@ it.instance("agent markdown permission config preserves user key order", () => Effect.gen(function* () { const test = yield* TestInstance yield* AppFileSystem.use.writeWithDirs( - path.join(test.directory, ".opencode", "agent", "ordered.md"), + path.join(test.directory, ".kilo", "agent", "ordered.md"), // kilocode_change `--- permission: bash: allow diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 5695da5534..1506e0e84f 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -87,7 +87,7 @@ it.instance("keeps server and tui plugin merge semantics aligned", () => Effect.gen(function* () { const fs = yield* AppFileSystem.Service const test = yield* TestInstance - const local = path.join(test.directory, ".opencode") + const local = path.join(test.directory, ".kilo") // kilocode_change yield* fs.makeDirectory(local, { recursive: true }) yield* fs.writeJson(path.join(Global.Path.config, "kilo.json"), { @@ -129,7 +129,7 @@ it.instance("loads tui config with the same precedence order as server config pa yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { theme: "global" }) yield* fs.writeJson(path.join(test.directory, "tui.json"), { theme: "project" }) yield* fs.writeWithDirs( - path.join(test.directory, ".opencode", "tui.json"), + path.join(test.directory, ".kilo", "tui.json"), // kilocode_change JSON.stringify({ theme: "local", diff_style: "stacked" }, null, 2), ) @@ -854,7 +854,7 @@ it.instance("silently skips malformed tui.json - load failures degrade to {}", ( const fs = yield* AppFileSystem.Service const test = yield* TestInstance yield* fs.writeFileString(path.join(test.directory, "tui.json"), '{ "theme": "broken",') - yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" })) + yield* fs.writeWithDirs(path.join(test.directory, ".kilo", "tui.json"), JSON.stringify({ theme: "fallback" })) // kilocode_change const config = yield* getTuiConfig(test.directory) expect(config.theme).toBe("fallback") @@ -868,7 +868,7 @@ it.instance("silently skips non-ENOENT read failures (e.g. tui.json is a directo const fs = yield* AppFileSystem.Service const test = yield* TestInstance yield* fs.makeDirectory(path.join(test.directory, "tui.json"), { recursive: true }) - yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" })) + yield* fs.writeWithDirs(path.join(test.directory, ".kilo", "tui.json"), JSON.stringify({ theme: "fallback" })) // kilocode_change const config = yield* getTuiConfig(test.directory) expect(config.theme).toBe("fallback") diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index f3e6149b5c..e405398721 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -413,6 +413,96 @@ describe("agent config", () => { }) }) +describe("project config directory precedence", () => { + test("prefers .kilo over legacy .kilocode and ignores .opencode", async () => { + await using tmp = await tmpdir() + const entries = [ + { + root: ".opencode", + source: "opencode", + config: { + username: "opencode", + model: "test/opencode", + small_model: "test/opencode", + }, + names: ["shared", "legacy", "opencode-only"], + }, + { + root: ".kilocode", + source: "kilocode", + config: { + username: "kilocode", + model: "test/kilocode", + }, + names: ["shared", "legacy"], + }, + { + root: ".kilo", + source: "kilo", + config: { + username: "kilo", + }, + names: ["shared"], + }, + ] as const + + for (const item of entries) { + const dir = path.join(tmp.path, item.root) + await writeConfig(dir, { + $schema: "https://app.kilo.ai/config.json", + ...item.config, + }) + for (const name of item.names) { + await Filesystem.write( + path.join(dir, "command", `${name}.md`), + `---\ndescription: ${item.source} command\n---\n${item.source} command template`, + ) + await Filesystem.write( + path.join(dir, "agent", `${name}.md`), + `---\ndescription: ${item.source} agent\nmode: subagent\n---\n${item.source} agent prompt`, + ) + } + await Filesystem.write(path.join(dir, "plugin", `${item.source}.ts`), "export default {}") + } + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + + expect(config.username).toBe("kilo") + expect(config.model).toBe("test/kilocode") + expect(config.small_model).toBeUndefined() + + expect(config.command?.shared).toMatchObject({ + description: "kilo command", + template: "kilo command template", + }) + expect(config.command?.legacy).toMatchObject({ + description: "kilocode command", + template: "kilocode command template", + }) + expect(config.command?.["opencode-only"]).toBeUndefined() + + expect(config.agent?.shared).toMatchObject({ + description: "kilo agent", + prompt: "kilo agent prompt", + }) + expect(config.agent?.legacy).toMatchObject({ + description: "kilocode agent", + prompt: "kilocode agent prompt", + }) + expect(config.agent?.["opencode-only"]).toBeUndefined() + + const plugins = JSON.stringify(config.plugin) + expect(plugins).toContain("kilocode.ts") + expect(plugins).toContain("kilo.ts") + expect(plugins).not.toContain("opencode.ts") + }, + }) + }) +}) + describe("linked worktree config", () => { test("uses primary config directories as local fallbacks", async () => { await using primary = await tmpdir({ git: true }) @@ -448,6 +538,10 @@ describe("linked worktree config", () => { try { await Bun.write(path.join(directory, "placeholder"), "") + await Bun.write( + path.join(primary.path, "packages", ".opencode", "kilo.jsonc"), + JSON.stringify({ snapshot: true, autoupdate: false, share: "auto", default_agent: "opencode-only" }), + ) await Bun.write( path.join(primary.path, "packages", ".kilocode", "kilo.jsonc"), JSON.stringify({ snapshot: true, autoupdate: "notify", share: "disabled" }), @@ -460,6 +554,7 @@ describe("linked worktree config", () => { expect(config.snapshot).toBe(false) expect(config.autoupdate).toBe("notify") expect(config.share).toBe("manual") + expect(config.default_agent).toBeUndefined() } finally { await $`git worktree remove --force ${worktree}`.cwd(primary.path).quiet().nothrow() } diff --git a/packages/opencode/test/kilocode/plugin-dependencies.ts b/packages/opencode/test/kilocode/plugin-dependencies.ts index 79c4fc3249..175b7bb67e 100644 --- a/packages/opencode/test/kilocode/plugin-dependencies.ts +++ b/packages/opencode/test/kilocode/plugin-dependencies.ts @@ -11,5 +11,5 @@ async function mark(dir: string) { } export function preparePluginDependencies(dir: string) { - return Promise.all([mark(path.join(dir, ".opencode")), mark(Global.Path.config)]) + return Promise.all([mark(path.join(dir, ".kilo")), mark(Global.Path.config)]) } diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts index aa626da999..6767b9fa01 100644 --- a/packages/opencode/test/kilocode/server/config-overlay.test.ts +++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts @@ -7,6 +7,7 @@ import { Config } from "../../../src/config/config" import { KilocodeConfigOverlay } from "../../../src/kilocode/config/overlay" import { Permission } from "../../../src/permission" import { PtyPaths } from "../../../src/server/routes/instance/httpapi/groups/pty" +import { Filesystem } from "../../../src/util/filesystem" import { resetDatabase } from "../../fixture/db" import { disposeAllInstances, tmpdir } from "../../fixture/fixture" @@ -91,6 +92,58 @@ describe("config overlay routes", () => { expect(Object.hasOwn(patched, "prototype")).toBe(false) }) + test("prefers .kilo over legacy .kilocode and ignores .opencode in project overlays", async () => { + await using project = await tmpdir() + const entries = [ + { + root: ".opencode", + source: "opencode", + value: { username: "opencode", model: "test/opencode", small_model: "test/opencode" }, + }, + { + root: ".kilocode", + source: "kilocode", + value: { username: "kilocode", model: "test/kilocode" }, + }, + { + root: ".kilo", + source: "kilo", + value: { username: "kilo" }, + }, + ] as const + + for (const item of entries) { + const dir = path.join(project.path, item.root) + await Filesystem.write(path.join(dir, "kilo.json"), JSON.stringify(item.value)) + await Filesystem.write( + path.join(dir, "agent", "shared.md"), + `---\ndescription: ${item.source} agent\nmode: subagent\n---\n${item.source} agent prompt`, + ) + } + await Filesystem.write( + path.join(project.path, ".opencode", "agent", "opencode-only.md"), + "---\ndescription: opencode-only agent\nmode: subagent\n---\nopencode-only agent prompt", + ) + + const body = await KilocodeConfigOverlay.resolve({ + directory: project.path, + scope: "project", + effective: {}, + global: {}, + sources: [], + }) + + expect(body.project.username).toBe("kilo") + expect(body.project.model).toBe("test/kilocode") + expect(body.project.small_model).toBeUndefined() + expect(body.project.agent?.shared).toMatchObject({ + description: "kilo agent", + prompt: "kilo agent prompt", + }) + expect(body.project.agent?.["opencode-only"]).toBeUndefined() + expect(body.targets.project).toBe(path.join(project.path, ".kilo", "kilo.json")) + }) + test.serial("marks global values inherited in project scope", async () => { await using global = await tmpdir() await using project = await tmpdir() diff --git a/packages/opencode/test/kilocode/server/config-sources.test.ts b/packages/opencode/test/kilocode/server/config-sources.test.ts index 13c22d1a5a..660389117b 100644 --- a/packages/opencode/test/kilocode/server/config-sources.test.ts +++ b/packages/opencode/test/kilocode/server/config-sources.test.ts @@ -78,9 +78,11 @@ describe("config source routes", () => { await Bun.write(path.join(dir, "env.json"), "{}") await Bun.write(path.join(dir, "kilo.json"), "{}") - const local = path.join(dir, ".kilo") - await fs.mkdir(local, { recursive: true }) - await Bun.write(path.join(local, "kilo.jsonc"), "{}") + for (const root of [".opencode", ".kilocode", ".kilo"]) { + const local = path.join(dir, root) + await fs.mkdir(local, { recursive: true }) + await Bun.write(path.join(local, "kilo.jsonc"), "{}") + } const extra = path.join(dir, "extra") await fs.mkdir(extra, { recursive: true }) @@ -94,6 +96,8 @@ describe("config source routes", () => { const envFile = path.join(tmp.path, "env.json") const projectFile = path.join(tmp.path, "kilo.json") + const opencodeFile = path.join(tmp.path, ".opencode", "kilo.jsonc") + const kilocodeFile = path.join(tmp.path, ".kilocode", "kilo.jsonc") const configFile = path.join(tmp.path, ".kilo", "kilo.jsonc") const extraFile = path.join(tmp.path, "extra", "opencode.json") const managedFile = path.join(tmp.path, "managed", "kilo.json") @@ -108,7 +112,9 @@ describe("config source routes", () => { const inline = body.sources.find((source) => source.source === "KILO_CONFIG_CONTENT") expect(order(body, envFile)).toBeLessThan(order(body, projectFile)) - expect(order(body, projectFile)).toBeLessThan(order(body, configFile)) + expect(order(body, projectFile)).toBeLessThan(order(body, kilocodeFile)) + expect(order(body, kilocodeFile)).toBeLessThan(order(body, configFile)) + expect(body.sources.some((source) => source.path === opencodeFile)).toBe(false) expect(order(body, configFile)).toBeLessThan(order(body, extraFile)) expect(inline?.order).toBeGreaterThan(order(body, extraFile)) expect(inline?.order).toBeLessThan(order(body, managedFile)) diff --git a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts index 0f631b66c2..dcfe8cc85c 100644 --- a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts +++ b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts @@ -304,10 +304,10 @@ export const kiloScenarios: Scenario[] = [ Effect.gen(function* () { const location = yield* file( ctx, - ".opencode/skill/httpapi-remove/SKILL.md", + ".kilo/skill/httpapi-remove/SKILL.md", "---\nname: httpapi-remove\ndescription: HTTP API removal fixture.\n---\n# HTTP API remove\n", ) - const sentinel = yield* file(ctx, ".opencode/skill/httpapi-remove/KEEP.txt", "synthetic sentinel\n") + const sentinel = yield* file(ctx, ".kilo/skill/httpapi-remove/KEEP.txt", "synthetic sentinel\n") return { location, sentinel } }), ) @@ -332,9 +332,7 @@ export const kiloScenarios: Scenario[] = [ http.protected .post("/kilocode/agent/remove", "kilocode.removeAgent") .mutating() - .seeded((ctx) => - file(ctx, ".opencode/agent/httpapi-remove.md", "---\ndescription: HTTP API remove\n---\nRemove me.\n"), - ) + .seeded((ctx) => file(ctx, ".kilo/agent/httpapi-remove.md", "---\ndescription: HTTP API remove\n---\nRemove me.\n")) .at((ctx) => ({ path: "/kilocode/agent/remove", headers: ctx.headers(), body: { name: "httpapi-remove" } })) .jsonEffect(200, (body, ctx) => Effect.gen(function* () { diff --git a/packages/opencode/test/kilocode/server/httpapi-global-sse.test.ts b/packages/opencode/test/kilocode/server/httpapi-global-sse.test.ts new file mode 100644 index 0000000000..9f2d2f78fa --- /dev/null +++ b/packages/opencode/test/kilocode/server/httpapi-global-sse.test.ts @@ -0,0 +1,68 @@ +import { NodeHttpServer } from "@effect/platform-node" +import { describe, expect } from "bun:test" +import { Context, Effect, Fiber, Layer, Option, Stream } from "effect" +import { HttpClient, HttpRouter } from "effect/unstable/http" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Auth } from "../../../src/auth" +import { GlobalBus } from "../../../src/bus/global" +import { Config } from "../../../src/config/config" +import { Installation } from "../../../src/installation" +import { ServerAuth } from "../../../src/server/auth" +import { RootHttpApi } from "../../../src/server/routes/instance/httpapi/api" +import { GlobalPaths } from "../../../src/server/routes/instance/httpapi/groups/global" +import { controlHandlers } from "../../../src/server/routes/instance/httpapi/handlers/control" +import { globalHandlers } from "../../../src/server/routes/instance/httpapi/handlers/global" +import { authorizationLayer } from "../../../src/server/routes/instance/httpapi/middleware/authorization" +import { schemaErrorLayer } from "../../../src/server/routes/instance/httpapi/middleware/schema-error" +import { pollWithTimeout, testEffect } from "../../lib/effect" + +const apiLayer = HttpRouter.serve( + HttpApiBuilder.layer(RootHttpApi).pipe( + Layer.provide([controlHandlers, globalHandlers]), + Layer.provide([authorizationLayer, schemaErrorLayer]), + ), + { disableListenLog: true, disableLogger: true }, +).pipe( + Layer.provideMerge(NodeHttpServer.layerTest), + Layer.provide(Layer.mock(Auth.Service)({})), + Layer.provide(Layer.mock(Config.Service)({})), + Layer.provide( + Layer.mock(Installation.Service)({ + method: () => Effect.succeed("npm"), + latest: () => Effect.succeed("9.9.9"), + upgrade: () => Effect.void, + }), + ), + Layer.provide(ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })), + // Raw HttpApi routes expose an opaque handler context at the web boundary. + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion + Layer.provide(Layer.succeedContext(Context.empty() as Context.Context)), +) +const it = testEffect(apiLayer) + +describe("global SSE lifecycle", () => { + it.live( + "removes event listeners after clients disconnect", + () => + Effect.gen(function* () { + const count = GlobalBus.listenerCount("event") + + for (const attempt of [1, 2, 3]) { + const response = yield* HttpClient.get(GlobalPaths.event) + expect(response.status).toBe(200) + const fiber = yield* response.stream.pipe(Stream.runDrain, Effect.forkChild({ startImmediately: true })) + + yield* pollWithTimeout( + Effect.sync(() => (GlobalBus.listenerCount("event") === count + 1 ? true : undefined)), + `global event stream ${attempt} did not subscribe`, + ) + yield* Fiber.interrupt(fiber) + yield* pollWithTimeout( + Effect.sync(() => (GlobalBus.listenerCount("event") === count ? true : undefined)), + `global event stream ${attempt} did not remove its listener`, + ) + } + }), + 15_000, + ) +}) diff --git a/packages/opencode/test/kilocode/server/tui-config.test.ts b/packages/opencode/test/kilocode/server/tui-config.test.ts index 2bba0a2f92..22909b26fe 100644 --- a/packages/opencode/test/kilocode/server/tui-config.test.ts +++ b/packages/opencode/test/kilocode/server/tui-config.test.ts @@ -43,6 +43,25 @@ describe("TUI config routes", () => { expect(body.plugin_origins).toBeUndefined() }) + test("loads legacy .kilocode TUI config and ignores .opencode", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + await fs.mkdir(path.join(dir, ".kilocode"), { recursive: true }) + await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ theme: "dracula" })) + await Bun.write(path.join(dir, ".kilocode", "tui.json"), JSON.stringify({ theme: "nord" })) + }, + }) + + const response = await Server.Default().app.request("/tui/config", { + headers: { "x-kilo-directory": tmp.path }, + }) + + expect(response.status).toBe(200) + const body = (await response.json()) as { theme?: string } + expect(body.theme).toBe("nord") + }) + test("lists valid TUI keybinds", async () => { await using tmp = await tmpdir() diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index 298180e73a..092716c7e5 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -51,7 +51,7 @@ describe("plugin.auth-override", () => { Effect.gen(function* () { const tmp = yield* TestInstance const fs = yield* AppFileSystem.Service - const pluginDir = path.join(tmp.directory, ".opencode", "plugin") + const pluginDir = path.join(tmp.directory, ".kilo", "plugin") // kilocode_change yield* fs.writeWithDirs( path.join(pluginDir, "custom-copilot-auth.ts"), diff --git a/packages/opencode/test/plugin/install-concurrency.test.ts b/packages/opencode/test/plugin/install-concurrency.test.ts index 6160bd6e25..728d8b6dbe 100644 --- a/packages/opencode/test/plugin/install-concurrency.test.ts +++ b/packages/opencode/test/plugin/install-concurrency.test.ts @@ -82,7 +82,7 @@ describe("plugin.install.concurrent", () => { expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0)) expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([]) - const cfg = await read(path.join(tmp.path, ".opencode", "opencode.jsonc")) + const cfg = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) // kilocode_change expectPlugins(cfg.plugin, all) }, 25_000) @@ -105,8 +105,10 @@ describe("plugin.install.concurrent", () => { expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0)) expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([]) - const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc")) - const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc")) + // kilocode_change start + const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) + const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc")) + // kilocode_change end expectPlugins(server.plugin, all) expectPlugins(tui.plugin, all) }, 25_000) @@ -114,7 +116,7 @@ describe("plugin.install.concurrent", () => { test("preserves updates when existing config uses .json", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.json") + const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write(cfg, JSON.stringify({ plugin: ["seed@1.0.0"] }, null, 2)) @@ -135,6 +137,6 @@ describe("plugin.install.concurrent", () => { const json = await read(cfg) expectPlugins(json.plugin, ["seed@1.0.0", ...next]) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change }, 25_000) }) diff --git a/packages/opencode/test/plugin/install.test.ts b/packages/opencode/test/plugin/install.test.ts index 6dc9175be4..4a05be769a 100644 --- a/packages/opencode/test/plugin/install.test.ts +++ b/packages/opencode/test/plugin/install.test.ts @@ -122,8 +122,10 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(true) - const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc")) - const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc")) + // kilocode_change start + const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) + const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc")) + // kilocode_change end expect(server.plugin).toEqual(["acme@1.2.3"]) expect(tui.plugin).toEqual(["acme@1.2.3"]) }) @@ -144,8 +146,10 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(true) - const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc")) - const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc")) + // kilocode_change start + const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) + const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc")) + // kilocode_change end expect(server.plugin).toEqual([["acme@1.2.3", { custom: true, other: false }]]) expect(tui.plugin).toEqual([["acme@1.2.3", { compact: true }]]) }) @@ -153,7 +157,7 @@ describe("plugin.install.task", () => { test("preserves JSONC comments when adding plugins to server and tui config", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server", "tui"]) - const cfg = path.join(tmp.path, ".opencode") + const cfg = path.join(tmp.path, ".kilo") // kilocode_change const server = path.join(cfg, "opencode.jsonc") const tui = path.join(cfg, "tui.jsonc") await fs.mkdir(cfg, { recursive: true }) @@ -212,7 +216,7 @@ describe("plugin.install.task", () => { test("preserves JSONC comments when force replacing plugin version", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.jsonc") + const cfg = path.join(tmp.path, ".kilo", "opencode.jsonc") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write( cfg, @@ -257,14 +261,14 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(true) - const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc")) + const server = await read(path.join(tmp.path, ".kilo", "opencode.jsonc")) // kilocode_change expect(server.plugin).toEqual(["acme@1.2.3"]) }) test("does not change configured package version without force", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.json") + const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write(cfg, JSON.stringify({ plugin: ["acme@1.0.0"] }, null, 2)) @@ -284,7 +288,7 @@ describe("plugin.install.task", () => { test("does not change scoped package version without force", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.json") + const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write(cfg, JSON.stringify({ plugin: ["@scope/acme@1.0.0"] }, null, 2)) @@ -304,7 +308,7 @@ describe("plugin.install.task", () => { test("keeps file plugin entries and still adds npm plugin", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.json") + const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write(cfg, JSON.stringify({ plugin: ["file:///tmp/acme.ts"] }, null, 2)) @@ -324,7 +328,7 @@ describe("plugin.install.task", () => { test("force replaces configured package version and keeps tuple options", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.json") + const cfg = path.join(tmp.path, ".kilo", "opencode.json") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) await Bun.write( cfg, @@ -367,7 +371,7 @@ describe("plugin.install.task", () => { expect(ok).toBe(true) expect(await Filesystem.exists(path.join(global, "opencode.jsonc"))).toBe(true) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change }) test("writes local scope under directory when vcs is not git", async () => { @@ -386,8 +390,10 @@ describe("plugin.install.task", () => { const ok = await run(ctxDir(directory, worktree)) expect(ok).toBe(true) - expect(await Filesystem.exists(path.join(directory, ".opencode", "opencode.jsonc"))).toBe(true) - expect(await Filesystem.exists(path.join(worktree, ".opencode", "opencode.jsonc"))).toBe(false) + // kilocode_change start + expect(await Filesystem.exists(path.join(directory, ".kilo", "opencode.jsonc"))).toBe(true) + expect(await Filesystem.exists(path.join(worktree, ".kilo", "opencode.jsonc"))).toBe(false) + // kilocode_change end }) test("writes local scope under directory when worktree is root slash", async () => { @@ -404,7 +410,7 @@ describe("plugin.install.task", () => { const ok = await run(ctxRoot(directory)) expect(ok).toBe(true) - expect(await Filesystem.exists(path.join(directory, ".opencode", "opencode.jsonc"))).toBe(true) + expect(await Filesystem.exists(path.join(directory, ".kilo", "opencode.jsonc"))).toBe(true) // kilocode_change }) test("writes tui local scope under directory when worktree is root slash", async () => { @@ -421,7 +427,7 @@ describe("plugin.install.task", () => { const ok = await run(ctxRoot(directory)) expect(ok).toBe(true) - expect(await Filesystem.exists(path.join(directory, ".opencode", "tui.jsonc"))).toBe(true) + expect(await Filesystem.exists(path.join(directory, ".kilo", "tui.jsonc"))).toBe(true) // kilocode_change }) test("writes only tui config for tui-only plugins", async () => { @@ -436,8 +442,10 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(true) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(true) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + // kilocode_change start + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(true) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) + // kilocode_change end }) test("writes tui config for oc-themes-only packages", async () => { @@ -454,10 +462,12 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(true) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(true) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + // kilocode_change start + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(true) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) + // kilocode_change end - const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc")) + const tui = await read(path.join(tmp.path, ".kilo", "tui.jsonc")) // kilocode_change expect(tui.plugin).toEqual(["acme@1.2.3"]) }) @@ -473,15 +483,19 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + // kilocode_change start + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) + // kilocode_change end }) test("force replaces version in both server and tui configs", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server", "tui"]) - const server = path.join(tmp.path, ".opencode", "opencode.json") - const tui = path.join(tmp.path, ".opencode", "tui.json") + // kilocode_change start + const server = path.join(tmp.path, ".kilo", "opencode.json") + const tui = path.join(tmp.path, ".kilo", "tui.json") + // kilocode_change end await fs.mkdir(path.dirname(server), { recursive: true }) await Bun.write(server, JSON.stringify({ plugin: ["acme@1.0.0", "other@1.0.0"] }, null, 2)) await Bun.write(tui, JSON.stringify({ plugin: [["acme@1.0.0", { mode: "safe" }], "other@1.0.0"] }, null, 2)) @@ -505,7 +519,7 @@ describe("plugin.install.task", () => { test("returns false and keeps config unchanged for invalid JSONC", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server"]) - const cfg = path.join(tmp.path, ".opencode", "opencode.jsonc") + const cfg = path.join(tmp.path, ".kilo", "opencode.jsonc") // kilocode_change await fs.mkdir(path.dirname(cfg), { recursive: true }) const bad = '{"plugin": ["acme@1.0.0",}' await Bun.write(cfg, bad) @@ -534,8 +548,10 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "tui.jsonc"))).toBe(false) + // kilocode_change start + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "tui.jsonc"))).toBe(false) + // kilocode_change end }) test("returns false when manifest cannot be read", async () => { @@ -551,7 +567,7 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change }) test("returns false when install fails", async () => { @@ -565,6 +581,6 @@ describe("plugin.install.task", () => { const ok = await run(ctx(tmp.path)) expect(ok).toBe(false) - expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, ".kilo", "opencode.jsonc"))).toBe(false) // kilocode_change }) }) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 07721ed9d5..cbd0ba4469 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1640,7 +1640,7 @@ const provideMultiInstance = (eff: Effect.Effect) => it.effect("plugin config providers persist after instance dispose", () => Effect.gen(function* () { const dir = yield* tmpdirScoped() - const configDir = path.join(dir, ".opencode") + const configDir = path.join(dir, ".kilo") // kilocode_change const root = path.join(configDir, "plugin") yield* Effect.promise(() => mkdir(root, { recursive: true })) yield* Effect.promise(() => markPluginDependenciesReady(configDir)) @@ -1697,7 +1697,7 @@ it.instance( "plugin config enabled and disabled providers are honored", Effect.gen(function* () { const instance = yield* TestInstance - const configDir = path.join(instance.directory, ".opencode") + const configDir = path.join(instance.directory, ".kilo") // kilocode_change const root = path.join(configDir, "plugin") yield* Effect.promise(() => mkdir(root, { recursive: true })) yield* Effect.promise(() => markPluginDependenciesReady(configDir)) diff --git a/packages/opencode/test/server/httpapi-provider.test.ts b/packages/opencode/test/server/httpapi-provider.test.ts index d1ca0fe463..a2a6535f0d 100644 --- a/packages/opencode/test/server/httpapi-provider.test.ts +++ b/packages/opencode/test/server/httpapi-provider.test.ts @@ -122,7 +122,7 @@ function writeProviderAuthPlugin(dir: string) { yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change yield* fs.writeWithDirs( - path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"), + path.join(dir, ".kilo", "plugin", "provider-oauth-parity.ts"), // kilocode_change [ "export default {", ' id: "test.provider-oauth-parity",', @@ -157,7 +157,7 @@ function writeProviderAuthValidationPlugin(dir: string) { yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change yield* fs.writeWithDirs( - path.join(dir, ".opencode", "plugin", "provider-oauth-validation.ts"), + path.join(dir, ".kilo", "plugin", "provider-oauth-validation.ts"), // kilocode_change [ "export default {", ' id: "test.provider-oauth-validation",', @@ -199,7 +199,7 @@ function writeFunctionOptionsPlugin(dir: string) { yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change yield* fs.writeWithDirs( - path.join(dir, ".opencode", "plugin", "provider-function-options.ts"), + path.join(dir, ".kilo", "plugin", "provider-function-options.ts"), // kilocode_change [ "export default {", ' id: "test.provider-function-options",', @@ -231,7 +231,7 @@ function writeProviderModelsMutationPlugin(dir: string) { yield* Effect.promise(() => preparePluginDependencies(dir)) // kilocode_change yield* fs.writeWithDirs( - path.join(dir, ".opencode", "plugin", "provider-models-mutation.ts"), + path.join(dir, ".kilo", "plugin", "provider-models-mutation.ts"), // kilocode_change [ "export default {", ' id: "test.provider-models-mutation",', diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index 7962d18e86..8f3179ebba 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -294,7 +294,7 @@ function writeStandardFiles(dir: string) { function writeProjectSkill(dir: string) { return AppFileSystem.Service.use((fs) => fs.writeWithDirs( - path.join(dir, ".opencode", "skills", "project-rest-skill", "SKILL.md"), + path.join(dir, ".kilo", "skills", "project-rest-skill", "SKILL.md"), // kilocode_change `--- name: project-rest-skill description: A project skill visible to REST API prompts. diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 7cae92b8b2..318344d1d8 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -194,10 +194,12 @@ Just some content without YAML frontmatter. provideTmpdirInstance( (dir) => Effect.gen(function* () { + // kilocode_change start - load .kilo skills without falling back to .opencode yield* Effect.promise(() => - Bun.write( - path.join(dir, ".opencode", "skill", "manual-skill", "SKILL.md"), - `--- + Promise.all([ + Bun.write( + path.join(dir, ".kilo", "skill", "manual-skill", "SKILL.md"), + `--- name: manual-skill --- @@ -205,8 +207,18 @@ name: manual-skill Instructions here. `, - ), + ), + Bun.write( + path.join(dir, ".opencode", "skill", "ignored-skill", "SKILL.md"), + `--- +name: ignored-skill +description: This skill must not load. +--- +`, + ), + ]), ) + // kilocode_change end const skill = yield* Skill.Service const list = discovered(yield* skill.all()) // kilocode_change @@ -485,16 +497,18 @@ description: A skill in the .agents/skills directory. # Agent Skill `, ), + // kilocode_change start Bun.write( - path.join(dir, ".opencode", "skill", "opencode-skill", "SKILL.md"), + path.join(dir, ".kilo", "skill", "opencode-skill", "SKILL.md"), `--- name: opencode-skill -description: A skill in the .opencode/skill directory. +description: A skill in the .kilo/skill directory. --- # OpenCode Skill `, ), + // kilocode_change end ]), ) @@ -532,26 +546,28 @@ description: A skill in the .agents/skills directory. # Agent Skill `, ), + // kilocode_change start Bun.write( - path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"), + path.join(dir, ".kilo", "skill", "agent-skill", "SKILL.md"), `--- name: opencode-skill -description: A skill in the .opencode/skill directory. +description: A skill in the .kilo/skill directory. --- # OpenCode Skill `, ), Bun.write( - path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"), + path.join(dir, ".kilo", "skills", "agent-skill", "SKILL.md"), `--- name: opencode-skill -description: A skill in the .opencode/skills directory. +description: A skill in the .kilo/skills directory. --- # OpenCode Skill `, ), + // kilocode_change end ]), ) diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 7dd6bd7aa6..d9a50c80d8 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -40,7 +40,7 @@ import { run as runSandbox, type Profile } from "@kilocode/sandbox" // kilocode_ const node = CrossSpawnSpawner.defaultLayer const configLayer = TestConfig.layer({ - directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])), + directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".kilo")])), // kilocode_change }) type RegistryLayerOptions = { @@ -206,10 +206,10 @@ describe("tool.registry", () => { }), ) - it.instance("loads tools from .opencode/tool (singular)", () => + it.instance("loads tools from .kilo/tool (singular)" /* kilocode_change */, () => Effect.gen(function* () { const test = yield* TestInstance - const opencode = path.join(test.directory, ".opencode") + const opencode = path.join(test.directory, ".kilo") // kilocode_change const tool = path.join(opencode, "tool") yield* Effect.promise(() => fs.mkdir(tool, { recursive: true })) yield* Effect.promise(() => @@ -233,10 +233,10 @@ describe("tool.registry", () => { }), ) - it.instance("ignores non-tool exports in .opencode/tool files", () => + it.instance("ignores non-tool exports in .kilo/tool files" /* kilocode_change */, () => Effect.gen(function* () { const test = yield* TestInstance - const tool = path.join(test.directory, ".opencode", "tool") + const tool = path.join(test.directory, ".kilo", "tool") // kilocode_change yield* Effect.promise(() => fs.mkdir(tool, { recursive: true })) yield* Effect.promise(() => Bun.write( @@ -269,7 +269,7 @@ describe("tool.registry", () => { it.instance("tolerates a custom tool exporting null/undefined args (no-args fallback)", () => Effect.gen(function* () { const test = yield* TestInstance - const tool = path.join(test.directory, ".opencode", "tool") + const tool = path.join(test.directory, ".kilo", "tool") // kilocode_change yield* Effect.promise(() => fs.mkdir(tool, { recursive: true })) yield* Effect.promise(() => Bun.write( @@ -311,10 +311,10 @@ describe("tool.registry", () => { }), ) - it.instance("loads tools from .opencode/tools (plural)", () => + it.instance("loads tools from .kilo/tools (plural)" /* kilocode_change */, () => Effect.gen(function* () { const test = yield* TestInstance - const opencode = path.join(test.directory, ".opencode") + const opencode = path.join(test.directory, ".kilo") // kilocode_change const tools = path.join(opencode, "tools") yield* Effect.promise(() => fs.mkdir(tools, { recursive: true })) yield* Effect.promise(() => @@ -341,7 +341,7 @@ describe("tool.registry", () => { it.instance("loads Zod-schema custom tools with JSON Schema and validation", () => Effect.gen(function* () { const test = yield* TestInstance - const customTools = path.join(test.directory, ".opencode", "tools") + const customTools = path.join(test.directory, ".kilo", "tools") // kilocode_change const pluginTool = pathToFileURL(path.resolve(import.meta.dir, "../../../plugin/src/tool.ts")).href yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true })) yield* Effect.promise(() => @@ -394,7 +394,7 @@ describe("tool.registry", () => { () => Effect.gen(function* () { const test = yield* TestInstance - const opencode = path.join(test.directory, ".opencode") + const opencode = path.join(test.directory, ".kilo") // kilocode_change const customTools = path.join(opencode, "tools") const plugin = path.join(opencode, "node_modules", "@kilocode", "plugin") // kilocode_change yield* Effect.promise(() => fs.mkdir(path.join(plugin, "dist"), { recursive: true })) @@ -459,7 +459,7 @@ describe("tool.registry", () => { it.instance("preserves attachments from structured custom tool results", () => Effect.gen(function* () { const test = yield* TestInstance - const customTools = path.join(test.directory, ".opencode", "tools") + const customTools = path.join(test.directory, ".kilo", "tools") // kilocode_change const pluginTool = pathToFileURL(path.resolve(import.meta.dir, "../../../plugin/src/tool.ts")).href yield* Effect.promise(() => fs.mkdir(customTools, { recursive: true })) yield* Effect.promise(() => @@ -504,7 +504,7 @@ describe("tool.registry", () => { it.instance("loads legacy JSON-schema-shaped custom tools with wire schema", () => Effect.gen(function* () { const test = yield* TestInstance - const tools = path.join(test.directory, ".opencode", "tools") + const tools = path.join(test.directory, ".kilo", "tools") // kilocode_change yield* Effect.promise(() => fs.mkdir(tools, { recursive: true })) yield* Effect.promise(() => Bun.write( @@ -536,7 +536,7 @@ describe("tool.registry", () => { it.instance("loads tools with external dependencies without crashing", () => Effect.gen(function* () { const test = yield* TestInstance - const opencode = path.join(test.directory, ".opencode") + const opencode = path.join(test.directory, ".kilo") // kilocode_change const tools = path.join(opencode, "tools") yield* Effect.promise(() => fs.mkdir(tools, { recursive: true })) yield* Effect.promise(() => diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index df5378a74d..5b85895ae5 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -36,7 +36,7 @@ describe("tool.skill", () => { unix("execute returns skill content block with files", () => provideTmpdirInstance((dir) => Effect.gen(function* () { - const skill = path.join(dir, ".opencode", "skill", "tool-skill") + const skill = path.join(dir, ".kilo", "skill", "tool-skill") // kilocode_change yield* Effect.promise(() => Bun.write( path.join(skill, "SKILL.md"),