diff --git a/.changeset/fair-gateway-statuses.md b/.changeset/fair-gateway-statuses.md new file mode 100644 index 00000000000..98724d132c8 --- /dev/null +++ b/.changeset/fair-gateway-statuses.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Preserve upstream error statuses for cloud session and KiloClaw gateway requests. diff --git a/.changeset/nullable-kilo-gateway-sdk.md b/.changeset/nullable-kilo-gateway-sdk.md new file mode 100644 index 00000000000..3b9c4b0b8c9 --- /dev/null +++ b/.changeset/nullable-kilo-gateway-sdk.md @@ -0,0 +1,5 @@ +--- +"@kilocode/sdk": patch +--- + +Preserve nullable Kilo gateway response fields in the generated TypeScript SDK. diff --git a/.changeset/restore-console-daemon.md b/.changeset/restore-console-daemon.md new file mode 100644 index 00000000000..e2a921f6674 --- /dev/null +++ b/.changeset/restore-console-daemon.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Restore `kilo console` startup in packaged CLI builds. diff --git a/.changeset/restore-mercury-next-edit.md b/.changeset/restore-mercury-next-edit.md new file mode 100644 index 00000000000..1ed5c75bb5b --- /dev/null +++ b/.changeset/restore-mercury-next-edit.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Restore Kilo Gateway-backed Mercury Next Edit completions. diff --git a/.github/workflows/codeql-kotlin.yml b/.github/workflows/codeql-kotlin.yml new file mode 100644 index 00000000000..5f0cec56c87 --- /dev/null +++ b/.github/workflows/codeql-kotlin.yml @@ -0,0 +1,62 @@ +# kilocode_change - new file +name: "CodeQL Kotlin" + +on: + push: + branches: ["main"] + paths: + - "packages/kilo-jetbrains/**" + - ".github/workflows/codeql-kotlin.yml" + pull_request: + branches: ["main"] + paths: + - "packages/kilo-jetbrains/**" + - ".github/workflows/codeql-kotlin.yml" + schedule: + - cron: "29 10 * * 5" + workflow_dispatch: + +jobs: + analyze: + name: Analyze (java-kotlin) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: java-kotlin + build-mode: manual + config-file: ./.github/codeql/codeql-config.yml + + - name: Build Java/Kotlin + shell: bash + run: ./gradlew typecheck --rerun-tasks --no-build-cache + working-directory: packages/kilo-jetbrains + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:java-kotlin" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b37055f328e..86b7a69a053 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,11 +14,11 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '29 10 * * 5' + - cron: "29 10 * * 5" workflow_dispatch: jobs: @@ -45,12 +45,10 @@ jobs: fail-fast: false matrix: include: - - language: actions - build-mode: none - - language: java-kotlin - build-mode: manual - - language: javascript-typescript - build-mode: none + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -60,58 +58,24 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v6 - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - name: Setup Bun - if: matrix.language == 'java-kotlin' - uses: ./.github/actions/setup-bun + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - - name: Setup Java - if: matrix.language == 'java-kotlin' - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - - name: Setup Gradle - if: matrix.language == 'java-kotlin' - uses: gradle/actions/setup-gradle@v4 - with: - cache-read-only: ${{ github.ref != 'refs/heads/main' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - config-file: ./.github/codeql/codeql-config.yml - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Build Java/Kotlin - if: matrix.language == 'java-kotlin' - shell: bash - run: ./gradlew typecheck - working-directory: packages/kilo-jetbrains - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/bun.lock b/bun.lock index a921861a7d9..6d2a5d1aeea 100644 --- a/bun.lock +++ b/bun.lock @@ -711,7 +711,7 @@ "@types/bun": "1.3.12", "@types/cross-spawn": "6.0.6", "@types/luxon": "3.7.1", - "@types/node": "24.12.2", + "@types/node": "24.12.4", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20260316.1", "ai": "6.0.168", @@ -2199,7 +2199,7 @@ "@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="], - "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -4535,6 +4535,8 @@ "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "@anthropic-ai/sdk/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4599,6 +4601,8 @@ "@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@manypkg/find-root/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -4739,6 +4743,30 @@ "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@types/cacache/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/cross-spawn/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/mssql/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/node-fetch/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/npm-registry-fetch/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/npmcli__arborist/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/npmlog/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/pacote/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/qrcode/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/readable-stream/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/ssri/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + + "@types/ws/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -4777,6 +4805,8 @@ "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "apache-arrow/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -4811,6 +4841,8 @@ "buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "bun-types/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -5025,6 +5057,8 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "protobufjs/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -5095,6 +5129,8 @@ "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tedious/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "tedious/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], @@ -5179,6 +5215,8 @@ "@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], + "@morphllm/morphsdk/openai/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -5471,6 +5509,8 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "kilo-code/openai/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], diff --git a/nix/hashes.json b/nix/hashes.json index c120a3334f6..73c2a024745 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-b1eputqQyEDXT8LRZcGtx31TLm9wjUHHPJ8IivkPihY=", - "aarch64-linux": "sha256-0FGGe2pztqcWcMw1Ie+QlNTmJv0VM7vXTmCZ18M70hk=", - "aarch64-darwin": "sha256-McaXAipKk7yv2Ia1Py6Zp/WH2kDwRroy1/tnavbDYb4=", - "x86_64-darwin": "sha256-ARngNvK5UwQYZa5Uvx7o/cKWaQWY5/AlSXecX0UdtDM=" + "x86_64-linux": "sha256-6hzvHtqno/S86gUYai5ijRocbhWD2L5JnGCD65ZZPCQ=", + "aarch64-linux": "sha256-tNRoMGOkAcbAzSeD3MgSwU9xmCoQBrndDbPwn0c9Www=", + "aarch64-darwin": "sha256-0DTDN6RKDPo2LibZhLcVocdpO2PWW+3PMWK8e4Sxh3E=", + "x86_64-darwin": "sha256-s3flZ06pIrKD59XBH9NMpBl/k1Bj6tJwjZ/gjX50O4E=" } } diff --git a/package.json b/package.json index 2c24470b0cf..cf4c47eaaa5 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", - "@types/node": "24.12.2", + "@types/node": "24.12.4", "@types/semver": "7.7.1", "@tsconfig/node22": "22.0.2", "@tsconfig/bun": "1.0.9", diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 36e2d728f1c..4665df73512 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -49,7 +49,9 @@ "sonnet", "anthropic", "openai", - "zoo code" + "zoo code", + "opencode", + "open code" ], "activationEvents": [ "onStartupFinished" diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index a142f5dd81d..9abcb7bb52a 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -23,14 +23,14 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"), path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"), - path.join(ROOT, "webview-ui/agent-manager/FullScreenDiffView.tsx"), - path.join(ROOT, "webview-ui/agent-manager/MarkdownDiffView.tsx"), - path.join(ROOT, "webview-ui/agent-manager/MarkdownAnnotationLayer.tsx"), - path.join(ROOT, "webview-ui/agent-manager/markdown-comment-ranges.ts"), - path.join(ROOT, "webview-ui/agent-manager/DiffEndMarker.tsx"), - path.join(ROOT, "webview-ui/agent-manager/FileTree.tsx"), - path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"), - path.join(ROOT, "webview-ui/agent-manager/review-annotation-speech.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/MarkdownDiffView.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/markdown-comment-ranges.ts"), + path.join(ROOT, "webview-ui/diff-viewer/DiffEndMarker.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/FileTree.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/review-annotations.ts"), + path.join(ROOT, "webview-ui/diff-viewer/review-annotation-speech.tsx"), path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"), path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), 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 4c88fdceb7a..8e80715a477 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,5 +1,5 @@ import { describe, expect, it } from "bun:test" -import { mergeWorktreeDiffs } from "../../webview-ui/agent-manager/diff-state" +import { mergeWorktreeDiffs } from "../../webview-ui/diff-viewer/diff-state" import { EAGER_DIFF_REVIEW_LINES, EXTREME_DIFF_CHANGED_LINES, @@ -8,7 +8,7 @@ import { expandableOpenFiles, initialOpenFiles, toggleOpenFiles, -} from "../../webview-ui/agent-manager/diff-open-policy" +} from "../../webview-ui/diff-viewer/diff-open-policy" import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages" function diff(overrides: Partial): WorktreeFileDiff { diff --git a/packages/kilo-vscode/tests/unit/diff-viewer-css-arch.test.ts b/packages/kilo-vscode/tests/unit/diff-viewer-css-arch.test.ts index ce6614c519b..f854b92e219 100644 --- a/packages/kilo-vscode/tests/unit/diff-viewer-css-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-viewer-css-arch.test.ts @@ -1,5 +1,5 @@ /** - * Architecture test: FullScreenDiffView CSS co-location. + * Architecture test: FullScreenDiffView CSS imports. * * `FullScreenDiffView` and its children (`FileTree`, etc.) rely on classes * defined in BOTH `agent-manager.css` and `agent-manager-review.css`. The @@ -22,10 +22,10 @@ import fs from "node:fs" import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") -const FULL_SCREEN_DIFF_VIEW = path.join(ROOT, "webview-ui/agent-manager/FullScreenDiffView.tsx") -const REQUIRED = ["./agent-manager.css", "./agent-manager-review.css"] as const +const FULL_SCREEN_DIFF_VIEW = path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx") +const REQUIRED = ["../agent-manager/agent-manager.css", "../agent-manager/agent-manager-review.css"] as const -describe("FullScreenDiffView — CSS co-location", () => { +describe("FullScreenDiffView — CSS imports", () => { it("imports every stylesheet required to render correctly", () => { const src = fs.readFileSync(FULL_SCREEN_DIFF_VIEW, "utf-8") const missing = REQUIRED.filter((css) => !src.includes(`import "${css}"`)) diff --git a/packages/kilo-vscode/tests/unit/file-tree.test.ts b/packages/kilo-vscode/tests/unit/file-tree.test.ts index a54908dab20..2f1c066f6d8 100644 --- a/packages/kilo-vscode/tests/unit/file-tree.test.ts +++ b/packages/kilo-vscode/tests/unit/file-tree.test.ts @@ -5,7 +5,7 @@ import { flattenChain, treeOrder, type FileTreeNode, -} from "../../webview-ui/agent-manager/file-tree-utils" +} from "../../webview-ui/diff-viewer/file-tree-utils" import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages" function diff(file: string, status?: "added" | "deleted" | "modified"): WorktreeFileDiff { diff --git a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts index 3354e61943c..f69cbdd92b7 100644 --- a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts +++ b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test" -import { isAnnotationMutation } from "../../webview-ui/agent-manager/markdown-annotation-mutation" +import { isAnnotationMutation } from "../../webview-ui/diff-viewer/markdown-annotation-mutation" function mutation(target: Node): Pick { return { target } diff --git a/packages/kilo-vscode/tests/unit/review-comments.test.ts b/packages/kilo-vscode/tests/unit/review-comments.test.ts index c3602ab1cda..91adacced9c 100644 --- a/packages/kilo-vscode/tests/unit/review-comments.test.ts +++ b/packages/kilo-vscode/tests/unit/review-comments.test.ts @@ -6,8 +6,8 @@ import { getDirectory, getFilename, type ReviewComment, -} from "../../webview-ui/agent-manager/review-comments" -import { markdownCommentBlocks } from "../../webview-ui/agent-manager/markdown-comment-ranges" +} from "../../webview-ui/diff-viewer/review-comments" +import { markdownCommentBlocks } from "../../webview-ui/diff-viewer/markdown-comment-ranges" import { buildFileAnnotations, clearReviewComposer, @@ -17,7 +17,7 @@ import { reviewComposerEdit, reviewDraftSpeechKey, reviewEditSpeechKey, -} from "../../webview-ui/agent-manager/review-annotations" +} from "../../webview-ui/diff-viewer/review-annotations" import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages" function diff(file: string, before: string, after: string): WorktreeFileDiff { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 37425b8501d..c29fcf82c08 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -103,11 +103,11 @@ import { renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-render import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" import { createRevertFile } from "./revert-file" -import { FullScreenDiffView } from "./FullScreenDiffView" +import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" import { ApplyDialog } from "./ApplyDialog" import { groupApplyConflicts } from "./apply-conflicts" -import type { ReviewComment } from "./review-comments" -import { clearReviewComposer, createReviewComposer } from "./review-annotations" +import type { ReviewComment } from "../diff-viewer/review-comments" +import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations" import { CurrentTabsMenu, createCurrentTabItems, focusCurrentTab } from "./CurrentTabsMenu" import { BranchSelect } from "../src/components/shared/BranchSelect" import { WorktreeItem } from "./WorktreeItem" @@ -125,7 +125,7 @@ import { } from "./section-helpers" import { sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" -import { mergeWorktreeDiffs } from "./diff-state" +import { mergeWorktreeDiffs } from "../diff-viewer/diff-state" import { initialMessage, seedInitialVariant } from "./initial-message" import { createMarkdownRender } from "./review-preferences" import { createSidebarCollapse } from "./sidebar-collapse" diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx index cbce7dbb7ab..b27ff31cdd1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx @@ -4,7 +4,7 @@ import { Button } from "@kilocode/kilo-ui/button" import { Spinner } from "@kilocode/kilo-ui/spinner" import type { AgentManagerApplyWorktreeDiffStatus, WorktreeFileDiff } from "../src/types/messages" import { useLanguage } from "../src/context/language" -import { FileTree } from "./FileTree" +import { FileTree } from "../diff-viewer/FileTree" import { mapApplyConflictReason, type ApplyConflictRow } from "./apply-conflicts" interface ApplyDialogProps { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index c32ff8cf607..a0a2bd68c3f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -20,7 +20,13 @@ import { useProvider } from "../src/context/provider" import { useConfig } from "../src/context/config" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" -import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments" +import { + getDirectory, + getFilename, + lineCount, + sanitizeReviewComments, + type ReviewComment, +} from "../diff-viewer/review-comments" import { buildFileAnnotations, buildReviewAnnotation, @@ -34,8 +40,8 @@ import { type AnnotationMeta, type ReviewComposer, type ReviewDraft, -} from "./review-annotations" -import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech" +} from "../diff-viewer/review-annotations" +import { createReviewAnnotationSpeechRenderer } from "../diff-viewer/review-annotation-speech" import { LONG_DIFF_MARKER_FILE_COUNT, allOpenFiles, @@ -43,11 +49,11 @@ import { initialOpenFiles, isLargeDiffFile, toggleOpenFiles, -} from "./diff-open-policy" -import { DiffEndMarker } from "./DiffEndMarker" -import { treeOrder } from "./file-tree-utils" -import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView" -import { createDiffRows, diffToken } from "./diff-state" +} from "../diff-viewer/diff-open-policy" +import { DiffEndMarker } from "../diff-viewer/DiffEndMarker" +import { treeOrder } from "../diff-viewer/file-tree-utils" +import { isMarkdownFile, MarkdownDiffView } from "../diff-viewer/MarkdownDiffView" +import { createDiffRows, diffToken } from "../diff-viewer/diff-state" // --- Data model --- diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffEndMarker.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffEndMarker.tsx similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/DiffEndMarker.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/DiffEndMarker.tsx diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index 795cec3b6eb..a60338c62c3 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -11,8 +11,8 @@ import { File } from "@kilocode/kilo-ui/file" import { Icon } from "@kilocode/kilo-ui/icon" import { ThemeProvider } from "@kilocode/kilo-ui/theme" import { Toast } from "@kilocode/kilo-ui/toast" -import { FullScreenDiffView } from "../agent-manager/FullScreenDiffView" -import { mergeWorktreeDiffs } from "../agent-manager/diff-state" +import { FullScreenDiffView } from "./FullScreenDiffView" +import { mergeWorktreeDiffs } from "./diff-state" import { LanguageProvider, useLanguage } from "../src/context/language" import { ServerProvider, useServer } from "../src/context/server" import { ConfigProvider } from "../src/context/config" diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FileTree.tsx similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/FileTree.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx similarity index 99% rename from packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index f0365924ee2..f31eedec301 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -1,9 +1,9 @@ import { type Component, createSignal, createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" -// Styles are co-located with the component so every consumer (sidebar diff viewer, -// agent manager, storybook) picks them up automatically. Do not move these out — +// Styles are imported by the component so every consumer (sidebar diff viewer, +// agent manager, storybook) picks them up automatically. Keep these imports here — // see tests/unit/diff-viewer-css-arch.test.ts for the invariant. -import "./agent-manager.css" -import "./agent-manager-review.css" +import "../agent-manager/agent-manager.css" +import "../agent-manager/agent-manager-review.css" import { Diff } from "@kilocode/kilo-ui/diff" import { Accordion } from "@kilocode/kilo-ui/accordion" import { StickyAccordionHeader } from "@kilocode/kilo-ui/sticky-accordion-header" diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/MarkdownDiffView.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/MarkdownDiffView.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/file-tree-utils.ts b/packages/kilo-vscode/webview-ui/diff-viewer/file-tree-utils.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/file-tree-utils.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/file-tree-utils.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/markdown-annotation-mutation.ts b/packages/kilo-vscode/webview-ui/diff-viewer/markdown-annotation-mutation.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/markdown-annotation-mutation.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/markdown-annotation-mutation.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/markdown-comment-ranges.ts b/packages/kilo-vscode/webview-ui/diff-viewer/markdown-comment-ranges.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/markdown-comment-ranges.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/markdown-comment-ranges.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-annotation-speech.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/review-annotation-speech.tsx rename to packages/kilo-vscode/webview-ui/diff-viewer/review-annotation-speech.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-comments.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-comments.ts similarity index 100% rename from packages/kilo-vscode/webview-ui/agent-manager/review-comments.ts rename to packages/kilo-vscode/webview-ui/diff-viewer/review-comments.ts diff --git a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx index 6a8614630e2..a9ccfdc1038 100644 --- a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx @@ -16,7 +16,7 @@ import { normalize } from "@kilocode/kilo-ui/session-diff" import { LanguageProvider, useLanguage } from "../src/context/language" import { ServerProvider, useServer } from "../src/context/server" import { getVSCodeAPI, VSCodeProvider } from "../src/context/vscode" -import { isMarkdownFile, MarkdownDiffView } from "../agent-manager/MarkdownDiffView" +import { isMarkdownFile, MarkdownDiffView } from "../diff-viewer/MarkdownDiffView" type DiffStyle = "unified" | "split" diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 36eca47f4af..e2f45756a34 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -6,9 +6,9 @@ import type { Meta, StoryObj } from "storybook-solidjs-vite" import { StoryProviders } from "./StoryProviders" -import { FileTree } from "../../agent-manager/FileTree" +import { FileTree } from "../../diff-viewer/FileTree" import { DiffPanel } from "../../agent-manager/DiffPanel" -import { FullScreenDiffView } from "../../agent-manager/FullScreenDiffView" +import { FullScreenDiffView } from "../../diff-viewer/FullScreenDiffView" import { WorktreeItem } from "../../agent-manager/WorktreeItem" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 700c7afcfdd..9694b29d272 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -126,7 +126,7 @@ function findBinary() { } // kilocode_change end -// kilocode_change start - copy tree-sitter WASM resources next to cached binary +// kilocode_change start - copy runtime resources next to cached binary function copyTreeSitterResources(binaryPath) { const source = path.join(path.dirname(binaryPath), "tree-sitter") const target = path.join(__dirname, "bin", "tree-sitter") @@ -137,6 +137,17 @@ function copyTreeSitterResources(binaryPath) { fs.rmSync(target, { recursive: true, force: true }) fs.cpSync(source, target, { recursive: true }) } + +function copyConsoleResources(binaryPath) { + const source = path.join(path.dirname(binaryPath), "console") + const target = path.join(__dirname, "bin", "console") + const index = path.join(source, "index.html") + + if (!fs.existsSync(index)) return + + fs.rmSync(target, { recursive: true, force: true }) + fs.cpSync(source, target, { recursive: true }) +} // kilocode_change end function main() { @@ -155,6 +166,7 @@ function main() { fs.copyFileSync(binaryPath, target) } copyTreeSitterResources(binaryPath) // kilocode_change + copyConsoleResources(binaryPath) // kilocode_change fs.chmodSync(target, 0o755) } diff --git a/packages/opencode/src/kilocode/daemon/daemon.ts b/packages/opencode/src/kilocode/daemon/daemon.ts index d214c485c3a..6bd0a60a3f1 100644 --- a/packages/opencode/src/kilocode/daemon/daemon.ts +++ b/packages/opencode/src/kilocode/daemon/daemon.ts @@ -213,11 +213,15 @@ export namespace Daemon { return await start(input) } - export function command(input?: string[]) { + export function command( + input?: string[], + proc = { argv: process.argv, execArgv: process.execArgv, execPath: process.execPath }, + ) { if (input?.length) return input - const script = process.argv[1] - if (script && /\.(ts|js|mjs|cjs)$/.test(script)) return [process.execPath, ...clean(process.execArgv), script] - return [process.execPath] + const script = proc.argv[1] + const bundled = script?.startsWith("/$bunfs/") || (script ? /^[A-Za-z]:[\\/]~BUN[\\/]/.test(script) : false) + if (script && !bundled && /\.(ts|js|mjs|cjs)$/.test(script)) return [proc.execPath, ...clean(proc.execArgv), script] + return [proc.execPath] } export function clean(input: string[]) { diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts index e77bebef2c3..e1ac80a0a77 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts @@ -23,10 +23,11 @@ import { DIRECT_FIM_ENV, requestMistralFim, resolveFimTarget } from "@kilocode/k import { DIRECT_EDIT_ENV, extractFencedBody, resolveEditTarget } from "@kilocode/kilo-gateway/edit" import { buildMercuryEditPrompt } from "@kilocode/kilo-gateway/edit-prompt" import { buildKiloHeaders } from "@kilocode/kilo-gateway" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import * as Stream from "effect/Stream" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" +import * as Log from "@opencode-ai/core/util/log" import { Auth } from "@/auth" import { EffectBridge } from "@/effect/bridge" import { Bus } from "@/bus" @@ -38,9 +39,18 @@ import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api" import { MessageTable, PartTable, SessionTable } from "@/session/session.sql" import { Session } from "@/session/session" import { Database } from "@/storage/db" -import { AudioTranscriptionsBody, EditBody, FimBody } from "../groups/kilo-gateway" +import { AudioTranscriptionsBody, ClawStatus, EditBody, FimBody } from "../groups/kilo-gateway" const FIM_TIMEOUT_MS = 30_000 +const log = Log.create({ service: "kilo-gateway" }) + +function jsonError(error: string, status: number) { + return HttpServerResponse.jsonUnsafe({ error }, { status }) +} + +function logError(route: string, err: unknown) { + log.error("unhandled error", { route, err }) +} export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", (handlers) => Effect.gen(function* () { @@ -158,14 +168,17 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", const edit = Effect.fn("KiloGatewayHttpApi.edit")(function* (ctx: { payload: typeof EditBody.Type }) { const target = resolveEditTarget(ctx.payload.provider, ctx.payload.model) - if (target.provider !== "inception") { + if (target.provider === "kilo" && !target.url) { return yield* Effect.fail(new HttpApiError.BadRequest({})) } + const proxy = target.provider === "kilo" ? yield* proxyAuth() : undefined const token = yield* Effect.gen(function* () { + if (target.provider === "kilo") return proxy?.token const item = yield* auth.get(target.provider).pipe(Effect.mapError(() => new HttpApiError.Unauthorized({}))) if (item?.type === "api") return item.key return DIRECT_EDIT_ENV[target.provider].map((key) => process.env[key]).find(Boolean) }) + if (target.provider === "kilo" && !proxy?.auth) return yield* Effect.fail(new HttpApiError.Unauthorized({})) if (!token) return yield* Effect.fail(new HttpApiError.Unauthorized({})) const request = yield* HttpServerRequest.HttpServerRequest @@ -188,27 +201,44 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", }) const response = yield* Effect.promise(async () => { - return fetch(target.url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - signal, - body: JSON.stringify({ - model: target.model, - max_tokens: ctx.payload.maxTokens ?? 512, - // Mercury rejects role:"system" on this endpoint — must be a single user message. - messages: [{ role: "user", content }], - }), - }) + try { + return await fetch(target.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + ...(target.provider === "kilo" + ? buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy?.organizationId }) + : {}), + ...(target.provider === "kilo" ? { [HEADER_FEATURE]: "autocomplete" } : {}), + }, + signal, + body: JSON.stringify({ + model: target.model, + max_tokens: ctx.payload.maxTokens ?? 512, + // Mercury rejects role:"system" on this endpoint — must be a single user message. + messages: [{ role: "user", content }], + }), + }) + } catch (err) { + if (err instanceof DOMException && err.name === "TimeoutError") + return Response.json({ error: "Edit request timed out" }, { status: 504 }) + if (signal.aborted) return Response.json({ error: "Edit request canceled" }, { status: 499 }) + throw err + } }) if (!response.ok) { // Pass the upstream status through (mirrors the FIM handler) so the // client can distinguish auth/credit/rate-limit/server failures // instead of collapsing everything to 400. - const text = yield* Effect.promise(() => response.text()) + const text = yield* Effect.promise(async () => { + try { + return await response.text() + } catch { + return "" + } + }) return HttpServerResponse.jsonUnsafe( { error: `Edit request failed: ${response.status} ${text}` }, { status: response.status }, @@ -314,13 +344,20 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", try: async () => { const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) if (!response.ok) throw new GatewayError(await response.text(), response.status) - return response.json() + return Schema.decodeUnknownPromise(ClawStatus)(await response.json()) }, - catch: (err) => - err instanceof GatewayError && err.status === 401 - ? new HttpApiError.Unauthorized({}) - : new HttpApiError.ServiceUnavailable({}), - }) + catch: (err) => err, + }).pipe( + Effect.match({ + onFailure: (err) => { + if (err instanceof GatewayError) + return jsonError(`KiloClaw request failed: ${err.status} ${err.message}`, err.status) + logError("claw/status", err) + return jsonError("Failed to reach KiloClaw", 502) + }, + onSuccess: (result) => result, + }), + ) }) const clawChatCredentials = Effect.fn("KiloGatewayHttpApi.clawChatCredentials")(function* () { @@ -349,11 +386,17 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", return yield* Effect.tryPromise({ try: () => getCloudSessions(token, query), - catch: (err) => - err instanceof GatewayError && err.status === 401 - ? new HttpApiError.Unauthorized({}) - : new HttpApiError.BadRequest({}), - }) + catch: (err) => err, + }).pipe( + Effect.match({ + onFailure: (err) => { + if (err instanceof GatewayError) return jsonError(err.message, err.status) + logError("cloud-sessions", err) + return jsonError("Internal error", 500) + }, + onSuccess: (result) => result, + }), + ) }) const cloudSession = Effect.fn("KiloGatewayHttpApi.cloudSession")(function* (ctx) { @@ -361,9 +404,19 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", const token = getToken(info) if (!token) return yield* Effect.fail(new HttpApiError.Unauthorized({})) - const result = yield* Effect.promise(() => fetchCloudSession(token, ctx.params.id)) - if (!result.ok && result.status === 404) return yield* Effect.fail(new HttpApiError.NotFound({})) - if (!result.ok) return yield* Effect.fail(new HttpApiError.Unauthorized({})) + const result = yield* Effect.tryPromise({ + try: () => fetchCloudSession(token, ctx.params.id), + catch: (err) => err, + }).pipe( + Effect.catch((err) => + Effect.sync(() => { + logError("cloud/session/get", err) + return undefined + }), + ), + ) + if (!result) return jsonError("Internal error", 500) + if (!result.ok) return jsonError(result.error, result.status) return result.data }) @@ -372,9 +425,19 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", const token = getToken(info) if (!token) return yield* Effect.fail(new HttpApiError.Unauthorized({})) - const fetched = yield* Effect.promise(() => fetchCloudSessionForImport(token, ctx.payload.sessionId)) - if (!fetched.ok && fetched.status === 404) return yield* Effect.fail(new HttpApiError.NotFound({})) - if (!fetched.ok) return yield* Effect.fail(new HttpApiError.BadRequest({})) + const fetched = yield* Effect.tryPromise({ + try: () => fetchCloudSessionForImport(token, ctx.payload.sessionId), + catch: (err) => err, + }).pipe( + Effect.catch((err) => + Effect.sync(() => { + logError("cloud/session/import", err) + return undefined + }), + ), + ) + if (!fetched) return jsonError("Internal error", 500) + if (!fetched.ok) return jsonError(fetched.error, fetched.status) if (!fetched.data?.info?.id) return yield* Effect.fail(new HttpApiError.BadRequest({})) const bridge = yield* EffectBridge.make() diff --git a/packages/opencode/src/kilocode/server/httpapi/public.ts b/packages/opencode/src/kilocode/server/httpapi/public.ts index 5de2211d033..78deb6d62b2 100644 --- a/packages/opencode/src/kilocode/server/httpapi/public.ts +++ b/packages/opencode/src/kilocode/server/httpapi/public.ts @@ -49,6 +49,26 @@ export function matchLegacyKiloOpenApi(input: Record) { const props = ref ? spec.components?.schemas?.[ref]?.properties : body?.properties if (props?.organizationId) props.organizationId = nullable(props.organizationId) + const json = (path: string) => spec.paths?.[path]?.get?.responses?.["200"]?.content?.["application/json"] + const profile = json("/kilo/profile")?.schema?.properties + if (profile?.balance) profile.balance = nullable(profile.balance) + if (profile?.currentOrgId) profile.currentOrgId = nullable(profile.currentOrgId) + + const sessions = json("/kilo/cloud-sessions")?.schema?.properties + const session = sessions?.cliSessions?.items?.properties + if (session?.title) session.title = nullable(session.title) + if (sessions?.nextCursor) sessions.nextCursor = nullable(sessions.nextCursor) + + const claw = json("/kilo/claw/status")?.schema?.properties + if (claw?.status) claw.status = nullable(claw.status) + if (claw?.openclawVersion) claw.openclawVersion = nullable(claw.openclawVersion) + if (claw?.lastStartedAt) claw.lastStartedAt = nullable(claw.lastStartedAt) + if (claw?.lastStoppedAt) claw.lastStoppedAt = nullable(claw.lastStoppedAt) + if (claw?.botName) claw.botName = nullable(claw.botName) + + const credentials = json("/kilo/claw/chat-credentials") + if (credentials?.schema) credentials.schema = nullable(credentials.schema) + const provider = spec.components?.schemas?.Config?.properties?.provider if (provider?.additionalProperties && typeof provider.additionalProperties === "object") provider.additionalProperties = nullable(provider.additionalProperties) diff --git a/packages/opencode/test/cli/install-artifact.test.ts b/packages/opencode/test/cli/install-artifact.test.ts index ed9e078d264..560f72d245d 100644 --- a/packages/opencode/test/cli/install-artifact.test.ts +++ b/packages/opencode/test/cli/install-artifact.test.ts @@ -7,6 +7,7 @@ import path from "path" const root = path.join(import.meta.dir, "..", "..") const wrapper = path.join(root, "bin", "kilo") +const postinstall = path.join(root, "script", "postinstall.mjs") describe("npm install artifact behavior", () => { test("keeps the CLI wrapper contract", async () => { @@ -17,6 +18,40 @@ describe("npm install artifact behavior", () => { expect(text).toContain("function findBinary(startDir)") }) + test("copies cached binary runtime resources during postinstall", async () => { + if (process.platform === "win32") return + const node = Bun.which("node") + if (!node) { + console.warn("Skipping postinstall artifact test: node is not available in PATH") + return + } + + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-postinstall-artifact-")) + try { + const pkg = path.join(tmp, "node_modules", "@kilocode", "cli") + const native = path.join(tmp, "node_modules", "@kilocode", `cli-${process.platform}-${process.arch}`) + const bin = path.join(native, "bin") + await fs.mkdir(path.join(pkg, "bin"), { recursive: true }) + await fs.mkdir(path.join(bin, "tree-sitter"), { recursive: true }) + await fs.mkdir(path.join(bin, "console", "assets"), { recursive: true }) + await fs.copyFile(postinstall, path.join(pkg, "postinstall.mjs")) + await Bun.write(path.join(native, "package.json"), JSON.stringify({ name: `@kilocode/cli-${process.platform}-${process.arch}` })) + await Bun.write(path.join(bin, "kilo"), "binary") + await Bun.write(path.join(bin, "tree-sitter", "tree-sitter.wasm"), "wasm") + await Bun.write(path.join(bin, "console", "index.html"), "console") + await Bun.write(path.join(bin, "console", "assets", "app.js"), "asset") + + const proc = Bun.spawn([node, path.join(pkg, "postinstall.mjs")], { cwd: pkg }) + expect(await proc.exited).toBe(0) + expect(await Bun.file(path.join(pkg, "bin", ".kilo")).text()).toBe("binary") + expect(await Bun.file(path.join(pkg, "bin", "tree-sitter", "tree-sitter.wasm")).text()).toBe("wasm") + expect(await Bun.file(path.join(pkg, "bin", "console", "index.html")).text()).toBe("console") + expect(await Bun.file(path.join(pkg, "bin", "console", "assets", "app.js")).text()).toBe("asset") + } finally { + await fs.rm(tmp, { recursive: true, force: true }) + } + }) + test("links npm bin commands to the wrapper during local install", async () => { const npmPath = Bun.which("npm") if (!npmPath) { diff --git a/packages/opencode/test/kilocode/daemon.test.ts b/packages/opencode/test/kilocode/daemon.test.ts index d1fb61efd04..bb53beebd03 100644 --- a/packages/opencode/test/kilocode/daemon.test.ts +++ b/packages/opencode/test/kilocode/daemon.test.ts @@ -68,6 +68,39 @@ describe("daemon manager", () => { expect(Daemon.clean(["--cwd=packages/opencode", "--conditions=browser"])).toStrictEqual(["--conditions=browser"]) }) + test("does not forward bundled bun entrypoints to the daemon child", () => { + const proc = { + argv: ["/tmp/kilo", "/$bunfs/root/src/index.js", "daemon", "start"], + execArgv: ["--user-agent=kilo/test", "--use-system-ca", "--"], + execPath: "/tmp/kilo", + } + expect(Daemon.command(undefined, proc)).toStrictEqual(["/tmp/kilo"]) + expect( + Daemon.command(undefined, { + ...proc, + argv: ["C:/tmp/kilo.exe", "B:/~BUN/root/src/index.js", "daemon", "start"], + execPath: "C:/tmp/kilo.exe", + }), + ).toStrictEqual(["C:/tmp/kilo.exe"]) + expect( + Daemon.command(undefined, { + ...proc, + argv: ["C:/tmp/kilo.exe", "b:\\~BUN\\root\\src\\index.js", "daemon", "start"], + execPath: "C:/tmp/kilo.exe", + }), + ).toStrictEqual(["C:/tmp/kilo.exe"]) + }) + + test("forwards source entrypoints to the daemon child", () => { + expect( + Daemon.command(undefined, { + argv: ["/tmp/bun", "/tmp/kilo/src/index.ts", "daemon", "start"], + execArgv: ["--conditions=browser"], + execPath: "/tmp/bun", + }), + ).toStrictEqual(["/tmp/bun", "--conditions=browser", "/tmp/kilo/src/index.ts"]) + }) + test("reuses one daemon across caller directories", async () => { await using tmp = await tmpdir() const env = opts(tmp.path) diff --git a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts index 232f483f4f3..4c89dbb863f 100644 --- a/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts +++ b/packages/opencode/test/kilocode/server/httpapi-exercise-scenarios.ts @@ -141,7 +141,7 @@ export const kiloScenarios: Scenario[] = [ http.protected .post("/kilo/edit", "kilo.edit") .at((ctx) => ({ path: "/kilo/edit", headers: ctx.headers(), body: edit })) - .status(400), + .status(401), http.protected .post("/kilo/audio/transcriptions", "kilo.audio.transcriptions") .at((ctx) => ({ diff --git a/packages/opencode/test/kilocode/server/httpapi-kilo-edit.test.ts b/packages/opencode/test/kilocode/server/httpapi-kilo-edit.test.ts new file mode 100644 index 00000000000..2223330b29b --- /dev/null +++ b/packages/opencode/test/kilocode/server/httpapi-kilo-edit.test.ts @@ -0,0 +1,221 @@ +import { afterEach, describe, expect, spyOn, test } from "bun:test" +import { ConfigProvider, Layer } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HEADER_FEATURE, HEADER_ORGANIZATIONID } from "@kilocode/kilo-gateway" +import * as Log from "@opencode-ai/core/util/log" +import { KiloGatewayPaths } from "../../../src/kilocode/server/httpapi/groups/kilo-gateway" +import { ExperimentalHttpApiServer } from "../../../src/server/routes/instance/httpapi/server" +import { resetDatabase } from "../../fixture/db" +import { disposeAllInstances, tmpdir } from "../../fixture/fixture" + +void Log.init({ print: false }) + +const env = { + KILO_AUTH_CONTENT: process.env.KILO_AUTH_CONTENT, + INCEPTION_API_KEY: process.env.INCEPTION_API_KEY, +} + +const edit = { + provider: "kilo", + model: "inception/mercury-next-edit", + currentFilePath: "src/index.ts", + currentFileContent: "export const value = 1\n", + cursorLine: 0, + cursorCharacter: 0, + editableRegionStartLine: 0, + editableRegionEndLine: 0, + recentlyViewedSnippets: [], + editDiffHistory: [], +} + +function app() { + const handler = HttpRouter.toWebHandler( + ExperimentalHttpApiServer.routes.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({})))), + { disableLogger: true }, + ).handler + + return { + request(input: string | URL | Request, init?: RequestInit) { + return handler( + input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init), + ExperimentalHttpApiServer.context, + ) + }, + } +} + +async function send(body: Record = edit, signal?: AbortSignal) { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + return app().request(KiloGatewayPaths.edit, { + method: "POST", + headers: { "content-type": "application/json", "x-kilo-directory": tmp.path }, + body: JSON.stringify(body), + signal, + }) +} + +function stub(run: (input: RequestInfo | URL, init?: RequestInit) => Promise) { + const fetch: typeof globalThis.fetch = Object.assign(run, { preconnect: globalThis.fetch.preconnect }) + return spyOn(globalThis, "fetch").mockImplementation(fetch) +} + +function url(input: RequestInfo | URL) { + if (typeof input === "string") return input + if (input instanceof URL) return input.href + return input.url +} + +function completion(input: RequestInfo | URL) { + return url(input).endsWith("/edit/completions") +} + +function authenticate() { + process.env.KILO_AUTH_CONTENT = JSON.stringify({ + kilo: { + type: "oauth", + refresh: "refresh-token", + access: "gateway-token", + expires: Date.now() + 60_000, + accountId: "org-1", + }, + }) +} + +function restore() { + for (const [key, value] of Object.entries(env)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } +} + +afterEach(async () => { + restore() + await disposeAllInstances() + await resetDatabase() +}) + +describe("HttpApi Kilo next edit", () => { + test("requires Kilo Gateway authentication for the Kilo-backed model", async () => { + process.env.KILO_AUTH_CONTENT = "{}" + expect((await send()).status).toBe(401) + }) + + test("rejects non-edit placeholder targets", async () => { + process.env.KILO_AUTH_CONTENT = "{}" + expect((await send({ ...edit, model: "mistralai/codestral-2508" })).status).toBe(400) + }) + + test("proxies Kilo-backed edits with gateway auth and autocomplete headers", async () => { + authenticate() + const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [] + const mock = stub(async (input, init) => { + if (!completion(input)) return Response.json([]) + calls.push({ input, init }) + return Response.json({ + choices: [{ message: { content: "```typescript\nexport const value = 2\n```" } }], + usage: { prompt_tokens: 12, completion_tokens: 6 }, + }) + }) + + try { + const response = await send() + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + content: "export const value = 2", + usage: { prompt_tokens: 12, completion_tokens: 6 }, + }) + expect(calls).toHaveLength(1) + const call = calls[0] + if (!call) throw new Error("missing edit request") + expect(url(call.input)).toEndWith("/api/edit/completions") + const headers = new Headers(call.init?.headers) + expect(headers.get("authorization")).toBe("Bearer gateway-token") + expect(headers.get(HEADER_ORGANIZATIONID)).toBe("org-1") + expect(headers.get(HEADER_FEATURE)).toBe("autocomplete") + if (typeof call.init?.body !== "string") throw new Error("missing edit request body") + const body: unknown = JSON.parse(call.init.body) + expect(body).toMatchObject({ model: "inception/mercury-edit-2", messages: [{ role: "user" }] }) + } finally { + mock.mockRestore() + } + }) + + test("preserves direct Inception BYOK edits", async () => { + process.env.KILO_AUTH_CONTENT = "{}" + process.env.INCEPTION_API_KEY = "inception-token" + const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [] + const mock = stub(async (input, init) => { + if (!completion(input)) return Response.json([]) + calls.push({ input, init }) + return Response.json({ choices: [{ message: { content: "```\nconst value = 2\n```" } }] }) + }) + + try { + const response = await send({ ...edit, provider: "inception", model: "mercury-next-edit" }) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ content: "const value = 2", usage: null }) + const call = calls[0] + if (!call) throw new Error("missing edit request") + expect(url(call.input)).toBe("https://api.inceptionlabs.ai/v1/edit/completions") + const headers = new Headers(call.init?.headers) + expect(headers.get("authorization")).toBe("Bearer inception-token") + expect(headers.get(HEADER_ORGANIZATIONID)).toBeNull() + expect(headers.get(HEADER_FEATURE)).toBeNull() + } finally { + mock.mockRestore() + } + }) + + test("passes upstream statuses through when the error body is unreadable", async () => { + authenticate() + const mock = stub(async (input) => { + if (!completion(input)) return Response.json([]) + return new Response( + new ReadableStream({ + start(controller) { + controller.error(new Error("broken upstream body")) + }, + }), + { status: 502 }, + ) + }) + + try { + const response = await send() + expect(response.status).toBe(502) + expect(await response.json()).toEqual({ error: "Edit request failed: 502 " }) + } finally { + mock.mockRestore() + } + }) + + test("maps upstream timeouts to 504", async () => { + authenticate() + const mock = stub(async (input) => { + if (!completion(input)) return Response.json([]) + throw new DOMException("timed out", "TimeoutError") + }) + + try { + expect((await send()).status).toBe(504) + } finally { + mock.mockRestore() + } + }) + + test("maps request cancellation to 499", async () => { + authenticate() + const controller = new AbortController() + const mock = stub(async (input) => { + if (!completion(input)) return Response.json([]) + controller.abort() + throw new DOMException("canceled", "AbortError") + }) + + try { + expect((await send(edit, controller.signal)).status).toBe(499) + } finally { + mock.mockRestore() + } + }) +}) diff --git a/packages/opencode/test/kilocode/server/httpapi-public.test.ts b/packages/opencode/test/kilocode/server/httpapi-public.test.ts index 979ed653bc7..1dc4ee7ffb0 100644 --- a/packages/opencode/test/kilocode/server/httpapi-public.test.ts +++ b/packages/opencode/test/kilocode/server/httpapi-public.test.ts @@ -6,6 +6,7 @@ import { PublicApi } from "../../../src/server/routes/instance/httpapi/public" type Schema = { anyOf?: Schema[] + items?: Schema properties?: Record type?: string minLength?: number @@ -51,6 +52,34 @@ describe("Kilo PublicApi OpenAPI contract", () => { expect(props?.organizationId).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) }) + test("keeps Kilo gateway responses nullable", () => { + const spec = OpenApi.fromApi(PublicApi) + const response = (path: string) => { + const body = spec.paths[path]?.get?.responses?.["200"] as Body | undefined + return body?.content?.["application/json"]?.schema + } + + const profile = response(KiloGatewayPaths.profile)?.properties + expect(profile?.balance).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] }) + expect(profile?.currentOrgId).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) + + const sessions = response(KiloGatewayPaths.cloudSessions)?.properties + expect(sessions?.cliSessions?.items?.properties?.title).toEqual({ + anyOf: [{ type: "string" }, { type: "null" }], + }) + expect(sessions?.nextCursor).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) + + const claw = response(KiloGatewayPaths.clawStatus)?.properties + expect(claw?.status).toEqual({ anyOf: [expect.objectContaining({ type: "string" }), { type: "null" }] }) + for (const field of ["openclawVersion", "lastStartedAt", "lastStoppedAt", "botName"]) { + expect(claw?.[field]).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) + } + + expect(response(KiloGatewayPaths.clawChatCredentials)).toEqual({ + anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }], + }) + }) + test("keeps transcription prompts in the public contract", () => { const spec = OpenApi.fromApi(PublicApi) const body = spec.paths[KiloGatewayPaths.audioTranscriptions]?.post?.requestBody as Body | undefined diff --git a/packages/opencode/test/kilocode/server/kilo-gateway-statuses.test.ts b/packages/opencode/test/kilocode/server/kilo-gateway-statuses.test.ts new file mode 100644 index 00000000000..27b16cef0b2 --- /dev/null +++ b/packages/opencode/test/kilocode/server/kilo-gateway-statuses.test.ts @@ -0,0 +1,183 @@ +import { NodeHttpServer } from "@effect/platform-node" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi" +import { Auth } from "../../../src/auth" +import { KiloGatewayApi, KiloGatewayPaths } from "../../../src/kilocode/server/httpapi/groups/kilo-gateway" +import { kiloGatewayHandlers } from "../../../src/kilocode/server/httpapi/handlers/kilo-gateway" +import { InstanceStore } from "../../../src/project/instance-store" +import { ModelCache } from "../../../src/provider/model-cache" +import { Session } from "../../../src/session/session" +import { Authorization } from "../../../src/server/routes/instance/httpapi/middleware/authorization" +import { InstanceContextMiddleware } from "../../../src/server/routes/instance/httpapi/middleware/instance-context" +import { + WorkspaceRouteContext, + WorkspaceRoutingMiddleware, +} from "../../../src/server/routes/instance/httpapi/middleware/workspace-routing" +import { testEffect } from "../../lib/effect" + +const TestHttpApi = HttpApi.make("opencode-instance").addHttpApi(KiloGatewayApi) +const auth = Layer.mock(Auth.Service)({ + get: () => Effect.succeed(new Auth.Api({ type: "api", key: "test-token" })), +}) +const store = Layer.mock(InstanceStore.Service)({}) +const cache = Layer.mock(ModelCache.Service)({}) +const session = Layer.mock(Session.Service)({}) +const passthroughAuthorization = Layer.succeed( + Authorization, + Authorization.of((effect) => effect), +) +const passthroughInstanceContext = Layer.succeed( + InstanceContextMiddleware, + InstanceContextMiddleware.of((effect) => effect), +) +const testWorkspaceRouting = Layer.succeed( + WorkspaceRoutingMiddleware, + WorkspaceRoutingMiddleware.of((effect) => + effect.pipe(Effect.provideService(WorkspaceRouteContext, WorkspaceRouteContext.of({ directory: process.cwd() }))), + ), +) +const layer = HttpRouter.serve( + HttpApiBuilder.layer(TestHttpApi).pipe( + Layer.provide(kiloGatewayHandlers), + Layer.provide([ + passthroughAuthorization, + passthroughInstanceContext, + testWorkspaceRouting, + auth, + store, + cache, + session, + ]), + ), + { disableListenLog: true, disableLogger: true }, +).pipe(Layer.provideMerge(NodeHttpServer.layerTest)) +const it = testEffect(layer) + +function stub(run: () => Response | Promise) { + // These tests run sequentially; scope the process-global override and delegate in-process server traffic. + const original = globalThis.fetch + const fetch: typeof globalThis.fetch = Object.assign( + async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + if (url.startsWith("http://127.0.0.1:")) return original(input, init) + return run() + }, + { preconnect: original.preconnect }, + ) + return Effect.acquireRelease( + Effect.sync(() => { + globalThis.fetch = fetch + }), + () => + Effect.sync(() => { + globalThis.fetch = original + }), + ) +} + +function post(path: string, body: Record) { + return HttpClientRequest.post(path).pipe(HttpClientRequest.bodyJson(body), Effect.flatMap(HttpClient.execute)) +} + +describe("Kilo gateway HttpApi statuses", () => { + it.live("preserves cloud session list rate limits", () => + Effect.gen(function* () { + yield* stub(() => new Response("rate limited", { status: 429 })) + + const response = yield* HttpClient.get(KiloGatewayPaths.cloudSessions) + + expect(response.status).toBe(429) + expect(yield* response.json).toEqual({ error: "Cloud sessions fetch failed: 429" }) + }), + ) + + it.live("maps cloud session list transport failures to internal errors", () => + Effect.gen(function* () { + yield* stub(() => Promise.reject(new TypeError("network error"))) + + const response = yield* HttpClient.get(KiloGatewayPaths.cloudSessions) + + expect(response.status).toBe(500) + expect(yield* response.json).toEqual({ error: "Internal error" }) + }), + ) + + it.live("preserves missing cloud session previews", () => + Effect.gen(function* () { + yield* stub(() => new Response("missing", { status: 404 })) + + const response = yield* HttpClient.get(KiloGatewayPaths.cloudSession.replace(":id", "missing")) + + expect(response.status).toBe(404) + expect(yield* response.json).toEqual({ error: "Session not found" }) + }), + ) + + it.live("preserves cloud session preview server failures", () => + Effect.gen(function* () { + yield* stub(() => new Response("failed", { status: 500 })) + + const response = yield* HttpClient.get(KiloGatewayPaths.cloudSession.replace(":id", "failed")) + + expect(response.status).toBe(500) + expect(yield* response.json).toEqual({ error: "Failed to fetch session" }) + }), + ) + + it.live("maps cloud session preview transport failures to internal errors", () => + Effect.gen(function* () { + yield* stub(() => Promise.reject(new TypeError("network error"))) + + const response = yield* HttpClient.get(KiloGatewayPaths.cloudSession.replace(":id", "failed")) + + expect(response.status).toBe(500) + expect(yield* response.json).toEqual({ error: "Internal error" }) + }), + ) + + it.live("preserves cloud session import authentication failures", () => + Effect.gen(function* () { + yield* stub(() => new Response("unauthorized", { status: 401 })) + + const response = yield* post(KiloGatewayPaths.cloudSessionImport, { sessionId: "unauthorized" }) + + expect(response.status).toBe(401) + expect(yield* response.json).toEqual({ error: "Import failed: 401" }) + }), + ) + + it.live("maps cloud session import transport failures to internal errors", () => + Effect.gen(function* () { + yield* stub(() => Promise.reject(new TypeError("network error"))) + + const response = yield* post(KiloGatewayPaths.cloudSessionImport, { sessionId: "failed" }) + + expect(response.status).toBe(500) + expect(yield* response.json).toEqual({ error: "Internal error" }) + }), + ) + + it.live("preserves KiloClaw worker failures", () => + Effect.gen(function* () { + yield* stub(() => new Response("worker failed", { status: 500 })) + + const response = yield* HttpClient.get(KiloGatewayPaths.clawStatus) + + expect(response.status).toBe(500) + expect(yield* response.json).toEqual({ error: "KiloClaw request failed: 500 worker failed" }) + }), + ) + + it.live("maps KiloClaw transport failures to bad gateway", () => + Effect.gen(function* () { + yield* stub(() => Promise.reject(new TypeError("network error"))) + + const response = yield* HttpClient.get(KiloGatewayPaths.clawStatus) + + expect(response.status).toBe(502) + expect(yield* response.json).toEqual({ error: "Failed to reach KiloClaw" }) + }), + ) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a0fb916a27b..f5bc92b87ca 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -8268,8 +8268,8 @@ export type KiloProfileResponses = { } balance: { balance: number - } - currentOrgId: string + } | null + currentOrgId: string | null } } @@ -8557,19 +8557,20 @@ export type KiloClawStatusResponses = { | "stopped" | "destroying" | "restoring" + | null sandboxId?: string flyRegion?: string machineSize?: { cpus: number memory_mb: number } - openclawVersion?: string - lastStartedAt?: string - lastStoppedAt?: string + openclawVersion?: string | null + lastStartedAt?: string | null + lastStoppedAt?: string | null channelCount?: number secretCount?: number userId?: string - botName?: string + botName?: string | null } } @@ -8594,7 +8595,7 @@ export type KiloClawChatCredentialsResponses = { expiresAt: string kiloChatUrl: string eventServiceUrl: string - } + } | null } export type KiloClawChatCredentialsResponse = KiloClawChatCredentialsResponses[keyof KiloClawChatCredentialsResponses] @@ -8628,12 +8629,12 @@ export type KiloCloudSessionsResponses = { 200: { cliSessions: Array<{ session_id: string - title: string + title: string | null created_at: string updated_at: string version: number }> - nextCursor: string + nextCursor: string | null } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 66750773bfb..294c6526d43 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10950,17 +10950,31 @@ "additionalProperties": false }, "balance": { - "type": "object", - "properties": { - "balance": { - "type": "number" + "anyOf": [ + { + "type": "object", + "properties": { + "balance": { + "type": "number" + } + }, + "required": ["balance"], + "additionalProperties": false + }, + { + "type": "null" } - }, - "required": ["balance"], - "additionalProperties": false + ] }, "currentOrgId": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, "required": ["profile", "balance", "currentOrgId"], @@ -11712,16 +11726,23 @@ "type": "object", "properties": { "status": { - "type": "string", - "enum": [ - "provisioned", - "starting", - "restarting", - "recovering", - "running", - "stopped", - "destroying", - "restoring" + "anyOf": [ + { + "type": "string", + "enum": [ + "provisioned", + "starting", + "restarting", + "recovering", + "running", + "stopped", + "destroying", + "restoring" + ] + }, + { + "type": "null" + } ] }, "sandboxId": { @@ -11744,13 +11765,34 @@ "additionalProperties": false }, "openclawVersion": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "lastStartedAt": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "lastStoppedAt": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "channelCount": { "type": "number" @@ -11762,7 +11804,14 @@ "type": "string" }, "botName": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, "required": ["status"], @@ -11821,23 +11870,30 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "token": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "kiloChatUrl": { + "type": "string" + }, + "eventServiceUrl": { + "type": "string" + } + }, + "required": ["token", "expiresAt", "kiloChatUrl", "eventServiceUrl"], + "additionalProperties": false }, - "expiresAt": { - "type": "string" - }, - "kiloChatUrl": { - "type": "string" - }, - "eventServiceUrl": { - "type": "string" + { + "type": "null" } - }, - "required": ["token", "expiresAt", "kiloChatUrl", "eventServiceUrl"], - "additionalProperties": false + ] } } } @@ -11916,7 +11972,14 @@ "type": "string" }, "title": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "created_at": { "type": "string" @@ -11933,7 +11996,14 @@ } }, "nextCursor": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, "required": ["cliSessions", "nextCursor"], diff --git a/script/check-workflows.ts b/script/check-workflows.ts index 35b509e7add..1fc0138d307 100644 --- a/script/check-workflows.ts +++ b/script/check-workflows.ts @@ -36,6 +36,7 @@ const active = new Set([ "check-org-member.yml", "close-issues.yml", "close-stale-prs.yml", + "codeql-kotlin.yml", "codeql.yml", "containers.yml", "docs-build.yml",