refactor: kilo compat for v1.17.9

This commit is contained in:
marius-kilocode
2026-07-22 13:31:23 +02:00
parent 95db2da8e9
commit a776bd4d29
5476 changed files with 27309 additions and 719942 deletions
-25
View File
@@ -1,25 +0,0 @@
# Changesets
This directory contains changeset files used to track changes for the next release.
## Adding a changeset
When making a user-facing change, prefer one concise changeset per PR, grouping related changes when possible. Run:
```sh
bunx changeset add
```
Or manually create a file `.changeset/<slug>.md`:
```md
---
"kilo-code": minor
---
Short description of the change for the changelog.
```
Use `patch` for bug fixes, `minor` for new features, `major` for breaking changes.
Changeset files are consumed at release time when the `publish.yml` workflow runs, generating changelog entries for the GitHub release notes.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---
Allow agents to stop and remove a targeted Agent Manager session.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Respect the Terminal Command Blocks display setting for background process tool cards.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Display here-document content as plain text in terminal approval prompts.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Support bidirectional text in chat messages so right-to-left and mixed-language conversations render in the correct direction.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---
Fix Cloud Agent session imports in installed CLI builds and prevent malformed exports or write failures from leaving partial imports.
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
"changelog": ["../script/changelog-github.cjs", { "repo": "Kilo-Org/kilocode" }],
"commit": false,
"fixed": [["kilo-code", "@kilocode/cli"]],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Open the Agent Manager new-worktree dialog from primary actions and keyboard shortcuts, with an explicit quick-create action in its dropdown.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep sessions usable after deprecated local review commands complete without a session status transition.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": minor
"@kilocode/kilo-indexing": minor
"kilo-code": minor
---
Configure a custom file extension allowlist for codebase indexing to limit scans to relevant project files and support additional text formats.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-indexing": patch
---
Allow remote embedder validation timeouts to retry before indexing reports a connection failure.
@@ -1,5 +0,0 @@
---
"@kilocode/cli": minor
---
Remote CLI: expose slash command discovery, execution, and `/new` session creation over the relay. `list_commands` and `send_command` (including the built-in `compact` flow) are scoped to the current session's directory. `create_session` creates a root session in that directory, attaches it to the relay heartbeat set, and returns the new session id only after the heartbeat completes so the mobile client can navigate immediately. Failures are sanitized; the command is not auto-retried and the user may retry manually after a transient relay failure.
-5
View File
@@ -7,8 +7,3 @@ end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
[*.{kt,kts}]
indent_size = 4
tab_width = 4
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
-4
View File
@@ -1,4 +0,0 @@
# Check if nix command is available before trying to use flake
if command -v nix >/dev/null 2>&1; then
use flake
fi
-39
View File
@@ -1,41 +1,2 @@
# Enforce LF line endings for all text files
* text=auto eol=lf
# Git LFS tracking for binary/media files
*.gif filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
# Visual regression baseline snapshots
packages/kilo-ui/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
packages/kilo-docs/public/img/screenshot-tests/**/*.png filter=lfs diff=lfs merge=lfs -text
# Hide most localization files by default in GitHub PR diffs. Show English as the source
# language and German so reviewers can catch untranslated copy-pasted English strings.
**/i18n/*.ts linguist-generated=true
**/i18n/**/*.ts linguist-generated=true
**/i18n/en.ts linguist-generated=false
**/i18n/en*.ts linguist-generated=false
**/i18n/**/en.ts linguist-generated=false
**/i18n/**/en*.ts linguist-generated=false
**/i18n/de.ts linguist-generated=false
**/i18n/de*.ts linguist-generated=false
**/i18n/**/de.ts linguist-generated=false
**/i18n/**/de*.ts linguist-generated=false
**/i18n/package-nls-en.ts linguist-generated=false
**/i18n/package-nls-de.ts linguist-generated=false
**/i18n/index.ts linguist-generated=false
**/i18n/**/index.ts linguist-generated=false
**/i18n/parity.test.ts linguist-generated=false
**/i18n/**/parity.test.ts linguist-generated=false
packages/kilo-i18n/src/*.ts linguist-generated=true
packages/kilo-i18n/src/en.ts linguist-generated=false
packages/kilo-i18n/src/de.ts linguist-generated=false
# Auto-generated CLI reference docs
packages/kilo-docs/markdoc/partials/cli-commands-table.md linguist-generated=true
packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md linguist-generated=true
# Upstream generated migration artifacts
packages/core/migration/**/snapshot.json linguist-generated
packages/core/src/database/migration.gen.ts linguist-generated
+4 -5
View File
@@ -1,6 +1,5 @@
name: Bug report
description: Report an issue that should be fixed
labels: ["bug"]
description: Report an issue that should be fixed (avoid pasting giant AI generated summaries or your issue may be closed/ignored)
body:
- type: textarea
id: description
@@ -20,10 +19,10 @@ body:
required: false
- type: input
id: kilo-version
id: opencode-version
attributes:
label: Kilo version
description: What version of Kilo are you using?
label: OpenCode version
description: What version of OpenCode are you using?
validations:
required: false
+2 -2
View File
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Join our Discord
url: https://kilo.ai/discord
- name: 💬 Discord Community
url: https://discord.gg/opencode
about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question.
@@ -1,6 +1,5 @@
name: 🚀 Feature Request
description: Suggest an idea, feature, or enhancement
labels: [discussion]
title: "[FEATURE]:"
body:
-1
View File
@@ -1,6 +1,5 @@
name: Question
description: Ask a question
labels: ["question"]
body:
- type: textarea
id: question
+4 -49
View File
@@ -33,10 +33,7 @@ runs:
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
# Restoring and extracting the ~1 GB cache took 2m23s on Windows, while a # kilocode_change
# fresh install took 1m27s. Keep Windows off this cache until that reverses. # kilocode_change
- name: Restore Bun dependencies
if: runner.os != 'Windows' # kilocode_change
id: bun-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
@@ -45,66 +42,24 @@ runs:
restore-keys: |
${{ runner.os }}-bun-
# kilocode_change start
- name: Setup Node for native dependency builds
if: runner.os != 'Windows'
uses: actions/setup-node@v6
with:
node-version: "24"
package-manager-cache: false
- name: Configure node-gyp Node headers
if: runner.os != 'Windows'
id: node-gyp
run: |
node <<'NODE'
const fs = require("node:fs")
const path = require("node:path")
const exe = path.dirname(fs.realpathSync(process.execPath))
const dirs = [exe, path.dirname(exe)]
const root = dirs.find((dir) => fs.existsSync(path.join(dir, "include", "node", "node.h")))
if (!root) {
throw new Error(`Node headers not found under ${dirs.join(", ")}`)
}
fs.appendFileSync(process.env.GITHUB_OUTPUT, `nodedir=${root}\n`)
NODE
shell: bash
# kilocode_change end
- name: Install setuptools for distutils compatibility
run: python3 -m pip install setuptools || pip install setuptools || true
shell: bash
- name: Install dependencies
run: |
# kilocode_change start
if [ "$RUNNER_OS" != "Windows" ]; then
export npm_config_nodedir="${{ steps.node-gyp.outputs.nodedir }}"
fi
# kilocode_change end
# Workaround for patched peer variants
# e.g. ./patches/ for standard-openapi
# https://github.com/oven-sh/bun/issues/28147 # kilocode_change
# https://github.com/oven-sh/bun/issues/28147
if [ "$RUNNER_OS" = "Windows" ]; then
# kilocode_change start
if ! bun install --frozen-lockfile --linker hoisted ${{ inputs.install-flags }}; then
echo "::warning::Bun install failed on Windows; retrying with conservative extraction"
sleep 5
BUN_FEATURE_FLAG_DISABLE_STREAMING_INSTALL=1 \
bun install --frozen-lockfile --linker hoisted --network-concurrency 16 ${{ inputs.install-flags }}
fi
# kilocode_change end
bun install --linker hoisted ${{ inputs.install-flags }}
else
bun install --frozen-lockfile ${{ inputs.install-flags }} # kilocode_change
bun install ${{ inputs.install-flags }}
fi
shell: bash
# Do not upload a Windows cache that Windows jobs intentionally never restore. # kilocode_change
- name: Save Bun dependencies
if: runner.os != 'Windows' && steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target' # kilocode_change
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
+6 -19
View File
@@ -1,14 +1,12 @@
name: "Setup Git Committer"
description: "Create app token and configure git user"
inputs:
# kilocode_change start
kilo-maintainer-app-id:
description: "Kilo Maintainer GitHub App ID"
opencode-app-id:
description: "OpenCode GitHub App ID"
required: true
kilo-maintainer-app-secret:
description: "Kilo Maintainer GitHub App private key"
opencode-app-secret:
description: "OpenCode GitHub App private key"
required: true
# kilocode_change end
outputs:
token:
description: "GitHub App token"
@@ -23,10 +21,8 @@ runs:
id: apptoken
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
with:
# kilocode_change start
app-id: ${{ inputs['kilo-maintainer-app-id'] }}
private-key: ${{ inputs['kilo-maintainer-app-secret'] }}
# kilocode_change end
app-id: ${{ inputs.opencode-app-id }}
private-key: ${{ inputs.opencode-app-secret }}
owner: ${{ github.repository_owner }}
- name: Configure git user
@@ -39,15 +35,6 @@ runs:
- name: Clear checkout auth
run: |
git config --local --unset-all http.https://github.com/.extraheader || true
# kilocode_change start
keys="$(git config --local --name-only --get-regexp '^includeIf\.gitdir:' || true)"
for key in $keys; do
path="$(git config --local --get "$key" || true)"
case "$path" in
*git-credentials-*.config) git config --local --unset-all "$key" || true ;;
esac
done
# kilocode_change end
shell: bash
- name: Configure git remote
-13
View File
@@ -1,13 +0,0 @@
# kilocode_change - new file
name: "Setup Kilo"
description: "Install Kilo CLI globally"
runs:
using: "composite"
steps:
- name: Install Kilo CLI
run: curl -fsSL https://kilo.ai/cli/install | bash
shell: bash
- name: Add Kilo to PATH
run: echo "$HOME/.kilo/bin" >> $GITHUB_PATH
shell: bash
@@ -1,21 +0,0 @@
# kilocode_change - new file
name: "Setup Linux Sandbox"
description: "Build the Linux bubblewrap helper"
runs:
using: "composite"
steps:
- name: Setup Zig
run: |
curl --fail --location --retry 3 \
https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \
--output "$RUNNER_TEMP/zig.tar.xz"
echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status
tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH"
shell: bash
- name: Build bubblewrap helper
run: |
bun packages/opencode/script/kilocode/bubblewrap.ts --arch x64 --output "$RUNNER_TEMP/bwrap"
echo "KILO_BWRAP_PATH=$RUNNER_TEMP/bwrap" >> "$GITHUB_ENV"
shell: bash
-12
View File
@@ -1,12 +0,0 @@
# kilocode_change - new file
name: Critical-only CodeQL config
query-filters:
- include:
kind:
- problem
- path-problem
- alert
- path-alert
tags contain: security
security-severity: /^(9(\.[0-9])?|10(\.0)?)$/
+17 -62
View File
@@ -1,74 +1,29 @@
## Issue
### Issue for this PR
<!-- Reference an existing issue with `Fixes #123`, `Closes #123`, or equivalent linked issue wording. -->
Closes #
Fixes #
### Type of change
## Context
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor / code improvement
- [ ] Documentation
<!-- Brief description of WHAT youre doing and WHY. -->
### What does this PR do?
## Implementation
Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
<!--
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
Some description of HOW you achieved it. Perhaps give a high level description of the program flow. Did you need to refactor something? What tradeoffs did you take? Are there things in here which youd particularly like people to pay close attention to? Keep this focused on context reviewers cannot infer from the diff; skip file-by-file summaries, placeholders, and other filler.
### How did you verify your code works?
-->
### Screenshots / recordings
## Screenshots / Video
_If this is a UI change, please include a screenshot or recording._
<!-- Required for visual changes. Include the relevant before/after or resulting state. -->
### Checklist
| before | after |
|---|---|
| | |
- [ ] I have tested my changes locally
- [ ] I have not included unrelated changes in this PR
## How to Test
### Manual/local verification
<!--
Describe local CLI, extension, or docs verification. Example:
- Opened the updated settings page and confirmed the new copy appears
-->
-
### Reviewer test steps
<!--
Provide steps reviewers can retrace. Example:
1. Sign in with a user with tracks
2. Activate `show_awesome_cat_gifs` feature by adding `?feature.show_awesome_cat_gifs=1` to your URL
3. Confirm you see a GIF with cats dancing
-->
-
### Blocked checks and substitute verification
<!--
If a relevant command could not be completed, name the command, explain the blocker or failure, and describe the substitute verification performed. Example:
- `bun run test:integration` could not complete because the required API key is unavailable; substitute verification was testing the affected flow manually in the development extension
-->
-
<!--
Every PR marked ready for review must include testing evidence. A bare "Not tested" or "N/A" answer is not sufficient. For each piece of evidence make clear if the test was executed by the agent or by a human
Docs-only, config-only, and similar changes still need concrete evidence. For examples, see [Testing Evidence for Pull Requests](https://kilo.ai/docs/contributing/development-environment#testing-evidence-for-pull-requests). Draft PRs may be incomplete until marked ready for review.
-->
## Checklist
- [ ] Issue linked above, or exception explained
- [ ] Tests/verification described
- [ ] Screenshots/video included for visual changes, or marked N/A
- [ ] Changeset considered for user-facing changes
- [ ] I personally reviewed the diff and can explain the changes, including any AI-assisted work.
## Get in Touch
<!-- We'd love to have a way to chat with you about your changes if necessary. If you're in the [Kilo Code Discord](https://kilo.ai/discord), please share your handle here. -->
_If you do not follow this template your PR will be automatically rejected._
-116
View File
@@ -1,116 +0,0 @@
name: Auto-docs dry-run intake
# Trigger on merged feature PRs, or manually with a specific PR number.
# IMPORTANT: This workflow intentionally does NOT check out PR code and does NOT
# execute any code from the PR branch. It only reads PR metadata and forwards it
# to a webhook. This is required for safe use of pull_request_target on a public repo.
on:
pull_request_target:
types: [closed]
workflow_dispatch:
inputs:
pr_number:
description: "PR number to test"
required: true
# Minimal permissions: read repo contents and PR metadata only.
permissions:
contents: read
pull-requests: read
# One run per PR at a time. Does not cancel in progress — let the current run
# finish so we don't drop events when a PR is quickly closed/reopened/closed.
concurrency:
group: auto-docs-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: false
jobs:
call-valtown:
# kilocode_change start
if: >
github.event_name == 'workflow_dispatch' ||
(
github.repository == 'Kilo-Org/kilocode' &&
github.event.pull_request.merged == true &&
(
startsWith(github.event.pull_request.title, 'feat:') ||
startsWith(github.event.pull_request.title, 'feat(')
)
)
# kilocode_change end
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Build webhook payload
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
PR_NUMBER_FROM_EVENT: ${{ github.event.pull_request.number }}
PR_NUMBER_FROM_INPUT: ${{ inputs.pr_number }}
run: |
set -euo pipefail
PR_NUMBER="${PR_NUMBER_FROM_EVENT:-$PR_NUMBER_FROM_INPUT}"
if [ -z "$PR_NUMBER" ]; then
echo "Missing PR number"
exit 1
fi
gh api "repos/$REPO/pulls/$PR_NUMBER" > pr.json
jq -n \
--arg event_name "$EVENT_NAME" \
--arg repo "$REPO" \
--arg pr_number "$PR_NUMBER" \
--slurpfile pr pr.json \
'{
event_name: $event_name,
repo: $repo,
pr_number: $pr_number,
title: $pr[0].title,
description: ($pr[0].body // ""),
author: $pr[0].user.login,
merged_at: ($pr[0].merged_at // ""),
pr_url: $pr[0].html_url,
base_branch: $pr[0].base.ref,
head_branch: $pr[0].head.ref
}' > payload.json
echo "Payload created:"
jq '{
event_name,
repo,
pr_number,
title,
author,
merged_at,
pr_url,
base_branch,
head_branch
}' payload.json
- name: Send webhook to Val Town
env:
DOC_WEBHOOK_URL: ${{ secrets.DOC_WEBHOOK_URL }}
DOC_WEBHOOK_SECRET: ${{ secrets.DOC_WEBHOOK_SECRET }}
run: |
set -euo pipefail
if [ -z "${DOC_WEBHOOK_URL:-}" ]; then
echo "DOC_WEBHOOK_URL secret is not set"
exit 1
fi
if [ -z "${DOC_WEBHOOK_SECRET:-}" ]; then
echo "DOC_WEBHOOK_SECRET secret is not set"
exit 1
fi
curl --fail-with-body -sS -X POST "$DOC_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Docs-Webhook-Secret: $DOC_WEBHOOK_SECRET" \
--data-binary @payload.json
+9 -9
View File
@@ -7,31 +7,31 @@ on:
jobs:
sync:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false # kilocode_change
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
id: committer
id: setup-git-committer
uses: ./.github/actions/setup-git-committer
with:
# kilocode_change start
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
# kilocode_change end
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Install OpenCode
run: bun i -g opencode-ai
- name: Sync beta branch
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: bun script/beta.ts
@@ -1,21 +0,0 @@
# kilocode_change - new file
name: Check forbidden strings
on:
pull_request:
workflow_dispatch:
jobs:
check:
name: Check forbidden strings
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: oven-sh/setup-bun@v2
- name: Run check
run: bun run script/check-forbidden-strings.ts
@@ -1,27 +0,0 @@
# kilocode_change - new file
name: Check Kilo generated artifacts
on:
pull_request:
paths:
- ".gitignore"
- ".github/workflows/check-kilo-generated-artifacts.yml"
- ".kilo/**"
- ".kilocode/**"
- "script/check-kilo-generated-artifacts.ts"
workflow_dispatch:
jobs:
check:
name: Check Kilo generated artifacts
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: oven-sh/setup-bun@v2
- name: Run check
run: bun run script/check-kilo-generated-artifacts.ts
@@ -1,24 +0,0 @@
name: Check markdown table padding
on:
pull_request:
paths:
- "**/*.md"
- "script/check-md-table-padding.ts"
- ".github/workflows/check-md-table-padding.yml"
workflow_dispatch:
jobs:
check:
name: Check markdown table padding
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: oven-sh/setup-bun@v2
- name: Run check
run: bun run script/check-md-table-padding.ts
@@ -1,49 +0,0 @@
name: Check shared upstream annotations
on:
pull_request:
paths:
- ".github/**"
- "github/**"
- "packages/extensions/**"
- "packages/opencode/**"
- "packages/script/**"
- "packages/shared/**"
- "packages/storybook/**"
- "packages/ui/**"
- "script/**"
workflow_dispatch:
jobs:
check-annotations:
name: Check kilocode_change annotations
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- name: Check kilocode_change annotations in shared upstream files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$BASE_SHA" ]; then
bun run script/check-opencode-annotations.ts --base "$BASE_SHA"
else
echo "No PR base SHA available (workflow_dispatch without PR context) — skipping."
fi
# kilocode_change start
- name: Check Effect Promise facade allowlist
run: bun run script/check-opencode-promise-facades.ts
- name: Check model tool network boundary
run: bun run script/check-model-tool-network.ts
- name: Check workflow allowlist
run: bun run script/check-workflows.ts
# kilocode_change end
-48
View File
@@ -1,48 +0,0 @@
name: check-org-member
on:
workflow_call:
inputs:
username:
required: true
type: string
secrets:
kilo-maintainer-app-id:
required: true
kilo-maintainer-app-secret:
required: true
outputs:
is-member:
description: Whether the user is an org member or kilo-maintainer bot
value: ${{ jobs.check.outputs['is-member'] }}
jobs:
check:
runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change
outputs:
is-member: ${{ steps.check.outputs['is-member'] }}
steps:
- name: Create app token
id: apptoken
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets['kilo-maintainer-app-id'] }}
private-key: ${{ secrets['kilo-maintainer-app-secret'] }}
owner: ${{ github.repository_owner }}
- name: Check if author is org member or kilo bot
id: check
env:
GITHUB_TOKEN: ${{ steps.apptoken.outputs.token }}
USERNAME: ${{ inputs.username }}
run: |
if [[ "$USERNAME" == "kilo-maintainer[bot]" || "$USERNAME" == "kiloconnect[bot]" || "$USERNAME" == "kiloconnect-lite[bot]" ]]; then
echo "is-member=true" >> "$GITHUB_OUTPUT"
echo "User is a kilo bot"
elif gh api orgs/Kilo-Org/members/"$USERNAME" --silent 2>/dev/null; then
echo "is-member=true" >> "$GITHUB_OUTPUT"
echo "User is an org member"
else
echo "is-member=false" >> "$GITHUB_OUTPUT"
echo "User is not an org member"
fi
-69
View File
@@ -1,69 +0,0 @@
# kilocode_change - new file
name: "CodeQL Kotlin"
on:
push:
branches: ["main"]
paths:
- "packages/kilo-jetbrains/**"
- ".github/workflows/codeql-kotlin.yml"
pull_request:
branches: ["main"]
paths:
- "packages/kilo-jetbrains/**"
- ".github/workflows/codeql-kotlin.yml"
schedule:
- cron: "29 10 * * 5"
workflow_dispatch:
jobs:
analyze:
name: Analyze (java-kotlin)
if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Free disk space for CodeQL
shell: bash
run: |
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
docker system prune --all --force || true
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: java-kotlin
build-mode: manual
config-file: ./.github/codeql/codeql-config.yml
- name: Build Java/Kotlin
shell: bash
run: ./gradlew typecheck --rerun-tasks --no-build-cache
working-directory: packages/kilo-jetbrains
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:java-kotlin"
-82
View File
@@ -1,82 +0,0 @@
# kilocode_change - new file
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "29 10 * * 5"
workflow_dispatch:
jobs:
analyze:
name: Analyze (${{ matrix.language }})
if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode'
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yml
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
+7 -15
View File
@@ -3,7 +3,7 @@ name: containers
on:
push:
branches:
- main # kilocode_change
- dev
paths:
- packages/containers/**
- .github/workflows/containers.yml
@@ -16,18 +16,12 @@ permissions:
jobs:
build:
if: github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode' # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
REGISTRY: ghcr.io/${{ github.repository_owner }}
TAG: "24.04"
steps:
# kilocode_change start
- name: Set lowercase repository owner
id: repo
run: echo "owner_lc=${OWNER,,}" >> $GITHUB_OUTPUT
env:
OWNER: ${{ github.repository_owner }}
# kilocode_change end
- uses: actions/checkout@v6 # kilocode_change
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-bun
@@ -47,7 +41,5 @@ jobs:
- name: Build and push containers
run: bun ./packages/containers/script/build.ts --push
env:
# kilocode_change start
REGISTRY: ghcr.io/${{ steps.repo.outputs.owner_lc }}
TAG: "24.04"
# kilocode_change end
REGISTRY: ${{ env.REGISTRY }}
TAG: ${{ env.TAG }}
@@ -1,38 +0,0 @@
name: kilo
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
kilo:
if: |
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') &&
(
contains(github.event.comment.body, ' /kc') ||
startsWith(github.event.comment.body, '/kc') ||
contains(github.event.comment.body, ' /kilo') ||
startsWith(github.event.comment.body, '/kilo')
)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
id-token: write
contents: read
pull-requests: read
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-bun
- name: Run opencode
uses: Kilo-Org/kilocode/github@latest
env:
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
KILO_PERMISSION: '{"bash": "deny"}'
with:
model: kilo/kilo-auto/frontier
@@ -1,46 +0,0 @@
name: nix-desktop
on:
push:
branches: [main]
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
pull_request:
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
workflow_dispatch:
jobs:
nix-desktop:
strategy:
fail-fast: false
matrix:
os:
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-ubuntu-2404-arm
- macos-15-intel
- macos-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Build desktop via flake
run: |
set -euo pipefail
nix --version
nix build .#desktop -L
@@ -1,35 +0,0 @@
name: "sync-zed-extension"
on:
workflow_dispatch:
release:
types: [published]
jobs:
zed:
name: Release Zed Extension
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
- name: Get version tag
id: get_tag
run: |
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
else
TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1)
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Using tag: ${TAG}"
- name: Sync Zed extension
run: |
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env:
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }}
-30
View File
@@ -1,30 +0,0 @@
name: docs-build
on:
push:
branches: [main]
paths:
- "packages/kilo-docs/**"
pull_request:
paths:
- "packages/kilo-docs/**"
workflow_dispatch:
jobs:
build:
name: Build docs site
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Build site
run: bun run --filter @kilocode/kilo-docs build
env:
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }}
- name: Run tests
run: bun run --filter @kilocode/kilo-docs test
-29
View File
@@ -1,29 +0,0 @@
name: Check Links
on:
push:
branches: [main]
pull_request:
paths:
- "packages/kilo-docs/**"
- ".github/workflows/docs-check-links.yml"
schedule:
# Run daily at 9am UTC
- cron: "0 9 * * *"
workflow_dispatch: # Allow manual trigger
jobs:
link-checker:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
- name: Link Checker
uses: Kilo-Org/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411
with:
args: "**/*.md"
workingDirectory: packages/kilo-docs
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -20,13 +20,13 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://kilo.ai/install | bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Check duplicates and compliance
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KILO_PERMISSION: |
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
@@ -118,7 +118,7 @@ jobs:
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
recheck-compliance:
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') # kilocode_change
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
@@ -132,13 +132,13 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://kilo.ai/install | bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Recheck compliance
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KILO_PERMISSION: |
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
+4 -11
View File
@@ -7,30 +7,23 @@ on:
jobs:
generate:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
# kilocode_change start
with:
persist-credentials: false
# kilocode_change end
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
# kilocode_change start
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
# kilocode_change end
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Generate
run: ./script/generate.ts
-366
View File
@@ -1,366 +0,0 @@
# kilocode_change - new file
name: kilo-auto-close
on:
workflow_dispatch:
inputs:
dryRun:
description: "Log actions without closing items"
type: boolean
default: true
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
issues: write
pull-requests: write
jobs:
close:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Close inactive PRs and issues
uses: actions/github-script@v8
env:
KILO_AUTO_CLOSE_ENABLED: ${{ vars.KILO_AUTO_CLOSE_ENABLED }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const PR_DAYS_INACTIVE = 30
const ISSUE_DAYS_INACTIVE = 60
const MAX_RETRIES = 3
// Adaptive delay: fast for small batches, slower for large to respect
// GitHub's 80 content-generating requests/minute limit
const SMALL_BATCH_THRESHOLD = 10
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 items)
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 items) = ~30 ops/min, well under 80 limit
const startTime = Date.now()
const prCutoff = new Date(Date.now() - PR_DAYS_INACTIVE * 24 * 60 * 60 * 1000)
const issueCutoff = new Date(Date.now() - ISSUE_DAYS_INACTIVE * 24 * 60 * 60 * 1000)
const { owner, repo } = context.repo
const dryRunInput = context.payload.inputs?.dryRun
const enabled = process.env.KILO_AUTO_CLOSE_ENABLED === "true"
const dryRun = dryRunInput === undefined
? !enabled
: dryRunInput !== "false"
core.info(`Dry run mode: ${dryRun}`)
core.info(`PR cutoff date: ${prCutoff.toISOString()}`)
core.info(`Issue cutoff date: ${issueCutoff.toISOString()}`)
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function withRetry(fn, description = 'API call') {
let lastError
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const result = await fn()
return result
} catch (error) {
lastError = error
const isRateLimited = error.status === 403 &&
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
if (!isRateLimited) {
throw error
}
if (attempt === MAX_RETRIES - 1) {
break
}
// Parse retry-after header, default to 60 seconds
const retryAfter = error.response?.headers?.['retry-after']
? parseInt(error.response.headers['retry-after'])
: 60
// Exponential backoff: retryAfter * 2^attempt
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
await sleep(backoffMs)
}
}
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
throw lastError
}
const query = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 100, states: OPEN, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
number
title
author {
login
}
createdAt
commits(last: 1) {
nodes {
commit {
committedDate
}
}
}
comments(last: 1) {
nodes {
createdAt
}
}
reviews(last: 1) {
nodes {
createdAt
}
}
reviewThreads(last: 100) {
nodes {
comments(last: 1) {
nodes {
createdAt
}
}
}
}
}
}
}
}
`
const allPrs = []
let cursor = null
let hasNextPage = true
let pageCount = 0
while (hasNextPage) {
pageCount++
core.info(`Fetching page ${pageCount} of open PRs...`)
const result = await withRetry(
() => github.graphql(query, { owner, repo, cursor }),
`GraphQL page ${pageCount}`
)
allPrs.push(...result.repository.pullRequests.nodes)
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
cursor = result.repository.pullRequests.pageInfo.endCursor
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
// Delay between pagination requests (use small batch delay for reads)
if (hasNextPage) {
await sleep(SMALL_BATCH_DELAY_MS)
}
}
core.info(`Found ${allPrs.length} open pull requests`)
function entry(source, date) {
if (!date) return null
return { source, date: new Date(date) }
}
function latest(items) {
return items.filter(Boolean).sort((a, b) => b.date.getTime() - a.date.getTime())[0]
}
const stalePrs = allPrs.flatMap((pr) => {
const activity = latest([
entry("created", pr.createdAt),
entry("commit", pr.commits.nodes[0]?.commit.committedDate),
entry("comment", pr.comments.nodes[0]?.createdAt),
entry("review", pr.reviews.nodes[0]?.createdAt),
...pr.reviewThreads.nodes.map((t) => entry("review comment", t.comments.nodes[0]?.createdAt)),
])
const text = activity
? `${activity.date.toISOString()} via ${activity.source}`
: "unknown"
if (!activity || activity.date > prCutoff) {
core.info(`PR #${pr.number} is fresh (last activity: ${text})`)
return []
}
core.info(`PR #${pr.number} is STALE (last activity: ${text})`)
return [{ ...pr, activity }]
})
core.info(`Found ${stalePrs.length} stale pull requests`)
const prDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
? LARGE_BATCH_DELAY_MS
: SMALL_BATCH_DELAY_MS
core.info(`Using ${prDelayMs}ms delay between PR operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
let closedPrCount = 0
let skippedPrCount = 0
for (const pr of stalePrs) {
const issue_number = pr.number
const closeComment = `To stay organized pull requests are automatically closed after ${PR_DAYS_INACTIVE} days of inactivity. If the pull request is still relevant please reopen it or create a fresh new one.`
if (dryRun) {
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'} (last activity: ${pr.activity.date.toISOString()} via ${pr.activity.source}): ${pr.title}`)
continue
}
try {
// Add comment
await withRetry(
() => github.rest.issues.createComment({
owner,
repo,
issue_number,
body: closeComment,
}),
`Comment on PR #${issue_number}`
)
// Close PR
await withRetry(
() => github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
state: "closed",
}),
`Close PR #${issue_number}`
)
closedPrCount++
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'} (last activity: ${pr.activity.date.toISOString()} via ${pr.activity.source}): ${pr.title}`)
// Delay before processing next PR
await sleep(prDelayMs)
} catch (error) {
skippedPrCount++
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
}
}
let page = 1
let stop = false
const staleIssues = []
while (!stop) {
core.info(`Fetching page ${page} of open issues...`)
const result = await withRetry(
() => github.rest.issues.listForRepo({
owner,
repo,
state: "open",
sort: "updated",
direction: "asc",
per_page: 100,
page,
}),
`Issues page ${page}`
)
if (!result.data.length) {
break
}
core.info(`Page ${page}: fetched ${result.data.length} issues`)
for (const issue of result.data) {
const updated = new Date(issue.updated_at)
if (updated >= issueCutoff) {
core.info(`Found fresh issue/PR #${issue.number} (${updated.toISOString()}), stopping issue scan`)
stop = true
break
}
if (issue.pull_request) {
core.info(`Skipping PR #${issue.number} in issue scan`)
continue
}
staleIssues.push({ ...issue, activity: updated })
}
if (!stop) {
page++
await sleep(SMALL_BATCH_DELAY_MS)
}
}
core.info(`Found ${staleIssues.length} stale issues`)
const issueDelayMs = staleIssues.length > SMALL_BATCH_THRESHOLD
? LARGE_BATCH_DELAY_MS
: SMALL_BATCH_DELAY_MS
core.info(`Using ${issueDelayMs}ms delay between issue operations (${staleIssues.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
let closedIssueCount = 0
let skippedIssueCount = 0
for (const issue of staleIssues) {
const closeComment = `To stay organized issues are automatically closed after ${ISSUE_DAYS_INACTIVE} days of no activity. If the issue is still relevant please reopen it or create a fresh new one.`
if (dryRun) {
core.info(`[dry-run] Would close issue #${issue.number} (last activity: ${issue.activity.toISOString()} via updated): ${issue.title}`)
continue
}
try {
await withRetry(
() => github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: closeComment,
}),
`Comment on issue #${issue.number}`
)
await withRetry(
() => github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned",
}),
`Close issue #${issue.number}`
)
closedIssueCount++
core.info(`Closed issue #${issue.number} (last activity: ${issue.activity.toISOString()} via updated): ${issue.title}`)
await sleep(issueDelayMs)
} catch (error) {
skippedIssueCount++
core.error(`Failed to close issue #${issue.number}: ${error.message}`)
}
}
const elapsed = Math.round((Date.now() - startTime) / 1000)
core.info(`\n========== Summary ==========`)
core.info(`Total open PRs found: ${allPrs.length}`)
core.info(`Stale PRs identified: ${stalePrs.length}`)
core.info(`PRs closed: ${closedPrCount}`)
core.info(`PRs skipped (errors): ${skippedPrCount}`)
core.info(`Stale issues identified: ${staleIssues.length}`)
core.info(`Issues closed: ${closedIssueCount}`)
core.info(`Issues skipped (errors): ${skippedIssueCount}`)
core.info(`Elapsed time: ${elapsed}s`)
core.info(`=============================`)
+20 -2
View File
@@ -16,7 +16,6 @@ permissions:
jobs:
nix-eval:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 15
steps:
@@ -39,7 +38,9 @@ jobs:
nix flake show --all-systems
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
PACKAGES="kilo"
PACKAGES="opencode"
# TODO: move 'desktop' to PACKAGES when #11755 is fixed
OPTIONAL_PACKAGES="desktop"
echo ""
echo "=== Evaluating packages for all systems ==="
@@ -59,6 +60,23 @@ jobs:
done
done
echo ""
echo "=== Evaluating optional packages ==="
for system in $SYSTEMS; do
echo ""
echo "--- $system ---"
for pkg in $OPTIONAL_PACKAGES; do
printf " %s: " "$pkg"
if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then
echo "✓"
else
echo "✗"
echo "::warning::Evaluation failed for packages.$system.$pkg"
echo "$output"
fi
done
done
echo ""
echo "=== Evaluating devShells for all systems ==="
for system in $SYSTEMS; do
+8 -12
View File
@@ -6,7 +6,7 @@ permissions:
on:
workflow_dispatch:
push:
branches: [main]
branches: [dev, beta]
paths:
- "bun.lock"
- "package.json"
@@ -25,7 +25,6 @@ jobs:
# Native runners required: bun install cross-compilation flags (--os/--cpu)
# do not produce byte-identical node_modules as native installs.
compute-hash:
if: github.repository == 'Kilo-Org/kilocode'
strategy:
fail-fast: false
matrix:
@@ -83,7 +82,7 @@ jobs:
echo "Computed hash for ${SYSTEM}: $HASH"
- name: Upload hash
uses: actions/upload-artifact@v7 # kilocode_change
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: hash-${{ matrix.system }}
path: hash.txt
@@ -91,32 +90,29 @@ jobs:
update-hashes:
needs: compute-hash
if: github.repository == 'Kilo-Org/kilocode' && github.event_name != 'pull_request'
if: github.event_name != 'pull_request'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Setup Git Committer
id: committer
- name: Setup git committer
uses: ./.github/actions/setup-git-committer
with:
# kilocode_change start
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
# kilocode_change end
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Pull latest changes
run: |
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
- name: Download hash artifacts
uses: actions/download-artifact@v8 # kilocode_change
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: hashes
pattern: hash-*
@@ -6,10 +6,7 @@ on:
jobs:
notify:
# kilocode_change start - disabled for kilo-cli fork (OpenCode Discord notifications)
if: false
# kilocode_change end
runs-on: depot-ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0
@@ -37,7 +37,7 @@ jobs:
- name: Install opencode
if: steps.team-check.outputs.is_team != 'true'
run: curl -fsSL https://kilo.ai/install | bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Build prompt
if: steps.team-check.outputs.is_team != 'true'
@@ -1,64 +0,0 @@
# kilocode_change - new file
name: prepare-jetbrains-release
on:
workflow_dispatch:
inputs:
kind:
description: "Release kind"
required: true
type: choice
options:
- rc
- stable
version:
description: "Version, e.g. 7.3.13-rc.1 or 7.3.13"
required: true
type: string
from_tag:
description: "Optional previous tag for changelog range"
required: false
type: string
permissions:
contents: write
pull-requests: write
concurrency:
group: prepare-jetbrains-release-${{ inputs.version }}
cancel-in-progress: false
jobs:
prepare:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Git Committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
- name: Create release tag and PR
run: |
args=(--kind "$KIND" --version "$VERSION")
if [[ -n "$FROM_TAG" ]]; then
args+=(--from-tag "$FROM_TAG")
fi
bun script/jetbrains-release-pr.ts "${args[@]}"
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
GH_REPO: ${{ github.repository }}
KIND: ${{ inputs.kind }}
VERSION: ${{ inputs.version }}
FROM_TAG: ${{ inputs.from_tag }}
@@ -22,13 +22,9 @@ jobs:
- run: git fetch --force --tags
- name: Setup Git Committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
- name: Publish
run: ./script/publish
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./script/publish
working-directory: ./github
-208
View File
@@ -1,208 +0,0 @@
# kilocode_change - new file
name: publish-jetbrains
on:
pull_request:
types:
- closed
branches:
- main
workflow_dispatch:
inputs:
pr:
description: Merged JetBrains release PR number to publish
required: true
type: string
merge_commit:
description: Merge commit SHA from the reviewed release PR
required: true
type: string
concurrency:
group: publish-jetbrains-pr-${{ github.event.pull_request.number || inputs.pr }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
publish:
if: >-
github.repository == 'Kilo-Org/kilocode' &&
(
(
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'jetbrains/release/') &&
contains(github.event.pull_request.labels.*.name, 'jetbrains-release') &&
github.event.pull_request.head.repo.full_name == github.repository
) ||
github.event_name == 'workflow_dispatch'
)
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout merged release PR
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.merge_commit_sha || inputs.merge_commit }}
- name: Setup Bun for validation
uses: ./.github/actions/setup-bun
- name: Validate release PR and tag
id: release
run: bun script/jetbrains-release-validate.ts --pr "$PR_NUMBER"
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }}
- name: Save reviewed release metadata
run: |
cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md"
cp packages/kilo-jetbrains/gradle.properties "$RUNNER_TEMP/jetbrains-gradle.properties"
- name: Checkout release tag
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ steps.release.outputs.tag }}
- name: Restore reviewed release metadata
run: |
cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md
cp "$RUNNER_TEMP/jetbrains-gradle.properties" packages/kilo-jetbrains/gradle.properties
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Install dependencies
run: bun install
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Install build tools
run: |
sudo apt-get update
sudo apt-get install -y patchelf zip
curl --fail --location \
https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \
--output "$RUNNER_TEMP/zig.tar.xz"
echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status
tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH"
- name: Validate publishing secrets
run: |
missing=0
for name in JETBRAINS_MARKETPLACE_TOKEN JETBRAINS_CERTIFICATE_CHAIN JETBRAINS_PRIVATE_KEY JETBRAINS_PRIVATE_KEY_PASSWORD; do
if [[ -z "${!name}" ]]; then
echo "Missing required secret: $name" >&2
missing=1
fi
done
exit "$missing"
env:
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }}
JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }}
JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }}
- name: Verify plugin
working-directory: packages/kilo-jetbrains
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.release.outputs.version }}
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
- name: Render release notes
working-directory: packages/kilo-jetbrains
run: |
if ! grep -Fq "## [$VERSION]" CHANGELOG.md; then
echo "Missing packages/kilo-jetbrains/CHANGELOG.md entry for $VERSION. Review and merge a release PR before publishing." >&2
exit 1
fi
./gradlew getChangelog --project-version "$VERSION" --no-header --no-empty-sections --output-file=build/release-notes.md
env:
VERSION: ${{ steps.release.outputs.version }}
- name: Publish to JetBrains Marketplace
working-directory: packages/kilo-jetbrains
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
VERSION: ${{ steps.release.outputs.version }}
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }}
JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }}
JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }}
- name: Resolve plugin archive
id: archive
run: |
mapfile -t signed < <(compgen -G "packages/kilo-jetbrains/build/distributions/*-signed.zip")
if [[ "${#signed[@]}" -eq 1 ]]; then
echo "path=${signed[0]}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${#signed[@]}" -gt 1 ]]; then
echo "Expected exactly one signed JetBrains plugin ZIP, found ${#signed[@]}." >&2
printf '%s\n' "${signed[@]}" >&2
exit 1
fi
mapfile -t files < <(compgen -G "packages/kilo-jetbrains/build/distributions/*.zip")
if [[ "${#files[@]}" -ne 1 ]]; then
echo "Expected exactly one JetBrains plugin ZIP, found ${#files[@]}." >&2
printf '%s\n' "${files[@]}" >&2
exit 1
fi
echo "path=${files[0]}" >> "$GITHUB_OUTPUT"
- name: Upload to GitHub Release
run: |
tag="$TAG"
title="JetBrains $VERSION"
flags=(--title "$title" --notes-file "$NOTES")
if [[ "$KIND" == "rc" ]]; then
flags+=(--prerelease)
fi
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$tag" "$ARCHIVE" --clobber --repo "$GITHUB_REPOSITORY"
gh release edit "$tag" "${flags[@]}" --repo "$GITHUB_REPOSITORY"
exit 0
fi
gh release create "$tag" "$ARCHIVE" "${flags[@]}" --repo "$GITHUB_REPOSITORY"
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
VERSION: ${{ steps.release.outputs.version }}
KIND: ${{ steps.release.outputs.kind }}
ARCHIVE: ${{ steps.archive.outputs.path }}
NOTES: packages/kilo-jetbrains/build/release-notes.md
- name: Upload workflow artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: kilo-jetbrains-${{ steps.release.outputs.version }}
path: packages/kilo-jetbrains/build/distributions/*.zip
if-no-files-found: ignore
+394 -353
View File
@@ -1,13 +1,14 @@
name: publish
run-name: "${{ format('{0} {1}', inputs.pre_release && 'pre-release' || 'release', inputs.bump) }}"
run-name: "${{ format('release {0}', inputs.bump) }}"
on:
# push:
# branches:
# - ci
# - dev
# - beta
# - snapshot-*
push:
branches:
- ci
- dev
- beta
- fix/npm-native-binary-install
- snapshot-*
workflow_dispatch:
inputs:
bump:
@@ -15,440 +16,467 @@ on:
required: false
type: choice
options:
- patch
- minor
- major
- minor
- patch
version:
description: "Override version (optional)"
required: false
type: string
# kilocode_change start
pre_release:
description: "Publish as pre-release (VS Code marketplace + npm rc channel)"
required: false
type: boolean
default: true
# kilocode_change end
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
permissions:
id-token: write
contents: write
issues: write # kilocode_change - label automated JetBrains CLI pin bump PRs
packages: write
pull-requests: write # kilocode_change - create automated JetBrains CLI pin bump PRs
jobs:
version:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'Kilo-Org/kilocode'
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v6 # kilocode_change
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
# kilocode_change start - install deps for version script workspace resolution
- name: Install dependencies
run: bun install
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Install Kilo
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g @kilocode/cli
# kilocode_change end
run: bun i -g opencode-ai
- id: version
run: |
./script/version.ts
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
KILO_BUMP: ${{ inputs.bump }}
KILO_VERSION: ${{ inputs.version }}
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
GH_TOKEN: ${{ steps.committer.outputs.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
tag: ${{ steps.version.outputs.tag }}
repo: ${{ steps.version.outputs.repo }}
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'Kilo-Org/kilocode'
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v6 # kilocode_change
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-tags: true
- uses: ./.github/actions/setup-bun
# kilocode_change start
- name: Setup Zig for Linux sandbox helpers
run: |
curl --fail --location --retry 3 \
https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \
--output "$RUNNER_TEMP/zig.tar.xz"
echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status
tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH"
# kilocode_change end
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Build
id: build
run: |
./packages/opencode/script/build.ts
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
./packages/cli/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
env:
KILO_VERSION: ${{ needs.version.outputs.version }}
KILO_RELEASE: ${{ needs.version.outputs.release }}
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_REPO: ${{ needs.version.outputs.repo }}
GH_TOKEN: ${{ steps.committer.outputs.token }}
# kilocode_change start - pack dist tree into a single zstd-compressed tar before upload.
# download-artifact@v4's streaming unzip is CPU-bound and processes entries one by one;
# collapsing ~480 files into one tar cuts build-vscode download from >30m to seconds.
# zstd compresses the wasms (~4x) and leaves the already-compact binaries alone, for
# ~1 GB artifact. upload-artifact@v7's `archive: false` skips the zip wrapper entirely
# so the tarball travels as-is (download-artifact@v8 auto-detects via Content-Type).
# With `archive: false` the `name:` input is ignored; the artifact is named after the
# file (kilo-cli.tar.zst). zstd + GNU tar are preinstalled on both github-hosted and
# blacksmith ubuntu-2404. See actions/upload-artifact#36, actions/toolkit#1533.
- name: Pack CLI dist into tar.zst
run: |
find packages/opencode/dist -name '*.map' -delete
tar --zstd -cf /tmp/kilo-cli.tar.zst -C packages/opencode/dist .
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
path: /tmp/kilo-cli.tar.zst
archive: false
# kilocode_change end
name: opencode-cli
path: |
packages/opencode/dist/opencode-darwin*
packages/opencode/dist/opencode-linux*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-windows
path: packages/opencode/dist/opencode-windows*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-preview-cli
path: packages/cli/dist/cli-*
outputs:
version: ${{ needs.version.outputs.version }}
# kilocode_change start - execute supported Unix CLI binaries before packaging VSIX artifacts
validate-cli-unix:
name: Validate CLI (${{ matrix.target }})
needs: build-cli
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
runner: macos-15
uname: arm64
package: "@kilocode/cli-darwin-arm64"
mode: host
- target: darwin-x64
runner: macos-15-intel
uname: x86_64
package: "@kilocode/cli-darwin-x64"
mode: host
- target: linux-arm64
runner: ubuntu-24.04-arm
uname: aarch64
package: "@kilocode/cli-linux-arm64"
mode: host
- target: linux-x64
runner: ubuntu-24.04
uname: x86_64
package: "@kilocode/cli-linux-x64"
mode: host
- target: alpine-arm64
runner: ubuntu-24.04-arm
uname: aarch64
package: "@kilocode/cli-linux-arm64-musl"
mode: alpine
platform: linux/arm64
- target: alpine-x64
runner: ubuntu-24.04
uname: x86_64
package: "@kilocode/cli-linux-x64-musl"
mode: alpine
platform: linux/amd64
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true
- name: Unpack CLI dist
run: |
mkdir -p dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C dist
- name: Run CLI smoke test
run: |
test "$(uname -m)" = "${{ matrix.uname }}"
smoke_host() {
binary="$1"
"$binary" --version
helper="$(dirname "$binary")/bwrap"
if [[ "${{ matrix.target }}" == linux-* ]]; then
test -x "$helper"
grep -q '^SPDX-License-Identifier: LGPL-2.0-or-later$' "$(dirname "$binary")/licenses/bubblewrap/NOTICE"
"$helper" --version
# The live user-namespace bootstrap depends on the runner's kernel/AppArmor policy
# (GitHub-hosted Ubuntu 24.04 sets kernel.apparmor_restrict_unprivileged_userns=1), not on
# the shipped artifact. The runtime probe degrades gracefully, so keep this check non-fatal.
"$helper" --unshare-user --disable-userns --unshare-pid --die-with-parent --new-session \
--ro-bind / / --dev /dev --proc /proc -- "$helper" --version \
|| echo "unprivileged user namespaces unavailable on this runner; skipping live sandbox check"
fi
root="$(mktemp -d)"
trap 'rm -rf "$root"' RETURN
(
unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR
export XDG_DATA_HOME="$root/data"
export XDG_CACHE_HOME="$root/cache"
export XDG_CONFIG_HOME="$root/config"
export XDG_STATE_HOME="$root/state"
export KILO_DISABLE_MODELS_FETCH=1
export KILO_DISABLE_PROJECT_CONFIG=1
export KILO_CONFIG_CONTENT='{"enabled_providers":["anthropic"]}'
export ANTHROPIC_API_KEY=dummy
"$binary" --pure models anthropic | grep -q '^anthropic/'
)
}
if [ "${{ matrix.mode }}" = "host" ]; then
smoke_host "./dist/${{ matrix.package }}/bin/kilo"
exit 0
fi
docker run --rm \
--platform "${{ matrix.platform }}" \
-v "$PWD/dist:/dist:ro" \
-e PACKAGE="${{ matrix.package }}" \
alpine:3.22 \
sh -c '
set -eu
# kilocode_change start - Bun musl binaries link against libstdc++ and libgcc_s
# (GCC C++ runtime). Alpine does not ship these by default; they are available
# as optional packages and must be installed for any Bun-compiled musl binary to run.
apk add --no-cache libstdc++ libgcc
# kilocode_change end
binary="/dist/$PACKAGE/bin/kilo" # kilocode_change
"$binary" --version # kilocode_change
"/dist/$PACKAGE/bin/bwrap" --version # kilocode_change
grep -q '\''^SPDX-License-Identifier: LGPL-2.0-or-later$'\'' "/dist/$PACKAGE/bin/licenses/bubblewrap/NOTICE" # kilocode_change
root="$(mktemp -d)"
trap '\''rm -rf "$root"'\'' EXIT
unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR
export XDG_DATA_HOME="$root/data"
export XDG_CACHE_HOME="$root/cache"
export XDG_CONFIG_HOME="$root/config"
export XDG_STATE_HOME="$root/state"
export KILO_DISABLE_MODELS_FETCH=1
export KILO_DISABLE_PROJECT_CONFIG=1
export KILO_CONFIG_CONTENT='\''{"enabled_providers":["anthropic"]}'\''
export ANTHROPIC_API_KEY=dummy
"$binary" --pure models anthropic | grep -q "^anthropic/"
'
validate-cli-windows:
name: Validate CLI (windows-${{ matrix.arch }})
needs: build-cli
runs-on: ${{ matrix.runner }}
timeout-minutes: 20 # kilocode_change
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: windows-11-arm
package: "@kilocode/cli-windows-arm64"
- arch: x64
runner: windows-2025
package: "@kilocode/cli-windows-x64"
steps:
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: ${{ runner.temp }}
skip-decompress: true
- name: Install zstd
shell: pwsh
run: |
if (-not (Get-Command zstd -ErrorAction SilentlyContinue)) {
choco install zstandard -y --no-progress
}
- name: Unpack CLI dist
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
zstd -d --stdout "$env:RUNNER_TEMP\kilo-cli.tar.zst" | tar -xf - -C dist
- name: Run CLI smoke test
shell: pwsh
run: |
$package = "${{ matrix.package }}".Replace("/", "\")
$binary = ".\dist\$package\bin\kilo.exe"
& $binary --version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$root = Join-Path $env:RUNNER_TEMP ([guid]::NewGuid().ToString())
New-Item -ItemType Directory -Force -Path $root | Out-Null
try {
foreach ($name in "KILO_MODELS_PATH", "KILO_MODELS_URL", "KILO_CONFIG", "KILO_CONFIG_DIR") {
Remove-Item "Env:$name" -ErrorAction SilentlyContinue
}
$env:XDG_DATA_HOME = Join-Path $root "data"
$env:XDG_CACHE_HOME = Join-Path $root "cache"
$env:XDG_CONFIG_HOME = Join-Path $root "config"
$env:XDG_STATE_HOME = Join-Path $root "state"
$env:KILO_DISABLE_MODELS_FETCH = "1"
$env:KILO_DISABLE_PROJECT_CONFIG = "1"
$env:KILO_CONFIG_CONTENT = '{"enabled_providers":["anthropic"]}'
$env:ANTHROPIC_API_KEY = "dummy"
$output = & $binary --pure models anthropic
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (-not ($output -match "(?m)^anthropic/")) {
throw "Compiled Windows binary did not list Anthropic models from the embedded snapshot"
}
} finally {
Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue
}
# kilocode_change end
build-vscode:
sign-cli-windows:
needs:
- build-cli
- validate-cli-unix
- validate-cli-windows
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'Kilo-Org/kilocode'
- version
runs-on: blacksmith-4vcpu-windows-2025
if: github.repository == 'anomalyco/opencode'
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
steps:
- uses: actions/checkout@v6 # kilocode_change
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-windows
path: packages/opencode/dist
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
with:
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files: |
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Verify Windows CLI signatures
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
)
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
- name: Repack Windows CLI archives
working-directory: packages/opencode/dist
shell: pwsh
run: |
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
- name: Upload signed Windows CLI release assets
if: needs.version.outputs.release != ''
shell: pwsh
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
run: |
gh release upload "v${{ needs.version.outputs.version }}" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
--clobber `
--repo "${{ needs.version.outputs.repo }}"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-signed-windows
path: |
packages/opencode/dist/opencode-windows-arm64
packages/opencode/dist/opencode-windows-x64
packages/opencode/dist/opencode-windows-x64-baseline
build-electron:
needs:
- build-cli
- version
if: github.repository == 'anomalyco/opencode'
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
strategy:
fail-fast: false
matrix:
settings:
- host: macos-26-intel
target: x86_64-apple-darwin
platform_flag: --mac --x64
bun_install_flags: --os=darwin --cpu=x64
- host: macos-26
target: aarch64-apple-darwin
platform_flag: --mac --arm64
bun_install_flags: --os=darwin --cpu=arm64
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: "windows-2025"
target: aarch64-pc-windows-msvc
platform_flag: --win --arm64
- host: "blacksmith-4vcpu-windows-2025"
target: x86_64-pc-windows-msvc
platform_flag: --win
- host: "blacksmith-4vcpu-ubuntu-2404"
target: x86_64-unknown-linux-gnu
platform_flag: --linux
- host: "blacksmith-4vcpu-ubuntu-2404-arm"
target: aarch64-unknown-linux-gnu
platform_flag: --linux --arm64
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
if: runner.os == 'macOS'
with:
keychain: build
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Setup Apple API Key
if: runner.os == 'macOS'
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- uses: ./.github/actions/setup-bun
with:
install-flags: ${{ matrix.settings.bun_install_flags }}
- uses: actions/setup-node@v6 # kilocode_change
- name: Azure login
if: runner.os == 'Windows'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install @vscode/vsce
run: bun install -g @vscode/vsce
# kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli
- uses: actions/download-artifact@v8
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true
path: ~/apt-cache
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-
- name: Unpack CLI dist
- name: Install dependencies (ubuntu only)
if: contains(matrix.settings.host, 'ubuntu')
run: |
mkdir -p packages/opencode/dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C packages/opencode/dist
# kilocode_change end
- name: Build VSIX packages
run: bun script/build.ts
working-directory: ./packages/kilo-vscode
env:
CLI_DIST_DIR: ../../packages/opencode/dist
KILO_VERSION: ${{ needs.build-cli.outputs.version }}
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
GH_REPO: ${{ github.repository }}
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
sudo apt-get update
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" rpm
sudo chmod -R a+rw ~/apt-cache
- uses: actions/upload-artifact@v7 # kilocode_change
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
name: kilo-vscode
path: packages/kilo-vscode/out
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
# kilocode_change start
# Run smoke tests against CLI assets uploaded to the draft GitHub release
# before publishing the release and package artifacts.
smoke-test:
name: Smoke Test (pre-publish gate)
needs:
- version
- build-cli
if: github.repository == 'Kilo-Org/kilocode'
uses: ./.github/workflows/smoke-test.yml
with:
cli_version: ${{ needs.version.outputs.version }}
secrets: inherit
# kilocode_change end
- name: Prepare
run: bun ./scripts/prepare.ts
working-directory: packages/desktop
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
- name: Build
run: bun run build
working-directory: packages/desktop
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
- name: Package
if: needs.version.outputs.release
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
working-directory: packages/desktop
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
GH_TOKEN: ${{ steps.committer.outputs.token }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
- name: Package (no publish)
if: ${{ !needs.version.outputs.release }}
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
working-directory: packages/desktop
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
- name: Create macOS .app.tar.gz
if: runner.os == 'macOS' && needs.version.outputs.release
working-directory: packages/desktop/dist
run: |
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
APP_DIR="mac"
OUT_NAME="opencode-desktop-mac-x64.app.tar.gz"
elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then
APP_DIR="mac-arm64"
OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz"
else
echo "Unknown macOS target: ${{ matrix.settings.target }}"
exit 1
fi
APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "No .app bundle found in $APP_DIR"
exit 1
fi
tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
- name: Verify signed Windows Electron artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = @()
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
foreach ($file in $files | Select-Object -Unique) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-desktop-${{ matrix.settings.target }}
path: packages/desktop/dist/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: needs.version.outputs.release
with:
name: latest-yml-${{ matrix.settings.target }}
path: packages/desktop/dist/latest*.yml
publish:
needs:
- version
- build-cli
- build-vscode
- validate-cli-unix # kilocode_change
- validate-cli-windows # kilocode_change
- smoke-test
runs-on: ubuntu-24.04
- sign-cli-windows
- build-electron
if: always() && !failure() && !cancelled()
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v6 # kilocode_change
# kilocode_change start
with:
persist-credentials: false
# kilocode_change end
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: actions/setup-node@v6 # kilocode_change
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false # kilocode_change
- name: Install @vscode/vsce
run: bun install -g @vscode/vsce
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli
path: packages/opencode/dist
- name: Setup Git Committer
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-windows
path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-signed-windows
path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-preview-cli
path: packages/cli/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.version.outputs.release
with:
pattern: latest-yml-*
path: /tmp/latest-yml
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.version.outputs.release
with:
pattern: opencode-desktop-*
path: /tmp/desktop
merge-multiple: true
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
# kilocode_change start
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
# kilocode_change end
# kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli
- uses: actions/download-artifact@v8
with:
name: kilo-cli.tar.zst
path: /tmp
skip-decompress: true
- name: Unpack CLI dist
run: |
mkdir -p packages/opencode/dist
tar --zstd -xf /tmp/kilo-cli.tar.zst -C packages/opencode/dist
# kilocode_change end
- uses: actions/download-artifact@v8
with:
name: kilo-vscode
path: packages/kilo-vscode/out
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Cache apt packages (AUR)
uses: actions/cache@v5 # kilocode_change
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }}
@@ -462,18 +490,31 @@ jobs:
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com"
git config --global user.name "kilo-maintainer[bot]"
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Upload desktop release assets
if: needs.version.outputs.release
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
run: |
shopt -s nullglob
files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz)
if (( ${#files[@]} == 0 )); then
echo "No desktop release assets found"
exit 1
fi
gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}"
- run: ./script/publish.ts
env:
KILO_VERSION: ${{ needs.version.outputs.version }}
KILO_RELEASE: ${{ needs.version.outputs.release }}
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
GH_REPO: ${{ github.repository }}
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
NPM_CONFIG_PROVENANCE: true
VSCE_PAT: ${{ secrets.VSCE_TOKEN }}
OPENVSX_TOKEN: ${{ secrets.OVSX_TOKEN }}
GH_REPO: ${{ needs.version.outputs.repo }}
NPM_CONFIG_PROVENANCE: false
LATEST_YML_DIR: /tmp/latest-yml
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
@@ -22,13 +22,8 @@ jobs:
- run: git fetch --force --tags
- name: Setup Git Committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
- name: Release
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./github/script/release
@@ -6,14 +6,10 @@ on:
jobs:
check-guidelines:
# kilocode_change start - disabled for kilo-cli fork (requires OpenCode install and API keys)
if: false
# kilocode_change end
# Original condition:
# if: |
# github.event.issue.pull_request &&
# startsWith(github.event.comment.body, '/review') &&
# contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
@@ -51,7 +47,7 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KILO_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
PR_TITLE: ${{ steps.pr-details.outputs.title }}
run: |
PR_BODY=$(jq -r .body pr_data.json)
-151
View File
@@ -1,151 +0,0 @@
# kilocode_change - new file
# Standalone smoke test — checks out kilo-bench and runs a small set of
# Harbor eval tasks against the latest published CLI (or a specific version).
#
# Tasks: hello-world (~$0.01), log-summary-date-ranges (~$0.13)
# Expected total: < $0.50, < 15 min wall clock
#
# Triggers:
# - workflow_dispatch: manually from Actions tab (optionally pass a CLI version)
# - workflow_call: from publish.yml after draft release assets are uploaded
#
# Required secrets:
# KILO_API_KEY — Kilo Gateway key
# KILO_ORG_ID — Kilo org ID
# BENCH_GITHUB_TOKEN — PAT with contents:read on Kilo-Org/kilo-bench (private repo)
name: smoke-test
on:
workflow_dispatch:
inputs:
cli_version:
description: "CLI version to test (e.g. 7.0.36). Leave blank for latest npm release."
required: false
type: string
workflow_call:
inputs:
cli_version:
description: "CLI version to test from draft release assets."
required: false
type: string
concurrency:
group: smoke-test
cancel-in-progress: false
permissions:
contents: write # needed to read draft release assets when called from publish.yml
jobs:
smoke-test:
name: Smoke Test
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
env:
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
steps:
- name: Checkout kilo-bench
uses: actions/checkout@v6 # kilocode_change
with:
repository: Kilo-Org/kilo-bench
token: ${{ secrets.BENCH_GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Set up Python
run: uv python install 3.13
- name: Install dependencies
run: uv sync --no-dev
- name: Validate API key
run: |
if [[ -z "$KILO_API_KEY" ]]; then
echo "::error::KILO_API_KEY secret is not set."
exit 1
fi
- name: Resolve CLI asset URL
id: cli
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ inputs.cli_version }}"
if [[ -z "$VERSION" ]]; then
echo "cli_url=" >> "$GITHUB_OUTPUT"
echo "::notice::Testing latest CLI from npm"
exit 0
fi
# Resolve the release asset via the API so this works for draft
# releases too (browser /releases/download/... URLs 404 on drafts).
# The installer passes Accept: application/octet-stream + bearer
# auth, which is what the API asset endpoint expects.
# The tag_name filter yields at most one release and one asset,
# so --jq returns a single URL without needing head/pipefail games.
URL=$(gh api "repos/${{ github.repository }}/releases" \
--jq ".[] | select(.tag_name == \"v${VERSION}\") | .assets[] | select(.name == \"kilo-linux-x64.tar.gz\") | .url")
if [[ -z "$URL" ]]; then
echo "::error::asset kilo-linux-x64.tar.gz not found for v${VERSION}"
exit 1
fi
echo "cli_url=$URL" >> "$GITHUB_OUTPUT"
echo "::notice::Testing CLI v${VERSION} via asset API: $URL"
# Harbor's default agent-setup timeout is 360s. The hello-world container
# (FROM ubuntu:24.04) needs apt-get update + apt-get install + a NodeSource
# curl|bash + apt install nodejs before the CLI even downloads, and on
# the Blacksmith runners that occasionally pushes past 6 min when an apt
# mirror or NodeSource cdn is slow, killing the run with
# AgentSetupTimeoutError. Doubling the multiplier to 2 gives enough
# headroom for transient mirror/cdn slowness while still finishing well
# under timeout-minutes. Note: --timeout-multiplier scales every timeout
# uniformly (setup, agent, verifier, env build), but they're all upper
# limits so this is harmless.
- name: Run smoke test — hello-world
env:
KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }}
KILO_CLI_GITHUB_TOKEN: ${{ github.token }}
run: |
./scripts/run_eval.sh \
-m kilo/anthropic/claude-sonnet-4.6 \
-d hello-world \
--job-name smoke-test-hello-world \
--timeout-multiplier 2
- name: Run smoke test — log-summary-date-ranges
env:
KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }}
KILO_CLI_GITHUB_TOKEN: ${{ github.token }}
run: |
./scripts/run_eval.sh \
-m kilo/anthropic/claude-sonnet-4.6 \
-d terminal-bench-sample \
--include-task-name "log-summary-date-ranges" \
--job-name smoke-test-log-summary \
--timeout-multiplier 2
- name: Validate results
run: python3 scripts/validate_smoke_test.py jobs/smoke-test-*/
# Also upload the agent setup logs (stdout/stderr/return-code from the
# CLI install script) so we can see exactly which step stalls when the
# next AgentSetupTimeoutError happens. These come from the install
# script in kilo-bench, which uses `set -euo pipefail` (no `set -x`)
# and never echoes auth tokens or API keys, so the captured output is
# safe to upload.
- name: Upload results
if: always()
uses: actions/upload-artifact@v7 # kilocode_change
with:
name: smoke-test-results
path: |
jobs/smoke-test-*/**/result.json
jobs/smoke-test-*/**/trajectory.json
jobs/smoke-test-*/**/agent/setup/*.txt
retention-days: 30
if-no-files-found: warn
-24
View File
@@ -1,24 +0,0 @@
name: Check Source Links
on:
pull_request:
paths:
- "packages/kilo-vscode/src/**"
- "packages/kilo-vscode/webview-ui/**"
- "packages/opencode/src/**"
- "packages/kilo-docs/source-links.md"
- "script/extract-source-links.ts"
- ".github/workflows/source-check-links.yml"
workflow_dispatch: # Allow manual trigger
jobs:
source-links-freshness:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # kilocode_change
- uses: oven-sh/setup-bun@v2
- name: Check source-links.md is up to date
run: bun run script/extract-source-links.ts --check
@@ -9,6 +9,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
stats:
if: github.repository == 'anomalyco/opencode'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
-115
View File
@@ -1,115 +0,0 @@
# kilocode_change - new file
name: test-jetbrains
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
checks: write
pull-requests: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
changes:
name: detect JetBrains changes
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }}
steps:
- name: Checkout repository
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@v6
- name: Detect JetBrains changes
if: github.event_name != 'workflow_dispatch'
id: filter
uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3
with:
predicate-quantifier: every
filters: |
jetbrains:
- '**'
- '!.changeset/**'
- '!packages/kilo-vscode/**'
- '!packages/kilo-docs/**'
unit:
name: jetbrains
needs: changes
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Mark workspace as git-safe
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: package.json
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Run JetBrains unit tests
run: bun script/test-ci.ts
working-directory: packages/kilo-jetbrains
- name: Publish JetBrains unit reports
if: always()
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0
with:
report_paths: packages/kilo-jetbrains/.artifacts/unit/junit.xml
check_name: "unit results (jetbrains)"
detailed_summary: true
include_time_in_summary: true
fail_on_failure: false
- name: Upload JetBrains unit artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: unit-jetbrains-${{ github.run_attempt }}
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
path: packages/kilo-jetbrains/.artifacts/unit/junit.xml
required:
name: result
needs:
- changes
- unit
if: always()
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Verify JetBrains jobs passed
run: |
echo "changes=${{ needs.changes.result }}"
echo "jetbrains=${{ needs.unit.result }}"
test "${{ needs.changes.result }}" = "success"
if [ "${{ needs.changes.outputs.jetbrains }}" = "true" ]; then
test "${{ needs.unit.result }}" = "success"
else
test "${{ needs.changes.outputs.jetbrains }}" = "false"
test "${{ needs.unit.result }}" = "skipped"
fi
-55
View File
@@ -1,55 +0,0 @@
name: test-vscode
on:
push:
branches:
- main
- dev
paths:
- "packages/kilo-vscode/**"
- "packages/ui/**"
- "packages/kilo-ui/**"
- ".github/workflows/test-vscode.yml"
pull_request:
paths:
- "packages/kilo-vscode/**"
- "packages/ui/**"
- "packages/kilo-ui/**"
- ".github/workflows/test-vscode.yml"
workflow_dispatch:
jobs:
unit:
name: unit tests
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Run unit tests
working-directory: packages/kilo-vscode
run: bun run test:unit
- name: Check lint (ESLint)
working-directory: packages/kilo-vscode
run: bun run lint
- name: Check formatting (prettier)
working-directory: packages/kilo-vscode
run: bun run format:check
- name: Check for dead code (knip)
working-directory: packages/kilo-vscode
run: bun run knip
- name: Check for kilocode_change markers
working-directory: packages/kilo-vscode
run: bun run check-kilocode-change
+78 -209
View File
@@ -3,14 +3,14 @@ name: test
on:
push:
branches:
- main # kilocode_change
- dev
pull_request:
workflow_dispatch:
concurrency:
# Keep every run on main so cancelled checks do not pollute the default branch # kilocode_change
# Keep every run on dev so cancelled checks do not pollute the default branch
# commit history. PRs and other branches still share a group and cancel stale runs.
group: ${{ case(github.ref == 'refs/heads/main', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} # kilocode_change
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
cancel-in-progress: true
permissions:
@@ -21,256 +21,125 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# kilocode_change start
changes:
name: detect general unit test changes
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
outputs:
settings: ${{ steps.matrix.outputs.settings }}
general: ${{ steps.matrix.outputs.general }}
steps:
- name: Checkout repository
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@v6
- name: Detect general unit test changes
if: github.event_name != 'workflow_dispatch'
id: filter
uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3
with:
predicate-quantifier: every
filters: |
general:
- '**'
- '!.changeset/**'
- '!packages/kilo-jetbrains/**'
- '!packages/kilo-vscode/**'
- '!packages/kilo-docs/**'
- '!packages/extensions/zed/**'
- '!specs/**'
- '!perf/**'
- name: Configure unit matrix
id: matrix
env:
GENERAL: ${{ github.event_name != 'pull_request' || steps.filter.outputs.general == 'true' }}
run: |
if [ "$GENERAL" != "true" ]; then
echo 'general=false' >> "$GITHUB_OUTPUT"
echo 'settings=[{"os":"linux","index":1,"total":1,"host":"blacksmith-4vcpu-ubuntu-2404","run":false,"packages":false}]' >> "$GITHUB_OUTPUT"
exit 0
fi
echo 'general=true' >> "$GITHUB_OUTPUT"
echo 'settings=[{"os":"linux","index":1,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":true},{"os":"linux","index":2,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":false},{"os":"macos","index":1,"total":1,"host":"macos-15","run":true,"packages":true},{"os":"windows","index":1,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":true},{"os":"windows","index":2,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":3,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":4,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false}]' >> "$GITHUB_OUTPUT"
# kilocode_change end
unit:
# kilocode_change start
name: ${{ !matrix.settings.run && 'unit (unchanged)' || matrix.settings.total > 1 && format('unit ({0}, {1}/{2})', matrix.settings.os, matrix.settings.index, matrix.settings.total) || format('unit ({0})', matrix.settings.os) }}
needs: changes
# kilocode_change end
name: unit (${{ matrix.settings.name }})
strategy:
fail-fast: false
matrix:
settings: ${{ fromJSON(needs.changes.outputs.settings) }} # kilocode_change
settings:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
- name: windows
host: blacksmith-4vcpu-windows-2025
runs-on: ${{ matrix.settings.host }}
timeout-minutes: 45 # kilocode_change
defaults:
run:
shell: bash
steps:
# kilocode_change start
- name: Skip unchanged general unit tests
if: ${{ !matrix.settings.run }}
run: echo "Only isolated product, documentation, or metadata files changed; general unit tests are unchanged."
# kilocode_change end
- name: Checkout repository
if: matrix.settings.run # kilocode_change
uses: actions/checkout@v6 # kilocode_change
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
# kilocode_change start
- name: Setup Node
if: matrix.settings.run
id: setup-node
continue-on-error: ${{ runner.os == 'Windows' }}
uses: actions/setup-node@v6
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
# kilocode_change end
# kilocode_change start
- name: Retry Setup Node on Windows
if: matrix.settings.run && runner.os == 'Windows' && steps.setup-node.outcome == 'failure'
uses: actions/setup-node@v6
with:
node-version: "24"
# kilocode_change end
- name: Setup Bun
if: matrix.settings.run # kilocode_change
uses: ./.github/actions/setup-bun
# kilocode_change start
- name: Setup Linux sandbox helper
if: matrix.settings.run && runner.os == 'Linux'
uses: ./.github/actions/setup-linux-sandbox
# kilocode_change end
- name: Configure git identity
if: matrix.settings.run # kilocode_change
# kilocode_change start
run: |
git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com"
git config --global user.name "kilo-maintainer[bot]"
# kilocode_change end
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
- name: Cache Turbo
if: matrix.settings.run # kilocode_change
uses: actions/cache@v5 # kilocode_change
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache # kilocode_change
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-${{ matrix.settings.os }}-${{ matrix.settings.index }}-${{ github.sha }} # kilocode_change
# kilocode_change start
path: node_modules/.cache/turbo
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-${{ matrix.settings.os }}-${{ matrix.settings.index }}-
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-
turbo-${{ runner.os }}-
# kilocode_change end
# kilocode_change start - test non-CLI packages separately from sharded CLI tests
- name: Run non-CLI unit tests
if: matrix.settings.run && matrix.settings.packages
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='!@kilocode/cli' --filter='!@kilocode/kilo-jetbrains'
# kilocode_change end
# kilocode_change start - ensure the Darwin profile cannot suppress its own validation
- name: Validate Darwin CLI test profile
if: matrix.settings.run && matrix.settings.os == 'macos'
working-directory: packages/opencode
run: bun test test/kilocode/test-profile.test.ts
# kilocode_change end
# kilocode_change start - run Kilo CLI tests through platform-specific shards
- name: Run CLI unit tests
if: matrix.settings.run
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli'
- name: Run unit tests
timeout-minutes: 20
run: bun turbo test --output-logs=errors-only --log-order=grouped --log-prefix=task
env:
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }}
KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }}
# kilocode_change end
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
# kilocode_change start
- name: Publish unit reports
if: always() && matrix.settings.run
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0
with:
report_paths: packages/*/.artifacts/unit/junit.xml
annotate_only: true
detailed_summary: true
include_time_in_summary: true
fail_on_failure: false
- name: Run HttpApi exerciser gates
if: runner.os == 'Linux'
working-directory: packages/opencode
run: bun run test:httpapi
- name: Upload unit artifacts
if: always() && matrix.settings.run
uses: actions/upload-artifact@v7
with:
name: unit-${{ matrix.settings.os }}-${{ matrix.settings.index }}-${{ github.run_attempt }}
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
path: packages/*/.artifacts/unit/junit.xml
# kilocode_change end
# kilocode_change start
httpapi:
name: HttpApi exerciser
needs: changes
if: needs.changes.outputs.general == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 15
e2e:
name: e2e (${{ matrix.settings.name }})
strategy:
fail-fast: false
matrix:
settings:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
- name: windows
host: blacksmith-4vcpu-windows-2025
runs-on: ${{ matrix.settings.host }}
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Linux sandbox helper
uses: ./.github/actions/setup-linux-sandbox
- name: Configure git identity
- name: Read Playwright version
id: playwright-version
run: |
git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com"
git config --global user.name "kilo-maintainer[bot]"
version=$(node -e 'console.log(require("./package.json").workspaces.catalog["@playwright/test"])')
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Turbo
uses: actions/cache@v5
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-httpapi-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-httpapi-
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', 'bun.lock') }}-
turbo-${{ runner.os }}-
path: ${{ github.workspace }}/.playwright-browsers
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
- name: Run HttpApi exerciser gates
run: bun turbo test:httpapi --filter='@kilocode/cli'
# kilocode_change end
- name: Install Playwright system dependencies
if: runner.os == 'Linux'
working-directory: packages/app
run: bunx playwright install-deps chromium
# kilocode_change start
jetbrains:
name: jetbrains
permissions:
contents: read
checks: write
pull-requests: read
uses: ./.github/workflows/test-jetbrains.yml
# kilocode_change end
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: packages/app
run: bunx playwright install chromium
# kilocode_change start
unit-required:
name: unit (linux)
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- unit
if: always()
steps:
- name: Verify unit matrix passed
run: |
echo "unit=${{ needs.unit.result }}"
test "${{ needs.unit.result }}" = "success"
# kilocode_change end
- name: Run app e2e tests
run: bun --cwd packages/app test:e2e:local
env:
CI: true
timeout-minutes: 30
# kilocode_change start
required:
name: test (linux)
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- changes
- unit
- httpapi
- jetbrains
if: always()
steps:
- name: Verify upstream test jobs passed # kilocode_change
run: |
echo "unit=${{ needs.unit.result }}"
echo "httpapi=${{ needs.httpapi.result }}"
echo "jetbrains=${{ needs.jetbrains.result }}"
test "${{ needs.unit.result }}" = "success"
if [ "${{ needs.changes.outputs.general }}" = "true" ]; then
test "${{ needs.httpapi.result }}" = "success"
else
test "${{ needs.httpapi.result }}" = "skipped"
fi
test "${{ needs.jetbrains.result }}" = "success"
# kilocode_change end
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
if-no-files-found: ignore
retention-days: 7
path: |
packages/app/e2e/test-results
packages/app/e2e/playwright-report
@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://kilo.ai/install | bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Triage issue
env:
+7 -88
View File
@@ -2,101 +2,20 @@ name: typecheck
on:
push:
branches:
- main
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
typecheck-js:
name: typecheck-js
runs-on: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
typecheck:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v6 # kilocode_change
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun
# kilocode_change start
- name: Run TypeScript typecheck
run: bun turbo typecheck --filter='!@kilocode/kilo-jetbrains'
- name: Build Kilo Console
run: bun turbo build --filter=@kilocode/kilo-console
# kilocode_change end
# kilocode_change start
jetbrains-changes:
name: detect JetBrains changes
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }}
steps:
- name: Checkout repository
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@v6
- name: Detect JetBrains changes
if: github.event_name != 'workflow_dispatch'
id: filter
uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3
with:
predicate-quantifier: every
filters: |
jetbrains:
- '**'
- '!.changeset/**'
- '!packages/kilo-vscode/**'
- '!packages/kilo-docs/**'
typecheck-jetbrains:
name: typecheck-jetbrains
needs: jetbrains-changes
if: github.event_name == 'workflow_dispatch' || needs.jetbrains-changes.outputs.jetbrains == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Run JetBrains typecheck
run: ./gradlew typecheck
working-directory: packages/kilo-jetbrains
required:
name: typecheck
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- typecheck-js
- jetbrains-changes
- typecheck-jetbrains
if: always()
steps:
- name: Verify typecheck jobs passed
run: |
echo "typecheck-js=${{ needs.typecheck-js.result }}"
echo "jetbrains-changes=${{ needs.jetbrains-changes.result }}"
echo "typecheck-jetbrains=${{ needs.typecheck-jetbrains.result }}"
test "${{ needs.typecheck-js.result }}" = "success"
test "${{ needs.jetbrains-changes.result }}" = "success"
if [ "${{ needs.jetbrains-changes.outputs.jetbrains }}" = "true" ]; then
test "${{ needs.typecheck-jetbrains.result }}" = "success"
else
test "${{ needs.jetbrains-changes.outputs.jetbrains }}" = "false"
test "${{ needs.typecheck-jetbrains.result }}" = "skipped"
fi
# kilocode_change end
- name: Run typecheck
run: bun typecheck
-412
View File
@@ -1,412 +0,0 @@
# kilocode_change - new file
name: Visual Regression Tests
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
check-paths:
name: Check changed paths
runs-on: ubuntu-latest # kilocode_change
outputs:
matched: ${{ steps.filter.outputs.matched }}
can_autocommit: ${{ steps.autocommit-check.outputs.can_autocommit }}
steps:
- uses: actions/checkout@v6
- name: Check changed files
id: filter
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: |
matched=false
while IFS= read -r file; do
case "$file" in
packages/kilo-ui/*|packages/ui/*|packages/util/*|packages/sdk/js/*|packages/kilo-vscode/webview-ui/*|packages/kilo-vscode/.storybook/*|packages/kilo-vscode/tests/visual-regression*|packages/kilo-vscode/tests/permission-dock-dropdown*|packages/kilo-vscode/tests/accessibility*|packages/kilo-docs/public/img/screenshot-tests/*|.github/actions/setup-bun/*|.github/workflows/visual-regression.yml)
matched=true
break
;;
esac
done < <(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR}/files" --jq '.[].filename')
echo "matched=$matched" >> "$GITHUB_OUTPUT"
echo "matched=$matched"
- name: Check if PR is from a fork
id: fork-check
run: |
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "is_fork=true" >> "$GITHUB_OUTPUT"
else
echo "is_fork=false" >> "$GITHUB_OUTPUT"
fi
- name: Check baseline auto-commit permissions
id: autocommit-check
env:
MAINTAINER_APP_ID: ${{ secrets.KILO_MAINTAINER_APP_ID }}
MAINTAINER_APP_SECRET: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
run: |
if [ "${{ steps.fork-check.outputs.is_fork }}" != "true" ] && [ -n "$MAINTAINER_APP_ID" ] && [ -n "$MAINTAINER_APP_SECRET" ]; then
echo "can_autocommit=true" >> "$GITHUB_OUTPUT"
else
echo "can_autocommit=false" >> "$GITHUB_OUTPUT"
fi
visual-regression:
needs: check-paths
if: needs.check-paths.outputs.matched == 'true'
name: Visual Regression (kilo-ui) # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 15
steps:
- name: Checkout (internal)
if: needs.check-paths.outputs.can_autocommit == 'true'
uses: actions/checkout@v6
with:
lfs: true
# Use github.token for LFS access. The maintainer app is used only for generated commits.
token: ${{ github.token }}
ref: ${{ github.head_ref }}
- name: Checkout (read-only)
if: needs.check-paths.outputs.can_autocommit != 'true'
uses: actions/checkout@v6
with:
lfs: true
- name: Check if HEAD is a baseline update commit
if: needs.check-paths.outputs.can_autocommit == 'true'
id: check-baseline-commit
run: |
COMMIT_MSG=$(git log -1 --format=%s)
if [ "$COMMIT_MSG" = "chore: update visual regression baselines" ] || [ "$COMMIT_MSG" = "chore: update kilo-vscode visual regression baselines" ]; then
echo "is_baseline_update=true" >> "$GITHUB_OUTPUT"
echo "HEAD commit is a baseline update commit — will not auto-commit again."
else
echo "is_baseline_update=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
uses: ./.github/actions/setup-bun # kilocode_change
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('packages/kilo-ui/package.json') }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: bunx playwright install chromium
working-directory: packages/kilo-ui
- name: Install Playwright system deps
run: bunx playwright install-deps chromium
working-directory: packages/kilo-ui
- name: Cache Storybook build
id: storybook-cache
uses: actions/cache@v5
with:
path: packages/kilo-ui/storybook-static
key: storybook-${{ hashFiles('packages/kilo-ui/src/**', 'packages/kilo-ui/.storybook/**', 'packages/ui/src/**', 'packages/kilo-ui/package.json') }}
- name: Build Storybook
if: steps.storybook-cache.outputs.cache-hit != 'true'
run: bun run build-storybook
working-directory: packages/kilo-ui
- name: Generate baselines for new/missing stories
run: bun run test:visual:update
working-directory: packages/kilo-ui
env:
CI: true
PLAYWRIGHT_WORKERS: "4"
- name: Remove stale baselines for deleted stories
run: |
node -e "
const fs = require('fs'), path = require('path');
const idx = JSON.parse(fs.readFileSync('storybook-static/index.json', 'utf8'));
const stories = Object.values(idx.entries || idx.stories || {}).filter(s => s.id && !s.id.endsWith('--docs'));
const expected = new Set(stories.map(s => { const [c,v] = s.id.split('--'); return c + '/' + v + '-chromium-linux.png'; }));
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-ui/visual-regression';
if (!fs.existsSync(dir)) process.exit(0);
for (const component of fs.readdirSync(dir)) {
const cdir = path.join(dir, component);
if (!fs.statSync(cdir).isDirectory()) continue;
for (const file of fs.readdirSync(cdir)) {
const rel = component + '/' + file;
if (file.endsWith('.png') && !expected.has(rel)) { console.log('Removing stale baseline:', rel); fs.unlinkSync(path.join(cdir, file)); }
}
}
"
working-directory: packages/kilo-ui
- name: Check for baseline changes (read-only PRs)
if: needs.check-paths.outputs.can_autocommit != 'true'
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
if git diff --cached --quiet; then
echo "No visual regression detected."
else
echo "::error::Visual regression detected. Screenshot baselines have changed."
echo "::error::Since this PR cannot receive baseline commits automatically, updated screenshots cannot be committed."
echo "::error::Please ask a Kilo developer for help updating the screenshots."
git diff --cached --stat
exit 1
fi
- name: Fail if baselines still changing after auto-update
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update == 'true'
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
if git diff --cached --quiet; then
echo "Baselines are stable after auto-update."
else
echo "::error::Visual regression baselines changed again after a previous auto-update commit."
echo "::error::This indicates non-deterministic screenshots that would cause an infinite update loop."
echo "::error::Please investigate the flaky screenshots and add non-deterministic stories to the SKIP set."
git diff --cached --stat
exit 1
fi
- name: Check for baseline updates
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update != 'true'
id: baseline-changes
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup Git Committer
if: steps.baseline-changes.outputs.changed == 'true'
uses: ./.github/actions/setup-git-committer
with:
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
- name: Commit and push new baselines (if any)
if: steps.baseline-changes.outputs.changed == 'true'
id: commit-baselines
run: |
git commit -m "chore: update visual regression baselines"
git lfs push --all origin
git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}"
echo "changed=true" >> "$GITHUB_OUTPUT"
- name: Fail if baselines changed
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines.outputs.changed == 'true'
run: |
echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review."
exit 1
- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: visual-regression-results
path: packages/kilo-ui/test-results/
retention-days: 7
visual-regression-vscode:
needs: check-paths
if: needs.check-paths.outputs.matched == 'true'
name: Visual Regression (kilo-vscode webview) # kilocode_change
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 15
env:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout (internal)
if: needs.check-paths.outputs.can_autocommit == 'true'
uses: actions/checkout@v6
with:
lfs: true
# Use github.token for LFS access. The maintainer app is used only for generated commits.
token: ${{ github.token }}
ref: ${{ github.head_ref }}
- name: Checkout (read-only)
if: needs.check-paths.outputs.can_autocommit != 'true'
uses: actions/checkout@v6
with:
lfs: true
- name: Check if HEAD is a baseline update commit
if: needs.check-paths.outputs.can_autocommit == 'true'
id: check-baseline-commit-vscode
run: |
COMMIT_MSG=$(git log -1 --format=%s)
if [ "$COMMIT_MSG" = "chore: update visual regression baselines" ] || [ "$COMMIT_MSG" = "chore: update kilo-vscode visual regression baselines" ]; then
echo "is_baseline_update=true" >> "$GITHUB_OUTPUT"
echo "HEAD commit is a baseline update commit — will not auto-commit again."
else
echo "is_baseline_update=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
uses: ./.github/actions/setup-bun # kilocode_change
- name: Cache Playwright browsers
id: playwright-cache-vscode
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-vscode-${{ hashFiles('packages/kilo-vscode/package.json') }}
- name: Install Playwright browsers
if: steps.playwright-cache-vscode.outputs.cache-hit != 'true'
run: bunx playwright install chromium
working-directory: packages/kilo-vscode
- name: Install Playwright system deps
run: bunx playwright install-deps chromium
working-directory: packages/kilo-vscode
- name: Cache Storybook build
id: storybook-cache-vscode
uses: actions/cache@v5
with:
path: packages/kilo-vscode/storybook-static
key: storybook-vscode-${{ hashFiles('packages/kilo-vscode/webview-ui/src/**', 'packages/kilo-vscode/.storybook/**', 'packages/kilo-ui/src/**', 'packages/ui/src/**', 'packages/kilo-vscode/package.json') }}
- name: Build Storybook
if: steps.storybook-cache-vscode.outputs.cache-hit != 'true'
run: bun run build-storybook
working-directory: packages/kilo-vscode
env:
NODE_OPTIONS: --max-old-space-size=4096
- name: Generate baselines and enforce webview accessibility checks
run: bun run test:visual:update
working-directory: packages/kilo-vscode
env:
CI: true
PLAYWRIGHT_WORKERS: "4"
NODE_OPTIONS: --max-old-space-size=4096
- name: Remove stale baselines for deleted stories
run: |
node -e "
const fs = require('fs'), path = require('path');
const idx = JSON.parse(fs.readFileSync('storybook-static/index.json', 'utf8'));
const stories = Object.values(idx.entries || idx.stories || {}).filter(s => s.id && !s.id.endsWith('--docs'));
const expected = new Set(stories.map(s => { const [c,v] = s.id.split('--'); return c + '/' + v + '-chromium-linux.png'; }));
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression';
if (!fs.existsSync(dir)) process.exit(0);
for (const component of fs.readdirSync(dir)) {
const cdir = path.join(dir, component);
if (!fs.statSync(cdir).isDirectory()) continue;
for (const file of fs.readdirSync(cdir)) {
const rel = component + '/' + file;
if (file.endsWith('.png') && !expected.has(rel)) { console.log('Removing stale baseline:', rel); fs.unlinkSync(path.join(cdir, file)); }
}
}
"
working-directory: packages/kilo-vscode
- name: Remove stale baselines for permission-dock-dropdown tests
run: |
node -e "
const fs = require('fs');
const specPath = 'tests/permission-dock-dropdown.spec.ts';
const expected = new Set();
if (fs.existsSync(specPath)) {
const spec = fs.readFileSync(specPath, 'utf8');
const re = /toHaveScreenshot\(\[([^\]]+)\]\)/g;
let m;
while ((m = re.exec(spec)) !== null) {
const parts = m[1].split(',').map(s => s.trim().replace(/[\"']/g, ''));
const file = parts[parts.length - 1];
if (file) expected.add(file.replace(/\.png$/, '-chromium-linux.png'));
}
}
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-vscode/permission-dock-dropdown';
if (!fs.existsSync(dir)) process.exit(0);
for (const file of fs.readdirSync(dir)) {
if (file.endsWith('.png') && !expected.has(file)) {
console.log('Removing stale baseline:', file);
fs.unlinkSync(dir + '/' + file);
}
}
"
working-directory: packages/kilo-vscode
- name: Check for baseline changes (read-only PRs)
if: needs.check-paths.outputs.can_autocommit != 'true'
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
if git diff --cached --quiet; then
echo "No visual regression detected."
else
echo "::error::Visual regression detected. Screenshot baselines have changed."
echo "::error::Since this PR cannot receive baseline commits automatically, updated screenshots cannot be committed."
echo "::error::Please ask a Kilo developer for help updating the screenshots."
git diff --cached --stat
exit 1
fi
- name: Fail if baselines still changing after auto-update
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update == 'true'
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
if git diff --cached --quiet; then
echo "Baselines are stable after auto-update."
else
echo "::error::Visual regression baselines changed again after a previous auto-update commit."
echo "::error::This indicates non-deterministic screenshots that would cause an infinite update loop."
echo "::error::Please investigate the flaky screenshots and add non-deterministic stories to the SKIP set."
git diff --cached --stat
exit 1
fi
- name: Check for baseline updates
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update != 'true'
id: baseline-changes-vscode
run: |
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup Git Committer
if: steps.baseline-changes-vscode.outputs.changed == 'true'
uses: ./.github/actions/setup-git-committer
with:
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
- name: Commit and push new baselines (if any)
if: steps.baseline-changes-vscode.outputs.changed == 'true'
id: commit-baselines-vscode
run: |
git commit -m "chore: update kilo-vscode visual regression baselines"
git lfs push --all origin
git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}"
echo "changed=true" >> "$GITHUB_OUTPUT"
- name: Fail if baselines changed
if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines-vscode.outputs.changed == 'true'
run: |
echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review."
exit 1
- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: visual-regression-vscode-results
path: packages/kilo-vscode/test-results/
retention-days: 7
@@ -1,71 +0,0 @@
on:
schedule:
- cron: "7,37 * * * *" # every 30 min at :07 and :37
workflow_dispatch:
concurrency:
group: opencode-release-watch
cancel-in-progress: false
permissions:
contents: read
jobs:
check-release:
if: vars.OPENCODE_WATCH_ENABLED != 'false' && (github.event_name == 'workflow_dispatch' || github.repository == 'Kilo-Org/kilocode') # kilocode_change
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- id: fetch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
data=$(gh release view --repo anomalyco/opencode --json tagName,name,url 2>/dev/null || echo '{}')
tag=$(echo "$data" | jq -r '.tagName // empty')
[ -z "$tag" ] && { echo "tag=" >> "$GITHUB_OUTPUT"; exit 0; }
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "name=$(echo "$data" | jq -r '.name // .tagName')" >> "$GITHUB_OUTPUT"
echo "url=$(echo "$data" | jq -r '.url')" >> "$GITHUB_OUTPUT"
- if: steps.fetch.outputs.tag != ''
id: cache
uses: actions/cache/restore@v4
with:
path: .cache/last-seen
key: last-seen-${{ steps.fetch.outputs.tag }}
restore-keys: last-seen-
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
name: Trigger opencode sync
env:
URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }}
SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }}
VERCEL: ${{ secrets.OPENCODE_WATCH_VERCEL_SECRET }}
run: |
if [ -n "$URL" ] && [ -n "$SECRET" ]; then
headers=(-H "x-sync-secret: $SECRET" -H "Content-Type: application/json")
[ -n "$VERCEL" ] && headers+=(-H "x-vercel-protection-bypass: $VERCEL")
curl -sf -X POST "$URL" "${headers[@]}" -d '{}' || true
fi
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
name: Notify Slack
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.OPENCODE_WATCH_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
name: "${{ steps.fetch.outputs.name }}"
tag: "${{ steps.fetch.outputs.tag }}"
url: "${{ steps.fetch.outputs.url }}"
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
name: Mark tag as seen
run: mkdir -p .cache/last-seen && echo "${{ steps.fetch.outputs.tag }}" > .cache/last-seen/tag.txt
- if: success() && steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
name: Save seen-tag cache
uses: actions/cache/save@v4
with:
path: .cache/last-seen
key: last-seen-${{ steps.fetch.outputs.tag }}
+2 -40
View File
@@ -1,29 +1,17 @@
.husky/_
.DS_Store
.secrets/
node_modules
.worktrees
.sst
.env
.env.local
.idea/*
!.idea/gradle.xml
!.idea/.run
.idea/*.iml
.vscode/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
# use vscode workspaces with peacock to have many vscodes with the same repo:
*.code-workspace
.idea
.vscode
.codex
*~
playground
tmp
dist
ts-dist
storybook-static
.turbo
**/.serena
.serena/
@@ -37,36 +25,10 @@ a.out
target
.scripts
.direnv/
packages/kilo-jetbrains/build-tasks/.gradle/
# Local dev files
opencode-dev
.kilo-dev/
UPCOMING_CHANGELOG.md
logs/
*.bun-build
# Telemetry ID
telemetry-id
tsconfig.tsbuildinfo
# Kilo
.kilo/.gitignore
.kilo/package.json
.kilo/package-lock.json
.kilo/pnpm-lock.yaml
.kilo/bun.lock
.kilo/yarn.lock
.kilo/node_modules
.kilo/plans/*upstream-merge-report-*.md
.kilocode/.gitignore
.kilocode/package.json
.kilocode/package-lock.json
.kilocode/pnpm-lock.yaml
.kilocode/bun.lock
.kilocode/yarn.lock
.kilocode/node_modules
# Test Artifacts
packages/app/.artifacts/
packages/opencode/.artifacts/
+1 -31
View File
@@ -17,34 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
}
'
# kilocode_change start
jetbrains=0
zero=0000000000000000000000000000000000000000
while read -r local_ref local_oid remote_ref remote_oid; do
if [ "$local_oid" = "$zero" ]; then
continue
fi
base="$remote_oid"
if [ "$remote_oid" = "$zero" ]; then
base=$(git merge-base "$local_oid" "$1/main" 2>/dev/null || git hash-object -t tree /dev/null)
fi
if ! git diff --quiet "$base" "$local_oid" -- . \
':(exclude).changeset/**' \
':(exclude)packages/kilo-vscode/**' \
':(exclude)packages/kilo-docs/**'; then
jetbrains=1
fi
done
bun typecheck --filter='!@kilocode/kilo-jetbrains'
if [ "$jetbrains" -eq 1 ]; then
bun typecheck --filter='@kilocode/kilo-jetbrains'
else
echo "Skipping JetBrains typecheck (no relevant changes)"
fi
# kilocode_change end
bun typecheck
-32
View File
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT">
<builds>
<build path="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" name="build-tasks">
<projects>
<project path="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" />
</projects>
</build>
</builds>
</compositeBuild>
</compositeConfiguration>
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
<option name="gradleJvm" value="#JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/packages/kilo-jetbrains" />
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/backend" />
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" />
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/frontend" />
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/shared" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-421
View File
@@ -1,421 +0,0 @@
---
description: Resolve upstream opencode merge conflicts interactively
mode: primary
permission:
read: ask
edit: ask
webfetch: ask
bash:
"*": ask
"git status *": allow
"git log *": allow
"git diff *": allow
"git show *": allow
"git ls-files *": allow
"git ls-tree *": allow
"git grep *": allow
"git hash-object *": allow
"git remote -v *": allow
"git rev-parse *": allow
"git merge-base *": allow
"git show-ref *": allow
"git worktree list": allow
"git branch --show-current": allow
"grep *": allow
"rg *": allow
"head *": allow
"tail *": allow
"cat *": allow
"wc *": allow
"ls *": allow
"pwd *": allow
"diff *": allow
"gh pr view *": allow
"gh run view *": allow
"gh api \"repos/sst/opencode/commits/dev\" *": allow
"axiom *": allow
"bun test *": allow
"bun run typecheck *": allow
"bun run lint *": allow
"bun run script/check-opencode-annotations.ts *": allow
"script/upstream/find-conflict-markers.sh *": allow
"./script/upstream/find-conflict-markers.sh *": allow
---
Resolve the manual part of an upstream merge.
**Do not load the `kilocode-merge-minimizer` skill.** That skill is for
authoring new Kilo changes against shared upstream files; during an upstream
merge it gives the wrong guidance (it nudges toward extracting Kilo logic out
of conflict regions, which is exactly the opposite of what merge resolution
needs). Follow the rules in this agent file instead.
The user will provide the upstream version (for example `v1.1.50` or `1.1.50`)
in their first message. If they don't, infer it from the current branch name,
from `upstream-merge-report-<version>.md`, or from the newest relevant report
file.
## Workflow
### 1. Inspect the current merge state
- `git status --short`
- `git diff --name-only --diff-filter=U`
- `upstream-merge-report-<version>.md` when present
- `.worktrees/opencode-merge/auto-merge` for the automated merge snapshot when present
### 2. Read every conflicted file end-to-end before planning
Use `script/upstream/find-conflict-markers.sh <file>` to jump to each region,
then read enough surrounding lines to understand the code — not just the
conflict hunk. Specifically check:
- is this a plain 3-way on a single expression, or a structural refactor?
- does upstream rename/move something that invalidates a HEAD-only declaration?
- does a `kilocode_change` marker in HEAD encode a bug fix, a feature, or a
defensive check?
- is the conflicted block referenced by *non-conflicted* code elsewhere in the
same file (imports, signatures, call sites) that will break if we drop it?
When HEAD includes a non-obvious Kilo-specific wrapper (e.g. a helper in
`packages/opencode/src/kilocode/`), find out why it exists before deciding to
keep or bypass it:
```bash
git log --all --oneline -S "<symbol>" -- packages/opencode/src/kilocode/
git log --all --oneline -- <kilo-file>
```
Look at the commit message and any PR reference. "We wrote our own because of
PR #NNNN" is a real constraint; "we wrote our own because of a typo" is not.
When upstream narrows an externally-visible compatibility list (models,
providers, routes, config keys, file formats), verify the intent from upstream
PRs, issues, release notes, or current docs before dropping entries. Treat
silent list shrinkage during a refactor as suspicious until proven intentional.
### 3. Write a plan in chat and get approval
For every conflicted file (and any adjacent file the resolution forces you to
touch — see §6) include:
- expected resolution kind: `hybrid`, `take-ours`, `take-theirs`, `regenerated`,
`removed`, `renamed`, or `other`
- risk level: `low`, `medium`, or `high`
- one-sentence rationale (what Kilo behaviour is preserved, what upstream
feature is adopted, what is dropped)
- verification commands you expect to run (targeted tests, typecheck)
Group files by risk level. Ask the user which batch to start with. You can
resolve an entire `low` batch in one pass if the user approves the batch, but
resolve `medium` and `high` files one at a time.
**Do not resolve a file until the user has approved that file's (or batch's)
strategy.**
### 4. Before every edit, explain reasoning before showing the diff
The user needs to review intent, not just the raw change. For each file, in
order:
1. Show the conflict's surrounding context (1030 lines around each conflict
region, in chat).
2. Explain what each of the three sides (HEAD, merge-base, upstream) is doing.
3. State which Kilo behaviour must survive and why (reference PR numbers /
`kilocode_change` comments when possible).
4. State the resolution and why it is better than the alternatives.
5. Then apply the edit. The tool will display the diff — the user only has to
verify the diff matches the reasoning.
Do not lead with the diff. A diff without reasoning forces the user to
reverse-engineer the decision.
### 5. Apply resolution rules
Reference worktrees when present:
- `.worktrees/opencode-merge/opencode` — pristine upstream tree
- `.worktrees/opencode-merge/kilo-main` — Kilo base snapshot
- `.worktrees/opencode-merge/auto-merge` — automated merge snapshot (original
conflict reference)
Apply in order:
- prefer upstream code and architecture whenever compatible with Kilo behaviour
- preserve Kilo-specific behaviour marked with `kilocode_change`
- keep `kilocode_change` markers around Kilo-specific code in shared opencode
files
- when upstream refactors a region that HEAD had annotated with
`kilocode_change`, **check whether the marker encodes a bug fix or a feature
delta**. Bug fixes (missing `await`, defensive null-check, error capture)
usually need to be re-applied on top of the upstream refactor. Example from
v1.14.30: `Workspace.isSyncing` was missing an `await` — upstream's Effect
refactor reintroduced the same bug, so we had to port the fix into the new
`Effect.gen` block.
- when a `take-theirs` drops a line that was the target of a Kilo pre-filter,
the upstream line may be actively wrong for Kilo — e.g. an inner `continue`
filter whose condition collides with an outer filter Kilo added. Re-read the
surrounding 20 lines before committing to `take-theirs`.
- if Kilo-specific code must be refactored to fit new upstream architecture,
explain the refactor in the final summary
- if upstream moved the relevant logic to another file, port the Kilo behaviour
there and list both paths in the final summary. Verify the new file already
carries the Kilo-renamed symbols (e.g. `x-kilo-directory`) by diffing against
pristine upstream.
- if upstream extracts shared policy into a helper, move Kilo-specific additions
into the helper when possible instead of keeping a pre-check at the old call
site. The extracted helper should stay the source of truth for all callers.
- if upstream deleted a file, analyse whether the Kilo behaviour should be
ported elsewhere or removed rather than restoring the deleted file
- if tests fail only because upstream intentionally removed behaviour, remove
or update the obsolete tests rather than adding the old file back
- do not modify unrelated files
When removing code that existed in one side of a conflict, prefer
**commenting it out with `kilocode_change` markers** over deletion when the
surrounding structure (an `if`, a loop) still makes sense. That keeps the
intent visible to the next merger. Example:
```ts
} else if (input?.scope !== "project" && !Flag.KILO_EXPERIMENTAL_WORKSPACES) {
// kilocode_change start - directory filtering handled by KiloSession.filters above
// if (input?.directory) {
// conditions.push(eq(SessionTable.directory, input.directory))
// }
// kilocode_change end
}
```
Use `TODO:` not `NOTE:` for follow-ups. `TODO` is searchable and implies an
owner will act on it; `NOTE` reads as permanent commentary.
### 6. Look for adjacent files the conflict forces you to touch
Upstream restructures sometimes split one file into several (e.g. `permission.ts`
`groups/permission.ts` + `handlers/permission.ts`). Only the *renamed* file
shows up in `git diff --diff-filter=U`; the new sibling may need a Kilo feature
ported in too. After resolving the flagged file, check:
- files that import from the resolved file — do they compile?
- files at paths implied by new imports (e.g. `../middleware/*`, `./handlers/*`)
- `kilocode_change` comments in the *auto-merge* snapshot that didn't end up in
the working tree because the hosting file was renamed
Add any such files to the plan as `hybrid` or `take-ours` with the same
approval flow.
### 6.5. Scan auto-merged files for latent bugs
Files not in `--diff-filter=U` merged without conflict markers but may still
be broken. Check every auto-merged file for:
- **Duplicate declarations in the same scope.** If both sides added equivalent
code independently, auto-merge keeps both. Grep touched functions for
repeated identifiers before trusting the merge.
- **Duplicate keys in config/manifest files.** If both sides added the same
entry to a shared manifest (dependencies, scripts, workflow lists), the
merged file may have the key twice. This often breaks install/setup before
any test runs — a cheap early win to scan for.
- **Orphaned imports and references.** A rename upstream may leave a Kilo
callsite pointing at a now-missing export. Run full typecheck from the repo
root; references that silently survived the merge surface there.
- **Partial auto-merges.** Upstream may have refactored a region Kilo
deliberately stubbed out (commented blocks, removed fallbacks). If the
auto-merge pulled in references to names that only exist in the removed
path, the file compiles upstream but breaks on Kilo.
### 7. Verify each resolution before moving on
- confirm `script/upstream/find-conflict-markers.sh <file>` prints nothing
- read the final file region (the new shape after edit) and sanity-check imports
- for apparently-unused symbols upstream introduced, `grep` the file and the
rest of the package before deleting — they may be called from non-conflicted
code elsewhere. Example: `isTheme` in `theme.tsx` looked unused at the
resolution site but was called twice further down.
- run the smallest relevant check (single `bun test` file, or `bun run
typecheck` in the touched package)
- summarise the exact resolution, tradeoff, and verification result in chat
- ask the user to approve the resolved file before staging it or resolving the
next one (for `medium` / `high`; `low` batches can be staged together)
### 8. Run the full checks once everything is resolved
- `git diff --name-only --diff-filter=U` returns empty
- `bun run typecheck` from `packages/opencode/` (targeted) and from repo root
(catches non-conflicted call-site breakage)
- relevant targeted tests. Tests that hang or time out in an unrelated part of
the graph may be pre-existing — note them, don't block the merge on them
- `bun run script/check-opencode-annotations.ts` if `packages/opencode/` shared
files changed. Note that this tool compares against the merge base via `HEAD`
and will be silent until the merge commit lands
- other CI guards that touched files imply (knip for `kilo-vscode/`,
`check-kilocode-change`, source-links, visual regression,
`script/check-forbidden-strings.ts`)
- if you encounter a hardcoded upstream URL, repo path, or attribution string
during conflict resolution that obviously shouldn't ship in Kilo (e.g. another
`https://opencode.ai/...` link, an `anomalyco/opencode` reference, an
attribution header naming "opencode"), suggest adding a literal pattern for
it to `script/check-forbidden-strings.ts` in the merge summary so future
merges catch it automatically. Don't add it silently mid-merge — flag it for
the user.
### 9. Commit with the standard message
Per `script/upstream/README.md`:
```bash
git commit -m "resolve merge conflicts"
```
The default `git merge` auto-message (`Merge branch '…' into …`) is also fine,
but `resolve merge conflicts` is the convention for these PRs.
### 9.5. Handle downstream API renames as separate commits
Upstream often renames exported APIs. The rename itself auto-merges cleanly in
shared code, but the change cascades into Kilo-only files (kilocode tests,
kilo-specific source, plugins) that still reference the old symbol. Those
files don't appear in `--diff-filter=U` because their own content didn't
conflict.
Keep the behavioural merge commit focused on resolution decisions. Land the
cascade in one or more follow-up commits:
- after the merge commit, run full repo typecheck and collect every "cannot
find name" / "property does not exist" error
- bulk-rename with a mechanical transform when the rename is one-to-one
- restructure or parameter-thread when upstream changed semantics, not just
the name (e.g. moved a helper behind a dependency-injected surface, so
callers now need the injected handle)
- split large downstream refactors into their own commits with messages that
name the rename
Reviewers can then skim the behavioural commit without untangling mechanical
rename noise from merge decisions.
### 9.6. Handle upstream-added tests that diverge from Kilo
Upstream sometimes adds tests that encode design contracts Kilo intentionally
breaks. These auto-merge cleanly and then fail. Three resolution patterns:
- **Rewrite the test** when the test is a contract assertion and Kilo has a
different but equally valid contract. Invert or adjust the assertion with a
`kilocode_change` marker explaining the divergence.
- **Skip the test** when the test relies on patterns that Kilo has replaced
(interception seams that are bypassed by dependency injection, fixture
helpers bound to a removed API, assumptions about serialization shape that
Kilo's extensions break). Mark with `kilocode_change` and a rationale
explaining what would need to change for the test to run.
- **Delete the test** when it covers functionality Kilo deliberately removed
(fallback paths, deprecated endpoints, products Kilo doesn't ship). Note
the deletion in the PR body.
Never silently delete; always leave a breadcrumb. A future reviewer should be
able to understand why this one upstream test is treated differently.
### 10. Resync version strings in a separate commit
Upstream stamps its own version into shared files — notably
`packages/extensions/zed/extension.toml` (version field + 5 Kilo-Org download
URLs), and any `package.json` that upstream bumped in the same release window.
After the merge this leaves parts of the tree pointing at upstream's version
(e.g. `1.14.30`), whose release tag does not exist on Kilo's pipeline, so the
Zed download URLs silently 404.
Fix this in a dedicated commit *after* `resolve merge conflicts`:
```bash
bun run script/sync-versions.ts # uses root package.json version
# or, to target an explicit version:
bun run script/sync-versions.ts 7.2.41
git add -A
git commit -m "chore: resync versions after upstream merge"
```
The script rewrites every top-level `"version"` in `package.json` files
(excluding `node_modules`, hidden dirs, and `packages/kilo-jetbrains/` which
tracks its own cadence), plus the Zed extension toml. It is idempotent — rerun
it any time to rebase the version back onto Kilo main (useful during
long-running upstream merges where `main` releases in the meantime).
Keeping this in its own commit makes reviewers' job easier: the merge commit
only contains behavioural resolutions, and the version resync is a trivial
diff they can skim in one glance.
### 11. Write the PR body
Structure the description so reviewers can skim:
- **Non-trivial merge decisions**: a short section per file (or group of
related files) that required more than a mechanical `take-ours`/`take-theirs`.
Focus on *what Kilo behaviour survived* and *what upstream features were
adopted*. Link to Kilo PRs when a `kilocode_change` encodes a specific fix.
- **Notable auto-merged changes**: new columns, new helper files, renamed
middleware — anything reviewers should eyeball even though git didn't flag
it.
- **What to test**: explicit, scenario-level test steps for each non-trivial
change. Don't list tests; list *user-visible behaviour* so a tester who
doesn't read the diff can exercise it.
- **CI guards to watch**: typecheck, knip, annotation check, visual regression.
- **Follow-ups**: any `TODO:` you left in code, as a bullet list with links.
## User-approval checkpoints
Every manual merge decision requires explicit user approval **before applying**
and **again after verification**. Be especially cautious when a decision is
destructive, changes auth, billing, data deletion, public API compatibility,
config schema behaviour, migrations, provider routing, or security posture.
## Common pitfalls
- Auto-merged code can reference declarations that still live inside conflict
blocks.
- Related sibling files can need edits even when they are not listed as
unmerged — especially after upstream structural splits.
- `renamed` should be used only when behaviour moves to a different file.
- Function signatures can drift across conflict boundaries (args added, return
types widened). Grep for every call site before finalising.
- Full-repo typecheck is the catch-all for non-conflicted call-site breakage.
- Upstream can reintroduce bugs a Kilo `kilocode_change` had already fixed —
during big refactors check every `kilocode_change` the refactor touched.
- "Take-theirs" on an inner conditional is often wrong when Kilo added an outer
pre-filter whose whole point was to widen what makes it to the inner block.
- Apparently-unused upstream-added declarations may be called from
non-conflicted code elsewhere. Grep before deleting.
- Stricter DOM lib types (upstream TS upgrade) can surface latent casting
issues around `WebSocket.send`, `Headers`, etc. — prefer narrowing the Kilo
type over adding `any` casts.
- Auto-merge can duplicate the same declaration twice in one scope when both
sides added equivalent code independently. Silent for git, caught by
typecheck. Same hazard for duplicated object keys in config/manifest files —
those can break install before any test runs.
- Kilo code may rely on ambient context (async-local storage, globally-set
flags, process env) being populated at a lifecycle moment that upstream
refactors away. If Kilo behaviour reads ambient state during init, forked
work, or event handlers, check the refactor still establishes that state at
the right time. Fix by restoring the ambient state, or by threading the
needed value through explicitly.
- Tests that intercept via process-global or module-global spies can become
no-ops after upstream moves the intercepted code path through dependency
injection. The production code no longer touches the spied symbol. Fixing
the test usually means injecting a mock at the new seam rather than tweaking
the spy.
- When Kilo extends a shared data shape with extra optional fields, different
serialization paths for that shape can diverge on whether missing values are
omitted or emitted as null. Parity tests between two such paths break on
every Kilo addition — audit the encoding assumption before adding fields.
- Rule ordering in allowlist/permission evaluation is usually last-match-wins.
Re-declaring a catch-all rule "for safety" in a later ruleset silently
overrides more specific allow rules from an earlier ruleset. Treat a
redundant catch-all as destructive, not defensive.
- Upstream-added tests can encode a design contract Kilo deliberately breaks.
The test passes upstream because upstream doesn't share Kilo's requirement.
Decide between refactoring Kilo to match the upstream contract or rewriting
the test to assert Kilo's divergent contract — with a `kilocode_change`
marker explaining the divergence.
- CI and local can show different test failures. Tests that read user-local
state (home dir, global config, auth tokens) pass in one environment but
fail in the other. A green local run does not imply green CI.
- Dependency manifests and lockfiles move together. When the merge edits one,
regenerate and commit the other in the same change — otherwise CI breaks on
the follow-up setup step.
-81
View File
@@ -1,81 +0,0 @@
---
description: Review an upstream merge PR and write reports
---
Review $1.
Branch off so we have all the code for the reviewed PR. Then run the following reviews in parallel, using one subagent per review, and save each result in the named markdown file at the repository root. A human will read those files, so if in doubt add a finding for human verification.
When all subagents are done, commit the report files and create a draft PR with the reviewed PR branch as base.
Do not include exhaustive per-file checklists in the report files. Summarize the scope and methodology, then list only findings, notable non-findings, command outputs, and limitations.
## KILOCODE_CHANGE_MARKERS.md
Review carefully, file by file, whether we accidentally removed any `kilocode_change` marker.
Get the full list of files changed in the reviewed PR. Check each changed file. For each file, compare both our `main` branch and the upstream-merged version in the reviewed PR. Determine whether any marker removal, marker move, or Kilo-specific change makes sense, and comment on that.
Do not include a full "Files Checked" section. Mention the number of changed files checked, but only list files that have findings or need human verification.
## INFRASTRUCTURE_CHANGE.md
Review whether this PR adds, removes, or changes any infrastructure, such as GitHub Actions, CI config, release/deploy scripts, Docker/build infrastructure, package manager/workspace infrastructure, repository automation, issue templates, changelog automation, or generated SDK/build automation.
We want to merge upstream code but keep our own infrastructure, so flag anything infrastructure-related. When in doubt, add a finding and say a human should check manually.
Do not include a full "Files Checked" section. Only list infrastructure-relevant files and findings.
## OPENCODE_MENTIONS.md
Check whether the merge now mentions OpenCode somewhere user-facing instead of Kilo, or links to OpenCode web properties.
Focus on UI strings, docs, help text, package metadata shown to users, URLs, CLI output, config docs, generated SDK/OpenAPI descriptions, and error messages.
## UNNECESSARY_MARKERS.md
Check whether any merged files now use `kilocode_change` markers without any actual difference to upstream.
Use `script/upstream/find-reset-candidates.ts --dry-run` to see whether any files changed in the PR are actually now identical to upstream. If you find candidates, verify them with `script/upstream/reset-to-upstream.ts --dry-run`.
## BROKEN_PIPELINE_CHAINS.md
Review this PR for broken end-to-end chains where our custom functionality requires changes across multiple files or layers, but the merge may have removed or altered an intermediate step. The code may still compile, so these issues can be silent.
Look for cases like:
- A parameter that is set but never read.
- A field that is populated but never passed through.
- An event that is emitted but no longer handled.
- A config option that is defined but never propagated to where it is used.
- A type definition extended on one side but not consumed on the other.
For each `kilocode_change` in this PR, trace the full chain: where the value or behavior is introduced, where it needs to flow through, and where it is ultimately consumed. Verify every link in the chain still exists after the merge.
Pay special attention to:
- Props or parameters passed through multiple component or function layers.
- Values written to state, context, or storage that are read elsewhere.
- Message types, events, or IPC handlers where sender and receiver must match.
- Configuration or feature flags defined in one place and checked in another.
- Type definitions extended on one side but consumed on the other.
When in doubt, add a finding. A human will verify it. Compiling code is not proof the chain is intact.
## CONFIG_REGRESSION.md
Check whether this PR introduces or re-introduces fallback logic for `opencode` config files, or accidentally breaks code that now correctly expects only `.kilo`-based configuration.
Kilo removed fallback support for `opencode` config directories. Look for:
- Any new or restored code that reads from `opencode` config paths.
- Upstream additions to config discovery, loading, or path resolution that add `opencode` fallback candidates we stripped.
- Changes that break `.kilo`-only config lookup by removing or reordering it in a multi-path search.
When in doubt, add a finding. A human should verify config path changes manually.
## TESTS.md
Check whether this PR removed any Kilo-specific tests.
Kilo-specific tests may live in paths containing `kilo` or `kilocode`, or may include Kilo-specific assertions, fixtures, or `kilocode_change` markers.
-37
View File
@@ -1,37 +0,0 @@
# Sample Plan With Todos
## Goal
Create a clear sample plan that breaks work into actionable todos.
## Todos
1. Define the objective
- Clarify the desired outcome.
- Identify the target audience.
- List success criteria.
2. Gather requirements
- Collect relevant inputs.
- Note constraints and assumptions.
- Identify dependencies.
3. Break down the work
- Split the goal into smaller tasks.
- Prioritize tasks by importance.
- Estimate effort for each task.
4. Execute the tasks
- Complete the highest-priority items first.
- Track progress as todos are finished.
- Adjust the plan if new information appears.
5. Review and validate
- Check that all todos are complete.
- Verify the result matches the goal.
- Document any follow-up work.
## Acceptance Criteria
- The plan has a clear goal.
- The plan includes a list of actionable todos.
- The todos are ordered logically.
- The plan includes a way to confirm completion.
-38
View File
@@ -1,38 +0,0 @@
# Sample Plan With Todos
## Goal
Create a simple sample plan that demonstrates how to organize work into clear, actionable todos.
## Implementation Steps
1. Define the objective
- State the desired outcome in one sentence.
- Identify who the plan is for.
- List the criteria that indicate success.
2. Gather requirements
- Capture relevant inputs and context.
- Note constraints, assumptions, and dependencies.
- Identify any missing information that may affect execution.
3. Break down the work
- Split the objective into smaller tasks.
- Order tasks by dependency and priority.
- Keep each todo specific enough to complete independently.
4. Execute the todos
- Work through the highest-priority tasks first.
- Mark todos complete as progress is made.
- Update the plan if new information changes the scope.
5. Review the result
- Confirm every todo has been addressed.
- Verify the result matches the original objective.
- Record any follow-up work separately.
## Acceptance Criteria
- The plan has a clear goal.
- The plan includes actionable todos.
- The todos are ordered logically.
- The plan includes a review step to confirm completion.
@@ -1,37 +0,0 @@
# Sample Plan: Add Todo-Based Workflow
## Goal
Create a small, traceable workflow for implementing a feature with clear todos, validation steps, and completion criteria.
## Scope
- Identify the affected package or feature area.
- Make the smallest correct code change.
- Add or update tests only where they verify behavior.
- Run the smallest relevant checks before completion.
## Todos
- [ ] Inspect the relevant files and existing patterns.
- [ ] Confirm whether the change belongs in Kilo-owned code or shared upstream code.
- [ ] Implement the minimal code change.
- [ ] Add or update targeted tests if behavior changes.
- [ ] Run formatting or linting if the touched package requires it.
- [ ] Run the smallest relevant typecheck or test command.
- [ ] Fix any failures introduced by the change.
- [ ] Summarize changed files and verification results.
## Implementation Notes
- Prefer Kilo-owned directories for Kilo-specific behavior.
- If shared upstream files must be edited, keep the change narrow and add `kilocode_change` markers where required.
- Avoid broad refactors unless they are necessary for the requested behavior.
- Preserve existing style, naming, and package conventions.
## Verification
- Run the targeted test for the changed behavior when available.
- Run the package typecheck if TypeScript code changes.
- Run any repo-specific guard required by touched files, such as annotation or source-link checks.
## Completion Criteria
- The requested behavior is implemented.
- Tests or checks relevant to the change pass, or any inability to run them is documented.
- No unrelated files are modified.
- The final response includes a concise summary and verification status.
-35
View File
@@ -1,35 +0,0 @@
# Sample Plan
## Goal
Create a small example feature or change using a clear implementation workflow and tracked todo list.
## Scope
- Add or update a minimal code path.
- Add focused tests for the changed behavior.
- Run the smallest relevant verification command.
- Summarize the result and any follow-up work.
## Todo
- [ ] Inspect the relevant files and existing patterns.
- [ ] Identify the smallest safe implementation approach.
- [ ] Make the code change.
- [ ] Add or update tests for the behavior.
- [ ] Run targeted tests or typecheck.
- [ ] Fix any failures introduced by the change.
- [ ] Provide a concise final summary with verification results.
## Implementation Notes
- Prefer the smallest correct change over broad refactors.
- Preserve existing style and naming conventions.
- Avoid touching unrelated files.
- If shared upstream-owned files are involved, use the repositorys `kilocode_change` marker guidance.
## Verification
Run the most specific applicable check for the touched area, such as:
- CLI: `bun test ./path/to/test.ts` from `packages/opencode/`
- VS Code extension: `bun run test:unit` from `packages/kilo-vscode/`
- Cross-package changes: `bun run typecheck` from the repo root
## Risks
- The sample scope may need adjustment once the real target files are known.
- Verification commands depend on which package is touched.
-69
View File
@@ -1,69 +0,0 @@
# Plan: Replace Internal SVGLoader Usage
## Context
The JetBrains plugin currently imports `com.intellij.util.SVGLoader` in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt` and calls `SVGLoader.load(ByteArrayInputStream(patch()), scale.toFloat())` from `SvgIcon.image(Graphics)`.
Inspection reports this as an internal API usage because `SVGLoader` and its `load(InputStream, float)` method are marked `@ApiStatus.Internal`.
I checked the local IntelliJ source reference via `$INTELLIJ_REPO=/Users/kirillk/products/intellij-community`:
- `platform/util/ui/src/com/intellij/util/SVGLoader.kt`
- `platform/util/ui/src/com/intellij/ui/svg/svg.kt`
- `platform/util/ui/src/com/intellij/ui/svg/jsvg.kt`
- `libraries/jsvg/intellij.libraries.jsvg.iml`
Important findings:
- IntelliJ `SVGLoader.load(stream, scale)` delegates to internal `loadSvg(...)` in `com.intellij.ui.svg`.
- The actual renderer uses `com.github.weisj:jsvg:2.1.0` and renders into a `BufferedImage` with antialiasing, bicubic interpolation, and pure stroke control.
- Copying `SVGLoader.kt` directly would keep references to other internal IntelliJ APIs (`IconLoader`, `ScaleContext`, `ImageUtil`, `SvgAttributePatcher`, `createJSvgDocument`, `renderSvgWithSize`, cache classes, etc.) and would be broader than needed.
## Implementation Plan
1. Add Kilo-owned SVG rendering code under the frontend UI package, for example:
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgLoader.kt`
- Package: `ai.kilocode.client.ui`
- Suggested internal object name: `SvgLoader`
2. Keep the copied implementation minimal and purpose-specific:
- Accept `InputStream` and `scale: Float`.
- Parse the SVG with `com.github.weisj.jsvg.parser.SVGLoader().load(stream)` or the equivalent public jsvg API available in version `2.1.0`.
- Determine image dimensions from the root SVG `width` and `height` already parsed by existing `SvgIconColorizer.size(...)`, or by passing dimensions into the loader if the public jsvg API does not expose reliable intrinsic size.
- Render into `BufferedImage(width * scale, height * scale, BufferedImage.TYPE_INT_ARGB)` using `Graphics2D` rendering hints matching IntelliJs `jsvg.kt`:
- `KEY_ANTIALIASING = VALUE_ANTIALIAS_ON`
- `KEY_INTERPOLATION = VALUE_INTERPOLATION_BICUBIC`
- `KEY_STROKE_CONTROL = VALUE_STROKE_PURE`
- Return `Image`/`BufferedImage`.
3. If jsvg does not expose intrinsic size cleanly from public API, adjust the API to avoid duplicating SVG parsing:
- Add `width` and `height` parameters to the Kilo loader.
- In `SvgIcon.image(Graphics)`, call the loader with the already-computed `size.first` and `size.second`.
- This is the likely smallest stable solution because current icons already require explicit `width` and `height` for `SvgIconColorizer.size(...)`.
4. Add the jsvg dependency explicitly to the plugin frontend module instead of relying on the IntelliJ-bundled library:
- In `packages/kilo-jetbrains/gradle/libs.versions.toml`, add `jsvg = "2.1.0"` and `jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }`.
- In `packages/kilo-jetbrains/frontend/build.gradle.kts`, add `implementation(libs.jsvg)`.
- This follows the package instruction to bundle third-party libraries with the plugin rather than relying on IDE-bundled versions.
5. Update `SvgIconColorizer.kt`:
- Remove `import com.intellij.util.SVGLoader`.
- Replace `SVGLoader.load(ByteArrayInputStream(patch()), scale.toFloat())` with the Kilo-owned loader call.
- Keep the existing color patching, cache key, scale calculation, and size parsing intact.
6. Add a focused frontend unit test:
- New test file: `frontend/src/test/kotlin/ai/kilocode/client/ui/SvgIconColorizerTest.kt` or `SvgLoaderTest.kt`.
- Exercise the actual loader/colorizer with a small SVG resource or inline SVG bytes.
- Assert that rendering returns a non-empty image at scale `1f` and `2f`, and that scaled dimensions are correct.
- If testing the colorizer, paint the icon into a `BufferedImage` and verify at least one expected patched pixel color appears.
7. Run verification from `packages/kilo-jetbrains/`:
- `./gradlew typecheck`
- A targeted frontend test if Gradle supports one cleanly, otherwise `./gradlew test`.
- The requested DevKit internal API inspection is IDE-based; note in final response that the code no longer references `com.intellij.util.SVGLoader`, and typecheck/tests were run.
## Notes And Risks
- The user said “copy SaveLoaer”; I interpret this as “copy `SVGLoader`”. The implementation should copy the relevant rendering behavior, not IntelliJs full internal `SVGLoader` object, because the full source depends on multiple other internal APIs and cache infrastructure.
- This change is in `packages/kilo-jetbrains/`, a Kilo-owned package, so `kilocode_change` markers are not needed.
- jsvg package names and constructors should be confirmed during implementation against the downloaded dependency/API available to Gradle. If the API differs from IntelliJ source usage, inspect the source jar or compile error and adapt the minimal wrapper accordingly.
-60
View File
@@ -1,60 +0,0 @@
# Remove JetBrains SVG Runtime Colorization
## Goal
Stop runtime SVG recoloring in the JetBrains plugin and rely on IntelliJ's standard light/dark icon asset resolution. Ensure every SVG asset used by the plugin has a light/dark pair.
## Findings
- Runtime colorization exists only in `frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt`.
- The only production caller is `frontend/src/main/kotlin/ai/kilocode/client/session/scroll/ScrollButtonIcon.kt`.
- `ScrollButtonIcon` colorizes two icons at runtime:
- `/icons/scroll-bottom.svg`
- `/icons/scroll-question.svg`
- Both already have dark variants:
- `scroll-bottom_dark.svg`
- `scroll-question_dark.svg`
- Other used frontend icons already have light/dark pairs:
- `send.svg` / `send_dark.svg`
- `stop.svg` / `stop_dark.svg`
- `shield.svg` / `shield_dark.svg`
- `shield-filled.svg` / `shield-filled_dark.svg`
- `compress.svg` / `compress_dark.svg`
- `chevron-down.svg` / `chevron-down_dark.svg`
- `arrow-up.svg` / `arrow-up_dark.svg`
- `arrow-down-to-line.svg` / `arrow-down-to-line_dark.svg`
- `kilo.svg` / `kilo_dark.svg`
- `kilo-content.svg` / `kilo-content_dark.svg`
- `plus.svg` / `plus_dark.svg` exists, though no current direct source reference was found.
- `kilo@20x20.svg` / `kilo@20x20_dark.svg` exists, though no current direct source reference was found.
- `src/main/resources/META-INF/pluginIcon.svg` is present and has no `pluginIcon_dark.svg`. It is not referenced in source/XML, but JetBrains treats this filename conventionally as plugin metadata/marketplace icon, so it should be paired for completeness.
- The SVG assets use literal colors and no `currentColor`, `<style>`, CSS classes, or CSS variables were found in `frontend/src/main/resources/icons/*.svg`.
## Implementation Plan
1. Replace `ScrollButtonIcon` runtime colorization with standard `IconLoader.getIcon(...)` loading.
- Remove imports for `colorizedSvgIcon`, `SessionUiStyle`, `UiStyle`, `JBUI`, and `Color` from `ScrollButtonIcon.kt`.
- Add `IconLoader` import.
- Load `bottom` with `IconLoader.getIcon("/icons/scroll-bottom.svg", ScrollButtonIcon::class.java)`.
- Load `prompt` with `IconLoader.getIcon("/icons/scroll-question.svg", ScrollButtonIcon::class.java)`.
- Keep `create(question: Boolean = false)` unchanged so callers continue to receive the appropriate icon.
- IntelliJ should automatically resolve `_dark.svg` variants when the look and feel is dark.
2. Delete `frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt` after confirming no callers remain.
3. Remove now-unused scroll icon source palette constants from `SessionUiStyle.ScrollIcon`.
- Delete the `ScrollIcon` object if no references remain.
- This also removes stale comments about runtime colorization.
4. Add `src/main/resources/META-INF/pluginIcon_dark.svg` as the dark-mode pair for `pluginIcon.svg`.
- Use the same artwork/colors as `pluginIcon.svg` unless there is a desired brand-specific dark variant; the current icon already has a black background and yellow mark, so an identical dark pair is acceptable and satisfies IntelliJ's expected pair convention.
5. Re-scan the JetBrains package after edits.
- Confirm there are no `colorizedSvgIcon` or `SvgIconColorizer` references.
- Confirm every used SVG path has a sibling `_dark.svg` where IntelliJ expects one.
- Confirm no frontend SVG uses `currentColor`, `<style>`, CSS classes, or CSS variables.
6. Run the smallest relevant verification from `packages/kilo-jetbrains/`:
- `bun run typecheck` or `./gradlew typecheck`.
## Files Expected To Change
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/ScrollButtonIcon.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt`
- Delete `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt`
- Add `packages/kilo-jetbrains/src/main/resources/META-INF/pluginIcon_dark.svg`
## Risks
- The scroll icons will no longer follow arbitrary runtime theme colors; they will use the static light/dark SVG asset colors. This matches the requested behavior.
- If a non-default theme expects palette overrides, static literal SVG colors still follow normal IntelliJ SVG handling better than custom byte-patching, but the exact custom-color behavior may differ from the old colorizer.
-49
View File
@@ -1,49 +0,0 @@
# Fix JetBrains Session Question Scroll
## Goal
When a question-like active view first appears in the JetBrains session UI, do not auto-scroll the transcript if the user is already away from the tail and the scroll-to-bottom button is visible. This applies to permission prompts and question-based views. Existing behavior should remain: if the transcript is already at the bottom, new questions/permissions keep the view at the bottom; explicit question navigation can still jump to the active card.
## Current Findings
- `SessionUi` wires `QuestionView` with:
- `follow = { scroll.following() }`
- `scroll = { scroll.followBottom(it) }`
- `QuestionView.show()` runs on first display and calls:
- `val tail = follow()` before rendering the card
- `scroll(tail)` after rendering
- `SessionScroll.following()` currently returns `component.viewport.view === messages && tail`.
- This can use stale internal follow state instead of the live scrollbar state. The user-visible condition is better represented by `SessionScroll.atBottom()`, which is also what `SessionController.beforeUpdate` uses for normal transcript updates.
- Existing `SessionScrollTest` already covers question and login-required middle-scroll preservation, but it does not cover permission first appearance and likely does not cover a stale-tail/live-not-at-bottom edge.
## Implementation Plan
1. Update question first-show follow sampling to use live bottom state.
- In `SessionUi.buildUi()`, change the `QuestionView` `follow` lambda from `scroll.following()` to `scroll.atBottom()`.
- Alternatively, if a single semantic API is preferred, change `SessionScroll.following()` to return `component.viewport.view === messages && atBottom()` and keep the `SessionUi` call site unchanged. Prefer the smaller, clearer call-site change unless tests show other `following()` behavior should change too.
2. Add permission scroll regression tests in `SessionScrollTest`.
- Add `test permission appearing at bottom keeps scroll at bottom`.
- Add `test permission appearing while user is in middle preserves scroll position`.
- Emit `ChatEventDto.PermissionAsked("ses_test", PermissionRequestDto(...))` using the same DTO style as the existing recovered permission test.
- Assert the scrollbar value is preserved and `jumpButton().isVisible` stays true when initially away from bottom.
3. Strengthen question regression coverage if needed.
- Keep the existing `test question appearing while user is in middle preserves scroll position`.
- If the bug reproduces only when the jump button is visible but `tail` can be stale, add a targeted regression that creates the visible jump state before `QuestionAsked` and asserts first display does not jump.
- Avoid adding production-only test hooks; use existing `setValue`, `setValuePassive`, `drainScroll`, and event helpers.
4. Preserve intentional explicit navigation behavior.
- Do not change `QuestionView.goForward()`, `goBack()`, or `goReview()`, which intentionally call `scroll(true)` and are covered by existing tests that expect navigation to jump to the active question card.
- Do not change prompt growth or normal streaming follow behavior unless a failing test shows the same stale-state issue applies there.
5. Verification.
- Run the targeted JetBrains frontend session scroll tests from `packages/kilo-jetbrains/` using Gradle test filtering for `SessionScrollTest` if supported.
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/` if the targeted tests pass or if Gradle filtering is unavailable.
- If Java 21 is unavailable, first check/report that blocker as instructed by the repo docs.
## Expected Files To Touch
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt`
## Non-Goals
- No backend or RPC protocol changes.
- No redesign of the session scroll algorithm.
- No changes to explicit question navigation auto-scroll behavior.
-81
View File
@@ -1,81 +0,0 @@
# Plan: JetBrains Question-Style Scroll Button
## Goal
Fix JetBrains session scrolling so that when any inline prompt card appears while the scroll/jump button is already visible, the transcript scroll position is preserved and only the scroll button state changes. This applies to all current `BaseQuestionView`-backed session cards: questions, permissions, and login-required prompts. Preserve existing behavior that question navigation, review/back/next, and bottom-following content changes auto-scroll when they are supposed to.
## Findings
- The relevant files are `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`, `.../scroll/SessionScroll.kt`, `.../views/question/QuestionView.kt`, `.../views/permission/PermissionView.kt`, `.../views/LoginRequiredView.kt`, `.../views/base/BaseQuestionView.kt`, and `frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt`.
- `QuestionView`, `PermissionView`, and `LoginRequiredView` do not inherit from `BaseQuestionView`; they compose it as the shared card shell. Scroll behavior should stay outside `BaseQuestionView` because it is a pure visual component with no session scroll context.
- The current uncommitted `SessionUi.kt` attempt changes `QuestionView` from `follow = { scroll.following() }` to `follow = { scroll.atBottom() }`.
- That fixes initial question appearance when the user is visibly away from bottom, but it is too broad because `QuestionView.follow()` is also used after custom editor growth, selection refresh, reply/reject, and other internal updates.
- `QuestionView` already intentionally calls `scroll(true)` in `goBack`, `goForward`, and `goReview`; those forced calls are the behavior that preserves auto-scroll when switching question pages.
- `PermissionView.show()` and `LoginRequiredView.show()` do not call scroll directly. They rely on `SessionController.beforeUpdate` / `afterUpdate`, which samples `scroll.atBottom()` before the state change and calls `scroll.followBottom(follow)` afterward. With the stale-tail fix, this path can preserve scroll for all non-question inline prompt cards.
- The uncommitted `SessionScroll.followBottom(false)` change that clears stale `tail` when the transcript is visible and not actually at bottom is directionally correct. It prevents later `following()` calls from resurrecting auto-follow after a non-follow decision.
- The current scroll button pending state is named `question` / `setQuestionPending` and is only enabled for non-plan `AwaitingQuestion`. To cover all `BaseQuestionView`-backed prompts, this should become a generic pending-input/attention state and include `AwaitingPermission` and `LoginRequired`.
- Existing behavior intentionally keeps plan follow-up questions using the normal scroll icon (`test plan followup question keeps scroll icon`), so preserve that exception unless product intent changes separately.
- The uncommitted `build.gradle.kts` and `script/build-sign-check.sh` changes are unrelated to this bug and should not be touched as part of this fix.
## Implementation Plan
1. In `QuestionView.kt`, split the scroll-follow decision into two callbacks.
- Keep the existing `follow` callback for question-internal updates.
- Add a second callback for initial question appearance, with a default that delegates to `follow` so existing tests/call sites remain compatible.
- In `show(q)`, sample the new initial callback before making the question visible or rebuilding the card, then call `scroll(tail)` after `syncPage()` as today.
- Leave `scroll(true)` in `goBack`, `goForward`, and `goReview` unchanged.
- Leave existing `scroll(follow())` calls in custom editor changes, option selection, reply, and reject paths unchanged.
2. In `SessionUi.kt`, wire the callbacks with distinct meanings.
- Use `follow = { scroll.following() }` for question-internal updates.
- Use the new initial callback as `{ scroll.atBottom() }` for initial question appearance.
- Keep `scroll = { scroll.followBottom(it) }`.
3. Generalize the scroll button pending state for all inline prompt cards.
- Rename `SessionScroll.setQuestionPending` and the backing `question` boolean to a generic name such as `setInputPending` / `pending` or `setAttentionPending` / `pending`.
- Rename `questionPending(state)` in `SessionUi.kt` to match the broader meaning.
- Return `true` for non-plan `SessionState.AwaitingQuestion`, `SessionState.AwaitingPermission`, and `SessionState.LoginRequired`.
- Keep returning `false` for plan follow-up questions so the existing plan-specific icon behavior is preserved.
- Reuse the existing question-style icon and tooltip for now, unless a separate product decision asks for a different permission/login-specific icon or string.
4. In `SessionScroll.kt`, keep the stale-tail reconciliation in `followBottom(false)`.
- When `follow` is false, increment `seq`, and if the active view is the transcript and `!atBottom()`, set `tail = false` before `updateJump()`.
- Do not change `following()` to use `atBottom()`, because `tail` is needed to preserve bottom-follow behavior across deferred layout/height growth.
5. Do not add scroll callbacks to `PermissionView`, `LoginRequiredView`, or `BaseQuestionView` unless tests prove the controller-level path is insufficient.
- `PermissionView` and `LoginRequiredView` should remain retained visual views that show/hide themselves and refresh their parent.
- `BaseQuestionView` should stay a scroll-unaware card shell.
- The shared behavior for those cards should come from the controller-level state-change follow decision plus the generalized pending-button state.
6. In `SessionScrollTest.kt`, update tests around question-style prompt appearance and preserved behavior.
- Keep or add an assertion that the jump button is visible before emitting a question in the middle-scroll test.
- Keep a regression for stale `tail` plus visible jump button if no public-only scenario can reproduce the failed state; keep the reflective helper local to this test file and avoid adding production test hooks.
- Keep existing behavior tests for question navigation from the middle, forced navigation resuming follow, custom question answer growth at bottom, custom question answer growth in the middle, and option selection not resuming follow.
- Add or keep permission bottom/middle tests to prove `AwaitingPermission` preserves scroll when away from bottom and follows when at bottom.
- Add or keep login-required bottom/middle tests to prove `LoginRequired` preserves scroll when away from bottom and follows when at bottom.
- Add explicit button-state assertions for permission and login-required while away from bottom: the button remains visible, switches to the pending-input icon/tooltip, and clicking it still scrolls to bottom.
- Keep the plan follow-up test proving plan prompts do not switch to the pending-input icon.
7. Add a patch changeset for the JetBrains plugin unless an existing branch changeset is intentionally covering this exact fix.
- Suggested release note: `Preserve JetBrains chat scroll position when inline prompts appear while the scroll button is visible.`
## Verification
- From `packages/kilo-jetbrains/`, run `./gradlew test --tests "ai.kilocode.client.session.SessionScrollTest"`.
- From `packages/kilo-jetbrains/`, run `./gradlew typecheck`.
- Do not run root `bun test`.
## Expected Outcome
- If the transcript is away from bottom and the jump button is visible, a newly appearing question, permission prompt, or login-required prompt changes the button state without moving `bar.value`.
- If the transcript is at bottom, a newly appearing question, permission prompt, or login-required prompt follows to the new bottom.
- Clicking the visible pending-input scroll button still jumps to the bottom for every inline prompt card.
- Question carousel next/back/review still jumps to the bottom even if the transcript was previously in the middle.
- Custom question editor growth still follows when the transcript was following and preserves position when it was not.
-109
View File
@@ -1,109 +0,0 @@
# Plan: JetBrains Feedback & Support Button
## Goal
Add the VS Code empty-state `Feedback & Support` affordance to the JetBrains plugin, including a non-modal popup with the same three destinations:
- GitHub issues: `https://github.com/Kilo-Org/kilocode/issues/new/choose`
- Discord: `https://kilo.ai/discord`
- Customer support: `https://kilo.ai/support`
The popup must hide when clicking outside or pressing Escape.
## Findings
- The JetBrains empty state is `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt`.
- It already renders the logo, welcome copy, recents, and `Show History`; this is the right place to add the button.
- The VS Code implementation is:
- Button: `packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx`
- Popup content: `packages/kilo-vscode/webview-ui/src/components/chat/FeedbackDialog.tsx`
- Styling: `packages/kilo-vscode/webview-ui/src/styles/welcome.css`
- Local IntelliJ API source is available at `$INTELLIJ_REPO=/Users/kirillk/products/intellij-community`.
- IntelliJ popup API confirmed in local source:
- `JBPopupFactory.getInstance().createComponentPopupBuilder(content, focusComponent)`
- `setModalContext(false)` for non-modal context
- `setRequestFocus(false)` / `setFocusable(false)` if we want the popup not to steal focus
- `setCancelOnClickOutside(true)` for outside-click dismissal
- `setCancelKeyEnabled(true)` for Escape dismissal
- `setCancelOnWindowDeactivation(true)` and `setCancelOnOtherWindowOpen(true)` are available and appropriate for cleanup
- IntelliJ platform icons found in `AllIcons`:
- Feedback button: `AllIcons.Ide.Feedback`
- GitHub action: `AllIcons.Vcs.Vendors.Github`
- Support/help action: `AllIcons.Actions.Help` or `AllIcons.General.ContextHelp`
- Discord: no platform icon found. Add a Discord icon by borrowing the existing VS Code/kilo-ui Discord SVG artwork and adapting it into the JetBrains plugin resources with IntelliJ-compatible SVG colors and dark variant if needed.
## Implementation Steps
1. Add localized JetBrains bundle keys.
- Add base English strings to `frontend/src/main/resources/messages/KiloBundle.properties`:
- `feedback.button=Feedback & Support`
- `feedback.dialog.message=We'd love to hear your feedback or help with any issues you're experiencing.`
- `feedback.dialog.github=Report an issue on GitHub`
- `feedback.dialog.discord=Join our Discord community`
- `feedback.dialog.support=Customer Support`
- Reuse existing cancel text if present; otherwise add `common.cancel=Cancel` only if needed.
- Add matching keys to localized `KiloBundle_*.properties` files. If accurate translations are not already available from the VS Code i18n files, copy the VS Code translations for matching locales where practical and leave English fallback only if the JetBrains bundle mechanism supports it cleanly.
2. Extend `EmptySessionPanel` UI.
- Add a new retained `FeedbackButton`, styled to match VS Code conceptually:
- feedback icon + `Feedback & Support` text
- dashed link-colored border
- transparent background
- hand cursor
- hover state fills with link color and flips foreground/background for contrast
- Use IntelliJ theme APIs instead of raw colors:
- link color: `JBUI.CurrentTheme.Link.Foreground.ENABLED`
- hover link color: `JBUI.CurrentTheme.Link.Foreground.HOVERED` if useful
- editor/panel background from existing session style or `UiStyle.Colors.editorBackground()` / component background
- spacing via `UiStyle.Gap` / `JBUI.Borders`
- rounded arc via `UiStyle.Arc.component()` or `JBUI.getInt("Button.arc", 6)` at paint time
- Add the feedback button below the existing `Show History` button in the empty-state south area, keeping the current centered layout.
- Keep Swing mutations on EDT and preserve retained-component behavior.
3. Implement the feedback popup in frontend Swing code.
- Add a small popup builder method/class inside `EmptySessionPanel.kt` or a new nearby UI file if the file would become too large.
- Content should mirror VS Code:
- Kilo logo at top using existing `/icons/kilo-content.svg`
- message text
- three action buttons/rows for GitHub, Discord, and Customer Support
- every action should have an icon: `AllIcons.Vcs.Vendors.Github` for GitHub, the borrowed Discord SVG for Discord, and `AllIcons.Actions.Help` or `AllIcons.General.ContextHelp` for Customer Support
- optional Cancel button if it fits the JetBrains UX; Escape/outside click already dismisses
- Add the Discord icon asset under `frontend/src/main/resources/icons/` using the plugin's existing icon naming pattern, for example `discord.svg` and `discord_dark.svg` if the borrowed asset needs separate theme variants.
- Load the Discord icon with `IconLoader.getIcon("/icons/discord.svg", EmptySessionPanel::class.java)` or a small Kilo-owned icon object if multiple call sites need it.
- Use `BrowserUtil.browse(url)` to open links.
- Close the popup after opening a URL.
- Use `JBPopupFactory.createComponentPopupBuilder(content, null)` and configure:
- `.setModalContext(false)`
- `.setRequestFocus(false)`
- `.setFocusable(false)` unless keyboard tabbing inside popup is desired; if buttons need keyboard focus, use `.setFocusable(true)` with `.setRequestFocus(false)`
- `.setCancelOnClickOutside(true)`
- `.setCancelKeyEnabled(true)`
- `.setCancelOnWindowDeactivation(true)`
- `.setCancelOnOtherWindowOpen(true)`
- `.setResizable(false)`
- `.setMovable(false)`
- Show with `popup.showUnderneathOf(feedbackButton)`.
4. Add test coverage in `EmptySessionPanelTest.kt`.
- Assert feedback button text uses `KiloBundle.message("feedback.button")`.
- Assert feedback button has hand cursor and visible border semantics.
- Add an injectable browse callback or popup action callback only if needed for testing without launching browsers; keep it minimal and avoid production-only test hooks if component traversal can exercise enough behavior.
- Add a test that clicking the feedback button creates/shows popup content, if feasible in `BasePlatformTestCase`; otherwise test the retained popup content builder/action rows directly through package-internal methods.
- Assert the three action labels exist and URL callbacks map to the same URLs as VS Code.
- Assert the Discord action has an icon, so the plan cannot regress to a text-only Discord row.
5. Add release note.
- This is user-facing for the JetBrains plugin, so add a patch changeset under `.changeset/` unless the repo has a JetBrains-specific release-note mechanism that supersedes changesets.
- Suggested text: `Add Feedback & Support to the JetBrains empty session screen.`
6. Verify.
- Run the focused frontend/JetBrains tests first, preferably from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests "ai.kilocode.client.session.ui.EmptySessionPanelTest"`
- Run JetBrains typecheck from `packages/kilo-jetbrains/`:
- `bun run typecheck` or `./gradlew typecheck`
- If touching only frontend UI and tests pass, no backend/SDK generation is needed.
## Notes
- Do not add Kotlin UI DSL, Compose, or JCEF.
- Do not modify shared opencode files; this work is entirely under `packages/kilo-jetbrains/` plus a changeset.
- Prefer platform icons where available, but explicitly borrow/add the Discord icon because `AllIcons` does not provide one and the popup should match the VS Code button set visually.
@@ -1,84 +0,0 @@
# JetBrains Model Picker Details Plan
## Goal
Add a JetBrains model picker maximize/minimize affordance that expands the popup to roughly double width and shows model details comparable to VS Code's `ModelPreview`. Persist the last expanded/collapsed state in IntelliJ `PropertiesComponent`. First-time default is collapsed.
## Decisions
- Scope is full VS Code parity where JetBrains can receive the same model metadata: pricing, release date, descriptions, modalities/capabilities, terminal-bench, auto-routing details, context limits, badges, and provider/model identity.
- Default state is collapsed for users without a saved property.
- Expanded state is app-level UI state stored in `PropertiesComponent`, not backend/session state.
- Use Swing/IntelliJ platform components only. Do not introduce Compose, JCEF, or UI DSL.
- Keep existing compact list behavior when collapsed. In expanded mode, show a retained details panel beside the list.
## Affected Areas
- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt`
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt`
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt`
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker*.kt`
- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle*.properties`
- Existing tests under `packages/kilo-jetbrains/frontend/src/test/...` and `backend/src/test/...`
## Implementation Steps
1. Extend model metadata DTOs.
- Add serializable nested DTOs to `ProviderDto.kt` for preview metadata matching VS Code provider types where practical: cost/cache cost, capabilities/input modalities, options/description, auto-routing model ids, terminal-bench score/cost.
- Extend `ModelDto` with nullable fields such as `inputPrice`, `outputPrice`, `contextLength`, `releaseDate`, `latest`, `cost`, `capabilities`, `options`, `autoRouting`, and `terminalBench`.
- Preserve existing fields (`limit`, `variants`, `free`, `byok`, `mayTrainOnYourPrompts`) to avoid breaking callers.
2. Parse and map the extra metadata in the JetBrains backend.
- Extend `ModelInfo` in `KiloWorkspaceState.kt` with matching nullable metadata fields.
- Update `KiloCliDataParser.parseModel` and `parseModelDto` to read the fields currently used by VS Code: `inputPrice`, `outputPrice`, `contextLength`, `releaseDate`, `latest`, `cost`, `capabilities.input`, `options.description`, `autoRouting.models`, and `terminalBench`.
- Keep permissive parsing: missing or malformed optional fields should result in `null` or empty data, not provider load failure.
- Update `KiloWorkspaceDtoMapper.model` to copy all new fields to `ModelDto`.
3. Carry metadata through the frontend session/settings model flow.
- Extend session `ModelItem` and picker `ModelPicker.Item` to hold the preview metadata.
- Update `SessionController` -> `SessionModel`, `SessionUi`, and `ModelsSettingsUi.items()` mappings so metadata survives into the picker.
- Pass `limit` through to `ModelPicker.Item`; it is already available in `ModelItem` but currently dropped in `SessionUi`.
4. Add a retained Swing details panel.
- Create a small `ModelDetailsPanel` near `session/ui/model/` or inside `ModelPicker.kt` if compact enough.
- Use `JBLabel`, `SimpleColoredComponent`, `JBHtmlPane` or safe HTML helpers, `Stack`, and `JBUI` spacing.
- Render VS Code-equivalent sections where data exists: header name/provider/star, free/BYOK/training badges, pricing, context window, release date, capabilities/modalities, terminal-bench, description, auto-routing candidates, and ids.
- Sanitize/escape model descriptions. Use `HtmlChunk`/`HtmlBuilder` or platform escaping helpers; do not concatenate unescaped HTML.
- Details panel should update from the currently previewed row in expanded mode and fall back to the active selected model when no preview row is set.
5. Rework popup layout and size behavior.
- Add an expand/minimize button to the search/header row in `ModelPicker.showPopup()`.
- Use pointer cursor on the button and localized tooltips/accessibility names: `Maximize model details` and `Minimize model details` or equivalent.
- Persist state under a key such as `kilo.model.picker.expanded` using `PropertiesComponent.getInstance().getBoolean(key, false)` and `setValue(key, value.toString())`.
- Collapsed mode keeps the current list-only width behavior.
- Expanded mode lays out list and details side-by-side with a splitter/separator and sets preferred width to approximately double the collapsed width, clamped to available screen bounds. Keep `setLocateWithinScreenBounds(true)`.
- Make `JBPopup` resizable only if the implementation can keep the retained layout stable; otherwise keep non-resizable and compute deterministic sizes.
6. Match VS Code interaction semantics where useful.
- Collapsed mode: clicking a model selects it immediately, preserving current behavior.
- Expanded mode: clicking/hovering or keyboard navigation updates the preview; Enter selects; double-click selects; Escape closes.
- Favorite star behavior remains unchanged and updates the details panel favorite state when toggled.
- When collapsing from expanded, clear transient preview state and show selected/active model on next expansion.
7. Add localization strings.
- Add base `KiloBundle.properties` entries for maximize/minimize tooltips, detail labels, pricing labels, context, capabilities, terminal-bench, description, auto-routing choices, and unavailable values.
- If existing localized files require key completeness, add English fallback values there too; otherwise rely on base bundle fallback consistent with project practice.
8. Add tests.
- Backend parser tests: ensure all new metadata fields parse from representative provider JSON and missing optional fields are tolerated.
- Mapper/serialization tests: ensure new DTO fields survive backend -> shared DTO serialization.
- `ModelPickerTest`: verify expanded property default is collapsed, toggle persists true/false, tooltip/icon state switches maximize/minimize, expanded width is greater than collapsed width, preview panel displays selected/hovered model metadata, and pointer cursor is set on the expand control.
- Renderer/details tests should exercise actual Swing components on the EDT using existing `BasePlatformTestCase` patterns.
- Update existing tests/helpers that construct `ModelPicker.Item`, `ModelItem`, or `ModelDto` as needed using default values to keep changes minimal.
## Risks And Constraints
- Full parity depends on metadata already present in CLI provider JSON. If a field is absent from the backend response for a provider, the details panel should omit that row rather than showing placeholders everywhere.
- Adding fields to shared serializable DTOs affects split-mode RPC compatibility. Keep new fields nullable/defaulted for safe deserialization.
- Avoid making provider loading fragile: optional preview metadata parsing must not throw on unexpected types.
- The popup is a lightweight `JBPopup`; changing size after showing may require recreating/repacking carefully. Prefer computing expanded/collapsed preferred size before show and updating the content with `revalidate()`/`repaint()` on toggle.
- Keep all Swing mutation on the EDT.
## Validation
- From `packages/kilo-jetbrains/`: run `./gradlew typecheck` or `bun run typecheck`.
- From `packages/kilo-jetbrains/`: run targeted tests for backend parser/serialization and frontend model picker tests, or `./gradlew test` if targeted Gradle filters are not straightforward.
- Manually verify in sandbox if feasible: `./gradlew runIde`, open model picker, confirm first open is collapsed, maximize doubles width and shows details, minimize returns to compact width, tooltip/cursor are correct, and the last state persists after closing/reopening the popup.
@@ -1,111 +0,0 @@
# Prompt Rollback + Redo — JetBrains Parity
Bring the VS Code "rollback / redo" (CLI `revert` / `unrevert`) feature to the JetBrains plugin, and fix the misleading VS Code hint string. Implementation-capable agent required — this plan touches source and tests.
## Context (verified)
- CLI already exposes everything needed; no CLI release or SDK regen required. Pinned CLI version is `7.4.1` (`packages/kilo-jetbrains/package.json`).
- `POST /session/{id}/revert` body `{ messageID, partID? }`, `POST /session/{id}/unrevert`.
- `Session.revert?: { messageID, partID?, snapshot?, diff? }``packages/sdk/js/src/gen/types.gen.ts:554`. Pushed via `session.updated` SSE.
- VS Code entry points: `RevertBanner.tsx`, `VscodeSessionTurn.tsx`, `session.tsx` context. Per-user-message rollback restores reverted prompt text into the input; redo banner shows count + per-file diff stats + Redo / Redo All + hint; reverted messages/parts hidden (`id >= revert.messageID`, partID partial-turn boundary).
- JetBrains split-mode packages differ from AGENTS.md's stated `ai.kilocode.jetbrains`; actual source roots are:
- shared → `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/`
- backend → `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/`
- frontend → `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/`
- All target files confirmed to exist: `SessionDto.kt`, `KiloSessionRpcApi.kt`, `KiloBackendChatManager.kt`, `KiloSessionRpcApiImpl.kt`, `KiloSessionService.kt`, `SessionController.kt`, `SessionModel.kt`, `SessionModelEvent.kt`, `SessionView.kt`, `MessageView.kt`, `MessageToolbar.kt`, `PromptPanel.kt`, `PromptEditorTextField.kt`, plus reuse targets `DiffStatBadge.kt`, `HoverIcon.kt`, `PermissionDiffView.kt`.
## Decisions
- **Reuse `DiffStatBadge`** (already the shared per-file +/- renderer used by `PermissionDiffView`) for the banner's per-file diff rows. No new diff-stat component.
- **Reuse the existing user-prompt hover toolbar (`MessageToolbar`)** for the per-message rollback control.
- **Reuse the existing `session.updated → model.setSession(...)` pipeline** for the revert boundary; no new SSE handling.
- **Stale non-English hint strings: reset all to the new English value** and flag for re-translation (applies to VS Code locales; JetBrains base bundle only, others fall back).
## Part A — VS Code string fix
File: `packages/kilo-vscode/webview-ui/src/i18n/en.ts:138`
- Change `revert.banner.hint` from `"Send a new message to make this permanent"` to `"You can redo these changes until you send a new message"`.
- In the 19 non-English locale files under `packages/kilo-vscode/webview-ui/src/i18n/` (ar, br, bs, da, de, es, fr, it, ja, ko, nl, no, pl, ru, th, tr, uk, zh, zht), overwrite `revert.banner.hint` with the identical new English string; leave a note that these need re-translation.
- Do not touch the other revert keys (`revert.banner.count_one`, `revert.banner.count_other`, `revert.banner.redo`, `revert.banner.redo.all`, `revert.disabled.agentBusy`) — only the hint changes meaning.
## Part B — JetBrains implementation
### 1. Shared (DTO + RPC contract)
- `shared/.../rpc/dto/SessionDto.kt`: add `val revert: SessionRevertDto? = null`; add `@Serializable data class SessionRevertDto(val messageID: String, val partID: String? = null, val snapshot: String? = null, val diff: String? = null)`. Field names must match CLI JSON for kotlinx auto-mapping through `session.updated`.
- `shared/.../rpc/KiloSessionRpcApi.kt`: add
- `suspend fun revert(id: String, directory: String, messageID: String, partID: String?)`
- `suspend fun unrevert(id: String, directory: String)`
### 2. Backend (RPC impl + HTTP)
- `backend/.../app/KiloBackendChatManager.kt`: add `revert(id, dir, messageID, partID?)``POST /session/$id/revert?directory=…` with `{messageID, partID?}` body, and `unrevert(id, dir)``POST /session/$id/unrevert?directory=…`. Mirror the existing `compact()` OkHttp pattern; add JSON body builders to `KiloCliDataParser`.
- `backend/.../rpc/KiloSessionRpcApiImpl.kt`: implement both, resolve worktree dir via `sessions.getDirectory(id, directory)`, wrap in `ready { }` like `compact`/`abort`.
### 3. Frontend service
- `frontend/.../app/KiloSessionService.kt`: add `suspend revert(...)` / `unrevert(...)` wrappers over the RPC, using `durable {}` per split-mode rules.
### 4. Frontend controller (`session/controller/SessionController.kt`)
- Add EDT-entry actions dispatched to the coroutine scope:
- `revert(messageID, partID?)`
- `unrevert()`
- `redo()` — mirror `RevertBanner.handleRedo`: find the next user message after the boundary and revert to it; `unrevert()` if none.
- `redoAll()``unrevert()`.
- Revert boundary already arrives via `is ChatEventDto.SessionUpdated -> model.setSession(...)` — no new SSE handling.
- On revert, restore the reverted user prompt text into the input (`session/ui/prompt/PromptPanel.kt` / `PromptEditorTextField`); on full redo (`redoAll` / final `unrevert`), clear it — matching VS Code and TUI.
### 5. Frontend model (`session/model/SessionModel.kt`, `SessionModelEvent.kt`)
- Add `SessionModelEvent.RevertChanged(revert: SessionRevertDto?)` with a stable `toString()`, fired from `setSession()` when the revert value changes. Reset in `loadHistory()` and `clear()`.
- Add derived helpers: `revert(): SessionRevertDto?` and `revertedCount()` (count of user messages with `id >= boundary`, mirroring `session.tsx`).
- Transcript hiding: expose whether a message id is hidden by the boundary (`id >= revert.messageID`, with partID partial-turn handling), applied both to newly-arriving messages and on `RevertChanged`.
- Follow the AGENTS.md "Adding a New Event" checklist (event subclass → model field + mutation + resets → controller handling → `-> Unit` stubs in exhaustive `when` blocks).
### 6. Frontend views
- New `session/ui/RevertBanner.kt` Swing view, built once and mutated via `update()` per retained-Swing rules. Listens to `RevertChanged` + `DiffUpdated`. Contents:
- arrow-left icon + count label (localized count.one/other),
- per-file diff rows reusing `ai.kilocode.client.ui.DiffStatBadge` fed from `model.diff` (`DiffFileDto.additions/deletions`),
- Redo button → `controller.redo()`, Redo All (only when count > 1) → `controller.redoAll()`,
- hint label (same new wording as Part A).
- Placed in `session/ui/SessionView.kt` at the bottom of the transcript, above the prompt input; shown only when `revert != null`.
- Per-user-message rollback control in `session/views/MessageView.kt`: user prompt bubbles already carry a hover `promptToolbar` (`MessageToolbar` with copy) + `promptHover` state. Add a rollback `HoverIcon` (arrow-left) alongside copy for user messages, wired through `SessionView → controller.revert(msgId)`. Disable/hide while the agent is busy or when there are no assistant messages after it, with the `revert.disabled.agentBusy` tooltip — mirroring `VscodeSessionTurn` `onRevert` + `data-revert-disabled`. Extend `MessageToolbar.kt` to host the extra action (or add a sibling control).
- Apply transcript hiding: `SessionView`/`TurnView` toggle `MessageView` visibility on `RevertChanged` (prefer visibility toggle over rebuild, per Swing lifecycle guidance).
### 7. i18n (`frontend/src/main/resources/messages/KiloBundle*.properties`)
- Add to base `KiloBundle.properties`: `revert.banner.count.one`, `revert.banner.count.other`, `revert.banner.redo`, `revert.banner.redo.all`, `revert.banner.hint` (new wording), `revert.disabled.agentBusy`.
- Other locale bundles fall back to base until translated; do not add stale translations.
### 8. Tests
- Backend: extend `KiloSessionRpcApiImplTest` / a `KiloBackendChatManager` test against `MockCliServer` asserting the exact HTTP path + JSON body for `revert`/`unrevert`, and that a session carrying `revert` deserializes into `SessionRevertDto`.
- Frontend controller (`SessionControllerTestBase` + `FakeSessionRpcApi`, add revert/unrevert call tracking): `revert()`/`unrevert()`/`redo()`/`redoAll()` hit the right RPC; `RevertChanged` fires; count logic; reverted messages hidden and restored; prompt-text restore on revert / clear on full redo.
- Frontend views (`BasePlatformTestCase`): `RevertBanner` renders count + per-file `DiffStatBadge` + redo buttons (Redo All only when count > 1); `MessageView` shows the rollback control on user messages and disables it while busy; retained-component assertions (`assertSame`, bounded count) per Swing lifecycle rules.
## Deliverables
| Area | Files |
|---|---|
| VS Code string | `webview-ui/src/i18n/en.ts` + 19 non-English locale hints reset |
| Shared | `SessionDto.kt` (+`SessionRevertDto`), `KiloSessionRpcApi.kt` |
| Backend | `KiloBackendChatManager.kt`, `KiloCliDataParser`, `KiloSessionRpcApiImpl.kt` |
| Frontend service/controller/model | `KiloSessionService.kt`, `SessionController.kt`, `SessionModel.kt`, `SessionModelEvent.kt` |
| Frontend views | new `RevertBanner.kt` (reusing `DiffStatBadge`), `MessageView.kt`, `MessageToolbar.kt`, `SessionView.kt`, `PromptPanel.kt` |
| i18n | `KiloBundle.properties` |
| Tests | backend RPC/chat-manager, frontend controller + view tests, `FakeSessionRpcApi` |
## Validation
- VS Code string change: `bun run typecheck` from repo root (or from `packages/kilo-vscode/`).
- JetBrains: `./gradlew typecheck` and `./gradlew test` from `packages/kilo-jetbrains/` (Java 21).
- Manual: `./gradlew runIde` (or split mode) to exercise rollback → redo → redo-all and reverted-transcript hiding.
## Risks / Notes
- Confirm no `kilocode_change` markers are needed for the VS Code i18n edits (Kilo-owned webview path — none required).
- Reset (not delete) is chosen for VS Code locale hints, so the fallback path is not relied upon; flag the 19 files for re-translation.
- Keep new Kotlin identifiers single-word per repo style; annotate EDT/model-touching methods with `@RequiresEdt`; wrap frontend RPC in `durable {}`.
@@ -1,199 +0,0 @@
# JetBrains Rollback Feature — Hardening Plan
Harden the new JetBrains rollback/redo feature: (1) communicate when only messages were
reverted because snapshots are disabled, and (24) close the code-duplication, testing,
end-to-end, and AGENTS-conformance gaps.
All work is inside `packages/kilo-jetbrains/`. No shared `packages/opencode/` files change,
so no `kilocode_change` markers are needed.
## Context an implementer needs
- Revert flow: user clicks the per-message Rollback button (`MessageToolbar`/`MessageView`)
`SessionController.revert()` aborts a busy turn then POSTs `/session/:id/revert`
→ CLI sets a revert marker → `SessionUpdated` event → `SessionModel.setRevert()`
`RevertBanner` shows + reverted messages hidden. `redo()`/`redoAll()` drive
`/session/:id/revert` (next user msg) and `/session/:id/unrevert`.
- Snapshot signal: `SessionRevertDto.snapshot` (`shared/.../rpc/dto/SessionDto.kt:22`) is
`null` exactly when the server did NOT restore files (snapshots disabled: `config.snapshot
=== false`, non-git worktree, or ACP). It is already parsed and reaches the frontend via
`SessionModel.revert()`. When `snapshot == null` the diff is also empty, so the banner shows
no file rows today. This is the reliable, no-new-API signal for "history only".
- Test harnesses already exist: frontend `SessionControllerTestBase` + `FakeSessionRpcApi`
(already tracks `rpc.reverts` and `rpc.unreverts`); backend `MockCliServer` +
`KiloBackendChatManagerTest`. Banner tests instantiate `RevertBanner(model, redo, redoAll)`
and read components via `components(banner)`.
## Key files
- `frontend/.../session/ui/RevertBanner.kt`
- `frontend/.../session/controller/SessionController.kt` (`revert`, `unrevert`, `redo`, `redoAll`, `capture`, `sessionProps`)
- `frontend/.../session/model/SessionModel.kt` (`revert()`, `revertedCount()`, `isRevertedMessage()`)
- `frontend/src/main/resources/messages/KiloBundle.properties` (+ translated `KiloBundle_*.properties`)
- `backend/.../app/KiloBackendChatManager.kt` (`revert`/`unrevert`), `backend/.../cli/KiloCliDataParser.kt` (`buildRevertJson`)
- `backend/.../app/KiloBackendSessionManager.kt` (`revertDto` duplication)
- Tests: `frontend/.../session/controller/TurnLifecycleTest.kt` (or new `RevertFlowTest.kt`),
`frontend/.../session/ui/SessionMessageListPanelTest.kt`,
`frontend/.../session/model/SessionModelTest.kt`,
`backend/.../testing/MockCliServer.kt`, `backend/.../app/KiloBackendChatManagerTest.kt`,
`backend/.../cli/KiloCliDataParserTest.kt`
## Resolved decisions
- **Do NOT hide/disable the Rollback button when snapshots are off.** This matches VS Code and
keeps history-only revert usable. We only add messaging.
- **Detect "files not restored" via `model.revert()?.snapshot == null`.** No new config/API,
no shared-code change.
- **Message it in the `RevertBanner`** with a distinct localized line, shown only when
`snapshot == null`.
- Proactively warning on the button before a revert (needs `config.snapshot`/`vcs` exposed to
the frontend) is **out of scope**.
---
## Phase 1 — Communicate history-only revert (snapshots disabled)
1. Add bundle key in `KiloBundle.properties` (place near the other `revert.banner.*` keys):
- `revert.banner.filesNotRestored=Snapshots are off — only the conversation was reverted; your files were not changed.`
- Add the same key to every translated `KiloBundle_*.properties` file. If translations are
not available, copy the English value (mirror how the other `revert.banner.*` keys were
seeded across bundles) so no key is missing.
2. In `RevertBanner.kt`:
- Add a retained `JBLabel` field `notice` styled like `hint` (`JBFont.small()`), created
once in `init` and added to `body` after `hint`.
- In `update()`, after computing `revert`, set
`notice.isVisible = revert.snapshot == null` and its text to
`KiloBundle.message("revert.banner.filesNotRestored")`.
- Color it via a theme API in `applyStyle` (use `UIUtil.getContextHelpForeground()` to match
the secondary `hint`; do not hardcode a color). Re-evaluate in `applyStyle` alongside
`hint.foreground`.
- Keep the existing empty-diff behavior (no file rows) — the notice replaces the missing
information rather than adding chrome.
3. Remove the redundant `isOpaque = false` on the `body` and `files` `Stack`s (see Phase 4);
do this while editing the file.
## Phase 2 — Close code-duplication gaps
4. In `KiloBackendSessionManager.kt`, collapse the two identical `revertDto(...)` overloads
(`SessionRevert` and `GlobalSessionRevert`) into a single private helper that maps the four
fields (`messageID`, `partID`, `snapshot`, `diff`) from a small shared shape. Simplest
approach: one private `fun revertDto(messageID: String?, partID: String?, snapshot: String?,
diff: String?)` returning `SessionRevertDto?` (null when `messageID` is null), and have both
call sites extract fields from their respective model type. Keep behavior identical; this is
a pure refactor covered by existing `KiloBackendSessionManagerTest` parsing assertions.
## Phase 3 — Fill testing + end-to-end gaps
### Frontend controller/model logic
5. `SessionModelTest.kt` — add direct model tests:
- `revertedCount()` returns the number of user messages from the marker to the end
(build entries `u1,a1,u2,a2`; marker at `u1` → 2; marker at `u2` → 1; marker id absent → 0).
- `isRevertedMessage()` true for the marker message and everything after, false before.
- `snapshot`-null marker still yields a non-zero `revertedCount()` (history-only case).
6. `TurnLifecycleTest.kt` (or new `RevertFlowTest.kt`) — add controller tests using `prompted()`:
- `redo()` reverts to the NEXT user message: seed `u1,a1,u2,a2`, apply marker at `u1`,
`edt { m.redo() }`, flush → assert `rpc.reverts` last call targets `u2`.
- `redo()` at the last user message calls unrevert: marker at `u2``redo()` → assert
`rpc.unreverts` recorded and no new revert call.
- `redoAll()` calls unrevert: assert `rpc.unreverts` contains `("ses_test","/test")`.
- `unrevert()` clears via RPC: assert `rpc.unreverts` populated.
### Frontend banner → controller wiring (end-to-end within the UI layer)
7. `SessionMessageListPanelTest.kt` — add wiring + history-only tests:
- Redo button click invokes the controller: build `RevertBanner(model, redo = { redo++ },
redoAll = { all++ })`, set a marker, `banner...first { text == redo }.doClick()` → assert
the redo lambda fired; same for Redo All when `revertedCount > 1`.
- History-only notice: marker with `SessionRevertDto("u1", snapshot = null)` → assert the
`revert.banner.filesNotRestored` label is visible and no file rows are present.
- Snapshot-present: marker with `snapshot = "snap1"` → assert the notice label is hidden.
### Full round-trip end-to-end (frontend)
8. Add one `RevertFlowTest` case that exercises the whole loop through `SessionControllerTestBase`:
- Seed a multi-turn session, `edt { m.revert(userMsgId) }`, flush → assert busy-abort
ordering + `rpc.reverts`.
- `emit(SessionUpdated(..., revert = SessionRevertDto(userMsgId, snapshot = "s1")))` →
assert `model.isRevertedMessage(userMsgId)` and reverted views hidden.
- Drive Redo All via `m.redoAll()`; `emit(SessionUpdated(..., revert = null))` → assert
messages restored (`isRevertedMessage` false) and marker cleared.
Use existing `emit`/`flush` helpers; no timeouts.
### Backend HTTP contract
9. `MockCliServer.kt` — add routing + capture fields (the current generic `/session/ses_*`
regexes are full-match and won't catch the `/revert` suffix, so new cases are safe):
- Fields: `lastRevertPath`, `lastRevertBody`, `lastUnrevertPath`, `lastUnrevertBody`
(`@Volatile var ... : String? = null`).
- Cases (place beside the `summarize`/`prompt_async` cases):
- `bare.matches(Regex("/session/ses_[^/]+/revert")) && method == "POST"` → store path+body,
`respond(output, 200, sessionCreate)`.
- `bare.matches(Regex("/session/ses_[^/]+/unrevert")) && method == "POST"` → store path+body,
`respond(output, 200, sessionCreate)`.
10. `KiloBackendChatManagerTest.kt` — mirror the existing `compact posts summarize request` test:
- `revert posts messageID and partID to revert endpoint`: call
`chat.revert("ses_abc", "/test/project", "msg1", "prt1")`; assert
`requestCount("/session/ses_abc/revert") == 1`, path starts with
`/session/ses_abc/revert?directory=`, body == `{"messageID":"msg1","partID":"prt1"}`.
- `revert omits partID when null`: body == `{"messageID":"msg1"}`.
- `unrevert posts empty body to unrevert endpoint`: body == `{}` and path prefix asserted.
11. `KiloCliDataParserTest.kt` — add `buildRevertJson` unit tests:
- `buildRevertJson("m1", null) == {"messageID":"m1"}`.
- `buildRevertJson("m1", "p1") == {"messageID":"m1","partID":"p1"}`.
- Escaping: an id containing a quote/backslash is JSON-escaped (reuse the parser's `escape`).
## Phase 4 — AGENTS conformance cleanup
12. Remove the unused bundle key `revert.disabled.agentBusy` from `KiloBundle.properties` and all
translated bundles (grep confirms it is referenced nowhere in Kotlin). Alternatively, if the
team wants VS Code parity, wire it as a disabled-state tooltip on the Rollback button — but
the recommended, lower-risk action is removal, since JetBrains intentionally aborts the busy
turn instead of disabling the button.
13. Remove redundant `isOpaque = false` on the transparent `Stack`s in `RevertBanner.kt`
(`body`, `files`) — `Stack` is already a no-color panel. Keep `isOpaque = false` on the
`BorderLayoutPanel` root only if it visibly overlays the session surface; otherwise remove it
too and rely on the default.
14. Add usage metrics for the new user-facing actions in `SessionController.kt`, using the
existing `capture(event, props)` + `sessionProps(id)` helpers:
- In `revert()` (on the success path, after `sessions.revert`): `capture("Session Rollback",
sessionProps(id))`.
- In `redo()`/`redoAll()`/`unrevert()`: `capture("Session Redo", sessionProps(id))` /
`capture("Session Redo All", ...)` as appropriate. Keep names consistent with existing
event naming; do not add PII.
## Out of scope
- Exposing `config.snapshot`/`vcs` to the frontend to proactively warn before a revert.
- Changing the retained-vs-rebuild strategy of `RevertBanner.files` (low-frequency update;
acceptable as-is).
- Replacing the full-project `ACTION_SYNCHRONIZE` in `synchronizeFromDisk` with a scoped VFS
refresh (only revisit if it shows up as a perf problem).
- Any VS Code webview parity changes beyond the shared i18n strings already present.
## Validation
Run from `packages/kilo-jetbrains/`:
- `./gradlew typecheck` — compiles all Kotlin including generated client.
- `./gradlew test` — runs frontend + backend suites, including the new tests. Requires Java 21;
only check Java if Gradle reports a Java error.
- Targeted while iterating: run the touched test classes (`TurnLifecycleTest`/`RevertFlowTest`,
`SessionMessageListPanelTest`, `SessionModelTest`, `KiloBackendChatManagerTest`,
`KiloCliDataParserTest`).
Manual smoke (optional, `./gradlew runIde`): trigger a rollback in a non-git directory (or with
`snapshot: false`) and confirm the banner shows the "files not changed" notice with no file rows;
trigger one in a git repo and confirm the notice is hidden and file rows appear.
## Changeset
Add `.changeset/<slug>.md` (`"@kilocode/kilo-jetbrains": patch`) describing the user-facing
change, e.g. "Clarify in JetBrains rollback that only the conversation was reverted when
snapshots are disabled." Test-only and refactor tasks do not need their own changeset.
## Suggested order
Phase 3 backend tests (911) and model tests (5) are independent and can be done first to lock
behavior. Then Phase 1 (user-facing messaging) with its banner tests (7). Then Phase 2 refactor,
Phase 4 cleanups, controller tests (6, 8), and the changeset last.
@@ -1,341 +0,0 @@
# JetBrains custom provider: close on OK, select, edit, edit/disconnect buttons
## Goal
Fix the JetBrains "Add custom provider" flow and add edit support:
1. Confirming the dialog closes it (OK label "Add" for add, "Save" for edit).
2. The added/edited provider becomes the selected row in the list.
3. Double-clicking an editable custom provider opens it for editing instead of disconnecting it.
4. Editable custom providers show both **Edit** and **Disconnect** action buttons.
All work is in `packages/kilo-jetbrains/frontend/`. No backend, RPC, or DTO changes. `saveCustom` already upserts, and `ProviderSettingsDto.config[id]` already carries the fields needed to prefill an edit. No `kilocode_change` markers are needed (all files are under Kilo-owned `packages/kilo-jetbrains/`).
## Root causes (verified in code)
- **Dialog does not close**: `CustomProviderDialog.syncActions()` sets `isOKActionEnabled = !saving && !fetching && modelIds().isNotEmpty()` (ProvidersSettingsUi.kt:957). On save success `doOKAction()` sets `outcome` and calls `closeOk()``super.doOKAction()` (line 966), which only closes when `getOKAction().isEnabled()` is true. `saving` stays `true`, so OK is disabled and nothing closes even though the log prints "closing dialog".
- **Double-click deletes**: `SettingsListView.mouseClicked` fires `primary(item)` on non-button double-clicks (SettingsListView.kt:65-72), and `primary` runs the first enabled cell. A configured custom provider only exposes `DISCONNECT` (ProviderListRows.kt:90), so double-click disconnects.
- **Edit feasibility**: `KiloBackendProviderSettingsManager.saveCustom` patches config as an upsert and only touches auth when a key/env is supplied (lines 154-166); a blank key on edit preserves the stored secret. Prefill source is `state.config[id]`: `name`, `options["baseURL"]`, `env` (first entry), `models` (model ids). The API key is a secret and is never returned, so the key field starts blank on edit.
- OK label is already `settings.providers.customAdd=Add`; edit mode needs a new "Save" label.
## Exact edits
### 1. `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
Add `settings.providers.edit=Edit` right after line 456 (`settings.providers.disconnect=Disconnect`):
```
settings.providers.edit=Edit
```
Add two keys in the custom block (after line 470 `customTitle` / line 471 `customAdd`):
```
settings.providers.customEditTitle=Edit OpenAI-Compatible Provider
settings.providers.customSave=Save
```
Only edit the base `KiloBundle.properties`; other locale files fall back to base for missing keys.
### 2. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt`
Add a helper at the end of the file (after `configured(...)`, line 99):
```kotlin
internal fun customEditable(provider: ProviderSettingsProviderDto, state: ProviderSettingsDto) =
state.config[provider.id]?.npm == CUSTOM_PROVIDER_PACKAGE
```
`CUSTOM_PROVIDER_PACKAGE` (`"@ai-sdk/openai-compatible"`) is already defined at line 19 and is the same signal the backend disconnect path uses.
### 3. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt`
Add `EDIT` to the enum (line 12-17):
```kotlin
internal enum class ProviderListAction {
CONNECT,
OAUTH,
EDIT,
DISCONNECT,
ENABLE,
}
```
Update `cells` (lines 35-43) to mark EDIT primary and keep both EDIT and DISCONNECT visible when connected:
```kotlin
override val cells: List<SettingsListCell>
get() = actions.map { action ->
SettingsListCell(
action.name,
providerListActionText(action),
enabled(action),
alwaysVisible = (action == ProviderListAction.DISCONNECT || action == ProviderListAction.EDIT) && connected,
primary = action == ProviderListAction.EDIT,
)
}
```
`enabled(action)` (line 45) already returns true for EDIT (`action != DISCONNECT`), no change.
Add the EDIT label to `providerListActionText` (lines 48-53):
```kotlin
ProviderListAction.EDIT -> KiloBundle.message("settings.providers.edit")
```
Update `providerActions` (lines 83-96) so configured editable custom providers expose EDIT + DISCONNECT:
```kotlin
internal fun providerActions(
provider: ProviderSettingsProviderDto,
state: ProviderSettingsDto,
disabled: Set<String> = state.disabled.toSet(),
): List<ProviderListAction> {
if (provider.id in disabled) return listOf(ProviderListAction.ENABLE)
if (provider.id == KILO_PROVIDER_ID && configured(provider, state, state.connected.toSet())) return emptyList()
if (configured(provider, state, state.connected.toSet())) {
return if (customEditable(provider, state)) {
listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT)
} else {
listOf(ProviderListAction.DISCONNECT)
}
}
val methods = providerMethods(provider, state)
return buildList {
if (methods.any { it.type == "oauth" }) add(ProviderListAction.OAUTH)
if (methods.any { it.type == "api" }) add(ProviderListAction.CONNECT)
}
}
```
### 4. `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt`
**4a. Import.** Add to the settings.base imports (near line 7):
```kotlin
import ai.kilocode.client.settings.base.SettingsListSelection
```
**4b. `view` field (line 196).** Pass the new edit callback:
```kotlin
private val view = ProvidersContent(::connect, ::oauth, ::disconnect, ::enable, ::edit)
```
**4c. Replace `custom()` (lines 314-330)** with add + edit + shared open:
```kotlin
@RequiresEdt
private fun custom() {
checkEdt()
openCustomDialog(null)
}
@RequiresEdt
private fun edit(provider: ProviderSettingsProviderDto) {
checkEdt()
val cfg = state.config[provider.id] ?: return
openCustomDialog(
CustomProviderEdit(
id = provider.id,
name = cfg.name ?: provider.name,
baseUrl = cfg.options["baseURL"].orEmpty(),
envVar = cfg.env.firstOrNull(),
models = cfg.models.values.map { it.id },
),
)
}
// The dialog performs the save itself so failures can be shown inline and the user can
// correct their input without re-typing. It only closes on a verified success.
@RequiresEdt
private fun openCustomDialog(existing: CustomProviderEdit?) {
checkEdt()
val dialog = CustomProviderDialog(
cs,
directory,
{ service<KiloProviderService>().fetchCustomModels(it) },
{ service<KiloProviderService>().saveCustom(it) },
existing,
)
if (!dialog.showAndGet()) return
val next = dialog.outcome ?: return
state = next
view.update(next, dialog.savedId)
clearProgress()
}
```
**4d. `ProvidersContent` (lines 520-591).** Add the `edit` callback and handle EDIT + selection.
Constructor (lines 520-525):
```kotlin
internal class ProvidersContent(
private val connect: (ProviderSettingsProviderDto) -> Unit,
private val oauth: (ProviderSettingsProviderDto) -> Unit,
private val disconnect: (ProviderSettingsProviderDto) -> Unit,
private val enable: (ProviderSettingsProviderDto) -> Unit,
private val edit: (ProviderSettingsProviderDto) -> Unit,
) : BaseContentPanel() {
```
`update` (lines 536-545) — add optional `select`:
```kotlin
@RequiresEdt
fun update(state: ProviderSettingsDto, select: String? = null) {
checkEdt()
val notes = state.providers.count { providerDescription(it).isNotBlank() }
ProvidersSettingsUi.LOG.info("provider settings content update: start providers=${state.providers.size} connected=${state.connected.size} disabled=${state.disabled.size} descriptions=$notes")
this.state = state
val rows = providerListRows(state, "", disabledRows = busy)
if (select != null) view.update(rows, SettingsListSelection.Key(select)) else view.update(rows)
ProvidersSettingsUi.LOG.info("provider settings content update: completed rows=${rows.size}")
}
```
`activate` `when` block (lines 580-585) — add EDIT:
```kotlin
when (action) {
ProviderListAction.CONNECT -> connect(row.provider)
ProviderListAction.OAUTH -> oauth(row.provider)
ProviderListAction.DISCONNECT -> disconnect(row.provider)
ProviderListAction.ENABLE -> enable(row.provider)
ProviderListAction.EDIT -> edit(row.provider)
}
```
**4e. `CustomProviderDialog`.** Add the edit-prefill data class, constructor param, prefill, `savedId`, edit-aware title/button, and the close fix.
Add near the dialog (e.g. above `internal class CustomProviderDialog`):
```kotlin
internal data class CustomProviderEdit(
val id: String,
val name: String,
val baseUrl: String,
val envVar: String?,
val models: List<String>,
)
```
Constructor (lines 645-650) — add `existing` as the last param (keeps existing positional test calls valid):
```kotlin
internal class CustomProviderDialog(
private val cs: CoroutineScope,
private val directory: String,
private val fetch: suspend (CustomModelFetchDto) -> CustomModelFetchResultDto,
private val save: suspend (CustomProviderSaveDto) -> ProviderActionResultDto,
private val existing: CustomProviderEdit? = null,
) : DialogWrapper(true) {
```
Add the `savedId` property next to `outcome` (lines 667-669):
```kotlin
// Set once the save succeeds; the panel reads it after the dialog closes to update the list.
var outcome: ProviderSettingsDto? = null
private set
// Id of the provider the save persisted; used to select the row after the dialog closes.
var savedId: String? = null
private set
```
`init` (lines 671-686) — edit-aware title/button and prefill:
```kotlin
init {
title = if (existing != null) {
KiloBundle.message("settings.providers.customEditTitle")
} else {
KiloBundle.message("settings.providers.customTitle")
}
setOKButtonText(
if (existing != null) KiloBundle.message("settings.providers.customSave")
else KiloBundle.message("settings.providers.customAdd"),
)
init()
initValidation()
existing?.let { prefill(it) }
models.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
syncActions()
}
})
pick.addActionListener {
if (fetching) cancelFetch()
else selectModels()
}
syncActions()
}
@RequiresEdt
private fun prefill(edit: CustomProviderEdit) {
id.text = edit.id
id.isEditable = false
name.text = edit.name
url.text = edit.baseUrl
env.text = edit.envVar.orEmpty()
models.text = edit.models.joinToString(", ")
}
```
Save-success branch in `doOKAction` (lines 767-769) — reset `saving`, re-enable OK, record id, close unconditionally:
```kotlin
ProvidersSettingsUi.LOG.info("custom provider add: save succeeded id='${input.id}', closing dialog")
outcome = result.state
savedId = input.id
saving = false
syncActions()
close(OK_EXIT_CODE)
```
Remove the now-unused `closeOk()` helper (line 966), or replace its body with `close(OK_EXIT_CODE)`; do not leave `super.doOKAction()` in the success path. `OK_EXIT_CODE` is a `DialogWrapper` constant available to the subclass without an import.
## Tests — `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt`
Run against the real EDT/component tree with `FakeProviderRpcApi` (no mocks).
**Update existing tests broken by the new EDIT action / callback:**
- `content()` helper (line 965): change to `edt { ProvidersContent({}, {}, {}, {}, {}) }` (5 lambdas).
- `test configured custom provider exposes only disconnect` (lines 306-324): rename to reflect edit + disconnect and assert `assertEquals(listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT), row.actions)`.
- `test source custom catalog providers remain visible while configured custom providers are connected` (line 412): change the `rows[0].actions` assertion to `listOf(ProviderListAction.EDIT, ProviderListAction.DISCONNECT)`.
**Add new tests:**
- Dialog closes on save success: reuse the `submit` + `flushUntil { edt { dialog.outcome != null } }` pattern; after success assert `dialog.savedId == "my-openai"` and `edt { dialog.isOKActionEnabled }` is `true` (proves the disabled-OK regression is fixed).
- Edit prefill: build `CustomProviderDialog(cs, "/tmp", { CustomModelFetchResultDto(listOf("gpt-4o")) }, { ProviderActionResultDto(...) }, CustomProviderEdit("my-openai", "My OpenAI", "https://example.com/v1", null, listOf("gpt-4o")))`. Assert the text fields (indices 0/1/2/4/5 for id/name/url/env/models) are populated, the key field (index 3) is blank, `id` field `isEditable == false`, and the OK button text equals `KiloBundle.message("settings.providers.customSave")`. (Field order via `filterIsInstance<JTextField>()`: 0=id, 1=name, 2=url, 3=key, 4=env, 5=models — `JBPasswordField` is a `JTextField`.)
- `providerActions` editable: given a provider with `config[id].npm == "@ai-sdk/openai-compatible"` and configured, assert `[EDIT, DISCONNECT]`, and that the EDIT cell has `primary == true` (via `row.cells.first { it.id == "EDIT" }.primary`).
- Non-editable connected provider unchanged: a connected non-custom provider (e.g. `anthropic`, connected) still yields `[DISCONNECT]`.
- Edit callback fires instead of disconnect on primary: construct `ProvidersContent` with recording lambdas (capture which provider each callback got), update with a configured editable provider, trigger the primary path, and assert the edit lambda ran and disconnect did not.
- Selection after update: `content.update(state, select = "local-openai")` then assert the list's selected row key is `"local-openai"`.
## Validation
Run from `packages/kilo-jetbrains/`:
- `bun run typecheck` (or `./gradlew typecheck`).
- `./gradlew test` (target `ProvidersSettingsUiTest` first if iterating).
- Manual smoke via `./gradlew runIde`: add a custom provider → dialog closes and the new row is selected; double-click the row → edit dialog opens prefilled with the id field locked and "Save" button; the connected custom row shows both Edit and Disconnect.
Java 21 is required for Gradle; only check `java -version` if a Gradle command fails with a Java error.
## Notes / risks
- API key is intentionally not prefilled on edit (secret). Blank key preserves the stored key; a new value updates it — matches `saveCustom` behavior.
- The id field is locked on edit because the id is the config key; renaming would create a second provider.
- Double-click on a visible action button (Edit/Disconnect) still routes through the single-cell click path, not `primary`; only double-clicks on the non-button row area trigger the primary (EDIT) action.
- Keep new identifiers single-word where clear (`existing`, `cfg`, `select`, `edit`, `prefill`, `savedId`).
## Out of scope
- Editing headers, per-model capabilities (reasoning), or config scope (global vs workspace) beyond the current add dialog.
- Any changes to shared `packages/opencode/` code, RPC contracts, or DTOs.
@@ -1,198 +0,0 @@
# Consolidate JetBrains model pickers into a shared PickerPopup base
## Goal
Remove duplication between the two list-picker popups in the JetBrains frontend by
extracting a generic base, and make both consume it:
1. Prompt model picker — `session/ui/model/ModelPicker.kt` (`showPopup`) — single-select
commit, search field, expand/collapse details panel, sections, per-row favorite (star)
toggle.
2. Custom provider model picker — `settings/providers/ProvidersSettingsUi.kt`
(`showModelPopup` inside `CustomProviderDialog`) — multi-select toggle, no search, no
details, selection stored in the `models` text field.
The base must expose configurable options; the provider picker enables: multi-select, no
expanded/details view, and extra header toolbar buttons ("Select All" / "Deselect All").
All work is under `packages/kilo-jetbrains/frontend/`. No backend/RPC/DTO changes. Files are
Kilo-owned (no `kilocode_change` markers).
## Resolved decisions
- **Approach**: Extract a generic `PickerPopup<T>` base; both pickers become thin configs.
- **Selection state**: Caller owns it. Base is selection-stateless: it calls `checked(row)`
to render check state and `onPrimary(row)` on activation. Provider keeps the `models` text
field as source of truth; prompt keeps its favorites callbacks.
- **Select All / Deselect All**: Two caller-supplied header toolbar buttons. Remove the
in-list `CustomModelRow.selectAll` row entirely.
- **Renderer**: Extract a base renderer skeleton too; `ModelPickerRenderer` and the provider
renderer extend it. Generalize the favorite hit-zone into a trailing click-zone helper.
- **Location**: New package `ai/kilocode/client/ui/picker/` with `PickerPopup<T>` and
`PickerListRenderer<T>`. `PickerRow` and `ModelSearch` stay where they are and are reused.
## Base design
### `ui/picker/PickerPopup.kt` — generic popup
Owns: `JBList<T>` + `CollectionListModel<T>`, `createComponentPopupBuilder` with the shared
flags (requestFocus/focusable/cancelOnClickOutside/cancelKeyEnabled/cancelOnWindowDeactivation/
locateWithinScreenBounds, non-resizable, non-movable), `popupBackground` helper (move the
duplicated `NewUI -> Popup.BACKGROUND else getListBackground()` here), header assembly,
mouse hit-testing, keyboard bindings, auto-select-on-move + scrolling install, and size
computation.
Config (constructor params / vars, defaults match current behavior):
- `anchor: JComponent`, `placement: Placement` (`ABOVE` / `BELOW` / `UNDERNEATH`). Prompt uses
`PopupShowOptions.aboveComponent` / `showUnderneathOf`; provider uses `showUnderneathOf(pick)`.
- `rows: (query: String) -> List<T>` — rebuilt when the search text changes. Provider passes a
function that ignores `query`.
- `renderer: PickerListRenderer<T>`.
- `checked: (T) -> Boolean` — drives the row check icon (single: `row.key == active`; multi:
`isSelected(row)`).
- `sectionTitle: (rows: List<T>, index: Int) -> String?` — optional; prompt supplies
`modelPickerSectionTitle`, provider passes `{ _, _ -> null }`.
- `mode: Mode` = `Single` | `Multi`.
- Single: on primary click/ENTER call `onPrimary(row)` then `popup.closeOk(null)`.
- Multi: on primary click/SPACE/ENTER call `onPrimary(row)`, repaint, stay open.
- `onPrimary: (T) -> Unit`. (Prompt decides activate-vs-clear inside based on `row.item == null`;
provider toggles membership in the text field.)
- Optional trailing toggle: `trailingHit: (list, bounds, point) -> Boolean` and
`onTrailing: (T) -> Unit`. Prompt supplies favorite hit-zone + favorite toggle; provider omits.
Keyboard: in Single mode `Shift+SPACE` triggers trailing when present.
- `search: Boolean` — show `SearchTextField` in header CENTER. Prompt true; provider false.
- `toolbar: List<JComponent>` — extra header buttons placed in header WEST. Provider passes
Select All / Deselect All; prompt passes empty.
- Optional details/expand: `details: JComponent?` + `onPreview: (T?) -> Unit` +
`expandStateKey: String?`. When non-null, base shows the expand `HoverIcon`, EAST details
panel, persists expanded state via `PropertiesComponent`, reserves details width when
expanded, and `Disposer.register(popup, details)` if `details is Disposable`. Provider passes
null (no expand toggle, no details).
- Sizing: `minWidth`, `maxWidth`, `maxVisibleRows`, `emptyListHeight` (defaults = current
ModelPicker constants 420/760/10/120). Provider passes `minWidth = 320` and a `maxWidth`
that preserves today's look. Reuse the existing `computeInitialPopupSize` /
`computeListPreferredWidth` / `computeListPreferredHeight` logic, moved into the base and
parameterized by these constants + optional details width.
Header layout: `BorderLayout` with `toolbar` row WEST, `search` CENTER (empty when
`search=false`), expand `HoverIcon` EAST (only when details present). Preserve
`AbstractPopup.customizeSearchFieldLook` and background wiring.
Keyboard (registered on both the list and, when present, the search editor):
- `UP`/`DOWN` → move selection (search field only; list uses `ScrollingUtil`).
- `ENTER` → primary on selected row.
- `ESC``popup.cancel()`.
- Single + trailing present → `Shift+SPACE` → trailing on selected row.
- Multi → `SPACE` → primary (toggle) on selected row.
Mouse (`mouseReleased`, `UIUtil.isActionClick`): resolve row via `locationToIndex` +
`getCellBounds`/`contains`; if trailing present and `trailingHit``onTrailing` + consume;
else `onPrimary` (Single closes, Multi consumes + repaints).
### `ui/picker/PickerListRenderer.kt` — renderer skeleton
Abstract `ListCellRenderer<T>` base: `PickerRow` wrap + optional top `GroupHeaderSeparator`
(driven by `sectionTitle`) + `[check icon | content | trailing]` layout with the shared
transparent row + `UiStyle.Gap.md/lg/md/pad` insets. Provides:
- `check` icon column (`AllIcons.Actions.Checked` / `EmptyIcon`), set from `checked(row)`.
- protected `content: JComponent` slot and optional `trailing: JComponent` slot.
- section separator top panel wiring (`top`/`sep` from `ModelPickerRenderer`).
- a companion `trailingClickZone(list, bounds, point, width)` generalizing
`ModelPickerRenderer.isFavoriteClick` (keep `FAVORITE_CLICK_AREA_WIDTH` behavior).
## Ordered implementation tasks
1. **Create `ui/picker/PickerListRenderer.kt`**: base renderer skeleton with check column,
`PickerRow` wrap, top section separator, content/trailing slots, transparent row + insets,
and the `trailingClickZone` helper (moved/generalized from `ModelPickerRenderer.isFavoriteClick`
/ `favoriteInset`).
2. **Create `ui/picker/PickerPopup.kt`**: generic popup per the design above, including the
moved `popupBackground` helper and the sizing helpers (parameterized).
3. **Refactor `ModelPickerRenderer`** to extend `PickerListRenderer`: content = title
(`SimpleColoredComponent`) + warn + free/BYOK badges + provider label; trailing = favorite
star; check via `checked`. Keep existing internal test accessors (`starIcon`,
`badgeVisible`, `badgeText`, `byokVisible`, `warningVisible`, `warningTooltip`) and the
`DATA_COLLECTED`/`checked`/`empty` companion members so `ModelPickerTest` compiles unchanged.
4. **Refactor `ModelPicker.showPopup`** to build a `PickerPopup<ModelPickerRow>` configured
as: Single mode; `rows = { q -> modelPickerRows(items, favorites(), q, allowEmpty, emptyText, includeSmall) }`;
`checked = { it.key == selected?.key }`; `sectionTitle = ::modelPickerSectionTitle`;
`onPrimary = { row -> row.item?.let(::activate) ?: clear() }`; trailing = favorite hit-zone +
`onFavoriteToggle`; `search = true`; details = `ModelDetailsPanel` with `expandStateKey =
MODEL_PICKER_EXPANDED_KEY`; placement from `placement`; sizing constants = current values.
Keep `ModelPicker`'s public API (`setItems`, `select`, `clearSelection`, `open`, callbacks,
`Placement`, `Item`) and test hooks (`selectedForTest`, `selectionKeyForTest`,
`expandedForTest`) unchanged so `PromptPanel`, `settings/models`, and `settings/agents`
consumers are unaffected.
5. **Add provider picker renderer** (small subclass of `PickerListRenderer<String>` in
`settings/providers/`, or a shared simple text renderer): content = a `JBLabel` showing the
model id; no trailing; check via `checked`.
6. **Rewrite `CustomProviderDialog.showModelPopup(ids)`** to build a `PickerPopup<String>`:
Multi mode; `rows = { _ -> ids }` (raw model ids, no select-all row); `checked = { it in modelIds().toSet() }`;
`onPrimary = { toggleModel(it) }`; `sectionTitle = { _, _ -> null }`; `search = false`;
`toolbar = listOf(selectAllButton, deselectAllButton)`; details = null; `minWidth = 320`;
anchor = `pick`, placement `UNDERNEATH`.
7. **Extract provider selection mutations into testable helpers** on/near `CustomProviderDialog`
operating on the `models` text field: `toggleModel(id)`, `selectAllModels(ids)`,
`clearModels()` (reuse existing `modelIds()` / `setModelIds()`). Wire the two toolbar buttons
to `selectAllModels(ids)` and `clearModels()`.
8. **Remove dead code**: `CustomModelRow`, `customModelRows`, `CustomModelRowRenderer`,
`CUSTOM_MODEL_POPUP_MIN_WIDTH`/`CUSTOM_MODEL_POPUP_MAX_ROWS` (fold into base config), and the
now-duplicated inline `popupBackground` in `ProvidersSettingsUi.kt`.
9. **Strings**: add a "Deselect All" key to `KiloBundle.properties` (e.g.
`settings.providers.customModelsDeselectAll=Deselect All`); reuse existing
`settings.providers.customModelsSelectAll` for "Select All".
## Tests
- **Keep passing unchanged**: `ModelPickerTest.kt` (row builder + renderer + `ModelSearch` +
favorite hit-zone). Because `ModelPickerRenderer` keeps its accessors and companion members and
`modelPickerRows`/`ModelPickerRow` are untouched, these should compile and pass as-is. Verify
the favorite hit-zone tests still reference a working symbol (either keep
`ModelPickerRenderer.isFavoriteClick` delegating to `PickerListRenderer.trailingClickZone`, or
update the 3 call sites in `ModelPickerTest.kt` to the new helper).
- **Update** `ProvidersSettingsUiTest.kt`:
- Replace `test custom model rows start with select all` (there is no select-all row anymore)
with tests for the extracted helpers: `toggleModel` adds/removes an id in the text field;
`selectAllModels(ids)` sets all; `clearModels()` empties it.
- Keep existing add/edit/delete and dialog-close tests working.
- **New**: a small `PickerListRenderer`/`PickerPopup` unit test where feasible without a live
popup — e.g. `trailingClickZone` geometry (mirroring the current `isFavoriteClick` tests) and
the provider text renderer check-state. Avoid asserting live-popup internals; follow the
existing pattern of testing renderers/row-builders/pure helpers directly (per package AGENTS
"test the real implementation, no mocks").
## Validation
From `packages/kilo-jetbrains/`:
- `./gradlew typecheck`
- `./gradlew test` (iterate with `--tests ai.kilocode.client.session.ui.model.ModelPickerTest`
and `--tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`).
- Manual smoke via `./gradlew runIde`:
- Prompt model picker: search, arrow/enter select, favorite star toggle, expand/collapse
details, placement — all unchanged.
- Add custom provider → fetch models → popup shows model list with "Select All"/"Deselect All"
toolbar buttons, per-row multi-select toggles, no details/expand, selection reflected in the
Model IDs field; Save persists selected models.
## Risks / notes
- `ModelPicker`'s public API and test hooks must stay stable — several settings screens depend on
it. Only the popup internals move.
- The details/expand panel is `Disposable`; the base must register it with the popup exactly as
today to avoid leaks.
- Sizing differs (prompt 420/760 dynamic + details; provider fixed 320). Parameterize rather than
hard-code; keep provider width visually equivalent to current.
- Popups are hard to unit test; keep coverage on pure helpers/renderers, not live-popup behavior.
- Keyboard/mouse semantics differ by mode (single-commit-close vs multi-toggle-stay-open) and by
presence of a trailing toggle — encode these in the base mode, don't special-case callers.
## Out of scope
- The other picker popups (`ModePicker`, `ReasoningPicker`, `SessionAccountOverlay`) — not part
of this consolidation.
- Adding search to the provider picker (kept off to match current behavior; base supports it).
- Any backend/RPC/DTO or CLI changes.
@@ -1,67 +0,0 @@
# JetBrains MdView Editor Theme Colors Plan
## Goal
Keep JetBrains markdown layout compact while making markdown colors and backgrounds come from the active editor color scheme, and ensure existing theme/editor-setting listeners refresh all existing `MdView` instances after changes.
## Findings
- `SessionUi` already subscribes to `EditorColorsManager.TOPIC` and `LafManagerListener.TOPIC`, then calls `applyStyle(SessionEditorStyle.current())` on the session tree.
- Most markdown consumers already propagate that style through `MdView.applyStyle(style)` via `TextView`, `ReasoningView`, message lists, and session panels.
- `PlanExitView.applyStyle()` sets only font/code font/foreground and does not call `md.applyStyle(style)`, so internal markdown role colors can stay tied to the initial style.
- `MdViewHybrid.applyStyle()` restyles retained HTML panes and code block containers, but retained `CodeField` editors need explicit reapplication of `SessionEditorStyle.applyToEditor()` after creation so syntax highlighting and editor colors follow scheme changes.
- `MdCommon.defaults()` currently mixes editor colors, UI theme colors, and one literal inline-code color fallback. The literal fallback should be removed, and markdown roles should derive from `EditorColorsScheme`/syntax attributes wherever possible.
- `TextView` currently overrides markdown background with `SessionUiStyle.Transcript.bgColor()` (`UiStyle.Colors.bg()`), while prompt markdown already uses `style.editorBackground`. If markdown surfaces should consistently use editor background, normal text views need to use `style.editorBackground` too.
## Implementation Plan
1. Keep compact CSS unchanged.
- Do not reintroduce line-height, margin, padding, heading sizing, or other geometry rules.
- Keep the current role color/background selectors only.
2. Derive markdown role defaults from editor settings.
- Update `MdCommon.defaults(style)` to use `style.editorForeground` and `style.editorBackground` for primary text/background.
- Add small local helper functions for editor attributes, for example foreground/background from `style.editorScheme.getAttributes(key)` and color keys from `style.editorScheme.getColor(key)`.
- Use public IntelliJ editor keys for role colors:
- Links: `CodeInsightColors.HYPERLINK_ATTRIBUTES.foregroundColor`, fallback to platform link color if absent.
- Inline code foreground/background: `DefaultLanguageHighlighterColors.DOC_CODE_INLINE`, fallback to `STRING`, then editor foreground/background.
- Code block foreground/background: `DefaultLanguageHighlighterColors.DOC_CODE_BLOCK`, fallback to editor foreground/background.
- Quote/emphasis/list weak text: comment/doc-comment attributes, fallback to editor foreground or `UIUtil.getContextHelpForeground()` only when the scheme has no useful value.
- Borders/HR/table/code border: editor preview/border color keys such as `EditorColors.PREVIEW_BORDER_COLOR`, fallback to `UiStyle.Colors.contentBorder()`.
- Remove `JBColor(0x...)` or other literal runtime color fallbacks from `MdCommon`.
- Keep public `MdView` API unchanged; role colors stay internal unless a concrete external override need appears.
3. Ensure style propagation reaches every markdown instance.
- Update `PlanExitView.applyStyle(style)` to call `md.applyStyle(style)` before applying its explicit font/code-font/foreground overrides.
- Audit existing markdown callers after the change; keep using the existing `SessionEditorStyleTarget` propagation path rather than adding per-`MdView` theme listeners.
- Keep explicit foreground overrides in `TextView`, `ReasoningView`, and `PlanExitView` where they intentionally set body text role, but let internal markdown role colors refresh from the new style snapshot.
4. Ensure retained hybrid code blocks update after editor setting changes.
- In `MdViewHybrid.CodeView.style(opts)`, for retained `CodeField` blocks, reapply `style.applyToEditor(editor)` to the underlying editor if it exists.
- Reapply code editor background/scroll pane/viewport backgrounds from `opts.preBg` in the same path.
- Preserve retained component/editor reuse: do not rebuild code block panes just to update style.
5. Align markdown backgrounds with editor settings.
- Keep `MdCommon` default background as `style.editorBackground`.
- Change normal `TextView` markdown background to `style.editorBackground` if the intent is that all markdown surfaces use editor background, matching `PromptView` and session root behavior.
- Preserve `transparent` handling: when `md.opaque = false`, background should still be the editor-derived value for child/code surfaces, but the Swing component should remain non-opaque.
6. Update tests.
- Extend `MdViewTest` to verify markdown role CSS changes when applying a `SessionEditorStyle` backed by a customized editor scheme, especially inline code, code block, link, and border colors.
- Extend `MdViewHybridTest` to assert `applyStyle()` updates retained HTML panes and retained code editors without replacing them, including editor scheme/background changes.
- Add or extend `PlanExitViewTest` so `applyStyle()` refreshes the nested markdown style, not just foreground/font overrides.
- Keep existing compactness expectations: tests should not assert new spacing, padding, margin, line-height, or size rules.
## Verification
- From `packages/kilo-jetbrains/`, run `./gradlew frontend:test --tests '*MdView*'`.
- From `packages/kilo-jetbrains/`, run `./gradlew frontend:test --tests '*PlanExitViewTest*'` if the focused test is not covered by the MdView filter.
- From `packages/kilo-jetbrains/`, run `bun run typecheck`.
## Constraints
- Do not introduce JCEF, Compose, or Kotlin UI DSL.
- Do not add new theme/editor listeners in `MdView`; use the existing `SessionUi` listener and `SessionEditorStyleTarget` propagation path.
- Avoid hardcoded runtime colors in markdown styling; prefer editor scheme attributes/color keys, then platform/theme APIs as non-literal fallbacks.
- Preserve retained Swing behavior and `MdViewHybrid.sync()` component reuse.
- Keep the public `MdView` override API stable unless implementation proves a new external override is required.
@@ -1,69 +0,0 @@
# JetBrains MdView VS Code Styling Plan
## Goal
Improve JetBrains markdown output so assistant/user transcript markdown visually matches the VS Code webview markdown style while keeping the existing Swing/JBHtmlPane + editor-backed code block architecture.
## Findings
- VS Code markdown styling is split across `packages/ui/src/components/markdown.css`, `packages/kilo-ui/src/components/markdown.css`, `packages/kilo-ui/src/styles/vscode-bridge.css`, and message-part overrides.
- Base VS Code markdown uses 14px sans text, 160% line height, tight first/last margins, same-size medium headings, 12px paragraph spacing, link-colored anchors, compact lists, weak list markers, weak blockquotes with a 2px left border, invisible HR spacing, bordered/padded code blocks, green inline code, and lightly bordered tables.
- The VS Code theme bridge maps markdown roles to editor/theme tokens: heading/link/list/image use `textLinkForeground`, text/strong/code-block use editor foreground, inline code uses charts/syntax green, quote/emphasis use description foreground, HR uses panel border.
- JetBrains markdown is rendered by `MdViewHybrid` and `MdViewHtmlPane`, with shared CSS from `MdCommon.rules()` and defaults from `MdCommon.defaults()`.
- JetBrains currently styles only broad tag font/color, links, code/pre colors, blockquote border/text color, and table border. It lacks VS Code-equivalent spacing, heading/strong/emphasis/list marker/table cell rules, inline-code foreground, blockquote geometry, HR spacing, and code block surface polish.
- JetBrains fenced code blocks are already stronger than VS Code in one respect: they use `EditorTextField` with real IDE syntax highlighting and streaming retention. Preserve this instead of switching to web/JCEF rendering.
## Implementation Plan
1. Expand JetBrains markdown style tokens.
- Add internal fields to `MdStyle` for heading, strong, emphasis, inline code foreground, list marker, HR, table/header, and code block border colors.
- Keep the public `MdView` override API stable unless a new external override is clearly needed.
- Compute defaults in `MdCommon.defaults(style)` from IntelliJ/editor theme sources and centralized Kilo semantic colors where no platform key matches.
- Use `JBColor.namedColor("Kilo.Markdown.*", fallback)` for Kilo-specific markdown palette fallbacks, so themes can override them and runtime code avoids scattered hardcoded colors.
2. Mirror VS Code markdown CSS in `MdCommon.rules()`.
- Add root/body wrapping rules: max width behavior, break-word wrapping, base line-height, and first/last-child margin trimming where supported by `JBHtmlPane` CSS.
- Add heading rules: same base size, medium/bold weight, role-specific color, line height, and bottom spacing.
- Add paragraph, list, list item, nested list, and marker rules. If Swing HTML does not support `::marker`, fall back to `li { color: ... }` plus child text color reset only if supported; otherwise keep list text normal and document the limitation in tests.
- Add strong/emphasis colors matching VS Code token roles.
- Add anchor styling matching VS Code: themed link color, no forced background, and underline behavior where `JBHtmlPane` supports it.
- Add blockquote geometry: 2px left border, 24px vertical margin, 8px left padding, weak text, and normal style.
- Add table layout rules: collapse borders, full width where possible, 24px vertical margin, 12px cell padding, weak row borders, stronger header text.
- Keep HRs visually hidden but spaced consistently with VS Code if the renderer includes them. `MdViewHybrid` currently filters thematic breaks, so this mainly benefits `MdViewHtmlPane` and future reuse.
- Add inline-code foreground and medium font weight. Avoid inline code backgrounds unless the current `JBHtmlPane` configuration already draws them acceptably.
3. Polish JetBrains code block containers without losing IDE highlighting.
- Keep `EditorTextField` for fenced/indented blocks and `JBTextArea` fallback.
- Style `JBScrollPane` code blocks to match VS Codes `markdown-code` wrapper feel: subtle background, subtle border, rounded-ish platform arc if feasible, 12px-ish padding, and thin horizontal scrollbar behavior.
- Use `SessionUiStyle.View.Code` for geometry constants. Add only minimal new constants there if current values cannot represent the VS Code spacing.
- Separate code block border color from table border internally so table styling can change without affecting code boxes.
- Continue applying `SessionEditorStyle.applyToEditor(ed)` so code blocks follow IDE syntax highlighting and editor font changes.
4. Add file/path affordance parity where safe.
- For markdown links whose `href` looks like a relative file path, keep existing link dispatch so the current caller can open files/URLs appropriately.
- Consider decorating inline code that looks like a path with a `file-link` class only when an `openFile` callback is available through the existing usage path. If the current `MdView` abstraction only has `openUrl`, do not widen it unless the call sites can pass file opening cleanly.
- At minimum, make inline code/path-looking content visually closer to VS Code by using the inline-code foreground and dotted underline for explicit file links where generated HTML contains link/code classes.
5. Preserve retained Swing behavior.
- Keep `MdViewHybrid.sync()` prefix reuse logic unchanged unless necessary.
- Ensure style updates call `reloadCssStylesheets()` and reassign text only for retained `JBHtmlPane` blocks, not by rebuilding all blocks.
- Keep streaming fenced-code fast path and editor disposal behavior intact.
6. Add focused tests.
- Extend `MdViewTest` and/or `MdViewHybridTest` to assert `overrideSheet()` contains the new VS Code-equivalent rules for headings, strong/emphasis, links, inline code foreground, list/table/blockquote spacing, HR, and code block/table border separation.
- Add component tests for code block pane styling: background, viewport background, border color, padding, scrollbar policy, and retained editor instance after `applyStyle()`.
- Keep existing stress/leak tests green. Add a small stress assertion only if the implementation changes style application semantics.
- Add a changeset: `@kilocode/kilo-jetbrains` patch with user-facing wording such as `Improve markdown readability in JetBrains chat transcripts.`
## Verification
- Run targeted JetBrains markdown tests first from `packages/kilo-jetbrains/`: `./gradlew frontend:test --tests '*MdView*'` if the Gradle module supports it; otherwise run the closest supported targeted Gradle test command.
- Run `bun run typecheck` from `packages/kilo-jetbrains/`.
- If targeted Gradle filtering is unreliable, run `./gradlew test` from `packages/kilo-jetbrains/`.
## Constraints
- Do not introduce JCEF, Compose, or Kotlin UI DSL.
- Keep changes inside `packages/kilo-jetbrains/` and `.changeset/` unless a shared Kilo UI source of truth is explicitly required.
- No `kilocode_change` markers are needed for JetBrains or Kilo UI paths.
- Prefer IntelliJ theme APIs and centralized semantic tokens over scattered literal colors.
@@ -1,63 +0,0 @@
# JetBrains Session UI Icons And Header Layout Plan
## Goal
Improve JetBrains session UI icon consistency and reduce accidental header interactions:
- Use Kilo/VS Code-aligned session icons in session views.
- Fix the reasoning header icon.
- Normalize session part collapse/expand chevrons so collapsed/expanded states do not jump between differently sized glyphs.
- Move the session-details toggle away from compaction and place it before the session title.
## Findings
- Session view icons are centralized in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt` and loaded from `frontend/src/main/resources/icons/views/*.svg`.
- The JetBrains `views` SVGs already mirror the shared VS Code/UI icon paths from `packages/ui/src/components/icon.tsx` for the audited names, including `brain`, `chevron-down`, `chevron-right`, `checklist`, `console`, `warning`, etc.
- The reasoning view currently renders `SessionViewIcons.eye` in `ReasoningView.kt`; VS Code/shared UI uses the `brain` icon for reasoning/thinking surfaces, and `SessionViewIcons.brain` already exists.
- Standard collapsible session parts use `SessionViewIcons.chevronDown` when expanded and `SessionViewIcons.chevronRight` when collapsed in `AbstractSessionPartView.kt`; `QuestionResultView.kt` repeats this pattern manually. The down/right SVG paths have different visual extents.
- The session header currently places the details toggle next to the compact button in the right-side controls in `SessionHeaderPanel.kt`, making the two actions easy to confuse.
## Implementation Steps
1. **Keep icon sources aligned with VS Code/shared UI**
- Treat `packages/ui/src/components/icon.tsx` as the source for Kilo web/session glyph shapes.
- Re-check `SessionViewIcons.kt` entries against available JetBrains assets; only update or add SVGs if a session view uses a Kilo icon missing from `frontend/src/main/resources/icons/views/`.
- Preserve JetBrains SVG theming rules: no `currentColor`; use literal palette colors and dark variants where assets are added or changed.
2. **Fix reasoning icon**
- In `ReasoningView.kt`, change the reasoning header glyph from `SessionViewIcons.eye` to `SessionViewIcons.brain`.
- Add or update test coverage in `ReasoningViewTest.kt` by inspecting the rendered Swing label tree and asserting the reasoning icon is `SessionViewIcons.brain`.
3. **Normalize collapse/expand chevrons for session parts**
- Stop using the mixed `chevronRight`/`chevronDown` pair for collapsible session content.
- Use a single base Kilo chevron glyph for both states, matching the current custom chevron used by the session header (`/icons/chevron-down.svg` / equivalent `SessionViewIcons.chevronDown`).
- Add a shared rotated icon for the opposite state instead of switching to a differently sized right-facing asset. Prefer a small reusable helper or centralized icon field rather than importing header-specific UI into session views.
- Update `AbstractSessionPartView.kt` and `QuestionResultView.kt` to use the normalized chevron pair.
- Leave `QuestionView.kt` navigation chevrons alone unless auditing shows they are being used for collapse/expand; those are previous/next controls, not expand/collapse controls.
4. **Relocate and change header show/hide details toggle**
- In `SessionHeaderPanel.kt`, replace the custom header details chevron with platform `AllIcons` arrows/chevrons, e.g. collapsed = `AllIcons.General.ArrowRight`, expanded = `AllIcons.General.ArrowDown`.
- Move the details toggle out of the right-side controls and into `BorderLayout.WEST` of the header row.
- Rebuild the top header as:
- outer border layout
- west: details toggle button
- center: inner border layout
- inner center: session title
- inner east: horizontal stack/row with price/context and compact button
- Remove the details toggle from the right-side row so compaction remains visually separate from show/hide details.
- Keep existing tooltip/accessibility strings and expansion persistence behavior unchanged.
5. **Tests**
- Update `SessionHeaderPanelTest.kt` to assert:
- collapsed/expanded header details icons use the selected `AllIcons` constants;
- the details toggle persists expansion state as before;
- the details toggle is parented/laid out separately from the compact button.
- Update `AbstractSessionPartViewTest.kt` to assert collapsible parts keep the same icon dimensions across collapsed/expanded states and no longer use the mismatched right/down pair.
- Update `QuestionResultViewTest.kt` similarly because it has its own chevron implementation.
- Update `ReasoningViewTest.kt` for the brain icon.
6. **Verification**
- Run the smallest relevant JetBrains checks from `packages/kilo-jetbrains/`:
- `./gradlew test --tests "ai.kilocode.client.session.views.ReasoningViewTest" --tests "ai.kilocode.client.session.views.base.AbstractSessionPartViewTest" --tests "ai.kilocode.client.session.views.QuestionResultViewTest" --tests "ai.kilocode.client.session.ui.header.SessionHeaderPanelTest"`
- `./gradlew typecheck`
- If the filtered Gradle test syntax is not accepted by the project, run `./gradlew test` from `packages/kilo-jetbrains/` instead.
## Notes
- No shared upstream `opencode` files are involved; changes stay under `packages/kilo-jetbrains/`.
- A changeset may be needed because this is user-facing JetBrains UI polish; confirm existing changeset policy for the private JetBrains package during implementation.
-1
View File
@@ -1 +0,0 @@
bun run extension
-68
View File
@@ -1,68 +0,0 @@
---
name: gh-issues
description: Use when creating, triaging, or commenting on GitHub issues for the Kilo VS Code extension or JetBrains plugin via `gh`. Covers issue templates, project board assignment, title conventions, and required `gh` scopes.
---
# GitHub Issues
Use this skill whenever you create or manage a GitHub issue with `gh` for either the VS Code extension or the JetBrains plugin.
## Templates
The repo defines issue templates in `.github/ISSUE_TEMPLATE/`. Pick the matching template instead of opening a blank issue:
| Template | When to use |
|---|---|
| `Bug report` (`bug-report.yml`) | Reproducible defects with steps, expected, and actual behavior |
| `Feature Request` (`feature-request.yml`) | New capabilities, enhancements, or behavior changes |
| `Question` (`question.yml`) | Usage or design questions that aren't obviously bugs or feature requests |
Pass the template title to `gh issue create --template`.
## Title Conventions
- Use a plain, descriptive title that reads cleanly as a standalone sentence.
- Do not add platform-specific prefixes such as `[JetBrains]`, `[Jetbrains]`, `[JB]`, `[VS Code]`, `[VSCode]`, or similar. Routing happens through project boards, not the title.
## Project Boards
Every new issue must land on the correct project board:
| Surface | Project | URL |
|---|---|---|
| VS Code extension | `VS Code Extension` | https://github.com/orgs/Kilo-Org/projects/25 |
| JetBrains plugin | `Jetbrains Plugin` | https://github.com/orgs/Kilo-Org/projects/39 |
Pass the project title to `gh issue create --project`.
## Recipes
Create a VS Code extension bug report and add it to the board:
```bash
gh issue create \
--template "Bug report" \
--project "VS Code Extension" \
--title "Sidebar chat fails to render after reload" \
--body "..."
```
Create a JetBrains feature request:
```bash
gh issue create \
--template "Feature Request" \
--project "Jetbrains Plugin" \
--title "Support Kotlin Multiplatform target detection" \
--body "..."
```
## Scope Errors
If `gh` reports a missing scope when assigning a project, refresh the auth token and retry:
```bash
gh auth refresh -s project
```
After the refresh succeeds, re-run the original `gh issue create` command. Do not fall back to creating the issue without the project — the board assignment is required.
@@ -1,121 +0,0 @@
---
name: kilocode-merge-minimizer
description: Use when changing shared upstream-owned files to add Kilo-specific behavior, editing `kilocode_change` markers in shared code, or moving additive behavior out of shared code to reduce upstream merge conflicts. Do not use for changes confined to Kilo-owned paths such as `packages/kilo-vscode/` or `packages/kilo-ui/`.
---
# Kilo Merge Minimizer
Use this skill whenever a normal development task touches shared upstream-owned code and includes Kilo-specific behavior, especially for marker cleanup, extraction work, or `kilocode_change` annotations.
Do not use this skill when all changes are confined to Kilo-owned paths, including `packages/kilo-vscode/`, `packages/kilo-ui/`, and paths with `kilocode` in their name. Those files are not merged from upstream and do not need merge-minimization guidance. If a task also touches shared upstream-owned code, use this skill for the shared portion only.
Do not use this as the primary guide for upstream merge resolution. Upstream merges have their own instructions and should not duplicate that workflow here.
## Goal
Minimize Kilo's long-term diff against upstream OpenCode while preserving behavior.
Prefer this shape for Kilo-specific additions:
1. Shared upstream file contains only a minimal hook, import, call, registration, or config entry.
2. Kilo-specific behavior lives in Kilo-owned code.
3. Unavoidable shared-file changes have narrow `kilocode_change` markers.
4. The annotation checker passes.
For changes to existing upstream behavior, prefer the smallest in-place shared-file diff with narrow markers. Do not move changed upstream logic into Kilo-owned code just to avoid textual conflicts, because that can create harder semantic merge conflicts.
## Core Rules
- Use `script/check-opencode-annotations.ts` as the source of truth for current shared scopes and exempt paths.
- Use `script/upstream/fix-kilocode-markers.ts` for stale or broad markers, inspecting `--dry-run` output before applying changes.
- Treat upstream-owned files as shared unless the checker or repo ownership rules exempt them.
- Put Kilo-owned UI, CLI, runtime logic, and tests in Kilo-owned paths where practical.
- Avoid adding Kilo business logic directly to shared files.
- Keep shared-file edits as close as possible to upstream shape.
- Do not change shared files unless the change is required for Kilo functionality, fixes a Kilo bug, or is a minimal targeted upstream-quality fix.
- Do not create a large Kilo-only fork for a general upstream-quality improvement. Prefer a minimal targeted fix, or leave the broader change for upstream.
- Do not duplicate upstream logic unless there is a concrete reason. If duplication is unavoidable, isolate the Kilo delta and keep the upstream dependency obvious.
## Shared File Structure
- Do not refactor, rename, split files, or extract helpers in shared files just to improve readability or make Kilo extraction cleaner.
- Avoid structural changes that make upstream behavior harder to compare or hide semantic dependency on upstream code.
## Shared File Style
- Preserve upstream formatting and import style in shared files, even when it differs from Kilo style.
- Put Kilo-only imports on separate marked lines instead of reorganizing upstream imports.
## Decision Rules
Extract Kilo logic when:
- The change is an additive Kilo feature or integration, not a modification of existing upstream behavior.
- The shared-file change has meaningful Kilo-owned behavior, not just a tiny condition, import, registration, or field.
- The code has loops, branching, error handling, async workflows, storage access, network calls, UI rendering, or telemetry.
- The shared file can become a small orchestrator that calls Kilo helpers.
- The Kilo code is independent enough that extraction will not hide future upstream fixes or behavior changes.
Keep the change inline when:
- The Kilo delta is a single field, import, call, simple condition, or small registry entry.
- Extraction would reshape upstream code more than the Kilo change itself.
- The change modifies an upstream algorithm, ordering, heuristic, control flow, or bug fix.
- Extraction would duplicate upstream logic or hide semantic dependency on upstream behavior.
- The Kilo helper closes over upstream-local state. Keep closure-scoped helpers inline and contiguous in one narrow marker block.
- The shared file owns the only route table, enum, schema, switch, or registry where the hook must exist.
- The change restores upstream shape or removes a stale Kilo divergence.
Always preserve upstream behavior order unless the Kilo behavior change is intentional and tested.
## Marker Rules
- Mark only Kilo-specific diff lines in shared upstream files.
- Prefer inline markers for single-line changes: `const value = 42 // kilocode_change`.
- Use block markers only for adjacent Kilo-specific lines:
```ts
// kilocode_change start
registerKiloFeature(app)
// kilocode_change end
```
- Use the file's native comment style, including JSX block comments inside JSX and `#` comments for YAML, TOML, and shell.
- Do not add markers in checker-exempt Kilo-owned paths.
- Remove stale markers when upstream already contains the behavior or when touching Kilo-owned files that still have old markers.
- Use `// kilocode_change - new file` only for unavoidable new Kilo-specific files inside shared upstream paths.
## Tests
- Put Kilo-specific CLI/runtime tests in Kilo-owned test paths.
- Move tests out of shared upstream test paths when the behavior under test is Kilo-specific.
- Tests should cover the real failing path, not private or unstable APIs chosen only for convenience.
- Do not add skip gates for required regression coverage.
## Verification
After editing shared files or marker comments, run:
```bash
bun run script/check-opencode-annotations.ts
```
If the PR uses a non-default comparison base, pass the correct base ref:
```bash
bun run script/check-opencode-annotations.ts --base <base-ref>
```
For stale or broad markers in one shared file, inspect the dry run before applying:
```bash
bun run script/upstream/fix-kilocode-markers.ts <repo-relative-file> --dry-run
```
Before finishing, confirm:
- Shared files contain minimal integration points only.
- Kilo logic and tests live in Kilo-owned paths where practical.
- Markers are narrow.
- Stale markers are removed.
- The annotation checker passed, or the reason it could not run is reported.
-237
View File
@@ -1,237 +0,0 @@
---
name: release-jetbrains
description: Use when releasing the Kilo JetBrains plugin -- resolve a version ("next rc" or explicit), run the prepare workflow, edit and commit a filtered human-readable changelog on the release PR, then watch publish to completion.
---
# JetBrains Release
Use this skill when releasing the Kilo JetBrains plugin.
This skill drives the existing JetBrains release workflows. It must not move, delete, or recreate JetBrains release tags. It must always confirm the resolved version with the user before dispatching the prepare workflow because the prepare workflow creates an immutable `jetbrains/v<version>` tag.
## Preconditions
- Run from the repository root.
- `gh` must be authenticated for `Kilo-Org/kilocode` with permission to dispatch workflows, read PRs, and write contents. Merge permission is only required if the user asks the skill to merge the release PR automatically.
- Check auth with `gh auth status`. For GitHub CLI OAuth, refresh common release scopes with `gh auth refresh -s repo -s workflow`; `repo` covers private-repo contents and PR operations, and `workflow` allows workflow dispatch. If using a fine-grained token instead, grant repository permissions for Actions read/write, Contents read/write, and Pull requests read/write. Merging still requires normal repository collaborator permission or a token/user allowed by branch protection.
- Reference `packages/kilo-jetbrains/RELEASING.md` for manual recovery rules.
- Do not locally check out the generated release branch. The helper scripts update the release branch through GitHub to avoid disturbing the current worktree.
## Version Resolution
Resolve the user's version request:
```bash
bun .kilo/skills/release-jetbrains/script/resolve-version.ts --spec "next rc"
```
Accepted specs:
| Spec | Meaning |
|---|---|
| `next rc` | If the latest JetBrains tag is an RC, increment its `rc.n`; otherwise start the next patch RC at `rc.1`. |
| `next stable` | If the latest JetBrains tag is an RC, use its base version; otherwise use the next patch stable. |
| `x.y.z-rc.n` | Explicit RC release. |
| `x.y.z` | Explicit stable release. |
Show the resolved `version`, `kind`, and default `fromTagDefault` to the user.
## CLI Pin Verification
Before dispatching prepare, verify the JetBrains plugin is pinned to the intended Kilo Core release. The plugin downloads the CLI version from `packages/kilo-jetbrains/package.json`, not from the JetBrains plugin version. Prepare tags `origin/main`, so the authoritative pin is the value on `origin/main`, not a local edit.
Run the pin preflight:
```bash
bun .kilo/skills/release-jetbrains/script/check-pin.ts
```
The script prints:
| Field | Meaning |
|---|---|
| `pinMain` | CLI version that `origin/main` will lock into the release tag. |
| `pinLocal` | CLI version in the current worktree, useful for catching stale local checkouts. |
| `latestCli` | Latest stable `v*` Kilo CLI GitHub release. |
| `prevJetbrainsCli` | CLI pin used by the latest `jetbrains/v*` release tag, for reviewing the jump. |
| `pinnedMain` / `pinnedLocal` | Whether `kilo.cli.pinned=true`; `false` means repo CLI dev mode. |
| `assetsOk` / `missingAssets` | Whether the pinned CLI release has every runtime asset. |
| `drift` | `up-to-date`, `behind`, `worktree-behind-main`, `repo-mode-on-main`, `repo-mode-local`, or `assets-missing`. |
Interpretation:
| Drift | Action |
|---|---|
| `up-to-date` | Continue after user confirmation. |
| `behind` | Stop and show `pinMain`, `latestCli`, and `prevJetbrainsCli`; ask whether to cancel, bump + test, or proceed anyway. |
| `worktree-behind-main` | Explain that prepare tags `origin/main`; refresh the worktree or rely on `pinMain` in the confirmation. |
| `repo-mode-on-main` | Stop. `kilo.cli.pinned=false` is dev-only and must be reset to `true` on `main` before release. |
| `repo-mode-local` | Stop or reset local `kilo.cli.pinned=true`; release checks should run from a releasable local state. |
| `assets-missing` | Stop. The pinned CLI release is incomplete and would fail runtime download. |
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, `pinMain`, `latestCli`, `prevJetbrainsCli`, and `assetsOk` to the user, then ask for confirmation before continuing. If the user wants a different CLI pin, use the bump workflow below and do not dispatch prepare until the bump is merged to `main`.
## Bump the CLI Pin
Use this only when the user wants to test or release with a different CLI than `origin/main` currently pins. The helper refuses versions whose GitHub release or runtime assets are missing.
Local test edit only:
```bash
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
# or
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1
```
Then test from `packages/kilo-jetbrains/`:
```bash
./gradlew typecheck
./gradlew test
```
If the user confirms the tested pin should be released, open or update a pin bump PR to `main`:
```bash
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr
# or
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1 --pr
```
After that PR merges to `main`, re-run `resolve-version.ts`, re-run `check-pin.ts`, confirm `drift=up-to-date`, then dispatch prepare. Do not dispatch prepare from a local-only pin edit; the prepare workflow tags `origin/main`.
## Prepare Workflow
After confirmation, dispatch and watch the prepare workflow:
```bash
bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind rc --version 7.0.1-rc.7
```
Pass a generous Bash timeout, such as `1800000` ms, because the script blocks on `gh run watch --exit-status`. If the shell times out but the workflow is still running, re-attach with:
```bash
bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind rc --version 7.0.1-rc.7 --run-id <run-id>
```
The script prints `prNumber`, `prUrl`, `runUrl`, and `branch` on success. Immediately show the `prUrl` to the user so they can open the release PR without asking for it later.
## Changelog Draft
Create a changelog draft after the prepare PR exists:
1. Read the PR body with `gh pr view <pr> --json body`.
2. Extract `JetBrains-From-Tag`, `JetBrains-Tag`, and `## Generated Notes`.
3. Fetch the release range tags if they are missing locally:
```bash
git fetch origin refs/tags/<from-tag>:refs/tags/<from-tag> refs/tags/<tag>:refs/tags/<tag>
```
4. Use the release range and path filter as the primary relevance signal:
```bash
git log --oneline <from-tag>..<tag> -- packages/opencode packages/kilo-jetbrains
```
Keep JetBrains and CLI/runtime changes. Drop unrelated VS Code, docs, gateway, telemetry, i18n, desktop, and webview-only changes unless they affect the CLI bundled into the JetBrains plugin.
Rewrite terse commit or PR titles into user-facing bullets grouped under `### Added`, `### Fixed`, and `### Changed`. Keep the exact generated header format:
```markdown
## [<version>] - <date>
```
Write the editable draft to:
```text
packages/kilo-jetbrains/build/release/<version>-changelog.md
```
Include source context in an HTML comment so it is easy to edit but not shipped:
```markdown
<!-- CONTEXT - deleted automatically on commit. Source PRs in range:
- #1234 feat(jetbrains): ... https://github.com/Kilo-Org/kilocode/pull/1234
- #1235 fix(cli): ... https://github.com/Kilo-Org/kilocode/pull/1235
-->
```
Ask the user to edit the file and confirm when done.
## Commit Changelog
After the user confirms the draft is ready, strip the `<!-- CONTEXT ... -->` block into a temporary cleaned file, then commit the cleaned section to the release branch:
```bash
bun .kilo/skills/release-jetbrains/script/update-changelog.ts --version 7.0.1-rc.7 --file /path/to/clean-section.md
```
The script updates `packages/kilo-jetbrains/CHANGELOG.md` on `jetbrains/release/v<version>` through the GitHub contents API and commits with:
```text
docs(jetbrains): edit changelog for v<version>
```
If `update-changelog.ts` fails with `gh: Not Found (HTTP 404)`, verify the release branch and changelog path with:
```bash
gh api "repos/Kilo-Org/kilocode/contents/packages/kilo-jetbrains/CHANGELOG.md?ref=jetbrains/release/v<version>"
```
Then either fix and retry the helper, or perform the equivalent contents API update using `ref` in the query string.
After the changelog commit succeeds, show the release PR URL again and tell the user that the PR needs manual approval and merge before publishing can continue.
## Approve And Publish
Ask the user to approve the release changelog and metadata. Before merging or publishing, verify the PR approval and required checks are green:
```bash
gh pr view <pr> --json mergeStateStatus,reviewDecision,statusCheckRollup
gh pr checks <pr> --watch --interval 10
```
Do not merge or publish while required checks are failing unless the user explicitly gives a maintainer override.
If a required check fails from an apparent flake, rerun only the failed jobs and wait for the run to finish:
```bash
gh run rerun <run-id> --failed
gh run watch <run-id> --exit-status
```
By default, have the user merge the release PR manually in GitHub, then watch the publish workflow:
```bash
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version 7.0.1-rc.7
```
Only merge automatically when the user explicitly asks for it and `gh` has merge permission:
```bash
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version 7.0.1-rc.7 --merge
```
Pass a generous Bash timeout, such as `1800000` ms. If the shell times out, re-attach with:
```bash
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version 7.0.1-rc.7 --run-id <run-id>
```
If `watch-publish.ts --merge` reports that the PR is already merged, or a transient GitHub API `5xx` interrupts publish-run discovery, rerun without `--merge`:
```bash
bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr <number> --version <version>
```
Report the Marketplace channel and GitHub Release URL. RC versions publish to the `eap` channel; stable versions publish to the default Marketplace channel.
## Recovery
- If prepare created the tag but failed before creating a PR, rerun prepare for the same version. The existing workflow reuses the tag if it points to the same commit.
- If a tag points to an unexpected SHA, stop and inspect manually. Do not move or delete release tags casually.
- If prepare tagged an unintended CLI pin, do not move the tag. Land the intended pin on `main`, resolve the next JetBrains version, and create a new release tag.
- If release PR checks fail from an apparent flake, use `gh run rerun <run-id> --failed`, then `gh run watch <run-id> --exit-status` before publishing.
- If publish fails after merge, rerun the failed workflow only if Marketplace did not already accept the version.
- If Marketplace succeeds but GitHub Release upload fails, manually create or edit the GitHub Release for `jetbrains/v<version>` using the reviewed changelog.
@@ -1,89 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
import { latest, missing } from "./pin-common"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun .kilo/skills/release-jetbrains/script/check-pin.ts
Checks the CLI pin that a JetBrains release would lock. Prepare tags origin/main,
so this reads packages/kilo-jetbrains/package.json from origin/main and compares
it with the latest published Kilo CLI release plus the local worktree pin.
Exit codes:
0 Pin is release-ready.
2 Pin drift, repo CLI mode, or missing CLI assets require maintainer review.
`)
process.exit(0)
}
await $`git fetch origin main --tags`.quiet()
const pinMain = JSON.parse(await $`git show origin/main:packages/kilo-jetbrains/package.json`.text()).version as string
const pinLocal = (await Bun.file("packages/kilo-jetbrains/package.json").json()).version as string
const propsMain = await $`git show origin/main:packages/kilo-jetbrains/gradle.properties`.text()
const propsLocal = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
const pinnedMain = pinned(propsMain)
const pinnedLocal = pinned(propsLocal)
const latestCli = await latest(repo)
const prevJetbrainsCli = await previous()
const missingAssets = await missing(repo, pinMain)
const assetsOk = missingAssets.length === 0
const drift = (() => {
if (!pinnedMain) return "repo-mode-on-main"
if (!pinnedLocal) return "repo-mode-local"
if (pinLocal !== pinMain) return "worktree-behind-main"
if (!assetsOk) return "assets-missing"
if (latestCli && semver.lt(pinMain, latestCli)) return "behind"
return "up-to-date"
})()
console.log(JSON.stringify({
pinMain,
pinLocal,
latestCli,
prevJetbrainsCli,
pinnedMain,
pinnedLocal,
assetsOk,
missingAssets,
drift,
}, null, 2))
if (drift !== "up-to-date") process.exit(2)
async function previous() {
const text = await $`git tag --list ${"jetbrains/v*"}`.text()
const tag = text
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean)
.map((tag) => ({ tag, version: tag.replace(/^jetbrains\/v/, "") }))
.filter((item) => semver.valid(item.version))
.sort((a, b) => semver.rcompare(a.version, b.version))[0]?.tag
if (!tag) return null
const res = await $`git show ${tag}:packages/kilo-jetbrains/package.json`.nothrow().text()
if (!res.trim()) return null
return JSON.parse(res).version as string
}
function pinned(text: string) {
const value = text.split(/\r?\n/).flatMap((line) => {
const [key, value] = line.split("=", 2)
if (key.trim() !== "kilo.cli.pinned") return []
return [value?.trim().toLowerCase()]
})[0]
return value == null || value === "true"
}
@@ -1,75 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { parseArgs } from "util"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const workflow = "prepare-jetbrains-release.yml"
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
kind: { type: "string" },
version: { type: "string" },
"from-tag": { type: "string" },
"run-id": { type: "string" },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(
`Usage: bun .kilo/skills/release-jetbrains/script/dispatch-prepare.ts --kind <rc|stable> --version <version> [--from-tag <tag>] [--run-id <id>]`,
)
process.exit(0)
}
const kind = values.kind
const ver = values.version
const branch = `jetbrains/release/v${ver}`
if (kind !== "rc" && kind !== "stable") throw new Error("--kind must be rc or stable")
if (!ver) throw new Error("--version is required")
const id = values["run-id"] ?? (await dispatch())
const url = `https://github.com/${repo}/actions/runs/${id}`
console.log(`prepareRunId=${id}`)
console.log(`runUrl=${url}`)
await $`gh run watch ${id} --repo ${repo} --exit-status`
const pr = (await $`gh pr view ${branch} --repo ${repo} --json number,url`.json()) as { number: number; url: string }
console.log(
JSON.stringify(
{
prNumber: pr.number,
prUrl: pr.url,
runUrl: url,
branch,
},
null,
2,
),
)
async function dispatch() {
const before = new Set((await runs()).map((run) => run.databaseId))
const args = ["workflow", "run", workflow, "--repo", repo, "-f", `kind=${kind}`, "-f", `version=${ver}`]
if (values["from-tag"]) args.push("-f", `from_tag=${values["from-tag"]}`)
await $`gh ${args}`
for (const _ of Array.from({ length: 60 })) {
const run = (await runs()).find((item) => !before.has(item.databaseId))
if (run) return String(run.databaseId)
await Bun.sleep(1000)
}
throw new Error(`No new ${workflow} run appeared after dispatch`)
}
async function runs() {
return (await $`gh run list --repo ${repo} --workflow ${workflow} --event workflow_dispatch --json databaseId,createdAt,status --limit 100`.json()) as {
databaseId: number
createdAt: string
status: string
}[]
}
@@ -1,30 +0,0 @@
import { $ } from "bun"
import semver from "semver"
export const assets = [
"kilo-darwin-arm64.zip",
"kilo-darwin-x64.zip",
"kilo-linux-arm64.tar.gz",
"kilo-linux-x64.tar.gz",
"kilo-windows-arm64.zip",
"kilo-windows-x64.zip",
]
export async function latest(repo: string) {
const list = (await $`gh release list --repo ${repo} --limit 100 --json tagName,isDraft,isPrerelease`.json()) as {
tagName: string
isDraft: boolean
isPrerelease: boolean
}[]
return list
.filter((item) => /^v\d+\.\d+\.\d+$/.test(item.tagName) && !item.isDraft && !item.isPrerelease)
.map((item) => item.tagName.slice(1))
.sort(semver.rcompare)[0] ?? null
}
export async function missing(repo: string, version: string) {
const res = await $`gh release view ${`v${version}`} --repo ${repo} --json assets --jq ${".assets[].name"}`.quiet().nothrow()
if (res.exitCode !== 0) return assets
const names = res.stdout.toString().split(/\r?\n/).map((item) => item.trim()).filter(Boolean)
return assets.filter((item) => !names.includes(item))
}
@@ -1,109 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
spec: { type: "string" },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(
`Usage: bun .kilo/skills/release-jetbrains/script/resolve-version.ts --spec <next rc|next stable|version>`,
)
process.exit(0)
}
const spec = values.spec?.trim().toLowerCase()
if (!spec) throw new Error("--spec is required")
await $`git fetch origin main --tags`.quiet()
const tags = await list()
const hit = explicit(spec) ?? next(spec, tags)
const from = base(hit.version, hit.kind, tags)
console.log(
JSON.stringify(
{
version: hit.version,
kind: hit.kind,
fromTagDefault: from,
},
null,
2,
),
)
type Kind = "rc" | "stable"
type Tag = { tag: string; version: string }
function explicit(spec: string) {
if (/^\d+\.\d+\.\d+-rc\.\d+$/.test(spec)) return { version: spec, kind: "rc" as Kind }
if (/^\d+\.\d+\.\d+$/.test(spec)) return { version: spec, kind: "stable" as Kind }
return undefined
}
function next(spec: string, tags: Tag[]) {
const latest = [...tags].sort((a, b) => semver.rcompare(a.version, b.version))[0]
if (!latest) throw new Error("No JetBrains release tags found; pass an explicit version")
const parsed = semver.parse(latest.version)
if (!parsed) throw new Error(`Invalid latest JetBrains tag: ${latest.tag}`)
if (spec === "next rc") return rc(parsed)
if (spec === "next stable") return stable(parsed)
throw new Error("--spec must be 'next rc', 'next stable', x.y.z-rc.n, or x.y.z")
}
function rc(ver: semver.SemVer) {
const pre = ver.prerelease
if (pre[0] === "rc" && typeof pre[1] === "number") {
return { version: `${ver.major}.${ver.minor}.${ver.patch}-rc.${pre[1] + 1}`, kind: "rc" as Kind }
}
return { version: `${ver.major}.${ver.minor}.${ver.patch + 1}-rc.1`, kind: "rc" as Kind }
}
function stable(ver: semver.SemVer) {
if (ver.prerelease.length) return { version: `${ver.major}.${ver.minor}.${ver.patch}`, kind: "stable" as Kind }
return { version: `${ver.major}.${ver.minor}.${ver.patch + 1}`, kind: "stable" as Kind }
}
function base(ver: string, kind: Kind, tags: Tag[]) {
const want = semver.parse(ver)
if (!want) throw new Error(`Invalid semver: ${ver}`)
const prior = tags
.filter((item) => !semver.prerelease(item.version) && semver.lt(item.version, ver))
.sort((a, b) => semver.rcompare(a.version, b.version))
if (kind === "stable") {
const hit = prior[0]
if (!hit) return null
return hit.tag
}
const prerelease = tags
.filter((item) => {
const parsed = semver.parse(item.version)
if (!parsed) return false
if (parsed.major !== want.major || parsed.minor !== want.minor || parsed.patch !== want.patch) return false
return Boolean(semver.prerelease(item.version)) && semver.lt(item.version, ver)
})
.sort((a, b) => semver.rcompare(a.version, b.version))
return (prerelease[0] ?? prior[0])?.tag ?? null
}
async function list() {
const text = await $`git tag --list ${"jetbrains/v*"}`.text()
return text
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean)
.map((tag) => ({ tag, version: tag.replace(/^jetbrains\/v/, "") }))
.filter((item) => semver.valid(item.version))
}
@@ -1,123 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
import { latest, missing } from "./pin-common"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const file = "packages/kilo-jetbrains/package.json"
const label = "jetbrains-cli-pin-bump"
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
version: { type: "string" },
latest: { type: "boolean", default: false },
pr: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun .kilo/skills/release-jetbrains/script/set-pin.ts (--latest | --version <x.y.z>) [--pr]
Without --pr, rewrites ${file} in the local worktree so you can test a CLI pin.
With --pr, opens or updates a PR against main using the GitHub API; prepare tags
origin/main, so the pin bump must merge there before a JetBrains release starts.
Examples:
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest
bun .kilo/skills/release-jetbrains/script/set-pin.ts --version 7.4.1
bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest --pr
`)
process.exit(0)
}
if (values.latest && values.version) throw new Error("Pass either --latest or --version, not both")
const version = values.latest ? await latest(repo) : values.version?.replace(/^v/, "")
if (!version || !semver.valid(version) || semver.prerelease(version)) {
throw new Error("Pass a stable CLI version with --version x.y.z or use --latest")
}
const miss = await missing(repo, version)
if (miss.length > 0) {
throw new Error(`CLI release v${version} is missing required assets: ${miss.join(", ")}`)
}
if (values.pr) {
await pr(version)
process.exit(0)
}
const pkg = await Bun.file(file).json()
const previous = pkg.version as string
pkg.version = version
await Bun.write(file, `${JSON.stringify(pkg, null, 2)}\n`)
if (previous === version) {
console.log(`${file} already pins CLI v${version}`)
} else {
console.log(`Pinned JetBrains CLI ${previous} -> ${version} in ${file}`)
}
console.log("Test locally with: cd packages/kilo-jetbrains && ./gradlew typecheck && ./gradlew test")
console.log("When satisfied, run this script again with --pr so the bump lands on main before prepare tags it.")
async function pr(version: string) {
await $`git fetch origin main`.quiet()
const branch = `chore/jetbrains-cli-pin-v${version}`
const main = (await $`git rev-parse origin/main`.text()).trim()
const text = await $`git show origin/main:${file}`.text()
const pkg = JSON.parse(text)
const previous = pkg.version as string
if (previous === version) {
console.log(`origin/main already pins CLI v${version}; no PR needed.`)
return
}
pkg.version = version
const body = `${JSON.stringify(pkg, null, 2)}\n`
await ensure(branch, main)
const current = (await $`gh api ${`repos/${repo}/contents/${file}?ref=${branch}`}`.json()) as { sha: string }
await $`gh api --method PUT ${`repos/${repo}/contents/${file}`} -f message=${`chore(jetbrains): bump CLI pin to v${version}`} -f content=${Buffer.from(body).toString("base64")} -f branch=${branch} -f sha=${current.sha}`.quiet()
const title = `chore(jetbrains): bump CLI pin to v${version}`
const desc = [
`Bumps the JetBrains CLI pin from v${previous} to v${version}.`,
"",
"Prepare tags origin/main, so this PR must merge before dispatching a JetBrains release that should lock this CLI.",
"",
"After merging, re-run:",
"",
"```bash",
"bun .kilo/skills/release-jetbrains/script/check-pin.ts",
"```",
].join("\n")
const view = await $`gh pr view ${branch} --repo ${repo} --json url --jq .url`.quiet().nothrow()
if (view.exitCode === 0 && view.stdout.toString().trim()) {
await $`gh pr edit ${branch} --repo ${repo} --title ${title} --body ${desc}`
await tag(branch)
console.log(view.stdout.toString().trim())
return
}
const url = await $`gh pr create --repo ${repo} --base main --head ${branch} --title ${title} --body ${desc}`.text()
await tag(branch)
console.log(url.trim())
}
async function ensure(branch: string, sha: string) {
const ref = `repos/${repo}/git/refs/heads/${branch}`
const exists = await $`gh api ${ref}`.nothrow().quiet()
if (exists.exitCode === 0) {
await $`gh api --method PATCH ${ref} -f sha=${sha} -F force=true`.quiet()
return
}
await $`gh api --method POST ${`repos/${repo}/git/refs`} -f ref=${`refs/heads/${branch}`} -f sha=${sha}`.quiet()
}
async function tag(branch: string) {
await $`gh label create ${label} --repo ${repo} --color 1D76DB --description ${"JetBrains pinned CLI version bump"}`.quiet().nothrow()
const result = await $`gh pr edit ${branch} --repo ${repo} --add-label ${label}`.quiet().nothrow()
if (result.exitCode !== 0) {
console.warn(`Warning: failed to add ${label} label to ${branch}`)
}
}
@@ -1,84 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { parseArgs } from "util"
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
const path = "packages/kilo-jetbrains/CHANGELOG.md"
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
version: { type: "string" },
file: { type: "string" },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(
`Usage: bun .kilo/skills/release-jetbrains/script/update-changelog.ts --version <version> --file <section.md>`,
)
process.exit(0)
}
const ver = values.version
const file = values.file
if (!ver) throw new Error("--version is required")
if (!file) throw new Error("--file is required")
const branch = `jetbrains/release/v${ver}`
const section = strip((await Bun.file(file).text()).trim())
validate(section, ver)
const current = (await $`gh api ${`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`}`.json()) as {
content: string
encoding: string
sha: string
}
if (current.encoding !== "base64") throw new Error(`Unexpected content encoding: ${current.encoding}`)
const text = Buffer.from(current.content.replace(/\s/g, ""), "base64").toString("utf8")
const pattern = regex(ver)
if (!pattern.test(text)) throw new Error(`${path} is missing a section for ${ver} on ${branch}`)
const next =
text
.replace(pattern, `\n${section}\n`)
.replace(/\n{3,}/g, "\n\n")
.trimEnd() + "\n"
if (next === text) {
console.log("CHANGELOG.md already contains the provided section")
process.exit(0)
}
const body = {
message: `docs(jetbrains): edit changelog for v${ver}`,
content: Buffer.from(next).toString("base64"),
sha: current.sha,
branch,
}
const tmp = await temp(body)
await $`gh api ${`repos/${repo}/contents/${path}`} --method PUT --input ${tmp}`
console.log(`Committed changelog update to ${branch}`)
function strip(text: string) {
return text.replace(/<!--\s*CONTEXT[\s\S]*?-->/g, "").trim()
}
function validate(section: string, ver: string) {
if (!section.startsWith(`## [${ver}]`)) throw new Error(`Section must start with ## [${ver}]`)
if (!/^### (Added|Fixed|Changed)$/m.test(section)) {
throw new Error("Section must contain at least one Added, Fixed, or Changed heading")
}
}
function regex(ver: string) {
const safe = ver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
return new RegExp(`\n?## \\[${safe}\\][\\s\\S]*?(?=\n## \\[|$)`)
}
async function temp(body: object) {
const file = `/tmp/kilo-jetbrains-changelog-${Date.now()}.json`
await Bun.write(file, JSON.stringify(body))
return file
}

Some files were not shown because too many files have changed in this diff Show More