mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge branch 'main' into catrielmuller/kiloclaw-vscode
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve cached Agent Manager git stats when reopening collapsed sections.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fixed default model falling back to the free model after login or org switch by invalidating cached provider state when auth changes.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix queued-state detection so prompts sent after a completed response are treated as active instead of queued.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
Include the feature header when requesting the models list from Kilo Gateway
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce git process load via visibility-aware polling and resolution caching in GitStatsPoller
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Include pnpm-lock.yaml and yarn.lock in the .kilo/.gitignore so lockfiles from alternative package managers don't appear as untracked files
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep local review follow-up questions visible after review output so prompt input is not blocked by an invisible pending question.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Merged upstream opencode changes from v1.3.10:
|
||||
|
||||
- Subagent tool calls stay clickable while pending
|
||||
- Improved storage migration reliability
|
||||
- Better muted text contrast in Catppuccin themes
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Merged upstream opencode changes from v1.3.6:
|
||||
|
||||
- Fixed token usage double-counting for Anthropic and Amazon Bedrock providers
|
||||
- Fixed variant dialog search filtering
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Merged upstream opencode changes from v1.3.7:
|
||||
|
||||
- Added first-class PowerShell support on Windows
|
||||
- Plugin installs now preserve JSONC comments in configuration files
|
||||
- Improved variant modal behavior to be less intrusive
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix duplicate text shown when typing a custom answer in the question tool. The preview text above the input field is now hidden while editing, so only the input value is visible.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix queued follow-up messages so they stay stacked at the bottom of the chat while the previous turn is still being processed, instead of getting interleaved with tool output.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Clarify suggest tool guidance so the assistant writes its final summary before offering a local review.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore earlier chat history when scrolling through virtualized message lists.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore localized tooltip text for message revert buttons
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Make subsession costs in TaskHeader tooltip more readable with many subsessions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix a stuck session state when the suggest tool was left open. If the suggestion was never accepted or dismissed (for example, because VS Code was closed while it was showing), the session stayed marked as busy and any follow-up messages appeared queued forever. The session is now marked idle while waiting for a response to a suggestion.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add @terminal context mention support to the chat input. Type @terminal to include your active VS Code terminal output as context, with output safety limits (500 lines / 50K chars) and truncation. Works in both the sidebar chat and Agent Manager.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
Update `@ai-sdk/anthropic` to 3.0.71, adding `xhigh` effort for Opus 4.7 adaptive thinking (3.0.70) and fixing fine-grained tool streaming beta header for Opus 4.7 (3.0.71)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Update `@ai-sdk/amazon-bedrock` to 4.0.96 and `@ai-sdk/google-vertex` to 4.0.112, both of which include Opus 4.7 support with `xhigh` adaptive thinking effort
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore spacing between tool output and queued user messages in the VS Code chat.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore spacing between sub-agent output and the following user message in the VS Code chat.
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
# kilocode_change start - install deps for changeset changelog generation
|
||||
# kilocode_change start - install deps for version script workspace resolution
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
@@ -254,89 +254,25 @@ jobs:
|
||||
# APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
|
||||
# kilocode_change start
|
||||
# Smoke test disabled as a release gate due to infrastructure issues.
|
||||
# The job is skipped via `if: false` so it no longer blocks publishing.
|
||||
# Re-enable by restoring the original `if:` condition and uncommenting
|
||||
# `- smoke-test` in the publish job's `needs` list below.
|
||||
# Run smoke tests against CLI assets uploaded to the draft GitHub release
|
||||
# before publishing the release and package artifacts.
|
||||
smoke-test:
|
||||
name: Smoke Test (pre-publish gate) [DISABLED]
|
||||
name: Smoke Test (pre-publish gate)
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
if: false # was: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Trigger kilo-bench smoke test
|
||||
id: trigger
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
BEFORE=$(date -u -d '60 seconds ago' +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
gh api repos/Kilo-Org/kilo-bench/dispatches \
|
||||
--method POST \
|
||||
-f event_type=smoke-test \
|
||||
-f 'client_payload[release_tag]=${{ needs.version.outputs.tag }}' \
|
||||
-f 'client_payload[source_run_id]=${{ github.run_id }}'
|
||||
|
||||
# Poll for the run created after our dispatch timestamp.
|
||||
# The dispatch API returns no run ID, so we query by created time
|
||||
# and pick the oldest match to avoid grabbing an unrelated run.
|
||||
echo "Waiting for smoke-test run to appear (dispatched after $BEFORE)..."
|
||||
for attempt in $(seq 1 30); do
|
||||
RUN_ID=$(gh api \
|
||||
"repos/Kilo-Org/kilo-bench/actions/workflows/smoke-test.yml/runs?event=repository_dispatch&created=>=$BEFORE" \
|
||||
--jq '.workflow_runs | sort_by(.created_at) | .[0].id // empty')
|
||||
|
||||
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
|
||||
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Smoke test run: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " attempt $attempt: run not yet registered, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "::error::Smoke test run did not appear within 5 minutes after dispatch."
|
||||
exit 1
|
||||
|
||||
- name: Wait for smoke test to complete
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
RUN_ID="${{ steps.trigger.outputs.run_id }}"
|
||||
echo "Waiting for run $RUN_ID..."
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
CONCLUSION=$(gh run view "$RUN_ID" \
|
||||
--repo Kilo-Org/kilo-bench \
|
||||
--json conclusion \
|
||||
--jq '.conclusion')
|
||||
|
||||
echo " attempt $i: $CONCLUSION"
|
||||
|
||||
if [[ "$CONCLUSION" == "success" ]]; then
|
||||
echo "::notice::Smoke test passed."
|
||||
exit 0
|
||||
elif [[ "$CONCLUSION" != "null" && "$CONCLUSION" != "" ]]; then
|
||||
echo "::error::Smoke test failed with conclusion: $CONCLUSION"
|
||||
echo "See: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Smoke test did not complete within 30 minutes."
|
||||
exit 1
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
uses: ./.github/workflows/smoke-test.yml
|
||||
with:
|
||||
cli_version: ${{ needs.version.outputs.version }}
|
||||
secrets: inherit
|
||||
# kilocode_change end
|
||||
publish:
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
- build-vscode
|
||||
# - smoke-test # disabled: infrastructure issues (see smoke-test job comment)
|
||||
- smoke-test
|
||||
# - build-tauri
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# Triggers:
|
||||
# - workflow_dispatch: manually from Actions tab (optionally pass a CLI version)
|
||||
# - push to main: automatically after every merge
|
||||
# - workflow_call: from publish.yml after draft release assets are uploaded
|
||||
#
|
||||
# Required secrets:
|
||||
# KILO_API_KEY — Kilo Gateway key
|
||||
@@ -23,13 +23,19 @@ on:
|
||||
description: "CLI version to test (e.g. 7.0.36). Leave blank for latest npm release."
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
cli_version:
|
||||
description: "CLI version to test from draft release assets."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: smoke-test
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # needed to read draft release assets when called from publish.yml
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
@@ -71,18 +77,31 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve CLI version
|
||||
- name: Resolve CLI asset URL
|
||||
id: cli
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ inputs.cli_version }}"
|
||||
if [[ -n "$VERSION" ]]; then
|
||||
URL="https://github.com/Kilo-Org/kilocode/releases/download/v${VERSION}/kilo-linux-x64.tar.gz"
|
||||
echo "cli_url=$URL" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing CLI v${VERSION} from release: $URL"
|
||||
else
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "cli_url=" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing latest CLI from npm"
|
||||
exit 0
|
||||
fi
|
||||
# Resolve the release asset via the API so this works for draft
|
||||
# releases too (browser /releases/download/... URLs 404 on drafts).
|
||||
# The installer passes Accept: application/octet-stream + bearer
|
||||
# auth, which is what the API asset endpoint expects.
|
||||
# The tag_name filter yields at most one release and one asset,
|
||||
# so --jq returns a single URL without needing head/pipefail games.
|
||||
URL=$(gh api "repos/${{ github.repository }}/releases" \
|
||||
--jq ".[] | select(.tag_name == \"v${VERSION}\") | .assets[] | select(.name == \"kilo-linux-x64.tar.gz\") | .url")
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "::error::asset kilo-linux-x64.tar.gz not found for v${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
echo "cli_url=$URL" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing CLI v${VERSION} via asset API: $URL"
|
||||
|
||||
- name: Run smoke test — hello-world
|
||||
env:
|
||||
|
||||
@@ -17,6 +17,9 @@ permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: unit (${{ matrix.settings.name }})
|
||||
@@ -38,6 +41,11 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -104,6 +112,11 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
|
||||
Generated
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": ".kilo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/plugin": {
|
||||
"version": "7.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "7.2.0",
|
||||
"zod": "4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/sdk": {
|
||||
"version": "7.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": ".kilocode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/plugin": {
|
||||
"version": "7.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "7.2.0",
|
||||
"zod": "4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/sdk": {
|
||||
"version": "7.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ plans
|
||||
package.json
|
||||
bun.lock
|
||||
.gitignore
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
references/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: effect
|
||||
description: Answer questions about the Effect framework
|
||||
---
|
||||
|
||||
# Effect
|
||||
|
||||
This codebase uses Effect, a framework for writing typescript.
|
||||
|
||||
## How to Answer Effect Questions
|
||||
|
||||
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
|
||||
`.opencode/references/effect-smol` in this project NOT the skill folder.
|
||||
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
|
||||
3. Provide responses based on the actual Effect source code and documentation
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Always use the explore agent with the cloned repository when answering Effect-related questions
|
||||
- Reference specific files and patterns found in the Effect codebase
|
||||
- Do not answer from memory - always verify against the source
|
||||
@@ -116,8 +116,8 @@
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
"dark": "#abafb7",
|
||||
"light": "textMuted"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
|
||||
@@ -203,6 +203,8 @@ We regularly merge upstream changes from opencode. To minimize merge conflicts a
|
||||
|
||||
4. **Avoid restructuring upstream code** - Don't refactor or reorganize code that comes from opencode unless absolutely necessary.
|
||||
|
||||
5. **Mirror new config keys to the cloud schema** - When adding a `kilocode_change` key to `Config.Info` in `packages/opencode/src/config/config.ts`, also add the matching JSON Schema entry in `apps/web/src/app/config.json/extras.ts` in the [cloud repo](https://github.com/Kilo-Org/cloud). See [CLI Config Schema](packages/kilo-docs/pages/contributing/architecture/config-schema.md) for the step-by-step.
|
||||
|
||||
The goal is to keep our diff from upstream as small as possible, making regular merges straightforward and reducing the risk of conflicts.
|
||||
|
||||
### Kilocode Change Markers
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
- ⚡ Inline autocomplete suggestions
|
||||
- 🤖 Latest AI models
|
||||
- 🎁 API keys optional
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.4
|
||||
|
||||
## Quick Links
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-B8kyomBd8nlqFG+9idfs+y8P1M+4eVUBP3sDJDh6upw=",
|
||||
"aarch64-linux": "sha256-Xzcrz1R3Gunp0aVgNpEVPLd+SahV8wyuwgUgVNOTgfI=",
|
||||
"aarch64-darwin": "sha256-5Yq09XbErOYRsO+DOqOYAUmz2go7kAQFnLvTXb2CDcc=",
|
||||
"x86_64-darwin": "sha256-c4Cxv5jFTbb1v56c9I18SgGaigtgHeeyEwfn7QjCUdY="
|
||||
"x86_64-linux": "sha256-US0AaNuPmVfFe6ThgH9Zgu028MIN1ZgdsqN1bA7sxoc=",
|
||||
"aarch64-linux": "sha256-Ypp6waiay+0A9BLbHDwk35mBulANocuo/rhVw9wAcUY=",
|
||||
"aarch64-darwin": "sha256-Oia0MscEDIvVqKnhlHnOZ+ZuxcTE8gSZPrqdWnZYcbE=",
|
||||
"x86_64-darwin": "sha256-7zd5/T0B5f7CKdjvMfKbm4wCJLDEYJFRABLRGeagW74="
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -7,7 +7,6 @@
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
@@ -52,25 +51,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
installPhase =
|
||||
''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
''
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
sysctl
|
||||
]
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
''
|
||||
+ ''
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
|
||||
+6
-6
@@ -25,7 +25,7 @@
|
||||
"packages/sdk/js"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/platform-node": "4.0.0-beta.43",
|
||||
"@effect/platform-node": "4.0.0-beta.46",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -46,8 +46,8 @@
|
||||
"dompurify": "3.3.3",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.43",
|
||||
"ai": "6.0.149",
|
||||
"effect": "4.0.0-beta.46",
|
||||
"ai": "6.0.158",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.12.12",
|
||||
"hono-openapi": "1.1.2",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"heap-snapshot-toolkit": "1.1.3",
|
||||
"typescript": "catalog:",
|
||||
"@morphllm/morphsdk": "0.2.141"
|
||||
"@morphllm/morphsdk": "0.2.166"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -120,7 +120,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.35",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.46",
|
||||
"path-to-regexp": ">=8.4.0",
|
||||
"picomatch": ">=2.3.2",
|
||||
"defu": "6.1.6",
|
||||
@@ -136,6 +136,6 @@
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
},
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,12 @@ async function waitForHealth(url: string, probe = "/global/health") {
|
||||
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
|
||||
}
|
||||
|
||||
function done(proc: ReturnType<typeof spawn>) {
|
||||
return proc.exitCode !== null || proc.signalCode !== null
|
||||
}
|
||||
|
||||
async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
|
||||
if (proc.exitCode !== null) return
|
||||
if (done(proc)) return
|
||||
await Promise.race([
|
||||
new Promise<void>((resolve) => proc.once("exit", () => resolve())),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, timeout)),
|
||||
@@ -123,11 +127,11 @@ export async function startBackend(label: string, input?: { llmUrl?: string }):
|
||||
return {
|
||||
url,
|
||||
async stop() {
|
||||
if (proc.exitCode === null) {
|
||||
if (!done(proc)) {
|
||||
proc.kill("SIGTERM")
|
||||
await waitExit(proc)
|
||||
}
|
||||
if (proc.exitCode === null) {
|
||||
if (!done(proc)) {
|
||||
proc.kill("SIGKILL")
|
||||
await waitExit(proc)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -155,7 +155,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
resetHeartbeat()
|
||||
streamErrorLogged = false
|
||||
const directory = event.directory ?? "global"
|
||||
const payload = event.payload
|
||||
if (event.payload.type === "sync") {
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = event.payload as Event
|
||||
|
||||
const k = key(directory, payload)
|
||||
if (k) {
|
||||
const i = coalesced.get(k)
|
||||
|
||||
@@ -274,7 +274,7 @@ const WorkspaceSessionList = (props: {
|
||||
<div class="relative w-full py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
||||
size="large"
|
||||
onClick={(e: MouseEvent) => {
|
||||
props.loadMore()
|
||||
|
||||
@@ -642,10 +642,10 @@ export function MessageTimeline(props: {
|
||||
onClick={props.onResumeScroll}
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-[var(--gray-dark-7)] bg-[color-mix(in_srgb,var(--gray-dark-3)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--gray-dark-8)] [--icon-base:var(--gray-dark-10)] group-hover:[--icon-base:var(--gray-dark-11)]"
|
||||
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
|
||||
style={{
|
||||
"box-shadow":
|
||||
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)",
|
||||
"0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
|
||||
}}
|
||||
>
|
||||
<Icon name="arrow-down-to-line" size="small" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) {
|
||||
y: state.y,
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
show: true,
|
||||
show: false,
|
||||
title: "OpenCode",
|
||||
icon: iconPath(),
|
||||
backgroundColor,
|
||||
@@ -94,6 +94,10 @@ export function createMainWindow(globals: Globals) {
|
||||
wireZoom(win)
|
||||
injectGlobals(win, globals)
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
win.show()
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.2.10"
|
||||
version = "7.2.14"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.10/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.14/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.10/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.14/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.10/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.14/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.10/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.14/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.10/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.14/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -15,7 +15,14 @@ export const AutomateNav: NavSection[] = [
|
||||
{ href: "/automate/code-reviews/gitlab", children: "GitLab" },
|
||||
],
|
||||
},
|
||||
{ href: "/automate/agent-manager", children: "Agent Manager" },
|
||||
{
|
||||
href: "/automate/agent-manager",
|
||||
children: "Agent Manager",
|
||||
subLinks: [
|
||||
{ href: "/automate/agent-manager", children: "Reference" },
|
||||
{ href: "/automate/agent-manager-workflows", children: "Workflows" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,6 +38,10 @@ export const ContributingNav: NavSection[] = [
|
||||
href: "/contributing/architecture/benchmarking",
|
||||
children: "Benchmarking",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/config-schema",
|
||||
children: "CLI Config Schema",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/enterprise-mcp-controls",
|
||||
children: "Enterprise MCP Controls",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -10,13 +10,11 @@ Kilo Code provides its own built-in API provider that gives you access to the la
|
||||
|
||||
## Getting Started
|
||||
|
||||
When you sign up for Kilo Code, you can start immediately with free models, or top up your account for the first time to get bonus credits.
|
||||
|
||||
To claim your bonus credits:
|
||||
When you sign up for Kilo Code, you can start immediately with free models, or add credits to your account to access premium models.
|
||||
|
||||
1. **Sign up:** Complete the registration process
|
||||
2. **First top-up:** Add funds to your account and get $20 bonus credits
|
||||
3. **Start Coding:** Enjoy your $20 in free credits
|
||||
2. **Add credits:** Top up your account at [app.kilo.ai](https://app.kilo.ai/profile)
|
||||
3. **Start Coding:** Use 500+ models including the latest frontier coding models
|
||||
|
||||
## Registration Process
|
||||
|
||||
@@ -64,7 +62,7 @@ With the Kilo Code provider, if you sign up with Google you can also connect oth
|
||||
|
||||
## Tips and Notes
|
||||
|
||||
- **Free Credits:** New users receive free credits to explore Kilo Code's capabilities
|
||||
- **Free Models:** New users can start with free models to explore Kilo Code's capabilities
|
||||
- **Identity Verification:** The temporary hold system ensures service reliability while preventing misuse
|
||||
- **Seamless Integration:** No need to manage multiple API keys or provider configurations
|
||||
- **Latest Models:** Automatic access to the most current frontier coding models
|
||||
|
||||
@@ -53,7 +53,7 @@ Then set your default model:
|
||||
|
||||
## Tips and Notes
|
||||
|
||||
- **Subscription Required:** You need an active ChatGPT Plus or Pro subscription. This provider won't work with free ChatGPT accounts. See [OpenAI's ChatGPT plans](https://openai.com/chatgpt/pricing) for more information.
|
||||
- **Subscription Required:** You need an active ChatGPT Plus or Pro subscription. This provider won't work with free ChatGPT accounts. See [OpenAI's ChatGPT plans](https://chatgpt.com/pricing/) for more information.
|
||||
- **Authentication Errors:** If you receive a CSRF or other error when completing OAuth authentication, ensure you do not have another application already listening on port 1455. You can check on Linux and Mac by using `lsof -i :1455`.
|
||||
- **No API Costs:** Usage through this provider counts against your ChatGPT subscription, not separately billed API usage.
|
||||
- **Sign Out:** To disconnect, use the "Sign Out" button in the provider settings.
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
title: "Agent Manager Workflows"
|
||||
description: "Scaling from the sidebar to multiple agents in parallel worktrees"
|
||||
---
|
||||
|
||||
# Agent Manager Workflows
|
||||
|
||||
If you already use the sidebar chat and want to start running multiple agents in parallel, this page is the fastest path to productive. For the full reference of buttons and settings, see the [Agent Manager reference](/docs/automate/agent-manager).
|
||||
|
||||
## Sidebar vs. Agent Manager
|
||||
|
||||
- **Sidebar** — one agent on your current branch. Best for small, interactive tasks where you want tight feedback.
|
||||
- **Agent Manager** — multiple agents, each in its own git worktree (its own branch checked out on disk). Best for long-running work, trying several approaches, or keeping side work isolated from your main branch.
|
||||
- **Multiple sessions inside one worktree** (`Cmd+T` / `Ctrl+T`) — same branch, separate conversations. Useful for planner + implementer splits or read-only investigations alongside the main agent.
|
||||
|
||||
Rule of thumb: if you would stash or switch branches to do the work, create a worktree instead.
|
||||
|
||||
{% callout type="info" %}
|
||||
All Agent Manager sessions share a single `kilo serve` process. What each worktree isolates is the filesystem and git state — the branch, the directory, the terminal. API keys, models, and configuration are shared.
|
||||
{% /callout %}
|
||||
|
||||
## What parallelizes well
|
||||
|
||||
Parallel work pays off when sessions are **independent** — neither one's output depends on the other, and they are unlikely to edit the same files.
|
||||
|
||||
- **Good candidates:** independent features, module-scoped refactors, a feature plus an unrelated bug fix, trying 2–4 approaches to the same problem.
|
||||
- **Poor candidates:** tasks editing the same files, steps with tight sequential dependencies.
|
||||
- **Always safe:** read-only work (investigation, code tours, running tests, log analysis). Nothing touches the filesystem, so multiple sessions on the same branch never collide.
|
||||
|
||||
## The default loop
|
||||
|
||||
Every productive worktree session follows the same rhythm:
|
||||
|
||||
1. **Create a worktree** (`Cmd+N` / `Ctrl+N`) and describe the task.
|
||||
2. **Let the agent run.** Switch to another worktree, another session, or step away.
|
||||
3. **Verify manually.** Before you trust "all tests pass", run the app with the run script (`Cmd+E` / `Ctrl+E`) or open the worktree's terminal (`Cmd+/` / `Ctrl+/`) and run the tests yourself.
|
||||
4. **Review the diff** (`Cmd+D` / `Ctrl+D`). Drop inline comments, then **Send to chat** to feed them back to the agent.
|
||||
5. **Iterate.** Re-run, re-review. Repeat until the diff is ready — not until the agent says it is done.
|
||||
6. **Ship it.** See [Merging worktree and parent branch](#merging-worktree-and-parent-branch).
|
||||
|
||||
The single biggest lever on this loop is **keeping each worktree's scope small**. A small diff tests quickly, reviews quickly, and PRs quickly.
|
||||
|
||||
## Workflows
|
||||
|
||||
### 1. Side quest
|
||||
|
||||
Something unrelated came up while you are mid-task. Create a new worktree for it (`Cmd+N`), let the agent work, review when it is done. Your main work is unaffected.
|
||||
|
||||
### 2. Build a skeleton, then split the work
|
||||
|
||||
For multi-part features where several pieces share a few core contracts — types, API boundaries, folder layout:
|
||||
|
||||
1. Build the walking skeleton in one worktree or the sidebar. Update AGENTS.md with the conventions.
|
||||
2. Merge it, then create one worktree per feature slice — each branched off the skeleton.
|
||||
3. Merge slices in dependency order as each goes green.
|
||||
|
||||
This mirrors how a human team works: agree the API contract first, then split backend and frontend in parallel. The contract removes the need to coordinate mid-flight.
|
||||
|
||||
### 3. Multiple approaches in parallel
|
||||
|
||||
For genuinely hard tasks where you do not know which approach will work:
|
||||
|
||||
1. Open the advanced new-worktree dialog (`Cmd+Shift+N` / `Ctrl+Shift+N`) and pick 2–4 versions.
|
||||
2. Optionally assign a different model to each.
|
||||
3. Review the diffs side by side, pick the winner, apply it, discard the rest.
|
||||
|
||||
### 4. Continue in Worktree
|
||||
|
||||
A sidebar task grew bigger than planned. From the sidebar chat, choose **Continue in Worktree** — the session history and any uncommitted changes move into a new worktree, and the sidebar is free again.
|
||||
|
||||
A related pattern: use the sidebar as an investigation surface. Start two or three investigation chats in the sidebar, then promote only the ones worth pursuing into worktrees.
|
||||
|
||||
### 5. A worktree per bug
|
||||
|
||||
For a day of small fixes: one worktree per bug (`Cmd+N`), one branch per fix, merge each quickly so none drift. Close the worktree when the fix lands.
|
||||
|
||||
### 6. Multiple sessions on one branch
|
||||
|
||||
Press `Cmd+T` / `Ctrl+T` inside an existing worktree to open another session on the same branch. Useful for:
|
||||
|
||||
- **Planner + implementer.** One session researches or plans; the other implements with a clean context.
|
||||
- **Fresh context on a long conversation.** Start a new tab, summarize the current state, continue there. The old session stays available.
|
||||
- **Read-only investigations** alongside the main agent — always safe because nothing touches the filesystem.
|
||||
- **Forked exploration.** Use **Fork Session** to spawn a new session seeded with an existing conversation, then steer it differently without losing the original.
|
||||
|
||||
Sessions sharing a branch can see each other's commits, so write-heavy work on the same branch needs a little coordination.
|
||||
|
||||
## Running and testing
|
||||
|
||||
- **Worktree terminal** (`Cmd+/` / `Ctrl+/`) — rooted at the worktree directory, so all commands scope to that branch. Use it for one-off tests, `git status`, reproducing a bug by hand.
|
||||
- **Run script** — create `.kilo/run-script` (or `.ps1` / `.cmd` / `.bat` on Windows) and trigger it with `Cmd+E` / `Ctrl+E`. Runs in whichever worktree is selected. Gets `WORKTREE_PATH` and `REPO_PATH` in the environment.
|
||||
- **Open in its own VS Code window** — right-click a worktree and choose **Open in VS Code** for a full editor rooted at the worktree path.
|
||||
|
||||
### Parallel worktrees need non-shared state
|
||||
|
||||
The moment two worktrees both try to use the same external resource — a port, a cache, an emulator, a container — they collide. Only one process can bind to `localhost:3000`; only one simulator can be "the simulator".
|
||||
|
||||
Two fixes, in order of preference:
|
||||
|
||||
1. **Change the app to read the address from the environment** with a free-port fallback. This solves the problem everywhere — Agent Manager, CI, tests, teammates — not just here.
|
||||
2. **Assign a unique instance per worktree** in the run script, derived from `WORKTREE_PATH`.
|
||||
|
||||
The same applies to caches (avoid pointing `CARGO_TARGET_DIR` at a shared path), emulators (create a named simulator per worktree), and containers (use unique container names or `COMPOSE_PROJECT_NAME`).
|
||||
|
||||
## Reviewing changes
|
||||
|
||||
Layer review in before asking a teammate:
|
||||
|
||||
- **Diff panel** (`Cmd+D`) — live diff against the parent branch. Drag filenames into the chat input for `@file` mentions. Inline-comment the lines you want revisited, then **Send to chat** to iterate.
|
||||
- **`/local-review-uncommitted`** — slash command, AI review of staged and unstaged changes in the worktree. Good as a last pass before committing.
|
||||
- **`/local-review`** — slash command, AI review of the whole branch vs. its base.
|
||||
- **`kilo review` in CI** — automated PR review. See [Code Reviews](/docs/automate/code-reviews/overview) for the setup.
|
||||
- **Human review** — push the branch from the session terminal and `gh pr create`. The PR badge appears on the worktree and stays in sync with CI and reviews.
|
||||
|
||||
A typical sequence: self-review in the diff panel → `/local-review-uncommitted` → push → CI review → teammate review.
|
||||
|
||||
## Merging worktree and parent branch
|
||||
|
||||
Over a worktree's life you will merge in two directions: from the worktree back to its parent branch (integrating the work), and from the parent branch into the worktree (staying current). The parent branch is whatever branch the worktree was created from — often `main`, but not always. The examples below use `main`; substitute your actual parent branch where relevant.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
parent["parent branch"]
|
||||
wt["worktree"]
|
||||
|
||||
parent -->|"Pull parent in (stay current)"| wt
|
||||
wt -->|"Apply / Merge / PR"| parent
|
||||
```
|
||||
|
||||
### Worktree → parent branch
|
||||
|
||||
Three ways, pick based on how much collaboration the change needs:
|
||||
|
||||
- **Apply to local** — from the diff panel. Copies the worktree's changes onto your checkout of the parent branch. You can stop there, or commit and push from your normal terminal. Fastest path for solo work.
|
||||
- **Merge directly** — from the session terminal: `git checkout main && git merge <branch>`. The natural flow on teams without a PR culture.
|
||||
- **Open a PR** — `git push -u origin <branch> && gh pr create --fill` from the session terminal. The PR badge appears on the worktree and stays in sync with CI and reviews.
|
||||
|
||||
### Parent branch → worktree
|
||||
|
||||
When the parent branch moves ahead, ask the agent from the worktree's session:
|
||||
|
||||
> Merge the latest `origin/main` into this branch and resolve any conflicts. Do not use `git stash`.
|
||||
|
||||
Save this as a reusable slash command if you do it often.
|
||||
|
||||
{% callout type="danger" %}
|
||||
**Never use `git stash` inside a worktree.** Stashes live in the shared `.git` directory that every worktree points at, so a stash made in one worktree can be popped in another — crossing uncommitted changes between agents. Use a WIP commit or a temporary branch instead.
|
||||
{% /callout %}
|
||||
|
||||
### Resolving conflicts
|
||||
|
||||
The Agent Manager is good at conflict resolution when you give it context. A low-context ask ("fix the conflicts") often produces a result that compiles but silently drops one side's intent. Tell the agent what each branch was trying to do:
|
||||
|
||||
> I am merging `<branch>` into `<target>`. `<branch>` did X. `<target>` has since added Y. Both need to survive.
|
||||
|
||||
### When several worktrees finish at once
|
||||
|
||||
Merge the most foundational one first. Then, in each remaining worktree, ask the agent to pull the updated parent branch in (same prompt as above) before merging. The agent handles the merge direction and only escalates conflicts it cannot resolve.
|
||||
|
||||
## Hygiene
|
||||
|
||||
- Merge within a day or two. Past that, pull the parent branch into the worktree rather than letting it drift.
|
||||
- After a branch merges, close the worktree from its context menu. The branch is preserved; the directory is removed.
|
||||
- Do not run more than four or five agents at once. The practical limit is review and integration cost, not memory.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Too many agents.** Coordination overhead exceeds the throughput gain above four or five.
|
||||
- **Overlapping file edits in parallel worktrees.** Worktrees isolate the filesystem, not the semantics — conflicts still happen at merge time.
|
||||
- **Skipping manual verification.** Trust the agent, but confirm with the run script or terminal.
|
||||
- **Stale shared context.** Update AGENTS.md before a swarm, not mid-flight.
|
||||
- **Hardcoded shared state.** Fixed ports, fixed container names, "the simulator" — refactor to take values from the environment.
|
||||
- **`git stash` inside a worktree.** Stashes cross between worktrees.
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
| Situation | Where |
|
||||
| ------------------------------------------------------ | ----------------------------- |
|
||||
| Small, interactive task | Sidebar |
|
||||
| Long task, want to do something else meanwhile | New worktree (`Cmd+N`) |
|
||||
| Two or three approaches, pick the winner | Multi-version (`Cmd+Shift+N`) |
|
||||
| Sidebar task outgrew the sidebar | Continue in Worktree |
|
||||
| Separate conversation on the same branch | New tab (`Cmd+T`) |
|
||||
| Long conversation, want a fresh context on same branch | New tab, summarize |
|
||||
| Run the app to verify | Run script (`Cmd+E`) |
|
||||
| One-off git or shell commands | Terminal (`Cmd+/`) |
|
||||
| Team review | Push + `gh pr create` |
|
||||
| Ship without ceremony | Apply to local |
|
||||
|
||||
## Related
|
||||
|
||||
- [Agent Manager reference](/docs/automate/agent-manager)
|
||||
- [Code Reviews](/docs/automate/code-reviews/overview)
|
||||
- [Shell integration](/docs/automate/extending/shell-integration)
|
||||
@@ -19,6 +19,10 @@ The Agent Manager is a **full-panel editor tab** built directly into the extensi
|
||||
- Session import from existing branches, external worktrees, or GitHub PR URLs
|
||||
- "Continue in Worktree" to promote a sidebar session to the Agent Manager
|
||||
|
||||
{% callout type="tip" %}
|
||||
New to running multiple agents in parallel? The [Agent Manager Workflows](/docs/automate/agent-manager-workflows) guide walks through when to use the sidebar vs. the Agent Manager, how to pick tasks that parallelize well, and the common patterns for testing, reviewing, and integrating changes across worktrees.
|
||||
{% /callout %}
|
||||
|
||||
## Opening the Agent Manager
|
||||
|
||||
- Keyboard shortcut: `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux)
|
||||
@@ -153,11 +157,13 @@ Right-click the section header and select **Delete Section**. The section is rem
|
||||
|
||||
Press `Cmd+D` (macOS) / `Ctrl+D` (Windows/Linux) to toggle the diff panel. It shows a live-updating diff between the worktree and its parent branch.
|
||||
|
||||
- Select files and click **Apply to Main Branch** to merge changes
|
||||
- Select files and click **Apply to local** to copy the worktree's changes onto your local checkout of the base branch
|
||||
- Conflicts are surfaced with a resolution dialog
|
||||
- Supports unified and split diff views
|
||||
- **Drag file headers into chat** — drag a file header from the diff panel into the chat input to insert an `@file` mention, giving the agent context about specific changed files
|
||||
|
||||
See [Agent Manager Workflows](/docs/automate/agent-manager-workflows#merging-worktree-and-parent-branch) for the full integration story, including when to apply locally vs. merge directly vs. open a pull request.
|
||||
|
||||
## Terminals
|
||||
|
||||
Each session has a dedicated integrated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session.
|
||||
|
||||
@@ -23,11 +23,15 @@ That's it. No configuration needed.
|
||||
|
||||
You can see which underlying models are used, as well as the cost, in the expanded model picker. Model mapping information is also available on the [Gateway Model page](/docs/gateway/models-and-providers#kilo-autofrontier).
|
||||
|
||||
{% callout type="info" title="Models can change" %}
|
||||
The underlying models behind each Auto Model tier are updated server-side as better options become available or as providers change pricing and availability. The tier you select stays the same; the model it routes to may change over time.
|
||||
{% /callout %}
|
||||
|
||||
## Tiers
|
||||
|
||||
- **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work.
|
||||
- **Balanced** — Follows the same mode-based routing structure as Frontier but uses a more cost-effective model across all modes. A good default for most developers who want strong AI assistance without paying frontier prices.
|
||||
- **Free** — Routes to the best available free model on OpenRouter. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the model may change over time.
|
||||
- **Balanced** — Uses a single cost-effective model across all modes. A good default for most developers who want strong AI assistance without paying frontier prices.
|
||||
- **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time.
|
||||
|
||||
## Benefits
|
||||
|
||||
|
||||
@@ -682,8 +682,9 @@ Positionals:
|
||||
sessionID session id to export [string]
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--sanitize redact sensitive transcript and file data [boolean]
|
||||
```
|
||||
|
||||
## kilo import
|
||||
|
||||
@@ -52,7 +52,7 @@ For the current mode-to-model mappings, see the [Auto Model user docs](/docs/cod
|
||||
|
||||
**Who it's for**: Cost-conscious developers who want better results than free models at a fraction of frontier cost.
|
||||
|
||||
**What it does**: Follows the same mode-based routing structure as Frontier but uses GPT 5.3 Codex — a cost-effective model with strong reasoning and coding capabilities — across all modes.
|
||||
**What it does**: Uses GPT 5.3 Codex — a cost-effective model with strong reasoning and coding capabilities — for every mode. Unlike Frontier, Balanced does not vary its underlying model by mode.
|
||||
|
||||
**Pricing**: Paid, but significantly cheaper than Frontier.
|
||||
|
||||
@@ -62,17 +62,17 @@ For the current mode-to-model mappings, see the [Auto Model user docs](/docs/cod
|
||||
|
||||
**Who it's for**: Users who want to try Kilo without a credit card, students, hobbyists, and anyone exploring AI-assisted coding.
|
||||
|
||||
**What it does**: Automatically maps to the best available free model(s) for each mode. As free model availability changes due to promotional periods, the mapping updates transparently. Users always get the best free option without having to track which models are currently available.
|
||||
**What it does**: Splits requests across the best available free models, weighted by a deterministic per-session hash so a given session sticks with one model. As free model availability changes due to promotional periods, the split and the underlying models are updated transparently server-side. Users always get the best free option without having to track which models are currently available.
|
||||
|
||||
**Pricing**: Free. No credits required.
|
||||
|
||||
**Constraints**: Free models may not provide sufficient breadth to justify different models per mode. In that case, a single model may be used for all modes. Quality will be lower than Frontier or Balanced tiers — this is a tradeoff users accept by choosing free.
|
||||
**Constraints**: Free models do not vary by mode — the same model is used for every mode within a session. Quality will be lower than Frontier or Balanced tiers — this is a tradeoff users accept by choosing free.
|
||||
|
||||
### Auto: Small (internal)
|
||||
|
||||
**Who it's for**: Not user-facing. Used internally by Kilo for lightweight background tasks (session titles, commit messages, conversation summaries).
|
||||
|
||||
**What it does**: Automatically selects the right small model for lightweight tasks. When credits are available, it uses a fast paid small model.
|
||||
**What it does**: Automatically selects the right small model for lightweight tasks. When the account has a positive balance, it uses a fast paid small model; otherwise it falls back to a free small model.
|
||||
|
||||
**Why it matters**: Users never think about background tasks, and they shouldn't have to. Auto: Small ensures these tasks always work, always feel fast, and never waste credits on an expensive model when a cheap one will do.
|
||||
|
||||
@@ -115,8 +115,8 @@ The Kilo API at `api.kilo.ai` defines which underlying models each `kilo-auto/*`
|
||||
{
|
||||
"opencode": {
|
||||
"variants": {
|
||||
"architect": { "model": "anthropic/claude-opus-4-6", ... },
|
||||
"code": { "model": "anthropic/claude-sonnet-4-6", ... }
|
||||
"architect": { "model": "anthropic/claude-opus-4.7", ... },
|
||||
"code": { "model": "anthropic/claude-sonnet-4.6", ... }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "CLI Config Schema"
|
||||
description: "How the Kilo CLI config JSON Schema is served at app.kilo.ai/config.json"
|
||||
---
|
||||
|
||||
# CLI Config Schema
|
||||
|
||||
The JSON Schema referenced by `"$schema": "https://app.kilo.ai/config.json"` in `kilo.json` files is served by the cloud repo. It is a runtime overlay of the upstream opencode schema with Kilo-specific additions on top.
|
||||
|
||||
## Flow
|
||||
|
||||
1. Client fetches `https://app.kilo.ai/config.json`.
|
||||
2. Cloud route `apps/web/src/app/config.json/route.ts` fetches `https://opencode.ai/config.json`, runs `merge()` on it, and returns the result.
|
||||
3. `merge()` overlays three sections from `apps/web/src/app/config.json/extras.ts`:
|
||||
- `top` — top-level keys like `commit_message`, `remote_control`, nullable `model` / `small_model`
|
||||
- `agents` — Kilo primary agents (`ask`, `debug`, `orchestrator`)
|
||||
- `experimental` — `codebase_search`, `openTelemetry`
|
||||
|
||||
## Adding a new Kilo-only config key
|
||||
|
||||
The source of truth is the zod schema in `packages/opencode/src/config/config.ts`. The cloud overlay must match it.
|
||||
|
||||
1. Add the zod field with a `kilocode_change` marker in `config.ts`.
|
||||
2. Generate the JSON Schema shape: `bun --bun packages/opencode/script/schema.ts /tmp/kilo.json`, then `jq '.properties.<new_key>' /tmp/kilo.json`.
|
||||
3. Paste the shape into the correct bucket in `apps/web/src/app/config.json/extras.ts` in the [cloud repo](https://github.com/Kilo-Org/cloud).
|
||||
- Top-level → `top`; under `experimental` → `experimental`; new primary agent → `agents`; anywhere else → add a new bucket and extend `merge()` in `route.ts`.
|
||||
4. Add an assertion in `apps/web/src/tests/cli-config-schema.test.ts`.
|
||||
|
||||
If step 3 is skipped, users with `$schema: https://app.kilo.ai/config.json` will see "unknown property" warnings for the new key.
|
||||
|
||||
## Caching
|
||||
|
||||
The cloud route caches the upstream fetch for 1 hour (`next: { revalidate: 3600 }`) and emits `s-maxage=3600, stale-while-revalidate=3600`, so the response is served from the Cloudflare + Vercel edge cache for all but one request per hour per region.
|
||||
@@ -58,6 +58,33 @@ git checkout -b docs/your-change-description
|
||||
- Reference issue numbers when applicable
|
||||
- Keep commits focused on a single change
|
||||
|
||||
### Changesets
|
||||
|
||||
User-facing changes (features, fixes, breaking changes) require a changeset file so the update shows up in the next release notes. Run the interactive tool, or create the file by hand:
|
||||
|
||||
```bash
|
||||
bunx changeset add
|
||||
```
|
||||
|
||||
Or create `.changeset/<slug>.md` manually:
|
||||
|
||||
```md
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Short description of the change for the changelog.
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Use `patch` for bug fixes, `minor` for new features, `major` for breaking changes.
|
||||
- Descriptions are read by end users in release notes — keep them concise and feature-oriented. Describe **what changed from the user's perspective**, not implementation details.
|
||||
- Write in imperative mood (e.g. "Support exporting conversations as markdown" rather than "Add a new export handler that serializes session messages to .md files").
|
||||
- Changesets are consumed at release time by the `publish.yml` workflow, which generates changelog entries for the GitHub release notes.
|
||||
|
||||
Skip the changeset only for internal refactors, CI tweaks, test-only changes, or docs that do not affect users.
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
- Run the test suite:
|
||||
|
||||
@@ -11,7 +11,7 @@ When working on complex tasks, conversations with Kilo Code can grow long and co
|
||||
|
||||
## The Problem: Context Window Limits
|
||||
|
||||
Every AI model has a maximum context window - a limit on how much text it can process at once. As your conversation grows with code snippets, file contents, and back-and-forth discussions, you may approach this limit. When this happens, you might experience:
|
||||
Every AI model has a maximum context window — a limit on how much text it can process at once. As your conversation grows with code snippets, file contents, and back-and-forth discussions, you may approach this limit. When this happens, you might experience:
|
||||
|
||||
- Slower responses as the model processes more tokens
|
||||
- Higher API costs due to increased token usage
|
||||
@@ -22,47 +22,47 @@ Every AI model has a maximum context window - a limit on how much text it can pr
|
||||
|
||||
## The Solution: Auto-Compaction
|
||||
|
||||
The new platform uses a **Compaction** system to manage context automatically. When your conversation approaches the token limit, compaction kicks in and produces a structured summary that captures:
|
||||
Kilo Code uses a **Compaction** system to manage context automatically. When your conversation approaches the token limit, compaction kicks in and produces a structured summary that captures:
|
||||
|
||||
- The overall goal of the session
|
||||
- Key discoveries made along the way
|
||||
- Instructions given along the way
|
||||
- Key discoveries made
|
||||
- What has been accomplished so far
|
||||
- Files that were modified
|
||||
- Relevant files and directories
|
||||
|
||||
This summary replaces the earlier conversation history, freeing up context window space while maintaining continuity in your work.
|
||||
|
||||
## How Compaction Works
|
||||
## How Compaction Triggers
|
||||
|
||||
### Automatic Compaction
|
||||
### Automatic trigger
|
||||
|
||||
Compaction triggers automatically when the conversation reaches the `usableWindow` token threshold. The full conversation history is sent to a dedicated **compaction agent**, which produces a structured summary. This happens in the background without interrupting your workflow.
|
||||
Kilo tracks the total token count for the session — input, output, and cached reads and writes — and compares it to the model's context window. Compaction runs when the total fills the window minus a reserved buffer of headroom kept free for the next turn.
|
||||
|
||||
How the buffer is chosen depends on what the model declares. When the model advertises a separate input limit, the buffer defaults to 20,000 tokens (or the model's maximum output size, whichever is smaller). When the model only declares a single context window, Kilo instead reserves the model's full output cap — up to 32,000 tokens.
|
||||
|
||||
Custom models that do not declare a context window are not tracked, and auto-compaction does not run for them.
|
||||
|
||||
### Context Pruning
|
||||
|
||||
In addition to compaction, the system can **prune** old tool outputs to reclaim context space incrementally. Tool results older than a 40,000-token recency window are replaced with `"[Old tool result content cleared]"`. This is a lighter-weight mechanism that runs alongside full compaction.
|
||||
Between turns, Kilo also runs a lighter **prune** pass. It walks completed tool outputs outside a 40,000-token recency window and replaces them with `"[Old tool result content cleared]"`. Pruning runs incrementally so large tool outputs don't consume space forever, even before full compaction is needed.
|
||||
|
||||
### Manual Compaction
|
||||
|
||||
You can also trigger compaction manually:
|
||||
You can trigger compaction at any time:
|
||||
|
||||
- **CLI TUI**: Press `<leader>c` to compact the current session
|
||||
- **Extension Webview**: Send a `CompactRequest` message to trigger compaction
|
||||
- **Slash command**: type `/compact` in chat (also findable by typing `smol` or `condense`)
|
||||
- **Task header button**: click the compact icon in the active task header
|
||||
- **Settings**: toggle auto-compaction in **Settings → Context**
|
||||
|
||||
{% callout type="info" %}
|
||||
There is no `/condense` chat command on the new platform. Use the keybinding or message-based invocation instead.
|
||||
{% /callout %}
|
||||
## Defaults
|
||||
|
||||
### The Compaction Process
|
||||
| Setting | Default | Effect |
|
||||
| --------------------- | -------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `compaction.auto` | `true` | Automatically compact when the usable window is reached |
|
||||
| `compaction.prune` | `true` | Clear old tool outputs beyond the 40K recency window |
|
||||
| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn — also defines the compaction trigger point |
|
||||
|
||||
When compaction is triggered:
|
||||
|
||||
1. **Threshold Check**: The system detects that context usage has reached the `usableWindow` limit
|
||||
2. **Agent Summarization**: The full conversation history is sent to a dedicated compaction agent
|
||||
3. **Structured Summary**: The agent produces a summary covering the goal, discoveries, accomplishments, and modified files
|
||||
4. **Replacement**: The detailed history is replaced with the compacted summary
|
||||
5. **Continuation**: You continue working with the freed-up context space
|
||||
|
||||
## Configuration Options
|
||||
## Configuration
|
||||
|
||||
Compaction is configured in your `kilo.jsonc` file:
|
||||
|
||||
@@ -70,64 +70,87 @@ Compaction is configured in your `kilo.jsonc` file:
|
||||
{
|
||||
"compaction": {
|
||||
"auto": true, // Enable or disable automatic compaction
|
||||
"reserved": 4096, // Number of tokens to reserve (keep free) after compaction
|
||||
"prune": true, // Enable pruning of old tool outputs beyond the recency window
|
||||
"reserved": 20000, // Token buffer kept free; smaller = later trigger, larger = earlier trigger
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| --------------------- | ------- | ------------------------------------------------------------------------ |
|
||||
| `compaction.auto` | boolean | Enable or disable automatic compaction when the context threshold is hit |
|
||||
| `compaction.reserved` | number | Number of tokens to reserve after compaction |
|
||||
| `compaction.prune` | boolean | Enable pruning of old tool outputs outside the 40K token recency window |
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------- | ------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `compaction.auto` | boolean | `true` | Enable or disable automatic compaction when the usable window is reached |
|
||||
| `compaction.prune` | boolean | `true` | Enable pruning of old tool outputs outside the 40K token recency window |
|
||||
| `compaction.reserved` | number | `min(20000, model_max_output)` | Token headroom reserved for the next turn. Applies only to models that advertise a separate input limit; models with a single context window use their full output cap as the reserve instead. |
|
||||
|
||||
### Use a different model for compaction
|
||||
|
||||
Summarization can use a cheaper or larger-context model than your main agent. Configure a dedicated compaction agent:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agent": {
|
||||
"compaction": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If no compaction agent is set, the current session's model is used.
|
||||
|
||||
### Environment overrides
|
||||
|
||||
| Variable | Effect |
|
||||
| ------------------------------------ | ------------------------------------------------- |
|
||||
| `KILO_DISABLE_AUTOCOMPACT=1` | Forces `compaction.auto = false` |
|
||||
| `KILO_DISABLE_PRUNE=1` | Forces `compaction.prune = false` |
|
||||
| `KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Overrides the 32,000 default output-token ceiling |
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
## The Solution: Auto-Compaction
|
||||
|
||||
The new platform uses a **Compaction** system to manage context automatically. When your conversation approaches the token limit, compaction kicks in and produces a structured summary that captures:
|
||||
Kilo CLI uses a **Compaction** system to manage context automatically. When your conversation approaches the token limit, compaction kicks in and produces a structured summary that captures:
|
||||
|
||||
- The overall goal of the session
|
||||
- Key discoveries made along the way
|
||||
- Instructions given along the way
|
||||
- Key discoveries made
|
||||
- What has been accomplished so far
|
||||
- Files that were modified
|
||||
- Relevant files and directories
|
||||
|
||||
This summary replaces the earlier conversation history, freeing up context window space while maintaining continuity in your work.
|
||||
|
||||
## How Compaction Works
|
||||
## How Compaction Triggers
|
||||
|
||||
### Automatic Compaction
|
||||
### Automatic trigger
|
||||
|
||||
Compaction triggers automatically when the conversation reaches the `usableWindow` token threshold. The full conversation history is sent to a dedicated **compaction agent**, which produces a structured summary. This happens in the background without interrupting your workflow.
|
||||
Kilo tracks the total token count for the session — input, output, and cached reads and writes — and compares it to the model's context window. Compaction runs when the total fills the window minus a reserved buffer of headroom kept free for the next turn.
|
||||
|
||||
How the buffer is chosen depends on what the model declares. When the model advertises a separate input limit, the buffer defaults to 20,000 tokens (or the model's maximum output size, whichever is smaller). When the model only declares a single context window, Kilo instead reserves the model's full output cap — up to 32,000 tokens.
|
||||
|
||||
[Custom models](/docs/code-with-ai/agents/custom-models) that do not declare a context window are not tracked, and auto-compaction does not run for them.
|
||||
|
||||
### Context Pruning
|
||||
|
||||
In addition to compaction, the system can **prune** old tool outputs to reclaim context space incrementally. Tool results older than a 40,000-token recency window are replaced with `"[Old tool result content cleared]"`. This is a lighter-weight mechanism that runs alongside full compaction.
|
||||
Between turns, Kilo also runs a lighter **prune** pass. It walks completed tool outputs outside a 40,000-token recency window and replaces them with `"[Old tool result content cleared]"`. Pruning runs incrementally so large tool outputs don't consume space forever, even before full compaction is needed.
|
||||
|
||||
### Manual Compaction
|
||||
|
||||
You can also trigger compaction manually:
|
||||
You can trigger compaction at any time:
|
||||
|
||||
- **CLI TUI**: Press `<leader>c` to compact the current session
|
||||
- **Extension Webview**: Send a `CompactRequest` message to trigger compaction
|
||||
- **Slash command**: type `/compact` in the TUI (alias: `/summarize`)
|
||||
- **Keybinding**: press `<leader>c` in the TUI
|
||||
|
||||
{% callout type="info" %}
|
||||
There is no `/condense` chat command on the new platform. Use the keybinding or message-based invocation instead.
|
||||
{% /callout %}
|
||||
## Defaults
|
||||
|
||||
### The Compaction Process
|
||||
| Setting | Default | Effect |
|
||||
| --------------------- | -------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `compaction.auto` | `true` | Automatically compact when the usable window is reached |
|
||||
| `compaction.prune` | `true` | Clear old tool outputs beyond the 40K recency window |
|
||||
| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn — also defines the compaction trigger point |
|
||||
|
||||
When compaction is triggered:
|
||||
|
||||
1. **Threshold Check**: The system detects that context usage has reached the `usableWindow` limit
|
||||
2. **Agent Summarization**: The full conversation history is sent to a dedicated compaction agent
|
||||
3. **Structured Summary**: The agent produces a summary covering the goal, discoveries, accomplishments, and modified files
|
||||
4. **Replacement**: The detailed history is replaced with the compacted summary
|
||||
5. **Continuation**: You continue working with the freed-up context space
|
||||
|
||||
## Configuration Options
|
||||
## Configuration
|
||||
|
||||
Compaction is configured in your `kilo.jsonc` file:
|
||||
|
||||
@@ -135,17 +158,41 @@ Compaction is configured in your `kilo.jsonc` file:
|
||||
{
|
||||
"compaction": {
|
||||
"auto": true, // Enable or disable automatic compaction
|
||||
"reserved": 4096, // Number of tokens to reserve (keep free) after compaction
|
||||
"prune": true, // Enable pruning of old tool outputs beyond the recency window
|
||||
"reserved": 20000, // Token buffer kept free; smaller = later trigger, larger = earlier trigger
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| --------------------- | ------- | ------------------------------------------------------------------------ |
|
||||
| `compaction.auto` | boolean | Enable or disable automatic compaction when the context threshold is hit |
|
||||
| `compaction.reserved` | number | Number of tokens to reserve after compaction |
|
||||
| `compaction.prune` | boolean | Enable pruning of old tool outputs outside the 40K token recency window |
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------- | ------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `compaction.auto` | boolean | `true` | Enable or disable automatic compaction when the usable window is reached |
|
||||
| `compaction.prune` | boolean | `true` | Enable pruning of old tool outputs outside the 40K token recency window |
|
||||
| `compaction.reserved` | number | `min(20000, model_max_output)` | Token headroom reserved for the next turn. Applies only to models that advertise a separate input limit; models with a single context window use their full output cap as the reserve instead. |
|
||||
|
||||
### Use a different model for compaction
|
||||
|
||||
Summarization can use a cheaper or larger-context model than your main agent. Configure a dedicated compaction agent:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agent": {
|
||||
"compaction": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If no compaction agent is set, the current session's model is used.
|
||||
|
||||
### Environment overrides
|
||||
|
||||
| Variable | Effect |
|
||||
| ------------------------------------ | ------------------------------------------------- |
|
||||
| `KILO_DISABLE_AUTOCOMPACT=1` | Forces `compaction.auto = false` |
|
||||
| `KILO_DISABLE_PRUNE=1` | Forces `compaction.prune = false` |
|
||||
| `KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Overrides the 32,000 default output-token ceiling |
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VSCode (Legacy)" %}
|
||||
@@ -219,17 +266,26 @@ If the condensed summary doesn't capture important details:
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Condense
|
||||
### When to Compact
|
||||
|
||||
- **Long sessions**: If you've been working for an extended period on a complex task
|
||||
- **Before major transitions**: When switching to a different aspect of your project
|
||||
- **When prompted**: When Kilo Code suggests condensing or compaction due to context limits
|
||||
- **When approaching limits**: Run `/compact` manually before hitting the automatic trigger if you want control over _when_ the summary is produced
|
||||
|
||||
### Tuning `compaction.reserved`
|
||||
|
||||
On models that advertise a separate input limit, the `reserved` value is a trade-off:
|
||||
|
||||
- **Lower value** (e.g. `10000`) → compaction triggers later, you get more turns out of the raw window, but you risk a mid-turn context overflow if a single response is larger than the buffer.
|
||||
- **Higher value** (e.g. `40000`) → compaction triggers earlier, fewer overflow errors, but shorter effective conversations between summaries.
|
||||
|
||||
The default of `~20K` is tuned to leave room for a full-size assistant response plus tool output. The setting has no effect on models with a single context window, which always reserve their full output cap instead.
|
||||
|
||||
### Maintaining Context Quality
|
||||
|
||||
- **Be specific in your initial task**: A clear task description helps create better summaries
|
||||
- **Use AGENTS.md**: Combine with [AGENTS.md](/docs/customize/agents-md) for persistent project context that doesn't need to be condensed
|
||||
- **Review the summary**: After condensing or compaction, the summary is visible in your chat history
|
||||
- **Use AGENTS.md**: Combine with [AGENTS.md](/docs/customize/agents-md) for persistent project context that doesn't need to be compacted
|
||||
- **Review the summary**: After compaction, the summary is visible in your chat history
|
||||
|
||||
## Related Features
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ This returns model information including pricing, context window, and supported
|
||||
|
||||
| Model ID | Provider | Description |
|
||||
| ------------------------------- | --------- | ----------------------------------------------- |
|
||||
| `anthropic/claude-opus-4.6` | Anthropic | Most capable Claude model for complex reasoning |
|
||||
| `anthropic/claude-opus-4.7` | Anthropic | Most capable Claude model for complex reasoning |
|
||||
| `anthropic/claude-sonnet-4.6` | Anthropic | Balanced performance and cost |
|
||||
| `anthropic/claude-haiku-4.5` | Anthropic | Fast and cost-effective |
|
||||
| `openai/gpt-5.4` | OpenAI | Latest GPT model |
|
||||
@@ -76,42 +76,45 @@ Provided under the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia
|
||||
|
||||
Kilo Auto virtual models automatically select the best underlying model based on the task type. The selection is controlled by the `x-kilocode-mode` request header.
|
||||
|
||||
{% callout type="info" title="Underlying models can change" %}
|
||||
The mappings below reflect the current routing. The underlying models behind each `kilo-auto/*` tier are updated server-side as better options become available or as providers change pricing and availability — the tier IDs themselves remain stable.
|
||||
{% /callout %}
|
||||
|
||||
### `kilo-auto/frontier`
|
||||
|
||||
Highest performance and capability for any task.
|
||||
Highest performance and capability for any task. Frontier requests are sent with medium reasoning effort and medium verbosity.
|
||||
|
||||
| Mode | Resolved Model |
|
||||
| -------------------------------------------------------------- | ----------------------------- |
|
||||
| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `anthropic/claude-opus-4.6` |
|
||||
| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `anthropic/claude-opus-4.7` |
|
||||
| `build`, `explore`, `code` | `anthropic/claude-sonnet-4.6` |
|
||||
| Default (no mode specified) | `anthropic/claude-sonnet-4.6` |
|
||||
| Default (no / unknown mode) | `anthropic/claude-sonnet-4.6` |
|
||||
|
||||
### `kilo-auto/balanced`
|
||||
|
||||
Great balance of price and capability.
|
||||
|
||||
| Mode | Resolved Model |
|
||||
| -------------------------------------------------------------- | ---------------------- |
|
||||
| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `openai/gpt-5.3-codex` |
|
||||
| `build`, `explore`, `code` | `openai/gpt-5.3-codex` |
|
||||
| Default (no mode specified) | `openai/gpt-5.3-codex` |
|
||||
|
||||
### `kilo-auto/free`
|
||||
|
||||
Free with limited capability. No credits required.
|
||||
Great balance of price and capability. Balanced routes to the same model regardless of mode, with low reasoning effort.
|
||||
|
||||
| Mode | Resolved Model |
|
||||
| --------- | ---------------------- |
|
||||
| All modes | `minimax/minimax-m2.5` |
|
||||
| All modes | `openai/gpt-5.3-codex` |
|
||||
|
||||
### `kilo-auto/free`
|
||||
|
||||
Free with limited capability. No credits required. Requests are split across the available free models; the mapping updates server-side as free model availability shifts.
|
||||
|
||||
| Routing | Resolved Model |
|
||||
| ------- | ----------------------------- |
|
||||
| 80% | `minimax/minimax-m2.5:free` |
|
||||
| 20% | `stepfun/step-3.5-flash:free` |
|
||||
|
||||
### `kilo-auto/small`
|
||||
|
||||
Automatically routes to a small, fast model.
|
||||
Automatically routes to a small, fast model for lightweight background tasks (session titles, commit messages, summaries).
|
||||
|
||||
| Mode | Resolved Model |
|
||||
| ------------- | -------------------- |
|
||||
| Default | `openai/gpt-5-nano` |
|
||||
| Free fallback | `openai/gpt-oss-20b` |
|
||||
| Condition | Resolved Model |
|
||||
| ------------------------- | -------------------------------- |
|
||||
| Account has paid balance | `google/gemma-4-31b-it` |
|
||||
| No balance / free account | `google/gemma-4-26b-a4b-it:free` |
|
||||
|
||||
### Example usage
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ That's it! You're ready to [start your first task](/docs/getting-started/quickst
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
{% callout type="tip" title="Bonus Credits" %}
|
||||
[Add credits to your account](https://app.kilo.ai/profile) and get $20 bonus credits, or sign up for [Kilo Pass](https://kilo.ai/features/kilo-pass).
|
||||
{% callout type="tip" title="Add Credits" %}
|
||||
[Add credits to your account](https://app.kilo.ai/profile), or sign up for [Kilo Pass](https://kilo.ai/features/kilo-pass).
|
||||
{% /callout %}
|
||||
|
||||
## Kilo Gateway API Key
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
---
|
||||
title: "Discord"
|
||||
description: "Connect your KiloClaw agent to Discord"
|
||||
description: "Use KiloClaw with Discord: setup, DM access control, and channel configuration"
|
||||
---
|
||||
|
||||
# Discord
|
||||
|
||||
Connect your KiloClaw agent to Discord by creating a bot in the Discord Developer Portal and linking it to your KiloClaw dashboard.
|
||||
This page covers everything you need to use KiloClaw with Discord: connecting your bot, controlling who can DM it, and adding it to specific channels.
|
||||
|
||||
## Connecting KiloClaw to Discord
|
||||
|
||||
Create a bot in the Discord Developer Portal and link it to your KiloClaw dashboard.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -65,3 +69,78 @@ After saving your token, click **Redeploy** (the yellow button at the top of the
|
||||
3. You should get a response back with a pairing code
|
||||
4. Return to [app.kilo.ai/claw](https://app.kilo.ai/claw) and confirm the pairing code and approve
|
||||
5. You should now be able to chat with the bot from Discord
|
||||
|
||||
## Restricting KiloClaw to DMs Only (Just You)
|
||||
|
||||
By default, KiloClaw will respond to any DMs. To lock it down to only DMs with you:
|
||||
|
||||
### Step 1: Find your Discord user ID
|
||||
|
||||
1. In Discord, go to **User Settings** → **Advanced** → enable **Developer Mode**
|
||||
2. Right-click your own avatar or username → **Copy User ID**
|
||||
|
||||
Your user ID is a large number (e.g. `987654321098765432`).
|
||||
|
||||
### Step 2: Configure DM-only access
|
||||
|
||||
Tell your KiloClaw agent (via DM):
|
||||
|
||||
> "Set Discord DM policy to allowlist with my user ID `987654321098765432` and disable guild responses."
|
||||
|
||||
Or configure it directly in the OpenClaw Control UI config:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"discord": {
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": ["987654321098765432"],
|
||||
"groupPolicy": "disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding KiloClaw to a Specific Discord Channel
|
||||
|
||||
By default, your KiloClaw will not respond in channels, even if added. To have KiloClaw participate in a specific channel:
|
||||
|
||||
### Step 1: Get your server and channel IDs
|
||||
|
||||
With Developer Mode enabled (User Settings → Advanced → Developer Mode):
|
||||
|
||||
- Right-click the **server icon** → **Copy Server ID**
|
||||
- Right-click the **channel name** in the sidebar → **Copy Channel ID**
|
||||
|
||||
### Step 2: Configure the channel
|
||||
|
||||
Tell your KiloClaw agent:
|
||||
|
||||
> "Add Discord server `YOUR_SERVER_ID` and channel `YOUR_CHANNEL_ID` to the allowlist. Only respond to user `YOUR_USER_ID`."
|
||||
|
||||
Or configure it directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"discord": {
|
||||
"groupPolicy": "allowlist",
|
||||
"guilds": {
|
||||
"YOUR_SERVER_ID": {
|
||||
"requireMention": true,
|
||||
"users": ["YOUR_USER_ID"],
|
||||
"channels": {
|
||||
"YOUR_CHANNEL_ID": { "allow": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `requireMention: false` if you want the bot to respond to every message without needing an @mention.
|
||||
|
||||
{% callout type="tip" %}
|
||||
Non-listed channels in a guild that has a `channels` block configured are automatically denied. Add each channel you want explicitly. See the [OpenClaw Discord documentation](https://docs.openclaw.ai/channels/discord) for advanced access control options.
|
||||
{% /callout %}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
---
|
||||
title: "Slack"
|
||||
description: "Connect your KiloClaw agent to Slack"
|
||||
description: "Using KiloClaw with Slack"
|
||||
---
|
||||
|
||||
# Slack
|
||||
|
||||
This page covers everything you need to use KiloClaw with Slack: connecting your bot, controlling who can DM it, and adding it to channels.
|
||||
|
||||
## Connecting KiloClaw to Slack
|
||||
|
||||
{% youtube url="https://youtu.be/Q5bt-qH-_pY" title="Slack Setup Guide" caption="How to connect your KiloClaw agent to Slack" /%}
|
||||
|
||||
Connect your KiloClaw agent to Slack by creating a Slack app from the OpenClaw manifest and linking it to your KiloClaw dashboard.
|
||||
Create a Slack app from the OpenClaw manifest and link it to your KiloClaw dashboard.
|
||||
|
||||
## Step 1: Create a Slack App from the OpenClaw Manifest
|
||||
### Step 1: Create a Slack App from the OpenClaw Manifest
|
||||
|
||||
1. Go to [Slack App Management](https://api.slack.com/apps) and click **Create New App** → **From a Manifest**
|
||||
2. Copy the manifest from the [OpenClaw docs](https://docs.openclaw.ai/channels/slack#manifest-and-scope-checklist)
|
||||
@@ -19,7 +23,7 @@ Connect your KiloClaw agent to Slack by creating a Slack app from the OpenClaw m
|
||||
- Update the slash command if desired (e.g., `/kiloclaw`)
|
||||
5. Click **Create**
|
||||
|
||||
## Step 2: Generate Tokens
|
||||
### Step 2: Generate Tokens
|
||||
|
||||
You need two tokens from Slack:
|
||||
|
||||
@@ -36,7 +40,7 @@ You need two tokens from Slack:
|
||||
2. Install the app to your workspace
|
||||
3. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
|
||||
|
||||
## Step 3: Connect Slack to KiloClaw
|
||||
### Step 3: Connect Slack to KiloClaw
|
||||
|
||||
1. In the [KiloClaw UI](https://app.kilo.ai/claw), find the Slack integration section (may show "not configured")
|
||||
2. Enter both tokens:
|
||||
@@ -45,10 +49,94 @@ You need two tokens from Slack:
|
||||
3. Click **Save**
|
||||
4. Scroll to the top of the KiloClaw UI and click **Redeploy**. Wait for the instance to come back up
|
||||
|
||||
## Step 4: Pair Slack with KiloClaw
|
||||
### Step 4: Pair Slack with KiloClaw
|
||||
|
||||
1. In Slack, DM the app and send any message — this triggers the pairing flow
|
||||
|
||||
2. The app will return a pairing code
|
||||
3. Return to [app.kilocode.ai/claw](https://app.kilocode.ai/claw) and confirm the pairing code and approve
|
||||
3. Return to [app.kilo.ai/claw](https://app.kilo.ai/claw) and confirm the pairing code and approve
|
||||
4. You should now be able to DM the bot from Slack. You will need to add the bot to any individual channels and tell it to update its config for any channels you want it to participate in.
|
||||
|
||||
## Changing Response Behavior
|
||||
|
||||
By default, KiloClaw can respond to any DMs and will not respond in Slack channels, even if added.
|
||||
|
||||
## Making KiloClaw DM-Only (from you)
|
||||
|
||||
By default, KiloClaw will respond to DMs from any user in Slack.
|
||||
|
||||
### Step 1: Find your Slack user ID
|
||||
|
||||
1. In Slack, click your name or profile picture
|
||||
2. Click **Profile**
|
||||
3. Click the **More** (⋯) menu → **Copy member ID**
|
||||
|
||||
Your user ID starts with `U` (e.g. `U12345678`).
|
||||
|
||||
### Step 2: Configure DM-only access
|
||||
|
||||
Tell your KiloClaw agent:
|
||||
|
||||
> "Set my Slack DM policy to allowlist with my user ID `U12345678` and disable group/channel responses."
|
||||
|
||||
Or configure it directly in the OpenClaw Control UI config:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"slack": {
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": ["U12345678"],
|
||||
"groupPolicy": "disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows only your user ID to DM the bot and blocks it from responding in any channels.
|
||||
|
||||
## Adding KiloClaw to a Slack Channel
|
||||
|
||||
By default, KiloClaw will not respond in Slack channels, even if added. To have KiloClaw participate in a Slack channel:
|
||||
|
||||
### Step 1: Invite the bot to the channel
|
||||
|
||||
1. Open the Slack channel where you want to add the bot
|
||||
2. Type `/invite @YourBotName` (use whatever name you gave your app)
|
||||
3. The bot should appear in the channel member list
|
||||
|
||||
### Step 2: Get the channel ID
|
||||
|
||||
Channel IDs are more reliable than names. To find a channel's ID:
|
||||
|
||||
1. Open the channel in Slack
|
||||
2. Click the channel name at the top to open channel details
|
||||
3. Scroll to the bottom — the channel ID starts with `C` (e.g. `C01234567`)
|
||||
|
||||
### Step 3: Configure the channel
|
||||
|
||||
Tell your KiloClaw agent (via DM):
|
||||
|
||||
> "Allow responses in Slack channel `C01234567`. Require an @mention to respond."
|
||||
|
||||
Or configure it directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"slack": {
|
||||
"groupPolicy": "allowlist",
|
||||
"channels": {
|
||||
"C01234567": {
|
||||
"requireMention": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `requireMention: false` if you want the bot to respond to every message in the channel without needing an @mention.
|
||||
|
||||
{% callout type="tip" %}
|
||||
You can restrict which channel members can trigger the bot by adding a `users` allowlist inside the channel config entry. See the [OpenClaw Slack documentation](https://docs.openclaw.ai/channels/slack) for advanced access control options.
|
||||
{% /callout %}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
---
|
||||
title: "Telegram"
|
||||
description: "Connect your KiloClaw agent to Telegram"
|
||||
description: "Use KiloClaw with Telegram: setup, DM access control, and group chat configuration"
|
||||
---
|
||||
|
||||
# Telegram
|
||||
|
||||
This page covers everything you need to use KiloClaw with Telegram: connecting your bot and adding it to group chats.
|
||||
|
||||
## Connecting KiloClaw to Telegram
|
||||
|
||||
{% youtube url="https://youtu.be/hIfKz073hGw" title="Telegram Setup Guide" caption="How to connect your KiloClaw agent to Telegram" /%}
|
||||
|
||||
Connect your KiloClaw agent to Telegram by creating a bot via BotFather and linking it to your KiloClaw dashboard.
|
||||
Create a bot via BotFather and link it to your KiloClaw dashboard.
|
||||
|
||||
1. Open Telegram and search for [@BotFather](https://t.me/BotFather)
|
||||
2. Send `/newbot` and follow the prompts to create your bot
|
||||
@@ -22,5 +26,60 @@ Connect your KiloClaw agent to Telegram by creating a bot via BotFather and link
|
||||
|
||||
You can remove or replace a configured token at any time.
|
||||
|
||||
> ℹ️ **Info**
|
||||
> Advanced settings such as DM policy, allow lists, and groups can be configured in the OpenClaw Control UI after connecting.
|
||||
## Adding KiloClaw to a Telegram Group Chat
|
||||
|
||||
By default, KiloClaw will not participate in a group chat, even if added. If you would like to use your KiloClaw in a group chat, you must configure the KiloClaw settings.
|
||||
|
||||
### Step 1: Add the bot to your group
|
||||
|
||||
1. Open the Telegram group where you want to add your bot
|
||||
2. Tap the group name at the top to open group info
|
||||
3. Tap **Add Members**
|
||||
4. Search for your bot's username and add it
|
||||
|
||||
### Step 2: Set group visibility (Privacy Mode)
|
||||
|
||||
By default, Telegram bots only see messages that directly mention them. To allow your bot to see all group messages:
|
||||
|
||||
1. Open a chat with [@BotFather](https://t.me/BotFather)
|
||||
2. Send `/setprivacy` and select your bot
|
||||
3. Choose **Disable**
|
||||
4. Remove the bot from the group and re-add it for the change to take effect
|
||||
|
||||
### Step 3: Get the group chat ID
|
||||
|
||||
You need the group's chat ID to configure access. Use one of these methods:
|
||||
|
||||
- Forward a message from the group to [@userinfobot](https://t.me/userinfobot) — it will show the chat ID
|
||||
- Or run `openclaw logs --follow` after sending a message in the group and read the `chat.id` value
|
||||
|
||||
Group and supergroup IDs are negative numbers (e.g. `-1001234567890`).
|
||||
|
||||
### Step 4: Configure the group in OpenClaw
|
||||
|
||||
Tell your KiloClaw bot to add the group to its configuration. You can do this via DM:
|
||||
|
||||
> "Add Telegram group `-1001234567890` to my allowed groups. Require a @mention to respond."
|
||||
|
||||
Or configure it directly in the OpenClaw Control UI config:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"groupPolicy": "allowlist",
|
||||
"groups": {
|
||||
"-1001234567890": {
|
||||
"requireMention": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `requireMention: false` if you want the bot to respond to every message in the group without needing to be @mentioned.
|
||||
|
||||
{% callout type="tip" %}
|
||||
To restrict which group members can trigger the bot, add your user IDs to `allowFrom` inside the group config. See the [OpenClaw groups documentation](https://docs.openclaw.ai/channels/groups) for advanced access control patterns.
|
||||
{% /callout %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 79 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 80 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.apertis.ai/v1>
|
||||
<!-- packages/opencode/src/provider/model-cache.ts -->
|
||||
@@ -53,6 +53,8 @@
|
||||
<!-- packages/opencode/src/lsp/server.ts -->
|
||||
- <https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts>
|
||||
<!-- packages/opencode/src/tool/edit.ts -->
|
||||
- <https://github.com/Kilo-Org/kilo-marketplace>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceContribute.tsx -->
|
||||
- <https://github.com/Kilo-Org/kilocode>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx -->
|
||||
- <https://github.com/Kilo-Org/kilocode/issues/6986>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
@@ -29,7 +29,7 @@
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
|
||||
@@ -118,6 +118,11 @@ export async function fetchKiloModels(options?: {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip models that don't support tools — Kilo requires tool calling
|
||||
if (!model.supported_parameters?.includes("tools")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const transformedModel = transformToModelDevFormat(model)
|
||||
models[model.id] = transformedModel
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.10",
|
||||
"version": "7.2.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "عدة جلسات تعمل وستتوقف",
|
||||
"marketplace.warning.installAnyway": "تثبيت على أي حال",
|
||||
"marketplace.warning.cancel": "إلغاء",
|
||||
"marketplace.contribute.prompt": "هل تفتقد مهارة أو وضعًا أو خادم MCP؟",
|
||||
"marketplace.contribute.cta": "ساهم على GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Várias sessões estão em execução e serão interrompidas",
|
||||
"marketplace.warning.installAnyway": "Instalar mesmo assim",
|
||||
"marketplace.warning.cancel": "Cancelar",
|
||||
"marketplace.contribute.prompt": "Está faltando uma skill, modo ou servidor MCP?",
|
||||
"marketplace.contribute.cta": "Contribuir no GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -74,4 +74,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Nekoliko sesija je pokrenuto i bit će prekinuto",
|
||||
"marketplace.warning.installAnyway": "Instaliraj svejedno",
|
||||
"marketplace.warning.cancel": "Otkaži",
|
||||
"marketplace.contribute.prompt": "Nedostaje vještina, način rada ili MCP server?",
|
||||
"marketplace.contribute.cta": "Doprinesi na GitHub-u",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Flere sessioner kører og vil blive afbrudt",
|
||||
"marketplace.warning.installAnyway": "Installer alligevel",
|
||||
"marketplace.warning.cancel": "Annuller",
|
||||
"marketplace.contribute.prompt": "Mangler du en skill, tilstand eller MCP-server?",
|
||||
"marketplace.contribute.cta": "Bidrag på GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Mehrere Sitzungen laufen und werden unterbrochen",
|
||||
"marketplace.warning.installAnyway": "Trotzdem installieren",
|
||||
"marketplace.warning.cancel": "Abbrechen",
|
||||
"marketplace.contribute.prompt": "Fehlt ein Skill, Modus oder MCP-Server?",
|
||||
"marketplace.contribute.cta": "Auf GitHub beitragen",
|
||||
}
|
||||
|
||||
@@ -71,4 +71,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Several sessions are running and will be interrupted",
|
||||
"marketplace.warning.installAnyway": "Install anyway",
|
||||
"marketplace.warning.cancel": "Cancel",
|
||||
"marketplace.contribute.prompt": "Missing a skill, mode, or MCP server?",
|
||||
"marketplace.contribute.cta": "Contribute on GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Varias sesiones están en ejecución y se interrumpirán",
|
||||
"marketplace.warning.installAnyway": "Instalar de todas formas",
|
||||
"marketplace.warning.cancel": "Cancelar",
|
||||
"marketplace.contribute.prompt": "¿Falta una skill, modo o servidor MCP?",
|
||||
"marketplace.contribute.cta": "Contribuir en GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Plusieurs sessions sont en cours et seront interrompues",
|
||||
"marketplace.warning.installAnyway": "Installer quand même",
|
||||
"marketplace.warning.cancel": "Annuler",
|
||||
"marketplace.contribute.prompt": "Il manque une skill, un mode ou un serveur MCP ?",
|
||||
"marketplace.contribute.cta": "Contribuer sur GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -67,4 +67,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "複数のセッションが実行中で中断されます",
|
||||
"marketplace.warning.installAnyway": "それでもインストール",
|
||||
"marketplace.warning.cancel": "キャンセル",
|
||||
"marketplace.contribute.prompt": "スキル、モード、MCP サーバーが見つかりませんか?",
|
||||
"marketplace.contribute.cta": "GitHub で貢献する",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -67,4 +67,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "여러 세션이 실행 중이며 중단됩니다",
|
||||
"marketplace.warning.installAnyway": "그래도 설치",
|
||||
"marketplace.warning.cancel": "취소",
|
||||
"marketplace.contribute.prompt": "스킬, 모드 또는 MCP 서버가 없나요?",
|
||||
"marketplace.contribute.cta": "GitHub에서 기여하기",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -71,4 +71,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Er zijn meerdere sessies actief en deze zullen worden onderbroken",
|
||||
"marketplace.warning.installAnyway": "Toch installeren",
|
||||
"marketplace.warning.cancel": "Annuleren",
|
||||
"marketplace.contribute.prompt": "Mist er een skill, modus of MCP-server?",
|
||||
"marketplace.contribute.cta": "Bijdragen op GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Flere økter kjører og vil bli avbrutt",
|
||||
"marketplace.warning.installAnyway": "Installer uansett",
|
||||
"marketplace.warning.cancel": "Avbryt",
|
||||
"marketplace.contribute.prompt": "Mangler du en skill, modus eller MCP-server?",
|
||||
"marketplace.contribute.cta": "Bidra på GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Kilka sesji jest uruchomionych i zostanie przerwanych",
|
||||
"marketplace.warning.installAnyway": "Zainstaluj mimo to",
|
||||
"marketplace.warning.cancel": "Anuluj",
|
||||
"marketplace.contribute.prompt": "Brakuje skilla, trybu lub serwera MCP?",
|
||||
"marketplace.contribute.cta": "Wnieś wkład na GitHubie",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Несколько сеансов выполняются и будут прерваны",
|
||||
"marketplace.warning.installAnyway": "Установить в любом случае",
|
||||
"marketplace.warning.cancel": "Отмена",
|
||||
"marketplace.contribute.prompt": "Не хватает навыка, режима или MCP-сервера?",
|
||||
"marketplace.contribute.cta": "Внести вклад на GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "มีหลายเซสชันกำลังทำงานและจะถูกขัดจังหวะ",
|
||||
"marketplace.warning.installAnyway": "ติดตั้งต่อไป",
|
||||
"marketplace.warning.cancel": "ยกเลิก",
|
||||
"marketplace.contribute.prompt": "ไม่พบทักษะ โหมด หรือเซิร์ฟเวอร์ MCP ที่ต้องการ?",
|
||||
"marketplace.contribute.cta": "ร่วมสมทบบน GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Birden fazla oturum çalışıyor ve kesintiye uğrayacak",
|
||||
"marketplace.warning.installAnyway": "Yine de yükle",
|
||||
"marketplace.warning.cancel": "İptal",
|
||||
"marketplace.contribute.prompt": "Bir yetenek, mod veya MCP sunucusu mu eksik?",
|
||||
"marketplace.contribute.cta": "GitHub'da katkıda bulun",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -69,4 +69,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "Виконується кілька сесій, їх буде перервано",
|
||||
"marketplace.warning.installAnyway": "Встановити все одно",
|
||||
"marketplace.warning.cancel": "Скасувати",
|
||||
"marketplace.contribute.prompt": "Бракує навички, режиму або MCP-сервера?",
|
||||
"marketplace.contribute.cta": "Зробити внесок на GitHub",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -66,4 +66,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "多个会话正在运行,将被中断",
|
||||
"marketplace.warning.installAnyway": "仍然安装",
|
||||
"marketplace.warning.cancel": "取消",
|
||||
"marketplace.contribute.prompt": "缺少技能、模式或 MCP 服务器?",
|
||||
"marketplace.contribute.cta": "在 GitHub 上贡献",
|
||||
}
|
||||
|
||||
Generated
+2
@@ -66,4 +66,6 @@ export const dict = {
|
||||
"marketplace.warning.busyMany": "多個工作階段正在執行,將被中斷",
|
||||
"marketplace.warning.installAnyway": "仍然安裝",
|
||||
"marketplace.warning.cancel": "取消",
|
||||
"marketplace.contribute.prompt": "缺少技能、模式或 MCP 伺服器?",
|
||||
"marketplace.contribute.cta": "在 GitHub 上貢獻",
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run JetBrains Plugin" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<configuration default="false" name="Run IDE (Backend)" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
@@ -10,10 +10,10 @@
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runIde" />
|
||||
<option value=":runIdeBackend" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
@@ -0,0 +1,27 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run IDE (Frontend)" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value=":runIdeFrontend" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run IDE (Split Mode)" type="CompoundRunConfigurationType" factoryName="Compound Run Configuration">
|
||||
<toRun type="GradleRunConfiguration" name="Run IDE (Backend)" />
|
||||
<toRun type="GradleRunConfiguration" name="Run IDE (Frontend)" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+21
-3
@@ -1,5 +1,7 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.cli.CliServer
|
||||
import ai.kilocode.backend.cli.KiloBackendCliManager
|
||||
import ai.kilocode.backend.util.IntellijLog
|
||||
import ai.kilocode.backend.util.KiloLog
|
||||
import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager
|
||||
@@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
@@ -89,8 +92,11 @@ class KiloBackendAppService private constructor(
|
||||
|
||||
val events: SharedFlow<SseEvent> get() = connection.events
|
||||
val api: DefaultApi? get() = connection.api
|
||||
val http: OkHttpClient? get() = connection.apiClient
|
||||
val port: Int get() = connection.port
|
||||
|
||||
val sessions = KiloBackendSessionManager(cs, log)
|
||||
val chat = KiloBackendChatManager(cs, log)
|
||||
val workspaces = KiloBackendWorkspaceManager(cs, sessions, log)
|
||||
|
||||
@Volatile var profile: KiloProfile200Response? = null
|
||||
@@ -230,7 +236,8 @@ class KiloBackendAppService private constructor(
|
||||
profile = prof
|
||||
config = cfg
|
||||
notifications = notifs
|
||||
sessions.start(connection.api!!, connection.events)
|
||||
sessions.start(connection.api!!, connection.apiClient!!, connection.port, connection.events)
|
||||
chat.start(connection.apiClient!!, connection.port, connection.events)
|
||||
workspaces.start(connection.api!!, connection.events)
|
||||
_appState.value = KiloAppState.Ready(
|
||||
AppData(
|
||||
@@ -256,8 +263,12 @@ class KiloBackendAppService private constructor(
|
||||
|
||||
/**
|
||||
* Fetch the user profile. Returns [FetchResult.ok] with the response
|
||||
* on success, [FetchResult.ok] with `null` when not logged in (401),
|
||||
* or [FetchResult.fail] on other errors. Never throws.
|
||||
* on success, [FetchResult.ok] with `null` when not logged in or when
|
||||
* the server cannot reach the profile endpoint. Never throws.
|
||||
*
|
||||
* Profile is optional — 401 (not logged in) and 5xx (gateway/network
|
||||
* errors) are both non-fatal. Only unexpected client errors are treated
|
||||
* as failures.
|
||||
*/
|
||||
private suspend fun fetchProfile(): FetchResult<KiloProfile200Response?> {
|
||||
val client = connection.api
|
||||
@@ -274,6 +285,12 @@ class KiloBackendAppService private constructor(
|
||||
log.warn("Profile fetch failed: HTTP ${e.statusCode}", e)
|
||||
logResponseBody("profile", e)
|
||||
FetchResult.fail("profile", e)
|
||||
} catch (e: ServerException) {
|
||||
// 5xx from the CLI — profile endpoint is unreachable (no auth,
|
||||
// gateway down, etc.). Treat the same as not-logged-in.
|
||||
log.warn("Profile fetch: server error (${e.statusCode}) — treating as unavailable", e)
|
||||
logResponseBody("profile", e)
|
||||
FetchResult.ok(null)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Profile fetch failed: ${e.message}", e)
|
||||
logResponseBody("profile", e)
|
||||
@@ -399,6 +416,7 @@ class KiloBackendAppService private constructor(
|
||||
eventWatcher?.cancel()
|
||||
}
|
||||
workspaces.stop()
|
||||
chat.stop()
|
||||
sessions.stop()
|
||||
profile = null
|
||||
config = null
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.cli.KiloCliDataParser
|
||||
import ai.kilocode.backend.util.KiloLog
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.PromptDto
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
/**
|
||||
* Chat orchestrator that handles message sending, history loading,
|
||||
* and SSE event routing for the agent chat UI.
|
||||
*
|
||||
* **Not an IntelliJ service** — owned by [KiloBackendAppService] which
|
||||
* calls [start] after [KiloAppState.Ready] and [stop] on disconnect.
|
||||
*
|
||||
* All JSON parsing is delegated to [KiloCliDataParser].
|
||||
*/
|
||||
class KiloBackendChatManager(
|
||||
private val cs: CoroutineScope,
|
||||
private val log: KiloLog,
|
||||
) {
|
||||
companion object {
|
||||
private val JSON_TYPE = "application/json".toMediaType()
|
||||
|
||||
private val CHAT_EVENTS = setOf(
|
||||
"message.updated",
|
||||
"message.removed",
|
||||
"message.part.updated",
|
||||
"message.part.delta",
|
||||
"session.turn.open",
|
||||
"session.turn.close",
|
||||
"session.error",
|
||||
)
|
||||
}
|
||||
|
||||
private val _events = MutableSharedFlow<ChatEventDto>(extraBufferCapacity = 128)
|
||||
val events: SharedFlow<ChatEventDto> = _events.asSharedFlow()
|
||||
|
||||
private var client: OkHttpClient? = null
|
||||
private var base: String? = null
|
||||
private var watcher: Job? = null
|
||||
|
||||
fun start(http: OkHttpClient, port: Int, sse: SharedFlow<SseEvent>) {
|
||||
client = http
|
||||
base = "http://127.0.0.1:$port"
|
||||
if (watcher?.isActive == true) return
|
||||
watcher = cs.launch {
|
||||
sse.collect { event ->
|
||||
if (event.type in CHAT_EVENTS) {
|
||||
log.debug("SSE chat event: type=${event.type}, data=${event.data.take(2000)}")
|
||||
val parsed = KiloCliDataParser.parseChatEvent(event.type, event.data)
|
||||
if (parsed != null) {
|
||||
_events.emit(parsed)
|
||||
} else {
|
||||
log.warn("SSE parse returned null for type=${event.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Chat manager started")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
watcher?.cancel()
|
||||
watcher = null
|
||||
client = null
|
||||
base = null
|
||||
log.info("Chat manager stopped")
|
||||
}
|
||||
|
||||
// ------ prompt ------
|
||||
|
||||
fun prompt(id: String, dir: String, prompt: PromptDto) {
|
||||
log.info("prompt: session=$id, dir=$dir, parts=${prompt.parts.size}, agent=${prompt.agent}, model=${prompt.providerID}/${prompt.modelID}")
|
||||
val http = requireClient()
|
||||
val url = requireBase()
|
||||
|
||||
val body = KiloCliDataParser.buildPromptJson(prompt)
|
||||
val target = "$url/session/$id/prompt_async?directory=${encode(dir)}"
|
||||
log.debug("prompt: POST $target, body=$body")
|
||||
val request = Request.Builder()
|
||||
.url(target)
|
||||
.post(body.toRequestBody(JSON_TYPE))
|
||||
.build()
|
||||
|
||||
try {
|
||||
http.newCall(request).execute().use { response ->
|
||||
val code = response.code
|
||||
if (!response.isSuccessful) {
|
||||
val raw = response.body?.string()
|
||||
log.warn("prompt_async failed: HTTP $code")
|
||||
log.debug("prompt_async error body: $raw")
|
||||
throw RuntimeException("prompt_async failed: HTTP $code")
|
||||
}
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
log.warn("prompt: HTTP call threw exception", e)
|
||||
throw RuntimeException("prompt_async HTTP call failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// ------ abort ------
|
||||
|
||||
fun abort(id: String, dir: String) {
|
||||
val http = requireClient()
|
||||
val url = requireBase()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$url/session/$id/abort?directory=${encode(dir)}")
|
||||
.post("".toRequestBody(JSON_TYPE))
|
||||
.build()
|
||||
|
||||
http.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
log.warn("abort failed: HTTP ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------ messages ------
|
||||
|
||||
fun messages(id: String, dir: String): List<MessageWithPartsDto> {
|
||||
val http = requireClient()
|
||||
val url = requireBase()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$url/session/$id/message?directory=${encode(dir)}")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return http.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
log.warn("messages failed: HTTP ${response.code}")
|
||||
return emptyList()
|
||||
}
|
||||
val raw = response.body?.string() ?: return emptyList()
|
||||
KiloCliDataParser.parseMessages(raw)
|
||||
}
|
||||
}
|
||||
|
||||
// ------ config update ------
|
||||
|
||||
fun updateConfig(dir: String, update: ConfigUpdateDto) {
|
||||
val http = requireClient()
|
||||
val url = requireBase()
|
||||
|
||||
val partial = KiloCliDataParser.buildConfigPartial(update)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$url/global/config")
|
||||
.patch(partial.toRequestBody(JSON_TYPE))
|
||||
.build()
|
||||
|
||||
http.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
val msg = response.body?.string() ?: "unknown error"
|
||||
log.warn("config update failed: HTTP ${response.code} — $msg")
|
||||
} else {
|
||||
log.info("Config updated: model=${update.model}, agent=${update.agent}, temp=${update.temperature}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------ utilities ------
|
||||
|
||||
private fun requireClient(): OkHttpClient =
|
||||
client ?: throw IllegalStateException("Chat manager not started")
|
||||
|
||||
private fun requireBase(): String =
|
||||
base ?: throw IllegalStateException("Chat manager not started")
|
||||
|
||||
private fun encode(value: String): String =
|
||||
java.net.URLEncoder.encode(value, "UTF-8")
|
||||
}
|
||||
+10
-8
@@ -1,7 +1,9 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.cli.KiloBackendHttpClients
|
||||
import ai.kilocode.backend.cli.KiloCliDataParser
|
||||
import ai.kilocode.backend.cli.CliServer
|
||||
import ai.kilocode.backend.util.IntellijLog
|
||||
import ai.kilocode.backend.util.KiloBackendHttpClients
|
||||
import ai.kilocode.backend.util.KiloLog
|
||||
import ai.kilocode.jetbrains.api.client.DefaultApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -64,7 +66,6 @@ class KiloConnectionService(
|
||||
private const val HEARTBEAT_TIMEOUT_MS = 15_000L
|
||||
private const val HEALTH_POLL_INTERVAL_MS = 10_000L
|
||||
private const val RECONNECT_DELAY_MS = 250L
|
||||
private val TYPE_REGEX = Regex(""""type"\s*:\s*"([^"]+)"""")
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
@@ -77,9 +78,13 @@ class KiloConnectionService(
|
||||
var api: DefaultApi? = null
|
||||
private set
|
||||
|
||||
private var apiClient: OkHttpClient? = null
|
||||
/** OkHttp client used for API calls — no call/read timeout. Null when disconnected. */
|
||||
var apiClient: OkHttpClient? = null
|
||||
private set
|
||||
private var healthClient: OkHttpClient? = null
|
||||
private var port = 0
|
||||
/** Port the CLI server is listening on. Zero when disconnected. */
|
||||
var port = 0
|
||||
private set
|
||||
private var password = ""
|
||||
|
||||
private val source = AtomicReference<EventSource?>(null)
|
||||
@@ -214,7 +219,7 @@ class KiloConnectionService(
|
||||
|
||||
override fun onEvent(src: EventSource, id: String?, type: String?, data: String) {
|
||||
lastEvent.set(System.currentTimeMillis())
|
||||
val kind = type ?: extractType(data)
|
||||
val kind = type ?: KiloCliDataParser.extractEventType(data)
|
||||
cs.launch { _events.emit(SseEvent(type = kind, data = data)) }
|
||||
}
|
||||
|
||||
@@ -327,9 +332,6 @@ class KiloConnectionService(
|
||||
_state.value = next
|
||||
}
|
||||
|
||||
internal fun extractType(data: String): String =
|
||||
TYPE_REGEX.find(data)?.groupValues?.get(1) ?: "unknown"
|
||||
|
||||
fun dispose() {
|
||||
disposed = true
|
||||
source.getAndSet(null)?.cancel()
|
||||
|
||||
+44
-48
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.cli.KiloCliDataParser
|
||||
import ai.kilocode.backend.util.KiloLog
|
||||
import ai.kilocode.jetbrains.api.client.DefaultApi
|
||||
import ai.kilocode.jetbrains.api.model.SessionStatus
|
||||
@@ -16,6 +17,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
@@ -29,30 +34,13 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
*
|
||||
* SSE `session.status` events are consumed directly from the events
|
||||
* flow passed to [start], keeping the live [statuses] map current.
|
||||
*
|
||||
* All raw JSON parsing is delegated to [KiloCliDataParser].
|
||||
*/
|
||||
class KiloBackendSessionManager(
|
||||
private val cs: CoroutineScope,
|
||||
private val log: KiloLog,
|
||||
) {
|
||||
companion object {
|
||||
private val FIELD_RE = ConcurrentHashMap<String, Regex>()
|
||||
|
||||
/** Extract a top-level string field from JSON without a full parser. */
|
||||
internal fun extractField(json: String, field: String): String? {
|
||||
val re = FIELD_RE.getOrPut(field) {
|
||||
Regex(""""$field"\s*:\s*"([^"]+)"""")
|
||||
}
|
||||
return re.find(json)?.groupValues?.get(1)
|
||||
}
|
||||
|
||||
/** Extract a string field nested one level deep. */
|
||||
internal fun extractNested(json: String, outer: String, inner: String): String? {
|
||||
val block = Regex(""""$outer"\s*:\s*\{([^}]+)}""")
|
||||
.find(json)?.groupValues?.get(1) ?: return null
|
||||
return extractField("{$block}", inner)
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-session directory overrides (sessionId → worktree path). */
|
||||
private val directories = ConcurrentHashMap<String, String>()
|
||||
|
||||
@@ -60,32 +48,34 @@ class KiloBackendSessionManager(
|
||||
val statuses: StateFlow<Map<String, SessionStatusDto>> = _statuses.asStateFlow()
|
||||
|
||||
private var client: DefaultApi? = null
|
||||
private var http: OkHttpClient? = null
|
||||
private var base: String? = null
|
||||
private var watcher: Job? = null
|
||||
|
||||
/**
|
||||
* Activate the session manager with a connected API client and SSE stream.
|
||||
* Called by [KiloBackendAppService] after [KiloAppState.Ready].
|
||||
*/
|
||||
fun start(api: DefaultApi, events: SharedFlow<SseEvent>) {
|
||||
fun start(api: DefaultApi, httpClient: OkHttpClient, port: Int, events: SharedFlow<SseEvent>) {
|
||||
client = api
|
||||
http = httpClient
|
||||
base = "http://127.0.0.1:$port"
|
||||
if (watcher?.isActive == true) return
|
||||
watcher = cs.launch {
|
||||
events.collect { event ->
|
||||
if (event.type == "session.status") {
|
||||
handleStatus(event.data)
|
||||
val pair = KiloCliDataParser.parseSessionStatus(event.data)
|
||||
if (pair != null) {
|
||||
_statuses.update { it + pair }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Session manager started")
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the session manager. Called by [KiloBackendAppService] on disconnect.
|
||||
*/
|
||||
fun stop() {
|
||||
watcher?.cancel()
|
||||
watcher = null
|
||||
client = null
|
||||
http = null
|
||||
base = null
|
||||
_statuses.value = emptyMap()
|
||||
log.info("Session manager stopped")
|
||||
}
|
||||
@@ -95,7 +85,6 @@ class KiloBackendSessionManager(
|
||||
|
||||
// ------ session CRUD ------
|
||||
|
||||
/** List root sessions for a directory and include current statuses. */
|
||||
fun list(dir: String): SessionListDto {
|
||||
seed(dir)
|
||||
val raw = requireClient().sessionList(directory = dir, roots = true)
|
||||
@@ -105,16 +94,34 @@ class KiloBackendSessionManager(
|
||||
return SessionListDto(mapped, relevant)
|
||||
}
|
||||
|
||||
/** Create a new session in the given directory. */
|
||||
fun create(dir: String): SessionDto =
|
||||
dto(requireClient().sessionCreate(directory = dir))
|
||||
|
||||
/**
|
||||
* Get a single session by ID.
|
||||
* Create a new session in the given directory.
|
||||
*
|
||||
* Uses the session list endpoint and filters by ID since
|
||||
* [DefaultApi] does not expose the single-session GET.
|
||||
* Uses raw HTTP because the generated client sends malformed JSON
|
||||
* for the optional request body (Content-Type set but empty body).
|
||||
*/
|
||||
fun create(dir: String): SessionDto {
|
||||
val h = http ?: throw IllegalStateException("Session manager not started")
|
||||
val url = base ?: throw IllegalStateException("Session manager not started")
|
||||
val encoded = java.net.URLEncoder.encode(dir, "UTF-8")
|
||||
log.info("Creating session: POST $url/session?directory=$encoded")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$url/session?directory=$encoded")
|
||||
.post("{}".toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
h.newCall(request).execute().use { response ->
|
||||
val raw = response.body?.string()
|
||||
if (!response.isSuccessful) {
|
||||
log.warn("Session create failed: HTTP ${response.code}, body=$raw")
|
||||
throw RuntimeException("Session create failed: HTTP ${response.code} — $raw")
|
||||
}
|
||||
log.info("Session created: HTTP ${response.code}")
|
||||
return KiloCliDataParser.parseSession(raw!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun get(id: String, dir: String): SessionDto {
|
||||
val all = requireClient().sessionList(directory = dir)
|
||||
val raw = all.firstOrNull { it.id == id }
|
||||
@@ -122,13 +129,11 @@ class KiloBackendSessionManager(
|
||||
return dto(raw)
|
||||
}
|
||||
|
||||
/** Delete a session. */
|
||||
fun delete(id: String, dir: String) {
|
||||
requireClient().sessionDelete(sessionID = id, directory = dir)
|
||||
directories.remove(id)
|
||||
}
|
||||
|
||||
/** Seed status map from the server for a specific directory. */
|
||||
fun seed(dir: String) {
|
||||
try {
|
||||
val raw = requireClient().sessionStatus(directory = dir)
|
||||
@@ -149,16 +154,7 @@ class KiloBackendSessionManager(
|
||||
fun getDirectory(id: String, fallback: String): String =
|
||||
directories[id] ?: fallback
|
||||
|
||||
// ------ SSE event handling ------
|
||||
|
||||
private fun handleStatus(data: String) {
|
||||
val id = extractField(data, "sessionID") ?: return
|
||||
val type = extractNested(data, "status", "type") ?: "idle"
|
||||
val msg = extractNested(data, "status", "message")
|
||||
_statuses.update { it + (id to SessionStatusDto(type, msg)) }
|
||||
}
|
||||
|
||||
// ------ mapping ------
|
||||
// ------ mapping (generated API model → DTO) ------
|
||||
|
||||
private fun dto(s: ai.kilocode.jetbrains.api.model.Session) = SessionDto(
|
||||
id = s.id,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.backend.app
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
/**
|
||||
* Abstraction over the CLI process lifecycle.
|
||||
+6
-54
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.backend.app
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import ai.kilocode.backend.util.IntellijLog
|
||||
import ai.kilocode.backend.util.KiloLog
|
||||
@@ -43,21 +43,11 @@ class KiloBackendCliManager(
|
||||
private var process: Process? = null
|
||||
private var hook: Thread? = null
|
||||
|
||||
/**
|
||||
* When true, the next [extractCli] call deletes and re-extracts the binary
|
||||
* regardless of the size check. Reset to false after extraction.
|
||||
*/
|
||||
@Volatile
|
||||
override var forceExtract = false
|
||||
|
||||
override fun process(): Process? = process
|
||||
|
||||
/**
|
||||
* Extract the CLI binary (if needed) and spawn `kilo serve`.
|
||||
*
|
||||
* Must be called under [KiloBackendAppService]'s mutex — no internal
|
||||
* synchronization is performed.
|
||||
*/
|
||||
override suspend fun init(): CliServer.State {
|
||||
return try {
|
||||
val path = extractCli()
|
||||
@@ -67,8 +57,6 @@ class KiloBackendCliManager(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warn("CLI startup failed", e)
|
||||
// If spawn started a process but timed out (or failed after start),
|
||||
// kill the orphaned process so it doesn't leak.
|
||||
process?.let { proc ->
|
||||
log.info("Cleaning up orphaned CLI process (pid=${proc.pid()})")
|
||||
process = null
|
||||
@@ -82,19 +70,12 @@ class KiloBackendCliManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given process as exited and clear state.
|
||||
* Called from the process monitor when the CLI process dies.
|
||||
*/
|
||||
override fun exited(proc: Process) {
|
||||
if (process != proc) return
|
||||
process = null
|
||||
uninstall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the running CLI process and reset state so the next [init] spawns fresh.
|
||||
*/
|
||||
override fun stop() {
|
||||
val proc = process ?: return
|
||||
process = null
|
||||
@@ -209,38 +190,27 @@ class KiloBackendCliManager(
|
||||
val proc = process ?: return
|
||||
process = null
|
||||
uninstall()
|
||||
|
||||
kill(proc, "Disposing")
|
||||
}
|
||||
|
||||
private fun install(proc: Process) {
|
||||
uninstall()
|
||||
|
||||
val next = Thread({
|
||||
log.info("Shutdown hook — killing CLI process tree (pid ${proc.pid()})")
|
||||
kill(proc, "Shutdown hook", wait = false)
|
||||
}, "kilo-cli-shutdown")
|
||||
|
||||
val ok = runCatching {
|
||||
Runtime.getRuntime().addShutdownHook(next)
|
||||
}
|
||||
|
||||
val ok = runCatching { Runtime.getRuntime().addShutdownHook(next) }
|
||||
if (ok.isFailure) {
|
||||
log.warn("Failed to install CLI shutdown hook", ok.exceptionOrNull())
|
||||
return
|
||||
}
|
||||
|
||||
hook = next
|
||||
}
|
||||
|
||||
private fun uninstall() {
|
||||
val curr = hook ?: return
|
||||
hook = null
|
||||
|
||||
val ok = runCatching {
|
||||
Runtime.getRuntime().removeShutdownHook(curr)
|
||||
}
|
||||
|
||||
val ok = runCatching { Runtime.getRuntime().removeShutdownHook(curr) }
|
||||
if (ok.isFailure) {
|
||||
log.info("Skipping CLI shutdown hook removal: ${ok.exceptionOrNull()?.message}")
|
||||
}
|
||||
@@ -250,9 +220,7 @@ class KiloBackendCliManager(
|
||||
log.info("$source — killing CLI process tree (pid ${proc.pid()})")
|
||||
children(proc).forEach { it.destroy() }
|
||||
proc.destroy()
|
||||
|
||||
if (!wait) return
|
||||
|
||||
if (!proc.waitFor(KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
log.warn("CLI process did not exit after SIGTERM, sending SIGKILL")
|
||||
children(proc).forEach { it.destroyForcibly() }
|
||||
@@ -260,22 +228,15 @@ class KiloBackendCliManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun children(proc: Process): List<ProcessHandle> {
|
||||
return proc.toHandle().descendants().toList().asReversed()
|
||||
}
|
||||
private fun children(proc: Process): List<ProcessHandle> =
|
||||
proc.toHandle().descendants().toList().asReversed()
|
||||
|
||||
private fun platform(): String {
|
||||
val os = when {
|
||||
SystemInfo.isMac -> "darwin"
|
||||
SystemInfo.isLinux -> "linux"
|
||||
SystemInfo.isWindows -> "windows"
|
||||
else -> throw IllegalStateException(
|
||||
"Unsupported OS: ${
|
||||
System.getProperty(
|
||||
"os.name"
|
||||
)
|
||||
}"
|
||||
)
|
||||
else -> throw IllegalStateException("Unsupported OS: ${System.getProperty("os.name")}")
|
||||
}
|
||||
val arch = when (CpuArch.CURRENT) {
|
||||
CpuArch.ARM64 -> "arm64"
|
||||
@@ -285,11 +246,6 @@ class KiloBackendCliManager(
|
||||
return "$os-$arch"
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect IDE-specific env vars for telemetry and gateway attribution.
|
||||
* Catches all exceptions since these are best-effort — missing values
|
||||
* won't prevent the CLI from starting.
|
||||
*/
|
||||
private fun ideEnv(): Map<String, String> = buildMap {
|
||||
runCatching {
|
||||
val info = ApplicationInfo.getInstance()
|
||||
@@ -310,10 +266,6 @@ class KiloBackendCliManager(
|
||||
}.onFailure { log.info("Could not read machine ID: ${it.message}") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistent machine ID stored in the IntelliJ system directory.
|
||||
* Generated once and reused across restarts.
|
||||
*/
|
||||
private fun machineId(): String {
|
||||
val file = File(PathManager.getSystemPath(), "kilo/machine-id")
|
||||
if (file.exists()) return file.readText().trim()
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.backend.util
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.Interceptor
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
import ai.kilocode.rpc.dto.MessageErrorDto
|
||||
import ai.kilocode.rpc.dto.MessageTimeDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionSummaryDto
|
||||
import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Stateless parser that centralizes all CLI server response parsing.
|
||||
*
|
||||
* Callers pass raw JSON strings (SSE event data, HTTP response bodies)
|
||||
* and get typed DTOs back. All JSON navigation, regex extraction, and
|
||||
* manual serialization is contained here — no caller needs to know
|
||||
* about [JsonObject] or kotlinx.serialization.
|
||||
*
|
||||
* Designed for testability: every public method is pure (no side effects,
|
||||
* no dependencies). When a new parsing bug is found, add a test case
|
||||
* with the raw JSON that caused the issue.
|
||||
*/
|
||||
object KiloCliDataParser {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val TYPE_REGEX = Regex(""""type"\s*:\s*"([^"]+)"""")
|
||||
private val FIELD_RE = ConcurrentHashMap<String, Regex>()
|
||||
|
||||
// ================================================================
|
||||
// SSE event parsing
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Extract the event type from raw SSE JSON data.
|
||||
* Used as a fallback when OkHttp's EventSourceListener receives a null type.
|
||||
*/
|
||||
fun extractEventType(data: String): String =
|
||||
TYPE_REGEX.find(data)?.groupValues?.get(1) ?: "unknown"
|
||||
|
||||
/**
|
||||
* Parse an SSE chat event into a [ChatEventDto].
|
||||
* Returns null if the event type is unrecognized or the JSON is malformed.
|
||||
*
|
||||
* Handles the GlobalEvent wrapper: `{ directory, payload: { type, properties } }`
|
||||
* as well as flat events with top-level `properties`.
|
||||
*/
|
||||
fun parseChatEvent(type: String, data: String): ChatEventDto? {
|
||||
val obj = tryParseObject(data) ?: return null
|
||||
|
||||
// SSE data is a GlobalEvent: { directory, payload: { type, properties } }
|
||||
val payload = obj["payload"]?.jsonObject ?: obj
|
||||
val props = payload["properties"]?.jsonObject ?: return null
|
||||
|
||||
return when (type) {
|
||||
"message.updated" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
val info = props["info"]?.jsonObject ?: return null
|
||||
ChatEventDto.MessageUpdated(sid, parseMessage(info))
|
||||
}
|
||||
|
||||
"message.removed" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
val mid = props.str("messageID") ?: return null
|
||||
ChatEventDto.MessageRemoved(sid, mid)
|
||||
}
|
||||
|
||||
"message.part.updated" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
val part = props["part"]?.jsonObject ?: return null
|
||||
ChatEventDto.PartUpdated(sid, parsePart(part))
|
||||
}
|
||||
|
||||
"message.part.delta" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
val mid = props.str("messageID") ?: return null
|
||||
val pid = props.str("partID") ?: return null
|
||||
val field = props.str("field") ?: return null
|
||||
val delta = props.str("delta") ?: return null
|
||||
ChatEventDto.PartDelta(sid, mid, pid, field, delta)
|
||||
}
|
||||
|
||||
"session.turn.open" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
ChatEventDto.TurnOpen(sid)
|
||||
}
|
||||
|
||||
"session.turn.close" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
val reason = props.str("reason") ?: "completed"
|
||||
ChatEventDto.TurnClose(sid, reason)
|
||||
}
|
||||
|
||||
"session.error" -> {
|
||||
val sid = props.str("sessionID")
|
||||
val err = props["error"]?.jsonObject?.let { parseError(it) }
|
||||
ChatEventDto.Error(sid, err)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an SSE `session.status` event into a (sessionID, [SessionStatusDto]) pair.
|
||||
* Returns null if the required fields are missing.
|
||||
*
|
||||
* Uses regex extraction (no full JSON parse) for consistency with
|
||||
* the existing high-throughput status event handling.
|
||||
*/
|
||||
fun parseSessionStatus(data: String): Pair<String, SessionStatusDto>? {
|
||||
val id = extractField(data, "sessionID") ?: return null
|
||||
val type = extractNested(data, "status", "type") ?: "idle"
|
||||
val msg = extractNested(data, "status", "message")
|
||||
return id to SessionStatusDto(type, msg)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// HTTP response parsing
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Parse a session creation response (`POST /session`) into [SessionDto].
|
||||
*/
|
||||
fun parseSession(raw: String): SessionDto {
|
||||
val obj = json.parseToJsonElement(raw).jsonObject
|
||||
return parseSessionObject(obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse message history response (`GET /session/{id}/message`)
|
||||
* into a list of messages with their parts.
|
||||
*/
|
||||
fun parseMessages(raw: String): List<MessageWithPartsDto> {
|
||||
val arr = tryParseArray(raw) ?: return emptyList()
|
||||
return arr.mapNotNull { elem ->
|
||||
val obj = elem.jsonObject
|
||||
val info = obj["info"]?.jsonObject ?: return@mapNotNull null
|
||||
val parts = obj["parts"]?.jsonArray ?: JsonArray(emptyList())
|
||||
MessageWithPartsDto(
|
||||
info = parseMessage(info),
|
||||
parts = parts.map { parsePart(it.jsonObject) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// JSON serialization (DTO → JSON for outgoing requests)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Build the JSON body for `POST /session/{id}/prompt_async`.
|
||||
*/
|
||||
fun buildPromptJson(prompt: PromptDto): String {
|
||||
val parts = prompt.parts.joinToString(",") { part ->
|
||||
"""{"type":"${part.type}","text":${escape(part.text)}}"""
|
||||
}
|
||||
val sb = StringBuilder()
|
||||
sb.append("""{"parts":[$parts]""")
|
||||
val pid = prompt.providerID
|
||||
val mid = prompt.modelID
|
||||
if (pid != null && mid != null) {
|
||||
sb.append(""","model":{"providerID":${escape(pid)},"modelID":${escape(mid)}}""")
|
||||
}
|
||||
val ag = prompt.agent
|
||||
if (ag != null) {
|
||||
sb.append(""","agent":${escape(ag)}""")
|
||||
}
|
||||
sb.append("}")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the partial JSON body for `PATCH /global/config`.
|
||||
*/
|
||||
fun buildConfigPartial(update: ConfigUpdateDto): String {
|
||||
val sb = StringBuilder("{")
|
||||
var first = true
|
||||
fun sep() { if (!first) sb.append(","); first = false }
|
||||
|
||||
val model = update.model
|
||||
if (model != null) {
|
||||
sep(); sb.append(""""model":${escape(model)}""")
|
||||
}
|
||||
val agent = update.agent
|
||||
if (agent != null) {
|
||||
sep(); sb.append(""""default_agent":${escape(agent)}""")
|
||||
}
|
||||
val temp = update.temperature
|
||||
if (temp != null) {
|
||||
val target = agent ?: "ask"
|
||||
sep(); sb.append(""""agent":{"$target":{"temperature":$temp}}""")
|
||||
}
|
||||
sb.append("}")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Internal — message/part/session parsing
|
||||
// ================================================================
|
||||
|
||||
internal fun parseMessage(obj: JsonObject): MessageDto {
|
||||
val time = obj["time"]?.jsonObject
|
||||
val tokens = obj["tokens"]?.jsonObject
|
||||
val error = obj["error"]?.jsonObject
|
||||
|
||||
return MessageDto(
|
||||
id = obj.str("id") ?: "",
|
||||
sessionID = obj.str("sessionID") ?: "",
|
||||
role = obj.str("role") ?: "unknown",
|
||||
time = MessageTimeDto(
|
||||
created = time?.num("created") ?: 0.0,
|
||||
completed = time?.num("completed"),
|
||||
),
|
||||
agent = obj.str("agent"),
|
||||
providerID = obj.str("providerID"),
|
||||
modelID = obj.str("modelID"),
|
||||
parentID = obj.str("parentID"),
|
||||
cost = obj.num("cost"),
|
||||
tokens = tokens?.let {
|
||||
val cache = it["cache"]?.jsonObject
|
||||
TokensDto(
|
||||
input = it.long("input") ?: 0,
|
||||
output = it.long("output") ?: 0,
|
||||
reasoning = it.long("reasoning") ?: 0,
|
||||
cacheRead = cache?.long("read") ?: 0,
|
||||
cacheWrite = cache?.long("write") ?: 0,
|
||||
)
|
||||
},
|
||||
error = error?.let { parseError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parsePart(obj: JsonObject): PartDto {
|
||||
val state = obj["state"]?.jsonObject
|
||||
return PartDto(
|
||||
id = obj.str("id") ?: "",
|
||||
sessionID = obj.str("sessionID") ?: "",
|
||||
messageID = obj.str("messageID") ?: "",
|
||||
type = obj.str("type") ?: "unknown",
|
||||
text = obj.str("text"),
|
||||
tool = obj.str("tool"),
|
||||
state = state?.str("status"),
|
||||
title = state?.str("title"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parseError(obj: JsonObject): MessageErrorDto {
|
||||
val type = obj.str("type") ?: obj.str("name") ?: "unknown"
|
||||
val msg = obj.str("message")
|
||||
?: obj["data"]?.jsonObject?.str("message")
|
||||
?: obj.str("error")
|
||||
return MessageErrorDto(type, msg)
|
||||
}
|
||||
|
||||
private fun parseSessionObject(obj: JsonObject): SessionDto {
|
||||
val time = obj["time"]?.jsonObject
|
||||
val summary = obj["summary"]?.jsonObject
|
||||
return SessionDto(
|
||||
id = obj.str("id") ?: "",
|
||||
projectID = obj.str("projectID") ?: "",
|
||||
directory = obj.str("directory") ?: "",
|
||||
parentID = obj.str("parentID"),
|
||||
title = obj.str("title") ?: "",
|
||||
version = obj.str("version") ?: "",
|
||||
time = SessionTimeDto(
|
||||
created = time?.num("created") ?: 0.0,
|
||||
updated = time?.num("updated") ?: 0.0,
|
||||
archived = time?.num("archived"),
|
||||
),
|
||||
summary = summary?.let {
|
||||
SessionSummaryDto(
|
||||
additions = it.long("additions")?.toInt() ?: 0,
|
||||
deletions = it.long("deletions")?.toInt() ?: 0,
|
||||
files = it.long("files")?.toInt() ?: 0,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Internal — regex-based field extraction (for status events)
|
||||
// ================================================================
|
||||
|
||||
private fun extractField(raw: String, field: String): String? {
|
||||
val re = FIELD_RE.getOrPut(field) {
|
||||
Regex(""""$field"\s*:\s*"([^"]+)"""")
|
||||
}
|
||||
return re.find(raw)?.groupValues?.get(1)
|
||||
}
|
||||
|
||||
private fun extractNested(raw: String, outer: String, inner: String): String? {
|
||||
val block = Regex(""""$outer"\s*:\s*\{([^}]+)}""")
|
||||
.find(raw)?.groupValues?.get(1) ?: return null
|
||||
return extractField("{$block}", inner)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Internal — JSON helpers
|
||||
// ================================================================
|
||||
|
||||
private fun tryParseObject(raw: String): JsonObject? =
|
||||
try { json.parseToJsonElement(raw).jsonObject } catch (_: Exception) { null }
|
||||
|
||||
private fun tryParseArray(raw: String): kotlinx.serialization.json.JsonArray? =
|
||||
try { json.parseToJsonElement(raw).jsonArray } catch (_: Exception) { null }
|
||||
|
||||
/** Escape and double-quote a string for manual JSON building. */
|
||||
private fun escape(value: String): String {
|
||||
val escaped = value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
return "\"$escaped\""
|
||||
}
|
||||
}
|
||||
|
||||
// JsonObject convenience extensions
|
||||
private fun JsonObject.str(key: String): String? =
|
||||
this[key]?.jsonPrimitive?.contentOrNull
|
||||
|
||||
private fun JsonObject.num(key: String): Double? =
|
||||
this[key]?.jsonPrimitive?.doubleOrNull
|
||||
|
||||
private fun JsonObject.long(key: String): Long? =
|
||||
this[key]?.jsonPrimitive?.longOrNull
|
||||
+3
-3
@@ -2,14 +2,14 @@
|
||||
|
||||
package ai.kilocode.backend.rpc
|
||||
|
||||
import ai.kilocode.rpc.KiloProjectRpcApi
|
||||
import ai.kilocode.rpc.KiloWorkspaceRpcApi
|
||||
import com.intellij.platform.rpc.backend.RemoteApiProvider
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
|
||||
internal class KiloProjectRpcApiProvider : RemoteApiProvider {
|
||||
override fun RemoteApiProvider.Sink.remoteApis() {
|
||||
remoteApi(remoteApiDescriptor<KiloProjectRpcApi>()) {
|
||||
KiloProjectRpcApiImpl()
|
||||
remoteApi(remoteApiDescriptor<KiloWorkspaceRpcApi>()) {
|
||||
KiloWorkspaceRpcApiImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-3
@@ -3,14 +3,21 @@
|
||||
package ai.kilocode.backend.rpc
|
||||
|
||||
import ai.kilocode.backend.app.KiloBackendAppService
|
||||
import ai.kilocode.backend.app.KiloBackendChatManager
|
||||
import ai.kilocode.backend.app.KiloBackendSessionManager
|
||||
import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager
|
||||
import ai.kilocode.rpc.KiloSessionRpcApi
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
/**
|
||||
* Backend implementation of [KiloSessionRpcApi].
|
||||
@@ -18,9 +25,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
* Session CRUD routes through the [KiloBackendWorkspaceManager] to
|
||||
* get the correct workspace for a directory. Status tracking and
|
||||
* worktree directory management go directly to the
|
||||
* [KiloBackendSessionManager].
|
||||
* [KiloBackendSessionManager]. Chat operations delegate to
|
||||
* [KiloBackendChatManager].
|
||||
*/
|
||||
class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance(KiloSessionRpcApiImpl::class.java)
|
||||
}
|
||||
|
||||
private val workspaces: KiloBackendWorkspaceManager
|
||||
get() = service<KiloBackendAppService>().workspaces
|
||||
@@ -28,11 +39,16 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
private val sessions: KiloBackendSessionManager
|
||||
get() = service<KiloBackendAppService>().sessions
|
||||
|
||||
private val chat: KiloBackendChatManager
|
||||
get() = service<KiloBackendAppService>().chat
|
||||
|
||||
override suspend fun list(directory: String): SessionListDto =
|
||||
workspaces.get(directory).sessions()
|
||||
|
||||
override suspend fun create(directory: String): SessionDto =
|
||||
workspaces.get(directory).createSession()
|
||||
override suspend fun create(directory: String): SessionDto {
|
||||
LOG.info("create session: directory=$directory")
|
||||
return workspaces.get(directory).createSession()
|
||||
}
|
||||
|
||||
override suspend fun get(id: String, directory: String): SessionDto {
|
||||
val dir = sessions.getDirectory(id, directory)
|
||||
@@ -52,4 +68,34 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
|
||||
override suspend fun getDirectory(id: String, fallback: String): String =
|
||||
sessions.getDirectory(id, fallback)
|
||||
|
||||
// ------ chat ------
|
||||
|
||||
override suspend fun prompt(id: String, directory: String, prompt: PromptDto) {
|
||||
LOG.info("prompt RPC: session=$id, dir=$directory, parts=${prompt.parts.size}")
|
||||
chat.prompt(id, directory, prompt)
|
||||
}
|
||||
|
||||
override suspend fun abort(id: String, directory: String) =
|
||||
chat.abort(id, directory)
|
||||
|
||||
override suspend fun messages(id: String, directory: String): List<MessageWithPartsDto> =
|
||||
chat.messages(id, directory)
|
||||
|
||||
override suspend fun events(id: String, directory: String): Flow<ChatEventDto> =
|
||||
chat.events.filter { event ->
|
||||
val sid = when (event) {
|
||||
is ChatEventDto.MessageUpdated -> event.sessionID
|
||||
is ChatEventDto.PartUpdated -> event.sessionID
|
||||
is ChatEventDto.PartDelta -> event.sessionID
|
||||
is ChatEventDto.TurnOpen -> event.sessionID
|
||||
is ChatEventDto.TurnClose -> event.sessionID
|
||||
is ChatEventDto.Error -> event.sessionID
|
||||
is ChatEventDto.MessageRemoved -> event.sessionID
|
||||
}
|
||||
sid == id
|
||||
}
|
||||
|
||||
override suspend fun updateConfig(directory: String, config: ConfigUpdateDto) =
|
||||
chat.updateConfig(directory, config)
|
||||
}
|
||||
|
||||
+12
-3
@@ -14,7 +14,7 @@ import ai.kilocode.backend.workspace.ModelInfo
|
||||
import ai.kilocode.backend.workspace.ProviderData
|
||||
import ai.kilocode.backend.workspace.ProviderInfo
|
||||
import ai.kilocode.backend.workspace.SkillInfo
|
||||
import ai.kilocode.rpc.KiloProjectRpcApi
|
||||
import ai.kilocode.rpc.KiloWorkspaceRpcApi
|
||||
import ai.kilocode.rpc.dto.AgentDto
|
||||
import ai.kilocode.rpc.dto.AgentsDto
|
||||
import ai.kilocode.rpc.dto.CommandDto
|
||||
@@ -26,6 +26,7 @@ import ai.kilocode.rpc.dto.ProviderDto
|
||||
import ai.kilocode.rpc.dto.ProvidersDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -34,19 +35,27 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Backend implementation of [KiloProjectRpcApi].
|
||||
* Backend implementation of [KiloWorkspaceRpcApi].
|
||||
*
|
||||
* Routes through the [KiloBackendWorkspaceManager] to get a workspace
|
||||
* for the given directory. No [ProjectManager] dependency — any
|
||||
* directory (including worktrees) can get a workspace.
|
||||
*/
|
||||
class KiloProjectRpcApiImpl : KiloProjectRpcApi {
|
||||
class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
||||
|
||||
private val app: KiloBackendAppService get() = service()
|
||||
|
||||
private val manager: KiloBackendWorkspaceManager
|
||||
get() = app.workspaces
|
||||
|
||||
override suspend fun resolveProjectDirectory(hint: String): String {
|
||||
// In monolith mode, find the open project whose basePath matches the hint.
|
||||
// In split mode, the backend's project.basePath is the real directory.
|
||||
val projects = ProjectManager.getInstance().openProjects
|
||||
val match = projects.firstOrNull { !it.isDefault }
|
||||
return match?.basePath ?: hint
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits workspace state for [directory]. Waits for the app to
|
||||
* reach [KiloAppState.Ready] before creating the workspace —
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user