diff --git a/.changeset/fix-cli-share-links.md b/.changeset/fix-cli-share-links.md new file mode 100644 index 00000000000..e96b25e6f51 --- /dev/null +++ b/.changeset/fix-cli-share-links.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Use Kilo session share links when sharing conversations from the CLI. diff --git a/.changeset/smooth-diff-worker-highlight.md b/.changeset/smooth-diff-worker-highlight.md new file mode 100644 index 00000000000..0bb9e666c17 --- /dev/null +++ b/.changeset/smooth-diff-worker-highlight.md @@ -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. diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index bc1094b11fd..0b0f9244445 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -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 }} diff --git a/bun.lock b/bun.lock index 0983fc24172..0b944528095 100644 --- a/bun.lock +++ b/bun.lock @@ -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:", diff --git a/nix/hashes.json b/nix/hashes.json index 7a0d099d63c..82e6694687d 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -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=" } } diff --git a/package.json b/package.json index b11c4723477..43c299e393e 100644 --- a/package.json +++ b/package.json @@ -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": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 1ccc8b551eb..3875f8a167a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 70f83fcdf2e..f1f65868847 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -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"] diff --git a/packages/kilo-docs/components/TableOfContents.tsx b/packages/kilo-docs/components/TableOfContents.tsx index c6b3f5b723f..f0e9157eddc 100644 --- a/packages/kilo-docs/components/TableOfContents.tsx +++ b/packages/kilo-docs/components/TableOfContents.tsx @@ -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).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) => { + 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 (
  • - {item.title} + + {item.title} +
  • ) })} diff --git a/packages/kilo-docs/components/Tabs.tsx b/packages/kilo-docs/components/Tabs.tsx index ecffe35dc6d..d826b818c2f 100644 --- a/packages/kilo-docs/components/Tabs.tsx +++ b/packages/kilo-docs/components/Tabs.tsx @@ -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).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 diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index 2f9800f869e..3fdcdff6d11 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -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: diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 38523df9c64..d1203040c55 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -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", diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index 2739752f3a6..a355d72bc48 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -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) } } } diff --git a/packages/kilo-docs/pages/getting-started/installing.md b/packages/kilo-docs/pages/getting-started/installing.md index e63c864adb5..095d1687af7 100644 --- a/packages/kilo-docs/pages/getting-started/installing.md +++ b/packages/kilo-docs/pages/getting-started/installing.md @@ -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: diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 93a75fd0ad8..c55f7888d58 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -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", diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 7d9b5134848..ba2736e9fac 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -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", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 57af8763e65..43409aa35b9 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -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", diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 6e145106e57..e30af42193d 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -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. diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md index 146feaba28e..2d704f9fee5 100644 --- a/packages/kilo-jetbrains/README.md +++ b/packages/kilo-jetbrains/README.md @@ -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 diff --git a/packages/kilo-jetbrains/RELEASING.md b/packages/kilo-jetbrains/RELEASING.md index 69bea003546..e08d34d98c0 100644 --- a/packages/kilo-jetbrains/RELEASING.md +++ b/packages/kilo-jetbrains/RELEASING.md @@ -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 ``, 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 ``, 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` 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` 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 diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 1839adbc8f2..a2dda3a6793 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -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) { val state = KiloAppState.Error(message, errors) - _appState.value = state warnAppError(state) + _appState.value = state } private fun warnAppError(state: KiloAppState.Error) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt index d18a86d831e..2d9bd74d8b7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt @@ -291,8 +291,8 @@ class KiloBackendWorkspace( } private fun setWorkspaceError(message: String, errors: List) { - _state.value = KiloWorkspaceState.Error(message, errors) log.warn("Workspace error [$directory]: $message") + _state.value = KiloWorkspaceState.Error(message, errors) } private data class FetchResult(val value: T?, val error: LoadError?) { diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index cfba42d354d..06e65495e9d 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -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 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 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() diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index d5066fb4c41..ec656f922ff 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -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 diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index b8a39255de7..219822bd0e4 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -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": {} diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index c9781d6f07b..2161a1b81ba 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -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", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 3de9ee70d26..4eadd6e4a9a 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.3.17", + "version": "7.3.18", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-ui/src/components/diff-ssr.tsx b/packages/kilo-ui/src/components/diff-ssr.tsx index 84b862f8d78..a233514c87a 100644 --- a/packages/kilo-ui/src/components/diff-ssr.tsx +++ b/packages/kilo-ui/src/components/diff-ssr.tsx @@ -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(props: SSRDiffProps) { 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(props: SSRDiffProps) { onCleanup(() => monitor.disconnect()) } - const virtualizer = getVirtualizer() + const virtualizer = local.virtualized === false ? undefined : getVirtualizer() fileDiffInstance = virtualizer ? new VirtualizedFileDiff( @@ -250,9 +253,12 @@ export function Diff(props: SSRDiffProps) { ) // @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, diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index febb70ca813..87db2bb4a70 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -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(props: DiffProps) { let container!: HTMLDivElement let observer: MutationObserver | undefined let sharedVirtualizer: NonNullable> | 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(props: DiffProps) { const [local, others] = splitProps(props, [ "before", "after", + "patch", "fileDiff", "class", "classList", @@ -163,6 +172,7 @@ export function Diff(props: DiffProps) { "selectedLines", "commentedLines", "onRendered", + "virtualized", ]) const mobile = createMediaQuery("(max-width: 640px)") @@ -178,11 +188,24 @@ export function Diff(props: DiffProps) { }) 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(props: DiffProps) { 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(props: DiffProps) { container.innerHTML = "" - if (local.fileDiff) { + if (metadata) { instance.render({ - fileDiff: local.fileDiff, + fileDiff: metadata, lineAnnotations: annotations, containerWrapper: container, }) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 0d2edfeea93..189a3147bf1 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -48,6 +48,11 @@ type DiffShared = FileDiffOptions & { 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 = FileDiffOptions & { type DiffPair = DiffShared & { 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 = DiffShared & { fileDiff: FileDiffMetadata before?: undefined after?: undefined + patch?: undefined } export type DiffProps = DiffPair | DiffPatch diff --git a/packages/kilo-vscode/AGENTS.md b/packages/kilo-vscode/AGENTS.md index f88a0f9f7b4..842cb9ba4c2 100644 --- a/packages/kilo-vscode/AGENTS.md +++ b/packages/kilo-vscode/AGENTS.md @@ -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 diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index 704953109de..a82df0e7d23 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -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(), ]) } } diff --git a/packages/kilo-vscode/knip.json b/packages/kilo-vscode/knip.json index 79b8eb7da65..98c83d5a1f0 100644 --- a/packages/kilo-vscode/knip.json +++ b/packages/kilo-vscode/knip.json @@ -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", diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index df5f1328a5e..310f8379cb9 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -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", diff --git a/packages/kilo-vscode/src/DiffVirtualProvider.ts b/packages/kilo-vscode/src/DiffVirtualProvider.ts index 90a170559dc..721cb71c3fd 100644 --- a/packages/kilo-vscode/src/DiffVirtualProvider.ts +++ b/packages/kilo-vscode/src/DiffVirtualProvider.ts @@ -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%; }", }) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 02d5afea78b..19c6316a83d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -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); }`, diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 8d5f9ed9aca..82b381e054c 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -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, }) diff --git a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts index 476cdb92034..7f773cafcfa 100644 --- a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts +++ b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts @@ -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; }", diff --git a/packages/kilo-vscode/src/diff/sources/session.ts b/packages/kilo-vscode/src/diff/sources/session.ts index cf10f0e2a2a..4df81854a17 100644 --- a/packages/kilo-vscode/src/diff/sources/session.ts +++ b/packages/kilo-vscode/src/diff/sources/session.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index 9af28badce3..03a54198ff7 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index 26f1210b928..2441bb7f85b 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index fdeea76a44a..f7e2a0e4d1a 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index 59dd384f5a4..a6d9def400f 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -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" diff --git a/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts b/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts index fb0c4beebfc..b6530ccc70a 100644 --- a/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts +++ b/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts @@ -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", }) diff --git a/packages/kilo-vscode/src/utils.ts b/packages/kilo-vscode/src/utils.ts index 1936575c222..87ff464e251 100644 --- a/packages/kilo-vscode/src/utils.ts +++ b/packages/kilo-vscode/src/utils.ts @@ -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(
    - + ` diff --git a/packages/kilo-vscode/src/webview-html-utils.ts b/packages/kilo-vscode/src/webview-html-utils.ts index afca8884703..ffe6e0b7168 100644 --- a/packages/kilo-vscode/src/webview-html-utils.ts +++ b/packages/kilo-vscode/src/webview-html-utils.ts @@ -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:`, diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 200c0a51008..ac10c6526d4 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.3.17", + "version": "7.3.18", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts index 5a12d4aaa39..4c88fdceb7a 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts @@ -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 { } 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) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts index 3aa9be076d5..f8a24d24c5c 100644 --- a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts @@ -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") diff --git a/packages/kilo-vscode/tests/unit/webview-html.test.ts b/packages/kilo-vscode/tests/unit/webview-html.test.ts index 465588e7cde..d54c99ff8cf 100644 --- a/packages/kilo-vscode/tests/unit/webview-html.test.ts +++ b/packages/kilo-vscode/tests/unit/webview-html.test.ts @@ -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") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 5ecd82318f4..acef4a9c2f1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -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 = (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 = (props) => { 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} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index 4dd0936b991..d609816367e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -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 = (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 = (props) => 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} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts index dc6f22d9553..64a52ac71ef 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts @@ -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 { + const eager = new Set() + 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) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts index 05a9657172f..e54e156e0ff 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts @@ -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 } diff --git a/packages/kilo-vscode/webview-ui/pierre-worker.ts b/packages/kilo-vscode/webview-ui/pierre-worker.ts new file mode 100644 index 00000000000..25c126394cb --- /dev/null +++ b/packages/kilo-vscode/webview-ui/pierre-worker.ts @@ -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"), + } +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 6a5a21e3ccb..c19d22c9dd5 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -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" diff --git a/packages/opencode/package.json b/packages/opencode/package.json index deea8eb894f..ffd2c0a73a4 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", diff --git a/packages/opencode/src/kilocode/session/index.ts b/packages/opencode/src/kilocode/session/index.ts index 34389450bda..c9d5e90213b 100644 --- a/packages/opencode/src/kilocode/session/index.ts +++ b/packages/opencode/src/kilocode/session/index.ts @@ -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 { - 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 { diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index f66341d97a3..ebd198ebe84 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -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 @@ -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), diff --git a/packages/opencode/test/kilocode/session-share.test.ts b/packages/opencode/test/kilocode/session-share.test.ts new file mode 100644 index 00000000000..280c0147e6f --- /dev/null +++ b/packages/opencode/test/kilocode/session-share.test.ts @@ -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), + ) +}) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 76cfdd4d68a..bf9d07ee59a 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -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": { diff --git a/packages/script/package.json b/packages/script/package.json index 9681da73979..74d5106dcd7 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -12,6 +12,6 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.3.17", + "version": "7.3.18", "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 0ccd7c9a6a3..e0190ed33ec 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -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": { diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 3f3854f053e..6999aa7ba65 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.3.17", + "version": "7.3.18", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index 3b52909318f..7f265caddc7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.3.17", + "version": "7.3.18", "type": "module", "license": "MIT", "exports": { diff --git a/script/jetbrains-release-pr.ts b/script/jetbrains-release-pr.ts index 2c2c4d859b8..db9d329e7ba 100644 --- a/script/jetbrains-release-pr.ts +++ b/script/jetbrains-release-pr.ts @@ -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} diff --git a/script/jetbrains-release-validate.ts b/script/jetbrains-release-validate.ts index f56f046d6a5..2cb64e6eb9c 100644 --- a/script/jetbrains-release-validate.ts +++ b/script/jetbrains-release-validate.ts @@ -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 +} diff --git a/script/upstream/package.json b/script/upstream/package.json index b32337b20d6..f9f4109cce0 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -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",