Merge branch 'main' into visual-gruyere

This commit is contained in:
Kirill Kalishev
2026-05-29 16:27:56 -04:00
committed by GitHub
68 changed files with 666 additions and 197 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Use Kilo session share links when sharing conversations from the CLI.
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Reduce lag and gray placeholders in the diff and Changes views by enabling worker-backed highlighting and rendering patch-backed review hunks without rebuilding full source files.
+10 -6
View File
@@ -46,8 +46,10 @@ jobs:
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Save reviewed changelog
run: cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md"
- 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
@@ -55,8 +57,10 @@ jobs:
fetch-depth: 0
ref: ${{ steps.release.outputs.tag }}
- name: Restore reviewed changelog
run: cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md
- 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
@@ -107,7 +111,7 @@ jobs:
- name: Verify plugin
working-directory: packages/kilo-jetbrains
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.version="$VERSION" -Pkilo.channel="$CHANNEL"
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
VERSION: ${{ steps.release.outputs.version }}
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
@@ -125,7 +129,7 @@ jobs:
- name: Publish to JetBrains Marketplace
working-directory: packages/kilo-jetbrains
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.version="$VERSION" -Pkilo.channel="$CHANNEL"
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
VERSION: ${{ steps.release.outputs.version }}
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
+15 -15
View File
@@ -33,7 +33,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.3.17",
"version": "7.3.18",
"bin": {
"opencode": "./bin/opencode",
},
@@ -68,7 +68,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -98,7 +98,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -134,7 +134,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.3.17",
"version": "7.3.18",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -144,7 +144,7 @@
},
"packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31",
@@ -176,11 +176,11 @@
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
"version": "7.3.17",
"version": "7.3.18",
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0",
@@ -194,7 +194,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11",
@@ -231,7 +231,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*",
@@ -296,7 +296,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.3.17",
"version": "7.3.18",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -456,7 +456,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -481,7 +481,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"semver": "^7.6.3",
},
@@ -492,7 +492,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -507,7 +507,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.3.17",
"version": "7.3.18",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -530,7 +530,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-d+/jrUA/Di+8N2Hn1xPN4anKRQb9COTfTgnnf+2o5ew=",
"aarch64-linux": "sha256-1tv43AwnfWGvuRtmM2Y08Zua7yRtemonjFkVlBkl9O8=",
"aarch64-darwin": "sha256-D6FwbOxmYZLCPf2TCY02Qyu2Tu2D2TMzgAA0UroOWgA=",
"x86_64-darwin": "sha256-WVw5WT8BqnAsieFj5ho+fLM2tmppJ2FyfEHm7BlD2y0="
"x86_64-linux": "sha256-KqxasDpdyLQBZC+N85z4HFXLn/15D9qqDyEqgvWTY7c=",
"aarch64-linux": "sha256-YEBF+S2+8imYWO4PfbCY5AOA5uIqAZY80GO6HK8k/sM=",
"aarch64-darwin": "sha256-CxE5cdMcvcAENZLCilCU6Ndx+wiZVbFxtx0UNySdNDA=",
"x86_64-darwin": "sha256-Z9eU77dkavAGAM0AlcnKXCXrrFuRyLnYwM/F9nrkwrk="
}
}
+1 -1
View File
@@ -150,6 +150,6 @@
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
},
"version": "7.3.17",
"version": "7.3.18",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.17",
"version": "7.3.18",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.3.17"
version = "7.3.18"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
@@ -1,16 +1,46 @@
import React, { useState, useEffect } from "react"
import Link from "next/link"
const TAB_SYNC_EVENT = "kilo-tab-select"
function slugify(label: string) {
return label
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
}
export function TableOfContents({ toc }) {
const items = toc.filter((item) => item.id && (item.level === 2 || item.level === 3))
const [tab, setTab] = useState("")
const [activeHash, setActiveHash] = useState("")
const items = toc.filter((item) => {
if (!item.id || (item.level !== 2 && item.level !== 3)) return false
return !item.tab || item.tab.slug === tab
})
useEffect(() => {
const updateHash = () => setActiveHash(window.location.hash)
updateHash()
window.addEventListener("hashchange", updateHash)
return () => window.removeEventListener("hashchange", updateHash)
}, [])
const update = () => {
const hash = window.location.hash.slice(1)
const item = toc.find((entry) => entry.id === hash && entry.tab)
setActiveHash(window.location.hash)
setTab(item?.tab?.slug ?? (toc.some((entry) => entry.tab?.slug === hash) ? hash : ""))
}
const sync = (e: Event) => {
const label = (e as CustomEvent<string>).detail
setActiveHash(window.location.hash)
setTab(slugify(label))
}
update()
window.addEventListener("hashchange", update)
window.addEventListener(TAB_SYNC_EVENT, sync)
return () => {
window.removeEventListener("hashchange", update)
window.removeEventListener(TAB_SYNC_EVENT, sync)
}
}, [toc])
if (items.length <= 1) {
return null
@@ -22,14 +52,27 @@ export function TableOfContents({ toc }) {
{items.map((item) => {
const href = `#${item.id}`
const active = activeHash === href
const select = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (!item.tab) return
e.preventDefault()
window.dispatchEvent(new CustomEvent(TAB_SYNC_EVENT, { detail: item.tab.label }))
setActiveHash(href)
setTab(item.tab.slug)
history.pushState(null, "", href)
requestAnimationFrame(() => document.getElementById(item.id)?.scrollIntoView())
}
return (
<li
key={item.title}
key={`${item.tab?.slug ?? "page"}-${item.id}`}
className={[active ? "active" : undefined, item.level === 3 ? "padded" : undefined]
.filter(Boolean)
.join(" ")}
>
<Link href={href}>{item.title}</Link>
<Link href={href} onClick={select}>
{item.title}
</Link>
</li>
)
})}
+35 -4
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, Children, isValidElement, ReactNode, ReactElement } from "react"
import React, { useState, useEffect, useRef, Children, isValidElement, ReactNode, ReactElement } from "react"
const TAB_SYNC_EVENT = "kilo-tab-select"
@@ -18,6 +18,17 @@ function slugify(label: string) {
.replace(/[^a-z0-9-]/g, "")
}
function contains(node: ReactNode, hash: string): boolean {
return Children.toArray(node).some((child) => {
if (!isValidElement(child)) return false
const props = child.props as { children?: ReactNode; id?: string }
if (props.id === hash) return true
return contains(props.children, hash)
})
}
export function Tab({ children }: TabProps) {
return <>{children}</>
}
@@ -32,19 +43,31 @@ export function Tabs({ children }: TabsProps) {
const hash = window.location.hash.slice(1)
if (!hash) return 0
const found = tabs.findIndex((tab) => slugify(tab.props.label) === hash)
return found >= 0 ? found : 0
if (found >= 0) return found
const child = tabs.findIndex((tab) => contains(tab.props.children, hash))
return child >= 0 ? child : 0
}
const [activeIndex, setActiveIndex] = useState(0)
const scroll = useRef(false)
useEffect(() => {
setActiveIndex(indexFromHash())
const onHashChange = () => setActiveIndex(indexFromHash())
const activate = () => {
const hash = window.location.hash.slice(1)
const index = indexFromHash()
scroll.current = Boolean(hash && contains(tabs[index]?.props.children, hash))
setActiveIndex(index)
}
activate()
const onHashChange = activate
window.addEventListener("hashchange", onHashChange)
const onSync = (e: Event) => {
const label = (e as CustomEvent<string>).detail
const found = tabs.findIndex((tab) => tab.props.label === label)
scroll.current = false
if (found >= 0) setActiveIndex(found)
}
window.addEventListener(TAB_SYNC_EVENT, onSync)
@@ -55,6 +78,14 @@ export function Tabs({ children }: TabsProps) {
}
}, [])
useEffect(() => {
const hash = window.location.hash.slice(1)
if (!hash || !scroll.current) return
scroll.current = false
requestAnimationFrame(() => document.getElementById(hash)?.scrollIntoView())
}, [activeIndex])
const selectTab = (index: number) => {
setActiveIndex(index)
const label = tabs[index].props.label
@@ -11,7 +11,10 @@ Before installing the Kilo Code plugin, ensure you have:
2. **Node.js:**
- Download LTS version from [https://nodejs.org/](https://nodejs.org/)
- Required for the extension's backend services
{% callout type="tip" %}
Try the [v7 Early Access Program plugin](#jetbrains-early-access) for a JetBrains-native experience that does not require Node.js or manual API key configuration.
{% /callout %}
### Install directly
@@ -28,12 +31,12 @@ Before installing the Kilo Code plugin, ensure you have:
4. Search for "Kilo Code"
5. Click **Install** and restart your IDE
### Try the v7 Early Access Program plugin
### Try the v7 Early Access Program plugin {% #jetbrains-early-access %}
The v7 EAP plugin is available for users who want to try the newest JetBrains experience before it reaches the default Marketplace channel. It uses a JetBrains-native UI and is designed to work well with JetBrains remote development.
{% callout type="info" %}
EAP builds may update more frequently than the default Marketplace release. Share feedback in the JetBrains channel on the [Kilo Discord](https://kilo.ai/discord).
The v7 EAP plugin supports JetBrains IDEs with 261+ builds. EAP builds may update more frequently than the default Marketplace release. Share feedback in the JetBrains channel on the [Kilo Discord](https://kilo.ai/discord).
{% /callout %}
To install the EAP build and receive updates:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.3.17",
"version": "7.3.18",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
+19 -4
View File
@@ -28,27 +28,42 @@ import type { MarkdocNextJsPageProps } from "@markdoc/next.js"
const TITLE = "Kilo Code Documentation"
const DESCRIPTION = "Build, ship, and iterate faster with the most popular open source coding agent."
function collectHeadings(node, sections = []) {
function slugify(label) {
return label
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
}
function collectHeadings(node, sections = [], tab = undefined) {
if (node) {
// Skip headings inside tabs - they're not always visible
if (node.name === "Tabs") {
for (const child of node.children || []) {
const label = child.attributes?.label
collectHeadings(
child,
sections,
typeof label === "string" ? { label, slug: slugify(label) } : undefined,
)
}
return sections
}
if (node.name === "Heading") {
const title = node.children[0]
const title = typeof node.children[0] === "string" ? node.children[0].trim() : node.children[0]
if (typeof title === "string") {
sections.push({
...node.attributes,
title,
tab,
})
}
}
if (node.children) {
for (const child of node.children) {
collectHeadings(child, sections)
collectHeadings(child, sections, tab)
}
}
}
@@ -25,49 +25,6 @@ The current Kilo Code extension is built on the [Kilo CLI](https://github.com/Ki
The "pre-release" label is a VS Code Marketplace distribution channel — the extension is stable and recommended for all users.
{% /callout %}
{% /tab %}
{% tab label="CLI" %}
## Command Line Interface
{% partial file="install-cli.md" /%}
{% /tab %}
{% tab label="VS Code (Legacy)" %}
## VS Code Legacy Extension
The legacy extension is the previous version of Kilo Code for VS Code. It is still available but is no longer actively developed. We recommend installing the current extension (see the **VS Code** tab).
To install or switch back to the legacy version:
1. Open VS Code
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
3. Search for "Kilo Code"
4. Click the dropdown arrow next to **Install** and select **Switch to Release Version**
{% /tab %}
{% tab label="JetBrains" %}
## JetBrains IDEs
{% partial file="install-jetbrains.md" /%}
{% /tab %}
{% tab label="Slack" %}
## Slack Integration
{% partial file="install-slack.md" /%}
{% /tab %}
{% tab label="Other IDEs" %}
{% partial file="install-other-ides.md" /%}
{% /tab %}
{% /tabs %}
## Manual Installations
### Open VSX Registry
@@ -136,6 +93,49 @@ If you plan to remain on that version for a while, you may also want to temporar
3. Add: `C:\Windows\System32\WindowsPowerShell\v1.0\`
4. Click **OK** and restart VS Code
{% /tab %}
{% tab label="CLI" %}
## Command Line Interface
{% partial file="install-cli.md" /%}
{% /tab %}
{% tab label="VS Code (Legacy)" %}
## VS Code Legacy Extension
The legacy extension is the previous version of Kilo Code for VS Code. It is still available but is no longer actively developed. We recommend installing the current extension (see the **VS Code** tab).
To install or switch back to the legacy version:
1. Open VS Code
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
3. Search for "Kilo Code"
4. Click the dropdown arrow next to **Install** and select **Switch to Release Version**
{% /tab %}
{% tab label="JetBrains" %}
## JetBrains IDEs
{% partial file="install-jetbrains.md" /%}
{% /tab %}
{% tab label="Slack" %}
## Slack Integration
{% partial file="install-slack.md" /%}
{% /tab %}
{% tab label="Other IDEs" %}
{% partial file="install-other-ides.md" /%}
{% /tab %}
{% /tabs %}
## Next Steps
After installation, check out these resources to get started:
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
+7 -2
View File
@@ -2,7 +2,12 @@
## [Unreleased]
## [0.0.0-dev]
## [7.0.1-rc.4] - 2026-05-29
### Added
- Initial JetBrains plugin development builds.
- Initial JetBrains plugin release with a native Kilo Code tool window.
- Chat sessions with streamed responses, tool output, reasoning, markdown, todos, and plan follow-ups.
- Native mode/model selection, account sign-in, permission prompts, and question flows.
- Local and cloud session history with search, reopen, rename/delete local sessions, and repository filtering.
- Migration wizard for legacy JetBrains plugin settings and chat history.
- Bundled Kilo CLI runtime for macOS, Linux, and Windows.
+2
View File
@@ -2,6 +2,8 @@
AI coding agent plugin for JetBrains IDEs.
To try the v7 Early Access Program plugin, follow the [JetBrains EAP installation guide](https://kilo.ai/docs/code-with-ai/platforms/jetbrains#jetbrains-early-access).
---
## Set up your environment
+4 -4
View File
@@ -58,12 +58,12 @@ The PR updates:
| File | Purpose |
|---|---|
| `packages/kilo-jetbrains/package.json` | JetBrains plugin package version. |
| `packages/kilo-jetbrains/gradle.properties` | JetBrains plugin version in `kilo.jetbrains.version`. |
| `packages/kilo-jetbrains/CHANGELOG.md` | Release notes packaged into the plugin. |
Review and edit `packages/kilo-jetbrains/CHANGELOG.md` before merging. This changelog entry is rendered into JetBrains `<change-notes>`, so it appears on the Marketplace and inside IntelliJ plugin UI.
Review `packages/kilo-jetbrains/gradle.properties` and edit `packages/kilo-jetbrains/CHANGELOG.md` before merging. This changelog entry is rendered into JetBrains `<change-notes>`, so it appears on the Marketplace and inside IntelliJ plugin UI.
The PR can change release metadata such as `packages/kilo-jetbrains/package.json` and `packages/kilo-jetbrains/CHANGELOG.md`, but it does not change the tagged source code that will be built.
The PR can change release metadata such as `packages/kilo-jetbrains/gradle.properties` and `packages/kilo-jetbrains/CHANGELOG.md`, but it does not change the tagged source code that will be built.
## Merge and Publish
@@ -84,7 +84,7 @@ Publishing behavior:
| `x.y.z-rc.n` | `eap` | Prerelease |
| `x.y.z` | default | Stable release |
The workflow checks out `jetbrains/v<version>` for verification, signing, and Marketplace publishing. It overlays the reviewed `packages/kilo-jetbrains/CHANGELOG.md` from the merged PR before rendering release notes and before `publishPlugin`, so Marketplace metadata and the GitHub Release use the reviewed changelog.
The workflow checks out `jetbrains/v<version>` for verification, signing, and Marketplace publishing. It overlays the reviewed `packages/kilo-jetbrains/gradle.properties` and `packages/kilo-jetbrains/CHANGELOG.md` from the merged PR before rendering release notes and before `publishPlugin`, so the Marketplace plugin version, Marketplace notes, and GitHub Release use the reviewed metadata.
## Installing RC Builds
@@ -542,14 +542,14 @@ class KiloBackendAppService private constructor(
private fun setAppReady(data: AppData) {
warnings = data.warnings
_appState.value = KiloAppState.Ready(data)
if (data.warnings.isNotEmpty()) warnAppWarnings(data.warnings)
_appState.value = KiloAppState.Ready(data)
}
private fun setAppError(message: String, errors: List<LoadError>) {
val state = KiloAppState.Error(message, errors)
_appState.value = state
warnAppError(state)
_appState.value = state
}
private fun warnAppError(state: KiloAppState.Error) {
@@ -291,8 +291,8 @@ class KiloBackendWorkspace(
}
private fun setWorkspaceError(message: String, errors: List<LoadError>) {
_state.value = KiloWorkspaceState.Error(message, errors)
log.warn("Workspace error [$directory]: $message")
_state.value = KiloWorkspaceState.Error(message, errors)
}
private data class FetchResult<T>(val value: T?, val error: LoadError?) {
+5 -4
View File
@@ -42,10 +42,11 @@ fun gitTag(): String? {
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
val override = providers.gradleProperty("kilo.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
val ver = override?.let(::checked) ?: if (release) checked(
gitTag()?.removePrefix("jetbrains/v")
?: error("Missing JetBrains plugin version. Publish builds must run from a jetbrains/v<version> tag."),
) else checked(gitTag()?.removePrefix("jetbrains/v") ?: "0.0.0-dev")
val prop = providers.gradleProperty("kilo.jetbrains.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
val tag = gitTag()?.removePrefix("jetbrains/v")
val ver = override?.let(::checked) ?: prop?.let(::checked) ?: if (release) checked(
tag ?: error("Missing JetBrains plugin version. Publish builds must set kilo.jetbrains.version or run from a jetbrains/v<version> tag."),
) else checked(tag ?: "0.0.0-dev")
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").orNull?.let(::port) ?: fallback()
@@ -1,4 +1,5 @@
kotlin.stdlib.default.dependency=false
kilo.jetbrains.version=7.0.1-rc.4
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
+1 -1
View File
@@ -8,7 +8,7 @@
"test": "./gradlew test",
"test:ci": "bun script/test-ci.ts"
},
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"exports": {
+10 -4
View File
@@ -1,4 +1,4 @@
import { DIFFS_TAG_NAME, FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import { DIFFS_TAG_NAME, FileDiff, processFile, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js"
import { Dynamic, isServer } from "solid-js/web"
@@ -16,11 +16,14 @@ export function Diff<T>(props: SSRDiffProps<T>) {
const [local, others] = splitProps(props, [
"before",
"after",
"patch",
"fileDiff",
"class",
"classList",
"annotations",
"selectedLines",
"commentedLines",
"virtualized",
])
const workerPool = useWorkerPool(props.diffStyle)
@@ -227,7 +230,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
onCleanup(() => monitor.disconnect())
}
const virtualizer = getVirtualizer()
const virtualizer = local.virtualized === false ? undefined : getVirtualizer()
fileDiffInstance = virtualizer
? new VirtualizedFileDiff<T>(
@@ -250,9 +253,12 @@ export function Diff<T>(props: SSRDiffProps<T>) {
)
// @ts-expect-error - fileContainer is private but needed for SSR hydration
fileDiffInstance.fileContainer = fileDiffRef
const patch = "patch" in local && typeof local.patch === "string" ? local.patch : ""
const metadata = local.fileDiff ?? (patch ? processFile(patch, { cacheKey: patch }) : undefined)
fileDiffInstance.hydrate({
oldFile: local.before,
newFile: local.after,
oldFile: metadata ? undefined : local.before,
newFile: metadata ? undefined : local.after,
fileDiff: metadata,
lineAnnotations: local.annotations,
fileContainer: fileDiffRef,
containerWrapper: container,
+39 -5
View File
@@ -1,5 +1,12 @@
import { sampledChecksum } from "@opencode-ai/core/util/encode"
import { FileDiff, type FileDiffOptions, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import {
FileDiff,
type FileDiffMetadata,
type FileDiffOptions,
processFile,
type SelectedLineRange,
VirtualizedFileDiff,
} from "@pierre/diffs"
import { createMediaQuery } from "@solid-primitives/media"
import { createEffect, createMemo, createSignal, on, onCleanup, splitProps, untrack } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
@@ -142,6 +149,7 @@ export function Diff<T>(props: DiffProps<T>) {
let container!: HTMLDivElement
let observer: MutationObserver | undefined
let sharedVirtualizer: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
let parsed: { patch: string; diff: FileDiffMetadata } | undefined
let renderToken = 0
let selectionFrame: number | undefined
let dragFrame: number | undefined
@@ -156,6 +164,7 @@ export function Diff<T>(props: DiffProps<T>) {
const [local, others] = splitProps(props, [
"before",
"after",
"patch",
"fileDiff",
"class",
"classList",
@@ -163,6 +172,7 @@ export function Diff<T>(props: DiffProps<T>) {
"selectedLines",
"commentedLines",
"onRendered",
"virtualized",
])
const mobile = createMediaQuery("(max-width: 640px)")
@@ -178,11 +188,24 @@ export function Diff<T>(props: DiffProps<T>) {
})
const estimate = createMemo(() => {
const value = Math.max(lines(before()), lines(after())) * ESTIMATED_LINE_HEIGHT
// A tracked detail response already carries a hunk-bounded git patch. Base
// placeholder height on that patch instead of the full source file so a
// tiny change in a large file does not reserve a large gray body.
const patch = "patch" in local && typeof local.patch === "string" ? local.patch : ""
const value = (patch ? lines(patch) : Math.max(lines(before()), lines(after()))) * ESTIMATED_LINE_HEIGHT
if (value === 0) return MIN_PLACEHOLDER_HEIGHT
return Math.max(MIN_PLACEHOLDER_HEIGHT, Math.min(value, MAX_PLACEHOLDER_HEIGHT))
})
const patchDiff = () => {
if (!("patch" in local) || typeof local.patch !== "string" || local.patch.length === 0) return
if (parsed?.patch === local.patch) return parsed.diff
const diff = processFile(local.patch, { cacheKey: local.patch })
if (!diff) return
parsed = { patch: local.patch, diff }
return diff
}
const large = createMemo(() => {
return Math.max(before().length, after().length) > 500_000
})
@@ -681,8 +704,19 @@ export function Diff<T>(props: DiffProps<T>) {
const opts = options()
const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
const virtualizer = getVirtualizer()
// Eager (non-virtualized) patch-backed diffs render their visible hunks once
// and never re-render on scroll or height changes, avoiding Pierre's
// re-render-all storms. Full-content or oversized diffs keep virtualizing.
const virtualizer = local.virtualized === false ? undefined : getVirtualizer()
if (local.virtualized === false && sharedVirtualizer) {
sharedVirtualizer.release()
sharedVirtualizer = undefined
}
const annotations = untrack(() => local.annotations)
// Parse hunk-bounded patches only after the deferred visibility gate. This
// preserves quick session switching while avoiding a full before/after diff
// reconstruction for tiny changes inside large source files.
const metadata = local.fileDiff ?? patchDiff()
// Preserve container height during re-render to prevent scroll jumps.
// When Pierre tears down the DOM (innerHTML = ""), the container collapses
@@ -699,9 +733,9 @@ export function Diff<T>(props: DiffProps<T>) {
container.innerHTML = ""
if (local.fileDiff) {
if (metadata) {
instance.render({
fileDiff: local.fileDiff,
fileDiff: metadata,
lineAnnotations: annotations,
containerWrapper: container,
})
+8
View File
@@ -48,6 +48,11 @@ type DiffShared<T> = FileDiffOptions<T> & {
commentedLines?: SelectedLineRange[]
onLineNumberSelectionEnd?: (selection: SelectedLineRange | null) => void
onRendered?: () => void
// When false, render the supplied diff once instead of row-virtualizing it.
// Callers should supply hunk-bounded `fileDiff`/`patch` data for large source
// files so eager rendering does not expand full before/after content.
// Defaults to virtualized.
virtualized?: boolean
class?: string
classList?: ComponentProps<"div">["classList"]
}
@@ -55,6 +60,8 @@ type DiffShared<T> = FileDiffOptions<T> & {
type DiffPair<T> = DiffShared<T> & {
before: FileContents
after: FileContents
/** Unified patch used to parse only rendered hunks instead of full file contents. */
patch?: string
fileDiff?: undefined
}
@@ -62,6 +69,7 @@ type DiffPatch<T> = DiffShared<T> & {
fileDiff: FileDiffMetadata
before?: undefined
after?: undefined
patch?: undefined
}
export type DiffProps<T = {}> = DiffPair<T> | DiffPatch<T>
+7
View File
@@ -181,6 +181,12 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
- **Prefer kilo-ui styles**: Always reuse existing kilo-ui CSS variables, tokens, and component styles instead of writing custom CSS. If a style doesn't exist in kilo-ui yet, add it there and reuse it rather than inlining or duplicating styles in the webview.
- **Icons**: kilo-ui has 75+ custom SVG icons in [`packages/ui/src/components/icon.tsx`](../../packages/ui/src/components/icon.tsx). To list all available icon names: `node -e "const c=require('fs').readFileSync('../../packages/ui/src/components/icon.tsx','utf8');[...c.matchAll(/^\\s{2}[\"']?([\\w-]+)[\"']?:\\s*\x60/gm)].map(m=>m[1]).sort().forEach(n=>console.log(n))"`. Icon names use both hyphenated (`arrow-left`) and bare-word (`brain`, `console`, `providers`) keys.
### Diff Rendering Performance
- Preserve hunk-bounded unified `patch` data through Changes/review detail flows and pass patch-derived `FileDiffMetadata` to Pierre when available. Do not eagerly render Pierre from complete `before`/`after` contents based only on changed-line counts: a tiny patch in a large source file can otherwise parse and render the entire file while the user sees a placeholder.
- Pierre workers can offload highlighted updates, but they do not make an expensive synchronous initial render safe. Keep initial rendering hunk-bounded, and keep patch parsing behind deferred visibility/activation where session-switch responsiveness depends on it.
- When changing diff scheduling, verify both rapid session switching and fast scrolling through a review. Improving one by shifting work into the other is a regression, not an optimization.
## Docs Screenshot Stories
When adding or updating Storybook stories for screenshots used by docs, make the story content match the docs page closely before replacing the docs image. Do not replace screenshots from VSCode Legacy docs tabs or sections.
@@ -191,6 +197,7 @@ Generated screenshot baselines live under `packages/kilo-docs/public/img/screens
- Extension logs: "Extension Host" output channel (not Debug Console)
- Webview logs: Command Palette → "Developer: Open Webview Developer Tools"
- In Chrome/VS Code performance traces, associate CPU `ProfileChunk` events to their `Profile.id` target before attributing work to a thread. `v8:ProfEvntProc` is a profile delivery thread, not evidence that application work ran off the webview main thread.
- All debug output must be prepended with `[Kilo New]` for easy filtering
## Naming Conventions
+57 -33
View File
@@ -56,45 +56,46 @@ const esbuildProblemMatcherPlugin = {
}
/**
* Stub the pierre worker module so the Diff/Code components work without
* web workers in the VS Code webview. The `@pierre/diffs` library handles
* undefined worker pools gracefully (renders without syntax highlighting).
* Route the shared `@opencode-ai/ui/pierre/worker` module (and its relative
* variants) to the Kilo implementation in `webview-ui/pierre-worker.ts`.
*
* We stub the entire worker module rather than just the URL import because
* `new Worker('')` would throw at runtime.
* The upstream module loads Pierre's Shiki worker via a Vite-only
* `?worker&url` import that esbuild can't resolve. The Kilo replacement loads
* the worker from the bundled `dist/shiki-worker.js` asset instead, so syntax
* highlighting runs off the main thread. `@pierre/diffs/worker` (used by that
* replacement) is left alone.
*
* @type {import('esbuild').Plugin}
*/
const pierreWorkerStubPlugin = {
name: "pierre-worker-stub",
const pierreWorkerAliasPlugin = {
name: "pierre-worker-alias",
setup(build) {
// Stub the Vite-specific ?worker&url import
build.onResolve({ filter: /\?worker&url$/ }, (args) => ({
path: args.path,
namespace: "worker-url-stub",
}))
build.onLoad({ filter: /.*/, namespace: "worker-url-stub" }, () => ({
contents: "export default ''",
loader: "js",
}))
// Stub the pierre worker module so getWorkerPool always returns undefined
build.onResolve({ filter: /pierre\/worker$/ }, (args) => {
// Only stub the local UI worker module, not @pierre/diffs/worker
if (args.path.includes("@pierre")) return
return {
path: args.path,
namespace: "pierre-worker-stub",
}
return { path: path.join(__dirname, "webview-ui", "pierre-worker.ts") }
})
},
}
/**
* Resolve the synthetic `kilo-shiki-worker` entry point to Pierre's Shiki worker
* so esbuild can bundle it (and its inlined oniguruma WebAssembly) into a single
* `dist/shiki-worker.js` asset loaded by `webview-ui/pierre-worker.ts`. Switch to
* `worker-portable.js` to drop WebAssembly and use the JS regex engine instead.
*
* @type {import('esbuild').Plugin}
*/
const shikiWorkerEntryPlugin = {
name: "shiki-worker-entry",
setup(build) {
build.onResolve({ filter: /^kilo-shiki-worker$/ }, async () => {
const resolved = await build.resolve("@pierre/diffs/worker/worker.js", {
kind: "import-statement",
resolveDir: __dirname,
})
if (resolved.errors.length > 0) return { errors: resolved.errors }
return { path: resolved.path }
})
build.onLoad({ filter: /.*/, namespace: "pierre-worker-stub" }, () => ({
contents: `
export function getWorkerPool() { return undefined }
export function getWorkerPools() { return { unified: undefined, split: undefined } }
export function workerFactory() { return undefined }
`,
loader: "js",
}))
},
}
@@ -157,7 +158,7 @@ function createBrowserWebviewContext(entryPoint, outfile) {
},
plugins: [
solidDedupePlugin,
pierreWorkerStubPlugin,
pierreWorkerAliasPlugin,
svgSpritePlugin,
cssPackageResolvePlugin,
solidPlugin(),
@@ -166,6 +167,23 @@ function createBrowserWebviewContext(entryPoint, outfile) {
})
}
// Bundle Pierre's Shiki worker into a single self-contained asset that the
// webviews load off the main thread for syntax highlighting.
function createShikiWorkerContext() {
return esbuild.context({
entryPoints: ["kilo-shiki-worker"],
bundle: true,
format: "iife",
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: "browser",
outfile: "dist/shiki-worker.js",
logLevel: "silent",
plugins: [shikiWorkerEntryPlugin, esbuildProblemMatcherPlugin],
})
}
async function main() {
// Build extension
const extensionCtx = await esbuild.context({
@@ -200,6 +218,9 @@ async function main() {
// Build webview
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
// Build the shared Shiki highlighting worker asset
const shikiWorkerCtx = await createShikiWorkerContext()
if (watch) {
await Promise.all([
extensionCtx.watch(),
@@ -208,6 +229,7 @@ async function main() {
diffViewerCtx.watch(),
diffVirtualCtx.watch(),
kiloClawCtx.watch(),
shikiWorkerCtx.watch(),
])
} else {
await Promise.all([
@@ -217,14 +239,16 @@ async function main() {
kiloClawCtx.rebuild(),
diffViewerCtx.rebuild(),
diffVirtualCtx.rebuild(),
shikiWorkerCtx.rebuild(),
])
await Promise.all([
extensionCtx.dispose(),
webviewCtx.dispose(),
agentManagerCtx.dispose(),
kiloClawCtx.dispose(),
diffViewerCtx.dispose(),
diffVirtualCtx.dispose(),
kiloClawCtx.dispose(),
shikiWorkerCtx.dispose(),
])
}
}
+1
View File
@@ -6,6 +6,7 @@
"webview-ui/diff-viewer/index.tsx",
"webview-ui/diff-virtual/index.tsx",
"webview-ui/kiloclaw/index.tsx",
"webview-ui/pierre-worker.ts",
"webview-ui/src/index.tsx",
"src/**/__tests__/**/*.{ts,spec.ts}",
"src/**/*.test.ts",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.3.17",
"version": "7.3.18",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
@@ -113,6 +113,7 @@ export class DiffVirtualProvider implements vscode.Disposable {
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.js")),
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.css")),
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
title: "Diff Virtual",
extraStyles: "#root { display: flex; flex-direction: column; height: 100%; }",
})
+1
View File
@@ -3494,6 +3494,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "webview.js")),
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "webview.css")),
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
title: "Kilo Code",
port: this.connectionService.getServerInfo()?.port,
extraStyles: `.container { height: 100%; display: flex; flex-direction: column; height: 100vh; border-right: 1px solid var(--border-weak-base); }`,
@@ -84,6 +84,7 @@ export class VscodeHost implements Host {
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.js")),
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.css")),
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
workerUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
title: "Agent Manager",
port,
})
@@ -238,6 +238,7 @@ export class DiffViewerProvider implements vscode.Disposable {
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.js")),
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.css")),
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
title: "Changes",
port: this.connection.getServerInfo()?.port,
extraStyles: "#root { display: flex; flex-direction: column; }",
@@ -71,6 +71,7 @@ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile {
file: raw.file,
before: view ? text(view, "deletions") : "",
after: view ? text(view, "additions") : "",
patch: raw.patch,
additions: raw.additions,
deletions: raw.deletions,
status: raw.status,
@@ -90,11 +90,17 @@ export function createStagedDiffSource(): DiffSource {
// For added: HEAD has no blob. For deleted: index has no blob.
const before = entry.status === "added" ? "" : await showBlob(git, dir, "HEAD", file)
const after = entry.status === "deleted" ? "" : await showBlob(git, dir, INDEX_REF, file)
const result = await git.execGit(
["-c", "core.quotepath=false", "diff", "--cached", "--no-ext-diff", "--no-renames", "HEAD", "--", file],
dir,
)
const patch = result.code === 0 ? result.stdout : undefined
const summarized = before === "" && after === "" && entry.status === "modified"
return {
file,
before,
after,
patch,
additions: entry.additions,
deletions: entry.deletions,
status: entry.status,
@@ -130,6 +130,10 @@ export function createUnstagedDiffSource(): DiffSource {
// after = disk content (or "" for deleted).
const before = !entry.tracked || entry.status === "added" ? "" : await showBlob(git, dir, INDEX_REF, file)
const after = entry.status === "deleted" ? "" : await readDisk(dir, file)
const result = entry.tracked
? await git.execGit(["-c", "core.quotepath=false", "diff", "--no-ext-diff", "--no-renames", "--", file], dir)
: undefined
const patch = result?.code === 0 ? result.stdout : undefined
const summarized = before === "" && after === "" && entry.status === "modified"
// For untracked added files numstat doesn't return counts, so backfill
@@ -139,6 +143,7 @@ export function createUnstagedDiffSource(): DiffSource {
file,
before,
after,
patch,
additions,
deletions: entry.deletions,
status: entry.status,
@@ -139,15 +139,16 @@ async function resolveOverrideRef(
/**
* Project a `WorktreeDiffEntry` from `local-diff.ts` onto the `DiffFile` shape
* expected by the diff viewer. Drops `patch` (the webview rebuilds before/after
* for itself) and coerces optional `before`/`after` to empty strings when the
* entry is summarized.
* expected by the diff viewer. Preserve its hunk-bounded `patch` so Pierre can
* parse the git diff directly rather than recomputing a diff from full source
* contents; summarized entries still coerce optional content to empty strings.
*/
function toDiffFile(entry: WorktreeDiffEntry): DiffFile {
return {
file: entry.file,
before: entry.before ?? "",
after: entry.after ?? "",
patch: entry.patch,
additions: entry.additions,
deletions: entry.deletions,
status: entry.status,
+2
View File
@@ -17,6 +17,8 @@ export interface DiffFile {
file: string
before: string
after: string
/** Hunk-bounded unified patch used by Pierre to avoid re-diffing full files. */
patch?: string
additions: number
deletions: number
status?: "added" | "deleted" | "modified"
@@ -144,6 +144,7 @@ export class KiloClawProvider implements vscode.Disposable {
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "kiloclaw.js")),
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "kiloclaw.css")),
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "assets", "icons")),
workerUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "shiki-worker.js")),
title: "KiloClaw",
})
+2 -1
View File
@@ -37,6 +37,7 @@ export function buildWebviewHtml(
scriptUri: vscode.Uri
styleUri: vscode.Uri
iconsBaseUri: vscode.Uri
workerUri: vscode.Uri
title: string
port?: number
extraStyles?: string
@@ -81,7 +82,7 @@ export function buildWebviewHtml(
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}";</script>
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}";</script>
<script nonce="${nonce}" src="${opts.scriptUri}"></script>
</body>
</html>`
@@ -26,6 +26,8 @@ export function buildCspString(cspSource: string, nonce: string, port?: number):
"default-src 'none'",
`style-src 'unsafe-inline' ${cspSource}`,
`script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`,
// Allow the bundled Shiki highlighting worker (loaded as a webview resource).
`worker-src ${cspSource}`,
`font-src ${cspSource}`,
`connect-src ${cspSource} ${connectSrc}`,
`img-src ${cspSource} data: https:`,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
@@ -1,8 +1,10 @@
import { describe, expect, it } from "bun:test"
import { mergeWorktreeDiffs } from "../../webview-ui/agent-manager/diff-state"
import {
EAGER_DIFF_REVIEW_LINES,
EXTREME_DIFF_CHANGED_LINES,
allOpenFiles,
eagerDiffFiles,
expandableOpenFiles,
initialOpenFiles,
toggleOpenFiles,
@@ -26,16 +28,27 @@ function diff(overrides: Partial<WorktreeFileDiff>): WorktreeFileDiff {
}
describe("agent manager diff state", () => {
it("preserves loaded detail when summary metadata is unchanged", () => {
const prev = [diff({ summarized: false, before: "old\n", after: "new\n" })]
it("preserves loaded detail and patch when summary metadata is unchanged", () => {
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })]
const next = [diff({ summarized: true })]
const result = mergeWorktreeDiffs(prev, next)
expect(result.diffs).toEqual([diff({ summarized: false, before: "old\n", after: "new\n" })])
expect(result.diffs).toEqual([
diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" }),
])
expect(result.diffs[0]).toBe(prev[0])
expect(result.stale.size).toBe(0)
})
it("replaces detailed content when patch anchors change", () => {
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })]
const next = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -100 +100 @@\n-old\n+new\n" })]
const result = mergeWorktreeDiffs(prev, next)
expect(result.diffs[0]).toBe(next[0])
expect(result.diffs[0]?.patch).toContain("@@ -100 +100 @@")
})
it("preserves cached content and marks stale when summary metadata changes", () => {
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", additions: 1 })]
const next = [diff({ summarized: true, additions: 2 })]
@@ -97,3 +110,46 @@ describe("agent manager diff state", () => {
expect(toggleOpenFiles(diffs, ["src/app.ts", "src/panel.ts"])).toEqual([])
})
})
describe("eager diff files", () => {
it("renders hunk-bounded detailed patches eagerly", () => {
const diffs = [
diff({ file: "src/a.ts", patch: "@@ -1 +1 @@\n-a\n+b\n", additions: 10, deletions: 5 }),
diff({ file: "src/b.ts", patch: "@@ -1 +1 @@\n-a\n+b\n", additions: 3, deletions: 0 }),
]
expect(eagerDiffFiles(diffs)).toEqual(new Set(["src/a.ts", "src/b.ts"]))
})
it("virtualizes a full-content detail without a hunk-bounded patch", () => {
const diffs = [diff({ file: "src/large-source.ts", before: "a\n".repeat(4000), after: "b\n", additions: 1 })]
expect(eagerDiffFiles(diffs)).toEqual(new Set())
})
it("virtualizes files larger than the large-file threshold", () => {
const diffs = [
diff({ file: "src/big.ts", patch: "large", additions: EXTREME_DIFF_CHANGED_LINES + 1, deletions: 0 }),
diff({ file: "src/small.ts", patch: "small", additions: 5, deletions: 0 }),
]
expect(eagerDiffFiles(diffs)).toEqual(new Set(["src/small.ts"]))
})
it("stops rendering eagerly once the review budget is exhausted", () => {
// Each file is under the large-file threshold, but together they exceed the
// aggregate budget, so the overflow falls back to virtualization.
const diffs = [
diff({ file: "src/a.ts", patch: "a", additions: 2000, deletions: 0 }),
diff({ file: "src/b.ts", patch: "b", additions: 2000, deletions: 0 }),
diff({ file: "src/c.ts", patch: "c", additions: 2000, deletions: 0 }),
diff({ file: "src/d.ts", patch: "d", additions: EAGER_DIFF_REVIEW_LINES - 6005, deletions: 0 }),
diff({ file: "src/e.ts", patch: "e", additions: 2000, deletions: 0 }),
diff({ file: "src/f.ts", patch: "f", additions: 5, deletions: 0 }),
]
const eager = eagerDiffFiles(diffs)
expect(eager.has("src/a.ts")).toBe(true)
expect(eager.has("src/d.ts")).toBe(true)
// Budget exhausted, so the next sizeable file virtualizes.
expect(eager.has("src/e.ts")).toBe(false)
// A smaller later file still fits within the remaining budget.
expect(eager.has("src/f.ts")).toBe(true)
})
})
@@ -69,6 +69,7 @@ describe("createSessionDiffSource.fetch", () => {
expect(foo.file).toBe("foo.ts")
expect(foo.before).toBe("keep\nold\n")
expect(foo.after).toBe("keep\nnew\n")
expect(foo.patch).toBe(modifiedPatch)
expect(foo.additions).toBe(1)
expect(foo.deletions).toBe(1)
expect(foo.status).toBe("modified")
@@ -48,6 +48,12 @@ describe("buildCspString", () => {
expect(result).toContain(`font-src ${cspSource}`)
})
it("allows only webview resources for the Shiki worker", () => {
const result = buildCspString(cspSource, nonce)
expect(result).toContain(`worker-src ${cspSource}`)
expect(result).not.toContain(`worker-src ${cspSource} blob:`)
})
it("includes cspSource and https: in img-src", () => {
const result = buildCspString(cspSource, nonce)
expect(result).toContain("img-src")
@@ -33,6 +33,7 @@ import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
eagerDiffFiles,
initialOpenFiles,
isLargeDiffFile,
toggleOpenFiles,
@@ -119,6 +120,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
// Reorder diffs to match the file-tree's depth-first visual order so
// scrolling through the accordion matches the tree grouping.
const sorted = createMemo(() => treeOrder(props.diffs))
const eager = createMemo(() => eagerDiffFiles(sorted()))
const comments = () => props.comments
const setComments = (next: ReviewComment[]) => props.onCommentsChange(next)
@@ -611,7 +613,9 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
<Diff<AnnotationMeta>
before={{ name: diff.file, contents: diff.before }}
after={{ name: diff.file, contents: diff.after }}
patch={diff.patch}
diffStyle={props.diffStyle ?? "unified"}
virtualized={!eager().has(diff.file)}
annotations={annotationsForFile(diff.file)}
renderAnnotation={buildAnnotation}
enableGutterUtility={true}
@@ -41,6 +41,7 @@ import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
eagerDiffFiles,
initialOpenFiles,
isLargeDiffFile,
toggleOpenFiles,
@@ -135,6 +136,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
// Reorder diffs to match the file-tree's depth-first visual order so
// scrolling through the diff panel matches the tree on the left.
const sorted = createMemo(() => treeOrder(props.diffs))
const eager = createMemo(() => eagerDiffFiles(sorted()))
const comments = () => props.comments
const setComments = (next: ReviewComment[]) => props.onCommentsChange(next)
@@ -708,7 +710,9 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
<Diff<AnnotationMeta>
before={{ name: diff.file, contents: diff.before }}
after={{ name: diff.file, contents: diff.after }}
patch={diff.patch}
diffStyle={props.diffStyle}
virtualized={!eager().has(diff.file)}
annotations={annotationsForFile(diff.file)}
renderAnnotation={buildAnnotation}
enableGutterUtility={props.canComment !== false}
@@ -2,11 +2,32 @@ import type { WorktreeFileDiff } from "../src/types/messages"
export const LONG_DIFF_MARKER_FILE_COUNT = 50
export const EXTREME_DIFF_CHANGED_LINES = 2_000
// Total changed rows rendered eagerly (non-virtualized) across one review before
// the remainder falls back to virtualization. Bounds eager DOM for huge reviews
// while letting normal reviews render fully so scrolling never shows gap buffers.
export const EAGER_DIFF_REVIEW_LINES = 8_000
export function isLargeDiffFile(diff: WorktreeFileDiff): boolean {
return diff.additions + diff.deletions > EXTREME_DIFF_CHANGED_LINES
}
// Files whose hunk-bounded patches should render eagerly (no row virtualization)
// so their rows stay mounted and never re-render on scroll. Never eager-render a
// detail without a patch: a tiny change in a very large source file would make
// Pierre re-diff and render full before/after contents on the main thread.
export function eagerDiffFiles(diffs: WorktreeFileDiff[]): Set<string> {
const eager = new Set<string>()
let used = 0
for (const diff of diffs) {
if (!diff.patch || isLargeDiffFile(diff)) continue
const size = diff.additions + diff.deletions
if (used + size > EAGER_DIFF_REVIEW_LINES) continue
used += size
eager.add(diff.file)
}
return eager
}
export function expandableOpenFiles(diffs: WorktreeFileDiff[]): string[] {
return diffs.filter((diff) => !isLargeDiffFile(diff) && diff.generatedLike !== true).map((diff) => diff.file)
}
@@ -38,6 +38,7 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD
existing.file === diff.file &&
existing.before === diff.before &&
existing.after === diff.after &&
existing.patch === diff.patch &&
sameDiffMeta(existing, diff)
)
return existing
@@ -45,8 +46,19 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD
if (!diff.summarized) return diff
// Metadata matches — restore cached content as before.
if (sameDiffMeta({ ...existing, summarized: true }, diff)) {
const merged = { ...diff, before: existing.before, after: existing.after, summarized: false }
if (existing.before === merged.before && existing.after === merged.after && sameDiffMeta(existing, merged))
const merged = {
...diff,
before: existing.before,
after: existing.after,
patch: existing.patch,
summarized: false,
}
if (
existing.before === merged.before &&
existing.after === merged.after &&
existing.patch === merged.patch &&
sameDiffMeta(existing, merged)
)
return existing
return merged
}
@@ -0,0 +1,67 @@
// Kilo-specific replacement for the shared `@opencode-ai/ui/pierre/worker`
// module (wired up by the pierre-worker-alias plugin in esbuild.js).
//
// The upstream module loads Pierre's Shiki worker through a Vite-only
// `?worker&url` import, which esbuild cannot resolve and which the VS Code
// webview CSP blocks. As a result the webview previously ran syntax
// highlighting synchronously on the main thread (`findNextMatchSync`), which
// froze scrolling on large diffs.
//
// Here we instead load the worker from a real dist asset (`dist/shiki-worker.js`,
// also produced by esbuild.js) using the webview URI the extension injects into
// the page. Pierre can offload highlighted updates to the pool after its initial
// plain render. The diff wrapper still needs to keep that initial render cheap,
// which is why review surfaces pass hunk-bounded patches instead of full files.
import { WorkerPoolManager } from "@pierre/diffs/worker"
export type WorkerPoolStyle = "unified" | "split"
// Oniguruma WebAssembly engine (matches the highlighting quality of the rest of
// the app). The CSP already allows `wasm-unsafe-eval`, and if the worker fails to
// instantiate, WorkerPoolManager falls back to the main-thread highlighter. To
// avoid WebAssembly entirely, build the portable worker and use "shiki-js".
const ENGINE = "shiki-wasm"
function uri(): string | undefined {
if (typeof window === "undefined") return undefined
return (window as { KILO_SHIKI_WORKER_URI?: string }).KILO_SHIKI_WORKER_URI
}
export function workerFactory(): Worker {
const url = uri()
if (!url) throw new Error("KILO_SHIKI_WORKER_URI is not set")
return new Worker(url)
}
function createPool(lineDiffType: "none" | "word-alt") {
const pool = new WorkerPoolManager(
{ workerFactory, poolSize: 2 },
{ theme: "Kilo", lineDiffType, preferredHighlighter: ENGINE },
)
void pool.initialize()
return pool
}
let unified: WorkerPoolManager | undefined
let split: WorkerPoolManager | undefined
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
// No injected worker URI means we can't spawn the worker; returning undefined
// makes Pierre fall back to the existing main-thread highlighter.
if (!uri()) return undefined
if (style === "split") {
if (!split) split = createPool("word-alt")
return split
}
if (!unified) unified = createPool("none")
return unified
}
export function getWorkerPools() {
return {
unified: getWorkerPool("unified"),
split: getWorkerPool("split"),
}
}
@@ -123,6 +123,8 @@ export interface WorktreeFileDiff {
file: string
before: string
after: string
/** Hunk-bounded unified patch used by Pierre to avoid re-diffing full files. */
patch?: string
additions: number
deletions: number
status?: "added" | "deleted" | "modified"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.17",
"version": "7.3.18",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -1,8 +1,9 @@
// kilocode_change - new file
import { remapChildren as _remapChildren } from "./fork"
import z from "zod"
import { Effect, Schema } from "effect"
import { Cause, Effect, Schema } from "effect"
import { BusEvent } from "@/bus/bus-event"
import { EffectBridge } from "@/effect/bridge"
import { Session } from "@/session/session"
import { MessageID, SessionID } from "@/session/schema"
import { fn } from "@/util/fn"
@@ -230,14 +231,18 @@ export namespace KiloSession {
// Session lifecycle hooks (share, unshare, remove)
// ---------------------------------------------------------------------------
export async function shareSession(id: string): Promise<{ url: string }> {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
return KiloSessions.share(id)
export function shareSession(id: SessionID) {
return EffectBridge.fromPromise(async () => {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
return KiloSessions.share(id)
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
}
export async function unshareSession(id: string): Promise<void> {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
await KiloSessions.unshare(id)
export function unshareSession(id: SessionID) {
return EffectBridge.fromPromise(async () => {
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
await KiloSessions.unshare(id)
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
}
export async function removeSession(id: string): Promise<void> {
+3 -5
View File
@@ -4,7 +4,7 @@ import { SyncEvent } from "@/sync"
import { Effect, Layer, Scope, Context } from "effect"
import { Config } from "@/config/config"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as ShareNext from "./share-next"
import { KiloSession } from "@/kilocode/session" // kilocode_change
export interface Interface {
readonly create: (input?: Session.CreateInput) => Effect.Effect<Session.Info>
@@ -19,20 +19,19 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const cfg = yield* Config.Service
const session = yield* Session.Service
const shareNext = yield* ShareNext.Service
const scope = yield* Scope.Scope
const sync = yield* SyncEvent.Service
const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
const conf = yield* cfg.get()
if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
const result = yield* shareNext.create(sessionID)
const result = yield* KiloSession.shareSession(sessionID) // kilocode_change - use Kilo public share URLs
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } })
return result
})
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
yield* shareNext.remove(sessionID)
yield* KiloSession.unshareSession(sessionID) // kilocode_change - use Kilo public share URLs
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })
})
@@ -50,7 +49,6 @@ export const layer = Layer.effect(
)
export const defaultLayer = layer.pipe(
Layer.provide(ShareNext.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
@@ -0,0 +1,63 @@
import { expect, spyOn } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { Session } from "../../src/session/session"
import { SessionShare } from "../../src/share/session"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Auth.defaultLayer, Storage.defaultLayer, CrossSpawnSpawner.defaultLayer))
const layer = SessionShare.layer.pipe(
Layer.provideMerge(Session.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
)
it.instance("shares and unshares sessions through Kilo public URLs", () => {
const urls: string[] = []
const fetch: typeof globalThis.fetch = Object.assign(
async (input: RequestInfo | URL) => {
const url = String(input)
urls.push(url)
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
if (url.endsWith("/share")) return Response.json({ public_id: "public-1" })
if (url.endsWith("/unshare")) return new Response(null, { status: 200 })
return new Response("{}", { status: 200 })
},
{ preconnect: globalThis.fetch.preconnect },
)
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
return Effect.gen(function* () {
const auth = yield* Auth.Service
const share = yield* SessionShare.Service
const session = yield* Session.Service
const storage = yield* Storage.Service
yield* auth.set("kilo", { type: "api", key: "test-token" })
const info = yield* share.create({ title: "share-test" })
yield* storage.write(["session_share", info.id], { id: "remote-1", ingestPath: "/api/ingest/session-1" })
const result = yield* share.share(info.id)
expect(result.url).toBe("https://app.kilo.ai/s/public-1")
expect((yield* session.get(info.id)).share?.url).toBe("https://app.kilo.ai/s/public-1")
yield* share.unshare(info.id)
expect((yield* session.get(info.id)).share).toBeUndefined()
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/share`))).toBe(true)
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/unshare`))).toBe(true)
}).pipe(
Effect.ensuring(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove("kilo").pipe(Effect.ignore)
request.mockRestore()
}),
),
Effect.provide(layer),
)
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -12,6 +12,6 @@
"exports": {
".": "./src/index.ts"
},
"version": "7.3.17",
"version": "7.3.18",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sdk",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -26,7 +26,7 @@
"typescript": "catalog:",
"vite": "catalog:"
},
"version": "7.3.17",
"version": "7.3.18",
"dependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "7.3.17",
"version": "7.3.18",
"type": "module",
"license": "MIT",
"exports": {
+11 -8
View File
@@ -5,7 +5,7 @@ import { $ } from "bun"
import semver from "semver"
import { parseArgs } from "util"
const pkgfile = new URL("../packages/kilo-jetbrains/package.json", import.meta.url).pathname
const props = new URL("../packages/kilo-jetbrains/gradle.properties", import.meta.url).pathname
const log = new URL("../packages/kilo-jetbrains/CHANGELOG.md", import.meta.url).pathname
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
@@ -67,9 +67,9 @@ if (dry) {
}
await $`git checkout -B ${branch} ${sha}`
await writepkg(ver)
await writeprops(ver)
await writelog(ver, entry)
await $`git add packages/kilo-jetbrains/package.json packages/kilo-jetbrains/CHANGELOG.md`
await $`git add packages/kilo-jetbrains/gradle.properties packages/kilo-jetbrains/CHANGELOG.md`
const changed = await $`git diff --cached --quiet`.nothrow()
if (changed.exitCode !== 0) await $`git commit -m ${`release(jetbrains): v${ver}`}`
@@ -203,10 +203,13 @@ function entries(notes: string) {
return groups
}
async function writepkg(ver: string) {
const pkg = await Bun.file(pkgfile).json()
pkg.version = ver
await Bun.write(pkgfile, `${JSON.stringify(pkg, null, 2)}\n`)
async function writeprops(ver: string) {
const current = await Bun.file(props).text()
const line = `kilo.jetbrains.version=${ver}`
const next = current.match(/^kilo\.jetbrains\.version=/m)
? current.replace(/^kilo\.jetbrains\.version=.*$/m, line)
: `${current.trim()}\n${line}\n`
await Bun.write(props, next.endsWith("\n") ? next : `${next}\n`)
}
async function writelog(ver: string, entry: string) {
@@ -229,7 +232,7 @@ function regex(ver: string) {
function body(ver: string, kind: string, from: string, tag: string, sha: string, notes: string) {
return `## Summary
- Prepare JetBrains ${kind} release ${ver}.
- Review and edit \`packages/kilo-jetbrains/CHANGELOG.md\` before merging.
- Review \`packages/kilo-jetbrains/gradle.properties\` and edit \`packages/kilo-jetbrains/CHANGELOG.md\` before merging.
JetBrains-Version: ${ver}
JetBrains-Kind: ${kind}
+10 -2
View File
@@ -69,8 +69,8 @@ if (existing.exitCode !== 0) throw new Error(`${tag} does not exist`)
const sha = (await $`git rev-list -n 1 ${tag}`.text()).trim()
if (sha !== commit) throw new Error(`${tag} points at ${sha}, expected ${commit}`)
const pkg = await Bun.file("packages/kilo-jetbrains/package.json").json()
if (pkg.version !== ver) throw new Error(`packages/kilo-jetbrains/package.json version is ${pkg.version}, expected ${ver}`)
const prop = await props()
if (prop !== ver) throw new Error(`packages/kilo-jetbrains/gradle.properties kilo.jetbrains.version is ${prop}, expected ${ver}`)
const changelog = await Bun.file("packages/kilo-jetbrains/CHANGELOG.md").text()
if (!changelog.includes(`## [${ver}]`)) throw new Error(`CHANGELOG.md is missing section for ${ver}`)
@@ -102,3 +102,11 @@ function need(body: string, key: string) {
if (!value) throw new Error(`PR body is missing ${key}`)
return value
}
async function props() {
const text = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
const line = text.split(/\r?\n/).find((item) => item.startsWith("kilo.jetbrains.version="))
const value = line?.split("=", 2)[1]?.trim()
if (!value) throw new Error("packages/kilo-jetbrains/gradle.properties is missing kilo.jetbrains.version")
return value
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/upstream-merge",
"version": "7.3.17",
"version": "7.3.18",
"private": true,
"type": "module",
"description": "Scripts for automating upstream opencode merges into Kilo",