mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge branch 'main' into vagabond-melody
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Stop loading `.opencode` config directories and use `.kilo` instead, while retaining `.kilocode` as a legacy fallback.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Release disconnected event streams so long-running servers do not retain queued session diffs.
|
||||
@@ -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/`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ A **rig identity** is how your Gas Town is addressed on the Wasteland. It's comp
|
||||
|
||||
<!-- TODO: verify — confirm whether rig handle changes are disallowed or just discouraged -->
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -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 <your-handle>`.
|
||||
4. Click **Connect**.
|
||||
|
||||
Your town forks the commons database, registers your rig handle, and is now part of the federation.
|
||||
|
||||
<!-- TODO(screenshots): replace placeholder with real UI capture -->
|
||||
{% 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" %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 372 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:da91a2dd21f1b647d3d7e95756755d5e8357bbba7f964f328d1f58363990e2cd
|
||||
size 12377
|
||||
oid sha256:e2b861de27fb8e6d64becd50bb4b3b05d748fba74b98aafaf5cb2a182ff74655
|
||||
size 26259
|
||||
|
||||
@@ -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<WorktreeItemProps> = (props) => {
|
||||
>
|
||||
{(pr) => {
|
||||
const accent = () => prAccentColor(pr())
|
||||
const indicator = () => prBadgeIndicator(pr())
|
||||
return (
|
||||
<span
|
||||
class="am-pr-badge"
|
||||
style={{ "--pr-accent": accent() }}
|
||||
data-pending={pr().state === "open" && pr().checks.status === "pending" ? "" : undefined}
|
||||
data-pending={prChecksRunning(pr()) ? "" : undefined}
|
||||
onClick={handleOpenPR}
|
||||
>
|
||||
<Icon name={pr().review === "approved" ? "check-small" : "branch"} size="small" />
|
||||
<Switch fallback={<Icon name="branch" size="small" />}>
|
||||
<Match when={indicator() === "failure"}>
|
||||
<Icon name="circle-x" size="small" class="am-pr-badge-status" data-status="failure" />
|
||||
</Match>
|
||||
<Match when={indicator() === "changes"}>
|
||||
<Icon name="warning" size="small" class="am-pr-badge-status" data-status="changes" />
|
||||
</Match>
|
||||
<Match when={indicator() === "approved"}>
|
||||
<Icon
|
||||
name="circle-check"
|
||||
size="small"
|
||||
class="am-pr-badge-status"
|
||||
data-status="approved"
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span class="am-pr-badge-number">#{pr().number}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -573,10 +573,11 @@ export const WithPRBadges: Story = {
|
||||
name: "Section — worktrees with PR badges",
|
||||
render: () => (
|
||||
<StoryProviders noPadding>
|
||||
<div style={{ "max-height": "400px", overflow: "auto" }}>
|
||||
<div style={{ "max-height": "560px", overflow: "auto" }}>
|
||||
<DndWrap>
|
||||
<SectionHeader section={sec("s1", 0, { name: "In Review", color: "Blue" })} count={3} {...sectionProps}>
|
||||
<SectionHeader section={sec("s1", 0, { name: "In Review", color: "Blue" })} count={8} {...sectionProps}>
|
||||
<div class="am-section-group-body">
|
||||
{/* Open + passing + approved → green badge, green check */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-1", "feat/api-v2")}
|
||||
@@ -595,6 +596,7 @@ export const WithPRBadges: Story = {
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + failing checks → green badge, red ✗ (no longer confusable with closed) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-2", "fix/race-cond")}
|
||||
@@ -605,28 +607,101 @@ export const WithPRBadges: Story = {
|
||||
title: "fix: race condition",
|
||||
url: "#",
|
||||
state: "open",
|
||||
review: "changes_requested",
|
||||
review: null,
|
||||
additions: 15,
|
||||
deletions: 8,
|
||||
files: 3,
|
||||
checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + changes requested → green badge, amber warning */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-3", "feat/cache")}
|
||||
worktree={wt("wt-3", "feat/search")}
|
||||
label="feat/search"
|
||||
subtitle="feat/search"
|
||||
pr={{
|
||||
number: 91,
|
||||
title: "feat: search",
|
||||
url: "#",
|
||||
state: "open",
|
||||
review: "changes_requested",
|
||||
additions: 60,
|
||||
deletions: 12,
|
||||
files: 4,
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-4", "feat/cache")}
|
||||
label="feat/cache"
|
||||
subtitle="feat/cache"
|
||||
pr={{
|
||||
number: 103,
|
||||
title: "feat: cache layer",
|
||||
url: "#",
|
||||
state: "draft",
|
||||
state: "open",
|
||||
review: null,
|
||||
additions: 200,
|
||||
deletions: 0,
|
||||
files: 8,
|
||||
checks: { status: "pending", total: 5, passed: 0, failed: 0, pending: 5, items: [] },
|
||||
checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Draft → gray badge */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-5", "wip/refactor")}
|
||||
label="wip/refactor"
|
||||
subtitle="wip/refactor"
|
||||
pr={{
|
||||
number: 110,
|
||||
title: "wip: refactor",
|
||||
url: "#",
|
||||
state: "draft",
|
||||
review: null,
|
||||
additions: 30,
|
||||
deletions: 5,
|
||||
files: 2,
|
||||
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Merged → purple badge, no status icon */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-6", "feat/done")}
|
||||
label="feat/done"
|
||||
subtitle="feat/done"
|
||||
pr={{
|
||||
number: 70,
|
||||
title: "feat: done",
|
||||
url: "#",
|
||||
state: "merged",
|
||||
review: "approved",
|
||||
additions: 90,
|
||||
deletions: 20,
|
||||
files: 6,
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Closed → red badge, no status icon (distinct from a failing open PR) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-7", "spike/idea")}
|
||||
label="spike/idea"
|
||||
subtitle="spike/idea"
|
||||
pr={{
|
||||
number: 65,
|
||||
title: "spike: idea",
|
||||
url: "#",
|
||||
state: "closed",
|
||||
review: null,
|
||||
additions: 10,
|
||||
deletions: 4,
|
||||
files: 1,
|
||||
checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -195,8 +195,8 @@ npm plugins can declare a version compatibility range in `package.json` using th
|
||||
- Alias: `kilo plug <module>`.
|
||||
- `-g` / `--global` writes into the global config dir.
|
||||
- Local installs resolve target dir inside `patchPluginConfig`.
|
||||
- For local scope, path is `<worktree>/.opencode` only when VCS is git and `worktree !== "/"`; otherwise `<directory>/.opencode`.
|
||||
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.opencode`) is covered by regression tests.
|
||||
- For local scope, path is `<worktree>/.kilo` only when VCS is git and `worktree !== "/"`; otherwise `<directory>/.kilo`.
|
||||
- Root-worktree fallback (`worktree === "/"` uses `<directory>/.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`.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
)),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -441,7 +441,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildPro
|
||||
|
||||
const root: Node = { name: "", children: new Map() }
|
||||
for (const file of list) {
|
||||
if (file.includes(".kilo") || file.includes(".opencode")) continue // kilocode_change
|
||||
if (file.includes(".kilo") || file.includes(".kilocode")) continue // kilocode_change
|
||||
const parts = file.split(path.sep)
|
||||
if (parts.length < 2) continue
|
||||
let node = root
|
||||
|
||||
@@ -43,12 +43,9 @@ export namespace KilocodeConfig {
|
||||
/** All config file names in precedence order (kilo + opencode). */
|
||||
export const ALL_CONFIG_FILES = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const
|
||||
|
||||
/** Directory suffixes that Kilo recognizes in addition to .opencode. */
|
||||
/** Config directory suffixes in update-target preference order. */
|
||||
export const KILO_DIR_SUFFIXES = [".kilo", ".kilocode"] as const
|
||||
|
||||
/** All config directory suffixes Kilo can update, including upstream .opencode. */
|
||||
export const ALL_CONFIG_DIR_SUFFIXES = [".kilo", ".kilocode", ".opencode"] as const
|
||||
|
||||
/** Path patterns for resolving kilo agent names from file paths. */
|
||||
export const AGENT_PATTERNS = ["/.kilo/agent/", "/.kilo/agents/", "/.kilocode/agent/", "/.kilocode/agents/"] as const
|
||||
|
||||
@@ -73,7 +70,7 @@ export namespace KilocodeConfig {
|
||||
worktree?: string
|
||||
}) {
|
||||
const dirs = yield* input.fs
|
||||
.up({ targets: [...ALL_CONFIG_DIR_SUFFIXES], start: input.directory, stop: input.worktree })
|
||||
.up({ targets: [...KILO_DIR_SUFFIXES], start: input.directory, stop: input.worktree })
|
||||
.pipe(Effect.orDie)
|
||||
const roots = yield* input.fs
|
||||
.up({ targets: [...ALL_CONFIG_FILES], start: input.directory, stop: input.worktree })
|
||||
@@ -437,6 +434,6 @@ export namespace KilocodeConfig {
|
||||
|
||||
/** Check whether a directory path should be treated as a config directory (for loading config files). */
|
||||
export function isConfigDir(dir: string, flagDir?: string): boolean {
|
||||
return dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir.endsWith(".opencode") || dir === flagDir
|
||||
return dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir === flagDir
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export namespace KilocodeConfigOverlay {
|
||||
}
|
||||
|
||||
const files = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"] as const
|
||||
const dirs = [".kilo", ".kilocode", ".opencode"] as const
|
||||
const dirs = [".kilocode", ".kilo"] as const
|
||||
|
||||
const fieldPaths = [
|
||||
["model"],
|
||||
@@ -124,7 +124,7 @@ export namespace KilocodeConfigOverlay {
|
||||
}
|
||||
|
||||
export async function projectTarget(input: { directory: string; worktree?: string }) {
|
||||
const found = await Filesystem.findUp([...dirs], input.directory, input.worktree)
|
||||
const found = await Filesystem.findUp(dirs.toReversed(), input.directory, input.worktree)
|
||||
const roots = await Filesystem.findUp([...files], input.directory, input.worktree)
|
||||
const candidates = [...found.flatMap((dir) => 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<Config.Info> {
|
||||
|
||||
@@ -56,7 +56,7 @@ export namespace KilocodeConfigSources {
|
||||
|
||||
type Pending = Omit<Source, "order">
|
||||
|
||||
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<Result> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<void>((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<void>((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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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/<name>/SKILL.md` (or `skill/<name>/SKILL.md`) with `name` and `description` in frontmatter. Discovered inside `.kilo/`, `.kilocode/`, and `.opencode/` directories.
|
||||
Skills are markdown files at `skills/<name>/SKILL.md` (or `skill/<name>/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
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export namespace KilocodeTuiConfig {
|
||||
export type Editable = Omit<Patch, "keybinds"> & { keybinds?: Record<string, string> }
|
||||
|
||||
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(
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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<GlobalBusEvent>((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* () {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -168,9 +168,9 @@ async function load(): Promise<Data> {
|
||||
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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)])
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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<unknown>)),
|
||||
)
|
||||
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,
|
||||
)
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1640,7 +1640,7 @@ const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
||||
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))
|
||||
|
||||
@@ -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",',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user