diff --git a/.changeset/fix-string-permission-migration.md b/.changeset/fix-string-permission-migration.md new file mode 100644 index 00000000000..99683ef8ecf --- /dev/null +++ b/.changeset/fix-string-permission-migration.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Handle string-form permission values when migrating bash permissions. diff --git a/.changeset/revert-file-moves.md b/.changeset/revert-file-moves.md new file mode 100644 index 00000000000..52e0a08868e --- /dev/null +++ b/.changeset/revert-file-moves.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Restore files to their original paths when reverting a task that moved or renamed them. diff --git a/.changeset/share-server-session-runtime.md b/.changeset/share-server-session-runtime.md new file mode 100644 index 00000000000..16bcb875580 --- /dev/null +++ b/.changeset/share-server-session-runtime.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Keep server controls and events connected to active sessions and subagents. diff --git a/.changeset/steady-shell-expansion.md b/.changeset/steady-shell-expansion.md new file mode 100644 index 00000000000..0e4fbb09616 --- /dev/null +++ b/.changeset/steady-shell-expansion.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent large highlighted shell outputs from blanking the VS Code webview when expanded. diff --git a/.changeset/stop-moved-agent-sessions.md b/.changeset/stop-moved-agent-sessions.md new file mode 100644 index 00000000000..ee125bf2d8f --- /dev/null +++ b/.changeset/stop-moved-agent-sessions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the Stop button working when an active session moves between the workspace and an Agent Manager worktree. diff --git a/.github/workflows/test-jetbrains.yml b/.github/workflows/test-jetbrains.yml index 7fde0ec1ea3..8469b98776a 100644 --- a/.github/workflows/test-jetbrains.yml +++ b/.github/workflows/test-jetbrains.yml @@ -41,6 +41,8 @@ jobs: needs: changes if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 + container: + image: ghcr.io/kilo-org/build/jetbrains:24.04 defaults: run: shell: bash @@ -50,19 +52,11 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Bun - uses: ./.github/actions/setup-bun + - name: Mark workspace as git-safe + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-read-only: ${{ github.ref != 'refs/heads/main' }} + - name: Install dependencies + run: bun install - name: Run JetBrains unit tests run: bun script/test-ci.ts diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index b4d0da57a9f..11ef4e1ab75 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -21,6 +21,9 @@ jobs: # kilocode_change start - name: Run TypeScript typecheck run: bun turbo typecheck --filter='!@kilocode/kilo-jetbrains' + + - name: Build Kilo Console + run: bun turbo build --filter=@kilocode/kilo-console # kilocode_change end # kilocode_change start diff --git a/bun.lock b/bun.lock index c4927fff9c0..bcf7b29638d 100644 --- a/bun.lock +++ b/bun.lock @@ -122,6 +122,7 @@ "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", + "@pierre/diffs": "catalog:", "@solidjs/router": "catalog:", "ghostty-web": "0.4.0", "solid-js": "catalog:", diff --git a/nix/hashes.json b/nix/hashes.json index 49ae8cbc98c..f532c00b9fa 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-qzU+msL1HicrkrYHNCitcutUqTFUatMArEuGgf/gOoE=", - "aarch64-linux": "sha256-GebxO5jgwnsNbvBdrbOlJ6u9C3/r6J7Q8WbzBv+wmhI=", - "aarch64-darwin": "sha256-NtAQ6QiL+iGaDxuFyvBiMFvBoEgBb5pK8b8xgkRdKx0=", - "x86_64-darwin": "sha256-rfxQmcEFsPD7AzSCBUZNxkNQUnkPnoN8a4shieEK1+Q=" + "x86_64-linux": "sha256-0BmSpfTtd6HSwwpFoOO2xv8WFiGG2jWTYh+wWGVXrQI=", + "aarch64-linux": "sha256-7xIsN/OSYYfe480/AISZclzc0mdw6GHzp9Iyj6gvsMA=", + "aarch64-darwin": "sha256-/qiT20m9l3Wr5UHpfh6VDhTuu2RmIbUkzngxMHgpL8g=", + "x86_64-darwin": "sha256-MkRgwFs/YVIXz1P+707godlSxwsTL2wv7JrZAGaYRMQ=" } } diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 999c2cfa2f7..b002dd1077d 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -14,6 +14,7 @@ "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", + "@pierre/diffs": "catalog:", "@solidjs/router": "catalog:", "ghostty-web": "0.4.0", "solid-js": "catalog:", diff --git a/packages/kilo-docs/docs/getting-started/switching-from-cline.md b/packages/kilo-docs/docs/getting-started/switching-from-cline.md index 8f04bb864ca..2c7dcd6be0c 100644 --- a/packages/kilo-docs/docs/getting-started/switching-from-cline.md +++ b/packages/kilo-docs/docs/getting-started/switching-from-cline.md @@ -202,9 +202,9 @@ One-click deployments from directly within Kilo. Go from code to production with Automatically analyzes your PRs using your choice of AI model. Reviews happen the moment a PR is opened or updated, covering performance, security, style, and test coverage. -### Managed Indexing +### Codebase Indexing -Semantic search across your repositories using cloud-hosted embeddings. Kilo indexes your codebase to deliver more relevant, context-aware responses. +Semantic search across your repositories using configurable embedding providers and vector stores. Kilo indexes your codebase to deliver more relevant, context-aware responses. ### Autocomplete diff --git a/packages/kilo-docs/lib/nav/deploy-secure.ts b/packages/kilo-docs/lib/nav/deploy-secure.ts index 7c41f137996..5120c5f5fb2 100644 --- a/packages/kilo-docs/lib/nav/deploy-secure.ts +++ b/packages/kilo-docs/lib/nav/deploy-secure.ts @@ -6,7 +6,6 @@ export const DeploySecureNav: NavSection[] = [ links: [ { href: "/deploy-secure", children: "Overview" }, { href: "/deploy-secure/deploy", children: "Deploy" }, - { href: "/deploy-secure/managed-indexing", children: "Managed Indexing" }, ], }, { diff --git a/packages/kilo-docs/mappingplan.md b/packages/kilo-docs/mappingplan.md index d94063a4f67..7131580834f 100644 --- a/packages/kilo-docs/mappingplan.md +++ b/packages/kilo-docs/mappingplan.md @@ -113,7 +113,6 @@ | New Item | Existing Page(s) | |---|---| | Deploy | `advanced-usage/deploy` | -| Managed Indexing | `advanced-usage/managed-indexing` | | Security Reviews | `contributing/architecture/security-reviews` (move out of contributing) | --- diff --git a/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md b/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md index 5bba0acd3f9..7ae258de9f1 100644 --- a/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md +++ b/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md @@ -71,7 +71,7 @@ Low Depth indicates that developers may be trying AI but not trusting or shippin **Actions:** -1. Enable [Managed Indexing](/docs/deploy-secure/managed-indexing) to improve context quality +1. Enable [Codebase Indexing](/docs/customize/context/codebase-indexing) to improve context quality 2. Review whether suggestions are relevant to your codebase 3. Introduce chained workflows to increase multi-stage usage @@ -119,7 +119,7 @@ Use the score tiers as milestones: **For Depth:** - "Chain Challenge" — Complete one feature using plan → build → review -- Managed Indexing rollout — Enable better context for the whole team +- Codebase Indexing rollout - Enable better context for the whole team - Deploy previews — Validate AI output before merging **For Coverage:** @@ -170,7 +170,7 @@ The AI Adoption Score is designed to be quotable: > > **Key Actions Taken:** > -> - Enabled Managed Indexing for better AI context +> - Enabled Codebase Indexing for better AI context > - Introduced Code Reviews for all PRs > - Onboarded 3 inactive team members > diff --git a/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md b/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md index 126a8adc156..949329076f5 100644 --- a/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md +++ b/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md @@ -73,7 +73,7 @@ Linking coding → review → deploy actions significantly boosts your Depth sco If acceptance rates are low, the issue is often context. The AI is making suggestions without understanding your codebase. -**Action:** Enable [Managed Indexing](/docs/deploy-secure/managed-indexing) to give the model vector-backed search across your repository. +**Action:** Enable [Codebase Indexing](/docs/customize/context/codebase-indexing) to give the model vector-backed search across your repository. Better context leads to: @@ -171,7 +171,7 @@ Other ways to spread usage: 1. Identify your most active users and learn what they're doing 2. Introduce Code Reviews to spread usage -3. Enable Managed Indexing for better context +3. Enable Codebase Indexing for better context 4. Set a monthly score goal (e.g., "reach 55 by next month") ### If You're at 51–75 (Growing Adoption) diff --git a/packages/kilo-docs/pages/deploy-secure/index.md b/packages/kilo-docs/pages/deploy-secure/index.md index d929c64349e..91a52cfdac1 100644 --- a/packages/kilo-docs/pages/deploy-secure/index.md +++ b/packages/kilo-docs/pages/deploy-secure/index.md @@ -32,15 +32,6 @@ Ship your applications with one-click deployment: - Real-time log streaming - Deployment history with one-click rollbacks -## Managed Indexing - -Fast, scalable code indexing for better AI context: - -- [**Managed Indexing**](/docs/deploy-secure/managed-indexing) — Cloud-based code indexing -- Improved context for large codebases -- Faster initial indexing times -- Reduced local resource usage - ## Security Reviews AI-powered dependency vulnerability triage for your codebase: @@ -61,8 +52,7 @@ AI-powered dependency vulnerability triage for your codebase: 1. Enable [GitHub Integration](/docs/deploy-secure/deploy#prerequisites) for deployments 2. Set up your first [deployment](/docs/deploy-secure/deploy) in the dashboard -3. Configure [managed indexing](/docs/deploy-secure/managed-indexing) for large projects -4. Enable the [Security Agent](/docs/deploy-secure/security-reviews) to triage your Dependabot alerts +3. Enable the [Security Agent](/docs/deploy-secure/security-reviews) to triage your Dependabot alerts ## Best Practices diff --git a/packages/kilo-docs/pages/deploy-secure/managed-indexing.md b/packages/kilo-docs/pages/deploy-secure/managed-indexing.md deleted file mode 100644 index 1b799c40af2..00000000000 --- a/packages/kilo-docs/pages/deploy-secure/managed-indexing.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: "Managed Indexing" -description: "Cloud-managed codebase indexing" ---- - -# Managed Indexing - -Kilo's **Managed Indexing** feature provides semantic search across your repositories using cloud-hosted embeddings. When enabled, Kilo indexes your codebase to deliver more relevant, context-aware responses during development. - ---- - -## What Managed Indexing Enables - -- Semantic search across your entire codebase -- More accurate and context-aware AI responses -- Git-aware indexing that tracks your base branch and feature branch changes -- Shared indexes for teams and enterprise accounts -- Cost-effective cloud storage with automatic cleanup of stale indexes - ---- - -## Prerequisites - -Before enabling Managed Indexing: - -- **Your workspace must be a Git repository** - Indexing requires a Git repository root directory. Non-Git folders will not be indexed. - -- **Available credit balance** - If your balance reaches zero, managed indexing will be disabled and the extension will revert to local indexing (if configured). - ---- - -## Cost - -- **Currently free during beta** -- **Pricing coming soon** — A daily usage fee for index storage will be deducted from your AI credit balance. You will be charged per GB per day. -- **Embedding model** — Uses `mistralai/codestral-embed-2505` which currently charges $0.15/M input tokens. - ---- - -## How to Enable - -Codebase Indexing is rolling out across our users. It will automatically engage unless your repository root is configured to opt out. - -1. Create a `.kilocode/config.json` file in the root of your repository (if it doesn't already exist). -2. Add the following configuration: - -```json -{ - "project": { - "managedIndexingEnabled": false - } -} -``` - -### Configuration Options - -| Field | Type | Required | Description | -|---|---|---|---| -| `project.id` | string | No | Custom name for your project. Defaults to the name from your Git origin remote. | -| `project.baseBranch` | string | No | Specifies your base branch if it isn't `main`, `master`, `dev`, or `develop`. | -| `project.managedIndexingEnabled` | boolean | No | Set to `false` to disable indexing for individual project repositories. Defaults to `true`. | - -Organization-wide indexing is enabled for any organization that has a credit balance. If you want to disable indexing for a specific repository, set `managedIndexingEnabled` to `false` in the config file. - ---- - -## How Managed Indexing Works - -- **Base branch** — Indexed in its entirety -- **Feature branches** — Only changes from the base branch are indexed -- **Detached HEAD states** — Not indexed -- **Storage** — Embeddings are stored in Kilo Cloud. Your actual code is never stored, only the vector embeddings. -- **Team sharing** — For teams and enterprise accounts, indexes are shared among all team members. - -### Index Retention - -Indexes are stored for **7 days**. If a branch or repository index hasn't been updated within that window, it will be garbage collected. The next time you open the project in VS Code with Kilo running, it will be re-indexed automatically. - -This retention policy keeps costs minimal by only maintaining indexes for actively used code. - ---- - -## Managing Your Indexes - -A minimal UI is available at [app.kilo.ai](https://app.kilo.ai) to: - -- View the size and status of your indexed projects -- Delete old branches & projects. - ---- - -## Migration from Local Indexing - -Enabling managed indexing will **replace local self-hosted indexing entirely**. If you have already configured local indexing for a workspace it will take precedence until you disable it. - -### Automatic Reversion - -If your credit balance reaches zero, the extension will automatically revert to local indexing (if previously configured). - ---- - -## Perfect For - -Managed Indexing is ideal for: - -- **Developers wanting smarter, context-aware AI assistance** -- **Teams needing shared semantic search across repositories** -- **Large codebases where finding relevant code is difficult** -- **Organizations wanting centralized index management** - ---- - -## Limitations and Guidance - -- **Git repository required** — Only Git repository root directories can be indexed. We plan to extend this in the future. -- **Detached HEAD not supported** — Commits in detached HEAD state will not be indexed. -- **7-day retention** — Unused indexes are automatically removed after 7 days. -- **Beta capacity** — During beta, indexing capacity may be limited for very large repositories. -- **Organization indexing** — Shared organization indexes currently require contacting support. diff --git a/packages/kilo-docs/pages/index.tsx b/packages/kilo-docs/pages/index.tsx index f95b39d715f..62d29dafc20 100644 --- a/packages/kilo-docs/pages/index.tsx +++ b/packages/kilo-docs/pages/index.tsx @@ -187,7 +187,6 @@ const categories = [ ), links: [ { title: "Deploy", href: "/deploy-secure" }, - { title: "Managed Indexing", href: "/deploy-secure" }, { title: "Security Reviews", href: "/deploy-secure" }, ], }, diff --git a/packages/kilo-docs/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js index e9bb1472ce8..cd3f4d6b326 100644 --- a/packages/kilo-docs/previous-docs-redirects.js +++ b/packages/kilo-docs/previous-docs-redirects.js @@ -613,7 +613,13 @@ module.exports = [ }, { source: "/docs/advanced-usage/managed-indexing", - destination: "/docs/deploy-secure/managed-indexing", + destination: "/docs/customize/context/codebase-indexing", + basePath: false, + permanent: true, + }, + { + source: "/docs/deploy-secure/managed-indexing", + destination: "/docs/customize/context/codebase-indexing", basePath: false, permanent: true, }, diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png index 390e57e4890..0438ce9395f 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9d8327c530c9d071b5eb07ebbf48441ea8d64b8a4cd7201665a8fa037ca82ed -size 50349 +oid sha256:e8318f427b6c0ff71d4cce5c8ac03ba4778f99fda5723d24d23c38471523b622 +size 52203 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 49c8e1c151a..5a30e68d6ed 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -81,6 +81,8 @@ - +- + - - diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 12eb27370d9..cb9893acfd3 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -32,6 +32,21 @@ ## [Unreleased] +## [7.0.1-rc.11] - 2026-06-17 + +### Added + +- Provider settings management, including provider catalog sections, provider descriptions, provider settings actions, disconnect flows, provider auth handling, and provider/model picker improvements. +- Session copy controls for chat messages. + +### Fixed + +- Cap JetBrains prompt input growth and hide soft wrap glyphs in the prompt field. +- Keep JetBrains provider toolbars and authentication overlays fixed, and improve provider API key dialog sizing. +- Clean up restartless unload behavior. +- Silence interrupted session notifications across clients. +- Always deny tool calls for system agents. + ## [7.0.1-rc.10] - 2026-06-17 ### Added diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index c87de87457e..a1cdd86dc7a 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.1-rc.10 +kilo.jetbrains.version=7.0.1-rc.11 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index d244b157b91..d8a448ed0ed 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -2235,7 +2235,6 @@ ToolRegistry.register({ defaultOpen={props.defaultOpen ?? true} onOpenChange={setOpen} allowPendingToggle - animated trigger={
diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 3ef1f944182..da0ea7d794b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -72,7 +72,7 @@ import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-pa import { childID } from "./kilo-provider/task-session" import { VisibleTaskStreams } from "./kilo-provider/visible-task-streams" import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network" -import { abortSession } from "./kilo-provider/abort" +import { SessionAbort } from "./kilo-provider/abort" import { buildAutocompleteSettingsMessage, validAutocompleteSetting, @@ -310,6 +310,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private syncedChildSessions: Set = new Set() private sessionStatusMap = new Map() // Latest status used for destructive config warnings. private sessionDirectories = new Map() // Per-session directory overrides, such as Agent Manager worktrees. + private readonly aborts = new SessionAbort() private projectID: string | undefined // Current workspace project ID used to filter sessions. private loadMessagesAbort: AbortController | null = null // Current load request cancellation. private lastReconciledAt = new Map() // Per-session focus-mode reconcile timestamp. @@ -657,10 +658,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper * When set, all operations for this session use this directory instead of the workspace root. */ public setSessionDirectory(sessionId: string, directory: string): void { + this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), this.getWorkspaceDirectory(sessionId)) this.sessionDirectories.set(sessionId, directory) } public clearSessionDirectory(sessionId: string): void { + this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), this.getWorkspaceDirectory(sessionId)) this.sessionDirectories.delete(sessionId) } @@ -1755,6 +1758,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.visibleTaskStreams.delete(sessionID) this.syncedChildSessions.delete(sessionID) this.sessionDirectories.delete(sessionID) + this.aborts.delete(sessionID) this.lastReconciledAt.delete(sessionID) this.connectionService.pruneSession(sessionID) if (this.currentSession?.id === sessionID) { @@ -2754,24 +2758,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } private async handleAbort(sessionID?: string): Promise { - if (!this.client) { - return - } - - const targetSessionID = sessionID || this.currentSession?.id - if (!targetSessionID) { - return - } - - try { - await abortSession({ - client: this.client, - sessionID: targetSessionID, - dir: this.getWorkspaceDirectory(targetSessionID), - }) - } catch (error) { - console.error("[Kilo New] KiloProvider: Failed to abort session:", error) - } + const sid = sessionID || this.currentSession?.id + if (!this.client || !sid || !(await this.aborts.stop(this.client, sid, this.getWorkspaceDirectory(sid)))) return + this.sessionStatusMap.set(sid, "idle") + this.streams.flush(sid) + this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" }) } private async handleRevertSession(sessionID: string, messageID: string, partID?: string): Promise { @@ -3156,6 +3147,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "session.status") { const sid = event.properties.sessionID this.sessionStatusMap.set(sid, event.properties.status.type) + this.aborts.observe(sid, event.properties.status.type, directory) const msg = mapSSEEventToWebviewMessage(event, sid) if (msg) { this.streams.flush(sid) @@ -3188,6 +3180,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "server.instance.disposed") { const props = event.properties as Record | null const dir = typeof props?.directory === "string" ? props.directory : undefined + if (dir) for (const sid of this.aborts.dispose(dir)) this.sessionStatusMap.set(sid, "idle") if (dir && !sameDirectory(dir, this.getWorkspaceDirectory())) return void this.reloadAfterAuthChange() return @@ -3600,6 +3593,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.trackedSessionIds.clear() this.syncedChildSessions.clear() this.sessionDirectories.clear() + this.aborts.clear() this.sessionStatusMap.clear() this.ignoreController?.dispose() this.chatAutocomplete?.dispose() diff --git a/packages/kilo-vscode/src/kilo-provider/abort.ts b/packages/kilo-vscode/src/kilo-provider/abort.ts index 8d3d00676e9..7bb355b5b26 100644 --- a/packages/kilo-vscode/src/kilo-provider/abort.ts +++ b/packages/kilo-vscode/src/kilo-provider/abort.ts @@ -1,4 +1,69 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" +import type { KiloClient, SessionStatus } from "@kilocode/sdk/v2/client" +import { sameDirectory } from "../kilo-provider-utils" + +export class SessionAbort { + private active = new Map>() + + observe(sessionID: string, status: SessionStatus["type"], dir?: string) { + if (!dir) return + const dirs = this.active.get(sessionID) + if (status === "idle") { + if (!dirs) return + for (const entry of dirs) { + if (sameDirectory(entry, dir)) dirs.delete(entry) + } + if (dirs.size === 0) this.active.delete(sessionID) + return + } + if (!dirs) { + this.active.set(sessionID, new Set([dir])) + return + } + if (![...dirs].some((entry) => sameDirectory(entry, dir))) dirs.add(dir) + } + + preserve(sessionID: string, status: SessionStatus["type"] | undefined, dir: string) { + if (!status || status === "idle" || this.active.has(sessionID)) return + this.observe(sessionID, status, dir) + } + + async stop(client: KiloClient, sessionID: string, fallback: string) { + const known = this.active.has(sessionID) + const dirs = [...(this.active.get(sessionID) ?? [])] + if (!dirs.some((dir) => sameDirectory(dir, fallback))) dirs.push(fallback) + const results = await Promise.allSettled(dirs.map((dir) => abortSession({ client, sessionID, dir }))) + const failures = results.flatMap((result, index) => + result.status === "rejected" ? [{ dir: dirs[index], error: result.reason }] : [], + ) + if (failures.length > 0) { + console.error("[Kilo New] KiloProvider: Failed to abort session in one or more directories:", failures) + return false + } + if (known) this.active.delete(sessionID) + return known + } + + dispose(dir: string) { + const idle: string[] = [] + for (const [sessionID, dirs] of this.active) { + for (const entry of dirs) { + if (sameDirectory(entry, dir)) dirs.delete(entry) + } + if (dirs.size > 0) continue + this.active.delete(sessionID) + idle.push(sessionID) + } + return idle + } + + delete(sessionID: string) { + this.active.delete(sessionID) + } + + clear() { + this.active.clear() + } +} export async function abortSession(input: { client: KiloClient; sessionID: string; dir: string }) { await input.client.session.abort({ sessionID: input.sessionID, directory: input.dir }, { throwOnError: true }) diff --git a/packages/kilo-vscode/tests/unit/abort.test.ts b/packages/kilo-vscode/tests/unit/abort.test.ts index 126333e6a4e..e971a1211b8 100644 --- a/packages/kilo-vscode/tests/unit/abort.test.ts +++ b/packages/kilo-vscode/tests/unit/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import type { KiloClient } from "@kilocode/sdk/v2/client" -import { abortSession } from "../../src/kilo-provider/abort" +import { abortSession, SessionAbort } from "../../src/kilo-provider/abort" function client(calls: unknown[], fail = false) { return { @@ -14,6 +14,53 @@ function client(calls: unknown[], fail = false) { } as unknown as KiloClient } +describe("SessionAbort", () => { + it("stops the active owner and current mapped directory", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(true) + expect(calls).toEqual([ + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo" }, + opts: { throwOnError: true }, + }, + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo/worktree" }, + opts: { throwOnError: true }, + }, + ]) + }) + + it("forgets an owner when its instance becomes idle", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo") + aborts.observe("session_1", "idle", "/repo") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(false) + expect(calls).toEqual([ + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo/worktree" }, + opts: { throwOnError: true }, + }, + ]) + }) + + it("deduplicates equivalent directory paths", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo/worktree") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree/.")).toBe(true) + expect(calls).toHaveLength(1) + }) +}) + describe("abortSession", () => { it("calls session.abort with the session id and directory", async () => { const calls: unknown[] = [] diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts index 3145932a2ca..a909cbe407c 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "bun:test" +import { describe, it, expect, spyOn } from "bun:test" import type { PartUpdate } from "../../src/shared/stream-messages" // vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) @@ -44,12 +44,15 @@ function createClient(options?: { deleteDeferred?: Deferred sessionData?: unknown sessionGet?: (params: { sessionID: string; directory?: string }) => Promise<{ data: unknown }> + abortFailures?: string[] }) { const calls: { before?: string; limit?: number }[] = [] const stopped: { sessionID: string; directory?: string }[] = [] + const aborted: { sessionID: string; directory?: string }[] = [] return { calls, stopped, + aborted, session: { list: async () => ({ data: [] }), get: async (params: { sessionID: string; directory?: string }) => { @@ -57,6 +60,11 @@ function createClient(options?: { return { data: options?.sessionData ?? null } }, status: async () => ({ data: {} }), + abort: async (params: { sessionID: string; directory?: string }) => { + aborted.push(params) + if (params.directory && options?.abortFailures?.includes(params.directory)) throw new Error("abort failed") + return { data: true } + }, messages: async (params: { before?: string; limit?: number }) => { calls.push({ before: params.before, limit: params.limit }) if (options?.messagesDeferred) return options.messagesDeferred.promise @@ -118,7 +126,8 @@ type ProviderInternals = { trackedSessionIds: Set streams: { push: (msg: PartUpdate) => void } stopCurrentSessionProcesses: (next?: string) => void - handleEvent: (event: unknown) => void + handleEvent: (event: unknown, directory?: string) => void + handleAbort: (sid?: string) => Promise handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise handleDeleteSession: (sid: string) => Promise } @@ -137,6 +146,70 @@ function makeProvider(client: ReturnType) { return { provider, internal, sent } } +describe("KiloProvider.handleAbort", () => { + it("aborts the original owner after a running session moves to a worktree", async () => { + const client = createClient() + const { provider, internal, sent } = makeProvider(client) + internal.handleEvent( + { + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }, + "/repo", + ) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + expect(sent.at(-1)).toMatchObject({ type: "sessionStatus", sessionID: "s1", status: "idle" }) + }) + + it("preserves the original owner when the status event lacks a directory", async () => { + const client = createClient() + const { provider, internal } = makeProvider(client) + internal.handleEvent({ + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + }) + + it("attempts every owner and stays busy when one abort fails", async () => { + const error = spyOn(console, "error").mockImplementation(() => {}) + const client = createClient({ abortFailures: ["/repo"] }) + const { provider, internal, sent } = makeProvider(client) + internal.handleEvent( + { + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }, + "/repo", + ) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + expect(sent.at(-1)).toMatchObject({ type: "sessionStatus", sessionID: "s1", status: "busy" }) + expect(error).toHaveBeenCalledTimes(1) + error.mockRestore() + }) +}) + describe("KiloProvider.handleLoadMessages / focus mode freshness", () => { it("stops background processes for the previous session when switching sessions", async () => { const client = createClient({ diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index cd5e98c0e93..41e4cbd25f2 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -201,8 +201,9 @@ describe("Bash tool static terminal preview (source)", () => { expect(block).toContain("BashHighlightedOutput") }) - it("animates expanded bash details", () => { - expect(block).toMatch(/allowPendingToggle\s+animated\s+trigger=/) + it("does not animate expanded bash details", () => { + expect(block).toMatch(/allowPendingToggle\s+trigger=/) + expect(block).not.toMatch(/allowPendingToggle\s+animated/) }) it("BashHighlightedOutput syntax highlights the command next to the prompt", () => { diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts b/packages/kilo-vscode/tests/visual-regression.spec.ts index 2fa3fc5c4af..7eb8febaf1f 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts @@ -45,6 +45,30 @@ async function disableAnimations(page: Page) { }) } +async function settle(page: Page) { + const frames = () => + page.evaluate( + () => + new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + }), + ) + + await page.evaluate(async () => { + await document.fonts.ready + }) + await frames() + await page.waitForFunction( + () => { + const root = document.querySelector("#storybook-root") + return root && !root.querySelector('pre > code[data-lang]:not([data-lang="mermaid"])') + }, + undefined, + { timeout: 5_000 }, + ) + await frames() +} + // Stories to skip from visual regression (add IDs here if needed) // Spinner animation captures at an indeterminate frame, causing flaky diffs. // Permission dock config-preloaded has non-deterministic toggle rendering. @@ -97,6 +121,7 @@ for (const story of stories) { ) await disableAnimations(page) await page.waitForSelector("#storybook-root *", { state: "attached" }) + await settle(page) const [component, variant] = story.id.split("--") const root = page.locator("#storybook-root") diff --git a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx index 8c722585e6a..67deb6f99c9 100644 --- a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx @@ -11,6 +11,16 @@ import { import { useVSCode } from "./vscode" import type { KilocodeNotification, ExtensionMessage } from "../types/messages" +// Static notifications always shown unconditionally (not fetched from API) +const STATIC_NOTIFICATIONS: KilocodeNotification[] = [ + { + id: "star-giveaway-june-2026", + title: "GitHub Star Giveaway", + message: "We're giving away $500 of AI Credits when we reach 25,000 stars on GitHub. Support us:", + action: { actionText: "github.com/Kilo-Org/kilocode", actionURL: "https://github.com/Kilo-Org/kilocode/" }, + }, +] + interface NotificationsContextValue { notifications: Accessor filteredNotifications: Accessor @@ -53,7 +63,8 @@ export const NotificationsProvider: ParentComponent = (props) => { const filteredNotifications = createMemo(() => { const dismissed = dismissedIds() - return notifications().filter((n) => !dismissed.includes(n.id)) + const all = [...STATIC_NOTIFICATIONS, ...notifications()] + return all.filter((n) => !dismissed.includes(n.id)) }) const dismiss = (id: string) => { diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index 5621f65d837..7cf632ffdcf 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -353,7 +353,7 @@ export namespace KilocodeConfig { .catch(() => "") const data = parseJsonc(text) ?? {} configs.push({ file, data }) - if (isRecord(data.permission) && data.permission.bash) return + if (typeof data.permission === "string" || (isRecord(data.permission) && data.permission.bash)) return } // A schema-only file is generated for editor completion. It does not mean diff --git a/packages/opencode/src/kilocode/server/httpapi/server.ts b/packages/opencode/src/kilocode/server/httpapi/server.ts index e8e1e97879c..3c6078e9d17 100644 --- a/packages/opencode/src/kilocode/server/httpapi/server.ts +++ b/packages/opencode/src/kilocode/server/httpapi/server.ts @@ -1,4 +1,10 @@ import { Layer } from "effect" +import { FetchHttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" +import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors" +import { compressionLayer } from "@/server/routes/instance/httpapi/middleware/compression" +import { corsVaryFix } from "@/server/routes/instance/httpapi/middleware/cors-vary" +import { errorLayer } from "@/server/routes/instance/httpapi/middleware/error" +import { fenceLayer } from "@/server/routes/instance/httpapi/middleware/fence" import { agentBuilderHandlers } from "./handlers/agent-builder" import { backgroundProcessHandlers } from "./handlers/background-process" @@ -29,3 +35,23 @@ export const provide = Layer.provide([ suggestionHandlers, telemetryHandlers, ]) + +export function provideListener(opts?: CorsOptions) { + const cors = HttpRouter.middleware( + HttpMiddleware.cors({ + allowedOrigins: (origin) => isAllowedCorsOrigin(origin, opts), + maxAge: 86_400, + }), + { global: true }, + ) + return Layer.provide([ + errorLayer, + compressionLayer, + corsVaryFix, + fenceLayer, + cors, + FetchHttpClient.layer, + HttpServer.layerServices, + Layer.succeed(CorsConfig)(opts), + ]) +} diff --git a/packages/opencode/src/kilocode/server/listener.ts b/packages/opencode/src/kilocode/server/listener.ts new file mode 100644 index 00000000000..3981a96b290 --- /dev/null +++ b/packages/opencode/src/kilocode/server/listener.ts @@ -0,0 +1,8 @@ +import { AppLayer } from "@/effect/app-runtime" +import { memoMap } from "@opencode-ai/core/effect/memo-map" +import { Layer, Scope } from "effect" + +export function build(layer: Layer.Layer, scope: Scope.Scope) { + // Keep listener transport state fresh while AppLayer reuses the process-wide services. + return Layer.buildWithMemoMap(Layer.fresh(layer).pipe(Layer.provide(AppLayer)), memoMap, scope) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index daa799b7a8b..c110d06b368 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -8,5 +8,5 @@ import { sessionHandlers } from "./v2/session" export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers, modelHandlers, providerHandlers).pipe( Layer.provide(v2LocationLayer), - Layer.provide(SessionV2.defaultLayer), + Layer.provide(SessionV2.layer), // kilocode_change - use the application EventV2Bridge ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 331b3106794..dc56feb716b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -82,7 +82,12 @@ import { syncHandlers } from "./handlers/sync" import { tuiHandlers } from "./handlers/tui" import { v2Handlers } from "./handlers/v2" import { workspaceHandlers } from "./handlers/workspace" -import { provide as provideKiloHttpApiHandlers } from "@/kilocode/server/httpapi/server" // kilocode_change +// kilocode_change start +import { + provide as provideKiloHttpApiHandlers, + provideListener as provideKiloListenerRoutes, +} from "@/kilocode/server/httpapi/server" +// kilocode_change end import { instanceContextLayer, instanceRouterMiddleware } from "./middleware/instance-context" import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/workspace-routing" import { disposeMiddleware } from "./lifecycle" @@ -250,6 +255,14 @@ export function createRoutes( ) } +// kilocode_change start - keep listener routes local while application services come from AppRuntime +export function createListenerRoutes(corsOptions?: CorsOptions) { + return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, docRoute, uiRoute).pipe( + provideKiloListenerRoutes(corsOptions), + ) +} +// kilocode_change end + export const routes = createRoutes() export const webHandler = lazy(() => diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 9a448b78d62..81f5e55c4e9 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -13,6 +13,7 @@ import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker" import { PublicApi } from "./routes/instance/httpapi/public" import type { CorsOptions } from "./cors" import { lazy } from "@/util/lazy" +import * as KiloListener from "@/kilocode/server/listener" // kilocode_change // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -101,7 +102,7 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect Scope.close(scope, Exit.void).pipe(Effect.ignore)), Effect.map( diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 783227ca4f6..622e8254909 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -425,7 +425,12 @@ export const layer: Layer.Layer = Effect.gen(function* () { yield* add() const result = yield* git( - [...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])], + // kilocode_change start + [ + ...quote, + ...args(["diff", "--cached", "--no-ext-diff", "--no-renames", "--name-only", hash, "--", "."]), + ], + // kilocode_change end { cwd: state.directory, }, diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 71145cfdd0c..b501c54f8c5 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -1,4 +1,3 @@ -// kilocode_change - new file import { afterEach, describe, expect, test } from "bun:test" import { Effect, Layer, Option, Schema } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" @@ -13,6 +12,7 @@ import { Account } from "../../../src/account/account" import { Auth } from "../../../src/auth" import { Config } from "../../../src/config/config" import { ConfigMarkdown } from "../../../src/config/markdown" +import { ConfigParse } from "../../../src/config/parse" import { Env } from "../../../src/env" import { KiloIndexing } from "../../../src/kilocode/indexing" import { KilocodeConfig } from "../../../src/kilocode/config/config" @@ -409,3 +409,106 @@ describe("agent config", () => { } }) }) + +describe("bash permission migration", () => { + for (const action of ["allow", "ask", "deny"] as const) { + test(`preserves string-form ${action} permission in jsonc`, async () => { + const input = `{ + "$schema": "https://app.kilo.ai/config.json", + "permission": "${action}" +}` + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write(path.join(dir, "kilo.jsonc"), input) + }, + }) + + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = tmp.path + await clear() + await disposeAllInstances() + + try { + await KilocodeConfig.migrateBashPermission() + + const file = path.join(tmp.path, "kilo.jsonc") + const text = await Filesystem.readText(file) + const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(text, file), file) + expect(text).toBe(input) + expect(parsed.permission?.["*"]).toBe(action) + expect(parsed.permission?.bash).toBeUndefined() + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + + test(`preserves string-form ${action} permission in json`, async () => { + const input = JSON.stringify({ + $schema: "https://app.kilo.ai/config.json", + permission: action, + }) + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write(path.join(dir, "kilo.json"), input) + }, + }) + + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = tmp.path + await clear() + await disposeAllInstances() + + try { + await KilocodeConfig.migrateBashPermission() + + const file = path.join(tmp.path, "kilo.json") + const text = await Filesystem.readText(file) + const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(text, file), file) + expect(text).toBe(input) + expect(parsed.permission?.["*"]).toBe(action) + expect(parsed.permission?.bash).toBeUndefined() + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + } + + test("migrates object-form global permission in jsonc", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "kilo.jsonc"), + `{ + "$schema": "https://app.kilo.ai/config.json", + "permission": { + "read": "allow" + } +}`, + ) + }, + }) + + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = tmp.path + await clear() + await disposeAllInstances() + + try { + await KilocodeConfig.migrateBashPermission() + + const file = path.join(tmp.path, "kilo.jsonc") + const text = await Filesystem.readText(file) + const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(text, file), file) + expect(parsed.permission?.read).toBe("allow") + expect(parsed.permission?.bash).toBe("allow") + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) +}) diff --git a/packages/opencode/test/kilocode/server/listener-runtime.test.ts b/packages/opencode/test/kilocode/server/listener-runtime.test.ts new file mode 100644 index 00000000000..c3bd6c78bf3 --- /dev/null +++ b/packages/opencode/test/kilocode/server/listener-runtime.test.ts @@ -0,0 +1,67 @@ +import { afterEach, expect, test } from "bun:test" +import { Flag } from "@opencode-ai/core/flag/flag" +import * as Log from "@opencode-ai/core/util/log" +import { Effect } from "effect" +import { AppRuntime } from "../../../src/effect/app-runtime" +import { InstanceRef } from "../../../src/effect/instance-ref" +import { Server } from "../../../src/server/server" +import { SessionPaths } from "../../../src/server/routes/instance/httpapi/groups/session" +import { SessionRunState } from "../../../src/session/run-state" +import { SessionID } from "../../../src/session/schema" +import { withTimeout } from "../../../src/util/timeout" +import { resetDatabase } from "../../fixture/db" +import { disposeAllInstances, reloadTestInstance, tmpdir } from "../../fixture/fixture" + +void Log.init({ print: false }) + +const previous = { + flag: Flag.KILO_SERVER_PASSWORD, + env: process.env.KILO_SERVER_PASSWORD, +} + +afterEach(async () => { + Flag.KILO_SERVER_PASSWORD = previous.flag + if (previous.env === undefined) delete process.env.KILO_SERVER_PASSWORD + else process.env.KILO_SERVER_PASSWORD = previous.env + await disposeAllInstances() + await resetDatabase() +}) + +test("listener aborts shared session runners", async () => { + Flag.KILO_SERVER_PASSWORD = undefined + delete process.env.KILO_SERVER_PASSWORD + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const ctx = await reloadTestInstance({ directory: tmp.path }) + const sessionID = SessionID.descending() + const started = Promise.withResolvers() + const stopped = Promise.withResolvers() + const running = AppRuntime.runPromise( + SessionRunState.Service.use((state) => + state.ensureRunning( + sessionID, + Effect.interrupt, + Effect.sync(started.resolve).pipe(Effect.andThen(Effect.never), Effect.ensuring(Effect.sync(stopped.resolve))), + ), + ).pipe(Effect.provideService(InstanceRef, ctx)), + ).catch(() => undefined) + + try { + await withTimeout(started.promise, 5_000, "timed out waiting for shared session") + const listener = await Server.listen({ hostname: "127.0.0.1", port: 0 }) + try { + const response = await fetch(new URL(SessionPaths.abort.replace(":sessionID", sessionID), listener.url), { + method: "POST", + headers: { "x-kilo-directory": tmp.path }, + }) + expect(response.status).toBe(200) + await withTimeout(stopped.promise, 5_000, "listener did not interrupt the shared session") + } finally { + await withTimeout(listener.stop(true), 10_000, "timed out cleaning up shared-runtime listener") + } + } finally { + await AppRuntime.runPromise( + SessionRunState.Service.use((state) => state.cancel(sessionID)).pipe(Effect.provideService(InstanceRef, ctx)), + ).catch(() => undefined) + await running + } +}, 20_000) diff --git a/packages/opencode/test/kilocode/snapshot-revert-move.test.ts b/packages/opencode/test/kilocode/snapshot-revert-move.test.ts new file mode 100644 index 00000000000..965c79090ce --- /dev/null +++ b/packages/opencode/test/kilocode/snapshot-revert-move.test.ts @@ -0,0 +1,42 @@ +import { expect } from "bun:test" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import fs from "fs/promises" +import path from "path" +import { Effect, Layer } from "effect" +import { Snapshot } from "../../src/snapshot" +import { TestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, AppFileSystem.defaultLayer)) +const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/") +const write = (file: string, content: string) => AppFileSystem.Service.use((fs) => fs.writeWithDirs(file, content)) +const read = (file: string) => AppFileSystem.Service.use((fs) => fs.readFileString(file)) +const exists = (file: string) => AppFileSystem.Service.use((fs) => fs.existsSafe(file)) +const mkdir = (dir: string) => AppFileSystem.Service.use((fs) => fs.ensureDir(dir)) + +it.instance( + "restores both paths after moving a file", + Effect.gen(function* () { + const tmp = yield* TestInstance + const snapshot = yield* Snapshot.Service + const source = path.join(tmp.directory, "source", "file.txt") + const destination = path.join(tmp.directory, "moved folder", "file.txt") + + yield* write(source, "original content") + const before = yield* snapshot.track() + expect(before).toBeTruthy() + if (!before) throw new Error("snapshot tracking failed") + + yield* mkdir(path.dirname(destination)) + yield* Effect.promise(() => fs.rename(source, destination)) + + const patch = yield* snapshot.patch(before) + expect(patch.files).toContain(fwd(source)) + expect(patch.files).toContain(fwd(destination)) + + yield* snapshot.revert([patch]) + expect(yield* read(source)).toBe("original content") + expect(yield* exists(destination)).toBe(false) + }), + { git: true }, +) diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts index f3076f8f2df..dc8042c7614 100644 --- a/script/check-opencode-promise-facades.ts +++ b/script/check-opencode-promise-facades.ts @@ -35,6 +35,7 @@ const testAllow: Record = { "kilocode/session/platform-attribution.test.ts": { count: 3, reason: "existing runtime integration test" }, "kilocode/session-prompt-queue.test.ts": { count: 5, reason: "prompt queue legacy instance bridge regression" }, "server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" }, + "kilocode/server/listener-runtime.test.ts": { count: 3, reason: "listener and AppRuntime integration test" }, "tool/recall.test.ts": { count: 11, reason: "existing runtime integration test" }, }