mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into fix/cli-prompt-loop-missing-finish-reason
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager branch, mode, and model selectors usable in the new worktree dialog.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show the autocomplete model selector with the same picker layout as other model selectors and save changes from the settings save bar.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support copying, previewing, and exporting rendered Mermaid diagrams.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Make Agent Manager session tabs compact, status-aware, and easier to scan when many tabs are open.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add a searchable current-tabs menu to Agent Manager.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@opencode-ai/ui": patch
|
||||
---
|
||||
|
||||
Restore inline diff previews after session reloads while keeping large file contents stripped from VS Code webview messages.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Support global and per-project codebase indexing enablement.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show completed assistant steps in the JetBrains session timeline.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/kilo-gateway": minor
|
||||
"@kilocode/sdk": minor
|
||||
---
|
||||
|
||||
Migrate KiloClaw chat to the new kilo-chat backend. Replaces the single-channel Stream Chat integration with a multi-conversation experience that matches the web UX at app.kilo.ai/claw/kilo-chat: conversation list, reactions, typing indicators, editing, and action approvals. The TUI continues to render a single chat view backed by the user's primary conversation.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Rate assistant responses with thumbs up/down. Click the thumbs buttons next to the copy button on any assistant message, or press `<leader>=` / `<leader>-` in the terminal UI. Only shown when telemetry is enabled; feedback is sent to Kilo to help improve model and prompt quality.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Honor configured permission overrides in Ask and Plan modes, including persisted always-allow rules.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Support accepting command permission prompts with Enter and denying them with Escape.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Recover sessions when providers end a response with an error finish but no error details.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/sdk": patch
|
||||
---
|
||||
|
||||
Remove custom providers from settings when disconnecting them so they do not reappear after being disabled and re-enabled.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager model, mode, and reasoning effort selections independent per session and preserve them after restart.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Capture the active VS Code terminal when using `@terminal` in Agent Manager.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Unify the Changes panel into a single tab with a source dropdown to switch between workspace and current-session diffs. Show the session's additions and deletions on the sidebar changes badge, and warn in the panel when snapshots are disabled for the repository.
|
||||
@@ -96,6 +96,16 @@ jobs:
|
||||
echo "cli_url=$URL" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing CLI v${VERSION} via asset API: $URL"
|
||||
|
||||
# Harbor's default agent-setup timeout is 360s. The hello-world container
|
||||
# (FROM ubuntu:24.04) needs apt-get update + apt-get install + a NodeSource
|
||||
# curl|bash + apt install nodejs before the CLI even downloads, and on
|
||||
# the Blacksmith runners that occasionally pushes past 6 min when an apt
|
||||
# mirror or NodeSource cdn is slow, killing the run with
|
||||
# AgentSetupTimeoutError. Doubling the multiplier to 2 gives enough
|
||||
# headroom for transient mirror/cdn slowness while still finishing well
|
||||
# under timeout-minutes. Note: --timeout-multiplier scales every timeout
|
||||
# uniformly (setup, agent, verifier, env build), but they're all upper
|
||||
# limits so this is harmless.
|
||||
- name: Run smoke test — hello-world
|
||||
env:
|
||||
KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }}
|
||||
@@ -104,7 +114,8 @@ jobs:
|
||||
./scripts/run_eval.sh \
|
||||
-m kilo/anthropic/claude-sonnet-4.6 \
|
||||
-d hello-world \
|
||||
--job-name smoke-test-hello-world
|
||||
--job-name smoke-test-hello-world \
|
||||
--timeout-multiplier 2
|
||||
|
||||
- name: Run smoke test — log-summary-date-ranges
|
||||
env:
|
||||
@@ -115,11 +126,18 @@ jobs:
|
||||
-m kilo/anthropic/claude-sonnet-4.6 \
|
||||
-d terminal-bench-sample \
|
||||
-t "log-summary-date-ranges" \
|
||||
--job-name smoke-test-log-summary
|
||||
--job-name smoke-test-log-summary \
|
||||
--timeout-multiplier 2
|
||||
|
||||
- name: Validate results
|
||||
run: python3 scripts/validate_smoke_test.py jobs/smoke-test-*/
|
||||
|
||||
# Also upload the agent setup logs (stdout/stderr/return-code from the
|
||||
# CLI install script) so we can see exactly which step stalls when the
|
||||
# next AgentSetupTimeoutError happens. These come from the install
|
||||
# script in kilo-bench, which uses `set -euo pipefail` (no `set -x`)
|
||||
# and never echoes auth tokens or API keys, so the captured output is
|
||||
# safe to upload.
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
@@ -128,5 +146,6 @@ jobs:
|
||||
path: |
|
||||
jobs/smoke-test-*/**/result.json
|
||||
jobs/smoke-test-*/**/trajectory.json
|
||||
jobs/smoke-test-*/**/agent/setup/*.txt
|
||||
retention-days: 30
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -28,3 +28,11 @@ Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3
|
||||
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
|
||||
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
|
||||
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
||||
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
|
||||
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
|
||||
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
|
||||
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
|
||||
|
||||
@@ -251,7 +251,6 @@
|
||||
"quick-lru": "^7.0.0",
|
||||
"simple-git": "3.35.2",
|
||||
"solid-js": "^1.9.11",
|
||||
"stream-chat": "9.38.0",
|
||||
"uri-js": "^4.4.1",
|
||||
"virtua": "catalog:",
|
||||
"web-tree-sitter": "^0.24.7",
|
||||
@@ -392,7 +391,6 @@
|
||||
"semver": "^7.6.3",
|
||||
"simple-git": "3.35.2",
|
||||
"solid-js": "catalog:",
|
||||
"stream-chat": "9.38.0",
|
||||
"strip-ansi": "7.1.2",
|
||||
"tree-sitter-bash": "0.25.0",
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
@@ -461,8 +459,8 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.105",
|
||||
"@opentui/solid": ">=0.1.105",
|
||||
"@opentui/core": ">=0.2.2",
|
||||
"@opentui/solid": ">=0.2.2",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -580,7 +578,6 @@
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.46",
|
||||
@@ -608,10 +605,12 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.1.105",
|
||||
"@opentui/solid": "0.1.105",
|
||||
"@opentui/core": "0.2.2",
|
||||
"@opentui/solid": "0.2.2",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
"@sentry/vite-plugin": "4.6.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
@@ -1504,21 +1503,21 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.105", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.105", "@opentui/core-darwin-x64": "0.1.105", "@opentui/core-linux-arm64": "0.1.105", "@opentui/core-linux-x64": "0.1.105", "@opentui/core-win32-arm64": "0.1.105", "@opentui/core-win32-x64": "0.1.105", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vllSOOCW6VIThV/96GRLJ1IxIBuR+ci6FDvnPIAG4s7SJ/FW6zAkqDn1xrtBwwk/lM3QWjLqy8BZc+zwWvveJA=="],
|
||||
"@opentui/core": ["@opentui/core@0.2.2", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.2", "@opentui/core-darwin-x64": "0.2.2", "@opentui/core-linux-arm64": "0.2.2", "@opentui/core-linux-x64": "0.2.2", "@opentui/core-win32-arm64": "0.2.2", "@opentui/core-win32-x64": "0.2.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wxg1CD58SVrowu+WgbhZNi3UP/wWxPio2Kj2IeTjomoIE+6EXLxR8eCCxHYVuQUd9E4fknrKkY5HmiSsp6oPow=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.105", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1pIL7aer9amwj8EpYoMNtvavKetIe+nX8uBRmYsMQb+KvJoUAZUqENfRW+qHE5WrsOyxx8/QoyXTHw15GG5iLQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tY5n3ZRQx+b0kyhQJJLsyJMeZ+0w4FV37YZc/Qqv3qvOqE9kZPw/7adR77FYwWDm/7fax94mLMrR8Y5bKUkDmw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.105", "", { "os": "darwin", "cpu": "x64" }, "sha512-hLIRSWlK3gY2NRXJGWiTBiMYSmRDjOYFZF6WtUVXhY2SL3sp08dhmr/6dmAVH+3pKCsCipLEsrrcQX6SAihCTA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-W/R7OnqY30FXcTG0tiP2JkQFmgtYbIte5afQ5PC12TliRoee1RqG3iCG6kY1jxW+3Vg6jge88uiSjUEDpeV2gA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.105", "", { "os": "linux", "cpu": "arm64" }, "sha512-jlRKfPkozTZEkHEePuCWYcTIUtPm+ieInAwGVqGmjbvqjxdVv1/W/Dt6LEZ/9jpRiOPd+FjXAfLe6wa/XWHr+w=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1pzTYFEZauYuw6AGycw2TYGtAlZVGjuUtSdxH1fP51kBPS3oVWduUY2j7GKREz3SU5NulvO2Wc6HWsm3feMqwQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.105", "", { "os": "linux", "cpu": "x64" }, "sha512-kfWS1WMg6qHShmxZX9s1tZc/8JcXw6uyy2UtyTbJdRFExtXGH37oKHi8QK8iPL2ExCx4z7zqVnVJfO3X/Wh7lA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ucVwUtUYeOYGVFPBLbPoxzbrPdhD0PDyKNQ2X4n1AJ9jlQX4gqBZRcXMEF8hiXDjFxsikZwef7De0ciCcWvAMg=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.105", "", { "os": "win32", "cpu": "arm64" }, "sha512-UFx6A8OpBVbGWK6OAw4GqAqKZgIITJfSOd35pG9yDVKQouHN2OGc2HeeXrH2A4h42p40Xl6IfcqqfllkpC13Dg=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MPhYdJNdxmC5Bqsq6sis/+VkjRgkEjm+bQ1Tl++NSKLuiTU32Re0ImcZlgHbe+LZtZoGMZHVSgZlkGd3oYXO2g=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-19BroLfn2h0RDYfJS5o96Fc8kYCDhRBcseIXtHIkoKIsKMxx62KiDLo/byVye6rp+yQRRB7Xkd2uWqsbdiWo9w=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.105", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.105", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-uxnaMP802sCI487pv/Hk9xdFdIj9mkg3eNliAqbqR0Shmd4phcjKEZvPRpijjmI99j4s9nul71jzF3h1oz31Nw=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.2", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-ZBVfCoVAhcUGQWPAWOTdzuVldMaRkuPpCu4U1VZCqmIw9DtbCuiVr0WnDocDxKhJLbTu8bl3qEWtVCf6lTSi3w=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2134,8 +2133,6 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
|
||||
"@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="],
|
||||
@@ -2426,7 +2423,7 @@
|
||||
|
||||
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||
|
||||
"babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
"babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
@@ -2494,13 +2491,13 @@
|
||||
|
||||
"buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
|
||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
||||
|
||||
@@ -3404,8 +3401,6 @@
|
||||
|
||||
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
|
||||
|
||||
"linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="],
|
||||
|
||||
"load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
@@ -4156,8 +4151,6 @@
|
||||
|
||||
"storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="],
|
||||
|
||||
"stream-chat": ["stream-chat@9.38.0", "", { "dependencies": { "@types/jsonwebtoken": "^9.0.8", "@types/ws": "^8.5.14", "axios": "^1.12.2", "base64-js": "^1.5.1", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "jsonwebtoken": "^9.0.3", "linkifyjs": "^4.3.2", "ws": "^8.18.1" } }, "sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg=="],
|
||||
|
||||
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
@@ -4950,6 +4943,8 @@
|
||||
|
||||
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"esbuild-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
|
||||
"eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
@@ -5074,6 +5069,10 @@
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core": ["@opentui/core@0.1.105", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.105", "@opentui/core-darwin-x64": "0.1.105", "@opentui/core-linux-arm64": "0.1.105", "@opentui/core-linux-x64": "0.1.105", "@opentui/core-win32-arm64": "0.1.105", "@opentui/core-win32-x64": "0.1.105", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vllSOOCW6VIThV/96GRLJ1IxIBuR+ci6FDvnPIAG4s7SJ/FW6zAkqDn1xrtBwwk/lM3QWjLqy8BZc+zwWvveJA=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.105", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.105", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-uxnaMP802sCI487pv/Hk9xdFdIj9mkg3eNliAqbqR0Shmd4phcjKEZvPRpijjmI99j4s9nul71jzF3h1oz31Nw=="],
|
||||
|
||||
"ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
||||
@@ -5188,6 +5187,8 @@
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"vite-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
|
||||
"vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
@@ -5262,7 +5263,7 @@
|
||||
|
||||
"@kilocode/kilo-gateway/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@opentui/core/bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
"@kilocode/kilo-gateway/@opentui/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
@@ -5600,6 +5601,26 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.105", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1pIL7aer9amwj8EpYoMNtvavKetIe+nX8uBRmYsMQb+KvJoUAZUqENfRW+qHE5WrsOyxx8/QoyXTHw15GG5iLQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.105", "", { "os": "darwin", "cpu": "x64" }, "sha512-hLIRSWlK3gY2NRXJGWiTBiMYSmRDjOYFZF6WtUVXhY2SL3sp08dhmr/6dmAVH+3pKCsCipLEsrrcQX6SAihCTA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.105", "", { "os": "linux", "cpu": "arm64" }, "sha512-jlRKfPkozTZEkHEePuCWYcTIUtPm+ieInAwGVqGmjbvqjxdVv1/W/Dt6LEZ/9jpRiOPd+FjXAfLe6wa/XWHr+w=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.105", "", { "os": "linux", "cpu": "x64" }, "sha512-kfWS1WMg6qHShmxZX9s1tZc/8JcXw6uyy2UtyTbJdRFExtXGH37oKHi8QK8iPL2ExCx4z7zqVnVJfO3X/Wh7lA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.105", "", { "os": "win32", "cpu": "arm64" }, "sha512-UFx6A8OpBVbGWK6OAw4GqAqKZgIITJfSOd35pG9yDVKQouHN2OGc2HeeXrH2A4h42p40Xl6IfcqqfllkpC13Dg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||
|
||||
"ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
@@ -5800,6 +5821,8 @@
|
||||
|
||||
"mocha/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"pkg-conf/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
||||
|
||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-BkFDOCEvivFrxKOvMjAIhj3QZwmb9vE/KvCZv5puw6E=",
|
||||
"aarch64-linux": "sha256-sTkfQNjqCNCxmHbpQ8woA6azoiyD37e8Xy7UwB6eNsA=",
|
||||
"aarch64-darwin": "sha256-lKuagsTHqij5C764DM3d+BOe4JKs2KV8XiwSlJItGMg=",
|
||||
"x86_64-darwin": "sha256-g5gZMHLq8feSZHVaCOLGU6Tj2Qw56Zogif3pT9EIlz8="
|
||||
"x86_64-linux": "sha256-ZInDUMIVu0aBcf0GufNsBEBdL8pQ6GvlQmQq18mFzso=",
|
||||
"aarch64-linux": "sha256-I9oJ145BsbXUVTzaIWAZsG8YijmO/gUDO6DwinUwA3w=",
|
||||
"aarch64-darwin": "sha256-tr8lRQvUMToux9JyiyTk6rYDFmPMm6LNai/IQ8Qk23c=",
|
||||
"x86_64-darwin": "sha256-ffyoj7ubIzbUXhcm7/yOQBZA42BIt+fEBQs4khQqaSg="
|
||||
}
|
||||
}
|
||||
|
||||
+10
-7
@@ -7,11 +7,13 @@
|
||||
"packageManager": "bun@1.3.13",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup",
|
||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
@@ -31,8 +33,8 @@
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.1.105",
|
||||
"@opentui/solid": "0.1.105",
|
||||
"@opentui/core": "0.2.2",
|
||||
"@opentui/solid": "0.2.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -74,6 +76,8 @@
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"@sentry/solid": "10.36.0",
|
||||
"@sentry/vite-plugin": "4.6.0",
|
||||
"solid-js": "1.9.12",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.10"
|
||||
@@ -82,7 +86,6 @@
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
@@ -93,6 +96,7 @@
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
"turbo": "2.8.13",
|
||||
"@types/bun": "catalog:",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.10"
|
||||
},
|
||||
@@ -143,8 +147,7 @@
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@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",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch"
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
},
|
||||
"version": "7.2.42",
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -54,7 +54,9 @@ export const Flag = {
|
||||
|
||||
// Experimental
|
||||
KILO_EXPERIMENTAL,
|
||||
KILO_EXPERIMENTAL_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_FILEWATCHER").pipe(Config.withDefault(false)),
|
||||
KILO_EXPERIMENTAL_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_FILEWATCHER").pipe(
|
||||
Config.withDefault(false),
|
||||
),
|
||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe(
|
||||
Config.withDefault(false),
|
||||
),
|
||||
|
||||
+26
-11
@@ -5,6 +5,7 @@ import os from "os"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Flock } from "./util/flock"
|
||||
import { markNoIndex } from "./kilocode/spotlight" // kilocode_change
|
||||
import { Flag } from "./flag/flag"
|
||||
|
||||
const app = "kilo" // kilocode_change
|
||||
// kilocode_change start
|
||||
@@ -21,6 +22,7 @@ const cache = path.join(clean(xdgCache)!, app)
|
||||
const config = path.join(clean(xdgConfig)!, app)
|
||||
const state = path.join(clean(xdgState)!, app)
|
||||
// kilocode_change end
|
||||
const tmp = path.join(os.tmpdir(), app)
|
||||
|
||||
const paths = {
|
||||
get home() {
|
||||
@@ -32,6 +34,7 @@ const paths = {
|
||||
cache,
|
||||
config,
|
||||
state,
|
||||
tmp,
|
||||
}
|
||||
|
||||
export const Path = paths
|
||||
@@ -42,6 +45,7 @@ await Promise.all([
|
||||
fs.mkdir(Path.data, { recursive: true }),
|
||||
fs.mkdir(Path.config, { recursive: true }),
|
||||
fs.mkdir(Path.state, { recursive: true }),
|
||||
fs.mkdir(Path.tmp, { recursive: true }),
|
||||
fs.mkdir(Path.log, { recursive: true }),
|
||||
fs.mkdir(Path.bin, { recursive: true }),
|
||||
])
|
||||
@@ -58,23 +62,34 @@ export interface Interface {
|
||||
readonly cache: string
|
||||
readonly config: string
|
||||
readonly state: string
|
||||
readonly tmp: string
|
||||
readonly bin: string
|
||||
readonly log: string
|
||||
}
|
||||
|
||||
export function make(input: Partial<Interface> = {}): Interface {
|
||||
return {
|
||||
home: Path.home,
|
||||
data: Path.data,
|
||||
cache: Path.cache,
|
||||
config: Flag.KILO_CONFIG_DIR ?? Path.config,
|
||||
state: Path.state,
|
||||
tmp: Path.tmp,
|
||||
bin: Path.bin,
|
||||
log: Path.log,
|
||||
...input,
|
||||
}
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
return Service.of({
|
||||
home: Path.home,
|
||||
data: Path.data,
|
||||
cache: Path.cache,
|
||||
config: Path.config,
|
||||
state: Path.state,
|
||||
bin: Path.bin,
|
||||
log: Path.log,
|
||||
})
|
||||
}),
|
||||
Effect.sync(() => Service.of(make())),
|
||||
)
|
||||
|
||||
export const layerWith = (input: Partial<Interface>) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.sync(() => Service.of(make(input))),
|
||||
)
|
||||
|
||||
export * as Global from "./global"
|
||||
|
||||
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
|
||||
}
|
||||
})()
|
||||
|
||||
if (yield* afs.existsSafe(dir)) {
|
||||
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
||||
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||
}
|
||||
|
||||
const tree = yield* reify({ dir, add: [pkg] })
|
||||
const first = tree.edgesOut.values().next().value?.to
|
||||
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
|
||||
if (!first) {
|
||||
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||
if (Option.isSome(result.entrypoint)) return result
|
||||
return yield* new InstallFailedError({ add: [pkg], dir })
|
||||
}
|
||||
return resolveEntryPoint(first.name, first.path)
|
||||
}, Effect.scoped)
|
||||
|
||||
|
||||
@@ -18,20 +18,17 @@ function sleep(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const msg: Msg = JSON.parse(process.argv[2]!)
|
||||
const msg: Msg = JSON.parse(process.argv[2])
|
||||
|
||||
const testGlobal = Layer.succeed(
|
||||
Global.Service,
|
||||
Global.Service.of({
|
||||
home: os.homedir(),
|
||||
data: os.tmpdir(),
|
||||
cache: os.tmpdir(),
|
||||
config: os.tmpdir(),
|
||||
state: os.tmpdir(),
|
||||
bin: os.tmpdir(),
|
||||
log: os.tmpdir(),
|
||||
}),
|
||||
)
|
||||
const testGlobal = Global.layerWith({
|
||||
home: os.homedir(),
|
||||
data: os.tmpdir(),
|
||||
cache: os.tmpdir(),
|
||||
config: os.tmpdir(),
|
||||
state: os.tmpdir(),
|
||||
bin: os.tmpdir(),
|
||||
log: os.tmpdir(),
|
||||
})
|
||||
|
||||
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
||||
describe("global paths", () => {
|
||||
test("tmp path is under the system temp directory", () => {
|
||||
expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode"))
|
||||
expect(Global.make().tmp).toBe(Global.Path.tmp)
|
||||
})
|
||||
|
||||
test("tmp path is created on module load", async () => {
|
||||
expect((await fs.stat(Global.Path.tmp)).isDirectory()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,12 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
|
||||
const win = process.platform === "win32"
|
||||
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
|
||||
}),
|
||||
)
|
||||
|
||||
const npmLayer = (cache: string) =>
|
||||
Npm.layer.pipe(
|
||||
Layer.provide(EffectFlock.layer),
|
||||
Layer.provide(AppFileSystem.layer),
|
||||
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
|
||||
Layer.provide(NodeFileSystem.layer),
|
||||
)
|
||||
|
||||
describe("Npm.sanitize", () => {
|
||||
test("keeps normal scoped package specs unchanged", () => {
|
||||
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
||||
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Npm.add", () => {
|
||||
test("reifies when package cache directory exists without the package installed", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
||||
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
||||
name: "fixture-provider",
|
||||
main: "index.js",
|
||||
})
|
||||
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
|
||||
|
||||
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
|
||||
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
|
||||
|
||||
const entry = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return yield* npm.add(spec)
|
||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
||||
|
||||
expect(Option.isSome(entry.entrypoint)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Npm.install", () => {
|
||||
test("respects omit from project .npmrc", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
@@ -93,18 +93,15 @@ async function waitForFile(file: string, timeout = 3_000) {
|
||||
// Test layer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testGlobal = Layer.succeed(
|
||||
Global.Service,
|
||||
Global.Service.of({
|
||||
home: os.homedir(),
|
||||
data: os.tmpdir(),
|
||||
cache: os.tmpdir(),
|
||||
config: os.tmpdir(),
|
||||
state: os.tmpdir(),
|
||||
bin: os.tmpdir(),
|
||||
log: os.tmpdir(),
|
||||
}),
|
||||
)
|
||||
const testGlobal = Global.layerWith({
|
||||
home: os.homedir(),
|
||||
data: os.tmpdir(),
|
||||
cache: os.tmpdir(),
|
||||
config: os.tmpdir(),
|
||||
state: os.tmpdir(),
|
||||
bin: os.tmpdir(),
|
||||
log: os.tmpdir(),
|
||||
})
|
||||
|
||||
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
||||
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"types": ["bun"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@effect/language-service",
|
||||
"transform": "@effect/language-service/transform",
|
||||
"namespaceImportPackages": ["effect", "@effect/*"]
|
||||
}
|
||||
]
|
||||
"types": ["bun"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export const CodeWithAiNav: NavSection[] = [
|
||||
children: "Git Commit Generation",
|
||||
},
|
||||
{ href: "/code-with-ai/features/speech-to-text", children: "Voice Transcription" },
|
||||
{ href: "/code-with-ai/features/message-feedback", children: "Message Feedback" },
|
||||
{
|
||||
href: "/code-with-ai/features/browser-use",
|
||||
children: "Agent Behavior",
|
||||
|
||||
@@ -43,4 +43,6 @@ exclude = [
|
||||
# Consistently times out in CI
|
||||
'^https?://opncd\.ai',
|
||||
'^https?://zod\.dev/v4/changelog',
|
||||
# OpenAI docs return 404 to plain GET link checks but resolve in browsers
|
||||
'^https?://platform\.openai\.com/docs/api-reference/responses/create',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: "Message Feedback"
|
||||
description: "Rate assistant responses with thumbs up/down to help Kilo improve"
|
||||
---
|
||||
|
||||
# Message Feedback
|
||||
|
||||
You can give a thumbs up or thumbs down to any response Kilo gives you. It's a quick way to tell us when something worked well, or when it didn't.
|
||||
|
||||
{% callout type="info" %}
|
||||
Feedback is only available when telemetry is on. If you turn telemetry off, the buttons no longer display. See [Turning it off](#turning-it-off) below.
|
||||
{% /callout %}
|
||||
|
||||
## How to use it
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
When Kilo finishes a response, you'll see thumbs-up and thumbs-down buttons next to the **Copy response** button. Click one to send your rating. Click the same button again to take it back, or click the other one to change your mind.
|
||||
|
||||
If you reload the window or switch to a different session, the feedback status will be reset.
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="CLI" %}
|
||||
|
||||
In the terminal, two keybinds rate the most recent assistant message:
|
||||
|
||||
| Action | Keybind |
|
||||
|---|---|
|
||||
| Helpful | `<leader>=` |
|
||||
| Not helpful | `<leader>-` |
|
||||
|
||||
`<leader>` defaults to `Ctrl+X`, so you press `Ctrl+X` and then `=` for thumbs-up, or `Ctrl+X` and then `-` for thumbs-down.
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Why it matters
|
||||
|
||||
Your feedback is the most direct way to tell us what's working and what isn't. We use it to tune the prompts behind Kilo's behavior, pick better default models, and find and fix problems faster than we could on our own.
|
||||
|
||||
## What we send
|
||||
|
||||
When you rate a response, here's what gets sent:
|
||||
|
||||
- That you rated a response thumbs up or thumbs down.
|
||||
- The provider and model ID that produced the response (for example, `anthropic` and `claude-sonnet-4-5`).
|
||||
- For responses that came through Kilo Gateway, the request ID so we can match the rating back to the session.
|
||||
|
||||
No further information is included in the rating feedback. For more information on telemetry, see [PRIVACY.md](https://github.com/Kilo-Org/kilocode/blob/main/PRIVACY.md).
|
||||
|
||||
## Turning it off
|
||||
|
||||
Feedback is part of telemetry, so turning telemetry off will cause the feedback buttons to no longer appear.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
Open Settings (`Cmd+,` on Mac, `Ctrl+,` on Windows/Linux), search for **`telemetry.telemetryLevel`**, and set it to **Off**. The feedback buttons will no longer appear. Set it back to **All** to bring them back. (For more on what this setting controls, see [VS Code's telemetry docs](https://code.visualstudio.com/docs/getstarted/telemetry).)
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Set `KILO_TELEMETRY_LEVEL=off` in your environment before starting Kilo:
|
||||
|
||||
```bash
|
||||
KILO_TELEMETRY_LEVEL=off kilo
|
||||
```
|
||||
|
||||
With telemetry off, pressing the feedback keybinds shows a message saying feedback is disabled, and nothing is sent.
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
@@ -598,7 +598,7 @@ Options:
|
||||
--path directory path to generate the agent file [string]
|
||||
--description what the agent should do [string]
|
||||
--mode agent mode [string] [choices: "all", "primary", "subagent"]
|
||||
--permissions, --tools comma-separated list of permissions to allow (default: all). Available: "bash, read, edit, glob, grep, webfetch, task, todowrite, websearch, codesearch, lsp, skill" [string]
|
||||
--permissions, --tools comma-separated list of permissions to allow (default: all). Available: "bash, read, edit, glob, grep, webfetch, task, todowrite, websearch, lsp, skill" [string]
|
||||
-m, --model model to use in the format of provider/model [string]
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Add a thumbs-up / thumbs-down widget next to the existing copy button on every a
|
||||
| Surface | Approach |
|
||||
|---|---|
|
||||
| VS Code extension | Thumbs buttons inline next to the copy button |
|
||||
| TUI | Keybinds (`<leader>+` / `<leader>-`) on the last assistant message |
|
||||
| TUI | Keybinds (`<leader>=` / `<leader>-`) on the last assistant message |
|
||||
|
||||
### Telemetry Payload
|
||||
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee8d8e04b09b58973a1ff66d5a3d8648ba590f88312cf1f37412c39ccdac0480
|
||||
size 3973
|
||||
oid sha256:483e73147bed34050ee35e60607c33c04d7e726b8f746921ad27e582cb5b19ee
|
||||
size 2886
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0e9ea1be8fbb44f05bed7960d1ba13f0d4c5e904e4025e4ab4743c4991e93c8
|
||||
size 4696
|
||||
oid sha256:3b4687ed34fe06adb7e72797798b1f3602c225e9a36223842ea1c66eebea4601
|
||||
size 4794
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66a7ecedac685e17c442bb036d7392320e0642ef43da77ed8dc23be29bc7988e
|
||||
size 2935
|
||||
oid sha256:980c9baf0edafa113956157a34eb00495aff55293b10fb4cbb86a8cb5b28e8c0
|
||||
size 2865
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0d29bb3a9356cf8bec6bb0a9d6e4d0b59d147c1fa8adee7140c81a061eea407d
|
||||
size 6930
|
||||
oid sha256:067a5c757dfa32d27955dd634b8dea52edef2141111a5eecee0161df36c0ed8b
|
||||
size 7061
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:483c04840175e7678f0cfd1eba377ec199239404acd567fd7778367f4d2cb46c
|
||||
size 21219
|
||||
oid sha256:a1ca383c2a5fa9d6f477430d7c63c7330e13e2667bf717906d3c29c215f163c5
|
||||
size 21564
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2130db1ea14a5e8fbf879e96529198592a785b8460d29e12746175e102d50d5
|
||||
size 53617
|
||||
oid sha256:a67058f86869ec7457058feec03f880b7a3ac31c00b87ae6db5a55d977295856
|
||||
size 57421
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bdcac95e7969d021ebce19001f37f86b8ab670bd79e39086385c9419617294df
|
||||
size 27177
|
||||
@@ -12,6 +12,24 @@ export const DEFAULT_KILO_API_URL = "https://api.kilo.ai"
|
||||
/** Base URL for Kilo API - can be overridden by KILO_API_URL env var */
|
||||
export const KILO_API_BASE = process.env[ENV_KILO_API_URL] || DEFAULT_KILO_API_URL
|
||||
|
||||
/** Environment variable for custom Kilo Chat URL */
|
||||
export const KILO_CHAT_URL_ENV = "KILO_CHAT_URL"
|
||||
|
||||
/** Default Kilo Chat URL (REST endpoint for messages, conversations, etc.) */
|
||||
export const KILO_DEFAULT_CHAT_URL = "https://chat.kiloapps.io"
|
||||
|
||||
/** Base URL for Kilo Chat - can be overridden by KILO_CHAT_URL env var */
|
||||
export const KILO_CHAT_URL = process.env[KILO_CHAT_URL_ENV] || KILO_DEFAULT_CHAT_URL
|
||||
|
||||
/** Environment variable for custom Event Service URL */
|
||||
export const KILO_EVENT_SERVICE_URL_ENV = "EVENT_SERVICE_URL"
|
||||
|
||||
/** Default Event Service URL (WebSocket endpoint for kilo-chat events) */
|
||||
export const KILO_DEFAULT_EVENT_SERVICE_URL = "wss://events.kiloapps.io"
|
||||
|
||||
/** Base URL for Event Service - can be overridden by EVENT_SERVICE_URL env var */
|
||||
export const KILO_EVENT_SERVICE_URL = process.env[KILO_EVENT_SERVICE_URL_ENV] || KILO_DEFAULT_EVENT_SERVICE_URL
|
||||
|
||||
/** Default base URL for OpenRouter-compatible endpoint */
|
||||
export const KILO_OPENROUTER_BASE = `${KILO_API_BASE}/api/openrouter`
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
import { fetchProfile, fetchBalance } from "../api/profile.js"
|
||||
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
|
||||
import { fetchOrganizationModes, clearModesCache } from "../api/modes.js"
|
||||
import { KILO_API_BASE, HEADER_FEATURE, HEADER_ORGANIZATIONID } from "../api/constants.js"
|
||||
import {
|
||||
KILO_API_BASE,
|
||||
KILO_CHAT_URL,
|
||||
KILO_EVENT_SERVICE_URL,
|
||||
HEADER_FEATURE,
|
||||
HEADER_ORGANIZATIONID,
|
||||
} from "../api/constants.js"
|
||||
import { buildKiloHeaders } from "../headers.js"
|
||||
import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js"
|
||||
import { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "../cloud-sessions.js"
|
||||
@@ -32,7 +38,7 @@ interface KiloRoutesDeps extends ImportDeps {
|
||||
Auth: Auth
|
||||
ModelCache: ModelCache
|
||||
z: Z
|
||||
Instance: ImportDeps["Instance"] & { disposeAll(): Promise<void> }
|
||||
InstanceStore: { disposeAllInstances(): Promise<void> }
|
||||
}
|
||||
|
||||
const FIM_TIMEOUT_MS = 30_000
|
||||
@@ -79,6 +85,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
SessionCreatedEvent,
|
||||
Identifier,
|
||||
ModelCache,
|
||||
InstanceStore,
|
||||
} = deps
|
||||
|
||||
const Organization = z.object({
|
||||
@@ -210,7 +217,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
|
||||
ModelCache.clear("kilo")
|
||||
clearModesCache()
|
||||
await Instance.disposeAll()
|
||||
await InstanceStore.disposeAllInstances()
|
||||
|
||||
return c.json(true)
|
||||
},
|
||||
@@ -524,8 +531,24 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
// `recovering` and `restoring` are transitional states the
|
||||
// worker reports while it brings an instance back online
|
||||
// after an unexpected stop or a snapshot restore — see
|
||||
// cloud `services/kiloclaw/src/index.ts` and the
|
||||
// `PlatformStatusResponse` type in
|
||||
// cloud/apps/web/src/lib/kiloclaw/types.ts. Keeping them in
|
||||
// the enum so the SDK types stay accurate.
|
||||
status: z
|
||||
.enum(["provisioned", "starting", "restarting", "running", "stopped", "destroying"])
|
||||
.enum([
|
||||
"provisioned",
|
||||
"starting",
|
||||
"restarting",
|
||||
"recovering",
|
||||
"running",
|
||||
"stopped",
|
||||
"destroying",
|
||||
"restoring",
|
||||
])
|
||||
.nullable(),
|
||||
sandboxId: z.string().optional(),
|
||||
flyRegion: z.string().optional(),
|
||||
@@ -536,6 +559,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
channelCount: z.number().optional(),
|
||||
secretCount: z.number().optional(),
|
||||
userId: z.string().optional(),
|
||||
botName: z.string().nullable().optional(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -578,57 +602,52 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
"/claw/chat-credentials",
|
||||
describeRoute({
|
||||
summary: "Get KiloClaw chat credentials",
|
||||
description: "Fetch Stream Chat credentials for the user's KiloClaw instance",
|
||||
description:
|
||||
"Returns the bearer token and endpoint URLs the client uses to talk to the Kilo Chat worker " +
|
||||
"and the Event Service. The bearer is the user's existing long-lived Kilo JWT — kilo-chat and " +
|
||||
"event-service both verify it directly with NEXTAUTH_SECRET, so no separate token mint is needed.",
|
||||
operationId: "kilo.claw.chatCredentials",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Stream Chat credentials or null",
|
||||
description: "Kilo Chat credentials or null",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
apiKey: z.string(),
|
||||
userId: z.string(),
|
||||
userToken: z.string(),
|
||||
channelId: z.string(),
|
||||
token: z.string(),
|
||||
expiresAt: z.string(),
|
||||
kiloChatUrl: z.string(),
|
||||
eventServiceUrl: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(401, 502),
|
||||
...errors(401),
|
||||
},
|
||||
}),
|
||||
async (c: any) => {
|
||||
try {
|
||||
const auth = await Auth.get("kilo")
|
||||
if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401)
|
||||
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
|
||||
if (!token) return c.json({ error: "No valid token found" }, 401)
|
||||
const auth = await Auth.get("kilo")
|
||||
if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401)
|
||||
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
|
||||
if (!token) return c.json({ error: "No valid token found" }, 401)
|
||||
|
||||
const organizationId = auth.type === "oauth" ? auth.accountId : undefined
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if (organizationId) {
|
||||
headers[HEADER_ORGANIZATIONID] = organizationId
|
||||
}
|
||||
// For OAuth, expires is a millisecond epoch we already track. For
|
||||
// API tokens we don't have a verified expiry locally — the JWT is
|
||||
// signed by the cloud and validated by kilo-chat/event-service on
|
||||
// every request. Use a far-future placeholder so the client cache
|
||||
// doesn't refetch unnecessarily; on 401 the client clears the
|
||||
// cache and prompts re-auth.
|
||||
const expiresAtMs = auth.type === "oauth" ? auth.expires : Date.now() + 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/chat-credentials`, { headers })
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
return c.json({ error: `KiloClaw request failed: ${response.status} ${text}` }, response.status as any)
|
||||
}
|
||||
|
||||
return c.json(await response.json())
|
||||
} catch (err: any) {
|
||||
console.error("[Kilo Gateway] claw/chat-credentials: error", err?.message ?? err)
|
||||
return c.json({ error: "Failed to reach KiloClaw" }, 502)
|
||||
}
|
||||
return c.json({
|
||||
token,
|
||||
expiresAt: new Date(expiresAtMs).toISOString(),
|
||||
kiloChatUrl: KILO_CHAT_URL,
|
||||
eventServiceUrl: KILO_EVENT_SERVICE_URL,
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"أوقف لقطات Kilo لهذا المشروع. ستفقد ميزة التراجع/الإعادة لتعديلات Kilo، لكن git سيستمر في تتبع كل شيء.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "فتح في عارض الفروقات",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "كان هذا مفيدًا",
|
||||
"ui.message.feedback.notHelpful": "لم يكن هذا مفيدًا",
|
||||
"ui.message.feedback.clearRating": "مسح التقييم",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Desligue os snapshots do Kilo para este projeto. Você perde desfazer/refazer das mudanças feitas pelo Kilo, mas o git continua rastreando tudo.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Abrir no Visualizador de Diferenças",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Isso foi útil",
|
||||
"ui.message.feedback.notHelpful": "Isso não foi útil",
|
||||
"ui.message.feedback.clearRating": "Limpar avaliação",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -97,4 +97,9 @@ export const dict = {
|
||||
"Isključi Kilo snapshotove za ovaj projekat. Izgubićete poništi/vrati za izmjene koje napravi Kilo, ali git i dalje prati sve.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Otvori u pregledniku razlika",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Ovo je bilo korisno",
|
||||
"ui.message.feedback.notHelpful": "Ovo nije bilo korisno",
|
||||
"ui.message.feedback.clearRating": "Obriši ocjenu",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Slå Kilos snapshots fra for dette projekt. Du mister fortryd/gentag for Kilo-ændringer, men git sporer stadig alt.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Åbn i diff-viser",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Dette var nyttigt",
|
||||
"ui.message.feedback.notHelpful": "Dette var ikke nyttigt",
|
||||
"ui.message.feedback.clearRating": "Ryd bedømmelse",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Kilo-Snapshots für dieses Projekt ausschalten. Rückgängig/Wiederherstellen für Kilo-Änderungen ist nicht mehr möglich, aber git verfolgt weiterhin alles.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Im Diff-Viewer öffnen",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Das war hilfreich",
|
||||
"ui.message.feedback.notHelpful": "Das war nicht hilfreich",
|
||||
"ui.message.feedback.clearRating": "Bewertung löschen",
|
||||
}
|
||||
|
||||
@@ -99,4 +99,9 @@ export const dict = {
|
||||
|
||||
// Edit-tool header: hover-revealed action opening the diff in a full tab.
|
||||
"ui.messagePart.openInDiffViewer": "Open in Diff Viewer",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "This was helpful",
|
||||
"ui.message.feedback.notHelpful": "This wasn't helpful",
|
||||
"ui.message.feedback.clearRating": "Clear rating",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Apaga las instantáneas de Kilo para este proyecto. Perderás deshacer/rehacer de los cambios de Kilo, pero git seguirá rastreando todo.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Abrir en el visor de diferencias",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Esto fue útil",
|
||||
"ui.message.feedback.notHelpful": "Esto no fue útil",
|
||||
"ui.message.feedback.clearRating": "Borrar valoración",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Désactivez les instantanés Kilo pour ce projet. Vous perdez l'annulation/restauration des modifications faites par Kilo, mais git continue de tout suivre.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Ouvrir dans la visionneuse de différences",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "C'était utile",
|
||||
"ui.message.feedback.notHelpful": "Ce n'était pas utile",
|
||||
"ui.message.feedback.clearRating": "Effacer la notation",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -90,4 +90,9 @@ export const dict = {
|
||||
"このプロジェクトでは Kilo のスナップショットを無効にします。Kilo による変更の取り消し/やり直しはできなくなりますが、git は引き続きすべてを追跡します。",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "差分ビューアで開く",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "役に立ちました",
|
||||
"ui.message.feedback.notHelpful": "役に立ちませんでした",
|
||||
"ui.message.feedback.clearRating": "評価をクリア",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -90,4 +90,9 @@ export const dict = {
|
||||
"이 프로젝트의 Kilo 스냅샷을 끕니다. Kilo 변경에 대한 실행 취소/다시 실행은 사용할 수 없지만 git은 여전히 모든 것을 추적합니다.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Diff 뷰어에서 열기",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "도움이 됐어요",
|
||||
"ui.message.feedback.notHelpful": "도움이 안 됐어요",
|
||||
"ui.message.feedback.clearRating": "평가 지우기",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -94,4 +94,9 @@ export const dict = {
|
||||
"Zet Kilo-snapshots uit voor dit project. Je verliest ongedaan maken/opnieuw doen van Kilo-wijzigingen, maar git blijft alles volgen.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Openen in diff-viewer",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Dit was nuttig",
|
||||
"ui.message.feedback.notHelpful": "Dit was niet nuttig",
|
||||
"ui.message.feedback.clearRating": "Beoordeling wissen",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Slå av Kilos snapshots for dette prosjektet. Du mister angre/gjør om for Kilo-endringer, men git fortsetter å spore alt.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Åpne i diff-viser",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Dette var nyttig",
|
||||
"ui.message.feedback.notHelpful": "Dette var ikke nyttig",
|
||||
"ui.message.feedback.clearRating": "Fjern vurdering",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Wyłącz snapshoty Kilo dla tego projektu. Stracisz cofanie/przywracanie zmian Kilo, ale git nadal śledzi wszystko.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Otwórz w podglądzie różnic",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "To było pomocne",
|
||||
"ui.message.feedback.notHelpful": "To nie było pomocne",
|
||||
"ui.message.feedback.clearRating": "Wyczyść ocenę",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Выключите снимки Kilo для этого проекта. Вы потеряете отмену/повтор изменений Kilo, но git по-прежнему отслеживает всё.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Открыть в просмотрщике различий",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Это было полезно",
|
||||
"ui.message.feedback.notHelpful": "Это было бесполезно",
|
||||
"ui.message.feedback.clearRating": "Очистить оценку",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"ปิดสแน็ปช็อตของ Kilo สำหรับโปรเจกต์นี้ คุณจะสูญเสียการยกเลิก/ทำซ้ำสำหรับการเปลี่ยนแปลงของ Kilo แต่ git ยังคงติดตามทุกอย่าง",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "เปิดในตัวดูความแตกต่าง",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์",
|
||||
"ui.message.feedback.notHelpful": "สิ่งนี้ไม่มีประโยชน์",
|
||||
"ui.message.feedback.clearRating": "ล้างการให้คะแนน",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Bu proje için Kilo anlık görüntülerini kapat. Kilo değişiklikleri için geri alma/yeniden yapmayı kaybedersiniz, ancak git her şeyi izlemeye devam eder.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Fark görüntüleyicide aç",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Bu yardımcı oldu",
|
||||
"ui.message.feedback.notHelpful": "Bu yardımcı olmadı",
|
||||
"ui.message.feedback.clearRating": "Değerlendirmeyi temizle",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -92,4 +92,9 @@ export const dict = {
|
||||
"Вимкніть знімки Kilo для цього проєкту. Ви втратите скасування/повторення для змін Kilo, але git продовжить відстежувати все.",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "Відкрити у переглядачі відмінностей",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "Це було корисно",
|
||||
"ui.message.feedback.notHelpful": "Це не було корисно",
|
||||
"ui.message.feedback.clearRating": "Очистити оцінку",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -87,4 +87,9 @@ export const dict = {
|
||||
"关闭本项目的 Kilo 快照。你将失去对 Kilo 更改的撤销/重做,但 git 仍会追踪所有内容。",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "在差异查看器中打开",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "这有帮助",
|
||||
"ui.message.feedback.notHelpful": "这没有帮助",
|
||||
"ui.message.feedback.clearRating": "清除评分",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -87,4 +87,9 @@ export const dict = {
|
||||
"關閉本專案的 Kilo 快照。你將失去對 Kilo 變更的撤銷/重做,但 git 仍會追蹤所有內容。",
|
||||
|
||||
"ui.messagePart.openInDiffViewer": "在差異檢視器中開啟",
|
||||
|
||||
// Message feedback (thumbs up/down per assistant response)
|
||||
"ui.message.feedback.helpful": "這有幫助",
|
||||
"ui.message.feedback.notHelpful": "這沒有幫助",
|
||||
"ui.message.feedback.clearRating": "清除評分",
|
||||
}
|
||||
|
||||
+33
@@ -6,6 +6,7 @@ import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
@@ -49,6 +50,7 @@ class KiloBackendChatManager(
|
||||
"session.turn.close",
|
||||
"session.error",
|
||||
"session.status",
|
||||
"session.updated",
|
||||
"session.idle",
|
||||
"session.compacted",
|
||||
"session.diff",
|
||||
@@ -156,6 +158,37 @@ class KiloBackendChatManager(
|
||||
}
|
||||
}
|
||||
|
||||
// ------ compact ------
|
||||
|
||||
fun compact(id: String, dir: String, model: ModelSelectionDto) {
|
||||
log.info("${ChatLogSummary.sid(id)} kind=compact ${ChatLogSummary.dir(dir)} model=${model.providerID}/${model.modelID} op=summarize")
|
||||
val http = requireClient()
|
||||
val url = requireBase()
|
||||
val body = KiloCliDataParser.buildSummarizeJson(model)
|
||||
val request = Request.Builder()
|
||||
.url("$url/session/$id/summarize?directory=${encode(dir)}")
|
||||
.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("summarize failed: HTTP $code")
|
||||
raw?.let { log.debug { "${ChatLogSummary.sid(id)} kind=compact op=summarize error=${ChatLogSummary.body(it)}" } }
|
||||
throw RuntimeException("summarize failed: HTTP $code")
|
||||
}
|
||||
log.debug { "${ChatLogSummary.sid(id)} kind=compact op=summarize ok=true code=$code" }
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
log.warn("${ChatLogSummary.sid(id)} kind=compact op=summarize dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
|
||||
throw RuntimeException("summarize HTTP call failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// ------ messages ------
|
||||
|
||||
fun messages(id: String, dir: String): List<MessageWithPartsDto> {
|
||||
|
||||
+29
-10
@@ -174,6 +174,13 @@ object KiloCliDataParser {
|
||||
ChatEventDto.SessionStatusChanged(sid, dto)
|
||||
}
|
||||
|
||||
"session.updated" -> {
|
||||
val info = props["info"]?.jsonObject ?: return null
|
||||
val dto = parseSessionObject(info)
|
||||
val sid = props.str("sessionID") ?: dto.id.takeIf { it.isNotBlank() } ?: return null
|
||||
ChatEventDto.SessionUpdated(sid, dto)
|
||||
}
|
||||
|
||||
"session.idle" -> {
|
||||
val sid = props.str("sessionID") ?: return null
|
||||
ChatEventDto.SessionIdle(sid)
|
||||
@@ -316,6 +323,12 @@ object KiloCliDataParser {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the JSON body for `POST /session/{id}/summarize`.
|
||||
*/
|
||||
fun buildSummarizeJson(model: ModelSelectionDto): String =
|
||||
"""{"providerID":${escape(model.providerID)},"modelID":${escape(model.modelID)}}"""
|
||||
|
||||
/**
|
||||
* Build the partial JSON body for `PATCH /global/config`.
|
||||
*/
|
||||
@@ -363,22 +376,14 @@ object KiloCliDataParser {
|
||||
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,
|
||||
)
|
||||
},
|
||||
tokens = tokens?.let(::parseTokens),
|
||||
error = error?.let { parseError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parsePart(obj: JsonObject): PartDto {
|
||||
val state = obj["state"]?.jsonObject
|
||||
val tokens = obj["tokens"]?.jsonObject
|
||||
val top = obj.map("metadata")
|
||||
val meta = state.map("metadata") + top
|
||||
return PartDto(
|
||||
@@ -396,6 +401,20 @@ object KiloCliDataParser {
|
||||
output = state?.str("output"),
|
||||
error = state?.str("error"),
|
||||
time = obj.time("time") ?: state.time("time"),
|
||||
reason = obj.str("reason"),
|
||||
cost = obj.num("cost"),
|
||||
tokens = tokens?.let(::parseTokens),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTokens(obj: JsonObject): TokensDto {
|
||||
val cache = obj["cache"]?.jsonObject
|
||||
return TokensDto(
|
||||
input = obj.long("input") ?: 0,
|
||||
output = obj.long("output") ?: 0,
|
||||
reasoning = obj.long("reasoning") ?: 0,
|
||||
cacheRead = cache?.long("read") ?: 0,
|
||||
cacheWrite = cache?.long("write") ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -11,6 +11,7 @@ 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.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
@@ -88,6 +89,9 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
override suspend fun abort(id: String, directory: String) =
|
||||
chat.abort(id, directory)
|
||||
|
||||
override suspend fun compact(id: String, directory: String, model: ModelSelectionDto) =
|
||||
chat.compact(id, directory, model)
|
||||
|
||||
override suspend fun messages(id: String, directory: String): List<MessageWithPartsDto> =
|
||||
chat.messages(id, directory)
|
||||
|
||||
@@ -108,6 +112,7 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
is ChatEventDto.QuestionReplied -> event.sessionID
|
||||
is ChatEventDto.QuestionRejected -> event.sessionID
|
||||
is ChatEventDto.SessionStatusChanged -> event.sessionID
|
||||
is ChatEventDto.SessionUpdated -> event.sessionID
|
||||
is ChatEventDto.SessionIdle -> event.sessionID
|
||||
is ChatEventDto.SessionCompacted -> event.sessionID
|
||||
is ChatEventDto.SessionDiffChanged -> event.sessionID
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.LoadErrorDto
|
||||
import ai.kilocode.rpc.dto.ModelDto
|
||||
import ai.kilocode.rpc.dto.ModelLimitDto
|
||||
import ai.kilocode.rpc.dto.ProviderDto
|
||||
import ai.kilocode.rpc.dto.ProvidersDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
@@ -142,6 +143,7 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
||||
status = m.status,
|
||||
recommendedIndex = m.recommendedIndex,
|
||||
variants = m.variants,
|
||||
limit = m.limit?.let { ModelLimitDto(it.context, it.input, it.output) },
|
||||
)
|
||||
|
||||
private fun agents(d: AgentData) = AgentsDto(
|
||||
|
||||
+10
@@ -16,6 +16,7 @@ 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 kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -295,6 +296,7 @@ class KiloBackendWorkspace(
|
||||
|
||||
private fun model(id: String, obj: JsonObject): ModelInfo {
|
||||
val cap = obj["capabilities"]?.jsonObject
|
||||
val limit = obj["limit"]?.jsonObject
|
||||
return ModelInfo(
|
||||
id = obj.str("id") ?: id,
|
||||
name = obj.str("name") ?: id,
|
||||
@@ -306,6 +308,13 @@ class KiloBackendWorkspace(
|
||||
status = obj.str("status"),
|
||||
recommendedIndex = obj.num("recommendedIndex"),
|
||||
variants = variants(obj),
|
||||
limit = limit?.let {
|
||||
ModelLimitInfo(
|
||||
context = it.long("context") ?: 0,
|
||||
input = it.long("input"),
|
||||
output = it.long("output") ?: 0,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -361,3 +370,4 @@ private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.U
|
||||
private fun JsonObject.str(key: String) = this[key]?.jsonPrimitive?.contentOrNull
|
||||
private fun JsonObject?.bool(key: String) = this?.get(key)?.jsonPrimitive?.booleanOrNull ?: false
|
||||
private fun JsonObject.num(key: String) = this[key]?.jsonPrimitive?.doubleOrNull
|
||||
private fun JsonObject.long(key: String) = this[key]?.jsonPrimitive?.longOrNull
|
||||
|
||||
+7
@@ -57,6 +57,13 @@ data class ModelInfo(
|
||||
val status: String?,
|
||||
val recommendedIndex: Double?,
|
||||
val variants: List<String>,
|
||||
val limit: ModelLimitInfo?,
|
||||
)
|
||||
|
||||
data class ModelLimitInfo(
|
||||
val context: Long = 0,
|
||||
val input: Long? = null,
|
||||
val output: Long = 0,
|
||||
)
|
||||
|
||||
data class AgentData(
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.testing.MockCliServer
|
||||
import ai.kilocode.backend.testing.TestLog
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class KiloBackendChatManagerTest {
|
||||
|
||||
private val mock = MockCliServer()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
scope.cancel()
|
||||
mock.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compact posts summarize request with selected model`() {
|
||||
val port = mock.start()
|
||||
val chat = KiloBackendChatManager(scope, TestLog())
|
||||
chat.start(OkHttpClient(), port, MutableSharedFlow())
|
||||
|
||||
chat.compact("ses_abc", "/test/project", ModelSelectionDto("anthropic", "claude-4"))
|
||||
|
||||
assertEquals(1, mock.requestCount("/session/ses_abc/summarize"))
|
||||
assertNotNull(mock.lastSummarizePath)
|
||||
assertTrue(mock.lastSummarizePath!!.startsWith("/session/ses_abc/summarize?directory="))
|
||||
assertEquals("""{"providerID":"anthropic","modelID":"claude-4"}""", mock.lastSummarizeBody)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -188,7 +188,7 @@ class GeneratedApiModelSerializationTest {
|
||||
assertEquals(true, obj.native)
|
||||
assertEquals(false, obj.hidden)
|
||||
assertEquals("#FF5733", obj.color)
|
||||
assertEquals(5, obj.steps)
|
||||
assertEquals(5.0, obj.steps)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+59
@@ -451,6 +451,32 @@ class KiloCliDataParserTest {
|
||||
assertEquals("Read file.txt", part.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseMessages - step finish part with tokens`() {
|
||||
val raw = """[{
|
||||
"info": { "id": "m1", "sessionID": "s1", "role": "assistant", "time": { "created": 1.0 } },
|
||||
"parts": [{
|
||||
"id": "p1",
|
||||
"sessionID": "s1",
|
||||
"messageID": "m1",
|
||||
"type": "step-finish",
|
||||
"reason": "stop",
|
||||
"cost": 0.005,
|
||||
"tokens": { "input": 100, "output": 50, "reasoning": 10, "cache": { "read": 20, "write": 5 } }
|
||||
}]
|
||||
}]"""
|
||||
|
||||
val part = KiloCliDataParser.parseMessages(raw)[0].parts[0]
|
||||
assertEquals("step-finish", part.type)
|
||||
assertEquals("stop", part.reason)
|
||||
assertEquals(0.005, part.cost)
|
||||
assertEquals(100L, part.tokens?.input)
|
||||
assertEquals(50L, part.tokens?.output)
|
||||
assertEquals(10L, part.tokens?.reasoning)
|
||||
assertEquals(20L, part.tokens?.cacheRead)
|
||||
assertEquals(5L, part.tokens?.cacheWrite)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseMessages - malformed JSON returns empty`() {
|
||||
assertEquals(emptyList(), KiloCliDataParser.parseMessages("not json"))
|
||||
@@ -619,6 +645,13 @@ class KiloCliDataParserTest {
|
||||
assertTrue(result.contains("""line1\nline2\t\"quoted\""""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildSummarizeJson - writes provider and model`() {
|
||||
val result = KiloCliDataParser.buildSummarizeJson(ModelSelectionDto("anthropic", "claude-4"))
|
||||
|
||||
assertEquals("""{"providerID":"anthropic","modelID":"claude-4"}""", result)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// buildConfigPartial
|
||||
// ================================================================
|
||||
@@ -866,6 +899,32 @@ class KiloCliDataParserTest {
|
||||
assertTrue(result is ChatEventDto.SessionCompacted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseChatEvent - session updated`() {
|
||||
val data = globalEvent("""
|
||||
"type": "session.updated",
|
||||
"properties": {
|
||||
"sessionID": "ses_1",
|
||||
"info": {
|
||||
"id": "ses_1",
|
||||
"projectID": "proj_1",
|
||||
"directory": "/tmp/project",
|
||||
"title": "Updated title",
|
||||
"version": "1",
|
||||
"time": { "created": 1.0, "updated": 2.0 },
|
||||
"summary": { "additions": 3, "deletions": 1, "files": 2 }
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val result = KiloCliDataParser.parseChatEvent("session.updated", data)
|
||||
assertNotNull(result)
|
||||
assertTrue(result is ChatEventDto.SessionUpdated)
|
||||
assertEquals("ses_1", result.sessionID)
|
||||
assertEquals("Updated title", result.session.title)
|
||||
assertEquals(2, result.session.summary?.files)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseChatEvent - session diff`() {
|
||||
val data = globalEvent("""
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ class ProjectModelSerializationTest {
|
||||
assertEquals(true, obj.native)
|
||||
assertEquals(false, obj.hidden)
|
||||
assertEquals("#FF5733", obj.color)
|
||||
assertEquals(5, obj.steps)
|
||||
assertEquals(5.0, obj.steps)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+15
-1
@@ -55,12 +55,16 @@ class MockCliServer : AutoCloseable {
|
||||
@Volatile var recentSessions = "[]"
|
||||
@Volatile var sessionCreate = """{"id":"ses_test","slug":"test","projectID":"prj_test","directory":"/test","title":"New Session","version":"1.0.0","time":{"created":1000,"updated":1000}}"""
|
||||
@Volatile var sessionStatuses = "{}"
|
||||
@Volatile var summarizeResponse = "true"
|
||||
@Volatile var sessionsStatus = 200
|
||||
@Volatile var recentSessionsStatus = 200
|
||||
@Volatile var sessionCreateStatus = 200
|
||||
@Volatile var sessionGetStatus = 200
|
||||
@Volatile var sessionDeleteStatus = 200
|
||||
@Volatile var sessionStatusesStatus = 200
|
||||
@Volatile var summarizeStatus = 200
|
||||
@Volatile var lastSummarizePath: String? = null
|
||||
@Volatile var lastSummarizeBody: String? = null
|
||||
|
||||
/** Configurable delay for all endpoint responses (ms). 0 = no delay. */
|
||||
@Volatile var responseDelay: Long = 0
|
||||
@@ -173,11 +177,16 @@ class MockCliServer : AutoCloseable {
|
||||
val method = parts[0]
|
||||
val path = parts[1]
|
||||
|
||||
// Read all headers
|
||||
var len = 0
|
||||
while (true) {
|
||||
val header = input.readLine()
|
||||
if (header.isNullOrBlank()) break
|
||||
val parts = header.split(":", limit = 2)
|
||||
if (parts.size == 2 && parts[0].equals("Content-Length", ignoreCase = true)) {
|
||||
len = parts[1].trim().toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
val body = if (len > 0) CharArray(len).also { input.read(it, 0, len) }.concatToString() else ""
|
||||
|
||||
val output = BufferedWriter(OutputStreamWriter(socket.getOutputStream()))
|
||||
val bare = path.substringBefore("?")
|
||||
@@ -218,6 +227,11 @@ class MockCliServer : AutoCloseable {
|
||||
respond(output, sessionGetStatus, sessionCreate)
|
||||
bare.matches(Regex("/session/ses_[^/]+")) && method == "DELETE" ->
|
||||
respond(output, sessionDeleteStatus, "true")
|
||||
bare.matches(Regex("/session/ses_[^/]+/summarize")) && method == "POST" -> {
|
||||
lastSummarizePath = path
|
||||
lastSummarizeBody = body
|
||||
respond(output, summarizeStatus, summarizeResponse)
|
||||
}
|
||||
else -> respond(output, 404, """{"error":"Not found"}""")
|
||||
}
|
||||
} catch (_: SocketException) {
|
||||
|
||||
+4
-1
@@ -297,6 +297,9 @@ class KiloBackendWorkspaceTest {
|
||||
assertTrue(model.toolCall)
|
||||
assertEquals(2.0, model.recommendedIndex)
|
||||
assertEquals(listOf("low", "medium", "high"), model.variants)
|
||||
assertEquals(200000L, model.limit?.context)
|
||||
assertEquals(100000L, model.limit?.input)
|
||||
assertEquals(16000L, model.limit?.output)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -513,7 +516,7 @@ class KiloBackendWorkspaceTest {
|
||||
"interleaved": false
|
||||
},
|
||||
"cost": {"input": 0, "output": 0, "cache": {"read": 0, "write": 0}},
|
||||
"limit": {"context": 200000, "output": 16000},
|
||||
"limit": {"context": 200000, "input": 100000, "output": 16000},
|
||||
"status": "active",
|
||||
"recommendedIndex": 2,
|
||||
"variants": {"high": {}, "low": {}, "medium": {}},
|
||||
|
||||
+10
@@ -7,6 +7,7 @@ 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.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
@@ -87,6 +88,10 @@ class KiloSessionService internal constructor(
|
||||
suspend fun recent(dir: String, limit: Int): List<SessionDto> =
|
||||
call { recent(dir, limit) }.sessions
|
||||
|
||||
/** Get a single session. */
|
||||
suspend fun get(id: String, dir: String): SessionDto =
|
||||
call { get(id, dir) }
|
||||
|
||||
/** Create a new session. Caller awaits the result. */
|
||||
suspend fun create(dir: String): SessionDto {
|
||||
LOG.info("create: dir=$dir")
|
||||
@@ -138,6 +143,11 @@ class KiloSessionService internal constructor(
|
||||
call { abort(id, dir) }
|
||||
}
|
||||
|
||||
/** Summarize/compact a session. */
|
||||
suspend fun compact(id: String, dir: String, model: ModelSelectionDto) {
|
||||
call { compact(id, dir, model) }
|
||||
}
|
||||
|
||||
/** Load message history for a session. */
|
||||
suspend fun messages(id: String, dir: String): List<MessageWithPartsDto> =
|
||||
call { messages(id, dir) }
|
||||
|
||||
+4
-4
@@ -15,8 +15,8 @@ import javax.swing.JPanel
|
||||
class SessionSidePanelManager(
|
||||
private val project: Project,
|
||||
private val root: Workspace,
|
||||
private val create: (Project, Workspace, SessionManager, String?, Boolean) -> SessionUi = { project, workspace, manager, id, loading ->
|
||||
service<SessionUiFactory>().create(project, workspace, manager, id, loading)
|
||||
private val create: (Project, Workspace, SessionManager, String?, Boolean, SessionDto?) -> SessionUi = { project, workspace, manager, id, loading, session ->
|
||||
service<SessionUiFactory>().create(project, workspace, manager, id, loading, session)
|
||||
},
|
||||
private val resolve: (String) -> Workspace = { dir -> service<KiloWorkspaceService>().workspace(dir) },
|
||||
) : SessionManager, Disposable {
|
||||
@@ -37,13 +37,13 @@ class SessionSidePanelManager(
|
||||
val active = current
|
||||
if (active?.blank == true) return
|
||||
register(active)
|
||||
show(create(project, root, this, null, active == null))
|
||||
show(create(project, root, this, null, active == null, null))
|
||||
}
|
||||
|
||||
override fun openSession(session: SessionDto) {
|
||||
register(current)
|
||||
val ui = opened.getOrPut(session.id) {
|
||||
create(project, resolve(session.directory), this, session.id, false).also {
|
||||
create(project, resolve(session.directory), this, session.id, false, session).also {
|
||||
all.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -16,6 +16,7 @@ import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.session.ui.QuestionPanel
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.SessionMessageListPanel
|
||||
import ai.kilocode.client.session.ui.header.SessionHeaderPanel
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.session.update.EVENT_FLUSH_MS
|
||||
@@ -56,6 +57,7 @@ class SessionUi private constructor(
|
||||
displayMs: Long,
|
||||
open: (SessionDto) -> Unit,
|
||||
private val loading: Boolean,
|
||||
session: SessionDto? = null,
|
||||
) : JPanel(BorderLayout()), Disposable, SessionStyleTarget {
|
||||
|
||||
constructor(
|
||||
@@ -67,7 +69,8 @@ class SessionUi private constructor(
|
||||
id: String? = null,
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
) : this(project, workspace, sessions, app, cs, id, displayMs, open, id == null)
|
||||
session: SessionDto? = null,
|
||||
) : this(project, workspace, sessions, app, cs, session?.id ?: id, displayMs, open, id == null, session)
|
||||
|
||||
internal constructor(
|
||||
project: Project,
|
||||
@@ -79,7 +82,8 @@ class SessionUi private constructor(
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
loading: Boolean,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
) : this(project, workspace, sessions, app, cs, id, displayMs, open, loading)
|
||||
session: SessionDto? = null,
|
||||
) : this(project, workspace, sessions, app, cs, id, displayMs, open, loading, session)
|
||||
|
||||
companion object {
|
||||
private val LOG = KiloLog.create(SessionUi::class.java)
|
||||
@@ -101,6 +105,7 @@ class SessionUi private constructor(
|
||||
flushMs = flushMs,
|
||||
condense = Registry.`is`("kilo.session.condense", true),
|
||||
displayMs = displayMs,
|
||||
session = session,
|
||||
open = open,
|
||||
beforeUpdate = { if (opening) false else scroll.atBottom() },
|
||||
afterUpdate = { if (!opening) scroll.followBottom(it) },
|
||||
@@ -118,6 +123,8 @@ class SessionUi private constructor(
|
||||
|
||||
private lateinit var messageBody: SessionMessageListPanel
|
||||
|
||||
private lateinit var header: SessionHeaderPanel
|
||||
|
||||
internal lateinit var scroll: SessionScroll
|
||||
|
||||
private lateinit var question: QuestionPanel
|
||||
@@ -174,6 +181,7 @@ class SessionUi private constructor(
|
||||
), BorderLayout.CENTER)
|
||||
}
|
||||
messageBody = SessionMessageListPanel(controller.model, this)
|
||||
header = SessionHeaderPanel(controller, this)
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
question = QuestionPanel(controller)
|
||||
@@ -186,6 +194,7 @@ class SessionUi private constructor(
|
||||
onAbort = { controller.abort() },
|
||||
)
|
||||
|
||||
sessionContent.add(header, BorderLayout.NORTH)
|
||||
sessionContent.add(scroll.component, BorderLayout.CENTER)
|
||||
root.content.add(sessionContent, BorderLayout.CENTER)
|
||||
// Dock panels stay in normal flow so each visible state takes layout space
|
||||
@@ -278,6 +287,8 @@ class SessionUi private constructor(
|
||||
is SessionModelEvent.ContentRemoved,
|
||||
is SessionModelEvent.DiffUpdated,
|
||||
is SessionModelEvent.TodosUpdated,
|
||||
is SessionModelEvent.SessionUpdated,
|
||||
is SessionModelEvent.HeaderUpdated,
|
||||
is SessionModelEvent.Compacted,
|
||||
is SessionModelEvent.Cleared -> Unit
|
||||
}
|
||||
@@ -372,6 +383,7 @@ class SessionUi private constructor(
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
this.style = style
|
||||
loadingLabel.font = style.uiFont
|
||||
header.applyStyle(style)
|
||||
prompt.applyStyle(style)
|
||||
scroll.applyStyle(style)
|
||||
refresh()
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ package ai.kilocode.client.session
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -20,14 +21,16 @@ class SessionUiFactory(
|
||||
manager: SessionManager,
|
||||
id: String? = null,
|
||||
loading: Boolean = id == null,
|
||||
session: SessionDto? = null,
|
||||
): SessionUi = SessionUi(
|
||||
project = project,
|
||||
workspace = workspace,
|
||||
sessions = project.service<KiloSessionService>(),
|
||||
app = service<KiloAppService>(),
|
||||
cs = scope(),
|
||||
id = id,
|
||||
id = session?.id ?: id,
|
||||
loading = loading,
|
||||
session = session,
|
||||
open = manager::openSession,
|
||||
)
|
||||
|
||||
|
||||
+60
-1
@@ -2,6 +2,47 @@ package ai.kilocode.client.session.model
|
||||
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
import ai.kilocode.rpc.dto.PartTimeDto
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
|
||||
data class SessionHeaderSnapshot(
|
||||
val visible: Boolean,
|
||||
val title: String,
|
||||
val cost: Double?,
|
||||
val context: ContextUsage?,
|
||||
val tokens: TokensDto?,
|
||||
val timeline: List<TimelineItem>,
|
||||
val todos: TodoSummary,
|
||||
val canCompact: Boolean,
|
||||
)
|
||||
|
||||
data class ContextUsage(
|
||||
val tokens: Long,
|
||||
val percentage: Int?,
|
||||
val limit: Long?,
|
||||
val output: Long?,
|
||||
)
|
||||
|
||||
data class TimelineItem(
|
||||
val id: String,
|
||||
val part: Content,
|
||||
val title: String,
|
||||
val weight: Int,
|
||||
val durationMs: Long?,
|
||||
val active: Boolean,
|
||||
)
|
||||
|
||||
data class TodoSummary(
|
||||
val total: Int,
|
||||
val completed: Int,
|
||||
val items: List<TodoDto>,
|
||||
)
|
||||
|
||||
data class ModelLimitItem(
|
||||
val context: Long = 0,
|
||||
val input: Long? = null,
|
||||
val output: Long = 0,
|
||||
)
|
||||
|
||||
/** A single message with its typed contents. */
|
||||
class Message(
|
||||
@@ -25,7 +66,7 @@ class Reasoning(id: String) : Content(id) {
|
||||
}
|
||||
|
||||
/** Tool invocation with lifecycle state. */
|
||||
class Tool(id: String, val name: String) : Content(id) {
|
||||
class Tool(id: String, val name: String, var kind: ToolKind) : Content(id) {
|
||||
var state: ToolExecState = ToolExecState.PENDING
|
||||
var title: String? = null
|
||||
var input: Map<String, String> = emptyMap()
|
||||
@@ -38,6 +79,13 @@ class Tool(id: String, val name: String) : Content(id) {
|
||||
/** Context compaction marker. */
|
||||
class Compaction(id: String) : Content(id)
|
||||
|
||||
/** Assistant step completion marker used by the session header timeline. */
|
||||
class StepFinish(id: String) : Content(id) {
|
||||
var reason: String? = null
|
||||
var cost: Double? = null
|
||||
var tokens: TokensDto? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic fallback for part types not yet given a dedicated class.
|
||||
* Preserves the [type] string so unknown content is not silently dropped.
|
||||
@@ -46,6 +94,17 @@ class Generic(id: String, val type: String) : Content(id)
|
||||
|
||||
enum class ToolExecState { PENDING, RUNNING, COMPLETED, ERROR }
|
||||
|
||||
enum class ToolKind { READ, WRITE, GENERIC }
|
||||
|
||||
private val READ_TOOLS = setOf("read", "glob", "grep", "find", "ls", "diagnostics", "warpgrep")
|
||||
private val WRITE_TOOLS = setOf("edit", "write", "patch", "multi_edit", "multiedit", "apply_patch")
|
||||
|
||||
fun toolKind(name: String?): ToolKind = when (name?.lowercase()) {
|
||||
in READ_TOOLS -> ToolKind.READ
|
||||
in WRITE_TOOLS -> ToolKind.WRITE
|
||||
else -> ToolKind.GENERIC
|
||||
}
|
||||
|
||||
data class ToolCallRef(
|
||||
val messageId: String,
|
||||
val callId: String,
|
||||
|
||||
+181
-2
@@ -8,9 +8,12 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Pure session model — single source of truth for session content and runtime state.
|
||||
@@ -32,7 +35,7 @@ class SessionModel {
|
||||
|
||||
companion object {
|
||||
/** Part types that are internal server markers and must never be stored or rendered. */
|
||||
val SILENT_PART_TYPES = setOf("step-start", "step-finish")
|
||||
val SILENT_PART_TYPES = setOf("step-start")
|
||||
}
|
||||
|
||||
private val entries = LinkedHashMap<String, Message>()
|
||||
@@ -55,6 +58,12 @@ class SessionModel {
|
||||
var state: SessionState = SessionState.Idle
|
||||
private set
|
||||
|
||||
var session: SessionDto? = null
|
||||
private set
|
||||
|
||||
var header: SessionHeaderSnapshot = emptyHeader()
|
||||
private set
|
||||
|
||||
var diff: List<DiffFileDto> = emptyList()
|
||||
private set
|
||||
|
||||
@@ -95,12 +104,14 @@ class SessionModel {
|
||||
val updated = Message(dto).also { it.parts.putAll(existing.parts) }
|
||||
entries[dto.id] = updated
|
||||
fire(SessionModelEvent.MessageUpdated(updated))
|
||||
updateHeader()
|
||||
return false
|
||||
}
|
||||
val msg = Message(dto)
|
||||
entries[dto.id] = msg
|
||||
fire(SessionModelEvent.MessageAdded(msg))
|
||||
regroup()
|
||||
updateHeader()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -111,6 +122,7 @@ class SessionModel {
|
||||
entries[dto.id] = msg
|
||||
fire(SessionModelEvent.MessageAdded(msg))
|
||||
regroup()
|
||||
updateHeader()
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -118,12 +130,14 @@ class SessionModel {
|
||||
if (entries.remove(id) == null) return
|
||||
fire(SessionModelEvent.MessageRemoved(id))
|
||||
regroup()
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun removeContent(messageId: String, contentId: String) {
|
||||
val msg = entries[messageId] ?: return
|
||||
if (msg.parts.remove(contentId) == null) return
|
||||
fire(SessionModelEvent.ContentRemoved(messageId, contentId))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun updateContent(messageId: String, dto: PartDto) {
|
||||
@@ -137,6 +151,7 @@ class SessionModel {
|
||||
val content = fromDto(dto)
|
||||
msg.parts[dto.id] = content
|
||||
fire(SessionModelEvent.ContentAdded(messageId, content))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun appendDelta(messageId: String, contentId: String, delta: String) {
|
||||
@@ -156,11 +171,20 @@ class SessionModel {
|
||||
fire(SessionModelEvent.ContentAdded(messageId, content))
|
||||
}
|
||||
fire(SessionModelEvent.ContentDelta(messageId, contentId, delta))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun setState(state: SessionState) {
|
||||
this.state = state
|
||||
fire(SessionModelEvent.StateChanged(state))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun setSession(session: SessionDto) {
|
||||
if (this.session == session) return
|
||||
this.session = session
|
||||
fire(SessionModelEvent.SessionUpdated(session))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun setDiff(diff: List<DiffFileDto>) {
|
||||
@@ -171,15 +195,22 @@ class SessionModel {
|
||||
fun setTodos(todos: List<TodoDto>) {
|
||||
this.todos = todos
|
||||
fire(SessionModelEvent.TodosUpdated(todos))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun markCompacted() {
|
||||
compactionCount++
|
||||
fire(SessionModelEvent.Compacted(compactionCount))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun refreshHeader() {
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun loadHistory(history: List<MessageWithPartsDto>) {
|
||||
entries.clear()
|
||||
session = null
|
||||
state = SessionState.Idle
|
||||
diff = emptyList()
|
||||
todos = emptyList()
|
||||
@@ -195,16 +226,19 @@ class SessionModel {
|
||||
}
|
||||
rebuildTurnsSilently()
|
||||
fire(SessionModelEvent.HistoryLoaded)
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entries.clear()
|
||||
turnEntries.clear()
|
||||
session = null
|
||||
state = SessionState.Idle
|
||||
diff = emptyList()
|
||||
todos = emptyList()
|
||||
compactionCount = 0
|
||||
fire(SessionModelEvent.Cleared)
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
// ------ turn grouping ------
|
||||
@@ -315,6 +349,7 @@ class SessionModel {
|
||||
existing.done = dto.time?.end != null || dto.time == null
|
||||
}
|
||||
is Tool -> {
|
||||
existing.kind = toolKind(dto.tool)
|
||||
existing.state = parseToolState(dto.state)
|
||||
existing.title = dto.title
|
||||
existing.input = dto.input
|
||||
@@ -324,9 +359,15 @@ class SessionModel {
|
||||
existing.time = dto.time
|
||||
}
|
||||
is Compaction -> return
|
||||
is StepFinish -> {
|
||||
existing.reason = dto.reason
|
||||
existing.cost = dto.cost
|
||||
existing.tokens = dto.tokens
|
||||
}
|
||||
is Generic -> return
|
||||
}
|
||||
fire(SessionModelEvent.ContentUpdated(messageId, existing))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
private fun fromDto(dto: PartDto, text: CharSequence? = null): Content {
|
||||
@@ -339,7 +380,7 @@ class SessionModel {
|
||||
if (content != null && content.isNotEmpty()) this.content.append(content)
|
||||
done = dto.time?.end != null || dto.time == null
|
||||
}
|
||||
"tool" -> Tool(dto.id, dto.tool ?: "unknown").apply {
|
||||
"tool" -> Tool(dto.id, dto.tool ?: "unknown", toolKind(dto.tool)).apply {
|
||||
state = parseToolState(dto.state)
|
||||
title = dto.title
|
||||
input = dto.input
|
||||
@@ -349,6 +390,11 @@ class SessionModel {
|
||||
time = dto.time
|
||||
}
|
||||
"compaction" -> Compaction(dto.id)
|
||||
"step-finish" -> StepFinish(dto.id).apply {
|
||||
reason = dto.reason
|
||||
cost = dto.cost
|
||||
tokens = dto.tokens
|
||||
}
|
||||
else -> Generic(dto.id, dto.type)
|
||||
}
|
||||
}
|
||||
@@ -357,6 +403,61 @@ class SessionModel {
|
||||
for (l in listeners) l.onEvent(event)
|
||||
}
|
||||
|
||||
private fun updateHeader() {
|
||||
val next = buildHeader()
|
||||
if (next == header) return
|
||||
header = next
|
||||
fire(SessionModelEvent.HeaderUpdated(next))
|
||||
}
|
||||
|
||||
private fun buildHeader(): SessionHeaderSnapshot {
|
||||
val items = messages().toList()
|
||||
if (items.isEmpty()) return emptyHeader()
|
||||
val last = items.asReversed()
|
||||
.firstOrNull { it.info.role == "assistant" && (it.info.tokens?.total()?.let { total -> total > 0 } == true) }
|
||||
val tokens = last?.info?.tokens
|
||||
val limit = model?.let(::item)?.limit
|
||||
val total = tokens?.total() ?: 0
|
||||
val context = if (tokens == null || total == 0L) null else ContextUsage(
|
||||
tokens = total,
|
||||
percentage = limit?.context?.takeIf { it > 0 }?.let { (total.toDouble() / it.toDouble() * 100).roundToInt() },
|
||||
limit = limit?.context?.takeIf { it > 0 },
|
||||
output = limit?.output?.takeIf { it > 0 },
|
||||
)
|
||||
val cost = items
|
||||
.filter { it.info.role == "assistant" }
|
||||
.sumOf { it.info.cost ?: 0.0 }
|
||||
.takeIf { it > 0.0 }
|
||||
val done = todos.count { it.status == "completed" }
|
||||
return SessionHeaderSnapshot(
|
||||
visible = items.isNotEmpty(),
|
||||
title = session?.title?.takeIf { it.isNotBlank() } ?: "New Session",
|
||||
cost = cost,
|
||||
context = context,
|
||||
tokens = tokens,
|
||||
timeline = timeline(items),
|
||||
todos = TodoSummary(todos.size, done, todos),
|
||||
canCompact = !state.isBusy() && model?.let(::parseModelKey) != null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun timeline(items: List<Message>): List<TimelineItem> = items
|
||||
.filter { it.info.role == "assistant" }
|
||||
.flatMap { msg ->
|
||||
msg.parts.values.map { part ->
|
||||
TimelineItem(
|
||||
id = "${msg.info.id}/${part.id}",
|
||||
part = part,
|
||||
title = part.timelineTitle(),
|
||||
weight = part.weight().coerceIn(1, 10),
|
||||
durationMs = (part as? Tool)?.time?.durationMs(),
|
||||
active = (part as? Tool)?.state == ToolExecState.RUNNING || part is Reasoning && !part.done,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun item(key: String): ModelItem? = models.firstOrNull { it.key == key }
|
||||
|
||||
// ------ string representations ------
|
||||
|
||||
/**
|
||||
@@ -435,10 +536,87 @@ data class ModelItem(
|
||||
val recommendedIndex: Double?,
|
||||
val free: Boolean,
|
||||
val variants: List<String>,
|
||||
val limit: ModelLimitItem?,
|
||||
) {
|
||||
val key: String get() = "$provider/$id"
|
||||
}
|
||||
|
||||
private fun emptyHeader() = SessionHeaderSnapshot(
|
||||
visible = false,
|
||||
title = "New Session",
|
||||
cost = null,
|
||||
context = null,
|
||||
tokens = null,
|
||||
timeline = emptyList(),
|
||||
todos = TodoSummary(0, 0, emptyList()),
|
||||
canCompact = false,
|
||||
)
|
||||
|
||||
private fun TokensDto.total(): Long = listOf(input, output, reasoning, cacheRead, cacheWrite).fold(0L) { sum, value ->
|
||||
if (value <= 0) return@fold sum
|
||||
if (Long.MAX_VALUE - sum < value) return@fold Long.MAX_VALUE
|
||||
sum + value
|
||||
}
|
||||
|
||||
private fun TokensDto.stepWeight(): Int = (input.coerceIn(0L, 10L) + output.coerceIn(0L, 10L) + reasoning.coerceIn(0L, 10L))
|
||||
.coerceIn(1L, 10L)
|
||||
.toInt()
|
||||
|
||||
private fun parseModelKey(value: String): Pair<String, String>? {
|
||||
val slash = value.indexOf('/')
|
||||
if (slash <= 0 || slash >= value.length - 1) return null
|
||||
return value.substring(0, slash) to value.substring(slash + 1)
|
||||
}
|
||||
|
||||
private fun Content.timelineTitle(): String = when (this) {
|
||||
is Text -> "Text"
|
||||
is Reasoning -> "Reasoning"
|
||||
is Tool -> fileActionTitle() ?: title?.takeIf { it.isNotBlank() } ?: name
|
||||
is Compaction -> "Compaction"
|
||||
is StepFinish -> "Step finish"
|
||||
is Generic -> type
|
||||
}
|
||||
|
||||
private fun Tool.fileActionTitle(): String? {
|
||||
val verb = when (kind) {
|
||||
ToolKind.READ -> "Read"
|
||||
ToolKind.WRITE -> "Write"
|
||||
ToolKind.GENERIC -> return null
|
||||
}
|
||||
val path = listOf("filePath", "path", "file")
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
input[it]?.takeIf { value -> value.isNotBlank() }
|
||||
?: metadata[it]?.takeIf { value -> value.isNotBlank() }
|
||||
}
|
||||
.firstOrNull()
|
||||
?: return null
|
||||
return "$verb ${tail(path).ifBlank { path }}"
|
||||
}
|
||||
|
||||
private fun tail(path: String): String {
|
||||
val value = path.trimEnd('/', '\\')
|
||||
val index = maxOf(value.lastIndexOf('/'), value.lastIndexOf('\\'))
|
||||
if (index < 0) return value
|
||||
return value.substring(index + 1)
|
||||
}
|
||||
|
||||
private fun Content.weight(): Int = when (this) {
|
||||
is Text -> content.length / 200 + 1
|
||||
is Reasoning -> content.length / 200 + 1
|
||||
is Tool -> listOf(input.size, output?.length?.div(400) ?: 0, error?.length?.div(200) ?: 0).sum() + 1
|
||||
is Compaction -> 2
|
||||
is StepFinish -> tokens?.stepWeight() ?: 1
|
||||
is Generic -> 1
|
||||
}
|
||||
|
||||
private fun ai.kilocode.rpc.dto.PartTimeDto.durationMs(): Long? {
|
||||
val start = start ?: return null
|
||||
val end = end ?: return null
|
||||
if (end < start) return null
|
||||
return ((end - start) * 1000).toLong()
|
||||
}
|
||||
|
||||
private fun renderMessage(msg: Message): List<String> {
|
||||
val out = mutableListOf<String>()
|
||||
out.add("${msg.info.role}#${msg.info.id}")
|
||||
@@ -454,6 +632,7 @@ private fun renderMessage(msg: Message): List<String> {
|
||||
}
|
||||
is Tool -> out.add(renderTool(part))
|
||||
is Compaction -> out.add("compaction#${part.id}")
|
||||
is StepFinish -> out.add("step-finish#${part.id}")
|
||||
is Generic -> out.add("${part.type}#${part.id}")
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
package ai.kilocode.client.session.model
|
||||
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,12 @@ sealed class SessionModelEvent {
|
||||
data class TodosUpdated(val todos: List<TodoDto>) : SessionModelEvent() {
|
||||
override fun toString() = "TodosUpdated count=${todos.size}"
|
||||
}
|
||||
data class SessionUpdated(val session: SessionDto) : SessionModelEvent() {
|
||||
override fun toString() = "SessionUpdated ${session.id}"
|
||||
}
|
||||
data class HeaderUpdated(val header: SessionHeaderSnapshot) : SessionModelEvent() {
|
||||
override fun toString() = "HeaderUpdated visible=${header.visible}"
|
||||
}
|
||||
data class Compacted(val count: Int) : SessionModelEvent() {
|
||||
override fun toString() = "Compacted count=$count"
|
||||
}
|
||||
|
||||
+2
@@ -87,6 +87,8 @@ class SessionMessageListPanel(
|
||||
is SessionModelEvent.StateChanged,
|
||||
is SessionModelEvent.DiffUpdated,
|
||||
is SessionModelEvent.TodosUpdated,
|
||||
is SessionModelEvent.SessionUpdated,
|
||||
is SessionModelEvent.HeaderUpdated,
|
||||
is SessionModelEvent.Compacted -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import com.intellij.openapi.editor.colors.EditorColorsScheme
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Font
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -13,6 +14,8 @@ data class SessionStyle(
|
||||
val editorScheme: EditorColorsScheme,
|
||||
val editorFamily: String,
|
||||
val editorSize: Int,
|
||||
val editorForeground: Color,
|
||||
val editorBackground: Color,
|
||||
val transcriptFont: Font,
|
||||
val smallEditorFont: Font,
|
||||
val boldEditorFont: Font,
|
||||
@@ -44,6 +47,8 @@ data class SessionStyle(
|
||||
editorScheme = scheme,
|
||||
editorFamily = family,
|
||||
editorSize = size,
|
||||
editorForeground = scheme.defaultForeground,
|
||||
editorBackground = scheme.defaultBackground,
|
||||
transcriptFont = Font(family, Font.PLAIN, size),
|
||||
smallEditorFont = Font(family, Font.PLAIN, small),
|
||||
boldEditorFont = Font(family, Font.BOLD, size),
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package ai.kilocode.client.session.ui.header
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.ContextUsage
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.inline(), 0)) {
|
||||
private val used = JBLabel()
|
||||
private val limit = JBLabel()
|
||||
private val meter = Meter()
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
add(used, BorderLayout.WEST)
|
||||
add(meter, BorderLayout.CENTER)
|
||||
add(limit, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
fun setUsage(value: ContextUsage?) {
|
||||
val data = data(value)
|
||||
meter.data = data
|
||||
isVisible = data != null
|
||||
used.text = data?.used?.let(::num).orEmpty()
|
||||
limit.text = data?.limit?.let(::num).orEmpty()
|
||||
toolTipText = data?.tip()
|
||||
meter.toolTipText = toolTipText
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun applyStyle(style: SessionStyle) {
|
||||
background = style.editorBackground
|
||||
foreground = style.editorForeground
|
||||
meter.background = style.editorBackground
|
||||
used.font = style.smallUiFont
|
||||
used.foreground = style.editorForeground
|
||||
limit.font = style.smallUiFont
|
||||
limit.foreground = style.editorForeground
|
||||
}
|
||||
|
||||
fun foregrounds() = listOf(used.foreground, limit.foreground)
|
||||
|
||||
fun used(): Long? = meter.data?.used
|
||||
|
||||
fun reserved(): Long? = meter.data?.reserved
|
||||
|
||||
fun available(): Long? = meter.data?.available
|
||||
|
||||
fun limit(): Long? = meter.data?.limit
|
||||
|
||||
fun trackColor(): Color = meter.trackColor()
|
||||
|
||||
fun usedColor(): Color = meter.data?.let(meter::usedColor) ?: meter.usedColor()
|
||||
|
||||
fun reservedColor(): Color = meter.reservedColor()
|
||||
|
||||
private fun data(value: ContextUsage?): ContextData? {
|
||||
val ctx = value ?: return null
|
||||
val max = ctx.limit?.takeIf { it > 0 } ?: return null
|
||||
if (ctx.tokens <= 0) return null
|
||||
val used = ctx.tokens.coerceAtMost(max)
|
||||
val output = ctx.output?.takeIf { it > 0 } ?: 0L
|
||||
val reserved = output.coerceAtMost(max - used)
|
||||
val available = (max - used - reserved).coerceAtLeast(0)
|
||||
return ContextData(used, reserved, available, max, output)
|
||||
}
|
||||
|
||||
override fun getMaximumSize(): Dimension = Dimension(Int.MAX_VALUE, preferredSize.height)
|
||||
}
|
||||
|
||||
private data class ContextData(
|
||||
val used: Long,
|
||||
val reserved: Long,
|
||||
val available: Long,
|
||||
val limit: Long,
|
||||
val output: Long,
|
||||
) {
|
||||
fun tip(): String {
|
||||
val lines = mutableListOf(KiloBundle.message("session.header.context.used", num(used), num(limit)))
|
||||
if (output > 0) lines.add(KiloBundle.message("session.header.context.reserved", num(output)))
|
||||
if (available > 0) lines.add(KiloBundle.message("session.header.context.available", num(available)))
|
||||
return lines.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private class Meter : JComponent() {
|
||||
var data: ContextData? = null
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
preferredSize = JBUI.size(80, 4)
|
||||
minimumSize = JBUI.size(24, 4)
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val data = data ?: return
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val h = JBUI.scale(4).coerceAtMost(height).coerceAtLeast(1)
|
||||
val y = (height - h) / 2
|
||||
val arc = JBUI.scale(4)
|
||||
g2.color = trackColor()
|
||||
g2.fillRoundRect(0, y, width, h, arc, arc)
|
||||
val used = segment(data.used, data.limit)
|
||||
val reserved = segment(data.reserved, data.limit)
|
||||
fill(g2, 0, y, used, h, usedColor(data), arc)
|
||||
fill(g2, used, y, reserved, h, reservedColor(), arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun segment(value: Long, limit: Long): Int {
|
||||
if (value <= 0 || limit <= 0 || width <= 0) return 0
|
||||
return ((value.toDouble() / limit.toDouble()) * width).toInt().coerceIn(0, width)
|
||||
}
|
||||
|
||||
private fun fill(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, color: Color, arc: Int) {
|
||||
if (w <= 0) return
|
||||
g.color = color
|
||||
g.fillRoundRect(x, y, w, h, arc, arc)
|
||||
}
|
||||
|
||||
fun trackColor(): Color = shade(0.14f)
|
||||
|
||||
fun usedColor(): Color = shade(0.45f)
|
||||
|
||||
fun usedColor(data: ContextData): Color = usedColor()
|
||||
|
||||
fun reservedColor(): Color = shade(0.28f)
|
||||
|
||||
private fun shade(alpha: Float): Color {
|
||||
val base = background ?: UiStyle.Colors.panel()
|
||||
val grey = if (UiStyle.Colors.bright(base)) Color.BLACK else Color.WHITE
|
||||
return UiStyle.Colors.blend(base, grey, alpha)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package ai.kilocode.client.session.ui.header
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.ContextUsage
|
||||
import com.intellij.ui.components.JBLabel
|
||||
|
||||
internal fun set(label: JBLabel, value: String?) {
|
||||
val text = value.orEmpty()
|
||||
if (label.text != text) label.text = text
|
||||
val show = text.isNotEmpty()
|
||||
if (label.isVisible != show) label.isVisible = show
|
||||
}
|
||||
|
||||
internal fun money(value: Double?): String? {
|
||||
val cost = value ?: return null
|
||||
if (cost < 0.01) return "\$%.4f".format(cost)
|
||||
if (cost < 1.0) return "\$%.2f".format(cost)
|
||||
return "\$%.2f".format(cost)
|
||||
}
|
||||
|
||||
internal fun contextText(value: ContextUsage?): String? {
|
||||
val ctx = value ?: return null
|
||||
val pct = ctx.percentage
|
||||
if (pct != null) return "$pct%"
|
||||
if (ctx.tokens > 0) return num(ctx.tokens)
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun contextTip(value: ContextUsage?): String? {
|
||||
val ctx = value ?: return null
|
||||
val pct = ctx.percentage
|
||||
if (pct != null) return KiloBundle.message("session.header.context.tooltip.percent", num(ctx.tokens), pct)
|
||||
if (ctx.tokens > 0) return KiloBundle.message("session.header.context.tooltip.tokens", num(ctx.tokens))
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun todo(done: Int, total: Int): String? {
|
||||
if (total <= 0) return null
|
||||
if (done >= total) return KiloBundle.message("session.header.todos.done", total)
|
||||
return KiloBundle.message("session.header.todos.progress", done, total)
|
||||
}
|
||||
|
||||
internal fun num(value: Long): String {
|
||||
val abs = kotlin.math.abs(value)
|
||||
if (abs < 1_000) return value.toString()
|
||||
if (abs < 1_000_000) return "%.1fK".format(value / 1_000.0)
|
||||
return "%.1fM".format(value / 1_000_000.0)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package ai.kilocode.client.session.ui.header
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.geom.AffineTransform
|
||||
import javax.swing.Icon
|
||||
|
||||
internal class RotatedIcon(private val base: Icon) : Icon {
|
||||
override fun getIconWidth(): Int = base.iconWidth
|
||||
|
||||
override fun getIconHeight(): Int = base.iconHeight
|
||||
|
||||
override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
val tx = AffineTransform()
|
||||
tx.translate((x + iconWidth / 2.0), (y + iconHeight / 2.0))
|
||||
tx.rotate(Math.PI)
|
||||
tx.translate((-iconWidth / 2.0), (-iconHeight / 2.0))
|
||||
g2.transform(tx)
|
||||
base.paintIcon(c, g2, 0, 0)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
package ai.kilocode.client.session.ui.header
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.SessionHeaderSnapshot
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionAdapter
|
||||
import java.awt.event.MouseWheelEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JViewport
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class SessionHeaderPanel(
|
||||
private val controller: SessionController,
|
||||
parent: Disposable,
|
||||
) : BorderLayoutPanel(), SessionStyleTarget {
|
||||
|
||||
companion object {
|
||||
private val COMPRESS_ICON: Icon = IconLoader.getIcon("/icons/compress.svg", SessionHeaderPanel::class.java)
|
||||
private val CHEVRON_ICON: Icon = IconLoader.getIcon("/icons/chevron-down.svg", SessionHeaderPanel::class.java)
|
||||
private val CHEVRON_UP_ICON: Icon = RotatedIcon(CHEVRON_ICON)
|
||||
private val UP_ICON: Icon = IconLoader.getIcon("/icons/arrow-up.svg", SessionHeaderPanel::class.java)
|
||||
private val DOWN_ICON: Icon = IconLoader.getIcon("/icons/arrow-down-to-line.svg", SessionHeaderPanel::class.java)
|
||||
private const val TOUCH_BEGIN = 2
|
||||
private const val TOUCH_UPDATE = 3
|
||||
private const val TOUCH_END = 4
|
||||
internal const val EXPANDED_KEY = "kilo.session.header.expanded"
|
||||
}
|
||||
|
||||
private val title = JBLabel()
|
||||
private val cost = JBLabel()
|
||||
private val context = JBLabel()
|
||||
private val todos = JBLabel()
|
||||
private val compact = UiStyle.Buttons.HoverIcon().apply {
|
||||
icon = COMPRESS_ICON
|
||||
toolTipText = KiloBundle.message("session.header.compact.description")
|
||||
accessibleContext.accessibleName = KiloBundle.message("session.header.compact")
|
||||
addActionListener { controller.compact() }
|
||||
}
|
||||
private val expand = UiStyle.Buttons.HoverIcon().apply {
|
||||
icon = CHEVRON_ICON
|
||||
toolTipText = KiloBundle.message("session.header.expand")
|
||||
accessibleContext.accessibleName = KiloBundle.message("session.header.expand")
|
||||
addActionListener { toggle() }
|
||||
}
|
||||
private val timeline = TimelinePanel()
|
||||
private val viewport = JViewport().apply {
|
||||
isOpaque = false
|
||||
view = timeline
|
||||
}
|
||||
private var press: Point? = null
|
||||
private var origin = Point()
|
||||
private var rest = 0.0
|
||||
private val bar = ContextBar()
|
||||
private val tokenTitle = JBLabel(KiloBundle.message("session.header.tokens"))
|
||||
private val input = JBLabel().apply {
|
||||
icon = UP_ICON
|
||||
iconTextGap = UiStyle.Gap.xs()
|
||||
}
|
||||
private val output = JBLabel().apply {
|
||||
icon = DOWN_ICON
|
||||
iconTextGap = UiStyle.Gap.xs()
|
||||
}
|
||||
private val cacheRead = JBLabel().apply {
|
||||
icon = DOWN_ICON
|
||||
iconTextGap = UiStyle.Gap.xs()
|
||||
}
|
||||
private val cacheWrite = JBLabel().apply {
|
||||
icon = UP_ICON
|
||||
iconTextGap = UiStyle.Gap.xs()
|
||||
}
|
||||
private val top = BorderLayoutPanel()
|
||||
private val right = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.inline(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(cost)
|
||||
add(context)
|
||||
add(compact)
|
||||
add(expand)
|
||||
}
|
||||
private val tokens = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
add(tokenTitle)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.inline()))
|
||||
add(input)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(output)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(cacheRead)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(cacheWrite)
|
||||
}
|
||||
private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.inline(), 0)).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
add(todos)
|
||||
}
|
||||
private val body = JPanel().apply {
|
||||
isOpaque = false
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
add(viewport)
|
||||
add(tokens)
|
||||
add(bar)
|
||||
add(todoRow)
|
||||
}
|
||||
private var style = SessionStyle.current()
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
updateUI()
|
||||
|
||||
top.add(title, BorderLayout.CENTER)
|
||||
top.add(right, BorderLayout.EAST)
|
||||
add(top, BorderLayout.NORTH)
|
||||
timeline.addMouseListener(object : MouseAdapter() {
|
||||
override fun mousePressed(event: MouseEvent) {
|
||||
press = event.point
|
||||
origin = viewport.viewPosition
|
||||
}
|
||||
|
||||
override fun mouseReleased(event: MouseEvent) {
|
||||
press = null
|
||||
}
|
||||
})
|
||||
timeline.addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseDragged(event: MouseEvent) {
|
||||
drag(event)
|
||||
}
|
||||
})
|
||||
timeline.addMouseWheelListener { scroll(it) }
|
||||
viewport.addMouseWheelListener { scroll(it) }
|
||||
|
||||
controller.model.addListener(parent) { event ->
|
||||
when (event) {
|
||||
is SessionModelEvent.HeaderUpdated -> update(event.header)
|
||||
|
||||
is SessionModelEvent.MessageAdded,
|
||||
is SessionModelEvent.MessageUpdated,
|
||||
is SessionModelEvent.MessageRemoved,
|
||||
is SessionModelEvent.ContentAdded,
|
||||
is SessionModelEvent.ContentUpdated,
|
||||
is SessionModelEvent.ContentRemoved,
|
||||
is SessionModelEvent.ContentDelta,
|
||||
is SessionModelEvent.StateChanged,
|
||||
is SessionModelEvent.DiffUpdated,
|
||||
is SessionModelEvent.TodosUpdated,
|
||||
is SessionModelEvent.SessionUpdated,
|
||||
is SessionModelEvent.Compacted,
|
||||
is SessionModelEvent.HistoryLoaded,
|
||||
is SessionModelEvent.Cleared,
|
||||
is SessionModelEvent.TurnAdded,
|
||||
is SessionModelEvent.TurnUpdated,
|
||||
is SessionModelEvent.TurnRemoved -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle(style)
|
||||
syncExpanded(expanded())
|
||||
update(controller.model.header)
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(JBUI.CurrentTheme.ToolWindow.borderColor(), 1, 0, 1, 0),
|
||||
JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.PAD, UiStyle.Space.SM, UiStyle.Space.PAD),
|
||||
)
|
||||
}
|
||||
|
||||
fun update(header: SessionHeaderSnapshot) {
|
||||
val before = isVisible
|
||||
title.text = header.title
|
||||
title.toolTipText = header.title
|
||||
title.accessibleContext.accessibleName = header.title
|
||||
isVisible = header.visible
|
||||
if (!header.visible) {
|
||||
syncExpanded(false)
|
||||
if (before) refresh()
|
||||
return
|
||||
}
|
||||
|
||||
syncExpanded(expanded())
|
||||
|
||||
set(cost, money(header.cost))
|
||||
set(context, contextText(header.context))
|
||||
context.toolTipText = contextTip(header.context)
|
||||
setTokens(header.tokens)
|
||||
set(todos, todo(header.todos.completed, header.todos.total))
|
||||
todoRow.isVisible = todos.isVisible
|
||||
|
||||
compact.isEnabled = header.canCompact
|
||||
val appended = timeline.setItems(header.timeline)
|
||||
sizeTimeline()
|
||||
if (viewport.isVisible != timeline.isVisible) viewport.isVisible = timeline.isVisible
|
||||
if (appended) SwingUtilities.invokeLater { endTimeline() }
|
||||
bar.setUsage(header.context)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
this.style = style
|
||||
background = style.editorBackground
|
||||
foreground = style.editorForeground
|
||||
top.background = style.editorBackground
|
||||
right.background = style.editorBackground
|
||||
tokens.background = style.editorBackground
|
||||
todoRow.background = style.editorBackground
|
||||
body.background = style.editorBackground
|
||||
viewport.background = style.editorBackground
|
||||
title.font = style.boldUiFont
|
||||
title.foreground = style.editorForeground
|
||||
cost.font = style.uiFont
|
||||
cost.foreground = style.editorForeground
|
||||
context.font = style.uiFont
|
||||
context.foreground = style.editorForeground
|
||||
todos.font = style.smallUiFont
|
||||
todos.foreground = style.editorForeground
|
||||
tokenTitle.font = style.smallUiFont
|
||||
tokenTitle.foreground = style.editorForeground
|
||||
input.font = style.smallUiFont
|
||||
input.foreground = style.editorForeground
|
||||
output.font = style.smallUiFont
|
||||
output.foreground = style.editorForeground
|
||||
cacheRead.font = style.smallUiFont
|
||||
cacheRead.foreground = style.editorForeground
|
||||
cacheWrite.font = style.smallUiFont
|
||||
cacheWrite.foreground = style.editorForeground
|
||||
bar.applyStyle(style)
|
||||
refresh()
|
||||
}
|
||||
|
||||
internal fun titleText(): String = title.text
|
||||
|
||||
internal fun costText(): String = cost.text
|
||||
|
||||
internal fun contextText(): String = context.text
|
||||
|
||||
internal fun foregrounds() = listOf(title, cost, context, todos, tokenTitle, input, output, cacheWrite, cacheRead)
|
||||
.map { it.foreground }
|
||||
|
||||
internal fun tokenText(): String = listOf(tokenTitle, input, output, cacheWrite, cacheRead)
|
||||
.filter { it.isVisible }
|
||||
.joinToString(" ") { it.text }
|
||||
|
||||
internal fun tokenTip(): String = tokens.toolTipText
|
||||
|
||||
internal fun inputTokenText(): String = input.text
|
||||
|
||||
internal fun outputTokenText(): String = output.text
|
||||
|
||||
internal fun cacheReadText(): String = cacheRead.text
|
||||
|
||||
internal fun cacheWriteText(): String = cacheWrite.text
|
||||
|
||||
internal fun todoText(): String = todos.text
|
||||
|
||||
internal fun todoVisible() = todoRow.isVisible && todos.isVisible
|
||||
|
||||
internal fun compactButton() = compact
|
||||
|
||||
internal fun expandButton() = expand
|
||||
|
||||
internal fun isExpanded() = body.parent === this
|
||||
|
||||
internal fun bodyPanel() = body
|
||||
|
||||
internal fun bodyComponents() = body.components.toList()
|
||||
|
||||
internal fun timelineViewport(): JViewport = viewport
|
||||
|
||||
internal fun tokenPanel() = tokens
|
||||
|
||||
internal fun timelinePanel(): Component = timeline
|
||||
|
||||
internal fun timelineToolTip() = timeline.toolTipText
|
||||
|
||||
internal fun timelineHover() = timeline.hovered()
|
||||
|
||||
internal fun contextBar(): Component = bar
|
||||
|
||||
internal fun contextBarVisible() = bar.isVisible
|
||||
|
||||
internal fun contextBarUsed() = bar.used()
|
||||
|
||||
internal fun contextBarReserved() = bar.reserved()
|
||||
|
||||
internal fun contextBarAvailable() = bar.available()
|
||||
|
||||
internal fun contextBarLimit() = bar.limit()
|
||||
|
||||
internal fun contextBarForegrounds() = bar.foregrounds()
|
||||
|
||||
internal fun contextBarTip() = bar.toolTipText
|
||||
|
||||
internal fun contextBarTrackColor() = bar.trackColor()
|
||||
|
||||
internal fun contextBarUsedColor() = bar.usedColor()
|
||||
|
||||
internal fun contextBarReservedColor() = bar.reservedColor()
|
||||
|
||||
internal fun timelineCount() = timeline.count()
|
||||
|
||||
internal fun timelineParts() = timeline.parts()
|
||||
|
||||
internal fun timelineActive(index: Int) = timeline.active(index)
|
||||
|
||||
internal fun timelinePreferredSize() = timeline.preferredSize
|
||||
|
||||
internal fun timelineBarHeight(index: Int) = timeline.barHeight(index)
|
||||
|
||||
internal fun timelineBarWidth() = timeline.barWidth()
|
||||
|
||||
internal fun timelineViewportPreferredSize() = viewport.preferredSize
|
||||
|
||||
internal fun expandTip() = expand.toolTipText
|
||||
|
||||
private fun setTokens(value: TokensDto?) {
|
||||
val tk = value
|
||||
val sent = tk?.input ?: 0L
|
||||
val received = (tk?.output ?: 0L) + (tk?.reasoning ?: 0L)
|
||||
val write = tk?.cacheWrite ?: 0L
|
||||
val read = tk?.cacheRead ?: 0L
|
||||
val total = listOf(sent, received, write, read).fold(0L) { sum, value ->
|
||||
if (value <= 0) return@fold sum
|
||||
if (Long.MAX_VALUE - sum < value) return@fold Long.MAX_VALUE
|
||||
sum + value
|
||||
}
|
||||
|
||||
tokenTitle.text = KiloBundle.message("session.header.tokens")
|
||||
tokens.toolTipText = KiloBundle.message("session.header.tokens.description")
|
||||
tokenTitle.isVisible = total > 0
|
||||
set(input, if (sent > 0) num(sent) else null)
|
||||
set(output, if (received > 0) num(received) else null)
|
||||
set(cacheWrite, if (write > 0) KiloBundle.message("session.header.cache.write", num(write)) else null)
|
||||
set(cacheRead, if (read > 0) KiloBundle.message("session.header.cache.read", num(read)) else null)
|
||||
tokens.isVisible = total > 0
|
||||
}
|
||||
|
||||
private fun toggle() {
|
||||
val next = !isExpanded()
|
||||
syncExpanded(next)
|
||||
PropertiesComponent.getInstance().setValue(EXPANDED_KEY, next.toString())
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun syncExpanded(expanded: Boolean) {
|
||||
if (expanded) expand() else collapse()
|
||||
}
|
||||
|
||||
private fun expand(): Boolean {
|
||||
if (isExpanded()) return false
|
||||
add(body, BorderLayout.CENTER)
|
||||
setExpand(true)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun collapse(): Boolean {
|
||||
val attached = body.parent === this
|
||||
if (!attached) return false
|
||||
remove(body)
|
||||
setExpand(false)
|
||||
return attached
|
||||
}
|
||||
|
||||
private fun setExpand(expanded: Boolean) {
|
||||
val key = if (expanded) "session.header.collapse" else "session.header.expand"
|
||||
expand.icon = if (expanded) CHEVRON_UP_ICON else CHEVRON_ICON
|
||||
expand.toolTipText = KiloBundle.message(key)
|
||||
expand.accessibleContext.accessibleName = KiloBundle.message(key)
|
||||
}
|
||||
|
||||
private fun expanded() = PropertiesComponent.getInstance().getBoolean(EXPANDED_KEY, true)
|
||||
|
||||
private fun sizeTimeline() {
|
||||
val size = timeline.preferredSize
|
||||
viewport.preferredSize = Dimension(0, size.height)
|
||||
viewport.minimumSize = viewport.preferredSize
|
||||
viewport.maximumSize = Dimension(Int.MAX_VALUE, viewport.preferredSize.height)
|
||||
}
|
||||
|
||||
private fun drag(event: MouseEvent) {
|
||||
val start = press ?: return
|
||||
val max = limit()
|
||||
val x = (origin.x - (event.x - start.x)).coerceIn(0, max)
|
||||
move(x)
|
||||
}
|
||||
|
||||
private fun endTimeline() {
|
||||
move(limit())
|
||||
}
|
||||
|
||||
private fun scroll(event: MouseWheelEvent) {
|
||||
if (event.scrollType == TOUCH_BEGIN || event.scrollType == TOUCH_END) {
|
||||
rest = 0.0
|
||||
event.consume()
|
||||
return
|
||||
}
|
||||
val delta = delta(event)
|
||||
if (delta == 0.0) return
|
||||
rest += delta
|
||||
val x = rest.toInt()
|
||||
if (x == 0) return
|
||||
if (!move(viewport.viewPosition.x + x)) {
|
||||
rest = 0.0
|
||||
return
|
||||
}
|
||||
rest -= x
|
||||
event.consume()
|
||||
}
|
||||
|
||||
private fun delta(event: MouseWheelEvent): Double {
|
||||
if (!event.preciseWheelRotation.isFinite()) return 0.0
|
||||
if (event.scrollType == TOUCH_UPDATE) return event.preciseWheelRotation * event.scrollAmount
|
||||
if (event.scrollType == MouseWheelEvent.WHEEL_BLOCK_SCROLL) return event.preciseWheelRotation * viewport.extentSize.width
|
||||
return event.preciseWheelRotation * event.scrollAmount * timeline.barWidth()
|
||||
}
|
||||
|
||||
private fun move(x: Int): Boolean {
|
||||
val next = x.coerceIn(0, limit())
|
||||
if (viewport.viewPosition.x == next) return false
|
||||
viewport.setViewPosition(Point(next, 0))
|
||||
return true
|
||||
}
|
||||
|
||||
private fun limit(): Int {
|
||||
val wide = timeline.preferredSize.width.coerceAtLeast(timeline.width)
|
||||
return (wide - viewport.extentSize.width).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package ai.kilocode.client.session.ui.header
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Compaction
|
||||
import ai.kilocode.client.session.model.Reasoning
|
||||
import ai.kilocode.client.session.model.StepFinish
|
||||
import ai.kilocode.client.session.model.Text
|
||||
import ai.kilocode.client.session.model.TimelineItem
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.ToolKind
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionAdapter
|
||||
import kotlin.math.roundToInt
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class TimelinePanel : JPanel() {
|
||||
companion object {
|
||||
private const val WIDTH = 12
|
||||
private const val MIN = 8
|
||||
private const val PAD = 4
|
||||
private const val GAP = 2
|
||||
}
|
||||
|
||||
private var items: List<TimelineItem> = emptyList()
|
||||
private var heights: List<Int> = emptyList()
|
||||
private var hover = -1
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseMoved(event: MouseEvent) {
|
||||
val idx = index(event)
|
||||
toolTipText = items.getOrNull(idx)?.title
|
||||
if (hover == idx) return
|
||||
hover = idx
|
||||
repaint()
|
||||
}
|
||||
})
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseExited(event: MouseEvent) {
|
||||
toolTipText = null
|
||||
if (hover == -1) return
|
||||
hover = -1
|
||||
repaint()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setItems(items: List<TimelineItem>): Boolean {
|
||||
val appended = items.size > this.items.size
|
||||
val max = items.maxOfOrNull { it.weight }?.coerceAtLeast(1) ?: 1
|
||||
this.items = items
|
||||
heights = items.map { height(it.weight, max) }
|
||||
val show = items.isNotEmpty()
|
||||
if (isVisible != show) isVisible = show
|
||||
revalidate()
|
||||
repaint()
|
||||
return appended
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
super.paintComponent(g)
|
||||
val g2 = g.create() as Graphics2D
|
||||
val w = JBUI.scale(WIDTH)
|
||||
val gap = JBUI.scale(GAP)
|
||||
val grow = JBUI.scale(1)
|
||||
val tall = height.takeIf { it > 0 } ?: preferredSize.height
|
||||
try {
|
||||
for (idx in items.indices) {
|
||||
val over = idx == hover
|
||||
val h = heights[idx] + if (over) grow else 0
|
||||
val wide = w + if (over) grow * 2 else 0
|
||||
val x = grow + idx * (w + gap) - if (over) grow else 0
|
||||
val y = (tall - h).coerceAtLeast(0)
|
||||
g2.color = color(items[idx])
|
||||
g2.fillRect(x, y, wide, h)
|
||||
}
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun index(event: MouseEvent): Int {
|
||||
val w = JBUI.scale(WIDTH)
|
||||
val gap = JBUI.scale(GAP)
|
||||
val grow = JBUI.scale(1)
|
||||
val tall = height.takeIf { it > 0 } ?: preferredSize.height
|
||||
for (idx in items.indices) {
|
||||
val h = heights[idx]
|
||||
val x = grow + idx * (w + gap)
|
||||
val y = (tall - h).coerceAtLeast(0)
|
||||
val inside = event.x >= x && event.x < x + w && event.y >= y && event.y < y + h
|
||||
if (inside) return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension = Dimension(width(), height())
|
||||
|
||||
override fun getMinimumSize(): Dimension = Dimension(0, height())
|
||||
|
||||
override fun getMaximumSize(): Dimension = Dimension(Int.MAX_VALUE, height())
|
||||
|
||||
fun count() = items.size
|
||||
|
||||
fun parts(): List<Content> = items.map { it.part }
|
||||
|
||||
fun active(index: Int) = items[index].active
|
||||
|
||||
fun barHeight(index: Int) = heights[index]
|
||||
|
||||
fun barWidth() = JBUI.scale(WIDTH + GAP)
|
||||
|
||||
fun hovered() = hover
|
||||
|
||||
private fun height(weight: Int, max: Int): Int {
|
||||
val fill = MIN + (weight.toDouble() / max.toDouble()) * (MIN * 3 - MIN - PAD)
|
||||
return JBUI.scale(fill.roundToInt())
|
||||
}
|
||||
|
||||
private fun height(): Int {
|
||||
val max = heights.maxOrNull() ?: return 0
|
||||
return max + JBUI.scale(PAD)
|
||||
}
|
||||
|
||||
private fun width(): Int {
|
||||
if (items.isEmpty()) return 0
|
||||
return items.size * JBUI.scale(WIDTH) + (items.size - 1) * JBUI.scale(GAP) + JBUI.scale(2)
|
||||
}
|
||||
|
||||
private fun color(item: TimelineItem): Color {
|
||||
val part = item.part
|
||||
if (part is Tool && part.state == ToolExecState.ERROR) return UiStyle.Colors.timelineError
|
||||
if (part is Text) return UiStyle.Colors.timelineText
|
||||
if (part is Reasoning) return UiStyle.Colors.timelineText
|
||||
if (part is Compaction) return UiStyle.Colors.timelineStep
|
||||
if (part is StepFinish) return UiStyle.Colors.timelineSuccess
|
||||
if (part !is Tool) return UiStyle.Colors.timelineStep
|
||||
if (part.kind == ToolKind.READ) return UiStyle.Colors.timelineRead
|
||||
if (part.kind == ToolKind.WRITE) return UiStyle.Colors.timelineWrite
|
||||
return UiStyle.Colors.timelineTool
|
||||
}
|
||||
}
|
||||
+39
-1
@@ -5,6 +5,7 @@ import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.AgentItem
|
||||
import ai.kilocode.client.session.model.ModelLimitItem
|
||||
import ai.kilocode.client.session.model.ModelItem
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
@@ -68,6 +69,7 @@ class SessionController(
|
||||
private val flushMs: Long = EVENT_FLUSH_MS,
|
||||
private val condense: Boolean = true,
|
||||
private val displayMs: Long = DISPLAY_DELAY_MS,
|
||||
session: SessionDto? = null,
|
||||
private val open: (SessionDto) -> Unit = {},
|
||||
private val beforeUpdate: () -> Boolean = { false },
|
||||
private val afterUpdate: (Boolean) -> Unit = {},
|
||||
@@ -87,7 +89,8 @@ class SessionController(
|
||||
val model = SessionModel()
|
||||
|
||||
private val listeners = mutableListOf<SessionControllerListener>()
|
||||
private var sessionId: String? = id
|
||||
private var sessionId: String? = session?.id ?: id
|
||||
private val initialSession = session
|
||||
private val directory: String get() = workspace.directory
|
||||
private val updates = SessionUpdateQueue(
|
||||
parent,
|
||||
@@ -151,6 +154,9 @@ class SessionController(
|
||||
val session = sessions.create(directory)
|
||||
runEdt {
|
||||
sessionId = session.id
|
||||
updateModel {
|
||||
model.setSession(session)
|
||||
}
|
||||
}
|
||||
val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session"
|
||||
LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true")
|
||||
@@ -185,6 +191,29 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
fun compact() {
|
||||
assertEdt()
|
||||
val id = sessionId ?: return
|
||||
if (model.state.isBusy()) return
|
||||
if (model.isEmpty()) return
|
||||
val parsed = model.model?.let(::parseModel) ?: return
|
||||
val sel = ModelSelectionDto(parsed.first, parsed.second)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=compact model=${sel.providerID}/${sel.modelID}" }
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.compact(id, directory, sel)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=compact ok=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(id)} kind=compact dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
updateModel {
|
||||
model.setState(SessionState.Error(e.message ?: KiloBundle.message("session.error.compact")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retryConnection() {
|
||||
assertEdt()
|
||||
LOG.debug {
|
||||
@@ -296,6 +325,7 @@ class SessionController(
|
||||
}
|
||||
|
||||
init {
|
||||
initialSession?.let { model.setSession(it) }
|
||||
if (sessionId != null) {
|
||||
loadHistory()
|
||||
subscribeEvents()
|
||||
@@ -356,6 +386,7 @@ class SessionController(
|
||||
info.recommendedIndex,
|
||||
info.free,
|
||||
info.variants,
|
||||
info.limit?.let { ModelLimitItem(it.context, it.input, it.output) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -365,6 +396,7 @@ class SessionController(
|
||||
this@SessionController.model.agent = state.agents?.default
|
||||
}
|
||||
syncModelSelection()
|
||||
model.refreshHeader()
|
||||
}
|
||||
|
||||
if (state.status == KiloWorkspaceStatusDto.READY) {
|
||||
@@ -388,11 +420,13 @@ class SessionController(
|
||||
if (!model.showSession) setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress)
|
||||
}
|
||||
try {
|
||||
val session = initialSession ?: runCatching { sessions.get(id, directory) }.getOrNull()
|
||||
val items = sessions.messages(id, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(items)}" }
|
||||
runEdt {
|
||||
updateModel {
|
||||
this@SessionController.model.loadHistory(items)
|
||||
if (session != null) this@SessionController.model.setSession(session)
|
||||
}
|
||||
}
|
||||
recoverPending(id)
|
||||
@@ -604,6 +638,8 @@ class SessionController(
|
||||
model.setState(state)
|
||||
}
|
||||
|
||||
is ChatEventDto.SessionUpdated -> model.setSession(event.session)
|
||||
|
||||
is ChatEventDto.SessionIdle -> {
|
||||
// Treat session.idle as an explicit signal to return to Idle.
|
||||
// Only apply if we're not in a more specific non-terminal state.
|
||||
@@ -685,6 +721,7 @@ class SessionController(
|
||||
model.variants = item?.variants ?: emptyList()
|
||||
val saved = key?.let { app.models.value.variant[it] }
|
||||
model.variant = saved?.takeIf { it in model.variants } ?: model.variants.firstOrNull()
|
||||
model.refreshHeader()
|
||||
}
|
||||
|
||||
private fun item(key: String): ModelItem? = model.models.firstOrNull { it.key == key }
|
||||
@@ -943,6 +980,7 @@ private fun matchesSession(event: ChatEventDto, id: String): Boolean = when (eve
|
||||
is ChatEventDto.QuestionReplied -> event.sessionID == id
|
||||
is ChatEventDto.QuestionRejected -> event.sessionID == id
|
||||
is ChatEventDto.SessionStatusChanged -> event.sessionID == id
|
||||
is ChatEventDto.SessionUpdated -> event.sessionID == id
|
||||
is ChatEventDto.SessionIdle -> event.sessionID == id
|
||||
is ChatEventDto.SessionCompacted -> event.sessionID == id
|
||||
is ChatEventDto.SessionDiffChanged -> event.sessionID == id
|
||||
|
||||
+3
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Message
|
||||
import ai.kilocode.client.session.model.StepFinish
|
||||
import ai.kilocode.client.session.ui.SessionView
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
@@ -42,6 +43,7 @@ class MessageView(
|
||||
|
||||
// Populate content that already exists (e.g. after loadHistory)
|
||||
for ((_, content) in msg.parts) {
|
||||
if (content is StepFinish) continue
|
||||
val view = ViewFactory.create(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
@@ -51,6 +53,7 @@ class MessageView(
|
||||
|
||||
/** Add or update the renderer for [content]. */
|
||||
fun upsertPart(content: Content) {
|
||||
if (content is StepFinish) return
|
||||
val existing = parts[content.id]
|
||||
if (existing != null) {
|
||||
existing.update(content)
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import ai.kilocode.client.session.model.Compaction
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Generic
|
||||
import ai.kilocode.client.session.model.Reasoning
|
||||
import ai.kilocode.client.session.model.StepFinish
|
||||
import ai.kilocode.client.session.model.Text
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
|
||||
@@ -21,6 +22,7 @@ object ViewFactory {
|
||||
is Reasoning -> ReasoningView(content)
|
||||
is Tool -> ToolView(content)
|
||||
is Compaction -> CompactionView(content)
|
||||
is StepFinish -> error("step-finish is timeline-only")
|
||||
is Generic -> GenericView(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ object UiStyle {
|
||||
internal const val BORDER_DELTA = 64
|
||||
internal const val HOVER_ALPHA = 0.35f
|
||||
|
||||
val timelineRead: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff))
|
||||
val timelineWrite: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4))
|
||||
val timelineTool: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc))
|
||||
val timelineSuccess: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground())
|
||||
val timelineError: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71))
|
||||
val timelineText: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d))
|
||||
val timelineStep: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d))
|
||||
|
||||
fun bg(): Color = UIUtil.getPanelBackground()
|
||||
|
||||
fun fg(): Color = UIUtil.getLabelForeground()
|
||||
@@ -69,6 +77,8 @@ object UiStyle {
|
||||
|
||||
fun header(): Color = panel()
|
||||
|
||||
fun headerBar(): Color = JBUI.CurrentTheme.ToolWindow.headerBackground(false)
|
||||
|
||||
/** Local hover color for collapsible transcript card headers. */
|
||||
fun headerHover(): Color = panelHover()
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="#6C707E" stroke-width="1.25" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="#CED0D6" stroke-width="1.25" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user