mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into ionized-emmental
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the Agent Manager new-tab button visible at the end of the tab row, separated by a divider, and show all tab tooltips above the tabs.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Move Agent Manager tab creation and search controls to the left side of the tab bar with clearer toolbar separators.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Auto-approve Task subagent tool permissions when running `kilo run --auto`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/kilo-docs": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Remove `--dangerously-skip-permissions` CLI flag which did nothing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent subagents from spawning nested subagents.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains history navigation with Back support and a Show History action in recent sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a native session history panel to the JetBrains plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce delays in long sessions by compacting older tool outputs when the model request becomes too large.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Clarify that semantic search returns matching code snippets with paths, line ranges, and relevance scores.
|
||||
@@ -0,0 +1,27 @@
|
||||
# kilocode_change - new file
|
||||
name: Check Kilo generated artifacts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".gitignore"
|
||||
- ".github/workflows/check-kilo-generated-artifacts.yml"
|
||||
- ".kilo/**"
|
||||
- ".kilocode/**"
|
||||
- "script/check-kilo-generated-artifacts.ts"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check Kilo generated artifacts
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Run check
|
||||
run: bun run script/check-kilo-generated-artifacts.ts
|
||||
+15
-1
@@ -45,8 +45,22 @@ telemetry-id
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Kilo
|
||||
.kilo/.gitignore
|
||||
.kilo/package.json
|
||||
.kilo/package-lock.json
|
||||
.kilo/pnpm-lock.yaml
|
||||
.kilo/bun.lock
|
||||
.kilo/yarn.lock
|
||||
.kilo/node_modules
|
||||
.kilo/plans/*upstream-merge-report-*.md
|
||||
.kilocode/.gitignore
|
||||
.kilocode/package.json
|
||||
.kilocode/package-lock.json
|
||||
.kilocode/pnpm-lock.yaml
|
||||
.kilocode/bun.lock
|
||||
.kilocode/yarn.lock
|
||||
.kilocode/node_modules
|
||||
|
||||
# Test Artifacts
|
||||
packages/app/.artifacts/
|
||||
packages/opencode/.artifacts/
|
||||
packages/opencode/.artifacts/
|
||||
|
||||
Generated
-376
@@ -1,376 +0,0 @@
|
||||
{
|
||||
"name": ".kilo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "7.2.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/plugin": {
|
||||
"version": "7.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz",
|
||||
"integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "7.2.31",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.100",
|
||||
"@opentui/solid": ">=0.1.100"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/sdk": {
|
||||
"version": "7.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz",
|
||||
"integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "4.0.0-beta.48",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
|
||||
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-check": "^4.6.0",
|
||||
"find-my-way-ts": "^0.1.6",
|
||||
"ini": "^6.0.0",
|
||||
"kubernetes-types": "^1.30.0",
|
||||
"msgpackr": "^1.11.9",
|
||||
"multipasta": "^0.2.7",
|
||||
"toml": "^4.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
|
||||
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-my-way-ts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
|
||||
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kubernetes-types": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
|
||||
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multipasta": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
|
||||
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
|
||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-376
@@ -1,376 +0,0 @@
|
||||
{
|
||||
"name": ".kilocode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "7.2.31"
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/plugin": {
|
||||
"version": "7.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz",
|
||||
"integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "7.2.31",
|
||||
"effect": "4.0.0-beta.48",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.100",
|
||||
"@opentui/solid": ">=0.1.100"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@kilocode/sdk": {
|
||||
"version": "7.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz",
|
||||
"integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "4.0.0-beta.48",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz",
|
||||
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-check": "^4.6.0",
|
||||
"find-my-way-ts": "^0.1.6",
|
||||
"ini": "^6.0.0",
|
||||
"kubernetes-types": "^1.30.0",
|
||||
"msgpackr": "^1.11.9",
|
||||
"multipasta": "^0.2.7",
|
||||
"toml": "^4.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
|
||||
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-my-way-ts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
|
||||
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kubernetes-types": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
|
||||
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multipasta": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
|
||||
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
|
||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
|
||||
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
v1.14.33
|
||||
@@ -113,6 +113,11 @@ Our community is built on respect, inclusivity, and collaboration. Please review
|
||||
This project is licensed under the MIT License.
|
||||
You’re free to use, modify, and distribute this code, including for commercial purposes as long as you include proper attribution and license notices. See [License](/LICENSE).
|
||||
|
||||
### Where did Kilo CLI come from?
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary>Where did Kilo CLI come from?</summary>
|
||||
|
||||
Kilo CLI is a fork of [OpenCode](https://github.com/anomalyco/opencode), enhanced to work within the Kilo agentic engineering platform.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -178,7 +178,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -450,7 +450,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -475,7 +475,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -486,7 +486,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -501,7 +501,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -524,7 +524,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -615,8 +615,6 @@
|
||||
"@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",
|
||||
|
||||
+3
-7
@@ -7,13 +7,11 @@
|
||||
"packageManager": "bun@1.3.13",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"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-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
@@ -76,8 +74,6 @@
|
||||
"@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"
|
||||
@@ -149,6 +145,6 @@
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
},
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.2.49"
|
||||
version = "7.2.50"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.49/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.50/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.49/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.50/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.49/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.50/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.49/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.50/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.49/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.50/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -43,6 +43,8 @@ exclude = [
|
||||
# Consistently times out in CI
|
||||
'^https?://opncd\.ai',
|
||||
'^https?://zod\.dev/v4/changelog',
|
||||
# Example punycode domain used in homograph attack documentation — does not exist
|
||||
'^https?://xn--pitest',
|
||||
# OpenAI docs return 404 to plain GET link checks but resolve in browsers
|
||||
'^https?://platform\.openai\.com/docs/api-reference/responses/create',
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -184,7 +184,6 @@ Options:
|
||||
--variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string]
|
||||
--thinking show thinking blocks [boolean] [default: false]
|
||||
--auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false]
|
||||
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied (dangerous!) [boolean] [default: false]
|
||||
```
|
||||
|
||||
## kilo debug
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 81 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 84 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.apertis.ai/v1>
|
||||
<!-- packages/opencode/src/provider/model-cache.ts -->
|
||||
@@ -177,3 +177,9 @@
|
||||
<!-- packages/opencode/src/tool/warpgrep.ts -->
|
||||
- <https://www.rfc-editor.org/rfc/rfc8628#section-3.5>
|
||||
<!-- packages/opencode/src/plugin/github-copilot/copilot.ts -->
|
||||
- <https://x.com>
|
||||
<!-- packages/opencode/src/kilocode/util/url.ts -->
|
||||
- <https://xn--pitest-2nf.com/status>
|
||||
<!-- packages/opencode/src/kilocode/util/url.ts -->
|
||||
- <https://аpitest.com/status>
|
||||
<!-- packages/opencode/src/kilocode/util/url.ts -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -37,7 +37,7 @@ export const KILO_OPENROUTER_BASE = `${KILO_API_BASE}/api/openrouter`
|
||||
export const POLL_INTERVAL_MS = 3000
|
||||
|
||||
/** Default model for authenticated users */
|
||||
export const DEFAULT_MODEL = "kilo-auto/balanced"
|
||||
export const DEFAULT_MODEL = "kilo-auto/free"
|
||||
|
||||
/** Default model for anonymous/free usage */
|
||||
export const DEFAULT_FREE_MODEL = "kilo-auto/free"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.2.49",
|
||||
"version": "7.2.50",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -118,6 +118,20 @@ The chat session feature uses a three-layer Model / Controller / View architectu
|
||||
3. Handle the new `ChatEventDto` in `SessionController.handle()` by calling the model mutation method.
|
||||
4. Add `-> Unit` stubs for the new event in any existing exhaustive `when` blocks in view code.
|
||||
|
||||
### Editor-Dependent Session Styling
|
||||
|
||||
Session UI components that render text using editor fonts or colors must not read global editor settings directly on every paint. Instead they must:
|
||||
|
||||
1. Implement `SessionEditorStyleTarget` (`session/ui/style/SessionEditorStyle.kt`).
|
||||
2. Hold a snapshot field initialised with `SessionEditorStyle.current()`.
|
||||
3. Override `applyStyle(style: SessionEditorStyle)` and update all fonts/colors in one place without rebuilding Swing nodes.
|
||||
|
||||
`SessionUi` propagates a refreshed `SessionEditorStyle` to every registered `SessionEditorStyleTarget` child when the global editor scheme changes. New session elements that depend on editor settings must be registered through this flow, not through ad hoc `EditorColorsManager` listeners.
|
||||
|
||||
**Anti-patterns to avoid:**
|
||||
- Do not pass `SessionEditorStyle` fields through constructors or method parameters when the component can implement the interface and receive updates via `applyStyle`.
|
||||
- Do not store individual style properties (e.g. a separate `font` field copied from the style) when holding the full `SessionEditorStyle` snapshot is cleaner.
|
||||
|
||||
### Testing
|
||||
|
||||
Controller tests extend `SessionControllerTestBase` (`test/…/session/SessionControllerTestBase.kt`),
|
||||
@@ -714,6 +728,28 @@ Inspection `Plugin DevKit | Code | Undesirable class usage` highlights when you
|
||||
- Always create via `JBUI.Borders.empty(top, left, bottom, right)` and `JBUI.insets()` — DPI-aware and auto-update on zoom.
|
||||
- Use `JBUI.scale(int)` for any pixel dimension to ensure proper HiDPI scaling.
|
||||
|
||||
### UI Style Constants
|
||||
|
||||
Style tokens and layout constants are split across three files. Put new constants in the right place — do not scatter them into ad hoc fields or pass them through constructors.
|
||||
|
||||
**`UiStyle`** (`frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt`) — generic Swing style tokens that are not tied to any single session component:
|
||||
- `UiStyle.Gap` — DPI-aware spacing primitives (`xs`, `sm`, `md`, `lg`, `pad`). Use these for borders, gaps, and insets anywhere in the plugin.
|
||||
- `UiStyle.Colors` — theme-aware generic colors (`bg`, `fg`, `weak`, `editorBackground`, `errorLabelForeground`, `warningLabelForeground`).
|
||||
- `UiStyle.Components` — small reusable Swing helpers (e.g. `transparent()`).
|
||||
|
||||
**`SessionUiStyle`** (`frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt`) — static tokens that are specific to the chat/session UI:
|
||||
- `SessionUiStyle.SessionLayout` — transcript list geometry and scroll increments.
|
||||
- `SessionUiStyle.View` — card sizing, card borders, surfaces, hover colors, and nested objects for `Prompt`, `Reasoning`, `Message`, and `Tool`.
|
||||
- `SessionUiStyle.RecentSessions` — recent sessions list limits.
|
||||
- `SessionUiStyle.Timeline` — activity-indicator colors for the session header timeline.
|
||||
- `Dock` — border presets for question, permission, and connection dock panels.
|
||||
|
||||
**Rules for adding new constants:**
|
||||
- Generic layout constants (gaps, generic colors, reusable helpers) → `UiStyle`.
|
||||
- Session-specific constants (transcript layout, prompt chrome, card geometry, session colors) → `SessionUiStyle`.
|
||||
- Do not create extra fields whose only purpose is to hold a constant value. Reference the constant object directly, e.g. `UiStyle.Gap.lg()` or `SessionUiStyle.View.Prompt.EDITOR_LINES`.
|
||||
- Do not pass style constants through constructors or method parameters. Using the constant directly at the call site is correct and preferred. Only introduce parameters for values that are genuinely variable or test-controlled.
|
||||
|
||||
### Icons
|
||||
|
||||
- **Reuse platform icons**: browse at https://intellij-icons.jetbrains.design. Access via `AllIcons.*` constants.
|
||||
|
||||
+65
-4
@@ -6,6 +6,7 @@ import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.jetbrains.api.client.DefaultApi
|
||||
import ai.kilocode.jetbrains.api.model.GlobalSession
|
||||
import ai.kilocode.jetbrains.api.model.SessionStatus
|
||||
import ai.kilocode.rpc.dto.CloudSessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
@@ -155,6 +156,51 @@ class KiloBackendSessionManager(
|
||||
directories.remove(id)
|
||||
}
|
||||
|
||||
fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto {
|
||||
val h = http ?: throw IllegalStateException("Session manager not started")
|
||||
val url = base ?: throw IllegalStateException("Session manager not started")
|
||||
val params = listOfNotNull(
|
||||
"directory=${encode(dir)}",
|
||||
cursor?.let { "cursor=${encode(it)}" },
|
||||
"limit=$limit",
|
||||
gitUrl?.let { "gitUrl=${encode(it)}" },
|
||||
).joinToString("&")
|
||||
val path = "$url/kilo/cloud-sessions?$params"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(path)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
h.newCall(request).execute().use { response ->
|
||||
val raw = response.body?.string().orEmpty()
|
||||
if (!response.isSuccessful) {
|
||||
log.warn("Cloud sessions failed: HTTP ${response.code}, body=$raw")
|
||||
throw RuntimeException("Cloud sessions failed: HTTP ${response.code} — $raw")
|
||||
}
|
||||
return KiloCliDataParser.parseCloudSessions(raw)
|
||||
}
|
||||
}
|
||||
|
||||
fun importCloudSession(id: String, dir: String): SessionDto {
|
||||
val h = http ?: throw IllegalStateException("Session manager not started")
|
||||
val url = base ?: throw IllegalStateException("Session manager not started")
|
||||
val json = """{"sessionId":"${escape(id)}"}"""
|
||||
val request = Request.Builder()
|
||||
.url("$url/kilo/cloud/session/import?directory=${encode(dir)}")
|
||||
.post(json.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
h.newCall(request).execute().use { response ->
|
||||
val raw = response.body?.string().orEmpty()
|
||||
if (!response.isSuccessful) {
|
||||
log.warn("Cloud session import failed: HTTP ${response.code}, body=$raw")
|
||||
throw RuntimeException("Cloud session import failed: HTTP ${response.code} — $raw")
|
||||
}
|
||||
return KiloCliDataParser.parseSession(raw)
|
||||
}
|
||||
}
|
||||
|
||||
fun seed(dir: String) {
|
||||
try {
|
||||
val raw = requireClient().sessionStatus(directory = dir)
|
||||
@@ -186,8 +232,8 @@ class KiloBackendSessionManager(
|
||||
title = s.title,
|
||||
version = s.version,
|
||||
time = SessionTimeDto(
|
||||
created = s.time.created,
|
||||
updated = s.time.updated,
|
||||
created = s.time.created.toDouble(),
|
||||
updated = s.time.updated.toDouble(),
|
||||
archived = s.time.archived,
|
||||
),
|
||||
summary = s.summary?.let {
|
||||
@@ -207,8 +253,8 @@ class KiloBackendSessionManager(
|
||||
title = s.title,
|
||||
version = s.version,
|
||||
time = SessionTimeDto(
|
||||
created = s.time.created,
|
||||
updated = s.time.updated,
|
||||
created = s.time.created.toDouble(),
|
||||
updated = s.time.updated.toDouble(),
|
||||
archived = s.time.archived,
|
||||
),
|
||||
summary = s.summary?.let {
|
||||
@@ -227,4 +273,19 @@ class KiloBackendSessionManager(
|
||||
next = s.next.toLong(),
|
||||
requestID = s.requestID.ifBlank { null },
|
||||
)
|
||||
|
||||
private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8)
|
||||
|
||||
private fun escape(value: String) = buildString {
|
||||
for (c in value) {
|
||||
when (c) {
|
||||
'\\' -> append("\\\\")
|
||||
'"' -> append("\\\"")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> append("\\r")
|
||||
'\t' -> append("\\t")
|
||||
else -> if (c < '\u0020') append("\\u%04x".format(c.code)) else append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -1,6 +1,8 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionListDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
@@ -265,6 +267,25 @@ object KiloCliDataParser {
|
||||
}
|
||||
}
|
||||
|
||||
fun parseCloudSessions(raw: String): CloudSessionListDto {
|
||||
val obj = tryParseObject(raw) ?: return CloudSessionListDto(emptyList())
|
||||
val items = obj["cliSessions"]?.jsonArray ?: JsonArray(emptyList())
|
||||
val sessions = items.mapNotNull { elem ->
|
||||
val item = runCatching { elem.jsonObject }.getOrNull() ?: return@mapNotNull null
|
||||
CloudSessionDto(
|
||||
id = item.str("session_id") ?: return@mapNotNull null,
|
||||
title = item.str("title"),
|
||||
createdAt = item.str("created_at") ?: return@mapNotNull null,
|
||||
updatedAt = item.str("updated_at") ?: return@mapNotNull null,
|
||||
version = item.num("version") ?: return@mapNotNull null,
|
||||
)
|
||||
}
|
||||
return CloudSessionListDto(
|
||||
sessions = sessions,
|
||||
nextCursor = obj.str("nextCursor"),
|
||||
)
|
||||
}
|
||||
|
||||
fun parseModelState(raw: String): ModelStateDto {
|
||||
val obj = tryParseObject(raw) ?: return ModelStateDto()
|
||||
return ModelStateDto(
|
||||
|
||||
+7
@@ -9,6 +9,7 @@ import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.rpc.KiloSessionRpcApi
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionListDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
@@ -70,6 +71,12 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
|
||||
workspaces.get(dir).deleteSession(id)
|
||||
}
|
||||
|
||||
override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto =
|
||||
sessions.cloudSessions(directory, cursor, limit, gitUrl)
|
||||
|
||||
override suspend fun importCloudSession(id: String, directory: String): SessionDto =
|
||||
sessions.importCloudSession(id, directory)
|
||||
|
||||
override suspend fun statuses(): Flow<Map<String, SessionStatusDto>> =
|
||||
sessions.statuses
|
||||
|
||||
|
||||
+98
@@ -185,6 +185,104 @@ class KiloBackendSessionManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloudSessions returns cloud sessions from server`() = runBlocking {
|
||||
mock.cloudSessions = """{
|
||||
"cliSessions": [
|
||||
{"session_id":"cloud_1","title":"Cloud One","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z","version":2}
|
||||
],
|
||||
"nextCursor": "next_1"
|
||||
}"""
|
||||
val app = setup()
|
||||
ready(app)
|
||||
|
||||
val result = app.sessions.cloudSessions("/repo", null, 50, null)
|
||||
|
||||
assertEquals(1, result.sessions.size)
|
||||
assertEquals("cloud_1", result.sessions[0].id)
|
||||
assertEquals("Cloud One", result.sessions[0].title)
|
||||
assertEquals("next_1", result.nextCursor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloudSessions passes filters and cursor`() = runBlocking {
|
||||
val app = setup()
|
||||
ready(app)
|
||||
|
||||
app.sessions.cloudSessions("/repo path", "cur 1", 25, "git@github.com:Kilo-Org/kilo.git")
|
||||
|
||||
val path = mock.lastCloudSessionsPath ?: error("missing cloud sessions request")
|
||||
assertTrue(path.startsWith("/kilo/cloud-sessions?"))
|
||||
val decoded = URLDecoder.decode(path, "UTF-8")
|
||||
assertTrue(decoded.contains("directory=/repo path"), path)
|
||||
assertTrue(decoded.contains("cursor=cur 1"), path)
|
||||
assertTrue(decoded.contains("limit=25"), path)
|
||||
assertTrue(decoded.contains("gitUrl=git@github.com:Kilo-Org/kilo.git"), path)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloudSessions throws when not started`() = runBlocking {
|
||||
val app = setup()
|
||||
|
||||
assertFailsWith<IllegalStateException> {
|
||||
app.sessions.cloudSessions("/test", null, 50, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloudSessions surfaces server failure`() = runBlocking {
|
||||
mock.cloudSessionsStatus = 500
|
||||
mock.cloudSessions = """{"error":"boom"}"""
|
||||
val app = setup()
|
||||
ready(app)
|
||||
|
||||
val err = assertFailsWith<RuntimeException> {
|
||||
app.sessions.cloudSessions("/test", null, 50, null)
|
||||
}
|
||||
|
||||
assertTrue(err.message.orEmpty().contains("HTTP 500"))
|
||||
assertTrue(err.message.orEmpty().contains("boom"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importCloudSession posts cloud id and returns local session`() = runBlocking {
|
||||
mock.cloudSessionImport = """{
|
||||
"id":"ses_imported",
|
||||
"slug":"imported",
|
||||
"projectID":"prj_test",
|
||||
"directory":"/repo",
|
||||
"title":"Imported Cloud",
|
||||
"version":"1.0.0",
|
||||
"time":{"created":1000,"updated":2000}
|
||||
}"""
|
||||
val app = setup()
|
||||
ready(app)
|
||||
|
||||
val session = app.sessions.importCloudSession("cloud_1", "/repo path")
|
||||
|
||||
assertEquals("ses_imported", session.id)
|
||||
assertEquals("Imported Cloud", session.title)
|
||||
val path = mock.lastCloudSessionImportPath ?: error("missing cloud import request")
|
||||
assertTrue(path.startsWith("/kilo/cloud/session/import?"))
|
||||
assertTrue(URLDecoder.decode(path, "UTF-8").contains("directory=/repo path"), path)
|
||||
assertEquals("""{"sessionId":"cloud_1"}""", mock.lastCloudSessionImportBody)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importCloudSession surfaces server failure`() = runBlocking {
|
||||
mock.cloudSessionImportStatus = 500
|
||||
mock.cloudSessionImport = """{"error":"boom"}"""
|
||||
val app = setup()
|
||||
ready(app)
|
||||
|
||||
val err = assertFailsWith<RuntimeException> {
|
||||
app.sessions.importCloudSession("cloud_1", "/test")
|
||||
}
|
||||
|
||||
assertTrue(err.message.orEmpty().contains("HTTP 500"))
|
||||
assertTrue(err.message.orEmpty().contains("boom"))
|
||||
}
|
||||
|
||||
// ------ Session create ------
|
||||
|
||||
@Test
|
||||
|
||||
+27
@@ -398,6 +398,33 @@ class KiloCliDataParserTest {
|
||||
assertNull(result.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseCloudSessions maps cloud session list`() {
|
||||
val raw = """{
|
||||
"cliSessions": [
|
||||
{"session_id":"cloud_1","title":"Cloud One","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z","version":2},
|
||||
{"session_id":"cloud_2","title":null,"created_at":"2026-01-03T00:00:00Z","updated_at":"2026-01-04T00:00:00Z","version":3.5,"extra":true}
|
||||
],
|
||||
"nextCursor": "cursor_2"
|
||||
}"""
|
||||
|
||||
val result = KiloCliDataParser.parseCloudSessions(raw)
|
||||
|
||||
assertEquals(2, result.sessions.size)
|
||||
assertEquals("cloud_1", result.sessions[0].id)
|
||||
assertEquals("Cloud One", result.sessions[0].title)
|
||||
assertEquals("2026-01-02T00:00:00Z", result.sessions[0].updatedAt)
|
||||
assertEquals(2.0, result.sessions[0].version)
|
||||
assertNull(result.sessions[1].title)
|
||||
assertEquals("cursor_2", result.nextCursor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseCloudSessions tolerates malformed response`() {
|
||||
assertEquals(emptyList(), KiloCliDataParser.parseCloudSessions("not json").sessions)
|
||||
assertNull(KiloCliDataParser.parseCloudSessions("{}").nextCursor)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// parseMessages
|
||||
// ================================================================
|
||||
|
||||
+16
@@ -62,6 +62,13 @@ class MockCliServer : AutoCloseable {
|
||||
@Volatile var sessionGetStatus = 200
|
||||
@Volatile var sessionDeleteStatus = 200
|
||||
@Volatile var sessionStatusesStatus = 200
|
||||
@Volatile var cloudSessions = """{"cliSessions":[],"nextCursor":null}"""
|
||||
@Volatile var cloudSessionImport = """{"id":"ses_imported","slug":"imported","projectID":"prj_test","directory":"/test","title":"Imported Session","version":"1.0.0","time":{"created":1000,"updated":1000}}"""
|
||||
@Volatile var cloudSessionsStatus = 200
|
||||
@Volatile var cloudSessionImportStatus = 200
|
||||
@Volatile var lastCloudSessionsPath: String? = null
|
||||
@Volatile var lastCloudSessionImportPath: String? = null
|
||||
@Volatile var lastCloudSessionImportBody: String? = null
|
||||
@Volatile var summarizeStatus = 200
|
||||
@Volatile var lastSummarizePath: String? = null
|
||||
@Volatile var lastSummarizeBody: String? = null
|
||||
@@ -220,6 +227,15 @@ class MockCliServer : AutoCloseable {
|
||||
lastExperimentalSessionPath = path
|
||||
respond(output, recentSessionsStatus, recentSessions)
|
||||
}
|
||||
bare == "/kilo/cloud-sessions" -> {
|
||||
lastCloudSessionsPath = path
|
||||
respond(output, cloudSessionsStatus, cloudSessions)
|
||||
}
|
||||
bare == "/kilo/cloud/session/import" && method == "POST" -> {
|
||||
lastCloudSessionImportPath = path
|
||||
lastCloudSessionImportBody = body
|
||||
respond(output, cloudSessionImportStatus, cloudSessionImport)
|
||||
}
|
||||
bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses)
|
||||
bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions)
|
||||
bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate)
|
||||
|
||||
@@ -66,6 +66,12 @@ intellijPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
runIdeBackend {
|
||||
splitModeServerPort.set(12345)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<JavaExec>("runIde") {
|
||||
dependsOn(":backend:processResources")
|
||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client
|
||||
|
||||
import ai.kilocode.client.actions.HistoryAction
|
||||
import ai.kilocode.client.actions.NewSessionAction
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
@@ -65,7 +66,7 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||
manager.newSession()
|
||||
|
||||
ActionManager.getInstance().getAction("Kilo.Settings")?.let { settings ->
|
||||
toolWindow.setTitleActions(listOf(NewSessionAction(), settings))
|
||||
toolWindow.setTitleActions(listOf(NewSessionAction(), HistoryAction(), settings))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to set up Kilo tool window content", e)
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionManager
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
|
||||
class HistoryAction : AnAction(
|
||||
KiloBundle.message("action.Kilo.History.text"),
|
||||
KiloBundle.message("action.Kilo.History.description"),
|
||||
AllIcons.Vcs.History,
|
||||
), DumbAware {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
e.getData(SessionManager.KEY)?.showHistory()
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabled = e.getData(SessionManager.KEY) != null
|
||||
}
|
||||
}
|
||||
+22
-5
@@ -5,6 +5,7 @@ package ai.kilocode.client.app
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.rpc.KiloSessionRpcApi
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionListDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
@@ -15,6 +16,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import com.intellij.openapi.components.Service
|
||||
import ai.kilocode.log.KiloLog
|
||||
@@ -34,7 +36,7 @@ import kotlinx.coroutines.launch
|
||||
* Project-level frontend service for session management.
|
||||
*
|
||||
* Stateless with respect to "active session" — callers pass explicit
|
||||
* session IDs. [ai.kilocode.client.session.update.SessionController] owns the
|
||||
* session IDs. [ai.kilocode.client.session.controller.SessionController] owns the
|
||||
* active session concept.
|
||||
*/
|
||||
@Service(Service.Level.PROJECT)
|
||||
@@ -76,14 +78,19 @@ class KiloSessionService internal constructor(
|
||||
fun refresh(dir: String) {
|
||||
cs.launch {
|
||||
try {
|
||||
val result = call { list(dir) }
|
||||
_sessions.value = result.sessions
|
||||
list(dir)
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("kind=session-list dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun list(dir: String): SessionListDto {
|
||||
val result = call { list(dir) }
|
||||
_sessions.value = result.sessions
|
||||
return result
|
||||
}
|
||||
|
||||
/** Load recent sessions for the current worktree family. */
|
||||
suspend fun recent(dir: String, limit: Int): List<SessionDto> =
|
||||
call { recent(dir, limit) }.sessions
|
||||
@@ -105,14 +112,24 @@ class KiloSessionService internal constructor(
|
||||
fun delete(id: String, dir: String) {
|
||||
cs.launch {
|
||||
try {
|
||||
call { delete(id, dir) }
|
||||
refresh(dir)
|
||||
deleteSession(id, dir)
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(id)} kind=session delete=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteSession(id: String, dir: String) {
|
||||
call { delete(id, dir) }
|
||||
list(dir)
|
||||
}
|
||||
|
||||
suspend fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto =
|
||||
call { cloudSessions(dir, cursor, limit, gitUrl) }
|
||||
|
||||
suspend fun importCloudSession(id: String, dir: String): SessionDto =
|
||||
call { importCloudSession(id, dir) }
|
||||
|
||||
/** Register a worktree directory override for a session. */
|
||||
fun setDirectory(id: String, dir: String) {
|
||||
cs.launch {
|
||||
|
||||
+7
-1
@@ -10,5 +10,11 @@ interface SessionManager {
|
||||
|
||||
fun newSession()
|
||||
|
||||
fun openSession(session: SessionDto)
|
||||
fun showHistory()
|
||||
|
||||
fun openSession(ref: SessionRef)
|
||||
|
||||
fun openSession(session: SessionDto) {
|
||||
openSession(SessionRef.Local(session))
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package ai.kilocode.client.session
|
||||
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
|
||||
sealed interface SessionRef {
|
||||
enum class Type { LOCAL, CLOUD }
|
||||
|
||||
val type: Type
|
||||
val id: String
|
||||
val key: String
|
||||
|
||||
data class Local(override val id: String, val session: SessionDto? = null) : SessionRef {
|
||||
constructor(session: SessionDto) : this(session.id, session)
|
||||
|
||||
override val type: Type get() = Type.LOCAL
|
||||
override val key: String get() = id
|
||||
}
|
||||
|
||||
data class Cloud(override val id: String, val session: CloudSessionDto? = null) : SessionRef {
|
||||
constructor(session: CloudSessionDto) : this(session.id, session)
|
||||
|
||||
override val type: Type get() = Type.CLOUD
|
||||
override val key: String get() = "$CLOUD_PREFIX$id"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CLOUD_PREFIX = "cloud:"
|
||||
|
||||
fun from(id: String?): SessionRef? {
|
||||
val value = id?.takeIf { it.isNotBlank() } ?: return null
|
||||
if (value.startsWith(CLOUD_PREFIX)) return Cloud(value.removePrefix(CLOUD_PREFIX))
|
||||
return Local(value)
|
||||
}
|
||||
|
||||
fun cloud(id: String?): Cloud? {
|
||||
return from(id) as? Cloud
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -3,8 +3,8 @@ package ai.kilocode.client.session
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.SessionMessageListPanel
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
@@ -50,7 +50,7 @@ internal class SessionScroll(
|
||||
internal val jump: JBLabel
|
||||
val view: JComponent? get() = component.viewport.view as? JComponent
|
||||
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var tail = true
|
||||
private var auto = false
|
||||
private var opening = false
|
||||
@@ -71,7 +71,7 @@ internal class SessionScroll(
|
||||
component.verticalScrollBar.addAdjustmentListener { onScroll() }
|
||||
root.addOverlay(jump) { _, child ->
|
||||
val size = child.preferredSize
|
||||
val gap = JBUI.scale(UiStyle.Space.PAD)
|
||||
val gap = UiStyle.Gap.pad()
|
||||
Rectangle(
|
||||
host.x + host.width - size.width - gap,
|
||||
host.y + host.height - size.height - gap,
|
||||
@@ -83,7 +83,7 @@ internal class SessionScroll(
|
||||
|
||||
fun show(panel: JPanel) {
|
||||
if (component.viewport.view === panel) return
|
||||
(panel as? SessionStyleTarget)?.applyStyle(style)
|
||||
(panel as? SessionEditorStyleTarget)?.applyStyle(style)
|
||||
component.viewport.setView(panel)
|
||||
component.repaint()
|
||||
updateJump()
|
||||
@@ -128,12 +128,12 @@ internal class SessionScroll(
|
||||
updateJump()
|
||||
}
|
||||
|
||||
fun applyStyle(style: SessionStyle) {
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
jump.icon = patchedIcon(ICON)
|
||||
messages.applyStyle(style)
|
||||
val view = component.viewport.view
|
||||
if (view !== messages) (view as? SessionStyleTarget)?.applyStyle(style)
|
||||
if (view !== messages) (view as? SessionEditorStyleTarget)?.applyStyle(style)
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
||||
+101
-13
@@ -1,13 +1,19 @@
|
||||
package ai.kilocode.client.session
|
||||
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.client.session.history.HistoryController
|
||||
import ai.kilocode.client.session.history.HistoryPanel
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.DataProvider
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import kotlinx.coroutines.cancel
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
@@ -15,10 +21,11 @@ import javax.swing.JPanel
|
||||
class SessionSidePanelManager(
|
||||
private val project: Project,
|
||||
private val root: Workspace,
|
||||
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 create: (Project, Workspace, SessionManager, SessionRef?) -> SessionUi = { project, workspace, manager, ref ->
|
||||
service<SessionUiFactory>().create(project, workspace, manager, ref)
|
||||
},
|
||||
private val resolve: (String) -> Workspace = { dir -> service<KiloWorkspaceService>().workspace(dir) },
|
||||
private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null,
|
||||
) : SessionManager, Disposable {
|
||||
val component: JPanel = object : JPanel(BorderLayout()), DataProvider {
|
||||
override fun getData(dataId: String): Any? {
|
||||
@@ -30,28 +37,108 @@ class SessionSidePanelManager(
|
||||
private val opened = mutableMapOf<String, SessionUi>()
|
||||
private val all = mutableSetOf<SessionUi>()
|
||||
private var current: SessionUi? = null
|
||||
private var latest: SessionUi? = null
|
||||
private var panel: JComponent? = null
|
||||
|
||||
val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent
|
||||
val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent ?: (panel as? HistoryPanel)?.defaultFocusedComponent
|
||||
|
||||
override fun newSession() {
|
||||
val active = current
|
||||
if (active?.blank == true) return
|
||||
register(active)
|
||||
show(create(project, root, this, null, active == null, null))
|
||||
show(create(project, root, this, null))
|
||||
}
|
||||
|
||||
override fun openSession(session: SessionDto) {
|
||||
override fun openSession(ref: SessionRef) {
|
||||
register(current)
|
||||
val ui = opened.getOrPut(session.id) {
|
||||
create(project, resolve(session.directory), this, session.id, false, session).also {
|
||||
all.add(it)
|
||||
}
|
||||
val ui = opened[ref.key] ?: run {
|
||||
val local = (ref as? SessionRef.Local)?.session?.id
|
||||
val existing = local?.let { opened[it] }
|
||||
if (existing != null) {
|
||||
opened[ref.key] = existing
|
||||
existing
|
||||
} else create(ref)
|
||||
}
|
||||
show(ui)
|
||||
}
|
||||
|
||||
private fun create(ref: SessionRef): SessionUi {
|
||||
val workspace = when (ref) {
|
||||
is SessionRef.Local -> ref.session?.directory?.let(resolve) ?: root
|
||||
is SessionRef.Cloud -> root
|
||||
}
|
||||
return create(project, workspace, this, ref).also {
|
||||
all.add(it)
|
||||
opened[ref.key] = it
|
||||
val local = (ref as? SessionRef.Local)?.session?.id
|
||||
if (local != null) opened.putIfAbsent(local, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showHistory() {
|
||||
register(current)
|
||||
release(current)
|
||||
val cached = panel
|
||||
val view = cached ?: createHistory().also { panel = it }
|
||||
if (cached != null && view is HistoryPanel) view.refresh()
|
||||
if (current == null && component.componentCount == 1 && component.getComponent(0) === view) {
|
||||
focusHistory(view)
|
||||
return
|
||||
}
|
||||
current = null
|
||||
component.removeAll()
|
||||
component.add(view, BorderLayout.CENTER)
|
||||
component.revalidate()
|
||||
component.repaint()
|
||||
focusHistory(view)
|
||||
}
|
||||
|
||||
private fun focusHistory(view: JComponent) {
|
||||
val focus = (view as? HistoryPanel)?.defaultFocusedComponent ?: return
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
IdeFocusManager.getInstance(project).requestFocusInProject(focus, project)
|
||||
}, ModalityState.defaultModalityState())
|
||||
}
|
||||
|
||||
private fun createHistory(): JComponent {
|
||||
val custom = history
|
||||
if (custom != null) return custom(this, this::openSession, this::removeSession)
|
||||
val factory = service<SessionUiFactory>()
|
||||
val cs = factory.scope()
|
||||
val controller = HistoryController(
|
||||
sessions = project.service<KiloSessionService>(),
|
||||
workspace = root,
|
||||
cs = cs,
|
||||
open = this::openSession,
|
||||
deleted = this::removeSession,
|
||||
)
|
||||
Disposer.register(this) { cs.cancel() }
|
||||
return HistoryPanel(this, controller, nav = this::back).component
|
||||
}
|
||||
|
||||
private fun back() {
|
||||
val ui = latest
|
||||
if (ui != null && ui in all) {
|
||||
show(ui)
|
||||
return
|
||||
}
|
||||
latest = null
|
||||
newSession()
|
||||
}
|
||||
|
||||
private fun removeSession(id: String) {
|
||||
val ui = opened.remove(id) ?: return
|
||||
opened.entries.removeIf { it.value === ui }
|
||||
all.remove(ui)
|
||||
if (current === ui) current = null
|
||||
if (latest === ui) latest = null
|
||||
Disposer.dispose(ui)
|
||||
}
|
||||
|
||||
private fun show(ui: SessionUi) {
|
||||
all.add(ui)
|
||||
register(ui)
|
||||
latest = ui
|
||||
if (current === ui) return
|
||||
release(current)
|
||||
component.removeAll()
|
||||
@@ -62,13 +149,13 @@ class SessionSidePanelManager(
|
||||
}
|
||||
|
||||
private fun register(ui: SessionUi?) {
|
||||
val id = ui?.id ?: return
|
||||
opened.putIfAbsent(id, ui)
|
||||
val key = ui?.cacheKey ?: return
|
||||
opened.putIfAbsent(key, ui)
|
||||
}
|
||||
|
||||
private fun release(ui: SessionUi?) {
|
||||
if (ui == null) return
|
||||
if (ui.id != null) {
|
||||
if (ui.cacheKey != null) {
|
||||
register(ui)
|
||||
return
|
||||
}
|
||||
@@ -81,6 +168,7 @@ class SessionSidePanelManager(
|
||||
opened.clear()
|
||||
all.clear()
|
||||
current = null
|
||||
latest = null
|
||||
component.removeAll()
|
||||
items.forEach { Disposer.dispose(it) }
|
||||
}
|
||||
|
||||
+35
-70
@@ -3,11 +3,11 @@ 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.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.ui.ConnectionPanel
|
||||
import ai.kilocode.client.session.ui.EmptySessionPanel
|
||||
import ai.kilocode.client.session.ui.LoadingPanel
|
||||
import ai.kilocode.client.session.ui.ReasoningPicker
|
||||
import ai.kilocode.client.session.ui.mode.ModePicker
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
@@ -17,12 +17,11 @@ 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
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.update.SessionControllerEvent
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.controller.EVENT_FLUSH_MS
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.session.controller.SessionControllerEvent
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
@@ -32,8 +31,6 @@ import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.Centerizer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.BoxLayout
|
||||
@@ -47,43 +44,16 @@ import javax.swing.JPanel
|
||||
* It builds the session panels, wires controller/model listeners, and swaps the
|
||||
* center body between the empty state and the message list.
|
||||
*/
|
||||
class SessionUi private constructor(
|
||||
class SessionUi(
|
||||
project: Project,
|
||||
workspace: Workspace,
|
||||
sessions: KiloSessionService,
|
||||
app: KiloAppService,
|
||||
cs: CoroutineScope,
|
||||
id: String?,
|
||||
displayMs: Long,
|
||||
open: (SessionDto) -> Unit,
|
||||
private val loading: Boolean,
|
||||
session: SessionDto? = null,
|
||||
) : JPanel(BorderLayout()), Disposable, SessionStyleTarget {
|
||||
|
||||
constructor(
|
||||
project: Project,
|
||||
workspace: Workspace,
|
||||
sessions: KiloSessionService,
|
||||
app: KiloAppService,
|
||||
cs: CoroutineScope,
|
||||
id: String? = null,
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
session: SessionDto? = null,
|
||||
) : this(project, workspace, sessions, app, cs, session?.id ?: id, displayMs, open, id == null, session)
|
||||
|
||||
internal constructor(
|
||||
project: Project,
|
||||
workspace: Workspace,
|
||||
sessions: KiloSessionService,
|
||||
app: KiloAppService,
|
||||
cs: CoroutineScope,
|
||||
id: String? = null,
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
loading: Boolean,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
session: SessionDto? = null,
|
||||
) : this(project, workspace, sessions, app, cs, id, displayMs, open, loading, session)
|
||||
ref: SessionRef? = null,
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
private val manager: SessionManager? = null,
|
||||
) : JPanel(BorderLayout()), Disposable, SessionEditorStyleTarget {
|
||||
|
||||
companion object {
|
||||
private val LOG = KiloLog.create(SessionUi::class.java)
|
||||
@@ -91,7 +61,7 @@ class SessionUi private constructor(
|
||||
|
||||
private val project = project
|
||||
private val app = app
|
||||
private var opening = id != null
|
||||
private var opening = ref != null
|
||||
private var pending = false
|
||||
private var loaded: Boolean? = null
|
||||
private val flushMs =
|
||||
@@ -101,15 +71,14 @@ class SessionUi private constructor(
|
||||
?: EVENT_FLUSH_MS
|
||||
|
||||
private val controller = SessionController(
|
||||
this, id, sessions, workspace, app, cs, this,
|
||||
this, ref, sessions, workspace, app, cs, comp = this,
|
||||
flushMs = flushMs,
|
||||
condense = Registry.`is`("kilo.session.condense", true),
|
||||
displayMs = displayMs,
|
||||
session = session,
|
||||
open = open,
|
||||
open = { item -> manager?.openSession(item) },
|
||||
beforeUpdate = { if (opening) false else scroll.atBottom() },
|
||||
afterUpdate = { if (!opening) scroll.followBottom(it) },
|
||||
loaded = ::onHistoryLoaded,
|
||||
loaded = ::onSessionLoaded,
|
||||
)
|
||||
|
||||
|
||||
@@ -132,16 +101,16 @@ class SessionUi private constructor(
|
||||
private lateinit var connection: ConnectionPanel
|
||||
|
||||
private lateinit var prompt: PromptPanel
|
||||
private lateinit var loadingLabel: JBLabel
|
||||
private var style = SessionStyle.current()
|
||||
private lateinit var load: LoadingPanel
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
buildUi()
|
||||
scroll.show(body(controller.model.state))
|
||||
bindUi()
|
||||
bindStyle()
|
||||
applyStyle(style)
|
||||
onStateChanged(controller.model.state)
|
||||
scroll.show(startBody())
|
||||
loaded?.let(::finishOpen)
|
||||
}
|
||||
|
||||
@@ -159,6 +128,8 @@ class SessionUi private constructor(
|
||||
|
||||
internal val id: String? get() = controller.id
|
||||
|
||||
internal val cacheKey: String? get() = controller.refKey
|
||||
|
||||
internal fun currentStyle() = style
|
||||
|
||||
val defaultFocusedComponent: JComponent get() = prompt.defaultFocusedComponent
|
||||
@@ -172,14 +143,8 @@ class SessionUi private constructor(
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
progressBody = JPanel(BorderLayout()).apply {
|
||||
isOpaque = false
|
||||
loadingLabel = JBLabel(KiloBundle.message("session.empty.loading"))
|
||||
add(Centerizer(
|
||||
loadingLabel,
|
||||
Centerizer.TYPE.BOTH,
|
||||
), BorderLayout.CENTER)
|
||||
}
|
||||
load = LoadingPanel()
|
||||
progressBody = load
|
||||
messageBody = SessionMessageListPanel(controller.model, this)
|
||||
header = SessionHeaderPanel(controller, this)
|
||||
|
||||
@@ -253,8 +218,8 @@ class SessionUi private constructor(
|
||||
}
|
||||
|
||||
is SessionControllerEvent.ViewChanged.ShowRecents -> {
|
||||
val panel = EmptySessionPanel(this, controller, event.recents)
|
||||
scroll.show(panel)
|
||||
val panel = EmptySessionPanel(this, controller, event.recents) { manager?.showHistory() }
|
||||
scroll.show(panel.view)
|
||||
}
|
||||
|
||||
is SessionControllerEvent.ViewChanged.ShowSession -> {
|
||||
@@ -299,28 +264,28 @@ class SessionUi private constructor(
|
||||
val bus = ApplicationManager.getApplication().messageBus.connect(this)
|
||||
bus.subscribe(EditorColorsManager.TOPIC, EditorColorsListener {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
}
|
||||
})
|
||||
bus.subscribe(LafManagerListener.TOPIC, LafManagerListener {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun startBody(): JPanel {
|
||||
if (controller.model.showSession) return messageBody
|
||||
if (loading) return progressBody
|
||||
return blankBody
|
||||
}
|
||||
|
||||
private fun onHistoryLoaded(show: Boolean) {
|
||||
private fun onSessionLoaded(show: Boolean) {
|
||||
loaded = show
|
||||
if (!this::scroll.isInitialized) return
|
||||
finishOpen(show)
|
||||
}
|
||||
|
||||
private fun body(state: SessionState): JPanel {
|
||||
if (controller.model.showSession) return messageBody
|
||||
if (state is SessionState.Loading) return progressBody
|
||||
return blankBody
|
||||
}
|
||||
|
||||
private fun finishOpen(show: Boolean) {
|
||||
loaded = show
|
||||
if (!opening) return
|
||||
@@ -380,9 +345,9 @@ class SessionUi private constructor(
|
||||
root.repaint()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
loadingLabel.font = style.uiFont
|
||||
load.applyStyle(style)
|
||||
header.applyStyle(style)
|
||||
prompt.applyStyle(style)
|
||||
scroll.applyStyle(style)
|
||||
|
||||
+4
-9
@@ -3,7 +3,6 @@ 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
|
||||
@@ -19,22 +18,18 @@ class SessionUiFactory(
|
||||
project: Project,
|
||||
workspace: Workspace,
|
||||
manager: SessionManager,
|
||||
id: String? = null,
|
||||
loading: Boolean = id == null,
|
||||
session: SessionDto? = null,
|
||||
ref: SessionRef? = null,
|
||||
): SessionUi = SessionUi(
|
||||
project = project,
|
||||
workspace = workspace,
|
||||
sessions = project.service<KiloSessionService>(),
|
||||
app = service<KiloAppService>(),
|
||||
cs = scope(),
|
||||
id = session?.id ?: id,
|
||||
loading = loading,
|
||||
session = session,
|
||||
open = manager::openSession,
|
||||
ref = ref,
|
||||
manager = manager,
|
||||
)
|
||||
|
||||
private fun scope(): CoroutineScope {
|
||||
fun scope(): CoroutineScope {
|
||||
val parent = cs.coroutineContext[Job]
|
||||
return CoroutineScope(cs.coroutineContext + SupervisorJob(parent))
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.update
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
+209
-82
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.update
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
@@ -17,6 +17,7 @@ import ai.kilocode.client.session.model.Question
|
||||
import ai.kilocode.client.session.model.QuestionItem
|
||||
import ai.kilocode.client.session.model.QuestionOption
|
||||
import ai.kilocode.client.session.model.ToolCallRef
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigWarningDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
@@ -49,7 +50,7 @@ import java.awt.Component
|
||||
/**
|
||||
* Session lifecycle orchestrator for a single session.
|
||||
*
|
||||
* Accepts an optional [id] — if non-null, loads that session immediately.
|
||||
* Accepts an optional [ref] — if non-null, loads that session immediately.
|
||||
* If null, lazily creates a session on the first [prompt] call. This ensures
|
||||
* event subscription happens before the prompt is sent, eliminating races.
|
||||
*
|
||||
@@ -60,7 +61,7 @@ import java.awt.Component
|
||||
*/
|
||||
class SessionController(
|
||||
parent: Disposable,
|
||||
id: String?,
|
||||
ref: SessionRef? = null,
|
||||
private val sessions: KiloSessionService,
|
||||
private val workspace: Workspace,
|
||||
private val app: KiloAppService,
|
||||
@@ -69,8 +70,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 open: (SessionRef) -> Unit = {},
|
||||
private val beforeUpdate: () -> Boolean = { false },
|
||||
private val afterUpdate: (Boolean) -> Unit = {},
|
||||
private val loaded: (Boolean) -> Unit = {},
|
||||
@@ -89,8 +89,8 @@ class SessionController(
|
||||
val model = SessionModel()
|
||||
|
||||
private val listeners = mutableListOf<SessionControllerListener>()
|
||||
private var sessionId: String? = session?.id ?: id
|
||||
private val initialSession = session
|
||||
private var ref: SessionRef? = ref
|
||||
private val sid: String? get() = (ref as? SessionRef.Local)?.id
|
||||
private val directory: String get() = workspace.directory
|
||||
private val updates = SessionUpdateQueue(
|
||||
parent,
|
||||
@@ -98,31 +98,41 @@ class SessionController(
|
||||
flushMs,
|
||||
::handle,
|
||||
condense,
|
||||
id != null
|
||||
) { sessionId ?: "pending" }
|
||||
ref != null
|
||||
) { sid ?: ref?.key ?: "pending" }
|
||||
|
||||
private var disposed = false
|
||||
private var partType: String? = null
|
||||
private var tool: String? = null
|
||||
private var eventJob: Job? = null
|
||||
private var historyState: HistoryState = HistoryState.Idle
|
||||
private var sessionLoadState: SessionLoadState = SessionLoadState.Idle
|
||||
private var recentsState: RecentsState = RecentsState.Idle
|
||||
private var viewState: SessionControllerEvent.ViewChanged? = null
|
||||
private var connectionState: SessionControllerEvent.ConnectionChanged? = null
|
||||
private var connectionTargetState: SessionControllerEvent.ConnectionChanged? = null
|
||||
private val delayedState = DelayedState(displayMs)
|
||||
private val connectionDelay = DelayedState(displayMs)
|
||||
|
||||
val ready: Boolean get() = model.isReady()
|
||||
internal val blank: Boolean get() = sessionId == null && model.isEmpty() && !model.showSession
|
||||
internal val id: String? get() = sessionId
|
||||
internal val blank: Boolean get() = ref == null && model.isEmpty() && !model.showSession
|
||||
internal val id: String? get() = sid
|
||||
internal val refKey: String? get() = ref?.key
|
||||
internal val refType: SessionRef.Type? get() = ref?.type
|
||||
|
||||
fun openSession(session: SessionDto) {
|
||||
assertEdt()
|
||||
open(session)
|
||||
open(SessionRef.Local(session))
|
||||
}
|
||||
|
||||
fun openSession(ref: SessionRef) {
|
||||
assertEdt()
|
||||
open(ref)
|
||||
}
|
||||
|
||||
fun addListener(parent: Disposable, listener: SessionControllerListener) {
|
||||
if (disposed) return
|
||||
listeners.add(listener)
|
||||
Disposer.register(parent) { listeners.remove(listener) }
|
||||
replay(listener)
|
||||
}
|
||||
|
||||
internal fun snapshotState(): ControllerStateSnapshot {
|
||||
@@ -132,32 +142,38 @@ class SessionController(
|
||||
viewState = viewState,
|
||||
connectionState = connectionState,
|
||||
connectionTargetState = connectionTargetState,
|
||||
historyState = historyState.toString(),
|
||||
refKey = ref?.key,
|
||||
refType = ref?.type?.name,
|
||||
sessionLoadState = sessionLoadState.toString(),
|
||||
recentsState = recentsState.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun flushEvents() {
|
||||
assertEdt()
|
||||
if (disposed) return
|
||||
updates.requestFlush(true)
|
||||
}
|
||||
|
||||
fun prompt(text: String) {
|
||||
assertEdt()
|
||||
val sid = sessionId ?: "pending"
|
||||
val start = sid ?: ref?.key ?: "pending"
|
||||
val dto = promptDto(text)
|
||||
LOG.debug { "${ChatLogSummary.sid(sid)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" }
|
||||
showMessages()
|
||||
LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" }
|
||||
showSession()
|
||||
cs.launch {
|
||||
try {
|
||||
val id = sessionId ?: run {
|
||||
val id = sid ?: run {
|
||||
val session = sessions.create(directory)
|
||||
runEdt {
|
||||
sessionId = session.id
|
||||
if (disposed) return@runEdt
|
||||
ref = SessionRef.Local(session)
|
||||
setRecentSessionsState(RecentsState.Idle)
|
||||
updateModel {
|
||||
model.setSession(session)
|
||||
}
|
||||
}
|
||||
if (disposed) return@launch
|
||||
val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session"
|
||||
LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true")
|
||||
subscribeEvents()
|
||||
@@ -166,8 +182,9 @@ class SessionController(
|
||||
sessions.prompt(id, directory, dto)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sessionId ?: sid)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
if (disposed) return@edt
|
||||
val msg = e.message ?: KiloBundle.message("session.error.prompt")
|
||||
updateModel {
|
||||
model.setState(SessionState.Error(msg))
|
||||
@@ -179,8 +196,8 @@ class SessionController(
|
||||
|
||||
fun abort() {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=abort" }
|
||||
val id = sessionId ?: return
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=abort" }
|
||||
val id = sid ?: return
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.abort(id, directory)
|
||||
@@ -193,7 +210,7 @@ class SessionController(
|
||||
|
||||
fun compact() {
|
||||
assertEdt()
|
||||
val id = sessionId ?: return
|
||||
val id = sid ?: return
|
||||
if (model.state.isBusy()) return
|
||||
if (model.isEmpty()) return
|
||||
val parsed = model.model?.let(::parseModel) ?: return
|
||||
@@ -217,7 +234,7 @@ class SessionController(
|
||||
fun retryConnection() {
|
||||
assertEdt()
|
||||
LOG.debug {
|
||||
"${ChatLogSummary.sid(sessionId ?: "pending")} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}"
|
||||
"${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}"
|
||||
}
|
||||
setConnectionTargetState(SessionControllerEvent.ConnectionChanged.ShowConnecting)
|
||||
setVisibleConnectionState(SessionControllerEvent.ConnectionChanged.ShowConnecting)
|
||||
@@ -238,12 +255,12 @@ class SessionController(
|
||||
|
||||
fun selectAgent(name: String) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config agent=$name" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config agent=$name" }
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.updateConfig(directory, ConfigUpdateDto(agent = name))
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
fire(SessionControllerEvent.WorkspaceReady) {
|
||||
@@ -254,7 +271,7 @@ class SessionController(
|
||||
|
||||
fun selectModel(provider: String, id: String) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config model=$provider/$id" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config model=$provider/$id" }
|
||||
val agent = model.agent ?: return
|
||||
val key = "$provider/$id"
|
||||
if (item(key) == null && model.workspace.providers != null) return
|
||||
@@ -266,7 +283,7 @@ class SessionController(
|
||||
fun clearModelOverride() {
|
||||
assertEdt()
|
||||
val agent = model.agent ?: return
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config model-reset agent=$agent" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config model-reset agent=$agent" }
|
||||
app.clearModel(agent)
|
||||
val auto = configModel(agent) ?: providerModel(agent)
|
||||
selectResolvedModel(auto)
|
||||
@@ -277,7 +294,7 @@ class SessionController(
|
||||
assertEdt()
|
||||
val key = model.model ?: return
|
||||
if (value !in model.variants) return
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config variant=$key/$value" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config variant=$key/$value" }
|
||||
app.selectVariant(key, value)
|
||||
model.variant = value
|
||||
}
|
||||
@@ -286,53 +303,63 @@ class SessionController(
|
||||
|
||||
fun replyPermission(requestId: String, reply: PermissionReplyDto, rules: PermissionAlwaysRulesDto? = null) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" }
|
||||
cs.launch {
|
||||
try {
|
||||
if (rules != null) sessions.savePermissionRules(requestId, directory, rules)
|
||||
sessions.replyPermission(requestId, directory, reply)
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId ok=true" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId ok=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun replyQuestion(requestId: String, answers: QuestionReplyDto) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size}" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size}" }
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.replyQuestion(requestId, directory, answers)
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId ok=true" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun rejectQuestion(requestId: String) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId rejected=true" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true" }
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.rejectQuestion(requestId, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId ok=true" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initialSession?.let { model.setSession(it) }
|
||||
if (sessionId != null) {
|
||||
loadHistory()
|
||||
subscribeEvents()
|
||||
(ref as? SessionRef.Local)?.session?.let { model.setSession(it) }
|
||||
when (val item = ref) {
|
||||
is SessionRef.Cloud -> {
|
||||
val token = SessionLoadState.Loading()
|
||||
startSessionLoading(token)
|
||||
importCloud(item.id, token)
|
||||
}
|
||||
is SessionRef.Local -> {
|
||||
val token = SessionLoadState.Loading()
|
||||
startSessionLoading(token)
|
||||
loadSession(token)
|
||||
subscribeEvents()
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
model.addListener(this) { event ->
|
||||
LOG.debug { "session=$sessionId model: $event" }
|
||||
LOG.debug { "session=${sid ?: ref?.key ?: "pending"} model: $event" }
|
||||
}
|
||||
|
||||
app.connect()
|
||||
@@ -402,28 +429,24 @@ class SessionController(
|
||||
if (state.status == KiloWorkspaceStatusDto.READY) {
|
||||
fire(SessionControllerEvent.WorkspaceReady)
|
||||
edt {
|
||||
if (sessionId == null) refreshRecents()
|
||||
if (canUseRecents()) refreshRecents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHistory() {
|
||||
val id = sessionId ?: return
|
||||
private fun loadSession(token: SessionLoadState.Loading) {
|
||||
val target = ref as? SessionRef.Local ?: return
|
||||
val id = target.id
|
||||
cs.launch {
|
||||
val state = HistoryState.Loading()
|
||||
runEdt {
|
||||
setHistoryState(state)
|
||||
}
|
||||
delayedState.run(state, { historyState }) {
|
||||
if (!model.showSession) setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress)
|
||||
}
|
||||
try {
|
||||
val session = initialSession ?: runCatching { sessions.get(id, directory) }.getOrNull()
|
||||
val session = target.session ?: runCatching { sessions.get(id, directory) }.getOrNull()
|
||||
val items = sessions.messages(id, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(items)}" }
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
if (sid != id) return@runEdt
|
||||
updateModel {
|
||||
this@SessionController.model.loadHistory(items)
|
||||
if (session != null) this@SessionController.model.setSession(session)
|
||||
@@ -431,21 +454,27 @@ class SessionController(
|
||||
}
|
||||
recoverPending(id)
|
||||
runEdt {
|
||||
val show = !model.isEmpty()
|
||||
if (show) showMessages()
|
||||
if (!show) refreshRecents(force = true)
|
||||
loaded(show)
|
||||
if (disposed) return@runEdt
|
||||
if (sid != id) return@runEdt
|
||||
showSession()
|
||||
loaded(!model.isEmpty())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(id)} kind=history dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
refreshRecents(force = true)
|
||||
if (disposed) return@edt
|
||||
if (sid != id) return@edt
|
||||
updateModel {
|
||||
model.setState(SessionState.Error(e.message ?: KiloBundle.message("history.error.local")))
|
||||
}
|
||||
showSession()
|
||||
loaded(false)
|
||||
}
|
||||
} finally {
|
||||
edt {
|
||||
if (historyState != state) return@edt
|
||||
setHistoryState(HistoryState.Idle)
|
||||
if (disposed) return@edt
|
||||
if (sessionLoadState != token) return@edt
|
||||
setSessionLoadState(SessionLoadState.Idle)
|
||||
}
|
||||
updates.holdFlush(false)
|
||||
updates.requestFlush(true)
|
||||
@@ -453,8 +482,59 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun importCloud(id: String, token: SessionLoadState.Loading) {
|
||||
cs.launch {
|
||||
try {
|
||||
val session = sessions.importCloudSession(id, directory)
|
||||
val items = sessions.messages(session.id, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(session.id)} ${ChatLogSummary.history(items)}" }
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
ref = SessionRef.Local(session)
|
||||
setRecentSessionsState(RecentsState.Idle)
|
||||
updateModel {
|
||||
this@SessionController.model.loadHistory(items)
|
||||
this@SessionController.model.setSession(session)
|
||||
}
|
||||
}
|
||||
recoverPending(session.id)
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
subscribeEvents()
|
||||
showSession()
|
||||
loaded(!model.isEmpty())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(id)} kind=cloud-import dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
if (disposed) return@edt
|
||||
updateModel {
|
||||
model.setState(SessionState.Error(e.message ?: KiloBundle.message("history.error.cloud")))
|
||||
}
|
||||
showSession()
|
||||
loaded(false)
|
||||
}
|
||||
} finally {
|
||||
edt {
|
||||
if (disposed) return@edt
|
||||
if (sessionLoadState != token) return@edt
|
||||
setSessionLoadState(SessionLoadState.Idle)
|
||||
}
|
||||
updates.holdFlush(false)
|
||||
updates.requestFlush(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSessionLoading(token: SessionLoadState.Loading) {
|
||||
assertEdt()
|
||||
setSessionLoadState(token)
|
||||
model.setState(SessionState.Loading)
|
||||
if (!model.showSession) setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress)
|
||||
}
|
||||
|
||||
private fun subscribeEvents() {
|
||||
val id = sessionId ?: return
|
||||
val id = sid ?: return
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=subscription subscribe=true" }
|
||||
eventJob?.cancel()
|
||||
eventJob = cs.launch {
|
||||
@@ -489,6 +569,8 @@ class SessionController(
|
||||
"${ChatLogSummary.sid(id)} kind=recovery permissions=${permissions.size} questions=${questions.size} status=${status?.type ?: "none"} branch=$branch"
|
||||
}
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
if (sid != id) return@runEdt
|
||||
updateModel {
|
||||
if (permissions.isNotEmpty()) {
|
||||
model.setState(SessionState.AwaitingPermission(toPermission(permissions.last())))
|
||||
@@ -511,7 +593,7 @@ class SessionController(
|
||||
* for "busy" because no more-specific state has arrived yet.
|
||||
*/
|
||||
private fun seedStatus(dto: SessionStatusDto) {
|
||||
LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} evt=session.status ${ChatLogSummary.status(dto)}" }
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} evt=session.status ${ChatLogSummary.status(dto)}" }
|
||||
val state = when (dto.type) {
|
||||
"busy" -> SessionState.Busy(KiloBundle.message("session.status.considering"))
|
||||
"retry" -> SessionState.Retry(
|
||||
@@ -533,7 +615,7 @@ class SessionController(
|
||||
when (event) {
|
||||
is ChatEventDto.MessageUpdated -> {
|
||||
val added = model.upsertMessage(event.info)
|
||||
if (added) showMessages()
|
||||
if (added) showSession()
|
||||
}
|
||||
|
||||
is ChatEventDto.PartUpdated -> {
|
||||
@@ -734,12 +816,13 @@ class SessionController(
|
||||
|
||||
private fun updateModel(block: () -> Unit) {
|
||||
assertEdt()
|
||||
if (disposed) return
|
||||
val follow = beforeUpdate()
|
||||
block()
|
||||
afterUpdate(follow)
|
||||
}
|
||||
|
||||
private fun showMessages() {
|
||||
private fun showSession() {
|
||||
assertEdt()
|
||||
if (!model.showSession) {
|
||||
setControllerViewState(SessionControllerEvent.ViewChanged.ShowSession)
|
||||
@@ -764,50 +847,65 @@ class SessionController(
|
||||
|
||||
fun refreshRecents(force: Boolean = false) {
|
||||
assertEdt()
|
||||
if (model.showSession) return
|
||||
if (!canUseRecents()) return
|
||||
if (recentsState is RecentsState.Loading) return
|
||||
if (recentsState is RecentsState.Loaded && !force) return
|
||||
val state = RecentsState.Loading()
|
||||
setRecentSessionsState(state)
|
||||
delayedState.run(state, { recentsState }) {
|
||||
setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress)
|
||||
}
|
||||
cs.launch {
|
||||
try {
|
||||
val items = sessions.recent(directory, RECENT_LIMIT)
|
||||
edt {
|
||||
if (!canUseRecents()) return@edt
|
||||
if (recentsState != state) return@edt
|
||||
setRecentSessionsState(RecentsState.Loaded)
|
||||
if (model.showSession) return@edt
|
||||
if (!canUseRecents()) return@edt
|
||||
setControllerViewState(SessionControllerEvent.ViewChanged.ShowRecents(items))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("kind=session-recent dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
if (!canUseRecents()) return@edt
|
||||
if (recentsState != state) return@edt
|
||||
setRecentSessionsState(RecentsState.Loaded)
|
||||
if (model.showSession) return@edt
|
||||
if (!canUseRecents()) return@edt
|
||||
setControllerViewState(SessionControllerEvent.ViewChanged.ShowRecents(emptyList()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canUseRecents(): Boolean {
|
||||
assertEdt()
|
||||
if (disposed) return false
|
||||
if (ref != null) return false
|
||||
if (sessionLoadState !is SessionLoadState.Idle) return false
|
||||
return !model.showSession
|
||||
}
|
||||
|
||||
private fun setControllerViewState(event: SessionControllerEvent.ViewChanged) {
|
||||
assertEdt()
|
||||
if (disposed) return
|
||||
if (event is SessionControllerEvent.ViewChanged.ShowSession) openLocal()
|
||||
if (viewState == event) return
|
||||
fire(event) {
|
||||
viewState = event
|
||||
if (event is SessionControllerEvent.ViewChanged.ShowSession) {
|
||||
model.showSession = true
|
||||
setHistoryState(HistoryState.Idle)
|
||||
setSessionLoadState(SessionLoadState.Idle)
|
||||
setRecentSessionsState(RecentsState.Idle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLocal() {
|
||||
val session = model.session ?: return
|
||||
open(SessionRef.Local(session))
|
||||
}
|
||||
|
||||
private fun setConnectionTargetState(event: SessionControllerEvent.ConnectionChanged) {
|
||||
assertEdt()
|
||||
if (disposed) return
|
||||
connectionTargetState = event
|
||||
val state = event
|
||||
if (event is SessionControllerEvent.ConnectionChanged.Hide || event is SessionControllerEvent.ConnectionChanged.ShowWarning) {
|
||||
@@ -817,11 +915,14 @@ class SessionController(
|
||||
if (connectionState == event) {
|
||||
return
|
||||
}
|
||||
delayedState.run(event, { if (connectionTargetState == state) state else resolveConnectionState() }, ::setVisibleConnectionState)
|
||||
connectionDelay.run(event, { if (connectionTargetState == state) state else resolveConnectionState() }) {
|
||||
if (!disposed) setVisibleConnectionState(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setVisibleConnectionState(event: SessionControllerEvent.ConnectionChanged) {
|
||||
assertEdt()
|
||||
if (disposed) return
|
||||
if (connectionState == event) return
|
||||
if (connectionState == null && event is SessionControllerEvent.ConnectionChanged.Hide) {
|
||||
connectionState = event
|
||||
@@ -837,9 +938,9 @@ class SessionController(
|
||||
setConnectionTargetState(resolveConnectionState())
|
||||
}
|
||||
|
||||
private fun setHistoryState(state: HistoryState) {
|
||||
private fun setSessionLoadState(state: SessionLoadState) {
|
||||
assertEdt()
|
||||
historyState = state
|
||||
sessionLoadState = state
|
||||
}
|
||||
|
||||
private fun setRecentSessionsState(state: RecentsState) {
|
||||
@@ -882,19 +983,41 @@ class SessionController(
|
||||
}
|
||||
|
||||
private fun fire(event: SessionControllerEvent, before: (() -> Unit)? = null) {
|
||||
LOG.debug { "session=$sessionId controller: $event" }
|
||||
LOG.debug { "session=${sid ?: ref?.key ?: "pending"} controller: $event" }
|
||||
val application = ApplicationManager.getApplication()
|
||||
if (application.isDispatchThread) {
|
||||
if (disposed) return
|
||||
before?.invoke()
|
||||
for (l in listeners) l.onEvent(event)
|
||||
return
|
||||
}
|
||||
application.invokeLater {
|
||||
if (disposed) return@invokeLater
|
||||
before?.invoke()
|
||||
for (l in listeners) l.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replay(listener: SessionControllerListener) {
|
||||
if (disposed) return
|
||||
val app = ApplicationManager.getApplication()
|
||||
val block: () -> Unit = {
|
||||
if (!disposed) {
|
||||
viewState?.let(listener::onEvent)
|
||||
connectionState?.let(listener::onEvent)
|
||||
}
|
||||
}
|
||||
if (app.isDispatchThread) {
|
||||
if (disposed) return
|
||||
block()
|
||||
return
|
||||
}
|
||||
app.invokeLater {
|
||||
if (disposed) return@invokeLater
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertEdt() {
|
||||
check(ApplicationManager.getApplication().isDispatchThread) { "SessionController state must be accessed on EDT" }
|
||||
}
|
||||
@@ -913,7 +1036,8 @@ class SessionController(
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
delayedState.dispose()
|
||||
disposed = true
|
||||
connectionDelay.dispose()
|
||||
eventJob?.cancel()
|
||||
cs.cancel()
|
||||
}
|
||||
@@ -940,6 +1064,7 @@ class SessionController(
|
||||
|
||||
when (val state = model.state) {
|
||||
is SessionState.Idle -> out.add("[idle]")
|
||||
is SessionState.Loading -> out.add("[loading]")
|
||||
is SessionState.Busy -> {
|
||||
out.add("[busy]")
|
||||
out.add("[${state.text.toDumpText()}]")
|
||||
@@ -1046,9 +1171,9 @@ private sealed interface RecentsState {
|
||||
data object Loaded : RecentsState
|
||||
}
|
||||
|
||||
private sealed interface HistoryState {
|
||||
data object Idle : HistoryState
|
||||
data class Loading(val id: Any = Any()) : HistoryState
|
||||
private sealed interface SessionLoadState {
|
||||
data object Idle : SessionLoadState
|
||||
data class Loading(val id: Any = Any()) : SessionLoadState
|
||||
}
|
||||
|
||||
internal data class ControllerStateSnapshot(
|
||||
@@ -1056,7 +1181,9 @@ internal data class ControllerStateSnapshot(
|
||||
val viewState: SessionControllerEvent.ViewChanged?,
|
||||
val connectionState: SessionControllerEvent.ConnectionChanged?,
|
||||
val connectionTargetState: SessionControllerEvent.ConnectionChanged?,
|
||||
val historyState: String,
|
||||
val refKey: String?,
|
||||
val refType: String?,
|
||||
val sessionLoadState: String,
|
||||
val recentsState: String,
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.update
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.update
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.update
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.log.KiloLog
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HistoryController(
|
||||
private val sessions: KiloSessionService,
|
||||
private val workspace: Workspace,
|
||||
private val cs: CoroutineScope,
|
||||
open: (SessionRef) -> Unit = {},
|
||||
private val deleted: (String) -> Unit = {},
|
||||
) {
|
||||
companion object {
|
||||
const val CLOUD_LIMIT = 150
|
||||
}
|
||||
|
||||
val local = HistoryModel<LocalHistoryItem>()
|
||||
val cloud = CloudHistoryModel()
|
||||
|
||||
private val deleting = mutableSetOf<String>()
|
||||
private val opener = open
|
||||
private var git: String? = null
|
||||
|
||||
fun reload(gitUrl: String? = null) {
|
||||
reloadLocal()
|
||||
reloadCloud(gitUrl)
|
||||
}
|
||||
|
||||
fun reloadLocal() {
|
||||
edt { local.start() }
|
||||
cs.launch {
|
||||
try {
|
||||
val result = sessions.list(workspace.directory)
|
||||
val items = result.sessions.map(::localItem)
|
||||
edt { local.replace(items) }
|
||||
} catch (e: Exception) {
|
||||
edt { local.fail(e.message ?: KiloBundle.message("history.error.local")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadCloud(gitUrl: String? = null) {
|
||||
git = gitUrl
|
||||
loadCloud(reset = true)
|
||||
}
|
||||
|
||||
fun loadMoreCloud() {
|
||||
if (cloud.cursor == null || cloud.loading) return
|
||||
loadCloud(reset = false)
|
||||
}
|
||||
|
||||
fun delete(item: LocalHistoryItem) {
|
||||
edt {
|
||||
if (item.id in deleting) return@edt
|
||||
deleting.add(item.id)
|
||||
local.refresh()
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.deleteSession(item.id, item.directory ?: workspace.directory)
|
||||
edt {
|
||||
deleting.remove(item.id)
|
||||
local.remove(item.id)
|
||||
deleted(item.id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
edt {
|
||||
deleting.remove(item.id)
|
||||
local.fail(e.message ?: KiloBundle.message("history.error.local.delete"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting
|
||||
|
||||
fun open(item: LocalHistoryItem) {
|
||||
edt { opener(SessionRef.Local(item.session)) }
|
||||
}
|
||||
|
||||
fun open(item: CloudHistoryItem) {
|
||||
edt { opener(SessionRef.Cloud(item.session)) }
|
||||
}
|
||||
|
||||
private fun loadCloud(reset: Boolean) {
|
||||
val cursor = cloud.cursor.takeUnless { reset }
|
||||
val gitUrl = git
|
||||
edt { cloud.start(reset) }
|
||||
cs.launch {
|
||||
try {
|
||||
val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, gitUrl)
|
||||
val items = result.sessions.map(::cloudItem)
|
||||
edt {
|
||||
if (reset) cloud.replace(items, result.nextCursor)
|
||||
else cloud.append(items, result.nextCursor)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
edt { cloud.fail(e.message ?: KiloBundle.message("history.error.cloud")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun edt(block: () -> Unit) {
|
||||
val app = ApplicationManager.getApplication()
|
||||
if (app.isDispatchThread) {
|
||||
block()
|
||||
return
|
||||
}
|
||||
app.invokeLater(block)
|
||||
}
|
||||
|
||||
private fun localItem(session: SessionDto) = LocalHistoryItem(session)
|
||||
|
||||
private fun cloudItem(session: CloudSessionDto) = CloudHistoryItem(session)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
|
||||
sealed interface HistoryItem {
|
||||
val id: String
|
||||
val title: String
|
||||
val createdAt: String
|
||||
val updatedAt: String
|
||||
}
|
||||
|
||||
data class LocalHistoryItem(val session: SessionDto) : HistoryItem {
|
||||
override val id: String get() = session.id
|
||||
override val title: String get() = session.title
|
||||
override val createdAt: String get() = session.time.created.toString()
|
||||
override val updatedAt: String get() = session.time.updated.toString()
|
||||
val directory: String? get() = session.directory
|
||||
}
|
||||
|
||||
data class CloudHistoryItem(val session: CloudSessionDto) : HistoryItem {
|
||||
override val id: String get() = session.id
|
||||
override val title: String get() = session.title.orEmpty()
|
||||
override val createdAt: String get() = session.createdAt
|
||||
override val updatedAt: String get() = session.updatedAt
|
||||
}
|
||||
|
||||
enum class HistorySource { LOCAL, CLOUD }
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.ExperimentalUI
|
||||
import com.intellij.ui.GroupHeaderSeparator
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
private const val DELETE_CLICK_AREA_WIDTH = 32
|
||||
|
||||
internal open class HistoryRenderer<T : HistoryItem>(
|
||||
private val model: HistoryModel<T>,
|
||||
private val deletable: Boolean,
|
||||
) : JPanel(BorderLayout()), ListCellRenderer<T> {
|
||||
companion object {
|
||||
private val icon: Icon = AllIcons.Actions.GC
|
||||
private val empty: Icon = EmptyIcon.create(icon)
|
||||
|
||||
fun isDeleteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean {
|
||||
val width = JBUI.scale(DELETE_CLICK_AREA_WIDTH)
|
||||
val inset = deleteInset(list)
|
||||
if (list.componentOrientation.isLeftToRight) {
|
||||
val right = bounds.x + bounds.width - inset
|
||||
return point.x in (right - width)..right
|
||||
}
|
||||
val left = bounds.x + inset
|
||||
return point.x in left..(left + width)
|
||||
}
|
||||
|
||||
fun section(items: List<HistoryItem>, index: Int): String? {
|
||||
val item = items.getOrNull(index) ?: return null
|
||||
val current = HistoryTime.section(item)
|
||||
val previous = items.getOrNull(index - 1)?.let(HistoryTime::section)
|
||||
if (current == previous) return null
|
||||
return HistoryTime.title(current)
|
||||
}
|
||||
|
||||
private fun deleteInset(list: JList<*>): Int {
|
||||
if (!ExperimentalUI.isNewUI()) return 0
|
||||
val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets()
|
||||
val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get()
|
||||
return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left
|
||||
}
|
||||
}
|
||||
|
||||
private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets())
|
||||
private val top = JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
add(sep, BorderLayout.NORTH)
|
||||
}
|
||||
private val title = SimpleColoredComponent()
|
||||
private val time = JBLabel()
|
||||
private val del = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
}
|
||||
private val main = JPanel(BorderLayout()).apply {
|
||||
add(title, BorderLayout.CENTER)
|
||||
add(time, BorderLayout.EAST)
|
||||
}
|
||||
private val row = JPanel(BorderLayout()).apply {
|
||||
add(main, BorderLayout.CENTER)
|
||||
add(del, BorderLayout.EAST)
|
||||
}
|
||||
private val wrap = PickerRow()
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
top.isOpaque = true
|
||||
row.border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg())
|
||||
UiStyle.Components.transparent(row, main, title, time, del)
|
||||
wrap.setContent(row)
|
||||
add(top, BorderLayout.NORTH)
|
||||
add(wrap, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<out T>,
|
||||
value: T?,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
focus: Boolean,
|
||||
): JPanel {
|
||||
val focused = selected || list.hasFocus() || focus
|
||||
val fg = UIUtil.getListForeground(selected, focused)
|
||||
val weak = if (selected) fg else UIUtil.getContextHelpForeground()
|
||||
|
||||
background = list.background
|
||||
top.background = list.background
|
||||
wrap.update(list, selected, focused)
|
||||
sep.caption = section(model.visibleItems, index)
|
||||
sep.setHideLine(index == 0)
|
||||
top.isVisible = sep.caption != null
|
||||
|
||||
title.clear()
|
||||
title.append(
|
||||
value?.let(::title).orEmpty(),
|
||||
SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg),
|
||||
)
|
||||
time.text = value?.let(HistoryTime::relative).orEmpty()
|
||||
time.foreground = weak
|
||||
del.icon = if (deletable && selected) icon else empty
|
||||
|
||||
top.invalidate()
|
||||
return this
|
||||
}
|
||||
|
||||
fun deleteVisible(): Boolean = del.icon === icon
|
||||
}
|
||||
|
||||
internal class LocalHistoryRenderer(model: HistoryModel<LocalHistoryItem>) : HistoryRenderer<LocalHistoryItem>(model, deletable = true)
|
||||
|
||||
internal class CloudHistoryRenderer(model: HistoryModel<CloudHistoryItem>) : HistoryRenderer<CloudHistoryItem>(model, deletable = false)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import com.intellij.ui.components.JBList
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
internal fun title(item: HistoryItem): String = item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled")
|
||||
|
||||
internal fun <T> clicked(list: JBList<T>, e: MouseEvent): T? {
|
||||
val row = list.locationToIndex(e.point)
|
||||
val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null
|
||||
if (!box.contains(e.point)) return null
|
||||
list.selectedIndex = row
|
||||
return list.model.getElementAt(row)
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import javax.swing.AbstractListModel
|
||||
|
||||
open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
|
||||
private var all = emptyList<T>()
|
||||
private var rows = emptyList<T>()
|
||||
private var query = ""
|
||||
|
||||
var loading = false
|
||||
private set
|
||||
var error: String? = null
|
||||
private set
|
||||
var loaded = false
|
||||
private set
|
||||
|
||||
val items: List<T> get() = all
|
||||
val visibleItems: List<T> get() = rows
|
||||
|
||||
override fun getSize(): Int = rows.size
|
||||
|
||||
override fun getElementAt(index: Int): T = rows[index]
|
||||
|
||||
fun start() {
|
||||
loading = true
|
||||
error = null
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun replace(items: List<T>) {
|
||||
all = HistoryTime.sorted(items)
|
||||
loading = false
|
||||
loaded = true
|
||||
error = null
|
||||
filter()
|
||||
}
|
||||
|
||||
fun append(items: List<T>) {
|
||||
all = HistoryTime.sorted(all + items)
|
||||
loading = false
|
||||
loaded = true
|
||||
error = null
|
||||
filter()
|
||||
}
|
||||
|
||||
fun remove(id: String) {
|
||||
all = all.filterNot { it.id == id }
|
||||
filter()
|
||||
}
|
||||
|
||||
fun fail(message: String) {
|
||||
loading = false
|
||||
loaded = true
|
||||
error = message
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setFilter(value: String) {
|
||||
query = value.trim().lowercase()
|
||||
filter()
|
||||
}
|
||||
|
||||
fun hasFilter(): Boolean = query.isNotEmpty()
|
||||
|
||||
fun refresh() {
|
||||
val end = size.coerceAtLeast(1) - 1
|
||||
fireContentsChanged(this, 0, end)
|
||||
}
|
||||
|
||||
protected fun clear() {
|
||||
all = emptyList()
|
||||
filter()
|
||||
}
|
||||
|
||||
private fun filter() {
|
||||
val old = rows.size
|
||||
rows = all.filter(::matches)
|
||||
val new = rows.size
|
||||
when {
|
||||
old > new -> {
|
||||
if (new > 0) fireContentsChanged(this, 0, new - 1)
|
||||
fireIntervalRemoved(this, new, old - 1)
|
||||
}
|
||||
new > old -> {
|
||||
if (old > 0) fireContentsChanged(this, 0, old - 1)
|
||||
fireIntervalAdded(this, old, new - 1)
|
||||
}
|
||||
new > 0 -> fireContentsChanged(this, 0, new - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun matches(item: T): Boolean {
|
||||
if (query.isEmpty()) return true
|
||||
if (item.title.lowercase().contains(query)) return true
|
||||
if (item.id.lowercase().contains(query)) return true
|
||||
if (item is LocalHistoryItem && item.directory?.lowercase()?.contains(query) == true) return true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class CloudHistoryModel : HistoryModel<CloudHistoryItem>() {
|
||||
var cursor: String? = null
|
||||
private set
|
||||
|
||||
fun start(reset: Boolean) {
|
||||
cursor = cursor.takeUnless { reset }
|
||||
start()
|
||||
if (reset && !loaded) clear()
|
||||
}
|
||||
|
||||
fun replace(items: List<CloudHistoryItem>, next: String?) {
|
||||
cursor = next
|
||||
replace(items)
|
||||
}
|
||||
|
||||
fun append(items: List<CloudHistoryItem>, next: String?) {
|
||||
cursor = next
|
||||
append(items)
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.LoadingPanel
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.ListUtil
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.tabs.JBTabs
|
||||
import com.intellij.ui.tabs.JBTabsFactory
|
||||
import com.intellij.ui.tabs.JBTabsPosition
|
||||
import com.intellij.ui.tabs.TabInfo
|
||||
import com.intellij.ui.tabs.TabsListener
|
||||
import com.intellij.util.ui.Centerizer
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Cursor
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.HierarchyEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JList
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
|
||||
class HistoryPanel(
|
||||
parent: Disposable,
|
||||
private val controller: HistoryController,
|
||||
private val gitUrl: () -> String? = { null },
|
||||
private val nav: () -> Unit = {},
|
||||
) : BorderLayoutPanel(), Disposable {
|
||||
private val localSearch = search(controller.local)
|
||||
private val cloudSearch = search(controller.cloud)
|
||||
private val localList = localList()
|
||||
private val cloudList = cloudList()
|
||||
private val more = LoadMoreButton()
|
||||
private val localPanel = panel(localSearch, localList)
|
||||
private val cloudPanel = panel(cloudSearch, cloudList, more)
|
||||
private val cards = CardLayout()
|
||||
private val body = BorderLayoutPanel().apply { layout = cards }
|
||||
private val load = LoadingPanel()
|
||||
private val localInfo = TabInfo(localPanel)
|
||||
.setText(KiloBundle.message("history.tab.local"))
|
||||
.setForeSideComponent(back())
|
||||
private val cloudInfo = TabInfo(cloudPanel)
|
||||
.setText(KiloBundle.message("history.tab.cloud"))
|
||||
.setForeSideComponent(back())
|
||||
private var stale = false
|
||||
private val tabs: JBTabs = JBTabsFactory.createTabs(null, this).apply {
|
||||
presentation.setSingleRow(true)
|
||||
presentation.setTabsPosition(JBTabsPosition.top)
|
||||
presentation.showBorder = false
|
||||
addTab(localInfo).setPreferredFocusableComponent(localSearch.textEditor)
|
||||
addTab(cloudInfo).setPreferredFocusableComponent(cloudSearch.textEditor)
|
||||
addListener(object : TabsListener {
|
||||
override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) {
|
||||
sync()
|
||||
}
|
||||
}, this@HistoryPanel)
|
||||
}
|
||||
|
||||
init {
|
||||
Disposer.register(parent, this)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg())
|
||||
more.addActionListener { controller.loadMoreCloud() }
|
||||
bind(localList, controller.local)
|
||||
bind(cloudList, controller.cloud)
|
||||
bindTheme()
|
||||
addHierarchyListener { e ->
|
||||
if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener
|
||||
if (isShowing && stale) {
|
||||
refresh()
|
||||
return@addHierarchyListener
|
||||
}
|
||||
if (!isShowing) stale = true
|
||||
}
|
||||
body.add(load, CARD_LOAD)
|
||||
body.add(tabs.component, CARD_TABS)
|
||||
add(body, BorderLayout.CENTER)
|
||||
sync()
|
||||
refresh()
|
||||
}
|
||||
|
||||
val component: JComponent get() = this
|
||||
|
||||
val defaultFocusedComponent: JComponent get() = activeSearch().textEditor
|
||||
|
||||
fun refresh() {
|
||||
stale = false
|
||||
updateTheme()
|
||||
controller.reload(gitUrl())
|
||||
}
|
||||
|
||||
private fun bindTheme() {
|
||||
val bus = ApplicationManager.getApplication().messageBus.connect(this)
|
||||
bus.subscribe(LafManagerListener.TOPIC, LafManagerListener {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
updateTheme()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateTheme() {
|
||||
SwingUtilities.updateComponentTreeUI(this)
|
||||
SwingUtilities.updateComponentTreeUI(localPanel)
|
||||
SwingUtilities.updateComponentTreeUI(cloudPanel)
|
||||
load.applyStyle(SessionEditorStyle.current())
|
||||
updateRenderer(localList)
|
||||
updateRenderer(cloudList)
|
||||
sync()
|
||||
}
|
||||
|
||||
private fun updateRenderer(list: JBList<out HistoryItem>) {
|
||||
val view = list.cellRenderer
|
||||
if (view is JComponent) SwingUtilities.updateComponentTreeUI(view)
|
||||
}
|
||||
|
||||
private fun search(model: HistoryModel<out HistoryItem>) = SearchTextField(false).apply {
|
||||
textEditor.emptyText.text = KiloBundle.message("history.search.placeholder")
|
||||
textEditor.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
model.setFilter(text)
|
||||
}
|
||||
})
|
||||
textEditor.registerKeyboardAction(
|
||||
{ move(-1) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
textEditor.registerKeyboardAction(
|
||||
{ move(1) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
textEditor.registerKeyboardAction(
|
||||
{ activeList().selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
}
|
||||
|
||||
private fun back() = BorderLayoutPanel().apply {
|
||||
add(JButton(KiloBundle.message("history.back"), AllIcons.Actions.Back).apply {
|
||||
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
|
||||
isFocusable = false
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addActionListener { nav() }
|
||||
}, BorderLayout.WEST)
|
||||
border = JBUI.Borders.emptyRight(UiStyle.Gap.lg())
|
||||
}
|
||||
|
||||
private fun panel(search: SearchTextField, list: JList<out HistoryItem>, footer: JComponent? = null): JComponent {
|
||||
return BorderLayoutPanel().apply {
|
||||
add(search, BorderLayout.NORTH)
|
||||
add(JBScrollPane(list).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
viewportBorder = JBUI.Borders.empty()
|
||||
}, BorderLayout.CENTER)
|
||||
footer?.let {
|
||||
add(Centerizer(it, Centerizer.TYPE.HORIZONTAL).apply {
|
||||
border = JBUI.Borders.emptyTop(UiStyle.Gap.lg())
|
||||
}, BorderLayout.SOUTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun localList() = JBList(controller.local).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
isFocusable = false
|
||||
cellRenderer = LocalHistoryRenderer(controller.local)
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return
|
||||
val item = clicked(this@apply, e) ?: return
|
||||
if (deleteClick(this@apply, e)) {
|
||||
confirm(item)
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
activate(item)
|
||||
}
|
||||
})
|
||||
registerKeyboardAction(
|
||||
{ selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
ListUtil.installAutoSelectOnMouseMove(this)
|
||||
ScrollingUtil.installActions(this)
|
||||
}
|
||||
|
||||
private fun cloudList() = JBList(controller.cloud).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
isFocusable = false
|
||||
cellRenderer = CloudHistoryRenderer(controller.cloud)
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return
|
||||
clicked(this@apply, e)?.let(::activate)
|
||||
}
|
||||
})
|
||||
registerKeyboardAction(
|
||||
{ selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
ListUtil.installAutoSelectOnMouseMove(this)
|
||||
ScrollingUtil.installActions(this)
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> bind(list: JBList<T>, model: HistoryModel<T>) {
|
||||
val listener = object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) = sync()
|
||||
|
||||
override fun intervalRemoved(e: ListDataEvent) = sync()
|
||||
|
||||
override fun contentsChanged(e: ListDataEvent) = sync()
|
||||
}
|
||||
model.addListDataListener(listener)
|
||||
Disposer.register(this) { model.removeListDataListener(listener) }
|
||||
list.setPaintBusy(model.loading)
|
||||
}
|
||||
|
||||
private fun sync() {
|
||||
syncList(localList, controller.local)
|
||||
syncList(cloudList, controller.cloud)
|
||||
more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading
|
||||
more.isVisible = controller.cloud.cursor != null || controller.cloud.loading
|
||||
cards.show(body, if (loading()) CARD_LOAD else CARD_TABS)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun loading(): Boolean {
|
||||
if (controller.local.loaded || controller.cloud.loaded) return false
|
||||
return controller.local.loading || controller.cloud.loading
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> syncList(list: JBList<T>, model: HistoryModel<T>) {
|
||||
list.setPaintBusy(model.loading)
|
||||
list.emptyText.text = when {
|
||||
model.loading -> KiloBundle.message("history.loading")
|
||||
model.error != null -> model.error.orEmpty()
|
||||
else -> KiloBundle.message("history.empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteClick(list: JBList<LocalHistoryItem>, e: MouseEvent): Boolean {
|
||||
val row = list.locationToIndex(e.point)
|
||||
val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return false
|
||||
if (!box.contains(e.point)) return false
|
||||
return HistoryRenderer.isDeleteClick(list, box, e.point)
|
||||
}
|
||||
|
||||
private fun activate(item: HistoryItem) {
|
||||
when (item) {
|
||||
is LocalHistoryItem -> controller.open(item)
|
||||
is CloudHistoryItem -> controller.open(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirm(item: LocalHistoryItem) {
|
||||
if (controller.deleting(item)) return
|
||||
val result = Messages.showYesNoDialog(
|
||||
this,
|
||||
KiloBundle.message("history.delete.confirm.message", title(item)),
|
||||
KiloBundle.message("history.delete.confirm.title"),
|
||||
Messages.getWarningIcon(),
|
||||
)
|
||||
if (result != Messages.YES) return
|
||||
controller.delete(item)
|
||||
}
|
||||
|
||||
internal fun itemCount() = activeModel().size
|
||||
|
||||
internal fun selectedSource() = if (tabs.selectedInfo === cloudInfo) HistorySource.CLOUD else HistorySource.LOCAL
|
||||
|
||||
internal fun select(index: Int) {
|
||||
activeList().selectedIndex = index
|
||||
}
|
||||
|
||||
internal fun selectedIndex() = activeList().selectedIndex
|
||||
|
||||
internal fun listFocusable() = activeList().isFocusable
|
||||
|
||||
internal fun listCursor() = activeList().cursor.type
|
||||
|
||||
internal fun backText(): String? {
|
||||
val view = activeInfo().foreSideComponent ?: return null
|
||||
return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.text
|
||||
}
|
||||
|
||||
internal fun backCursor(): Int? {
|
||||
val view = activeInfo().foreSideComponent ?: return null
|
||||
return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.cursor?.type
|
||||
}
|
||||
|
||||
internal fun clickBack() {
|
||||
val view = activeInfo().foreSideComponent ?: return
|
||||
UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.doClick()
|
||||
}
|
||||
|
||||
internal fun clickDelete() {
|
||||
localList.selectedValue?.let(controller::delete)
|
||||
}
|
||||
|
||||
internal fun clickCloud() {
|
||||
tabs.select(cloudInfo, false)
|
||||
sync()
|
||||
}
|
||||
|
||||
internal fun clickLocal() {
|
||||
tabs.select(localInfo, false)
|
||||
sync()
|
||||
}
|
||||
|
||||
internal fun clickMore() {
|
||||
more.doClick()
|
||||
}
|
||||
|
||||
internal fun setSearch(value: String) {
|
||||
if (tabs.selectedInfo === cloudInfo) cloudSearch.text = value else localSearch.text = value
|
||||
}
|
||||
|
||||
internal fun groupTitles(): List<String> {
|
||||
val items = activeModel().visibleItems
|
||||
return items.indices.mapNotNull { HistoryRenderer.section(items, it) }
|
||||
}
|
||||
|
||||
internal fun deleteVisible(index: Int, selected: Boolean = true): Boolean {
|
||||
val item = controller.local.getElementAt(index)
|
||||
val view = localList.cellRenderer.getListCellRendererComponent(localList, item, index, selected, false)
|
||||
return view is HistoryRenderer<*> && view.deleteVisible()
|
||||
}
|
||||
|
||||
internal fun cloudDeleteVisible(index: Int, selected: Boolean = true): Boolean {
|
||||
val item = controller.cloud.getElementAt(index)
|
||||
val view = cloudList.cellRenderer.getListCellRendererComponent(cloudList, item, index, selected, false)
|
||||
return view is HistoryRenderer<*> && view.deleteVisible()
|
||||
}
|
||||
|
||||
private fun activeList(): JBList<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) cloudList else localList
|
||||
|
||||
private fun activeModel(): HistoryModel<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) controller.cloud else controller.local
|
||||
|
||||
private fun activeSearch(): SearchTextField = if (tabs.selectedInfo === cloudInfo) cloudSearch else localSearch
|
||||
|
||||
private fun activeInfo(): TabInfo = if (tabs.selectedInfo === cloudInfo) cloudInfo else localInfo
|
||||
|
||||
private fun move(step: Int) {
|
||||
val list = activeList()
|
||||
val size = list.model.size
|
||||
if (size <= 0) return
|
||||
val cur = list.selectedIndex.takeIf { it >= 0 } ?: if (step > 0) -1 else size
|
||||
val idx = (cur + step).coerceIn(0, size - 1)
|
||||
list.selectedIndex = idx
|
||||
ScrollingUtil.ensureIndexIsVisible(list, idx, 0)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private class LoadMoreButton : JButton(KiloBundle.message("history.cloud.load.more")) {
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setRequestFocusEnabled(false)
|
||||
isContentAreaFilled = false
|
||||
isBorderPainted = false
|
||||
isOpaque = false
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (isEnabled && over) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun showingLoading() = !controller.local.loaded && !controller.cloud.loaded && (controller.local.loading || controller.cloud.loading)
|
||||
|
||||
private companion object {
|
||||
const val CARD_LOAD = "load"
|
||||
const val CARD_TABS = "tabs"
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.abs
|
||||
|
||||
private const val SECOND_MS_LIMIT = 10_000_000_000L
|
||||
private const val MINUTE = 60_000L
|
||||
private const val HOUR = 60 * MINUTE
|
||||
private const val DAY = 24 * HOUR
|
||||
|
||||
enum class HistorySection {
|
||||
TODAY,
|
||||
YESTERDAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
OLDER,
|
||||
}
|
||||
|
||||
internal object HistoryTime {
|
||||
fun millis(item: HistoryItem): Long? {
|
||||
val raw = item.updatedAt.toDoubleOrNull()?.toLong() ?: return parse(item.updatedAt)
|
||||
if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000
|
||||
return raw
|
||||
}
|
||||
|
||||
fun section(item: HistoryItem, now: Long = System.currentTimeMillis()): HistorySection {
|
||||
val ms = millis(item) ?: return HistorySection.OLDER
|
||||
val zone = ZoneId.systemDefault()
|
||||
val date = Instant.ofEpochMilli(ms).atZone(zone).toLocalDate()
|
||||
val today = Instant.ofEpochMilli(now).atZone(zone).toLocalDate()
|
||||
if (date == today) return HistorySection.TODAY
|
||||
if (date == today.minusDays(1)) return HistorySection.YESTERDAY
|
||||
if (date.isAfter(today.minusDays(7)) && date.isBefore(today)) return HistorySection.WEEK
|
||||
if (date.isAfter(today.minusDays(30)) && date.isBefore(today)) return HistorySection.MONTH
|
||||
return HistorySection.OLDER
|
||||
}
|
||||
|
||||
fun title(section: HistorySection): String = when (section) {
|
||||
HistorySection.TODAY -> KiloBundle.message("history.group.today")
|
||||
HistorySection.YESTERDAY -> KiloBundle.message("history.group.yesterday")
|
||||
HistorySection.WEEK -> KiloBundle.message("history.group.week")
|
||||
HistorySection.MONTH -> KiloBundle.message("history.group.month")
|
||||
HistorySection.OLDER -> KiloBundle.message("history.group.older")
|
||||
}
|
||||
|
||||
fun relative(item: HistoryItem, now: Long = System.currentTimeMillis()): String {
|
||||
val ms = millis(item) ?: return item.updatedAt
|
||||
val diff = (now - ms).coerceAtLeast(0)
|
||||
if (diff < MINUTE) return KiloBundle.message("history.time.moments")
|
||||
if (diff < HOUR) return KiloBundle.message("history.time.minutes", (diff / MINUTE).coerceAtLeast(1))
|
||||
if (diff < DAY) return KiloBundle.message("history.time.hours", (diff / HOUR).coerceAtLeast(1))
|
||||
if (diff < 7 * DAY) return KiloBundle.message("history.time.days", (diff / DAY).coerceAtLeast(1))
|
||||
val date = LocalDate.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault())
|
||||
val today = LocalDate.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault())
|
||||
val months = ChronoUnit.MONTHS.between(date.withDayOfMonth(1), today.withDayOfMonth(1))
|
||||
if (months < 1) return KiloBundle.message("history.time.days", (diff / DAY).coerceAtLeast(1))
|
||||
if (months < 12) return KiloBundle.message("history.time.months", months)
|
||||
return KiloBundle.message("history.time.years", months / 12)
|
||||
}
|
||||
|
||||
fun <T : HistoryItem> sorted(items: List<T>): List<T> = items.sortedWith(
|
||||
compareByDescending<T> { millis(it) ?: Long.MIN_VALUE }
|
||||
.thenBy { it.title.lowercase() }
|
||||
.thenBy { it.id },
|
||||
)
|
||||
|
||||
private fun parse(value: String): Long? {
|
||||
val text = value.trim().replace(' ', 'T')
|
||||
val zoned = when {
|
||||
text.matches(Regex(".*[+-]\\d{2}$")) -> "$text:00"
|
||||
text.matches(Regex(".*[+-]\\d{4}$")) -> text.dropLast(2) + ":" + text.takeLast(2)
|
||||
else -> text
|
||||
}
|
||||
return runCatching { Instant.parse(zoned).toEpochMilli() }.getOrNull()
|
||||
?: runCatching { OffsetDateTime.parse(zoned).toInstant().toEpochMilli() }.getOrNull()
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,7 @@ import kotlin.math.roundToInt
|
||||
/**
|
||||
* Pure session model — single source of truth for session content and runtime state.
|
||||
*
|
||||
* **EDT-only access** — no synchronization. [ai.kilocode.client.session.update.SessionController] guarantees all
|
||||
* **EDT-only access** — no synchronization. [ai.kilocode.client.session.controller.SessionController] guarantees all
|
||||
* reads and writes happen on the EDT.
|
||||
*
|
||||
* In addition to the flat message list, the model maintains a derived
|
||||
@@ -175,6 +175,7 @@ class SessionModel {
|
||||
}
|
||||
|
||||
fun setState(state: SessionState) {
|
||||
if (this.state == state) return
|
||||
this.state = state
|
||||
fire(SessionModelEvent.StateChanged(state))
|
||||
updateHeader()
|
||||
|
||||
+3
-1
@@ -4,6 +4,8 @@ package ai.kilocode.client.session.model
|
||||
sealed class SessionState {
|
||||
data object Idle : SessionState()
|
||||
|
||||
data object Loading : SessionState()
|
||||
|
||||
data class Busy(val text: String) : SessionState()
|
||||
|
||||
data class AwaitingQuestion(val question: Question) : SessionState()
|
||||
@@ -17,7 +19,7 @@ sealed class SessionState {
|
||||
data class Error(val message: String, val kind: String? = null) : SessionState()
|
||||
|
||||
fun isBusy(): Boolean = when (this) {
|
||||
is Idle, is Error -> false
|
||||
is Idle, is Loading, is Error -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -1,9 +1,10 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.update.SessionControllerEvent
|
||||
import ai.kilocode.client.session.update.SessionControllerListener
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.session.controller.SessionControllerEvent
|
||||
import ai.kilocode.client.session.controller.SessionControllerListener
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
@@ -39,11 +40,11 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private val header = BorderLayoutPanel().apply {
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG, 0, UiStyle.Space.LG)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg())
|
||||
}
|
||||
|
||||
private val left = BorderLayoutPanel().apply {
|
||||
layout = UiStyle.Gap.layout(UiStyle.Space.SM)
|
||||
layout = BorderLayout(UiStyle.Gap.sm(), 0)
|
||||
addMouseListener(click)
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private val scroll = JBScrollPane(details).apply {
|
||||
border = JBUI.Borders.empty(0, UiStyle.Space.LG, UiStyle.Space.SM, 0)
|
||||
border = JBUI.Borders.empty(0, UiStyle.Gap.lg(), UiStyle.Gap.sm(), 0)
|
||||
// Match the banner background while retaining platform scroll behavior.
|
||||
isOpaque = false
|
||||
viewport.isOpaque = false
|
||||
@@ -93,7 +94,7 @@ class ConnectionPanel(
|
||||
// Keep the banner solid so expanded details cover transcript content beneath it.
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.bg()
|
||||
border = JBUI.Borders.customLine(UiStyle.Colors.line(), 1, 0, 0, 0)
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.line(), 1, 0, 0, 0)
|
||||
left.add(toggle, BorderLayout.WEST)
|
||||
left.add(label, BorderLayout.CENTER)
|
||||
header.add(left, BorderLayout.CENTER)
|
||||
@@ -135,7 +136,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private fun showError(text: String, detail: String?) {
|
||||
label.foreground = UiStyle.Colors.error()
|
||||
label.foreground = UiStyle.Colors.errorLabelForeground()
|
||||
label.text = text
|
||||
retry.isVisible = true
|
||||
this.detail = detail?.takeIf { it.isNotBlank() }
|
||||
@@ -145,7 +146,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private fun showWarning(text: String, detail: String?) {
|
||||
label.foreground = UiStyle.Colors.warning()
|
||||
label.foreground = UiStyle.Colors.warningLabelForeground()
|
||||
label.text = text
|
||||
retry.isVisible = true
|
||||
this.detail = detail?.takeIf { it.isNotBlank() }
|
||||
|
||||
+168
-103
@@ -1,73 +1,78 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.history.HistoryTime
|
||||
import ai.kilocode.client.session.history.LocalHistoryItem
|
||||
import ai.kilocode.client.session.history.clicked
|
||||
import ai.kilocode.client.session.history.title
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.ui.CenterShrinkPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.Centerizer
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionAdapter
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.DefaultListModel
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JList
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.ListSelectionModel
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Centered empty-session panel.
|
||||
* Empty-session panel.
|
||||
*
|
||||
* The content is a BorderLayout panel, wrapped in a
|
||||
* [CenterShrinkPanel] (exposed as [view]) so callers need not know about centering.
|
||||
*/
|
||||
class EmptySessionPanel(
|
||||
parent: Disposable,
|
||||
private val controller: SessionController,
|
||||
recents: List<SessionDto>,
|
||||
) : BorderLayoutPanel(), Disposable, SessionStyleTarget {
|
||||
private val history: () -> Unit = {},
|
||||
) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget {
|
||||
val view: CenterShrinkPanel = CenterShrinkPanel(this)
|
||||
|
||||
companion object {
|
||||
internal val LIMIT = UiStyle.Size.LIMIT
|
||||
internal val MAX_WIDTH = UiStyle.Size.WIDTH
|
||||
private const val SECOND_MS_LIMIT = 10_000_000_000L
|
||||
private const val MINUTE = 60_000L
|
||||
private const val HOUR = 60 * MINUTE
|
||||
private const val DAY = 24 * HOUR
|
||||
}
|
||||
|
||||
private val model = DefaultListModel<SessionDto>()
|
||||
private val model = DefaultListModel<LocalHistoryItem>()
|
||||
private var hover = -1
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
border = JBUI.Borders.emptyLeft(UiStyle.Space.LG)
|
||||
}
|
||||
|
||||
private val list = JBList(model).apply {
|
||||
// Blend the recent-session list into the centered empty-state surface.
|
||||
isOpaque = false
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
visibleRowCount = LIMIT
|
||||
visibleRowCount = SessionUiStyle.RecentSessions.LIMIT
|
||||
cellRenderer = SessionRenderer()
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.clear()
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
val index = row(e)
|
||||
if (index < 0) return
|
||||
selectedIndex = index
|
||||
controller.openSession(model.getElementAt(index))
|
||||
val item = clicked(this@apply, e) ?: return
|
||||
controller.openSession(SessionRef.Local(item.session))
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
@@ -77,72 +82,83 @@ class EmptySessionPanel(
|
||||
})
|
||||
addMouseMotionListener(object : MouseMotionAdapter() {
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
val index = row(e)
|
||||
val index = index(e)
|
||||
if (hover == index) return
|
||||
hover = index
|
||||
repaint()
|
||||
}
|
||||
})
|
||||
}
|
||||
private val md = MdView.html().apply {
|
||||
// MdView uses an HTML component; transparency keeps the centered panel seamless.
|
||||
opaque = false
|
||||
|
||||
private val historyButton = ShowHistoryButton().apply {
|
||||
addActionListener { history() }
|
||||
}
|
||||
|
||||
private val welcomeLabel = JBLabel(welcomeHtml()).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
set(KiloBundle.message("session.empty.welcome"))
|
||||
horizontalAlignment = JBLabel.CENTER
|
||||
setAllowAutoWrapping(true)
|
||||
}
|
||||
|
||||
private val description = object : BorderLayoutPanel() {
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
return Dimension(JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), size.height)
|
||||
}
|
||||
|
||||
override fun getMaximumSize(): Dimension {
|
||||
val size = super.getMaximumSize()
|
||||
return Dimension(JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), size.height)
|
||||
}
|
||||
}.apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg(), 0, UiStyle.Gap.lg(), 0)
|
||||
add(welcomeLabel, BorderLayout.CENTER)
|
||||
}
|
||||
private val content = createContent()
|
||||
|
||||
init {
|
||||
Disposer.register(parent, this)
|
||||
// The empty state floats on the tool-window background.
|
||||
isOpaque = false
|
||||
border = UiStyle.Insets.empty()
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
setSessions(recents)
|
||||
add(Centerizer(content, Centerizer.TYPE.BOTH), BorderLayout.CENTER)
|
||||
|
||||
val gap = UiStyle.Gap.pad()
|
||||
layout = BorderLayout(0, gap)
|
||||
|
||||
val logo = JBLabel(
|
||||
IconLoader.getIcon("/icons/kilo-content.svg", EmptySessionPanel::class.java),
|
||||
).apply {
|
||||
horizontalAlignment = JBLabel.CENTER
|
||||
}
|
||||
val header = BorderLayoutPanel(0, gap).apply {
|
||||
isOpaque = false
|
||||
add(logo, BorderLayout.NORTH)
|
||||
add(CenterShrinkPanel(description), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val recent = BorderLayoutPanel().apply {
|
||||
isOpaque = false
|
||||
add(recentTitle, BorderLayout.NORTH)
|
||||
add(list, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val south = BorderLayoutPanel().apply {
|
||||
isOpaque = false
|
||||
add(Centerizer(historyButton, Centerizer.TYPE.HORIZONTAL), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
add(header, BorderLayout.NORTH)
|
||||
add(recent, BorderLayout.CENTER)
|
||||
add(south, BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
private fun setSessions(sessions: List<SessionDto>) {
|
||||
model.clear()
|
||||
sessions.take(LIMIT).forEach(model::addElement)
|
||||
sessions.take(SessionUiStyle.RecentSessions.LIMIT).map(::LocalHistoryItem).forEach(model::addElement)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun createContent(): BorderLayoutPanel {
|
||||
val logo = JBLabel(
|
||||
IconLoader.getIcon("/icons/kilo-content.svg", EmptySessionPanel::class.java),
|
||||
).apply {
|
||||
alignmentX = CENTER_ALIGNMENT
|
||||
}
|
||||
val intro = BorderLayoutPanel().apply {
|
||||
alignmentX = CENTER_ALIGNMENT
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
border = JBUI.Borders.empty(0, UiStyle.Space.PAD, 0, UiStyle.Space.PAD)
|
||||
}
|
||||
val recent = BorderLayoutPanel().apply {
|
||||
alignmentX = CENTER_ALIGNMENT
|
||||
add(recentTitle, BorderLayout.NORTH)
|
||||
add(list, BorderLayout.CENTER)
|
||||
}
|
||||
val stack = BorderLayoutPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
add(logo)
|
||||
add(Box.createVerticalStrut(JBUI.scale(UiStyle.Space.LOGO)))
|
||||
add(intro)
|
||||
add(Box.createVerticalStrut(JBUI.scale(UiStyle.Space.RECENT)))
|
||||
add(recent)
|
||||
}
|
||||
return object : BorderLayoutPanel() {
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
return JBDimension(JBUI.scale(MAX_WIDTH), size.height)
|
||||
}
|
||||
}.apply {
|
||||
add(stack, BorderLayout.NORTH)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun recentCount() = model.size()
|
||||
|
||||
internal fun selectRecent(index: Int) {
|
||||
@@ -153,14 +169,32 @@ class EmptySessionPanel(
|
||||
|
||||
internal fun clickRecent(index: Int) {
|
||||
list.selectedIndex = index
|
||||
controller.openSession(model.getElementAt(index))
|
||||
controller.openSession(SessionRef.Local(model.getElementAt(index).session))
|
||||
}
|
||||
|
||||
internal fun clickShowHistory() {
|
||||
historyButton.doClick()
|
||||
}
|
||||
|
||||
internal fun showHistoryText() = historyButton.text
|
||||
|
||||
internal fun showHistoryBorderPainted() = historyButton.isBorderPainted
|
||||
|
||||
internal fun showHistoryCursor() = historyButton.cursor.type
|
||||
|
||||
internal fun recentCursor() = list.cursor.type
|
||||
|
||||
internal fun recentVisible() = true
|
||||
|
||||
internal fun explanationMarkdown() = md.markdown()
|
||||
internal fun explanationText() = KiloBundle.message("session.empty.welcome")
|
||||
|
||||
internal fun contentPreferredSize() = content.preferredSize
|
||||
internal fun welcomeLabelAlignment() = welcomeLabel.horizontalAlignment
|
||||
|
||||
internal fun descriptionPreferredSize() = description.preferredSize
|
||||
|
||||
internal fun descriptionMaximumSize() = description.maximumSize
|
||||
|
||||
internal fun historyButtonPreferredWidth() = historyButton.preferredSize.width
|
||||
|
||||
internal fun initialized() = true
|
||||
|
||||
@@ -168,13 +202,8 @@ class EmptySessionPanel(
|
||||
|
||||
internal fun activeView() = getComponent(0)
|
||||
|
||||
internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = time(session, now)
|
||||
|
||||
internal fun normalize(value: Double): Long {
|
||||
val raw = value.toLong()
|
||||
if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000
|
||||
return raw
|
||||
}
|
||||
internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) =
|
||||
HistoryTime.relative(LocalHistoryItem(session), now)
|
||||
|
||||
internal fun rendererComponent(
|
||||
session: SessionDto,
|
||||
@@ -183,32 +212,33 @@ class EmptySessionPanel(
|
||||
): Component {
|
||||
val old = this.hover
|
||||
this.hover = if (hover) 0 else -1
|
||||
return list.cellRenderer.getListCellRendererComponent(list, session, 0, selected, false).also {
|
||||
return list.cellRenderer.getListCellRendererComponent(list, LocalHistoryItem(session), 0, selected, false).also {
|
||||
this.hover = old
|
||||
}
|
||||
}
|
||||
|
||||
private fun row(e: MouseEvent): Int {
|
||||
val index = list.locationToIndex(e.point)
|
||||
if (index < 0) return -1
|
||||
val box = list.getCellBounds(index, index) ?: return -1
|
||||
private fun index(e: MouseEvent): Int {
|
||||
val idx = list.locationToIndex(e.point)
|
||||
if (idx < 0) return -1
|
||||
val box = list.getCellBounds(idx, idx) ?: return -1
|
||||
if (!box.contains(e.point)) return -1
|
||||
return index
|
||||
return idx
|
||||
}
|
||||
|
||||
private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer<SessionDto> {
|
||||
private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer<LocalHistoryItem> {
|
||||
private val title = JBLabel()
|
||||
private val time = JBLabel()
|
||||
|
||||
init {
|
||||
border = JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG)
|
||||
layout = BorderLayout(UiStyle.Gap.pad(), 0)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg())
|
||||
add(title, BorderLayout.CENTER)
|
||||
add(time, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<out SessionDto>,
|
||||
value: SessionDto?,
|
||||
list: JList<out LocalHistoryItem>,
|
||||
value: LocalHistoryItem?,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
focus: Boolean,
|
||||
@@ -219,32 +249,67 @@ class EmptySessionPanel(
|
||||
title.foreground = if (active) list.selectionForeground else UIUtil.getLabelForeground()
|
||||
time.foreground = if (active) list.selectionForeground else UIUtil.getContextHelpForeground()
|
||||
title.text = value?.let(::title) ?: ""
|
||||
time.text = value?.let { time(it) } ?: ""
|
||||
time.text = value?.let(HistoryTime::relative) ?: ""
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private fun title(session: SessionDto) =
|
||||
session.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("session.tab.untitled")
|
||||
private inner class ShowHistoryButton : JButton(KiloBundle.message("session.showHistory"), AllIcons.Vcs.History) {
|
||||
private var over = false
|
||||
|
||||
private fun time(session: SessionDto, now: Long = System.currentTimeMillis()): String {
|
||||
val ms = normalize(session.time.updated)
|
||||
val diff = (now - ms).coerceAtLeast(0)
|
||||
if (diff < MINUTE) return KiloBundle.message("session.empty.time.moments")
|
||||
if (diff < HOUR) return KiloBundle.message("session.empty.time.minutes", (diff / MINUTE).coerceAtLeast(1))
|
||||
if (diff < DAY) return KiloBundle.message("session.empty.time.hours", (diff / HOUR).coerceAtLeast(1))
|
||||
return KiloBundle.message("session.empty.time.days", (diff / DAY).coerceAtLeast(1))
|
||||
init {
|
||||
isFocusable = false
|
||||
setRequestFocusEnabled(false)
|
||||
isContentAreaFilled = false
|
||||
isBorderPainted = false
|
||||
isOpaque = false
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (isEnabled && over) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
md.font = style.uiFont
|
||||
welcomeLabel.font = style.uiFont
|
||||
recentTitle.font = style.smallUiFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun welcomeHtml() = XmlStringUtil.wrapInHtml(
|
||||
"<div style='text-align:center'>${XmlStringUtil.escapeString(KiloBundle.message("session.empty.welcome"))}</div>"
|
||||
)
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.Centerizer
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JPanel
|
||||
|
||||
class LoadingPanel : JPanel(BorderLayout()), SessionEditorStyleTarget {
|
||||
private val label = JBLabel(KiloBundle.message("session.empty.loading"))
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
add(Centerizer(label, Centerizer.TYPE.BOTH), BorderLayout.CENTER)
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
label.font = style.uiFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.model.Permission
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.session.ui.style.Dock
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
@@ -36,7 +36,7 @@ class PermissionPanel(
|
||||
private lateinit var requestId: String
|
||||
|
||||
init {
|
||||
border = UiStyle.Dock.warning()
|
||||
border = Dock.warning()
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -3,6 +3,8 @@ package ai.kilocode.client.session.ui
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
@@ -15,7 +17,7 @@ import java.awt.FlowLayout
|
||||
*
|
||||
* Reacts to [SessionModelEvent.StateChanged]:
|
||||
* - [SessionState.Busy] → shows an animated spinner and [SessionState.Busy.text]
|
||||
* - Any other state → hidden
|
||||
* - Any other state -> hidden
|
||||
*
|
||||
* Owned by [SessionMessageListPanel], which always re-anchors it as the last child so it
|
||||
* appears below all turn views inside the scroll pane.
|
||||
@@ -23,7 +25,7 @@ import java.awt.FlowLayout
|
||||
class ProgressPanel(
|
||||
model: SessionModel,
|
||||
parent: Disposable,
|
||||
) : SessionLayoutPanel(), SessionStyleTarget {
|
||||
) : SessionLayoutPanel(), SessionEditorStyleTarget {
|
||||
|
||||
private val label = JBLabel().apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
@@ -32,8 +34,8 @@ class ProgressPanel(
|
||||
init {
|
||||
isOpaque = false
|
||||
isVisible = false
|
||||
layout = FlowLayout(FlowLayout.LEFT, UiStyle.Gap.inline(), 0)
|
||||
applyStyle(SessionStyle.current())
|
||||
layout = FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0)
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
|
||||
add(JBLabel(AnimatedIcon.Default()))
|
||||
add(label)
|
||||
@@ -52,13 +54,14 @@ class ProgressPanel(
|
||||
label.text = state.text
|
||||
isVisible = true
|
||||
}
|
||||
is SessionState.Loading -> isVisible = false
|
||||
else -> isVisible = false
|
||||
}
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
label.font = style.uiFont
|
||||
revalidate()
|
||||
repaint()
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.model.Question
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.session.ui.style.Dock
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
@@ -35,7 +35,7 @@ class QuestionPanel(
|
||||
private var requestId: String? = null
|
||||
|
||||
init {
|
||||
border = UiStyle.Dock.neutral()
|
||||
border = Dock.neutral()
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.PickerButton
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.openapi.ui.popup.ListPopup
|
||||
@@ -21,7 +21,7 @@ import javax.swing.Icon
|
||||
* opens a list popup above the picker. Disabled (greyed out, not
|
||||
* clickable) when no items are loaded.
|
||||
*/
|
||||
class ReasoningPicker : UiStyle.Pickers.Label() {
|
||||
class ReasoningPicker : PickerButton() {
|
||||
|
||||
private companion object {
|
||||
val checked: Icon = AllIcons.Actions.Checked
|
||||
|
||||
+9
-8
@@ -1,7 +1,8 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
@@ -29,8 +30,8 @@ import java.awt.LayoutManager
|
||||
* the panel width and the layout always has a valid width to work with.
|
||||
*/
|
||||
class SessionLayout(
|
||||
private val gap: Int = UiStyle.Gap.part(),
|
||||
private val pad: Insets = UiStyle.Insets.none(),
|
||||
private val gap: Int = JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
private val pad: Insets = JBUI.emptyInsets(),
|
||||
) : LayoutManager {
|
||||
|
||||
override fun addLayoutComponent(name: String, comp: Component) = Unit
|
||||
@@ -76,9 +77,9 @@ class SessionLayout(
|
||||
private fun bounds(ins: Insets, width: Int, comp: Component): Bounds {
|
||||
val view = comp as? SessionView ?: return Bounds(ins.left, width)
|
||||
if (view.sessionViewKind != SessionView.Kind.UserPrompt) return Bounds(ins.left, width)
|
||||
val shift = UiStyle.Insets.userPrompt()
|
||||
val shift = JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT)
|
||||
val next = width - shift
|
||||
if (next < UiStyle.Size.userPromptMin()) return Bounds(ins.left, width)
|
||||
if (next < JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT)) return Bounds(ins.left, width)
|
||||
return Bounds(ins.left + shift, next)
|
||||
}
|
||||
|
||||
@@ -103,8 +104,8 @@ class SessionLayout(
|
||||
* [SessionLayout] a valid width to measure against.
|
||||
*/
|
||||
open class SessionLayoutPanel(
|
||||
gap: Int = UiStyle.Gap.part(),
|
||||
pad: Insets = UiStyle.Insets.none(),
|
||||
gap: Int = JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
pad: Insets = JBUI.emptyInsets(),
|
||||
) : BorderLayoutPanel(), javax.swing.Scrollable {
|
||||
init {
|
||||
layout = SessionLayout(gap, pad)
|
||||
@@ -117,7 +118,7 @@ open class SessionLayoutPanel(
|
||||
visibleRect: java.awt.Rectangle,
|
||||
@Suppress("UNUSED_PARAMETER") orientation: Int,
|
||||
@Suppress("UNUSED_PARAMETER") direction: Int,
|
||||
): Int = UiStyle.Gap.scroll()
|
||||
): Int = JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_INCREMENT)
|
||||
override fun getScrollableBlockIncrement(
|
||||
visibleRect: java.awt.Rectangle,
|
||||
@Suppress("UNUSED_PARAMETER") orientation: Int,
|
||||
|
||||
+15
-4
@@ -2,10 +2,13 @@ package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.MessageView
|
||||
import ai.kilocode.client.session.views.TurnView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.util.ui.JBUI
|
||||
|
||||
/**
|
||||
* Scrollable transcript panel that maps the model's turn grouping to
|
||||
@@ -31,12 +34,20 @@ import com.intellij.openapi.Disposable
|
||||
class SessionMessageListPanel(
|
||||
private val model: SessionModel,
|
||||
parent: Disposable,
|
||||
) : SessionLayoutPanel(UiStyle.Card.groupGap(), UiStyle.Insets.transcript()), SessionStyleTarget {
|
||||
) : SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
JBUI.insets(
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
),
|
||||
), SessionEditorStyleTarget {
|
||||
|
||||
private val turnViews = LinkedHashMap<String, TurnView>()
|
||||
private val msgToTurn = HashMap<String, TurnView>()
|
||||
private val msgToView = HashMap<String, MessageView>()
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
/** Progress footer — always the last child inside the scroll. */
|
||||
val progress = ProgressPanel(model, parent)
|
||||
@@ -240,7 +251,7 @@ class SessionMessageListPanel(
|
||||
repaint()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
for (view in turnViews.values) view.applyStyle(style)
|
||||
progress.applyStyle(style)
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ 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.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
@@ -15,14 +15,14 @@ import java.awt.RenderingHints
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.inline(), 0)) {
|
||||
internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 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)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0)
|
||||
add(used, BorderLayout.WEST)
|
||||
add(meter, BorderLayout.CENTER)
|
||||
add(limit, BorderLayout.EAST)
|
||||
@@ -40,7 +40,7 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.inline(), 0)) {
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun applyStyle(style: SessionStyle) {
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
background = style.editorBackground
|
||||
foreground = style.editorForeground
|
||||
meter.background = style.editorBackground
|
||||
@@ -143,7 +143,7 @@ private class Meter : JComponent() {
|
||||
fun reservedColor(): Color = shade(0.28f)
|
||||
|
||||
private fun shade(alpha: Float): Color {
|
||||
val base = background ?: UiStyle.Colors.panel()
|
||||
val base = background ?: UiStyle.Colors.editorBackground()
|
||||
val grey = if (UiStyle.Colors.bright(base)) Color.BLACK else Color.WHITE
|
||||
return UiStyle.Colors.blend(base, grey, alpha)
|
||||
}
|
||||
|
||||
+18
-17
@@ -3,9 +3,10 @@ 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.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
@@ -33,7 +34,7 @@ import javax.swing.SwingUtilities
|
||||
class SessionHeaderPanel(
|
||||
private val controller: SessionController,
|
||||
parent: Disposable,
|
||||
) : BorderLayoutPanel(), SessionStyleTarget {
|
||||
) : BorderLayoutPanel(), SessionEditorStyleTarget {
|
||||
|
||||
companion object {
|
||||
private val COMPRESS_ICON: Icon = IconLoader.getIcon("/icons/compress.svg", SessionHeaderPanel::class.java)
|
||||
@@ -51,13 +52,13 @@ class SessionHeaderPanel(
|
||||
private val cost = JBLabel()
|
||||
private val context = JBLabel()
|
||||
private val todos = JBLabel()
|
||||
private val compact = UiStyle.Buttons.HoverIcon().apply {
|
||||
private val compact = 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 {
|
||||
private val expand = HoverIcon().apply {
|
||||
icon = CHEVRON_ICON
|
||||
toolTipText = KiloBundle.message("session.header.expand")
|
||||
accessibleContext.accessibleName = KiloBundle.message("session.header.expand")
|
||||
@@ -90,7 +91,7 @@ class SessionHeaderPanel(
|
||||
iconTextGap = UiStyle.Gap.xs()
|
||||
}
|
||||
private val top = BorderLayoutPanel()
|
||||
private val right = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.inline(), 0)).apply {
|
||||
private val right = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.md(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(cost)
|
||||
add(context)
|
||||
@@ -99,32 +100,32 @@ class SessionHeaderPanel(
|
||||
}
|
||||
private val tokens = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0)
|
||||
add(tokenTitle)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.inline()))
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.md()))
|
||||
add(input)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.sm()))
|
||||
add(output)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.sm()))
|
||||
add(cacheRead)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
add(cacheWrite)
|
||||
}
|
||||
private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.inline(), 0)).apply {
|
||||
private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0)).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.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)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0)
|
||||
add(viewport)
|
||||
add(tokens)
|
||||
add(bar)
|
||||
add(todoRow)
|
||||
}
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
@@ -184,7 +185,7 @@ class SessionHeaderPanel(
|
||||
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),
|
||||
JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad(), UiStyle.Gap.sm(), UiStyle.Gap.pad()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ class SessionHeaderPanel(
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
background = style.editorBackground
|
||||
foreground = style.editorForeground
|
||||
|
||||
+10
-10
@@ -9,7 +9,7 @@ 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 ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
@@ -138,14 +138,14 @@ internal class TimelinePanel : JPanel() {
|
||||
|
||||
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
|
||||
if (part is Tool && part.state == ToolExecState.ERROR) return SessionUiStyle.Timeline.ERROR
|
||||
if (part is Text) return SessionUiStyle.Timeline.TEXT
|
||||
if (part is Reasoning) return SessionUiStyle.Timeline.TEXT
|
||||
if (part is Compaction) return SessionUiStyle.Timeline.STEP
|
||||
if (part is StepFinish) return SessionUiStyle.Timeline.SUCCESS
|
||||
if (part !is Tool) return SessionUiStyle.Timeline.STEP
|
||||
if (part.kind == ToolKind.READ) return SessionUiStyle.Timeline.READ
|
||||
if (part.kind == ToolKind.WRITE) return SessionUiStyle.Timeline.WRITE
|
||||
return SessionUiStyle.Timeline.TOOL
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.session.ui.mode
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.PickerButton
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.openapi.ui.popup.PopupShowOptions
|
||||
import java.awt.Cursor
|
||||
@@ -9,7 +9,7 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.ListSelectionModel
|
||||
|
||||
class ModePicker : UiStyle.Pickers.Label() {
|
||||
class ModePicker : PickerButton() {
|
||||
|
||||
data class Item(
|
||||
val id: String,
|
||||
|
||||
+13
-24
@@ -32,37 +32,26 @@ internal class ModePickerRenderer(
|
||||
private val icon = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
UiStyle.Components.transparent(this)
|
||||
}
|
||||
private val title = SimpleColoredComponent().apply {
|
||||
UiStyle.Components.transparent(this)
|
||||
}
|
||||
private val desc = SimpleColoredComponent().apply {
|
||||
UiStyle.Components.transparent(this)
|
||||
}
|
||||
private val badge = JBLabel(KiloBundle.message("mode.picker.deprecated")).apply {
|
||||
UiStyle.Components.transparent(this)
|
||||
}
|
||||
private val title = SimpleColoredComponent()
|
||||
private val desc = SimpleColoredComponent()
|
||||
private val badge = JBLabel(KiloBundle.message("mode.picker.deprecated"))
|
||||
private val head = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
UiStyle.Components.transparent(this)
|
||||
add(title)
|
||||
add(badge)
|
||||
}
|
||||
private val body = JPanel(BorderLayout()).apply {
|
||||
UiStyle.Components.transparent(this)
|
||||
}
|
||||
private val body = JPanel(BorderLayout())
|
||||
private val row = JPanel(BorderLayout())
|
||||
private val wrap = PickerRow()
|
||||
|
||||
init {
|
||||
UiStyle.Components.transparent(this)
|
||||
UiStyle.Components.transparent(row)
|
||||
(row.layout as BorderLayout).hgap = UiStyle.Gap.inline()
|
||||
UiStyle.Components.transparent(this, icon, title, desc, badge, head, body, row)
|
||||
(row.layout as BorderLayout).hgap = UiStyle.Gap.md()
|
||||
row.border = JBUI.Borders.empty(
|
||||
UiStyle.Space.MD,
|
||||
UiStyle.Space.LG,
|
||||
UiStyle.Space.MD,
|
||||
UiStyle.Space.LG,
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
)
|
||||
body.add(head, BorderLayout.NORTH)
|
||||
body.add(desc, BorderLayout.CENTER)
|
||||
@@ -82,7 +71,7 @@ internal class ModePickerRenderer(
|
||||
val focus = selected || list.hasFocus() || focused
|
||||
val fg = UIUtil.getListForeground(selected, focus)
|
||||
val weak = if (selected) fg else UiStyle.Colors.weak()
|
||||
val warn = if (selected) fg else UiStyle.Colors.warning()
|
||||
val warn = if (selected) fg else UiStyle.Colors.warningLabelForeground()
|
||||
|
||||
background = list.background
|
||||
wrap.update(list, selected, focus)
|
||||
@@ -98,8 +87,8 @@ internal class ModePickerRenderer(
|
||||
badge.border = JBUI.Borders.compound(
|
||||
JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()),
|
||||
JBUI.Borders.compound(
|
||||
RoundedLineBorder(warn, JBUI.scale(UiStyle.Space.SM)),
|
||||
JBUI.Borders.empty(0, UiStyle.Space.MD),
|
||||
RoundedLineBorder(warn, UiStyle.Gap.sm()),
|
||||
JBUI.Borders.empty(0, UiStyle.Gap.md()),
|
||||
),
|
||||
)
|
||||
icon.icon = icon(value)
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.session.ui.model
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.PickerButton
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
@@ -45,7 +45,7 @@ private const val MODEL_PICKER_MAX_WIDTH = 760
|
||||
private const val MODEL_PICKER_MAX_VISIBLE_ROWS = 10
|
||||
private const val MODEL_PICKER_EMPTY_LIST_HEIGHT = 120
|
||||
|
||||
class ModelPicker : UiStyle.Pickers.Label() {
|
||||
class ModelPicker : PickerButton() {
|
||||
|
||||
data class Item(
|
||||
val id: String,
|
||||
|
||||
+5
-10
@@ -93,17 +93,12 @@ internal class ModelPickerRenderer(
|
||||
init {
|
||||
isOpaque = true
|
||||
top.isOpaque = true
|
||||
UiStyle.Components.transparent(row)
|
||||
UiStyle.Components.transparent(check)
|
||||
UiStyle.Components.transparent(title)
|
||||
UiStyle.Components.transparent(head)
|
||||
UiStyle.Components.transparent(provider)
|
||||
UiStyle.Components.transparent(star)
|
||||
UiStyle.Components.transparent(row, check, title, head, provider, star)
|
||||
row.border = JBUI.Borders.empty(
|
||||
UiStyle.Space.MD,
|
||||
UiStyle.Space.LG,
|
||||
UiStyle.Space.MD,
|
||||
UiStyle.Space.LG + UiStyle.Space.SM,
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.pad(),
|
||||
)
|
||||
wrap.setContent(row)
|
||||
add(top, BorderLayout.NORTH)
|
||||
|
||||
+38
-23
@@ -4,11 +4,14 @@ import ai.kilocode.client.actions.SendPromptAction
|
||||
import ai.kilocode.client.actions.StopSessionAction
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.ReasoningPicker
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.ui.mode.ModePicker
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.iconButton
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.icons.AllIcons
|
||||
@@ -58,21 +61,19 @@ class PromptPanel(
|
||||
private val project: Project,
|
||||
private val onSend: (String) -> Unit,
|
||||
private val onAbort: () -> Unit,
|
||||
) : BorderLayoutPanel(), SessionStyleTarget, SendPromptContext {
|
||||
) : BorderLayoutPanel(), SessionEditorStyleTarget, SendPromptContext {
|
||||
|
||||
companion object {
|
||||
private val LOG = KiloLog.create(PromptPanel::class.java)
|
||||
private val SEND_ICON: Icon = IconLoader.getIcon("/icons/send.svg", PromptPanel::class.java)
|
||||
private val STOP_ICON: Icon = IconLoader.getIcon("/icons/stop.svg", PromptPanel::class.java)
|
||||
private const val ARC = 6
|
||||
private const val FOCUS = 2
|
||||
}
|
||||
|
||||
val mode = ModePicker()
|
||||
val model = ModelPicker()
|
||||
val reasoning = ReasoningPicker()
|
||||
var onReset: () -> Unit = {}
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
private val shell = PromptShell()
|
||||
private var bus: MessageBusConnection? = null
|
||||
|
||||
@@ -121,7 +122,7 @@ class PromptPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private val reset = UiStyle.Buttons.HoverIcon().apply {
|
||||
private val reset = HoverIcon().apply {
|
||||
icon = AllIcons.Actions.Cancel
|
||||
toolTipText = KiloBundle.message("model.picker.reset")
|
||||
accessibleContext.accessibleName = KiloBundle.message("model.picker.reset")
|
||||
@@ -142,7 +143,12 @@ class PromptPanel(
|
||||
init {
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLineTop(JBUI.CurrentTheme.ToolWindow.borderColor()),
|
||||
UiStyle.Insets.prompt(),
|
||||
JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING),
|
||||
),
|
||||
)
|
||||
|
||||
applyStyle(style)
|
||||
@@ -151,14 +157,14 @@ class PromptPanel(
|
||||
val bar = BorderLayoutPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.emptyTop(UiStyle.Space.SM)
|
||||
border = JBUI.Borders.emptyTop(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP))
|
||||
}
|
||||
bar.add(mode)
|
||||
bar.add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)))
|
||||
bar.add(model)
|
||||
bar.add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)))
|
||||
bar.add(reasoning)
|
||||
bar.add(Box.createHorizontalStrut(UiStyle.Gap.small()))
|
||||
bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)))
|
||||
bar.add(reset)
|
||||
bar.add(Box.createHorizontalGlue())
|
||||
bar.add(button)
|
||||
@@ -206,13 +212,13 @@ class PromptPanel(
|
||||
|
||||
internal val defaultFocusedComponent: JComponent get() = editor
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
editor.font = style.transcriptFont
|
||||
editor.getEditor(false)?.let(style::applyToEditor)
|
||||
editor.background = style.editorScheme.defaultBackground
|
||||
val height = style.transcriptFont.size * UiStyle.Size.LINES + JBUI.scale(
|
||||
UiStyle.Size.CHROME)
|
||||
val height = style.transcriptFont.size * SessionUiStyle.View.Prompt.EDITOR_LINES + JBUI.scale(
|
||||
SessionUiStyle.View.Prompt.EDITOR_CHROME)
|
||||
editor.preferredSize = JBDimension(0, height)
|
||||
editor.minimumSize = JBDimension(0, height)
|
||||
revalidate()
|
||||
@@ -303,7 +309,7 @@ class PromptPanel(
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
UiStyle.Buttons.icon(this)
|
||||
iconButton(this)
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
@@ -316,7 +322,10 @@ class PromptPanel(
|
||||
})
|
||||
}
|
||||
|
||||
override fun getPreferredSize() = JBUI.size(UiStyle.Size.BUTTON, UiStyle.Size.BUTTON)
|
||||
override fun getPreferredSize() = JBUI.size(
|
||||
SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE,
|
||||
SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE,
|
||||
)
|
||||
|
||||
override fun uiDataSnapshot(sink: DataSink) {
|
||||
sink.set(PromptDataKeys.SEND, this@PromptPanel)
|
||||
@@ -332,7 +341,7 @@ class PromptPanel(
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", ARC))
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
@@ -349,17 +358,23 @@ class PromptPanel(
|
||||
}
|
||||
|
||||
private inner class PromptShell : BorderLayoutPanel() {
|
||||
private val arc = JBValue.UIInteger("Button.arc", ARC)
|
||||
private val focus = JBValue.UIInteger("Component.focusWidth", FOCUS)
|
||||
private val arc = JBValue.UIInteger("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC)
|
||||
private val focus = JBValue.UIInteger("Component.focusWidth", SessionUiStyle.View.Prompt.FOCUS_WIDTH)
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Space.MD, UiStyle.Space.LG)
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = JBUI.Borders.empty(UiStyle.Space.MD, UiStyle.Space.LG)
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
@@ -376,7 +391,7 @@ class PromptPanel(
|
||||
g2.color = if (active) {
|
||||
JBUI.CurrentTheme.Focus.focusColor()
|
||||
} else {
|
||||
UiStyle.Colors.line()
|
||||
SessionUiStyle.View.line()
|
||||
}
|
||||
val bw = if (active) focus.get() else JBUI.scale(1)
|
||||
for (idx in 0 until bw) {
|
||||
|
||||
+15
-9
@@ -1,4 +1,4 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
package ai.kilocode.client.session.ui.style
|
||||
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.colors.EditorColorsScheme
|
||||
@@ -9,8 +9,13 @@ import java.awt.Color
|
||||
import java.awt.Font
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** Snapshot of editor-derived transcript styling applied to live session components. */
|
||||
data class SessionStyle(
|
||||
/**
|
||||
* Immutable snapshot of editor-derived fonts and colors for transcript components.
|
||||
*
|
||||
* Session UI uses this instead of reading editor globals in every component so font and color changes can be applied
|
||||
* consistently through [SessionEditorStyleTarget].
|
||||
*/
|
||||
data class SessionEditorStyle(
|
||||
val editorScheme: EditorColorsScheme,
|
||||
val editorFamily: String,
|
||||
val editorSize: Int,
|
||||
@@ -30,7 +35,8 @@ data class SessionStyle(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun current(): SessionStyle {
|
||||
/** Builds a style snapshot from the current global editor color scheme. */
|
||||
fun current(): SessionEditorStyle {
|
||||
val scheme = EditorColorsManager.getInstance().globalScheme
|
||||
return create(scheme, scheme.editorFontName, scheme.editorFontSize)
|
||||
}
|
||||
@@ -39,11 +45,11 @@ data class SessionStyle(
|
||||
scheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme,
|
||||
family: String = scheme.editorFontName,
|
||||
size: Int = scheme.editorFontSize,
|
||||
): SessionStyle {
|
||||
): SessionEditorStyle {
|
||||
val small = scaledSize(size, JBFont.small())
|
||||
val ui = JBUI.Fonts.label().deriveFont(size.toFloat())
|
||||
val smallUi = JBFont.small().deriveFont(small.toFloat())
|
||||
return SessionStyle(
|
||||
return SessionEditorStyle(
|
||||
editorScheme = scheme,
|
||||
editorFamily = family,
|
||||
editorSize = size,
|
||||
@@ -66,7 +72,7 @@ data class SessionStyle(
|
||||
}
|
||||
}
|
||||
|
||||
/** Implemented by session components that can update styling in place. */
|
||||
interface SessionStyleTarget {
|
||||
fun applyStyle(style: SessionStyle)
|
||||
/** Session component contract for applying a refreshed [SessionEditorStyle] without rebuilding Swing nodes. */
|
||||
interface SessionEditorStyleTarget {
|
||||
fun applyStyle(style: SessionEditorStyle)
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package ai.kilocode.client.session.ui.style
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.JBUI.Borders.customLine
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Color
|
||||
import javax.swing.border.Border
|
||||
|
||||
/** Static style tokens owned by the chat session UI. */
|
||||
object SessionUiStyle {
|
||||
/** Geometry for the transcript list and its scroll behavior. */
|
||||
object SessionLayout {
|
||||
const val GAP = 4
|
||||
const val TRANSCRIPT_PADDING = 12
|
||||
const val USER_PROMPT_INDENT = 100
|
||||
const val SCROLL_INCREMENT = 16
|
||||
}
|
||||
|
||||
/** Shared tokens for individual transcript views and cards. */
|
||||
object View {
|
||||
const val CARD_LAYOUT_GAP = 6
|
||||
const val CARD_VERTICAL_PADDING = 8
|
||||
const val CARD_HORIZONTAL_PADDING = 12
|
||||
const val CARD_BODY_EXTRA_HEIGHT = 16
|
||||
|
||||
internal const val BORDER_DELTA = 64
|
||||
internal const val HOVER_ALPHA = 0.35f
|
||||
|
||||
/** Creates a visible separator against editor-derived transcript surfaces. */
|
||||
fun line(): Color = JBColor.lazy { UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA) }
|
||||
|
||||
fun surface(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
fun header(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
/** Local hover color for collapsible transcript card headers. */
|
||||
fun headerHover(): Color = JBColor.lazy { UiStyle.Colors.blend(header(), line(), HOVER_ALPHA) }
|
||||
|
||||
fun card(): Border = cardBorder()
|
||||
|
||||
fun cardBorder(): Border = JBUI.Borders.customLine(line(), 1)
|
||||
|
||||
fun cardTop(): Border = JBUI.Borders.customLineTop(line())
|
||||
|
||||
/** Prompt input dimensions and chrome inside the session view. */
|
||||
object Prompt {
|
||||
const val EDITOR_LINES = 3
|
||||
const val EDITOR_CHROME = 16
|
||||
const val SEND_BUTTON_SIZE = 24
|
||||
const val CORNER_ARC = 6
|
||||
const val FOCUS_WIDTH = 2
|
||||
const val PANEL_VERTICAL_PADDING = 8
|
||||
const val PANEL_HORIZONTAL_PADDING = 12
|
||||
const val CONTROL_GAP = 4
|
||||
const val SHELL_VERTICAL_PADDING = 6
|
||||
const val SHELL_HORIZONTAL_PADDING = 8
|
||||
}
|
||||
|
||||
/** Reasoning block preview sizing. */
|
||||
object Reasoning {
|
||||
const val BODY_LINES = 5
|
||||
}
|
||||
|
||||
/** Message container roles and user bubble geometry. */
|
||||
object Message {
|
||||
const val USER_ROLE = "user"
|
||||
const val ASSISTANT_ROLE = "assistant"
|
||||
const val USER_BORDER_ARC = 8
|
||||
const val USER_BORDER_VERTICAL_PADDING = 8
|
||||
const val USER_BORDER_HORIZONTAL_PADDING = 12
|
||||
}
|
||||
|
||||
/** Tool card preview limits and state colors. */
|
||||
object Tool {
|
||||
const val BODY_LINES = 15
|
||||
const val PREVIEW_LIMIT = 20_000
|
||||
|
||||
fun pending(): Color = UiStyle.Colors.weak()
|
||||
|
||||
fun running(): Color = UiStyle.Colors.fg()
|
||||
|
||||
fun completed(): Color = UiStyle.Colors.weak()
|
||||
|
||||
fun error(): Color = UiStyle.Colors.errorLabelForeground()
|
||||
}
|
||||
}
|
||||
|
||||
/** Limits for the empty-state recent sessions list. */
|
||||
object RecentSessions {
|
||||
const val LIMIT = 5
|
||||
const val DESCRIPTION_WIDTH = 250
|
||||
}
|
||||
|
||||
/** Colors for timeline/activity indicators in the session header. */
|
||||
object Timeline {
|
||||
val READ: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff))
|
||||
val WRITE: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4))
|
||||
val TOOL: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc))
|
||||
val SUCCESS: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground())
|
||||
val ERROR: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71))
|
||||
val TEXT: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d))
|
||||
val STEP: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d))
|
||||
}
|
||||
}
|
||||
|
||||
/** Border presets for question, permission, and connection dock panels. */
|
||||
object Dock {
|
||||
fun banner(): Border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLineTop(SessionUiStyle.View.line()),
|
||||
JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()),
|
||||
)!!
|
||||
|
||||
fun neutral(): Border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(SessionUiStyle.View.line(), 1),
|
||||
JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()),
|
||||
)!!
|
||||
|
||||
fun warning(): Border = JBUI.Borders.compound(
|
||||
customLine(UiStyle.Colors.warningLabelForeground(), 1),
|
||||
JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()),
|
||||
)!!
|
||||
}
|
||||
+6
-5
@@ -3,7 +3,8 @@ package ai.kilocode.client.session.views
|
||||
import ai.kilocode.client.session.model.Compaction
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import java.awt.BorderLayout
|
||||
@@ -29,16 +30,16 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
|
||||
private val text = JBLabel(KiloBundle.message("session.part.compaction")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.empty(0, UiStyle.Gap.regular())
|
||||
border = JBUI.Borders.empty(0, UiStyle.Gap.lg())
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
|
||||
val line = { JPanel().apply {
|
||||
background = UiStyle.Colors.line()
|
||||
background = SessionUiStyle.View.line()
|
||||
isOpaque = true
|
||||
preferredSize = JBDimension(0, JBUI.scale(1))
|
||||
} }
|
||||
@@ -65,7 +66,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
|
||||
|
||||
override fun update(content: Content) {} // compaction has no mutable state
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
if (text.font == style.smallUiFont) return
|
||||
text.font = style.smallUiFont
|
||||
revalidate()
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Generic
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import java.awt.BorderLayout
|
||||
@@ -20,13 +20,13 @@ class GenericView(content: Generic) : PartView() {
|
||||
|
||||
private val label = JBLabel("[${content.type}]").apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
border = com.intellij.util.ui.JBUI.Borders.empty(UiStyle.Space.XS, 0)
|
||||
border = com.intellij.util.ui.JBUI.Borders.empty(UiStyle.Gap.xs(), 0)
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(label, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class GenericView(content: Generic) : PartView() {
|
||||
/** Exposed for tests. */
|
||||
fun labelText(): String = label.text
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
if (label.font == style.smallUiFont) return
|
||||
label.font = style.smallUiFont
|
||||
revalidate()
|
||||
|
||||
+27
-13
@@ -4,9 +4,11 @@ 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
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.ui.RoundedLineBorder
|
||||
import com.intellij.util.ui.JBUI
|
||||
|
||||
/**
|
||||
* A single message container inside a [TurnView].
|
||||
@@ -21,24 +23,26 @@ import ai.kilocode.client.ui.UiStyle
|
||||
*/
|
||||
class MessageView(
|
||||
val msg: Message,
|
||||
private var style: SessionStyle = SessionStyle.current(),
|
||||
) : ai.kilocode.client.session.ui.SessionLayoutPanel(UiStyle.Card.groupGap()), SessionStyleTarget, SessionView {
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
) : ai.kilocode.client.session.ui.SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
), SessionEditorStyleTarget, SessionView {
|
||||
|
||||
constructor(msg: Message) : this(msg, SessionStyle.current())
|
||||
constructor(msg: Message) : this(msg, SessionEditorStyle.current())
|
||||
|
||||
val role: String get() = msg.info.role
|
||||
|
||||
override val sessionViewKind: SessionView.Kind
|
||||
get() = if (role == "user") SessionView.Kind.UserPrompt else SessionView.Kind.Default
|
||||
get() = if (role == SessionUiStyle.View.Message.USER_ROLE) SessionView.Kind.UserPrompt else SessionView.Kind.Default
|
||||
|
||||
private val parts = LinkedHashMap<String, PartView>()
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
border = if (msg.info.role == "user") {
|
||||
UiStyle.Borders.user()
|
||||
border = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) {
|
||||
userBorder()
|
||||
} else {
|
||||
UiStyle.Borders.assistant()
|
||||
assistantBorder()
|
||||
}
|
||||
|
||||
// Populate content that already exists (e.g. after loadHistory)
|
||||
@@ -77,8 +81,8 @@ class MessageView(
|
||||
}
|
||||
|
||||
private fun syncBorder() {
|
||||
if (msg.info.role != "assistant") return
|
||||
border = UiStyle.Borders.assistant()
|
||||
if (msg.info.role != SessionUiStyle.View.Message.ASSISTANT_ROLE) return
|
||||
border = assistantBorder()
|
||||
}
|
||||
|
||||
/** Append a streaming delta to the renderer for [contentId]. */
|
||||
@@ -97,7 +101,7 @@ class MessageView(
|
||||
/** Compact dump for test assertions. */
|
||||
fun dump(): String = parts.values.joinToString(", ") { it.dumpLabel() }
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
for (view in parts.values) view.applyStyle(style)
|
||||
refresh()
|
||||
@@ -107,4 +111,14 @@ class MessageView(
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun userBorder() = JBUI.Borders.compound(
|
||||
RoundedLineBorder(SessionUiStyle.View.line(), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_ARC)),
|
||||
JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_HORIZONTAL_PADDING),
|
||||
),
|
||||
)!!
|
||||
|
||||
private fun assistantBorder() = JBUI.Borders.empty()
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ import javax.swing.JPanel
|
||||
*
|
||||
* All methods must be called on the EDT.
|
||||
*/
|
||||
abstract class PartView : JPanel(), SessionStyleTarget {
|
||||
abstract class PartView : JPanel(), SessionEditorStyleTarget {
|
||||
|
||||
/** Stable [Content.id] this renderer was created for. */
|
||||
abstract val contentId: String
|
||||
@@ -32,7 +32,7 @@ abstract class PartView : JPanel(), SessionStyleTarget {
|
||||
*/
|
||||
open fun appendDelta(delta: String) {}
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {}
|
||||
override fun applyStyle(style: SessionEditorStyle) {}
|
||||
|
||||
/** Readable name for test dumps. */
|
||||
open fun dumpLabel(): String = javaClass.simpleName
|
||||
|
||||
+26
-18
@@ -5,12 +5,14 @@ package ai.kilocode.client.session.views
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Reasoning
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
@@ -33,21 +35,27 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
private val arrow = JBLabel()
|
||||
private val body = TrackPanel().apply {
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.surface()
|
||||
border = UiStyle.Card.bodyInsets()
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
private val scroll = JBScrollPane(body).apply {
|
||||
border = UiStyle.Card.divider()
|
||||
border = SessionUiStyle.View.cardTop()
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.surface()
|
||||
viewport.background = UiStyle.Colors.surface()
|
||||
background = SessionUiStyle.View.surface()
|
||||
viewport.background = SessionUiStyle.View.surface()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
private val header = JPanel(UiStyle.Card.layout()).apply {
|
||||
private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply {
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.header()
|
||||
border = UiStyle.Card.headerInsets()
|
||||
background = SessionUiStyle.View.header()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
private val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
@@ -56,7 +64,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
}
|
||||
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var source = reasoning.content.toString()
|
||||
|
||||
private val click = object : MouseAdapter() {
|
||||
@@ -80,9 +88,9 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
border = UiStyle.Card.border()
|
||||
border = SessionUiStyle.View.card()
|
||||
|
||||
val left = JPanel(UiStyle.Card.layout()).apply {
|
||||
val left = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
add(icon, BorderLayout.WEST)
|
||||
add(title, BorderLayout.CENTER)
|
||||
@@ -95,7 +103,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
it.addMouseListener(mouse)
|
||||
}
|
||||
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
md.opaque = false
|
||||
md.set(source)
|
||||
body.add(md.component, BorderLayout.CENTER)
|
||||
@@ -142,11 +150,11 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
|
||||
internal fun horizontalPolicy() = scroll.horizontalScrollBarPolicy
|
||||
|
||||
internal fun bodyMaxRows() = UiStyle.Card.REASONING_LINES
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Reasoning.BODY_LINES
|
||||
|
||||
internal fun bodyCreated() = true
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
if (title.font != style.smallEditorFont) {
|
||||
@@ -172,7 +180,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
}
|
||||
|
||||
private fun setHover(value: Boolean) {
|
||||
val color = if (value) UiStyle.Colors.headerHover() else UiStyle.Colors.header()
|
||||
val color = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header()
|
||||
if (header.background?.rgb == color.rgb) return
|
||||
header.background = color
|
||||
header.repaint()
|
||||
@@ -254,7 +262,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() {
|
||||
}
|
||||
|
||||
private fun bodyMaxHeight(): Int = md.component.getFontMetrics(md.font).height * bodyMaxRows() +
|
||||
UiStyle.Card.scrollChrome()
|
||||
JBUI.scale(SessionUiStyle.View.CARD_BODY_EXTRA_HEIGHT)
|
||||
|
||||
override fun dumpLabel(): String {
|
||||
val state = if (bodyVisible()) "open" else "closed"
|
||||
@@ -270,7 +278,7 @@ private class TrackPanel : JPanel(BorderLayout()), Scrollable {
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
direction: Int,
|
||||
) = UiStyle.Gap.scroll()
|
||||
) = JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_INCREMENT)
|
||||
override fun getScrollableBlockIncrement(
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Text
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import java.awt.BorderLayout
|
||||
|
||||
@@ -20,7 +20,7 @@ class TextView(text: Text) : PartView() {
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
if (text.content.isNotEmpty()) md.set(text.content.toString())
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class TextView(text: Text) : PartView() {
|
||||
/** Current markdown source — used by tests to assert rendered content. */
|
||||
fun markdown(): String = md.markdown()
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
val changed = md.font != style.transcriptFont || md.codeFont != style.editorFamily
|
||||
if (md.font != style.transcriptFont) md.font = style.transcriptFont
|
||||
if (md.codeFont != style.editorFamily) md.codeFont = style.editorFamily
|
||||
|
||||
+34
-25
@@ -6,12 +6,14 @@ import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
@@ -34,17 +36,20 @@ class ToolView(tool: Tool) : PartView() {
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionStyle.current()
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
private val root = JPanel(BorderLayout()).apply {
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.surface()
|
||||
border = UiStyle.Card.border()
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = SessionUiStyle.View.card()
|
||||
}
|
||||
private val header = JPanel(UiStyle.Card.layout()).apply {
|
||||
private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply {
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.header()
|
||||
border = UiStyle.Card.headerInsets()
|
||||
background = SessionUiStyle.View.header()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
private val glyph = JBLabel()
|
||||
private val title = JBLabel()
|
||||
@@ -55,7 +60,7 @@ class ToolView(tool: Tool) : PartView() {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
}
|
||||
private val arrow = JBLabel()
|
||||
private val center = JPanel(UiStyle.Card.layout()).apply {
|
||||
private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
private val controls: JComponent = Box.createHorizontalBox().apply {
|
||||
@@ -69,14 +74,17 @@ class ToolView(tool: Tool) : PartView() {
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
foreground = bodyColor()
|
||||
background = UiStyle.Colors.surface()
|
||||
border = UiStyle.Card.bodyInsets()
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
private val scroll = JBScrollPane(text).apply {
|
||||
border = UiStyle.Card.divider()
|
||||
border = SessionUiStyle.View.cardTop()
|
||||
isOpaque = true
|
||||
background = UiStyle.Colors.surface()
|
||||
viewport.background = UiStyle.Colors.surface()
|
||||
background = SessionUiStyle.View.surface()
|
||||
viewport.background = SessionUiStyle.View.surface()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
@@ -114,7 +122,7 @@ class ToolView(tool: Tool) : PartView() {
|
||||
it.addMouseListener(click)
|
||||
}
|
||||
text.text = preview(item)
|
||||
applyStyle(SessionStyle.current())
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(root, BorderLayout.CENTER)
|
||||
sync()
|
||||
}
|
||||
@@ -171,11 +179,11 @@ class ToolView(tool: Tool) : PartView() {
|
||||
|
||||
internal fun bodyWrap() = text.lineWrap
|
||||
|
||||
internal fun bodyMaxRows() = UiStyle.Card.LINES
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
|
||||
internal fun bodyCreated() = true
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
changed = setFont(title, style.boldEditorFont) || changed
|
||||
@@ -193,7 +201,7 @@ class ToolView(tool: Tool) : PartView() {
|
||||
}
|
||||
|
||||
private fun setHover(value: Boolean) {
|
||||
val color = if (value) UiStyle.Colors.headerHover() else UiStyle.Colors.header()
|
||||
val color = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header()
|
||||
if (same(header.background, color)) return
|
||||
header.background = color
|
||||
header.repaint()
|
||||
@@ -279,10 +287,11 @@ class ToolView(tool: Tool) : PartView() {
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.error() else UiStyle.Colors.fg()
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
return text.getFontMetrics(text.font).height * bodyMaxRows() + UiStyle.Card.scrollChrome()
|
||||
return text.getFontMetrics(text.font).height * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.CARD_BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "ToolView#$contentId(${labelText()})"
|
||||
@@ -345,14 +354,14 @@ private fun setFont(component: JComponent, font: Font): Boolean {
|
||||
private fun same(a: Color?, b: Color): Boolean = a?.rgb == b.rgb
|
||||
|
||||
private fun color(tool: Tool) = when (tool.state) {
|
||||
ToolExecState.PENDING -> UiStyle.Colors.weak()
|
||||
ToolExecState.RUNNING -> UiStyle.Colors.running()
|
||||
ToolExecState.COMPLETED -> UiStyle.Colors.weak()
|
||||
ToolExecState.ERROR -> UiStyle.Colors.error()
|
||||
ToolExecState.PENDING -> SessionUiStyle.View.Tool.pending()
|
||||
ToolExecState.RUNNING -> SessionUiStyle.View.Tool.running()
|
||||
ToolExecState.COMPLETED -> SessionUiStyle.View.Tool.completed()
|
||||
ToolExecState.ERROR -> SessionUiStyle.View.Tool.error()
|
||||
}
|
||||
|
||||
private fun titleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) {
|
||||
UiStyle.Colors.error()
|
||||
UiStyle.Colors.errorLabelForeground()
|
||||
} else {
|
||||
UiStyle.Colors.fg()
|
||||
}
|
||||
@@ -475,7 +484,7 @@ private class Preview {
|
||||
|
||||
fun append(value: String): Preview {
|
||||
if (cut) return this
|
||||
val rem = UiStyle.Size.toolBodyLimit() - text.length
|
||||
val rem = SessionUiStyle.View.Tool.PREVIEW_LIMIT - text.length
|
||||
if (value.length <= rem) {
|
||||
text.append(value)
|
||||
return this
|
||||
|
||||
+8
-7
@@ -2,9 +2,10 @@ package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Message
|
||||
import ai.kilocode.client.session.ui.SessionLayoutPanel
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.session.ui.SessionStyleTarget
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
|
||||
/**
|
||||
* Top-level transcript item representing one conversational turn.
|
||||
@@ -17,10 +18,10 @@ import ai.kilocode.client.ui.UiStyle
|
||||
*/
|
||||
class TurnView(
|
||||
val id: String,
|
||||
private var style: SessionStyle = SessionStyle.current(),
|
||||
) : SessionLayoutPanel(UiStyle.Card.groupGap()), SessionStyleTarget {
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), SessionEditorStyleTarget {
|
||||
|
||||
constructor(id: String) : this(id, SessionStyle.current())
|
||||
constructor(id: String) : this(id, SessionEditorStyle.current())
|
||||
|
||||
private val messages = LinkedHashMap<String, MessageView>()
|
||||
|
||||
@@ -53,7 +54,7 @@ class TurnView(
|
||||
/** Compact dump for test assertions. */
|
||||
fun dump(): String = messages.entries.joinToString(", ") { (id, mv) -> "${mv.role}#$id" }
|
||||
|
||||
override fun applyStyle(style: SessionStyle) {
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
for (view in messages.values) view.applyStyle(style)
|
||||
revalidate()
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package ai.kilocode.client.ui
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* Centers its single child and shrinks it to available space when needed.
|
||||
* If available space is larger than the child's maximum size, the child is not expanded.
|
||||
*/
|
||||
class CenterShrinkPanel(child: Component) : JPanel(null) {
|
||||
init {
|
||||
isOpaque = false
|
||||
add(child)
|
||||
}
|
||||
|
||||
override fun doLayout() {
|
||||
if (componentCount == 0) return
|
||||
val child = getComponent(0)
|
||||
val insets = getInsets()
|
||||
val availW = width - insets.left - insets.right
|
||||
val availH = height - insets.top - insets.bottom
|
||||
val pref = child.preferredSize
|
||||
val max = child.maximumSize
|
||||
val w = minOf(pref.width, max.width, availW)
|
||||
val h = minOf(pref.height, max.height, availH)
|
||||
val x = insets.left + (availW - w) / 2
|
||||
val y = insets.top + (availH - h) / 2
|
||||
child.setBounds(x, y, w, h)
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
if (componentCount == 0) return super.getPreferredSize()
|
||||
val pref = getComponent(0).preferredSize
|
||||
val insets = getInsets()
|
||||
return Dimension(pref.width + insets.left + insets.right, pref.height + insets.top + insets.bottom)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package ai.kilocode.client.ui
|
||||
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JButton
|
||||
|
||||
class HoverIcon : JButton() {
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
iconButton(this)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension = JBUI.size(24, 24)
|
||||
|
||||
override fun getMinimumSize(): Dimension = preferredSize
|
||||
|
||||
override fun getMaximumSize(): Dimension = preferredSize
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (isEnabled && over) paintHover(g)
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun paintHover(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
fun iconButton(button: JButton) {
|
||||
button.isFocusable = false
|
||||
button.setRequestFocusEnabled(false)
|
||||
button.isContentAreaFilled = false
|
||||
button.isBorderPainted = false
|
||||
button.isOpaque = false
|
||||
button.border = JBUI.Borders.empty()
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package ai.kilocode.client.ui
|
||||
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.UIManager
|
||||
|
||||
open class PickerButton : JBLabel() {
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
border = pickerBorder()
|
||||
background = picker()
|
||||
// The custom rounded fill needs parent background around the corners.
|
||||
isOpaque = false
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = pickerBorder()
|
||||
background = picker()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = if (isEnabled && over) JBUI.CurrentTheme.ActionButton.hoverBackground() else picker()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun picker() = JBColor.lazy {
|
||||
UIManager.getColor("ComboBoxButton.background")
|
||||
?: UIManager.getColor("ComboBox.nonEditableBackground")
|
||||
?: UIUtil.getPanelBackground()
|
||||
}
|
||||
|
||||
private fun pickerBorder() = JBUI.Borders.empty(UiStyle.Gap.xs(), UiStyle.Gap.lg())
|
||||
}
|
||||
+17
-272
@@ -2,104 +2,47 @@ package ai.kilocode.client.ui
|
||||
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.RoundedLineBorder
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
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 java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.border.Border
|
||||
|
||||
/** Static UI tokens and helpers for JetBrains session Swing surfaces. */
|
||||
/** Shared Swing style tokens that are not tied to one session component. */
|
||||
object UiStyle {
|
||||
object Size {
|
||||
const val WIDTH = 350
|
||||
const val LIMIT = 5
|
||||
const val LINES = 3
|
||||
const val CHROME = 16
|
||||
const val BUTTON_WIDTH = 28
|
||||
const val BUTTON = 24
|
||||
const val SCROLL = 16
|
||||
const val USER_PROMPT = 100
|
||||
const val TOOL_BODY = 20_000
|
||||
|
||||
fun userPromptMin(): Int = JBUI.scale(USER_PROMPT)
|
||||
/** DPI-aware spacing primitives used across custom Swing layouts. */
|
||||
object Gap {
|
||||
fun xs() = JBUI.scale(2)
|
||||
|
||||
fun toolBodyLimit(): Int = TOOL_BODY
|
||||
}
|
||||
fun md() = JBUI.scale(6)
|
||||
|
||||
object Space {
|
||||
const val XS = 2
|
||||
const val SM = 4
|
||||
const val MD = 6
|
||||
const val LG = 8
|
||||
const val PAD = 12
|
||||
const val LOGO = 14
|
||||
const val RECENT = 28
|
||||
fun lg() = JBUI.scale(8)
|
||||
|
||||
fun sm() = JBUI.scale(4)
|
||||
|
||||
fun pad() = JBUI.scale(12)
|
||||
}
|
||||
|
||||
/** Theme-aware colors and color math used by multiple UI surfaces. */
|
||||
object Colors {
|
||||
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()
|
||||
|
||||
fun weak(): Color = UIUtil.getContextHelpForeground()
|
||||
|
||||
/** Creates a visible separator against editor-derived transcript surfaces. */
|
||||
fun line(): Color = JBColor.lazy { contrast(panel(), BORDER_DELTA) }
|
||||
|
||||
fun surface(): Color = panel()
|
||||
|
||||
/** Uses the editor background so chat cards feel native beside editor content. */
|
||||
fun panel(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground }
|
||||
fun editorBackground(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground }
|
||||
|
||||
fun panelHover(): Color = JBColor.lazy { blend(panel(), line(), HOVER_ALPHA) }
|
||||
fun errorLabelForeground(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground())
|
||||
|
||||
fun header(): Color = panel()
|
||||
|
||||
fun headerBar(): Color = JBUI.CurrentTheme.ToolWindow.headerBackground(false)
|
||||
|
||||
/** Local hover color for collapsible transcript card headers. */
|
||||
fun headerHover(): Color = panelHover()
|
||||
|
||||
fun error(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground())
|
||||
|
||||
fun warning(): Color = JBColor.lazy {
|
||||
fun warningLabelForeground(): Color = JBColor.lazy {
|
||||
UIManager.getColor("Component.warningFocusColor")
|
||||
?: UIManager.getColor("Label.warningForeground")
|
||||
?: UIUtil.getContextHelpForeground()
|
||||
}
|
||||
|
||||
fun running(): Color = JBColor.namedColor("ProgressBar.foreground", UIUtil.getLabelForeground())
|
||||
|
||||
fun picker(): Color = JBColor.lazy {
|
||||
UIManager.getColor("ComboBoxButton.background")
|
||||
?: UIManager.getColor("ComboBox.nonEditableBackground")
|
||||
?: UIUtil.getPanelBackground()
|
||||
}
|
||||
|
||||
fun pickerHover(): Color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
|
||||
internal fun contrast(base: Color, delta: Int): Color {
|
||||
val step = if (bright(base)) -delta else delta
|
||||
return Color(
|
||||
@@ -124,208 +67,10 @@ object UiStyle {
|
||||
(color.red * 0.299 + color.green * 0.587 + color.blue * 0.114) >= 128
|
||||
}
|
||||
|
||||
object Insets {
|
||||
fun none(): java.awt.Insets = JBUI.emptyInsets()
|
||||
|
||||
fun transcript(): java.awt.Insets = JBUI.insets(Space.PAD, Space.PAD, Space.PAD, Space.PAD)
|
||||
|
||||
fun userPrompt(): Int = Size.userPromptMin()
|
||||
|
||||
fun empty(): Border = JBUI.Borders.empty(Space.PAD)
|
||||
|
||||
fun prompt(): Border = JBUI.Borders.empty(Space.LG, Space.PAD, Space.LG, Space.PAD)
|
||||
|
||||
fun header(): Border = JBUI.Borders.empty(Space.LG, Space.LG)
|
||||
|
||||
fun body(): Border = JBUI.Borders.empty(Space.LG, Space.PAD)
|
||||
}
|
||||
|
||||
object Borders {
|
||||
fun card(): Border = cardBorder()
|
||||
|
||||
fun cardBorder(): Border = JBUI.Borders.customLine(Colors.line(), 1)
|
||||
|
||||
fun cardTop(): Border = JBUI.Borders.customLineTop(Colors.line())
|
||||
|
||||
fun warning(): Border = JBUI.Borders.customLine(Colors.warning(), 1)
|
||||
|
||||
fun picker(): Border = JBUI.Borders.empty(Space.XS, Space.LG)
|
||||
|
||||
fun user(): Border = JBUI.Borders.compound(
|
||||
RoundedLineBorder(Colors.line(), JBUI.scale(Space.LG)),
|
||||
JBUI.Borders.empty(Space.LG, Space.PAD),
|
||||
)!!
|
||||
|
||||
fun assistant(): Border = JBUI.Borders.empty()
|
||||
}
|
||||
|
||||
/** Shared geometry for collapsible transcript cards such as tools and reasoning. */
|
||||
object Card {
|
||||
const val LINES = 15
|
||||
const val REASONING_LINES = 5
|
||||
|
||||
fun layout(): BorderLayout = Gap.layout(Space.MD)
|
||||
|
||||
fun groupGap(): Int = Gap.small()
|
||||
|
||||
fun headerInsets(): Border = JBUI.Borders.empty(Space.LG, Space.PAD)
|
||||
|
||||
fun bodyInsets(): Border = JBUI.Borders.empty(Space.LG, Space.PAD)
|
||||
|
||||
fun border(): Border = Borders.card()
|
||||
|
||||
fun divider(): Border = Borders.cardTop()
|
||||
|
||||
fun scrollChrome(): Int = JBUI.scale(Size.CHROME)
|
||||
}
|
||||
|
||||
object Dock {
|
||||
fun banner(): Border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLineTop(Colors.line()),
|
||||
JBUI.Borders.empty(Space.SM, Space.LG, 0, Space.LG),
|
||||
)!!
|
||||
|
||||
fun neutral(): Border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(Colors.line(), 1),
|
||||
JBUI.Borders.empty(Space.LG, Space.PAD),
|
||||
)!!
|
||||
|
||||
fun warning(): Border = JBUI.Borders.compound(
|
||||
Borders.warning(),
|
||||
JBUI.Borders.empty(Space.LG, Space.PAD),
|
||||
)!!
|
||||
}
|
||||
|
||||
object Gap {
|
||||
fun xs() = JBUI.scale(Space.XS)
|
||||
|
||||
fun inline() = JBUI.scale(Space.MD)
|
||||
|
||||
fun regular() = JBUI.scale(Space.LG)
|
||||
|
||||
fun small() = JBUI.scale(Space.SM)
|
||||
|
||||
fun turn() = JBUI.scale(Space.PAD)
|
||||
|
||||
fun part() = JBUI.scale(Space.SM)
|
||||
|
||||
fun scroll() = JBUI.scale(Size.SCROLL)
|
||||
|
||||
fun layout(gap: Int = Space.LG) = BorderLayout(JBUI.scale(gap), 0)
|
||||
}
|
||||
|
||||
object Buttons {
|
||||
class HoverIcon : JButton() {
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setRequestFocusEnabled(false)
|
||||
isContentAreaFilled = false
|
||||
isBorderPainted = false
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty()
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension = JBUI.size(Size.BUTTON, Size.BUTTON)
|
||||
|
||||
override fun getMinimumSize(): Dimension = preferredSize
|
||||
|
||||
override fun getMaximumSize(): Dimension = preferredSize
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
if (isEnabled && over) paintHover(g)
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun paintHover(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
fun icon(button: JButton) {
|
||||
button.isFocusable = false
|
||||
button.setRequestFocusEnabled(false)
|
||||
button.isContentAreaFilled = false
|
||||
button.isBorderPainted = false
|
||||
button.isOpaque = false
|
||||
button.border = JBUI.Borders.empty()
|
||||
}
|
||||
}
|
||||
|
||||
object Pickers {
|
||||
open class Label : JBLabel() {
|
||||
private var over = false
|
||||
|
||||
init {
|
||||
border = Borders.picker()
|
||||
background = Colors.picker()
|
||||
// The custom rounded fill needs parent background around the corners.
|
||||
isOpaque = false
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
sync(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
sync(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = Borders.picker()
|
||||
background = Colors.picker()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = if (isEnabled && over) Colors.pickerHover() else Colors.picker()
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", 6))
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
private fun sync(value: Boolean) {
|
||||
if (over == value) return
|
||||
over = value
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Small component helpers that keep repeated Swing setup in one place. */
|
||||
object Components {
|
||||
fun transparent(component: JComponent) {
|
||||
component.isOpaque = false
|
||||
fun transparent(vararg components: JComponent) {
|
||||
components.forEach { it.isOpaque = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
class="ai.kilocode.client.actions.KiloSettingsAction"
|
||||
icon="AllIcons.General.GearPlain"/>
|
||||
|
||||
<action id="Kilo.History"
|
||||
class="ai.kilocode.client.actions.HistoryAction"/>
|
||||
|
||||
<action id="Kilo.SendPrompt"
|
||||
class="ai.kilocode.client.actions.SendPromptAction"
|
||||
text="Send Prompt"
|
||||
|
||||
@@ -8,10 +8,7 @@ session.connection.warning.config=Configuration warnings
|
||||
session.empty.welcome=Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.
|
||||
session.empty.loading=Loading...
|
||||
session.empty.recent=RECENT
|
||||
session.empty.time.moments=Moments ago
|
||||
session.empty.time.minutes={0} min ago
|
||||
session.empty.time.hours={0}h ago
|
||||
session.empty.time.days={0}d ago
|
||||
session.showHistory=Show History
|
||||
session.scroll.bottom=Scroll to bottom
|
||||
session.tab.new=New Session
|
||||
session.tab.untitled=Untitled Session
|
||||
@@ -85,10 +82,40 @@ model.picker.free=Free
|
||||
model.picker.reset=Reset model to default
|
||||
reasoning.picker.tooltip=Select reasoning effort
|
||||
|
||||
history.tab.local=Local
|
||||
history.tab.cloud=Cloud
|
||||
history.back=Back
|
||||
history.search.placeholder=Search sessions
|
||||
history.empty=No sessions
|
||||
history.loading=Loading...
|
||||
history.untitled=Untitled
|
||||
history.delete.text=Delete
|
||||
history.delete.confirm.title=Delete session?
|
||||
history.delete.confirm.message=Delete "{0}" from local history?
|
||||
history.cloud.load.more=Load more
|
||||
history.cloud.repo.only=Only this repository
|
||||
history.group.today=Today
|
||||
history.group.yesterday=Yesterday
|
||||
history.group.week=This Week
|
||||
history.group.month=This Month
|
||||
history.group.older=Older
|
||||
history.time.moments=Moments ago
|
||||
history.time.minutes={0} min ago
|
||||
history.time.hours={0}h ago
|
||||
history.time.days={0}d ago
|
||||
history.time.months={0}mo ago
|
||||
history.time.years={0}y ago
|
||||
history.error.local=Failed to load local history
|
||||
history.error.cloud=Failed to load cloud history
|
||||
history.error.local.delete=Failed to delete session
|
||||
history.error.cloud.delete=Cloud sessions cannot be deleted yet
|
||||
|
||||
action.Kilo.Settings.text=Settings
|
||||
action.Kilo.Settings.description=Kilo Code settings
|
||||
action.Kilo.NewSession.text=New Session
|
||||
action.Kilo.NewSession.description=Start a new Kilo session
|
||||
action.Kilo.History.text=History
|
||||
action.Kilo.History.description=Show session history
|
||||
action.Kilo.SendPrompt.text=Send Prompt
|
||||
action.Kilo.SendPrompt.description=Send the current Kilo prompt
|
||||
action.Kilo.StopSession.text=Stop Session
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.session.SessionManager
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.actionSystem.Presentation
|
||||
@@ -49,7 +49,10 @@ class NewSessionActionTest : BasePlatformTestCase() {
|
||||
created++
|
||||
}
|
||||
|
||||
override fun openSession(session: SessionDto) {
|
||||
override fun showHistory() {
|
||||
}
|
||||
|
||||
override fun openSession(ref: SessionRef) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package ai.kilocode.client.session
|
||||
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
import junit.framework.TestCase
|
||||
|
||||
class SessionRefTest : TestCase() {
|
||||
|
||||
fun `test local ref exposes type id and key`() {
|
||||
val ref = SessionRef.Local("ses_test")
|
||||
|
||||
assertEquals(SessionRef.Type.LOCAL, ref.type)
|
||||
assertEquals("ses_test", ref.id)
|
||||
assertEquals("ses_test", ref.key)
|
||||
assertNull(ref.session)
|
||||
}
|
||||
|
||||
fun `test metadata local ref keeps id identity`() {
|
||||
val dto = session("ses_test")
|
||||
val ref = SessionRef.Local(dto)
|
||||
|
||||
assertEquals(SessionRef.Type.LOCAL, ref.type)
|
||||
assertEquals("ses_test", ref.id)
|
||||
assertEquals("ses_test", ref.key)
|
||||
assertSame(dto, ref.session)
|
||||
}
|
||||
|
||||
fun `test cloud ref exposes type id and prefixed key`() {
|
||||
val dto = cloud("cloud_1")
|
||||
val ref = SessionRef.Cloud(dto)
|
||||
|
||||
assertEquals(SessionRef.Type.CLOUD, ref.type)
|
||||
assertEquals("cloud_1", ref.id)
|
||||
assertEquals("cloud:cloud_1", ref.key)
|
||||
assertSame(dto, ref.session)
|
||||
}
|
||||
|
||||
fun `test ref parser resolves cloud and local ids`() {
|
||||
assertEquals(SessionRef.Cloud("cloud_1"), SessionRef.from("cloud:cloud_1"))
|
||||
assertEquals(SessionRef.Local("ses_test"), SessionRef.from("ses_test"))
|
||||
assertNull(SessionRef.from(null))
|
||||
assertNull(SessionRef.from(""))
|
||||
}
|
||||
|
||||
private fun session(id: String) = SessionDto(
|
||||
id = id,
|
||||
projectID = "prj",
|
||||
directory = "/test",
|
||||
title = "Session $id",
|
||||
version = "1",
|
||||
time = SessionTimeDto(created = 1.0, updated = 2.0),
|
||||
)
|
||||
|
||||
private fun cloud(id: String) = CloudSessionDto(
|
||||
id = id,
|
||||
title = "Cloud $id",
|
||||
createdAt = "2026-01-01T00:00:00Z",
|
||||
updatedAt = "2026-01-02T00:00:00Z",
|
||||
version = 1.0,
|
||||
)
|
||||
}
|
||||
+215
-12
@@ -4,9 +4,11 @@ import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.session.history.HistoryPanel
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeSessionRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
@@ -19,6 +21,8 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
@@ -31,7 +35,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
private lateinit var app: KiloAppService
|
||||
private val managers = mutableListOf<SessionSidePanelManager>()
|
||||
private val created = mutableListOf<Pair<String, String?>>()
|
||||
private val loading = mutableListOf<Boolean>()
|
||||
private val refs = mutableListOf<SessionRef?>()
|
||||
private val ui = mutableListOf<SessionUi>()
|
||||
|
||||
override fun setUp() {
|
||||
@@ -78,7 +82,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
|
||||
assertNotSame(first, second)
|
||||
assertEquals(listOf("/test" to null, "/test" to null), created)
|
||||
assertEquals(listOf(true, false), loading)
|
||||
}
|
||||
|
||||
fun `test new session on blank session keeps active component`() {
|
||||
@@ -91,7 +94,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
|
||||
assertSame(first, second)
|
||||
assertEquals(listOf("/test" to null), created)
|
||||
assertEquals(listOf(true), loading)
|
||||
}
|
||||
|
||||
fun `test default focused component tracks active session`() {
|
||||
@@ -107,6 +109,16 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
assertNotSame(first.defaultFocusedComponent, manager.defaultFocusedComponent)
|
||||
}
|
||||
|
||||
fun `test default focused component tracks history filter`() {
|
||||
val manager = manager()
|
||||
|
||||
manager.showHistory()
|
||||
settle()
|
||||
val history = active(manager) as HistoryPanel
|
||||
|
||||
assertSame(history.defaultFocusedComponent, manager.defaultFocusedComponent)
|
||||
}
|
||||
|
||||
fun `test opening same existing session reuses component`() {
|
||||
val manager = manager()
|
||||
val session = session("ses_1")
|
||||
@@ -119,7 +131,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
|
||||
assertSame(first, second)
|
||||
assertEquals(listOf("/test" to "ses_1", "/test" to null), created)
|
||||
assertEquals(listOf(false, false), loading)
|
||||
}
|
||||
|
||||
fun `test prompted blank session is reused from recents`() {
|
||||
@@ -157,7 +168,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
manager.openSession(session("ses_1", "/repo"))
|
||||
|
||||
assertEquals(listOf("/repo" to "ses_1"), created)
|
||||
assertEquals(listOf(false), loading)
|
||||
}
|
||||
|
||||
fun `test open session seeds session metadata into ui`() {
|
||||
@@ -174,7 +184,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
manager.openSession(session("ses_1"))
|
||||
val first = active(manager) as SessionUi
|
||||
manager.openSession(session("ses_2"))
|
||||
val style = ai.kilocode.client.session.ui.SessionStyle.create(family = "Courier New", size = 24)
|
||||
val style = ai.kilocode.client.session.ui.style.SessionEditorStyle.create(family = "Courier New", size = 24)
|
||||
|
||||
first.applyStyle(style)
|
||||
manager.openSession(session("ses_1"))
|
||||
@@ -193,19 +203,192 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
assertEquals(0, manager.component.componentCount)
|
||||
}
|
||||
|
||||
private fun manager(): SessionSidePanelManager {
|
||||
fun `test show history swaps active component`() {
|
||||
val history = JLabel("History")
|
||||
val manager = manager(history = { _, _, _ -> history })
|
||||
|
||||
manager.newSession()
|
||||
manager.showHistory()
|
||||
|
||||
assertSame(history, manager.component.getComponent(0))
|
||||
assertNull(manager.defaultFocusedComponent)
|
||||
}
|
||||
|
||||
fun `test history back restores latest open session`() {
|
||||
val manager = manager()
|
||||
|
||||
manager.openSession(session("ses_1"))
|
||||
val first = active(manager)
|
||||
manager.showHistory()
|
||||
back(manager)
|
||||
|
||||
assertSame(first, active(manager))
|
||||
}
|
||||
|
||||
fun `test history back without latest session opens new session`() {
|
||||
val manager = manager()
|
||||
|
||||
manager.showHistory()
|
||||
back(manager)
|
||||
|
||||
assertTrue(active(manager) is SessionUi)
|
||||
assertEquals(listOf("/test" to null), created)
|
||||
}
|
||||
|
||||
fun `test history back ignores deleted latest session`() {
|
||||
val manager = manager()
|
||||
|
||||
manager.openSession(session("ses_1"))
|
||||
manager.showHistory()
|
||||
remove(manager, "ses_1")
|
||||
back(manager)
|
||||
|
||||
assertTrue(active(manager) is SessionUi)
|
||||
assertEquals(listOf("/test" to "ses_1", "/test" to null), created)
|
||||
}
|
||||
|
||||
fun `test opening local history item shows session ui`() {
|
||||
lateinit var open: (SessionRef) -> Unit
|
||||
val history = JLabel("History")
|
||||
val manager = manager(history = { _, fn, _ ->
|
||||
open = fn
|
||||
history
|
||||
})
|
||||
|
||||
manager.showHistory()
|
||||
open(SessionRef.Local(session("ses_1")))
|
||||
|
||||
assertTrue(active(manager) is SessionUi)
|
||||
assertEquals(listOf("/test" to "ses_1"), created)
|
||||
}
|
||||
|
||||
fun `test opening cloud history item shows session ui before import`() {
|
||||
lateinit var open: (SessionRef) -> Unit
|
||||
rpc.historyGate = kotlinx.coroutines.CompletableDeferred()
|
||||
rpc.importedCloudSession = session("ses_imported")
|
||||
val manager = manager(history = { _, fn, _ ->
|
||||
open = fn
|
||||
JLabel("History")
|
||||
})
|
||||
|
||||
manager.showHistory()
|
||||
open(SessionRef.Cloud(cloud("cloud_1")))
|
||||
settle()
|
||||
|
||||
assertTrue(active(manager) is SessionUi)
|
||||
assertEquals(listOf("/test" to "cloud:cloud_1"), created)
|
||||
assertEquals("cloud:cloud_1", refs.single()?.key)
|
||||
assertEquals(listOf("cloud_1" to "/test"), rpc.imports)
|
||||
|
||||
rpc.historyGate?.complete(Unit)
|
||||
settle()
|
||||
assertSame(active(manager), ui.single())
|
||||
assertEquals(listOf("/test" to "cloud:cloud_1"), created)
|
||||
}
|
||||
|
||||
fun `test opening same cloud session while in-flight reuses existing ui`() {
|
||||
rpc.historyGate = kotlinx.coroutines.CompletableDeferred()
|
||||
rpc.importedCloudSession = session("ses_imported")
|
||||
val manager = manager()
|
||||
val ref = SessionRef.Cloud(cloud("cloud_1"))
|
||||
|
||||
manager.openSession(ref)
|
||||
val first = active(manager)
|
||||
manager.newSession()
|
||||
manager.openSession(ref)
|
||||
val second = active(manager)
|
||||
|
||||
assertSame(first, second)
|
||||
assertEquals(listOf("/test" to "cloud:cloud_1", "/test" to null), created)
|
||||
|
||||
rpc.historyGate!!.complete(Unit)
|
||||
settle()
|
||||
}
|
||||
|
||||
fun `test imported cloud session is reused when opened as local`() {
|
||||
rpc.importedCloudSession = session("ses_imported")
|
||||
val manager = manager()
|
||||
|
||||
manager.openSession(SessionRef.Cloud(cloud("cloud_1")))
|
||||
settle()
|
||||
val first = active(manager)
|
||||
manager.openSession(session("ses_imported"))
|
||||
val second = active(manager)
|
||||
|
||||
assertSame(first, second)
|
||||
assertEquals(listOf("/test" to "cloud:cloud_1"), created)
|
||||
}
|
||||
|
||||
fun `test new session from history shows blank session`() {
|
||||
val history = JLabel("History")
|
||||
val manager = manager(history = { _, _, _ -> history })
|
||||
|
||||
manager.showHistory()
|
||||
manager.newSession()
|
||||
|
||||
assertTrue(active(manager) is SessionUi)
|
||||
assertEquals(listOf("/test" to null), created)
|
||||
}
|
||||
|
||||
fun `test opening same local session while in-flight reuses existing ui`() {
|
||||
val gate = kotlinx.coroutines.CompletableDeferred<Unit>()
|
||||
rpc.historyGate = gate
|
||||
val manager = manager()
|
||||
val session = session("ses_1")
|
||||
|
||||
manager.openSession(session)
|
||||
val first = active(manager)
|
||||
// Open the same session again while history is still loading
|
||||
manager.newSession()
|
||||
manager.openSession(session)
|
||||
val second = active(manager)
|
||||
|
||||
assertSame("in-flight session must be reused", first, second)
|
||||
assertEquals(listOf("/test" to "ses_1", "/test" to null), created)
|
||||
|
||||
gate.complete(Unit)
|
||||
settle()
|
||||
}
|
||||
|
||||
fun `test deleted cached history session is not reused`() {
|
||||
lateinit var deleted: (String) -> Unit
|
||||
val manager = manager(history = { _, _, fn ->
|
||||
deleted = fn
|
||||
JLabel("History")
|
||||
})
|
||||
val session = session("ses_1")
|
||||
|
||||
manager.openSession(session)
|
||||
val first = active(manager)
|
||||
manager.showHistory()
|
||||
deleted("ses_1")
|
||||
manager.openSession(session)
|
||||
|
||||
assertNotSame(first, active(manager))
|
||||
assertEquals(listOf("/test" to "ses_1", "/test" to "ses_1"), created)
|
||||
}
|
||||
|
||||
private fun manager(
|
||||
history: ((com.intellij.openapi.Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null,
|
||||
): SessionSidePanelManager {
|
||||
val manager = SessionSidePanelManager(
|
||||
project = project,
|
||||
root = workspace,
|
||||
create = { project, workspace, owner, id, show, session ->
|
||||
create = { project, workspace, owner, ref ->
|
||||
val id = when (ref) {
|
||||
is SessionRef.Local -> ref.id
|
||||
is SessionRef.Cloud -> ref.key
|
||||
null -> null
|
||||
}
|
||||
created.add(workspace.directory to id)
|
||||
loading.add(show)
|
||||
SessionUi(project, workspace, sessions, app, scope, id = id, loading = show, open = owner::openSession, session = session).also {
|
||||
refs.add(ref)
|
||||
SessionUi(project, workspace, sessions, app, scope, ref = ref, manager = owner).also {
|
||||
ui.add(it)
|
||||
Disposer.register(it) { ui.remove(it) }
|
||||
}
|
||||
},
|
||||
resolve = { workspaces.workspace(it) },
|
||||
history = history,
|
||||
)
|
||||
managers.add(manager)
|
||||
return manager
|
||||
@@ -213,10 +396,22 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
|
||||
private fun active(manager: SessionSidePanelManager) = manager.component.getComponent(0) as JPanel
|
||||
|
||||
private fun JPanel.controller(): ai.kilocode.client.session.update.SessionController {
|
||||
private fun JPanel.controller(): ai.kilocode.client.session.controller.SessionController {
|
||||
val field = SessionUi::class.java.getDeclaredField("controller")
|
||||
field.isAccessible = true
|
||||
return field.get(this) as ai.kilocode.client.session.update.SessionController
|
||||
return field.get(this) as ai.kilocode.client.session.controller.SessionController
|
||||
}
|
||||
|
||||
private fun remove(manager: SessionSidePanelManager, id: String) {
|
||||
val method = SessionSidePanelManager::class.java.getDeclaredMethod("removeSession", String::class.java)
|
||||
method.isAccessible = true
|
||||
method.invoke(manager, id)
|
||||
}
|
||||
|
||||
private fun back(manager: SessionSidePanelManager) {
|
||||
val method = SessionSidePanelManager::class.java.getDeclaredMethod("back")
|
||||
method.isAccessible = true
|
||||
method.invoke(manager)
|
||||
}
|
||||
|
||||
private fun settle() = kotlinx.coroutines.runBlocking {
|
||||
@@ -237,4 +432,12 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
time = SessionTimeDto(created = 1.0, updated = 2.0),
|
||||
)
|
||||
|
||||
private fun cloud(id: String) = CloudSessionDto(
|
||||
id = id,
|
||||
title = "Cloud $id",
|
||||
createdAt = "2026-01-01T00:00:00Z",
|
||||
updatedAt = "2026-01-02T00:00:00Z",
|
||||
version = 1.0,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+33
-8
@@ -48,7 +48,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
fun `test factory creates blank session ui`() {
|
||||
val ui = direct().create(project, workspace, FakeManager(), null, true)
|
||||
val ui = direct().create(project, workspace, FakeManager(), null)
|
||||
|
||||
assertNotNull(ui)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
fun `test factory wires open callback`() {
|
||||
val manager = FakeManager()
|
||||
val rpc = session("ses_1")
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, open = manager::openSession)
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager)
|
||||
val controller = controller(ui)
|
||||
|
||||
com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait {
|
||||
@@ -66,22 +66,38 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("ses_1"), manager.opened)
|
||||
}
|
||||
|
||||
fun `test empty panel opens through controller`() {
|
||||
fun `test empty panel opens through SessionRef via controller`() {
|
||||
val manager = FakeManager()
|
||||
val rpc = session("ses_1")
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, open = manager::openSession)
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager)
|
||||
val controller = controller(ui)
|
||||
val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, listOf(rpc))
|
||||
|
||||
panel.clickRecent(0)
|
||||
|
||||
// Recent click routes through SessionRef.Local path
|
||||
assertEquals(listOf("ses_1"), manager.opened)
|
||||
}
|
||||
|
||||
private fun controller(ui: SessionUi): ai.kilocode.client.session.update.SessionController {
|
||||
fun `test empty panel show history routes through manager`() {
|
||||
val manager = FakeManager()
|
||||
val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager)
|
||||
val controller = controller(ui)
|
||||
val panel = ai.kilocode.client.session.ui.EmptySessionPanel(
|
||||
testRootDisposable,
|
||||
controller,
|
||||
emptyList(),
|
||||
) { manager.showHistory() }
|
||||
|
||||
panel.clickShowHistory()
|
||||
|
||||
assertEquals(1, manager.history)
|
||||
}
|
||||
|
||||
private fun controller(ui: SessionUi): ai.kilocode.client.session.controller.SessionController {
|
||||
val field = SessionUi::class.java.getDeclaredField("controller")
|
||||
field.isAccessible = true
|
||||
return field.get(ui) as ai.kilocode.client.session.update.SessionController
|
||||
return field.get(ui) as ai.kilocode.client.session.controller.SessionController
|
||||
}
|
||||
|
||||
fun `test application service is available`() {
|
||||
@@ -101,11 +117,20 @@ class SessionUiFactoryTest : BasePlatformTestCase() {
|
||||
|
||||
private class FakeManager : SessionManager {
|
||||
val opened = mutableListOf<String>()
|
||||
var history = 0
|
||||
override fun newSession() {
|
||||
}
|
||||
|
||||
override fun openSession(session: SessionDto) {
|
||||
opened.add(session.id)
|
||||
override fun showHistory() {
|
||||
history++
|
||||
}
|
||||
|
||||
override fun openSession(ref: SessionRef) {
|
||||
val id = when (ref) {
|
||||
is SessionRef.Local -> ref.id
|
||||
is SessionRef.Cloud -> ref.key
|
||||
}
|
||||
opened.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
-10
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client.session
|
||||
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.model.Permission
|
||||
import ai.kilocode.client.session.model.PermissionMeta
|
||||
import ai.kilocode.client.session.model.Question
|
||||
@@ -8,13 +9,14 @@ import ai.kilocode.client.session.model.QuestionOption
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.ui.ConnectionPanel
|
||||
import ai.kilocode.client.session.ui.EmptySessionPanel
|
||||
import ai.kilocode.client.session.ui.LoadingPanel
|
||||
import ai.kilocode.client.session.ui.PermissionPanel
|
||||
import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.session.ui.QuestionPanel
|
||||
import ai.kilocode.client.session.ui.SessionMessageListPanel
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.header.SessionHeaderPanel
|
||||
import ai.kilocode.client.session.update.SessionControllerEvent
|
||||
import ai.kilocode.client.session.controller.SessionControllerEvent
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import javax.swing.JLayeredPane
|
||||
@@ -119,7 +121,7 @@ class SessionUiLayoutTest : SessionUiTestBase() {
|
||||
fun `test empty and message bodies share the same scroll pane`() {
|
||||
settle()
|
||||
val pane = scrollComponent()
|
||||
val empty = find<EmptySessionPanel>(ui)
|
||||
val empty = find<EmptySessionPanel>(ui).view
|
||||
|
||||
assertSame(empty, scrollView())
|
||||
|
||||
@@ -132,23 +134,35 @@ class SessionUiLayoutTest : SessionUiTestBase() {
|
||||
assertSame(find<SessionMessageListPanel>(ui), scrollView())
|
||||
}
|
||||
|
||||
fun `test new session starts with loading body`() {
|
||||
fun `test new session starts neutral before controller view state`() {
|
||||
ui = newUi(displayMs = 1_000)
|
||||
|
||||
assertFalse(scrollView() is EmptySessionPanel)
|
||||
assertFalse(scrollView() is LoadingPanel)
|
||||
}
|
||||
|
||||
fun `test action-created new session starts blank`() {
|
||||
ui = newUi(displayMs = 1_000, loading = false)
|
||||
ui = newUi(displayMs = 1_000)
|
||||
|
||||
assertFalse(scrollView() is EmptySessionPanel)
|
||||
assertFalse(scrollView() is SessionMessageListPanel)
|
||||
assertFalse(scrollView() is LoadingPanel)
|
||||
}
|
||||
|
||||
fun `test clicking recent session calls opener`() {
|
||||
fun `test existing session id shows loading body immediately`() {
|
||||
rpc.historyGate = kotlinx.coroutines.CompletableDeferred()
|
||||
|
||||
ui = newUi(id = "ses_test", displayMs = 1_000)
|
||||
|
||||
assertSame(find<LoadingPanel>(ui), scrollView())
|
||||
assertEquals(SessionState.Loading, controller().model.state)
|
||||
rpc.historyGate?.complete(Unit)
|
||||
}
|
||||
|
||||
fun `test clicking recent session calls opener via SessionRef`() {
|
||||
val opened = mutableListOf<String>()
|
||||
rpc.recent.add(session("ses_1"))
|
||||
ui = newUi(open = { opened.add(it.id) })
|
||||
ui = newUi(open = { ref -> if (ref is SessionRef.Local) opened.add(ref.id) })
|
||||
|
||||
settle()
|
||||
layout()
|
||||
@@ -166,10 +180,64 @@ class SessionUiLayoutTest : SessionUiTestBase() {
|
||||
assertSame(find<SessionMessageListPanel>(ui), scrollView())
|
||||
}
|
||||
|
||||
fun `test empty explicit session id shows message body`() {
|
||||
rpc.recent.add(session("ses_recent"))
|
||||
settle()
|
||||
rpc.recentCalls.clear()
|
||||
|
||||
ui = newUi(id = "ses_test")
|
||||
settle()
|
||||
|
||||
assertSame(find<SessionMessageListPanel>(ui), scrollView())
|
||||
assertNull(find(ui, EmptySessionPanel::class.java))
|
||||
assertTrue(rpc.recentCalls.isEmpty())
|
||||
}
|
||||
|
||||
fun `test explicit session id loading does not show recents`() {
|
||||
rpc.historyGate = kotlinx.coroutines.CompletableDeferred()
|
||||
rpc.recent.add(session("ses_recent"))
|
||||
settle()
|
||||
rpc.recentCalls.clear()
|
||||
|
||||
ui = newUi(id = "ses_test", displayMs = 50)
|
||||
settleShort(100)
|
||||
|
||||
assertSame(find<LoadingPanel>(ui), scrollView())
|
||||
assertNull(find(ui, EmptySessionPanel::class.java))
|
||||
assertTrue(rpc.recentCalls.isEmpty())
|
||||
|
||||
rpc.historyGate!!.complete(Unit)
|
||||
settle()
|
||||
|
||||
assertSame(find<SessionMessageListPanel>(ui), scrollView())
|
||||
assertTrue(rpc.recentCalls.isEmpty())
|
||||
}
|
||||
|
||||
fun `test explicit cloud session loading does not show recents`() {
|
||||
rpc.importedCloudSession = session("ses_imported")
|
||||
rpc.historyGate = kotlinx.coroutines.CompletableDeferred()
|
||||
rpc.recent.add(session("ses_recent"))
|
||||
settle()
|
||||
rpc.recentCalls.clear()
|
||||
|
||||
ui = newUi(id = "cloud:cloud_1", displayMs = 50)
|
||||
settleShort(100)
|
||||
|
||||
assertSame(find<LoadingPanel>(ui), scrollView())
|
||||
assertNull(find(ui, EmptySessionPanel::class.java))
|
||||
assertTrue(rpc.recentCalls.isEmpty())
|
||||
|
||||
rpc.historyGate!!.complete(Unit)
|
||||
settle()
|
||||
|
||||
assertSame(find<SessionMessageListPanel>(ui), scrollView())
|
||||
assertTrue(rpc.recentCalls.isEmpty())
|
||||
}
|
||||
|
||||
fun `test existing session history shows header above scroll pane`() {
|
||||
rpc.history.add(MessageWithPartsDto(message("msg1"), emptyList()))
|
||||
|
||||
ui = SessionUi(project, workspace, sessions, app, scope, id = "ses_test", displayMs = 0).apply {
|
||||
ui = SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.Local("ses_test"), displayMs = 0).apply {
|
||||
setSize(800, 600)
|
||||
}
|
||||
settle()
|
||||
@@ -181,31 +249,38 @@ class SessionUiLayoutTest : SessionUiTestBase() {
|
||||
assertTrue(header.y + header.height <= scroll.y)
|
||||
}
|
||||
|
||||
fun `test new session keeps loading body before recents delay`() {
|
||||
fun `test new session shows blank body while recents are loading`() {
|
||||
rpc.recentGate = kotlinx.coroutines.CompletableDeferred()
|
||||
ui = newUi(displayMs = 1_000)
|
||||
|
||||
settleShort(100)
|
||||
|
||||
// A new session (no id) shows blank body while recents are pending, not loading body
|
||||
assertFalse(scrollView() is EmptySessionPanel)
|
||||
assertFalse(scrollView() is LoadingPanel)
|
||||
rpc.recentGate!!.complete(Unit)
|
||||
}
|
||||
|
||||
fun `test slow recents switch to loading body only after progress event`() {
|
||||
fun `test slow recents never show loading body and show recents when complete`() {
|
||||
rpc.recentGate = kotlinx.coroutines.CompletableDeferred()
|
||||
rpc.recent.add(session("ses_1"))
|
||||
ui = newUi(displayMs = 50)
|
||||
|
||||
settleShort(20)
|
||||
// No loading body — recents do not trigger progress indicator
|
||||
assertFalse(scrollView() is EmptySessionPanel)
|
||||
assertFalse(scrollView() is LoadingPanel)
|
||||
|
||||
settleShort(80)
|
||||
// Still no loading body even after the delay interval passes
|
||||
assertFalse(scrollView() is EmptySessionPanel)
|
||||
assertFalse(scrollView() is LoadingPanel)
|
||||
|
||||
rpc.recentGate!!.complete(Unit)
|
||||
settle()
|
||||
|
||||
val panel = find<EmptySessionPanel>(ui)
|
||||
assertSame(panel, scrollView())
|
||||
assertSame(panel.view, scrollView())
|
||||
assertEquals(1, panel.recentCount())
|
||||
}
|
||||
|
||||
|
||||
+11
-4
@@ -6,10 +6,11 @@ import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeSessionRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
@@ -76,10 +77,16 @@ abstract class SessionUiTestBase : BasePlatformTestCase() {
|
||||
protected fun newUi(
|
||||
id: String? = null,
|
||||
displayMs: Long = 0,
|
||||
loading: Boolean = id == null,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
open: ((SessionRef) -> Unit)? = null,
|
||||
): SessionUi {
|
||||
return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, loading = loading, open = open).apply {
|
||||
val manager = open?.let { fn ->
|
||||
object : SessionManager {
|
||||
override fun newSession() {}
|
||||
override fun showHistory() {}
|
||||
override fun openSession(ref: SessionRef) = fn(ref)
|
||||
}
|
||||
}
|
||||
return SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.from(id), displayMs = displayMs, manager = manager).apply {
|
||||
setSize(800, 600)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user