mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge remote-tracking branch 'origin/main' into puddle-barometer
# Conflicts: # packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep the selected agent after switching from Ask to Code, and remind the model that previous Ask-mode restrictions no longer apply.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Remove borders around JetBrains prompt bubbles and markdown code surfaces, and round the collapsed card hover highlight and markdown code blocks to match the prompt bubble.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Remove installed agents from every writable configuration source and report removal failures.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix action button border artifacts in JetBrains Islands Light session dialogs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Show the collapsed hover preview on more transcript cards: grep, glob, other tool calls, and to-dos now open the same popup that shell and diffs use.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refresh the session UI colors around three tunable roles — session background, code-block background (shared by code blocks, the prompt bubble, and the input), and foreground — and apply them correctly on first paint instead of only after switching themes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Use a unified secondary text style for JetBrains session UI labels.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Typing extra text after the `/memory show` command (for example `/memory show my notes`) used to clear that text from the chat input when the command ran. The text now stays in the input so you don't lose what you typed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Stop memory auto-save from failing on OpenAI-compatible providers that stream by default.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the Changes chip and Git changes visible across tab switches in multi-repository workspaces.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Changes from opencode v1.17.13 to v1.18.0 upstream:
|
||||
|
||||
- Core Improvements: Added a code mode MCP adapter for running confined orchestration scripts against connected MCP tools.
|
||||
- Core Improvements: Hid the `execute` tool unless code mode is enabled.
|
||||
- Core Improvements: Add a model-specific system prompt for Meta Muse Spark.
|
||||
- Core Improvements: Updated Azure AI support for GPT-5.6.
|
||||
- Core Bugfixes: Fixed paginated MCP tool catalogs losing tool metadata and output schema validation.
|
||||
- Core Bugfixes: Preserved low reasoning effort for OpenRouter small-model variants instead of disabling it.
|
||||
- Core Bugfixes: Fixed GitHub Copilot model routing to honor each model's advertised chat or responses endpoint.
|
||||
- Core Bugfixes: Fixed session lists to match equivalent instance directories reliably.
|
||||
- Core Bugfixes: Fixed Cerebras reasoning replay so earlier assistant reasoning is sent back in the provider-supported field.
|
||||
- Core Bugfixes: Better classify Z.ai context-window overflow errors so oversized requests surface the right failure mode (@fengjikui)
|
||||
- Core Bugfixes: Handle unavailable config directories more gracefully when reading config files
|
||||
- Core Bugfixes: Exposed reasoning effort variants for Grok models.
|
||||
- Core Bugfixes: Improved xAI prompt cache routing and PDF file support in Responses models.
|
||||
- Core Bugfixes: Improved Meta model handling for reasoning variants and provider requests.
|
||||
- Core Bugfixes: Prevent crashes and bad pricing data when GitHub Copilot returns models with a zero billing batch size.
|
||||
- Core Bugfixes: Supported OpenAI pro reasoning mode.
|
||||
- Core Bugfixes: Disabled response storage by default for xAI Responses. (@geraint0923)
|
||||
- Core Bugfixes: Added OAuth support for Luna Responses Lite.
|
||||
- Core Bugfixes: Switched to another available org after logging out in the console.
|
||||
- Core Bugfixes: Used Codex context limits for GPT-5.6 over OAuth. (@nabilfreeman)
|
||||
- Core Bugfixes: Removed an obsolete Codex workaround that could interfere with OpenAI Luna Responses Lite requests.
|
||||
- TUI Bugfixes: Fixed spinner registration so loading indicators keep rendering across TUI surfaces.
|
||||
- TUI Bugfixes: Forwarded CLI environment variables to the TUI worker.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Adopt upstream improvements from v1.18.1 through v1.18.13, including model compatibility, MCP reliability, and TUI enhancements.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Always outline JetBrains prompt bubbles and use a rounder prompt bubble shape.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refine transcript card spacing, plan-ready styling, and diff scrollbar layout, and remove the stray stripe and gap left by empty messages at the top of turns.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Remove the built-in experimental Morph WarpGrep codebase search tool and ignore its retired configuration flag.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep JetBrains session prompts visible in themes where the panel and editor backgrounds match.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains session card header clicks and align raised content backgrounds with the editor surface.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep JetBrains session status and copy controls visually bounded to the transcript area.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve existing text when selecting a slash command at the start of the chat input. Previously, typing `/new` or another slash command with text before the cursor would wipe the existing content entirely.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render the JetBrains session backdrop consistently and keep transcript hover backgrounds from getting stuck.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Stop startup from crashing on a database migration that another Kilo process already applied
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Use provider model catalogs instead of hardcoded model-name heuristics for reasoning variants.
|
||||
@@ -40,8 +40,8 @@ runs:
|
||||
shell: bash
|
||||
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Restoring and extracting the ~1 GB cache took 2m23s on Windows, while a # kilocode_change
|
||||
# fresh install took 1m27s. Keep Windows off this cache until that reverses. # kilocode_change
|
||||
# Restoring the ~1 GB cache buys nothing on Windows even with Defender exclusions # kilocode_change
|
||||
# (measured 83-102s Setup Bun with cache vs ~86s fresh install); keep Windows off it. # kilocode_change
|
||||
- name: Restore Bun dependencies
|
||||
if: runner.os != 'Windows' # kilocode_change
|
||||
id: bun-cache
|
||||
|
||||
@@ -64,7 +64,10 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
echo 'general=true' >> "$GITHUB_OUTPUT"
|
||||
echo 'settings=[{"os":"linux","index":1,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":true},{"os":"linux","index":2,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":false},{"os":"macos","index":1,"total":1,"host":"macos-15","run":true,"packages":true},{"os":"windows","index":1,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":true},{"os":"windows","index":2,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":3,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":4,"total":4,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false}]' >> "$GITHUB_OUTPUT"
|
||||
# kilocode_change - Windows runs 6 shards at KILO_TEST_CONCURRENCY=2. Measured: a
|
||||
# 5-shard matrix was worse on both wall-clock (slowest job 9.3m vs 6.7m) and machine
|
||||
# minutes (43 vs 37) — with 2 workers per shard, packing more work per shard loses.
|
||||
echo 'settings=[{"os":"linux","index":1,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":true},{"os":"linux","index":2,"total":2,"host":"blacksmith-4vcpu-ubuntu-2404","run":true,"packages":false},{"os":"macos","index":1,"total":1,"host":"macos-15","run":true,"packages":true},{"os":"windows","index":1,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":true},{"os":"windows","index":2,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":3,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":4,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":5,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false},{"os":"windows","index":6,"total":6,"host":"blacksmith-4vcpu-windows-2025","run":true,"packages":false}]' >> "$GITHUB_OUTPUT"
|
||||
# kilocode_change end
|
||||
unit:
|
||||
# kilocode_change start
|
||||
@@ -77,6 +80,12 @@ jobs:
|
||||
settings: ${{ fromJSON(needs.changes.outputs.settings) }} # kilocode_change
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
timeout-minutes: 45 # kilocode_change
|
||||
# kilocode_change start - manual dispatches exist to validate real execution (soak runs,
|
||||
# timing measurements); every turbo step in this job bypasses the cache on dispatch
|
||||
# instead of replaying a same-commit hit and executing nothing.
|
||||
env:
|
||||
TURBO_FORCE: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
||||
# kilocode_change end
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -87,6 +96,28 @@ jobs:
|
||||
run: echo "Only isolated product, documentation, or metadata files changed; general unit tests are unchanged."
|
||||
# kilocode_change end
|
||||
|
||||
# kilocode_change start - Defender real-time scanning taxes every process spawn and
|
||||
# temp-file write on Windows; the suite spawns hundreds of test processes and builds
|
||||
# temp git repos, so exclude the runner's work and temp paths up front. Best effort by
|
||||
# design: on images where Defender is absent or locked down, the suite runs unchanged.
|
||||
- name: Exclude runner work dirs from Defender scanning
|
||||
if: matrix.settings.run && runner.os == 'Windows'
|
||||
continue-on-error: true
|
||||
shell: pwsh
|
||||
run: |
|
||||
$applied = 0
|
||||
foreach ($dir in @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, "$env:USERPROFILE\.bun")) {
|
||||
if (-not $dir) { continue }
|
||||
try {
|
||||
Add-MpPreference -ExclusionPath $dir -ErrorAction Stop
|
||||
$applied++
|
||||
} catch {
|
||||
Write-Host "Defender exclusion failed for ${dir}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
Write-Host "Defender path exclusions applied: $applied"
|
||||
# kilocode_change end
|
||||
|
||||
- name: Checkout repository
|
||||
if: matrix.settings.run # kilocode_change
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
@@ -172,6 +203,16 @@ jobs:
|
||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - was Windows-only; the CLI now starts a watcher per instance, too heavy/racy for unit tests. Watcher tests opt back in.
|
||||
KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }}
|
||||
KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }}
|
||||
# kilocode_change - cap parallelism on the 4-vCPU Windows runner. At the default
|
||||
# min(4, cpus)=4, four heavy real-server test files share 4 vCPUs (~1 each) and blow
|
||||
# their per-test timeouts; 2 gives each process real CPU headroom. Windows grows to
|
||||
# 6 shards to absorb the lower per-shard parallelism. Linux/macOS (not timeout
|
||||
# offenders; macOS is a single unsharded job) keep the default.
|
||||
KILO_TEST_CONCURRENCY: ${{ matrix.settings.os == 'windows' && '2' || '' }}
|
||||
# kilocode_change - 600s on every OS: the deadline guards against hangs, and a
|
||||
# saturated shard can legitimately hold a heavy real-subprocess file past 300s
|
||||
# (seen on Linux: run-process passing every test, killed at the deadline twice).
|
||||
KILO_TEST_FILE_TIMEOUT: "600000"
|
||||
# kilocode_change end
|
||||
|
||||
# kilocode_change start
|
||||
@@ -189,6 +230,9 @@ jobs:
|
||||
|
||||
- name: Upload unit artifacts
|
||||
if: always() && matrix.settings.run
|
||||
# kilocode_change - diagnostics only: a runner-side network blip here must not
|
||||
# fail a job whose tests already passed (seen as ECONNREFUSED on Blacksmith).
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit-${{ matrix.settings.os }}-${{ matrix.settings.index }}-${{ github.run_attempt }}
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"version": "7.4.20"
|
||||
"version": "7.4.22"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode-ai/client",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "7.4.20",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -182,7 +182,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode-ai/httpapi-codegen",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
"packages/kilo-console": {
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
@@ -251,7 +251,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -281,7 +281,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
@@ -317,7 +317,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -327,7 +327,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -359,11 +359,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.4.20",
|
||||
"version": "7.4.21",
|
||||
},
|
||||
"packages/kilo-memory": {
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -377,7 +377,7 @@
|
||||
},
|
||||
"packages/kilo-sandbox": {
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -392,7 +392,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -406,7 +406,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -444,7 +444,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -517,7 +517,7 @@
|
||||
},
|
||||
"packages/kilo-web-ui": {
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -534,7 +534,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -553,7 +553,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -721,7 +721,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"packages/plugin-atomic-chat": {
|
||||
"name": "@kilocode/plugin-atomic-chat",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
},
|
||||
@@ -764,7 +764,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode-ai/protocol",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -777,7 +777,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode-ai/schema",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -789,7 +789,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -800,7 +800,7 @@
|
||||
},
|
||||
"packages/sdk-next": {
|
||||
"name": "@opencode-ai/sdk-next",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -815,7 +815,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -830,7 +830,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -845,7 +845,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -890,7 +890,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/session-ui": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -915,7 +915,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -941,7 +941,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
|
||||
+1
-1
@@ -177,6 +177,6 @@
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"solid-js@1.9.12": "patches/solid-js@1.9.12.patch"
|
||||
},
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"version": "7.4.21"
|
||||
"version": "7.4.22"
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ export type SessionsListOutput = {
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}>
|
||||
readonly cursor: { readonly previous?: string | null; readonly next?: string | null }
|
||||
@@ -323,6 +324,7 @@ export type SessionsCreateOutput = {
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}
|
||||
}["data"]
|
||||
@@ -361,6 +363,7 @@ export type SessionsGetOutput = {
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}
|
||||
}["data"]
|
||||
@@ -509,6 +512,7 @@ export type SessionsStageOutput = {
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}["data"]
|
||||
|
||||
@@ -1114,6 +1118,7 @@ export type SessionsHistoryOutput = {
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1600,6 +1605,7 @@ export type SessionsEventsOutput =
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly workspace?: "restored" | "snapshots-disabled" | "unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "7.4.20",
|
||||
"version": "7.4.22",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { Context, Duration, Effect, Layer, Logger, Option, Schedule, Schema } from "effect" // kilocode_change
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
||||
import { Global } from "./global"
|
||||
@@ -12,6 +12,7 @@ import * as ModelsRefresh from "./kilocode/models-refresh" // kilocode_change
|
||||
import { EventV2 } from "./event"
|
||||
import { makeGlobalNode } from "./effect/app-node"
|
||||
import { httpClient } from "./effect/app-node-platform"
|
||||
import { Observability } from "./observability" // kilocode_change
|
||||
|
||||
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
|
||||
export type CatalogModelStatus = typeof CatalogModelStatus.Type
|
||||
@@ -156,6 +157,7 @@ const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const events = yield* EventV2.Service
|
||||
const loggers = yield* Effect.service(Logger.CurrentLoggers) // kilocode_change
|
||||
const http = HttpClient.filterStatusOk(
|
||||
(yield* HttpClient.HttpClient).pipe(
|
||||
HttpClient.retryTransient({
|
||||
@@ -259,6 +261,7 @@ const layer = Layer.effect(
|
||||
).pipe(
|
||||
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })),
|
||||
Effect.ignore,
|
||||
Effect.provideService(Logger.CurrentLoggers, loggers), // kilocode_change
|
||||
)
|
||||
})
|
||||
|
||||
@@ -271,6 +274,12 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
|
||||
// kilocode_change start - capture file/OTLP loggers before the refresh fork
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [FSUtil.node, EventV2.node, httpClient, Observability.node],
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
export * as ModelsDev from "./models-dev"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { afterAll, beforeAll, describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { mkdir, readFile, rm, writeFile } from "fs/promises"
|
||||
import path from "path"
|
||||
import * as TestConsole from "effect/testing/TestConsole"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const ORIGINAL_MODELS_PATH = Flag.KILO_MODELS_PATH
|
||||
const ORIGINAL_DISABLE_FETCH = Flag.KILO_DISABLE_MODELS_FETCH
|
||||
const cache = Global.Path.cache
|
||||
const log = Global.Path.log
|
||||
const root = path.join(Global.Path.tmp, `models-logger-${process.pid}-${Math.random().toString(36).slice(2)}`)
|
||||
const logs = path.join(root, "log")
|
||||
|
||||
beforeAll(async () => {
|
||||
Flag.KILO_MODELS_PATH = undefined
|
||||
Flag.KILO_DISABLE_MODELS_FETCH = true
|
||||
Global.Path.cache = root
|
||||
Global.Path.log = logs
|
||||
await mkdir(logs, { recursive: true })
|
||||
await writeFile(path.join(root, "models.json"), "{}")
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
Flag.KILO_MODELS_PATH = ORIGINAL_MODELS_PATH
|
||||
Flag.KILO_DISABLE_MODELS_FETCH = ORIGINAL_DISABLE_FETCH
|
||||
Global.Path.cache = cache
|
||||
Global.Path.log = log
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const client = HttpClient.make((request) =>
|
||||
Effect.succeed(HttpClientResponse.fromWeb(request, new Response("boom", { status: 500 }))),
|
||||
)
|
||||
|
||||
const layer = Layer.fresh(
|
||||
AppNodeBuilder.build(ModelsDev.node, [[LayerNodePlatform.httpClient, Layer.succeed(HttpClient.HttpClient, client)]]),
|
||||
)
|
||||
|
||||
async function logged() {
|
||||
const file = path.join(logs, "opencode.log")
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const text = await readFile(file, "utf8").catch(() => "")
|
||||
if (text.includes("Failed to fetch models.dev")) return text
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
return await readFile(file, "utf8").catch(() => "")
|
||||
}
|
||||
|
||||
describe("ModelsDev catalog refresh logging", () => {
|
||||
it.live("failed refresh logs to file without Effect defaultLogger", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* ModelsDev.Service.use((s) => s.refresh(true)).pipe(Effect.provide(layer))
|
||||
const lines = yield* TestConsole.logLines
|
||||
expect(lines.join("\n")).not.toContain("Failed to fetch models.dev")
|
||||
expect(yield* Effect.promise(logged)).toContain("Failed to fetch models.dev")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -3,3 +3,13 @@ import path from "path"
|
||||
process.env.KILO_DB = ":memory:"
|
||||
process.env.KILO_MODELS_PATH = path.join(import.meta.dir, "plugin", "fixtures", "models-dev.json")
|
||||
process.env.KILO_DISABLE_MODELS_FETCH = "true"
|
||||
|
||||
// kilocode_change start - fail closed: core unit tests do not redirect XDG dirs, so KILO_DB
|
||||
// is the only thing keeping them off the real ~/.local/share/kilo database. Verify the
|
||||
// resolved path (env is read at flag import time, so this must stay after the env writes).
|
||||
const { Database } = await import("../src/database/database")
|
||||
const resolved = Database.path()
|
||||
if (resolved !== ":memory:") {
|
||||
throw new Error(`unit test preload: database path must resolve to ":memory:", got "${resolved}"`)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.4.21"
|
||||
version = "7.4.22"
|
||||
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.4.21/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.22/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.4.21/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.22/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.21/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.22/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.4.21/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.22/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.4.21/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.22/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"version": "7.4.21"
|
||||
"version": "7.4.22"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -15,9 +15,9 @@ Kilo Code supports accessing models through Amazon Bedrock, a fully managed serv
|
||||
- **AWS Account:** You need an active AWS account.
|
||||
- **Bedrock Access:** You must request and be granted access to Amazon Bedrock. See the [AWS Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html) for details on requesting access.
|
||||
- **Model Access:** Within Bedrock, you need to request access to the specific models you want to use (e.g., Anthropic Claude).
|
||||
- **Install AWS CLI:** Use AWS CLI to configure your account for authentication
|
||||
- **AWS CLI (profile authentication only):** If you plan to use an AWS profile, install AWS CLI and configure your credentials:
|
||||
```bash
|
||||
aws configure
|
||||
aws configure
|
||||
```
|
||||
|
||||
## Getting Credentials
|
||||
@@ -27,7 +27,7 @@ You have three options for configuring AWS credentials:
|
||||
1. **Bedrock API Key:**
|
||||
- Create a Bedrock-specific API key in the AWS Console. This is a simple service-specific authentication method.
|
||||
- See the [AWS documentation on Bedrock credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_bedrock.html) for instructions on creating an API key.
|
||||
2. **AWS Access Keys (Recommended for Development):**
|
||||
2. **AWS Access Keys:**
|
||||
- Create an IAM user with the necessary permissions (at least `bedrock:InvokeModel`).
|
||||
- Generate an access key ID and secret access key for that user.
|
||||
- _(Optional)_ Create a session token if required by your IAM configuration.
|
||||
@@ -39,20 +39,32 @@ You have three options for configuring AWS credentials:
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
Open **Settings** (gear icon) and go to the **Providers** tab to add AWS Bedrock. The extension uses the AWS credentials chain for authentication — configure your AWS credentials using the AWS CLI or environment variables before adding the provider.
|
||||
1. Open **Settings** and select **Providers**.
|
||||
2. Find **Amazon Bedrock** and select **Connect**.
|
||||
3. Choose an authentication method:
|
||||
- **AWS access keys:** Enter the AWS access key ID, AWS secret access key, optional AWS session token, and AWS region.
|
||||
- **Bedrock API key:** Enter a Bedrock API key.
|
||||
4. Select **Submit**.
|
||||
|
||||
The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format.
|
||||
The extension stores these credentials in Kilo's credential store, not in `kilo.json`.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Bedrock uses the AWS credentials chain for authentication. Configure your AWS credentials using the AWS CLI or environment variables:
|
||||
Bedrock supports a Bedrock API key or the AWS credentials chain.
|
||||
|
||||
**Environment variables:**
|
||||
**Bedrock API key:**
|
||||
|
||||
```bash
|
||||
export AWS_BEARER_TOKEN_BEDROCK="your-bedrock-api-key"
|
||||
```
|
||||
|
||||
**AWS access key environment variables:**
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID="your-access-key"
|
||||
export AWS_SECRET_ACCESS_KEY="your-secret-key"
|
||||
export AWS_SESSION_TOKEN="your-session-token" # Optional
|
||||
export AWS_REGION="us-east-1"
|
||||
```
|
||||
|
||||
@@ -60,6 +72,7 @@ Or use an AWS profile:
|
||||
|
||||
```bash
|
||||
aws configure --profile bedrock
|
||||
export AWS_PROFILE="bedrock"
|
||||
```
|
||||
|
||||
**Config file** (`~/.config/kilo/kilo.json` or `./kilo.json`):
|
||||
|
||||
@@ -15,30 +15,37 @@ Kilo Code supports accessing models through Google Cloud Platform's Vertex AI, a
|
||||
- **Google Cloud Account:** You need an active Google Cloud Platform (GCP) account.
|
||||
- **Project:** You need a GCP project with the Vertex AI API enabled.
|
||||
- **Model Access:** You must request and be granted access to the specific Claude models on Vertex AI you want to use. See the [Google Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin) for instructions.
|
||||
- **Application Default Credentials (ADC):** Kilo Code uses Application Default Credentials to authenticate with Vertex AI. The easiest way to set this up is to:
|
||||
1. Install the Google Cloud CLI: [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install)
|
||||
2. Authenticate using: `gcloud auth application-default login`
|
||||
- **Service Account Key (Alternative):** Alternatively, you can authenticate using a Google Cloud Service Account key file. You'll need to generate this key in your GCP project. See the [Google Cloud documentation on creating service account keys](https://cloud.google.com/iam/docs/creating-managing-service-account-keys).
|
||||
- **Service Account Key:** To connect the VS Code extension, generate a JSON key for a service account that can access Vertex AI. See the [Google Cloud documentation on creating service account keys](https://cloud.google.com/iam/docs/creating-managing-service-account-keys).
|
||||
- **Application Default Credentials (CLI option):** To use ADC with the CLI, [install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install) and run `gcloud auth application-default login`.
|
||||
|
||||
## Configuration in Kilo Code
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
Open **Settings** (gear icon) and go to the **Providers** tab to add GCP Vertex AI. The extension uses Google Application Default Credentials (ADC) for authentication — run `gcloud auth application-default login` before adding the provider. Set your project ID and region in the provider settings.
|
||||
1. Open **Settings** and select **Providers**.
|
||||
2. Find **Google Vertex AI** and select **Connect**.
|
||||
3. Paste the complete service account JSON into **Service-account JSON**.
|
||||
4. Enter a **Google Cloud project ID** only if you want to override the `project_id` in the JSON.
|
||||
5. Enter the **Vertex AI location**, such as `us-central1` or `global`.
|
||||
6. Select **Submit**.
|
||||
|
||||
The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format.
|
||||
The extension stores the service account JSON in Kilo's credential store, not in `kilo.json`. The JSON is visible while you edit it so you can verify the value before connecting.
|
||||
|
||||
{% callout type="warning" %}
|
||||
Treat service account JSON like a password. Do not share it or commit it to source control.
|
||||
{% /callout %}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
Vertex AI uses Google Application Default Credentials (ADC) for authentication. Set up ADC using the Google Cloud CLI:
|
||||
The following CLI setup uses Google Application Default Credentials (ADC). Authenticate with the Google Cloud CLI:
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
|
||||
Set your project and region as environment variables:
|
||||
Set your project and location as environment variables:
|
||||
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
|
||||
@@ -139,6 +139,7 @@ Options:
|
||||
-s, --session session id to continue [string]
|
||||
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
||||
--cloud-fork fetch session from cloud and continue locally (use with --session) [boolean]
|
||||
--worktree create (or reuse) a git worktree with this name and start kilo there [string]
|
||||
--prompt prompt to use [string]
|
||||
--agent agent to use [string]
|
||||
--auto auto-approve permissions that are not explicitly denied (dangerous!) [boolean] [default: false]
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e2bb93bd5830bcfd208615e64c8b2b88855d1faa97cd7980df9d95cfada533d3
|
||||
size 3561
|
||||
oid sha256:dd306e8bb95d7457ef3b504ff46c44dcfa6b273a316021e2f920db29c2035021
|
||||
size 4992
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c49479d1fffe96d14300617d48be269e5bffe41bc50f555b1ee3fbb09d8919e1
|
||||
size 3896
|
||||
oid sha256:d77e49dab17d964344cd0ca5618b1593f38c3d09f8dffffc2dca892477c5ff5c
|
||||
size 5328
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f0e205d89dc56438971d53e7210f1de1b261e641666ccebf0b1a2f36c3811aa
|
||||
size 4195
|
||||
oid sha256:aa3a5abf6d9488d8d14db03b89e1c569dc7fc8181ee337478705d0af8bde4ea0
|
||||
size 5624
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.21",
|
||||
"version": "7.4.22",
|
||||
"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.4.21",
|
||||
"version": "7.4.22",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -291,6 +291,28 @@ Rules:
|
||||
- Put user-visible strings in `*.properties` files.
|
||||
- Do not add decorative helper functions, wrappers, or defensive UI code unless they materially improve clarity or correctness.
|
||||
|
||||
### Session UI Background Strategy
|
||||
|
||||
The chat session UI intentionally uses a single-backdrop model. Do not make every
|
||||
container paint `SessionUiStyle.Colors.sessionBackground()` just because it sits
|
||||
inside the session. This avoids fragile component-hierarchy coupling and prevents
|
||||
theme-specific artifacts in transparent/rounded Swing painting.
|
||||
|
||||
- `SessionRootPanel` is the primary opaque backdrop. It overrides `getBackground()` and returns `SessionUiStyle.Colors.sessionBackground()`.
|
||||
- `sessionBackground()` follows the panel background, but when that equals the raised editor surface (`codeBlockBackground()`) — e.g. Islands Dark/Darcula, where panel and editor backgrounds are identical — it shifts by `SESSION_DELTA` via `UiStyle.Colors.contrast` (lighter in dark themes, darker in light) so the prompt bubble/input and other raised surfaces stay visible. This is a universal fallback, not a per-theme override.
|
||||
- `SessionRootPanel.Blocker` is the only other session-background opaque panel. It also overrides `getBackground()` with `sessionBackground()` for modal blocking.
|
||||
- Scroll panes, viewports, transcript layout panels, message lists, turn containers, wrapper panels, and card bodies should be non-opaque unless they intentionally paint a distinct surface.
|
||||
- `applyStyle(style)` must not assign session-background colors. It may update fonts, foregrounds, editor colors, and other non-background styling. Background colors that must be dynamic should come from `getBackground()` or custom painting.
|
||||
- Transcript card header hover is `getBackground()`-driven: keep an `isHovered` flag on the hover row/header, set or clear it from mouse enter/exit, and repaint only that row/header. Do not assign `row.background` or `header.background` on hover.
|
||||
- Primary/secondary transcript card bodies are transparent by default. The actual content inside a body (code blocks, todo list, shell/tool output) is the raised surface: make that content component opaque and paint `SessionUiStyle.Colors.codeBlockBackground()` (the editor background), using its own insets so the fill covers the whole content. For `MdView`-backed bodies set `md.opaque = false` so the prose/body stays transparent while the code/output panes remain the opaque editor surface — do not make the whole body opaque.
|
||||
- Rounded cards that paint their own surface (for example `RoundedContentPanel`-based question/login cards) should do that in custom painting or a `contentColor()` override, not by making every nested panel opaque.
|
||||
- Standard platform buttons placed on custom-painted session cards should not force the card background. If a button sits on a rounded/custom-painted card, make it non-opaque when needed so Swing does not fill its rectangular bounds behind `DarculaButtonUI`'s rounded paint (notably visible in Islands Light).
|
||||
|
||||
When adding a new session view, start with transparent containers and add opaque
|
||||
painting only for components that are actual visual surfaces. If a component's
|
||||
background must react to hover/theme state, prefer an override such as
|
||||
`getBackground()` over writing `background = ...` during setup or `applyStyle`.
|
||||
|
||||
### Swing Component Lifecycle
|
||||
|
||||
Swing is retained-mode UI. For dynamic Swing surfaces such as session cards, transcript parts, hover rows, and collapsible panels, build a stable component tree once and then mutate existing components in response to model or interaction changes.
|
||||
@@ -303,6 +325,7 @@ Swing is retained-mode UI. For dynamic Swing surfaces such as session cards, tra
|
||||
- Derive expanded state from containment (e.g. `scroll.parent === root`) rather than maintaining an `open` boolean.
|
||||
- Derive hover state from the current header background or other component property rather than maintaining a `hover` boolean.
|
||||
- Expand/collapse should attach or detach the existing body component and update controls only if attachment changed.
|
||||
- Expandable session cards extend `AbstractSessionPartView`, which binds click-to-toggle and hover across the whole header subtree automatically (including children added later). Do not re-bind header parts per view. A header control that must not toggle the card (file link, copy button, toolbar action) simply owns its own mouse listener and is skipped automatically.
|
||||
- Hover should update only the affected component (usually the header background) and repaint only that component when the effective color changed.
|
||||
- Lazy-create expensive bodies such as `JBTextArea`, `JBScrollPane`, markdown panes, and HTML panes on first expansion or first direct access.
|
||||
- Parent containers should refresh for add/remove/reorder operations, not automatically after every delegated child update or streaming delta.
|
||||
|
||||
@@ -150,6 +150,18 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [7.0.16] - 2026-08-14
|
||||
|
||||
### Added
|
||||
|
||||
### Fixed
|
||||
- Improve JetBrains chat readability with refreshed session surfaces, softer spacing, clearer prompt bubbles, and more consistent hover states.
|
||||
- Make JetBrains CLI downloads more reliable by retrying transient failures and rate limits, and by avoiding authentication on public CLI asset fetches.
|
||||
|
||||
### Changed
|
||||
- Update the JetBrains plugin CLI pin to Kilo Core 7.4.22.
|
||||
- Refresh Kilo Core with upstream provider, model variant, and session runtime updates.
|
||||
|
||||
## [7.0.15] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.process.ExecOperations
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
import java.security.MessageDigest
|
||||
@@ -32,6 +33,8 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
private val DIGEST = Regex("^sha256:[a-f0-9]{64}$")
|
||||
private val JSON = Json { ignoreUnknownKeys = true }
|
||||
private const val API = "https://api.github.com/repos/Kilo-Org/kilocode/releases/tags"
|
||||
private const val ATTEMPTS = 8
|
||||
private const val DELAY_MS = 5_000L
|
||||
}
|
||||
|
||||
@get:Input
|
||||
@@ -156,6 +159,7 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
conn.readTimeout = 120_000
|
||||
conn.instanceFollowRedirects = true
|
||||
conn.setRequestProperty("Accept", "application/vnd.github+json")
|
||||
conn.setRequestProperty("User-Agent", "Kilo-JetBrains-Gradle")
|
||||
token.getOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
@@ -200,13 +204,35 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
|
||||
private fun download(url: String, file: File) {
|
||||
logger.lifecycle("Downloading pinned Kilo CLI from $url")
|
||||
var failure: IOException? = null
|
||||
for (attempt in 1..ATTEMPTS) {
|
||||
try {
|
||||
downloadOnce(url, file)
|
||||
return
|
||||
} catch (err: IOException) {
|
||||
failure = err
|
||||
if (file.exists() && !file.delete()) logger.warn("Failed to delete partial pinned Kilo CLI archive ${file.absolutePath}")
|
||||
if (attempt == ATTEMPTS) break
|
||||
logger.warn("Pinned Kilo CLI download failed (attempt $attempt/$ATTEMPTS), retrying: ${err.message}")
|
||||
Thread.sleep((DELAY_MS * attempt).coerceAtMost(60_000L))
|
||||
}
|
||||
}
|
||||
throw GradleException("Failed to download pinned Kilo CLI after $ATTEMPTS attempts from $url", failure)
|
||||
}
|
||||
|
||||
private fun downloadOnce(url: String, file: File) {
|
||||
val conn = URI(url).toURL().openConnection() as HttpURLConnection
|
||||
conn.connectTimeout = 30_000
|
||||
conn.readTimeout = 120_000
|
||||
conn.instanceFollowRedirects = true
|
||||
conn.setRequestProperty("Accept", "application/octet-stream")
|
||||
conn.setRequestProperty("User-Agent", "Kilo-JetBrains-Gradle")
|
||||
try {
|
||||
val code = conn.responseCode
|
||||
if (code !in 200..299) throw GradleException("Failed to download pinned Kilo CLI: HTTP $code from $url")
|
||||
if (code !in 200..299) {
|
||||
if (code == 429 || code in 500..599 || limited(conn, code)) throw IOException("HTTP $code from $url")
|
||||
throw GradleException("Failed to download pinned Kilo CLI: HTTP $code from $url")
|
||||
}
|
||||
conn.inputStream.use { input ->
|
||||
file.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
|
||||
+9
-30
@@ -102,7 +102,6 @@ import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.UIManager
|
||||
|
||||
/**
|
||||
* Top-level session UI composition root.
|
||||
@@ -209,8 +208,6 @@ class SessionUi(
|
||||
return listOf(text)
|
||||
}
|
||||
}
|
||||
private var editorTheme = style.editorScheme
|
||||
private var colorTheme = UIManager.getLookAndFeel()
|
||||
private var wasBusy = false
|
||||
// Kept separate so a background stat refresh (turn end / revert) can supersede another refresh
|
||||
// but never cancel an in-flight user-initiated open.
|
||||
@@ -318,6 +315,7 @@ class SessionUi(
|
||||
|
||||
private fun buildUi() {
|
||||
root = SessionRootPanel()
|
||||
// Containers stay transparent over the single self-rendered session root backdrop.
|
||||
fileLinks = SessionFileLinks(workspace.directory, workspaces, cs, root, ::openUrl)
|
||||
SessionContextMenu.install(root, this)
|
||||
|
||||
@@ -351,14 +349,9 @@ class SessionUi(
|
||||
)
|
||||
}
|
||||
|
||||
overlay = SessionHoverCopyOverlay(root, this)
|
||||
root.addOverlay(overlay) { pane, child ->
|
||||
overlay.bounds(pane, child)
|
||||
}
|
||||
sessionContent = JPanel(BorderLayout()).apply { isOpaque = false }
|
||||
|
||||
sessionContent = JPanel(BorderLayout())
|
||||
|
||||
blankBody = JPanel(BorderLayout())
|
||||
blankBody = JPanel(BorderLayout()).apply { isOpaque = false }
|
||||
|
||||
load = LoadingPanel()
|
||||
progressBody = load
|
||||
@@ -406,6 +399,10 @@ class SessionUi(
|
||||
header = SessionHeaderPanel(controller, this) { openBranchChanges() }
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
overlay = SessionHoverCopyOverlay(root, scroll.component, this)
|
||||
root.addOverlay(overlay) { pane, child ->
|
||||
overlay.bounds(pane, child)
|
||||
}
|
||||
messageBody.onReflow = { on -> if (on && !opening) scroll.followTail() }
|
||||
scroll.onScroll = {
|
||||
overlay.clear()
|
||||
@@ -639,11 +636,7 @@ class SessionUi(
|
||||
private fun bindStyle() {
|
||||
addHierarchyListener { event ->
|
||||
if ((event.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong()) == 0L) return@addHierarchyListener
|
||||
if (!isShowing) {
|
||||
popup.hideAll()
|
||||
return@addHierarchyListener
|
||||
}
|
||||
applyStyleIfThemeChanged()
|
||||
if (!isShowing) popup.hideAll()
|
||||
}
|
||||
|
||||
val bus = ApplicationManager.getApplication().messageBus.connect(this)
|
||||
@@ -1010,29 +1003,15 @@ class SessionUi(
|
||||
if (disposed) return
|
||||
this.style = style
|
||||
selection.applyStyle(style)
|
||||
editorTheme = style.editorScheme
|
||||
colorTheme = UIManager.getLookAndFeel()
|
||||
background = style.editorBackground
|
||||
root.background = style.editorBackground
|
||||
root.content.background = style.editorBackground
|
||||
sessionContent.background = style.editorBackground
|
||||
blankBody.background = style.editorBackground
|
||||
load.applyStyle(style)
|
||||
header.applyStyle(style)
|
||||
prompt.applyStyle(style)
|
||||
connection.applyStyle(style)
|
||||
scroll.applyStyle(style)
|
||||
empty?.applyStyle(style)
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun applyStyleIfThemeChanged() {
|
||||
if (disposed) return
|
||||
val next = SessionEditorStyle.current()
|
||||
val laf = UIManager.getLookAndFeel()
|
||||
if (editorTheme === next.editorScheme && colorTheme == laf) return
|
||||
applyStyle(next)
|
||||
}
|
||||
|
||||
private fun openProfileSettings() {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(
|
||||
project,
|
||||
|
||||
+3
-2
@@ -40,6 +40,9 @@ internal class SessionScroll(
|
||||
border = JBUI.Borders.empty()
|
||||
verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER
|
||||
// Transparent over the self-rendered SessionUi backdrop; the message list paints its own.
|
||||
isOpaque = false
|
||||
viewport.isOpaque = false
|
||||
}
|
||||
|
||||
internal val bar: JScrollBar get() = component.verticalScrollBar
|
||||
@@ -223,8 +226,6 @@ internal class SessionScroll(
|
||||
@RequiresEdt
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
component.background = style.editorBackground
|
||||
component.viewport.background = style.editorBackground
|
||||
syncIcon()
|
||||
messages.applyStyle(style)
|
||||
val view = component.viewport.view
|
||||
|
||||
+4
-4
@@ -64,7 +64,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private val label = JBLabel().apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
addMouseListener(click)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class ConnectionPanel(
|
||||
isOpaque = false
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
foreground = UiStyle.Colors.fg()
|
||||
foreground = SessionUiStyle.Colors.foreground()
|
||||
}
|
||||
|
||||
private val scroll = JBScrollPane(details).apply {
|
||||
@@ -136,7 +136,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private fun showConnecting() {
|
||||
label.foreground = UiStyle.Colors.weak()
|
||||
label.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
label.text = KiloBundle.message("session.connection.connecting")
|
||||
detail = null
|
||||
expanded = false
|
||||
@@ -147,7 +147,7 @@ class ConnectionPanel(
|
||||
}
|
||||
|
||||
private fun showDownloading(percent: Int, version: String?, platform: String?) {
|
||||
label.foreground = UiStyle.Colors.weak()
|
||||
label.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
val pct = percent.coerceIn(0, 100)
|
||||
label.text = if (version != null && platform != null) {
|
||||
KiloBundle.message("session.connection.downloading.version", version, platform, pct)
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import ai.kilocode.client.plugin.KiloBundle
|
||||
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.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.Centerizer
|
||||
@@ -33,7 +34,7 @@ class LoadingPanel : JPanel(BorderLayout()), SessionEditorStyleTarget {
|
||||
|
||||
else -> {
|
||||
label.text = KiloBundle.message("session.empty.loading")
|
||||
label.foreground = UiStyle.Colors.weak()
|
||||
label.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
}
|
||||
revalidate()
|
||||
|
||||
+8
-14
@@ -12,15 +12,14 @@ import ai.kilocode.client.session.ui.selection.hoverPlaceholder
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.session.views.tool.EditFileChange
|
||||
import ai.kilocode.client.session.views.tool.POPUP_OPTS
|
||||
import ai.kilocode.client.session.views.tool.PatchBody
|
||||
import ai.kilocode.client.session.views.tool.setFont
|
||||
import ai.kilocode.client.session.views.tool.setForeground
|
||||
import ai.kilocode.client.session.views.tool.setIcon
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.DiffBars
|
||||
import ai.kilocode.client.ui.ToolbarButtonAction
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
@@ -36,7 +35,7 @@ class ModifiedFilesView private constructor(
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: Header = Header(),
|
||||
private val body: PatchBody = PatchBody(selection, openFile),
|
||||
) : SecondarySessionPartView(parts.panel, { body.mountFiles(emptyList()) }), SessionCopyTarget {
|
||||
) : AbstractSessionPartView(parts.panel, { body.mountFiles(emptyList()) }), SessionCopyTarget {
|
||||
override val contentId = CONTENT_ID
|
||||
|
||||
private var style = SessionEditorStyle.current()
|
||||
@@ -56,7 +55,6 @@ class ModifiedFilesView private constructor(
|
||||
body.overflow = ::openDiffViewer
|
||||
parts.diff.addActionListener { openDiffViewer() }
|
||||
isVisible = false
|
||||
bindHeader(parts.glyph, parts.title, parts.count, parts.panel.left, parts.bars, parts.anchor)
|
||||
applyStyle(style)
|
||||
}
|
||||
|
||||
@@ -113,12 +111,8 @@ class ModifiedFilesView private constructor(
|
||||
override fun copyText(): String? = null
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded() || files.isEmpty()) return null
|
||||
return HeaderPopupRequest(row, build = { buildPopup(files) }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "changes"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("tool", "changes", files.isNotEmpty()) { buildPopup(files) }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
@@ -156,7 +150,7 @@ class ModifiedFilesView private constructor(
|
||||
}
|
||||
val panel = popup.mountFiles(files)
|
||||
popup.applyStyle(style)
|
||||
return HeaderPopupBody(panel, owner, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
|
||||
return HeaderPopupBody(panel, owner, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
|
||||
}
|
||||
|
||||
private class Header {
|
||||
@@ -173,7 +167,7 @@ class ModifiedFilesView private constructor(
|
||||
leading(glyph)
|
||||
left(title)
|
||||
titleGap()
|
||||
left(count, PartHeader.centered(bars), PartHeader.centered(anchor))
|
||||
left(count, PartHeader.centered(bars), anchor)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
@@ -189,8 +183,8 @@ class ModifiedFilesView private constructor(
|
||||
setForeground(glyph, SessionUiStyle.View.Tool.completed())
|
||||
setFont(title, style.boldEditorFont)
|
||||
setFont(count, style.transcriptFont)
|
||||
setForeground(title, UiStyle.Colors.fg())
|
||||
setForeground(count, UiStyle.Colors.weak())
|
||||
setForeground(title, SessionUiStyle.Colors.foreground())
|
||||
setForeground(count, SessionUiStyle.Text.Secondary.foreground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -6,6 +6,7 @@ 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.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.StackAxis
|
||||
@@ -17,6 +18,7 @@ import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.Color
|
||||
|
||||
/**
|
||||
* Progress footer rendered at the bottom of the session transcript while the
|
||||
@@ -44,13 +46,13 @@ class ProgressPanel(
|
||||
foreground = style.editorForeground
|
||||
}
|
||||
private val elapsed = JBLabel().apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
private val spinner = JBLabel(AnimatedIcon.Default())
|
||||
private val tick = clock.timer(1000) { syncElapsed() }
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
isOpaque = true
|
||||
isVisible = false
|
||||
border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.sm(),
|
||||
@@ -82,6 +84,8 @@ class ProgressPanel(
|
||||
/** Exposed for test assertions. */
|
||||
fun labelForeground() = label.foreground
|
||||
|
||||
override fun getBackground(): Color = SessionUiStyle.Colors.sessionBackground()
|
||||
|
||||
private fun onState(state: SessionState) {
|
||||
this.state = state
|
||||
when (state) {
|
||||
@@ -140,7 +144,7 @@ class ProgressPanel(
|
||||
this.style = style
|
||||
label.font = style.regularFont
|
||||
elapsed.font = style.regularFont
|
||||
elapsed.foreground = UiStyle.Colors.weak()
|
||||
elapsed.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
if (state is SessionState.Busy) label.foreground = style.editorForeground
|
||||
revalidate()
|
||||
repaint()
|
||||
|
||||
+3
-2
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.SessionModel
|
||||
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.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
@@ -170,10 +171,10 @@ class RevertBanner(
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
card.applyStyle(style)
|
||||
title.font = style.headerFont
|
||||
title.foreground = UiStyle.Colors.fg()
|
||||
title.foreground = SessionUiStyle.Colors.foreground()
|
||||
progress?.applyStyle(style)
|
||||
hint.foreground = UIUtil.getLabelForeground()
|
||||
notice.foreground = UIUtil.getContextHelpForeground()
|
||||
notice.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
rows.values.forEach { it.applyStyle() }
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ 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 ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
@@ -36,7 +37,7 @@ class RevertProgress(onCancel: () -> Unit) : JPanel(), SessionEditorStyleTarget
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
label.font = style.regularFont
|
||||
label.foreground = UiStyle.Colors.fg()
|
||||
label.foreground = SessionUiStyle.Colors.foreground()
|
||||
cancel.font = style.regularFont
|
||||
revalidate()
|
||||
repaint()
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import java.awt.Graphics
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Raised code surface for scrollable tool bodies. Paints the shared code-block background rounded
|
||||
* with the standard block arc — the same corners as the card header — and clips its content to that
|
||||
* shape so the opaque viewport never squares off the corners. Views keep their own content insets;
|
||||
* this only owns the rounded fill.
|
||||
*/
|
||||
internal open class SessionCodeScroll(view: JComponent) : JBScrollPane(view) {
|
||||
|
||||
override fun isOpaque(): Boolean = false
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
SessionSurface.fill(g, width, height)
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
override fun paintChildren(g: Graphics) {
|
||||
SessionSurface.clipped(g, width, height) { super.paintChildren(it) }
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.StackAxis
|
||||
import java.awt.Component
|
||||
|
||||
/**
|
||||
* Reusable body for an expanded session card. A transparent vertical column that stacks content
|
||||
* surfaces (usually [SessionSurfacePanel] boxes) separated by the standard [SessionUiStyle.View.contentGap],
|
||||
* followed by a transparent footer region where a view can add ambient components — for example an
|
||||
* auto-approved note — that read on the session backdrop rather than on a raised surface.
|
||||
*
|
||||
* The panel itself paints nothing: content pieces bring their own opaque surface, and the footer
|
||||
* stays transparent, so the session's single-backdrop strategy is preserved and views never
|
||||
* duplicate background painting.
|
||||
*/
|
||||
class SessionContentPanel : Stack(StackAxis.VERTICAL, SessionUiStyle.View.contentGap()) {
|
||||
|
||||
private val body = Stack.vertical(SessionUiStyle.View.contentGap())
|
||||
private val footer = Stack.vertical(SessionUiStyle.View.contentGap()).also { it.isVisible = false }
|
||||
|
||||
init {
|
||||
next(body)
|
||||
next(footer)
|
||||
}
|
||||
|
||||
/** Adds a content piece (its own raised surface) below the previous one, separated by the standard gap. */
|
||||
fun content(component: Component): SessionContentPanel {
|
||||
body.next(component)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Adds a transparent footer component below the content; the footer region appears only once used. */
|
||||
fun footer(component: Component): SessionContentPanel {
|
||||
footer.next(component)
|
||||
footer.isVisible = true
|
||||
return this
|
||||
}
|
||||
|
||||
/** True while the footer region holds at least one component. */
|
||||
fun hasFooter(): Boolean = footer.componentCount > 0
|
||||
}
|
||||
+1
@@ -149,6 +149,7 @@ open class SessionLayoutPanel(
|
||||
pad: Insets = JBUI.emptyInsets(),
|
||||
) : BorderLayoutPanel(), javax.swing.Scrollable {
|
||||
init {
|
||||
isOpaque = false
|
||||
layout = SessionLayout(gap, pad)
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -96,7 +96,6 @@ class SessionMessageListPanel(
|
||||
val progress = ProgressPanel(model, parent)
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
Disposer.register(parent, this)
|
||||
applyStyle(style)
|
||||
|
||||
@@ -381,10 +380,13 @@ class SessionMessageListPanel(
|
||||
|
||||
private fun syncReverted() {
|
||||
for ((id, view) in msgToView) {
|
||||
view.isVisible = !model.isRevertedMessage(id)
|
||||
view.setReverted(model.isRevertedMessage(id))
|
||||
}
|
||||
// Turn visibility tracks revert only, independent of a message being empty: an empty message
|
||||
// hides its own row (MessageView.setReverted/syncVisibility), but the turn stays visible so its
|
||||
// other content and modified-files card still render.
|
||||
for (view in turnViews.values) {
|
||||
view.isVisible = view.messageIds().any { msgToView[it]?.isVisible == true }
|
||||
view.isVisible = view.messageIds().any { !model.isRevertedMessage(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +612,6 @@ class SessionMessageListPanel(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
background = style.editorBackground
|
||||
for (view in turnViews.values) view.applyStyle(style)
|
||||
question?.applyStyle(style)
|
||||
permission?.applyStyle(style)
|
||||
|
||||
+19
-1
@@ -1,16 +1,34 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.LayeredOverlayPanel
|
||||
import java.awt.Color
|
||||
|
||||
class SessionRootPanel(
|
||||
private val sessionOverlay: Overlay = Overlay(),
|
||||
private val sessionBlocker: Blocker = Blocker(),
|
||||
) : LayeredOverlayPanel(overlay = sessionOverlay, blocker = sessionBlocker) {
|
||||
init {
|
||||
content.isOpaque = false
|
||||
}
|
||||
|
||||
override fun isOpaque(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getBackground(): Color {
|
||||
return SessionUiStyle.Colors.sessionBackground()
|
||||
}
|
||||
|
||||
override val overlay: Overlay get() = sessionOverlay
|
||||
|
||||
override val blocker: Blocker get() = sessionBlocker
|
||||
|
||||
class Overlay : LayeredOverlayPanel.Overlay()
|
||||
|
||||
class Blocker : LayeredOverlayPanel.Blocker()
|
||||
class Blocker : LayeredOverlayPanel.Blocker() {
|
||||
override fun getBackground(): Color {
|
||||
return SessionUiStyle.Colors.sessionBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.geom.RoundRectangle2D
|
||||
|
||||
/**
|
||||
* Shared painting for raised, rounded session code surfaces. Every inner block (tool output, task
|
||||
* rows, todo list, code panes) uses the same block arc as the card header hover, so a single helper
|
||||
* keeps the fill and the rounded child clip identical everywhere.
|
||||
*/
|
||||
internal object SessionSurface {
|
||||
|
||||
fun arc(): Int = JBUI.scale(SessionUiStyle.View.BLOCK_ARC)
|
||||
|
||||
/** Fills the component bounds with [color], rounded to the block arc. */
|
||||
fun fill(g: Graphics, width: Int, height: Int, color: Color = SessionUiStyle.Colors.codeBlockBackground()) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = color
|
||||
val arc = arc()
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs [paint] with the graphics clipped to the rounded block, keeping opaque content rounded. */
|
||||
inline fun clipped(g: Graphics, width: Int, height: Int, paint: (Graphics) -> Unit) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val arc = arc().toFloat()
|
||||
g2.clip(RoundRectangle2D.Float(0f, 0f, width.toFloat(), height.toFloat(), arc, arc))
|
||||
paint(g2)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.RoundedContentPanel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
|
||||
/**
|
||||
* A raised, opaque session block. Paints the shared code-block background with the standard session
|
||||
* block corner arc (the same arc as the card header hover) and no outline, so any view that needs an
|
||||
* opaque background gets the same rounded chrome without re-implementing the painting.
|
||||
*
|
||||
* The panel carries no padding — content decides its own insets (most views add a left inset to line
|
||||
* up with the header, while shell code and patch/diff blocks span the full width). Children are
|
||||
* clipped to the rounded rectangle, so an opaque rectangular child never squares off the corners.
|
||||
*/
|
||||
open class SessionSurfacePanel : RoundedContentPanel(0, 0, 0, 0) {
|
||||
|
||||
override fun contentColor(): Color = SessionUiStyle.Colors.codeBlockBackground()
|
||||
|
||||
override fun outlineColor(): Color? = null
|
||||
|
||||
override fun cornerArc(): Int = JBUI.scale(SessionUiStyle.View.BLOCK_ARC)
|
||||
|
||||
override fun paintChildren(g: Graphics) {
|
||||
SessionSurface.clipped(g, width, height) { super.paintChildren(it) }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -183,7 +183,7 @@ open class AttachmentCard(
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val arc = JBUI.scale(SessionUiStyle.View.Attachment.CORNER_ARC)
|
||||
g2.color = SessionUiStyle.View.Surface.bgColor()
|
||||
g2.color = SessionUiStyle.Colors.codeBlockBackground()
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
g2.color = SessionUiStyle.View.Outline.color()
|
||||
g2.drawRoundRect(0, 0, width - 1, height - 1, arc, arc)
|
||||
|
||||
+1
-2
@@ -24,7 +24,6 @@ import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.ui.components.JBLabel
|
||||
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 com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
@@ -68,7 +67,7 @@ class EmptySessionPanel(
|
||||
private val feedback = EmptySessionFeedback(browse)
|
||||
|
||||
private val welcomeLabel = JBLabel(welcomeHtml()).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
horizontalAlignment = JBLabel.CENTER
|
||||
setAllowAutoWrapping(true)
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,7 +19,6 @@ import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
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.Component
|
||||
@@ -42,7 +41,7 @@ internal class RecentsList(
|
||||
private var snapshot = HistoryActivitySnapshot()
|
||||
|
||||
private val title = JBLabel(KiloBundle.message("session.empty.recent")).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
|
||||
internal val list = JBList(model).apply {
|
||||
@@ -180,8 +179,8 @@ internal class RecentsList(
|
||||
val over = selected || hover == index
|
||||
isOpaque = over
|
||||
background = if (over) list.selectionBackground else list.background
|
||||
title.foreground = if (over) list.selectionForeground else UIUtil.getLabelForeground()
|
||||
time.foreground = if (over) list.selectionForeground else UIUtil.getContextHelpForeground()
|
||||
title.foreground = if (over) list.selectionForeground else SessionUiStyle.Colors.foreground()
|
||||
time.foreground = if (over) list.selectionForeground else SessionUiStyle.Text.Secondary.foreground()
|
||||
title.text = value?.let { snapshot.titles[it.id] ?: title(it) } ?: ""
|
||||
time.text = value?.let(HistoryTime::relative) ?: ""
|
||||
setBadge(value?.id?.let(snapshot.activity::get))
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.header
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.DiffStatBadge
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
@@ -65,7 +66,7 @@ internal class BranchChangesBadge(
|
||||
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
count.font = style.smallFont
|
||||
count.foreground = UiStyle.Colors.weak()
|
||||
count.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
|
||||
+5
-3
@@ -3,6 +3,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.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
@@ -41,9 +42,10 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) {
|
||||
}
|
||||
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
background = style.editorBackground
|
||||
val bg = SessionUiStyle.Colors.sessionBackground()
|
||||
background = bg
|
||||
foreground = style.editorForeground
|
||||
meter.background = style.editorBackground
|
||||
meter.background = bg
|
||||
used.font = style.smallFont
|
||||
used.foreground = style.editorForeground
|
||||
limit.font = style.smallFont
|
||||
@@ -143,7 +145,7 @@ private class Meter : JComponent() {
|
||||
fun reservedColor(): Color = shade(0.28f)
|
||||
|
||||
private fun shade(alpha: Float): Color {
|
||||
val base = background ?: UiStyle.Colors.editorBackground()
|
||||
val base = background ?: SessionUiStyle.Colors.sessionBackground()
|
||||
val grey = if (UiStyle.Colors.bright(base)) Color.BLACK else Color.WHITE
|
||||
return UiStyle.Colors.blend(base, grey, alpha)
|
||||
}
|
||||
|
||||
+11
-10
@@ -318,20 +318,21 @@ class SessionHeaderPanel(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
background = style.editorBackground
|
||||
val bg = SessionUiStyle.Colors.sessionBackground()
|
||||
background = bg
|
||||
foreground = style.editorForeground
|
||||
top.background = style.editorBackground
|
||||
top.background = bg
|
||||
top.isOpaque = true
|
||||
top.border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.sm(), UiStyle.Gap.md(), UiStyle.Gap.sm())
|
||||
centerGroup.background = style.editorBackground
|
||||
centerGroup.background = bg
|
||||
centerGroup.isOpaque = true
|
||||
right.background = style.editorBackground
|
||||
changes.background = style.editorBackground
|
||||
tokens.background = style.editorBackground
|
||||
todoRow.background = style.editorBackground
|
||||
todoBox.background = style.editorBackground
|
||||
body.background = style.editorBackground
|
||||
viewport.background = style.editorBackground
|
||||
right.background = bg
|
||||
changes.background = bg
|
||||
tokens.background = bg
|
||||
todoRow.background = bg
|
||||
todoBox.background = bg
|
||||
body.background = bg
|
||||
viewport.background = bg
|
||||
title.font = style.boldFont
|
||||
title.foreground = style.editorForeground
|
||||
changes.applyStyle(style)
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.mode
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
@@ -72,7 +73,7 @@ internal class ModePickerRenderer(
|
||||
): JPanel {
|
||||
val focus = selected || list.hasFocus() || focused
|
||||
val fg = UIUtil.getListForeground(selected, focus)
|
||||
val weak = if (selected) fg else UiStyle.Colors.weak()
|
||||
val secondary = if (selected) fg else SessionUiStyle.Text.Secondary.foreground()
|
||||
|
||||
background = list.background
|
||||
wrap.update(list, selected, focus)
|
||||
@@ -81,7 +82,7 @@ internal class ModePickerRenderer(
|
||||
desc.clear()
|
||||
desc.isVisible = value.description?.isNotBlank() == true
|
||||
value.description?.takeIf { it.isNotBlank() }?.let {
|
||||
desc.append(it, SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, weak))
|
||||
desc.append(it, SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, secondary))
|
||||
}
|
||||
badge.isVisible = value.deprecated
|
||||
icon.icon = icon(value)
|
||||
|
||||
+7
-6
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.model
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
@@ -32,10 +33,10 @@ internal class ModelDetailsPanel(
|
||||
private val toggle: (ModelPicker.Item) -> Unit,
|
||||
) : JPanel(BorderLayout()), Disposable {
|
||||
private val empty = JBLabel(KiloBundle.message("model.picker.details.empty")).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
private val title = JBLabel().apply { font = UiStyle.Fonts.bold() }
|
||||
private val provider = JBLabel().apply { foreground = UIUtil.getContextHelpForeground() }
|
||||
private val provider = JBLabel().apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
private val star = JBLabel().apply {
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
horizontalAlignment = JBLabel.CENTER
|
||||
@@ -241,8 +242,8 @@ private class RowsSection(title: String) {
|
||||
}
|
||||
|
||||
private class DetailRow : JPanel(BorderLayout()) {
|
||||
private val name = JBLabel().apply { foreground = UIUtil.getContextHelpForeground() }
|
||||
private val value = JBLabel().apply { foreground = UIUtil.getLabelForeground() }
|
||||
private val name = JBLabel().apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
private val value = JBLabel().apply { foreground = SessionUiStyle.Colors.foreground() }
|
||||
|
||||
init {
|
||||
add(name, BorderLayout.WEST)
|
||||
@@ -372,13 +373,13 @@ private fun descriptionText(value: String): String = value
|
||||
private class TagStyle(private val bg: Color) : UiStyle.Badge.Style {
|
||||
override fun bg() = bg
|
||||
|
||||
override fun fg() = UiStyle.Colors.fg()
|
||||
override fun fg() = SessionUiStyle.Colors.foreground()
|
||||
}
|
||||
|
||||
private fun tagStyle(index: Int): UiStyle.Badge.Style {
|
||||
if (index % 4 == 0) return UiStyle.Badge.Secondary
|
||||
val bg = when (index % 4) {
|
||||
1 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), UiStyle.Colors.fg(), 0.12f)
|
||||
1 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), SessionUiStyle.Colors.foreground(), 0.12f)
|
||||
2 -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), JBUI.CurrentTheme.Link.Foreground.ENABLED, 0.18f)
|
||||
else -> UiStyle.Colors.blend(UiStyle.Colors.contentBackground(), UiStyle.Badge.Primary.bg(), 0.18f)
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client.session.ui.model
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.picker.PickerListRenderer
|
||||
@@ -67,6 +68,7 @@ internal class ModelPickerRenderer private constructor(
|
||||
foreground: java.awt.Color,
|
||||
weak: java.awt.Color,
|
||||
) {
|
||||
val secondary = if (selected) weak else SessionUiStyle.Text.Secondary.foreground()
|
||||
parts.title.clear()
|
||||
val item = value.item
|
||||
if (item == null) {
|
||||
@@ -80,8 +82,8 @@ internal class ModelPickerRenderer private constructor(
|
||||
}
|
||||
val name = ModelText.parts(item)
|
||||
if (name.provider != null) {
|
||||
parts.title.append(name.provider, SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, weak))
|
||||
parts.title.append(" ", SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, weak))
|
||||
parts.title.append(name.provider, SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, secondary))
|
||||
parts.title.append(" ", SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, secondary))
|
||||
}
|
||||
parts.title.append(name.model, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, foreground))
|
||||
|
||||
@@ -90,7 +92,7 @@ internal class ModelPickerRenderer private constructor(
|
||||
parts.byokLabel.isVisible = item.byok
|
||||
parts.provider.isVisible = value.favorite
|
||||
parts.provider.text = item.providerName
|
||||
parts.provider.foreground = weak
|
||||
parts.provider.foreground = secondary
|
||||
parts.provider.border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
|
||||
val fav = item.key in favorites()
|
||||
|
||||
+11
-3
@@ -16,14 +16,22 @@ internal interface SessionCopyTarget {
|
||||
fun copyText(): String?
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves the trailing hover-action button's *width* in a card header so header content never sits
|
||||
* under the button, without reserving its height. The button is a hover overlay, so letting it
|
||||
* dictate the header height would make action cards (edit, modified files) taller than plain cards.
|
||||
* Added to a horizontal header [ai.kilocode.client.ui.layout.Stack] uncentered, it stretches to the
|
||||
* row height at layout; the zero preferred height marks it as an inline anchor so the overlay
|
||||
* vertically centers the button on the header row (aligned with the change badge).
|
||||
*/
|
||||
internal fun hoverPlaceholder(toolbar: JComponent): JComponent = object : JPanel() {
|
||||
init {
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension = Dimension(toolbar.preferredSize)
|
||||
override fun getPreferredSize(): Dimension = Dimension(toolbar.preferredSize.width, 0)
|
||||
|
||||
override fun getMinimumSize(): Dimension = Dimension(toolbar.minimumSize)
|
||||
override fun getMinimumSize(): Dimension = Dimension(toolbar.minimumSize.width, 0)
|
||||
|
||||
override fun getMaximumSize(): Dimension = Dimension(toolbar.maximumSize)
|
||||
override fun getMaximumSize(): Dimension = Dimension(toolbar.maximumSize.width, Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
+23
-4
@@ -18,6 +18,7 @@ import javax.swing.SwingUtilities
|
||||
|
||||
internal class SessionHoverCopyOverlay(
|
||||
private val root: JComponent,
|
||||
private val area: JComponent,
|
||||
parent: Disposable,
|
||||
) : JPanel(null), Disposable {
|
||||
private var target: SessionCopyTarget? = null
|
||||
@@ -57,18 +58,36 @@ internal class SessionHoverCopyOverlay(
|
||||
if (visible.isEmpty) return Rectangle()
|
||||
val size = child.preferredSize
|
||||
val gap = JBUI.scale(4)
|
||||
val limit = limit(pane)
|
||||
if (limit.isEmpty) return Rectangle()
|
||||
if (item.copyToolbar != null) {
|
||||
val pt = SwingUtilities.convertPoint(anchor, Point(visible.x, visible.y), pane)
|
||||
val x = (pt.x + visible.width - size.width).coerceIn(0, (pane.width - size.width).coerceAtLeast(0))
|
||||
val y = (pt.y + visible.height - size.height).coerceIn(0, (pane.height - size.height).coerceAtLeast(0))
|
||||
// A zero-height anchor is an inline header placeholder (edit/modified open-diff): center
|
||||
// the floating button on the header row so it lines up with the change badge. A real-height
|
||||
// anchor is a footer row (message/text copy): keep the button bottom-aligned inside it.
|
||||
val inline = anchor.preferredSize.height == 0
|
||||
val offset = if (inline) (visible.height - size.height) / 2 else visible.height - size.height
|
||||
val x = clamp(pt.x + visible.width - size.width, limit.x, limit.x + limit.width - size.width)
|
||||
val y = clamp(pt.y + offset, limit.y, limit.y + limit.height - size.height)
|
||||
return Rectangle(x, y, size.width, size.height)
|
||||
}
|
||||
val pt = SwingUtilities.convertPoint(anchor, Point(visible.x + visible.width, visible.y), pane)
|
||||
val x = (pt.x - size.width - gap).coerceIn(0, (pane.width - size.width).coerceAtLeast(0))
|
||||
val y = (pt.y + gap).coerceIn(0, (pane.height - size.height).coerceAtLeast(0))
|
||||
val x = clamp(pt.x - size.width - gap, limit.x, limit.x + limit.width - size.width)
|
||||
val y = clamp(pt.y + gap, limit.y, limit.y + limit.height - size.height)
|
||||
return Rectangle(x, y, size.width, size.height)
|
||||
}
|
||||
|
||||
private fun limit(pane: JPanel): Rectangle {
|
||||
if (!area.isShowing || area.parent == null) return Rectangle()
|
||||
val pt = SwingUtilities.convertPoint(area.parent, area.location, pane)
|
||||
return Rectangle(pt.x, pt.y, area.width, area.height)
|
||||
}
|
||||
|
||||
private fun clamp(value: Int, min: Int, max: Int): Int {
|
||||
if (max < min) return min
|
||||
return value.coerceIn(min, max)
|
||||
}
|
||||
|
||||
override fun doLayout() {
|
||||
child.setBounds(0, 0, width, height)
|
||||
layout(child)
|
||||
|
||||
+2
-4
@@ -23,8 +23,8 @@ import kotlin.math.roundToInt
|
||||
* Editor-specific fields ([editorFont], [editorForeground], [editorBackground]) are derived from the active editor color
|
||||
* scheme and are used for code/editor-rendered content.
|
||||
*
|
||||
* UI font fields ([transcriptFont], [smallEditorFont], [boldEditorFont], [headerFont], [hintFont], [regularFont],
|
||||
* [boldFont], [smallFont]) come from [UiStyle.Fonts] and follow standard platform typography. Transcript fonts use the
|
||||
* UI font fields ([transcriptFont], [smallEditorFont], [boldEditorFont], [headerFont], [regularFont], [boldFont],
|
||||
* [smallFont]) come from [UiStyle.Fonts] and follow standard platform typography. Transcript fonts use the
|
||||
* editor size so the session body tracks editor zoom without adopting the editor family.
|
||||
*/
|
||||
data class SessionEditorStyle(
|
||||
@@ -38,7 +38,6 @@ data class SessionEditorStyle(
|
||||
val smallEditorFont: Font,
|
||||
val boldEditorFont: Font,
|
||||
val headerFont: Font,
|
||||
val hintFont: Font,
|
||||
val regularFont: Font,
|
||||
val boldFont: Font,
|
||||
val smallFont: Font,
|
||||
@@ -133,7 +132,6 @@ data class SessionEditorStyle(
|
||||
smallEditorFont = uiFont(UiStyle.Fonts.small(), Font.PLAIN, small),
|
||||
boldEditorFont = uiFont(UiStyle.Fonts.regular(), Font.BOLD, size),
|
||||
headerFont = UiStyle.Fonts.header(),
|
||||
hintFont = UiStyle.Fonts.hint(),
|
||||
regularFont = UiStyle.Fonts.regular(),
|
||||
boldFont = UiStyle.Fonts.bold(),
|
||||
smallFont = UiStyle.Fonts.small(),
|
||||
|
||||
+89
-21
@@ -5,11 +5,72 @@ import com.intellij.ui.JBColor
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Color
|
||||
import java.awt.Font
|
||||
import javax.swing.UIManager
|
||||
|
||||
/** Static style tokens owned by the chat session UI. */
|
||||
object SessionUiStyle {
|
||||
/**
|
||||
* The session palette is driven by three authored keys; everything else is derived:
|
||||
* - [sessionBackground] paints the whole session backdrop (containers stay transparent over it).
|
||||
* - [codeBlockBackground] is the single raised surface (code blocks, tool/shell output, prompt bubble, prompt input).
|
||||
* - [foreground] is normal text and links; [Text.Secondary] owns secondary session text.
|
||||
*/
|
||||
object Colors {
|
||||
/**
|
||||
* Recess applied to the raised editor surface to derive a distinct backdrop. Used both when
|
||||
* the panel-background key is missing and when the panel background is identical to the
|
||||
* raised surface (e.g. Islands Dark/Darcula), so the prompt bubble/input and other raised
|
||||
* surfaces still stand out. [UiStyle.Colors.contrast] lightens dark themes and darkens light
|
||||
* themes, matching how generic themes separate the backdrop from editor content.
|
||||
*/
|
||||
private const val SESSION_DELTA = 8
|
||||
|
||||
/**
|
||||
* Whole session backdrop: follows the panel (chrome) background, distinct from raised
|
||||
* surfaces. When the panel background matches the raised surface — so raised surfaces would
|
||||
* be invisible against it — the backdrop is shifted by [SESSION_DELTA] instead.
|
||||
*
|
||||
* Not a `namedColor`: `JBColor.namedColor` resolves through theme `"*"` wildcard rules by
|
||||
* suffix (`name.endsWith("background")`), so any theme with a `*.background` rule would
|
||||
* hijack this key and skip the fallback below. We read an exact override key ourselves and
|
||||
* otherwise compute the backdrop, wrapped in `JBColor.lazy` so it re-resolves on LaF changes.
|
||||
*/
|
||||
fun sessionBackground(): Color = JBColor.lazy {
|
||||
UIManager.getColor("Kilo.Session.background") ?: run {
|
||||
val raised = codeBlockBackground()
|
||||
val panel = UIManager.getColor("Panel.background") ?: raised
|
||||
if (panel.rgb == raised.rgb) UiStyle.Colors.contrast(raised, SESSION_DELTA) else panel
|
||||
}
|
||||
}
|
||||
|
||||
/** Single raised surface (code blocks, tool/shell output, prompt bubble, prompt input): the editor background. */
|
||||
fun codeBlockBackground(): Color = JBColor.namedColor(
|
||||
"Kilo.Session.codeBlockBackground",
|
||||
UiStyle.Colors.editorBackground(),
|
||||
)
|
||||
|
||||
fun foreground(): Color = JBColor.namedColor(
|
||||
"Kilo.Session.foreground",
|
||||
UiStyle.Colors.fg(),
|
||||
)
|
||||
}
|
||||
|
||||
/** Text roles used by session UI labels and transcript chrome. */
|
||||
object Text {
|
||||
object Secondary {
|
||||
private const val BACKGROUND_BLEND = 0.35f
|
||||
|
||||
fun foreground(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.blend(Colors.foreground(), Colors.sessionBackground(), BACKGROUND_BLEND)
|
||||
}
|
||||
|
||||
fun font(style: SessionEditorStyle): Font = style.regularFont
|
||||
}
|
||||
}
|
||||
|
||||
object Transcript {
|
||||
fun bgColor(): Color = UiStyle.Colors.bg()
|
||||
fun bgColor(): Color = Colors.sessionBackground()
|
||||
}
|
||||
|
||||
/** Geometry for the transcript list and its scroll behavior. */
|
||||
@@ -30,13 +91,29 @@ object SessionUiStyle {
|
||||
|
||||
/** Shared tokens for individual transcript views and session views. */
|
||||
object View {
|
||||
/**
|
||||
* Corner arc (unscaled) for raised session blocks: the user prompt bubble and the
|
||||
* collapsed card hover fill. Scale with [JBUI.scale] at the call site.
|
||||
*/
|
||||
const val BLOCK_ARC = 8
|
||||
|
||||
object Layout {
|
||||
const val GAP = 5
|
||||
const val VERTICAL_PADDING = 7
|
||||
/** Header vertical padding for the compact card variant (e.g. reasoning). */
|
||||
const val COMPACT_VERTICAL_PADDING = 5
|
||||
const val HORIZONTAL_PADDING = 12
|
||||
const val BODY_EXTRA_HEIGHT = 16
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard transparent inset separating an expanded card header from its content, and
|
||||
* separating stacked content surfaces inside a [ai.kilocode.client.session.ui.SessionContentPanel].
|
||||
* Matches the gap between views in the transcript ([SessionLayout.GAP]), so a card's internal
|
||||
* spacing reads the same as the spacing between cards.
|
||||
*/
|
||||
fun contentGap() = JBUI.scale(SessionLayout.GAP)
|
||||
|
||||
/**
|
||||
* Single source of truth for the spacing of every session-card header (see `PartHeader`).
|
||||
* Keep header gaps here so all cards stay aligned; do not hardcode header spacing elsewhere.
|
||||
@@ -69,13 +146,13 @@ object SessionUiStyle {
|
||||
internal const val HOVER_FILL_ALPHA = 0.10f
|
||||
|
||||
object Surface {
|
||||
fun bgColor(): Color = UiStyle.Colors.editorBackground()
|
||||
fun bgColor(): Color = Colors.sessionBackground()
|
||||
|
||||
fun headerBgColor(): Color = UiStyle.Colors.editorBackground()
|
||||
fun headerBgColor(): Color = Colors.sessionBackground()
|
||||
|
||||
/** Subtle hover fill, softer than the session-view outline. */
|
||||
fun headerHoverBgColor(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.blend(headerBgColor(), Outline.hoverColor(), HOVER_FILL_ALPHA)
|
||||
UiStyle.Colors.blend(Colors.sessionBackground(), Outline.hoverColor(), HOVER_FILL_ALPHA)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +160,7 @@ object SessionUiStyle {
|
||||
fun color(): Color = UiStyle.Colors.contentBorder()
|
||||
|
||||
fun brightColor(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA)
|
||||
UiStyle.Colors.contrast(Colors.sessionBackground(), BORDER_DELTA)
|
||||
}
|
||||
|
||||
/** Subtle hover outline, stronger than the hover fill. */
|
||||
@@ -96,20 +173,12 @@ object SessionUiStyle {
|
||||
|
||||
/** Prompt input dimensions and chrome inside the session view. */
|
||||
object Prompt {
|
||||
/**
|
||||
* Background of the prompt input and the transcript user-prompt bubble. Uses a dedicated
|
||||
* theme key so the prompt surface can be restyled independently, defaulting to the
|
||||
* code-fragment background so the prompt matches rendered code blocks.
|
||||
*/
|
||||
fun bgColor(style: SessionEditorStyle): Color = JBColor.namedColor(
|
||||
"Kilo.Session.Prompt.Background",
|
||||
UiStyle.Colors.codeBlockBackground(style.editorScheme),
|
||||
)
|
||||
fun bgColor(_style: SessionEditorStyle): Color = Colors.codeBlockBackground()
|
||||
|
||||
const val EDITOR_LINES = 1
|
||||
const val EDITOR_CHROME = 16
|
||||
const val SEND_BUTTON_SIZE = 24
|
||||
const val CORNER_ARC = 6
|
||||
const val CORNER_ARC = 10
|
||||
const val FOCUS_WIDTH = 2
|
||||
const val PANEL_VERTICAL_PADDING = 8
|
||||
const val PANEL_HORIZONTAL_PADDING = 12
|
||||
@@ -145,7 +214,7 @@ object SessionUiStyle {
|
||||
private const val SCRIM_ALPHA = 210
|
||||
|
||||
fun scrim(): Color = JBColor.lazy {
|
||||
val bg = UiStyle.Colors.bg()
|
||||
val bg = Colors.sessionBackground()
|
||||
Color(bg.red, bg.green, bg.blue, SCRIM_ALPHA)
|
||||
}
|
||||
|
||||
@@ -155,7 +224,6 @@ object SessionUiStyle {
|
||||
/** Reasoning block preview sizing. */
|
||||
object Reasoning {
|
||||
const val BODY_LINES = 5
|
||||
const val HEADER_VERTICAL_PADDING = 5
|
||||
const val BODY_VERTICAL_PADDING = 4
|
||||
const val BODY_HORIZONTAL_PADDING = 8
|
||||
}
|
||||
@@ -220,11 +288,11 @@ object SessionUiStyle {
|
||||
*/
|
||||
const val DIFF_MAX_LINES = 2_000
|
||||
|
||||
fun pending(): Color = UiStyle.Colors.weak()
|
||||
fun pending(): Color = Text.Secondary.foreground()
|
||||
|
||||
fun running(): Color = UiStyle.Colors.fg()
|
||||
fun running(): Color = Colors.foreground()
|
||||
|
||||
fun completed(): Color = UiStyle.Colors.weak()
|
||||
fun completed(): Color = Text.Secondary.foreground()
|
||||
|
||||
fun error(): Color = UiStyle.Colors.errorLabelForeground()
|
||||
}
|
||||
@@ -249,7 +317,7 @@ object SessionUiStyle {
|
||||
val TOOL: Color = JBColor.namedColor("Kilo.Session.Timeline.Tool", Color(0x00, 0x7a, 0xcc))
|
||||
val SUCCESS: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground())
|
||||
val ERROR: Color = JBColor.namedColor("Kilo.Session.Timeline.Error", UIUtil.getErrorForeground())
|
||||
val TEXT: Color = JBColor.namedColor("Kilo.Session.Timeline.Text", UIUtil.getContextHelpForeground())
|
||||
val TEXT: Color = Text.Secondary.foreground()
|
||||
val STEP: Color = JBColor.namedColor("Kilo.Session.Timeline.Step", JBColor.border())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
|
||||
|
||||
override val contentId: String = compaction.id
|
||||
private val text = JBLabel(KiloBundle.message("session.part.compaction")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.empty(0, UiStyle.Gap.lg())
|
||||
}
|
||||
|
||||
+32
-14
@@ -35,7 +35,6 @@ import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Point
|
||||
import java.awt.Graphics
|
||||
@@ -96,6 +95,7 @@ class MessageView(
|
||||
private var wrap: PromptWrap? = null
|
||||
private var openDiff: SessionDiffOpener = { _, _, _ -> }
|
||||
private var sessionId: String? = null
|
||||
private var reverted = false
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
@@ -108,6 +108,7 @@ class MessageView(
|
||||
if (isHidden(content)) continue
|
||||
addPart(content)
|
||||
}
|
||||
syncVisibility()
|
||||
}
|
||||
|
||||
fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) {
|
||||
@@ -471,7 +472,11 @@ class MessageView(
|
||||
super.paintComponent(g)
|
||||
return
|
||||
}
|
||||
paintPromptBox(g, this)
|
||||
// Historical user prompts render their text as a plain child that relies on this surface fill,
|
||||
// so paint it whenever the message has content. An empty user message (a bare turn anchor with
|
||||
// no parts) lays out ~1px tall; painting its bubble there would leave a thin light stripe at
|
||||
// the top of the turn, so skip it.
|
||||
if (componentCount > 0) paintPromptBox(g, this)
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
@@ -479,26 +484,39 @@ class MessageView(
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
val arc = JBUI.scale(SessionUiStyle.View.BLOCK_ARC)
|
||||
val pt = if (box === this) Point() else SwingUtilities.convertPoint(box, Point(), this)
|
||||
val bg = SessionUiStyle.View.Prompt.bgColor(style)
|
||||
g2.color = bg
|
||||
g2.color = SessionUiStyle.View.Prompt.bgColor(style)
|
||||
g2.fillRoundRect(pt.x, pt.y, box.width, box.height, arc, arc)
|
||||
// When the prompt shares the session background there is no fill contrast, so draw the
|
||||
// outline to keep the bubble visible.
|
||||
if (bg.rgb == style.editorBackground.rgb) {
|
||||
val w = box.width - 1
|
||||
val h = box.height - 1
|
||||
g2.color = SessionUiStyle.View.Outline.color()
|
||||
if (w > 0 && h > 0) g2.drawRoundRect(pt.x, pt.y, w, h, arc, arc)
|
||||
}
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this message as reverted (rolled back). A reverted message is hidden regardless of its
|
||||
* content; the panel drives this from the model's revert state.
|
||||
*/
|
||||
@RequiresEdt
|
||||
fun setReverted(value: Boolean) {
|
||||
reverted = value
|
||||
syncVisibility()
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty message (no rendered parts) would otherwise lay out as a ~1px row and add a stray gap
|
||||
* at the top of its turn — a bare user turn anchor is the common case. Keep such a message present
|
||||
* for lookup/streaming but invisible so [ai.kilocode.client.session.ui.SessionLayout] skips it and
|
||||
* its gap; it reappears as soon as content arrives. Reverted messages stay hidden either way.
|
||||
*/
|
||||
@RequiresEdt
|
||||
private fun syncVisibility() {
|
||||
isVisible = componentCount > 0 && !reverted
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun refresh() {
|
||||
syncVisibility()
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
@@ -623,7 +641,7 @@ class MessageView(
|
||||
private fun queue(onDelete: () -> Unit) = Stack.horizontal(UiStyle.Gap.sm()).also { row ->
|
||||
row.isOpaque = false
|
||||
row.next(JBLabel(KiloBundle.message("session.queued")).apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
})
|
||||
row.next(toolbarButton(
|
||||
ToolbarButtonAction(
|
||||
|
||||
+8
@@ -6,11 +6,13 @@ 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.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import ai.kilocode.client.session.openSessionLink
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class PlanExitView(
|
||||
@@ -32,6 +34,9 @@ class PlanExitView(
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
// Prose, not a raised block, but still indented to the regular card header's leading edge so
|
||||
// the plan-ready line aligns left with the tool/message views above and below it.
|
||||
border = JBUI.Borders.empty(0, SessionUiStyle.View.Header.left(), 0, 0)
|
||||
Disposer.register(this, md)
|
||||
md.addLinkListener { openSessionLink(it, openFile, openUrl) }
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
@@ -47,6 +52,9 @@ class PlanExitView(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
md.applyStyle(style)
|
||||
// Plan-ready is transcript prose, not a raised block: stay transparent so it reads on the
|
||||
// session backdrop like the assistant's other text.
|
||||
md.opaque = false
|
||||
md.font = style.transcriptFont
|
||||
md.codeFont = style.editorFamily
|
||||
md.foreground = style.editorForeground
|
||||
|
||||
+17
-53
@@ -12,9 +12,8 @@ import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
@@ -40,10 +39,11 @@ class ReasoningView(
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ReasoningParts = reasoningParts(selection),
|
||||
) :
|
||||
SecondarySessionPartView(
|
||||
AbstractSessionPartView(
|
||||
parts.header,
|
||||
{ parts.scroll(openFile, openUrl) },
|
||||
expanded = reasoning.content.isNotBlank() && !reasoning.done,
|
||||
compact = true,
|
||||
) {
|
||||
|
||||
override val contentId: String = reasoning.id
|
||||
@@ -70,16 +70,8 @@ class ReasoningView(
|
||||
private var pinned = false
|
||||
|
||||
init {
|
||||
row.border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.HEADER_VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.left(),
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.HEADER_VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.right(),
|
||||
)
|
||||
bindHeader(parts.title, parts.icon)
|
||||
applyStyle(style)
|
||||
if (bodyVisible()) syncBody()
|
||||
syncBorder()
|
||||
sync()
|
||||
}
|
||||
|
||||
@@ -87,20 +79,11 @@ class ReasoningView(
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
syncBorder()
|
||||
syncBody()
|
||||
applyBodyStyle()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (!changed) return false
|
||||
syncBorder()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Reasoning) return
|
||||
@@ -179,13 +162,8 @@ class ReasoningView(
|
||||
internal fun bodyScrollBottom() = parts.scrollOrNull?.verticalScrollBar?.let { it.maximum - it.visibleAmount } ?: 0
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded()) return null
|
||||
val text = source.takeIf { it.isNotBlank() } ?: return null
|
||||
return HeaderPopupRequest(row, build = { buildPopupBody(text) }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "part" to "reasoning"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("part", "reasoning", source.isNotBlank()) { buildPopupBody(source) }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
@@ -203,7 +181,7 @@ class ReasoningView(
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
val height = row.preferredSize.height + expandedGap() + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@@ -224,20 +202,6 @@ class ReasoningView(
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
0,
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(0, 1, 0, 0)
|
||||
}
|
||||
|
||||
private fun apply(md: MdView): Boolean {
|
||||
var changed = false
|
||||
val font = style.smallEditorFont.deriveFont(Font.ITALIC)
|
||||
@@ -245,8 +209,8 @@ class ReasoningView(
|
||||
md.font = font
|
||||
changed = md.codeFont != style.editorFamily || changed
|
||||
md.codeFont = style.editorFamily
|
||||
changed = md.foreground.rgb != UiStyle.Colors.weak().rgb || changed
|
||||
md.foreground = UiStyle.Colors.weak()
|
||||
changed = md.foreground.rgb != SessionUiStyle.Text.Secondary.foreground().rgb || changed
|
||||
md.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
return changed
|
||||
}
|
||||
|
||||
@@ -280,22 +244,22 @@ class ReasoningView(
|
||||
md.applyStyle(style)
|
||||
md.font = style.smallEditorFont.deriveFont(Font.ITALIC)
|
||||
md.codeFont = style.editorFamily
|
||||
md.foreground = UiStyle.Colors.weak()
|
||||
md.background = style.editorBackground
|
||||
md.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
md.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.component.border = JBUI.Borders.empty()
|
||||
md.set(text)
|
||||
// The shared popup wrapper (HeaderPopupBody) provides the scroll pane, so pass the content
|
||||
// panel directly instead of nesting a second scroll pane here.
|
||||
val panel = TrackPanel().apply {
|
||||
isOpaque = true
|
||||
background = style.editorBackground
|
||||
background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_HORIZONTAL_PADDING),
|
||||
)
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
}
|
||||
return HeaderPopupBody(panel, md, style.editorBackground)
|
||||
return HeaderPopupBody(panel, md, SessionUiStyle.Colors.codeBlockBackground())
|
||||
}
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
@@ -360,7 +324,7 @@ class ReasoningParts(
|
||||
}
|
||||
val panel = TrackPanel().apply {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_HORIZONTAL_PADDING),
|
||||
@@ -370,8 +334,8 @@ class ReasoningParts(
|
||||
val scroll = JBScrollPane(panel).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
viewport.background = SessionUiStyle.View.Surface.bgColor()
|
||||
background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
viewport.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
@@ -386,8 +350,8 @@ class ReasoningBody(
|
||||
)
|
||||
|
||||
private fun reasoningParts(selection: SessionSelection? = null): ReasoningParts {
|
||||
val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = UiStyle.Colors.weak() }
|
||||
val icon = JBLabel(SessionViewIcons.brain).apply { foreground = UiStyle.Colors.weak() }
|
||||
val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
val icon = JBLabel(SessionViewIcons.brain).apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
val header = PartHeader().apply {
|
||||
leading(icon)
|
||||
left(title)
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import ai.kilocode.client.session.openSessionLink
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Text
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.ui.selection.SessionCopyTarget
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
@@ -124,7 +125,7 @@ open class TextView(
|
||||
|
||||
protected open fun styleFont(style: SessionEditorStyle) = style.transcriptFont
|
||||
|
||||
protected open fun styleBackground(style: SessionEditorStyle) = style.editorBackground
|
||||
protected open fun styleBackground(style: SessionEditorStyle) = SessionUiStyle.Colors.codeBlockBackground()
|
||||
|
||||
protected fun refresh() {
|
||||
revalidate()
|
||||
|
||||
+202
-29
@@ -1,23 +1,50 @@
|
||||
package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupBody
|
||||
import ai.kilocode.client.session.ui.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockFactory
|
||||
import ai.kilocode.client.ui.md.MdCodeBlockOptions
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.awt.Cursor
|
||||
import java.awt.Font
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.ContainerAdapter
|
||||
import java.awt.event.ContainerEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
* Base for every collapsible session card. A card is a header [row] plus an optional lazily built
|
||||
* body. Collapsed, the card is just the header with a rounded hover fill. Expanded, the body is
|
||||
* attached under the header and separated from it by a transparent [SessionUiStyle.View.contentGap]
|
||||
* (the BorderLayout vgap) — the card draws no outline and no separator line, so content reads as
|
||||
* standalone raised surfaces on the shared session backdrop.
|
||||
*/
|
||||
abstract class AbstractSessionPartView(
|
||||
header: JComponent,
|
||||
private val makeBody: () -> JComponent,
|
||||
expanded: Boolean = false,
|
||||
private val expandable: Boolean = true,
|
||||
private val compact: Boolean = false,
|
||||
) : PartView() {
|
||||
|
||||
constructor(
|
||||
@@ -25,11 +52,13 @@ abstract class AbstractSessionPartView(
|
||||
body: JComponent,
|
||||
expanded: Boolean = false,
|
||||
expandable: Boolean = true,
|
||||
) : this(header, { body }, expanded, expandable)
|
||||
compact: Boolean = false,
|
||||
) : this(header, { body }, expanded, expandable, compact)
|
||||
|
||||
protected val arrow = JBLabel()
|
||||
protected val row = JPanel(BorderLayout(SessionUiStyle.View.Header.gap(), 0))
|
||||
private val bound = linkedSetOf<Component>()
|
||||
protected val row = Row()
|
||||
private val clickable = linkedSetOf<Component>()
|
||||
private val watched = linkedSetOf<Component>()
|
||||
private var body: JComponent? = null
|
||||
|
||||
private val click = object : MouseAdapter() {
|
||||
@@ -38,7 +67,10 @@ abstract class AbstractSessionPartView(
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
private val mouse = object : MouseAdapter() {
|
||||
// Hover is tracked across the whole header subtree so leaving the row via any nested
|
||||
// element (e.g. an unbound file link) still clears the hover fill. Swing only delivers
|
||||
// mouseExited to the deepest component, so a single listener on the row is not enough.
|
||||
private val pointer = object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
setHovered(true)
|
||||
}
|
||||
@@ -48,18 +80,35 @@ abstract class AbstractSessionPartView(
|
||||
setHovered(false)
|
||||
}
|
||||
}
|
||||
private val nested = object : ContainerAdapter() {
|
||||
override fun componentAdded(e: ContainerEvent) = watch(e.child)
|
||||
override fun componentRemoved(e: ContainerEvent) = unwatch(e.child)
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
// The vgap becomes the transparent inset between the header and the body once a body is
|
||||
// attached; collapsed cards have only the NORTH row, so no gap shows.
|
||||
layout = BorderLayout(0, SessionUiStyle.View.contentGap())
|
||||
isOpaque = false
|
||||
val pad = JBUI.scale(
|
||||
if (compact) SessionUiStyle.View.Layout.COMPACT_VERTICAL_PADDING
|
||||
else SessionUiStyle.View.Layout.VERTICAL_PADDING,
|
||||
)
|
||||
row.border = JBUI.Borders.empty(pad, SessionUiStyle.View.Header.left(), pad, SessionUiStyle.View.Header.right())
|
||||
row.add(header, BorderLayout.CENTER)
|
||||
row.add(arrow, BorderLayout.EAST)
|
||||
add(row, BorderLayout.NORTH)
|
||||
bindHeader(row, header, arrow)
|
||||
watch(row)
|
||||
if (expanded && expandable) add(body(), BorderLayout.CENTER)
|
||||
if (!expandable) syncExpandable(false) else syncArrow()
|
||||
}
|
||||
|
||||
/**
|
||||
* The transparent header-to-content inset once expanded, else 0. Subclasses that cap their
|
||||
* preferred height (`row.height + bodyHeight`) add this so the separator inset is accounted for.
|
||||
*/
|
||||
protected fun expandedGap(): Int = if (isExpanded()) SessionUiStyle.View.contentGap() else 0
|
||||
|
||||
fun isExpanded(): Boolean = body?.parent === this
|
||||
|
||||
fun toggle() {
|
||||
@@ -119,46 +168,170 @@ abstract class AbstractSessionPartView(
|
||||
return changed || detached || moved || icon
|
||||
}
|
||||
|
||||
protected fun bindHeader(vararg items: Component) {
|
||||
items.forEach { bind(it) }
|
||||
}
|
||||
|
||||
protected fun unbindHeader(vararg items: Component) {
|
||||
items.forEach { unbind(it) }
|
||||
}
|
||||
|
||||
protected fun refresh() {
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
protected open fun hoverColor(value: Boolean): Color? = null
|
||||
/**
|
||||
* Standard collapsed hover-preview request anchored to the card header, or null when the card is
|
||||
* not expandable, is already expanded, or has no [present] preview content. [kind]/[name] are the
|
||||
* telemetry attributes (e.g. `"tool"`/`"bash"`, `"part"`/`"reasoning"`). [body] is built lazily
|
||||
* when the popup actually shows; its disposable is owned by the popup controller and disposed on
|
||||
* hide, so subclasses just build fresh, self-contained content.
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun popup(kind: String, name: String, present: Boolean, body: () -> HeaderPopupBody): HeaderPopupRequest? {
|
||||
if (!expandable || isExpanded() || !present) return null
|
||||
return HeaderPopupRequest(row, body) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", kind to name))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a markdown-backed [HeaderPopupBody] from [markdown]. The created [MdView] is owned by a
|
||||
* fresh disposable that the popup controller disposes when the popup hides, so its editor is always
|
||||
* released. [options] renders through an editor-only code block (shell/diff style); null renders
|
||||
* prose. Height is bounded centrally by the popup panel to the same cap every popup shares; width
|
||||
* uses the wide or normal popup cap.
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun markdownPopupBody(
|
||||
style: SessionEditorStyle,
|
||||
markdown: String,
|
||||
wide: Boolean = true,
|
||||
options: MdCodeBlockOptions? = null,
|
||||
font: Font = style.editorFont,
|
||||
foreground: Color = style.editorForeground,
|
||||
link: ((String) -> Unit)? = null,
|
||||
afterSet: (MdView) -> Unit = {},
|
||||
): HeaderPopupBody {
|
||||
val owner = Disposer.newDisposable("Header popup body")
|
||||
val md = if (options != null) {
|
||||
MdViewFactory.create(style, null, MdCodeBlockFactory.default(options))
|
||||
} else {
|
||||
MdViewFactory.create(style, null)
|
||||
}
|
||||
Disposer.register(owner, md)
|
||||
link?.let { l -> md.addLinkListener { l(it.href) } }
|
||||
md.applyStyle(style)
|
||||
md.font = font
|
||||
md.foreground = foreground
|
||||
md.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.codeFont = style.editorFamily
|
||||
md.component.border = JBUI.Borders.empty()
|
||||
md.set(markdown)
|
||||
afterSet(md)
|
||||
val width = if (wide) SessionUiStyle.View.Popup.WIDE_MAX_WIDTH else SessionUiStyle.View.Popup.MAX_WIDTH
|
||||
return HeaderPopupBody(md.component, owner, SessionUiStyle.Colors.codeBlockBackground(), width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a popup body around an already-created Swing [component]. The disposable owner is still
|
||||
* routed through the same popup-controller lifecycle as markdown popups, so the content subtree is
|
||||
* released when the popup hides even when the component itself has no disposable resources today.
|
||||
*/
|
||||
@RequiresEdt
|
||||
protected fun componentPopupBody(
|
||||
component: JComponent,
|
||||
background: Color = SessionUiStyle.Colors.codeBlockBackground(),
|
||||
wide: Boolean = true,
|
||||
): HeaderPopupBody {
|
||||
val owner = Disposer.newDisposable("Header popup body")
|
||||
if (component is Disposable) Disposer.register(owner, component)
|
||||
val width = if (wide) SessionUiStyle.View.Popup.WIDE_MAX_WIDTH else SessionUiStyle.View.Popup.MAX_WIDTH
|
||||
return HeaderPopupBody(component, owner, background, width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Header background, hovered or not. The header keeps the same rounded fill whether the card is
|
||||
* collapsed or expanded — only the arrow toggles — because the card no longer draws an outline
|
||||
* for the expanded body to meet.
|
||||
*/
|
||||
protected open fun hoverColor(value: Boolean): Color =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
|
||||
/** Corner arc (scaled) for the header hover fill. */
|
||||
protected open fun hoverArc(): Int = JBUI.scale(SessionUiStyle.View.BLOCK_ARC)
|
||||
|
||||
override fun setHovered(value: Boolean) {
|
||||
hover?.invoke(this, value)
|
||||
val color = hoverColor(value) ?: return
|
||||
if (row.background?.rgb == color.rgb) return
|
||||
row.background = color
|
||||
val old = row.background
|
||||
row.isHovered = value
|
||||
val color = row.background
|
||||
if (old.rgb == color.rgb) return
|
||||
row.repaint()
|
||||
}
|
||||
|
||||
protected inner class Row : JPanel(BorderLayout(SessionUiStyle.View.Header.gap(), 0)) {
|
||||
var isHovered = false
|
||||
|
||||
// Painted by paintComponent so the hover fill can be rounded; a rounded opaque fill would
|
||||
// leave the corners stale, so the row stays non-opaque and the backdrop shows through.
|
||||
override fun isOpaque(): Boolean = false
|
||||
|
||||
override fun getBackground(): Color {
|
||||
return hoverColor(isHovered)
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
super.paintComponent(g)
|
||||
val color = hoverColor(isHovered)
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = color
|
||||
val arc = hoverArc()
|
||||
if (arc > 0) g2.fillRoundRect(0, 0, width, height, arc, arc) else g2.fillRect(0, 0, width, height)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun inside(e: MouseEvent): Boolean {
|
||||
val point = SwingUtilities.convertPoint(e.component, e.point, row)
|
||||
return row.contains(point)
|
||||
}
|
||||
|
||||
private fun bind(component: Component) {
|
||||
if (bound.contains(component)) return
|
||||
bound.add(component)
|
||||
component.addMouseListener(click)
|
||||
component.addMouseListener(mouse)
|
||||
/**
|
||||
* Attaches hover (and, for non-interactive elements, click-to-toggle) to [component] and its
|
||||
* whole subtree, keeping up with later children. Hover covers everything so leaving the row via
|
||||
* any nested element clears the fill. Click-to-toggle is bound only where the element does not
|
||||
* already own a mouse listener, so controls like file links and copy buttons keep their own
|
||||
* click action and do not also toggle the card. This means a control must install its own mouse
|
||||
* listener before it joins the header subtree: a control that adds its listener later would
|
||||
* already carry the toggle listener and would both act and toggle. All current call sites bind
|
||||
* their listeners in constructors, so this ordering holds.
|
||||
*/
|
||||
private fun watch(component: Component) {
|
||||
if (!watched.add(component)) return
|
||||
if (component.mouseListeners.isEmpty()) {
|
||||
component.addMouseListener(click)
|
||||
clickable.add(component)
|
||||
// Toggle-clickable header elements show the hand cursor while the card is toggleable.
|
||||
// Arrow visibility mirrors that state, and syncExpandable resets the cursor when it flips.
|
||||
if (arrow.isVisible) component.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
component.addMouseListener(pointer)
|
||||
if (component is Container) {
|
||||
component.addContainerListener(nested)
|
||||
component.components.forEach { watch(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun unbind(component: Component) {
|
||||
if (!bound.remove(component)) return
|
||||
component.removeMouseListener(click)
|
||||
component.removeMouseListener(mouse)
|
||||
component.cursor = Cursor.getDefaultCursor()
|
||||
private fun unwatch(component: Component) {
|
||||
if (!watched.remove(component)) return
|
||||
if (clickable.remove(component)) {
|
||||
component.removeMouseListener(click)
|
||||
component.cursor = Cursor.getDefaultCursor()
|
||||
}
|
||||
component.removeMouseListener(pointer)
|
||||
if (component is Container) {
|
||||
component.removeContainerListener(nested)
|
||||
component.components.forEach { unwatch(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun body(): JComponent {
|
||||
@@ -169,7 +342,7 @@ abstract class AbstractSessionPartView(
|
||||
|
||||
private fun syncCursor(cursor: Cursor): Boolean {
|
||||
var changed = false
|
||||
bound.forEach {
|
||||
clickable.forEach {
|
||||
if (it.cursor?.type != cursor.type) {
|
||||
it.cursor = cursor
|
||||
changed = true
|
||||
|
||||
+11
-17
@@ -92,8 +92,8 @@ class BaseQuestionView(
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
private val headerText: JBTextArea = makeText("", UiStyle.Colors.fg(), bold = true)
|
||||
private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false).apply {
|
||||
private val headerText: JBTextArea = makeText("", SessionUiStyle.Colors.foreground(), bold = true)
|
||||
private val descriptionText: JBTextArea = makeText("", SessionUiStyle.Text.Secondary.foreground(), bold = false).apply {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
@@ -386,25 +386,19 @@ class BaseQuestionView(
|
||||
}
|
||||
|
||||
private fun applyFont(area: JBTextArea, bold: Boolean) {
|
||||
val font = if (bold) style.headerFont else style.hintFont
|
||||
val font = if (bold) style.headerFont else SessionUiStyle.Text.Secondary.font(style)
|
||||
if (area.font != font) area.font = font
|
||||
}
|
||||
|
||||
private fun makeButton(id: String, text: String): JButton {
|
||||
val btn = object : JButton(text) {
|
||||
init {
|
||||
syncBackground()
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
syncBackground()
|
||||
}
|
||||
|
||||
private fun syncBackground() {
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
}
|
||||
}
|
||||
// Standard platform buttons: primary uses DEFAULT_STYLE_KEY (accent), the rest render as
|
||||
// ordinary secondary buttons. DarculaButtonUI paints the rounded fill and border itself
|
||||
// via isContentAreaFilled, so the button must be non-opaque. If it stays opaque, Swing
|
||||
// first fills the rectangular bounds with the component background; in the Islands Light
|
||||
// theme that color differs from the card surface and leaks as a stray frame around the
|
||||
// rounded button (other themes happen to match, so they look fine).
|
||||
val btn = JButton(text)
|
||||
btn.isOpaque = false
|
||||
btn.addActionListener {
|
||||
actionHandlers[id]?.invoke()
|
||||
focus?.invoke()
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.base
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Generic
|
||||
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
|
||||
|
||||
@@ -16,14 +17,14 @@ import com.intellij.ui.components.JBLabel
|
||||
class GenericView private constructor(
|
||||
content: Generic,
|
||||
private val label: JBLabel,
|
||||
) : SecondarySessionPartView(label, JBLabel()) {
|
||||
) : AbstractSessionPartView(label, JBLabel()) {
|
||||
|
||||
constructor(content: Generic) : this(content, JBLabel("[${content.type}]"))
|
||||
|
||||
override val contentId: String = content.id
|
||||
|
||||
init {
|
||||
label.foreground = UiStyle.Colors.weak()
|
||||
label.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
syncExpandable(false)
|
||||
border = null
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import javax.swing.JComponent
|
||||
|
||||
abstract class PrimarySessionPartView(
|
||||
header: JComponent,
|
||||
content: JComponent,
|
||||
expanded: Boolean = false,
|
||||
expandable: Boolean = true,
|
||||
) : AbstractSessionPartView(header, content, expanded, expandable) {
|
||||
init {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
row.isOpaque = true
|
||||
row.background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
row.border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.left(),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.right(),
|
||||
)
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width())
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import javax.swing.JComponent
|
||||
|
||||
abstract class SecondarySessionPartView(
|
||||
header: JComponent,
|
||||
content: () -> JComponent,
|
||||
expanded: Boolean = false,
|
||||
expandable: Boolean = true,
|
||||
) : AbstractSessionPartView(header, content, expanded, expandable) {
|
||||
|
||||
constructor(
|
||||
header: JComponent,
|
||||
content: JComponent,
|
||||
expanded: Boolean = false,
|
||||
expandable: Boolean = true,
|
||||
) : this(header, { content }, expanded, expandable)
|
||||
init {
|
||||
row.isOpaque = true
|
||||
row.background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
row.border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.left(),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
SessionUiStyle.View.Header.right(),
|
||||
)
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width())
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -12,6 +12,7 @@ import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
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.SessionViewIcons
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.iconButton
|
||||
@@ -171,8 +172,8 @@ class PermissionView(
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
card.applyStyle(style)
|
||||
desc.font = style.hintFont
|
||||
desc.foreground = UiStyle.Colors.weak()
|
||||
desc.font = SessionUiStyle.Text.Secondary.font(style)
|
||||
desc.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
rules.applyStyle(style)
|
||||
md?.let { applyCodeStyle(it) }
|
||||
for (dv in diffViews) {
|
||||
@@ -276,7 +277,7 @@ class PermissionView(
|
||||
view.applyStyle(style)
|
||||
view.font = style.transcriptFont
|
||||
view.foreground = style.editorForeground
|
||||
view.background = style.editorBackground
|
||||
view.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
view.preBg = MdCommon.defaults(style).preBg
|
||||
view.codeFont = style.editorFamily
|
||||
view.component.border = JBUI.Borders.empty()
|
||||
@@ -353,8 +354,8 @@ class PermissionView(
|
||||
caret.isSelectionVisible = false
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
foreground = UiStyle.Colors.weak()
|
||||
font = style.hintFont
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
font = SessionUiStyle.Text.Secondary.font(style)
|
||||
border = JBUI.Borders.empty()
|
||||
isVisible = false
|
||||
}
|
||||
@@ -705,8 +706,8 @@ internal class PermissionRulesView(
|
||||
|
||||
@RequiresEdt
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
hint.font = style.hintFont
|
||||
hint.foreground = UiStyle.Colors.weak()
|
||||
hint.font = SessionUiStyle.Text.Secondary.font(style)
|
||||
hint.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
field.applyStyle(style)
|
||||
}
|
||||
|
||||
@@ -771,11 +772,11 @@ internal class PermissionRulesView(
|
||||
ed.setBorder(JBUI.Borders.empty())
|
||||
ed.scrollPane.border = JBUI.Borders.empty()
|
||||
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
|
||||
ed.backgroundColor = style.editorBackground
|
||||
ed.scrollPane.background = style.editorBackground
|
||||
ed.backgroundColor = SessionUiStyle.Colors.codeBlockBackground()
|
||||
ed.scrollPane.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
ed.scrollPane.isOpaque = true
|
||||
ed.scrollPane.viewport.isOpaque = true
|
||||
ed.scrollPane.viewport.background = style.editorBackground
|
||||
ed.scrollPane.viewport.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
ed.settings.isUseSoftWraps = false
|
||||
ed.settings.isAdditionalPageAtBottom = false
|
||||
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
@@ -839,7 +840,7 @@ internal class PermissionRulesView(
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
val base = UiStyle.Colors.bg()
|
||||
val base = SessionUiStyle.Colors.sessionBackground()
|
||||
g2.color = when {
|
||||
active -> UiStyle.Colors.blend(base, if (approve) UiStyle.Colors.addedForeground() else UiStyle.Colors.removedForeground(), 0.15f)
|
||||
else -> UiStyle.Colors.actionHoverBackground()
|
||||
|
||||
+109
-187
@@ -7,206 +7,167 @@ import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class QuestionResultView(tool: Tool, private val selection: SessionSelection? = null) : PartView() {
|
||||
class QuestionResultView(
|
||||
tool: Tool,
|
||||
selection: SessionSelection? = null,
|
||||
private val parts: QuestionParts = questionParts(selection),
|
||||
) : AbstractSessionPartView(parts.header, { parts.body }) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var result = QuestionResultParser.parse(tool) ?: QuestionResult(emptyList(), emptyList())
|
||||
private var result = parse(tool)
|
||||
private var style = SessionEditorStyle.current()
|
||||
private val texts = mutableListOf<Pair<JBTextArea, Boolean>>()
|
||||
private val regs = mutableListOf<Disposable>()
|
||||
|
||||
private val root = object : JPanel(BorderLayout()) {
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
private val header = object : JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)) {
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
}
|
||||
private val glyph = JBLabel(SessionViewIcons.bubble)
|
||||
private val title = JBLabel()
|
||||
private val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
private val arrow = JBLabel()
|
||||
private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
private var pane: JPanel? = null
|
||||
|
||||
private val click = object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) { toggle() }
|
||||
}
|
||||
|
||||
private val mouse = object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) { setHovered(true) }
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
if (inside(e)) return
|
||||
setHovered(false)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
isOpaque = false
|
||||
|
||||
center.add(title, BorderLayout.WEST)
|
||||
center.add(sub, BorderLayout.CENTER)
|
||||
header.add(glyph, BorderLayout.WEST)
|
||||
header.add(center, BorderLayout.CENTER)
|
||||
header.add(arrow, BorderLayout.EAST)
|
||||
root.add(header, BorderLayout.NORTH)
|
||||
|
||||
listOf(header, glyph, title, sub, arrow, center).forEach {
|
||||
it.addMouseListener(click)
|
||||
it.addMouseListener(mouse)
|
||||
it.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
header.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
add(root, BorderLayout.CENTER)
|
||||
applyStyle(style)
|
||||
syncLabels()
|
||||
syncArrow()
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
parts.body.set(result.questions, result.answers)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
val next = QuestionResultParser.parse(content) ?: QuestionResult(emptyList(), emptyList())
|
||||
val next = parse(content)
|
||||
if (next == result) return
|
||||
result = next
|
||||
syncLabels()
|
||||
syncBody()
|
||||
if (isExpanded()) parts.body.set(result.questions, result.answers)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
val t = setFont(title, style.boldFont)
|
||||
val s = setFont(sub, style.smallFont)
|
||||
val label = t || s
|
||||
val body = texts.fold(false) { acc, item -> setFont(item.first, item.second) || acc }
|
||||
if (!label && !body) return
|
||||
refresh()
|
||||
var changed = setFont(parts.title, style.boldFont)
|
||||
changed = setFont(parts.sub, style.smallFont) || changed
|
||||
changed = parts.body.applyStyle(style) || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
resize?.invoke(this) { toggleBody() } ?: toggleBody()
|
||||
syncArrow()
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun toggleBody() {
|
||||
if (isExpanded()) {
|
||||
pane?.let { root.remove(it) }
|
||||
} else {
|
||||
root.add(body(), BorderLayout.CENTER)
|
||||
}
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
fun isExpanded(): Boolean = pane?.parent === root
|
||||
|
||||
fun labelText(): String = listOf(title.text, sub.text).filter { it.isNotBlank() }.joinToString(" ")
|
||||
fun labelText(): String = listOf(parts.title.text, parts.sub.text).filter { it.isNotBlank() }.joinToString(" ")
|
||||
|
||||
fun bodyText(): String = result.questions.mapIndexed { i, q ->
|
||||
val joined = result.answers.getOrNull(i)?.joinToString(", ").orEmpty()
|
||||
listOf(q, joined.ifBlank { KiloBundle.message("session.question.review.notAnswered") }).joinToString("\n")
|
||||
}.joinToString("\n")
|
||||
|
||||
fun bodyCreated(): Boolean = pane != null
|
||||
fun bodyCreated(): Boolean = hasBody()
|
||||
|
||||
fun bodyFonts(): List<Font> = texts.map { it.first.font }
|
||||
fun bodyFonts(): List<Font> = parts.body.fonts()
|
||||
|
||||
fun titleFont(): Font = title.font
|
||||
fun subFont(): Font = sub.font
|
||||
fun titleFont(): Font = parts.title.font
|
||||
|
||||
override fun dispose() {
|
||||
disposeRegs()
|
||||
texts.clear()
|
||||
}
|
||||
fun subFont(): Font = parts.sub.font
|
||||
|
||||
override fun dispose() = parts.body.dispose()
|
||||
|
||||
override fun dumpLabel(): String = "QuestionResultView#$contentId(${labelText()})"
|
||||
|
||||
private fun syncLabels() {
|
||||
parts.title.text = KiloBundle.message("session.question.result.title")
|
||||
val count = result.answers.count { it.isNotEmpty() }
|
||||
parts.sub.text = KiloBundle.message("session.question.result.answered", count)
|
||||
parts.sub.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
|
||||
private fun setFont(label: JBLabel, font: Font): Boolean {
|
||||
if (label.font == font) return false
|
||||
label.font = font
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = QuestionResultParser.parse(tool) != null
|
||||
}
|
||||
}
|
||||
|
||||
private fun body(): JPanel {
|
||||
pane?.let { return it }
|
||||
val panel = object : JPanel() {
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.brightColor(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
}
|
||||
pane = panel
|
||||
syncBody()
|
||||
return panel
|
||||
class QuestionParts(
|
||||
val header: PartHeader,
|
||||
val glyph: JBLabel,
|
||||
val title: JBLabel,
|
||||
val sub: JBLabel,
|
||||
val body: QuestionResultBody,
|
||||
)
|
||||
|
||||
private fun questionParts(selection: SessionSelection?): QuestionParts {
|
||||
val glyph = JBLabel(SessionViewIcons.bubble)
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
val header = PartHeader().apply {
|
||||
leading(glyph)
|
||||
left(title)
|
||||
titleGap()
|
||||
left(sub)
|
||||
}
|
||||
return QuestionParts(header, glyph, title, sub, QuestionResultBody(selection))
|
||||
}
|
||||
|
||||
private fun parse(tool: Tool) = QuestionResultParser.parse(tool) ?: QuestionResult(emptyList(), emptyList())
|
||||
|
||||
/** Lazy body for [QuestionResultView]: a transparent column of question/answer text areas. */
|
||||
class QuestionResultBody(private val selection: SessionSelection?) : JPanel() {
|
||||
|
||||
private var questions: List<String> = emptyList()
|
||||
private var answers: List<List<String>> = emptyList()
|
||||
private var style = SessionEditorStyle.current()
|
||||
private val texts = mutableListOf<Pair<JBTextArea, Boolean>>()
|
||||
private val regs = mutableListOf<Disposable>()
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
// Transparent answers body: the base separates it from the header with the standard gap, so
|
||||
// only content padding remains — no separator line.
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
|
||||
private fun syncLabels() {
|
||||
title.text = KiloBundle.message("session.question.result.title")
|
||||
val count = result.answers.count { it.isNotEmpty() }
|
||||
sub.text = KiloBundle.message("session.question.result.answered", count)
|
||||
sub.foreground = UiStyle.Colors.weak()
|
||||
fun set(questions: List<String>, answers: List<List<String>>) {
|
||||
this.questions = questions
|
||||
this.answers = answers
|
||||
rebuild()
|
||||
}
|
||||
|
||||
private fun syncBody() {
|
||||
val panel = pane ?: return
|
||||
panel.removeAll()
|
||||
fun applyStyle(style: SessionEditorStyle): Boolean {
|
||||
this.style = style
|
||||
return texts.fold(false) { acc, item -> setFont(item.first, item.second) || acc }
|
||||
}
|
||||
|
||||
fun fonts(): List<Font> = texts.map { it.first.font }
|
||||
|
||||
fun dispose() {
|
||||
disposeRegs()
|
||||
texts.clear()
|
||||
}
|
||||
|
||||
for ((i, q) in result.questions.withIndex()) {
|
||||
private fun rebuild() {
|
||||
removeAll()
|
||||
disposeRegs()
|
||||
texts.clear()
|
||||
for ((i, q) in questions.withIndex()) {
|
||||
val row = JPanel().apply {
|
||||
isOpaque = false
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
@@ -214,21 +175,23 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
}
|
||||
if (i > 0) row.border = JBUI.Borders.emptyTop(UiStyle.Gap.lg())
|
||||
|
||||
val qText = makeText(q, UiStyle.Colors.weak(), false)
|
||||
val qText = makeText(q, SessionUiStyle.Text.Secondary.foreground(), false)
|
||||
qText.alignmentX = Component.LEFT_ALIGNMENT
|
||||
qText.border = JBUI.Borders.emptyBottom(UiStyle.Gap.xs())
|
||||
row.add(qText)
|
||||
|
||||
val joined = result.answers.getOrNull(i)?.joinToString(", ").orEmpty()
|
||||
val joined = answers.getOrNull(i)?.joinToString(", ").orEmpty()
|
||||
val aText = makeText(
|
||||
joined.ifBlank { KiloBundle.message("session.question.review.notAnswered") },
|
||||
UiStyle.Colors.fg(),
|
||||
SessionUiStyle.Colors.foreground(),
|
||||
true,
|
||||
)
|
||||
aText.alignmentX = Component.LEFT_ALIGNMENT
|
||||
row.add(aText)
|
||||
panel.add(row)
|
||||
add(row)
|
||||
}
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun makeText(value: String, color: Color, bold: Boolean): JBTextArea {
|
||||
@@ -285,51 +248,10 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
regs.clear()
|
||||
}
|
||||
|
||||
private fun syncArrow() {
|
||||
arrow.icon = if (isExpanded()) SessionViewIcons.chevronExpanded else SessionViewIcons.chevronCollapsed
|
||||
}
|
||||
|
||||
override fun setHovered(value: Boolean) {
|
||||
hover?.invoke(this, value)
|
||||
val color =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
if (header.background?.rgb != color.rgb) {
|
||||
header.background = color
|
||||
header.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
root.border = JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.brightColor(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
)
|
||||
return
|
||||
}
|
||||
root.border = JBUI.Borders.empty(1)
|
||||
}
|
||||
|
||||
private fun inside(e: MouseEvent): Boolean {
|
||||
val point = SwingUtilities.convertPoint(e.component, e.point, header)
|
||||
return header.contains(point)
|
||||
}
|
||||
|
||||
private fun setFont(label: JBLabel, font: Font): Boolean {
|
||||
if (label.font == font) return false
|
||||
label.font = font
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setFont(area: JBTextArea, bold: Boolean): Boolean {
|
||||
val font = if (bold) style.boldFont else style.regularFont
|
||||
if (area.font == font) return false
|
||||
area.font = font
|
||||
return true
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -185,7 +185,7 @@ class QuestionView(
|
||||
card.applyStyle(style)
|
||||
customEditor?.let { ed ->
|
||||
style.applyTranscriptToField(ed)
|
||||
ed.background = style.editorBackground
|
||||
ed.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
syncEditorHeight(ed)
|
||||
}
|
||||
val changed = texts.fold(false) { acc, item -> setFont(item.first, item.second) || acc }
|
||||
@@ -223,7 +223,7 @@ class QuestionView(
|
||||
val total = q.items.size
|
||||
val shown = minOf(idx + 1, total)
|
||||
summary.text = KiloBundle.message("session.question.summary", shown, total)
|
||||
summary.foreground = UiStyle.Colors.weak()
|
||||
summary.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
summary.isVisible = total > 1
|
||||
nav.isVisible = total > 1
|
||||
topPanel.isVisible = total > 1
|
||||
@@ -331,7 +331,7 @@ class QuestionView(
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.lg())
|
||||
}
|
||||
val qText = text(item.question, UiStyle.Colors.weak())
|
||||
val qText = text(item.question, SessionUiStyle.Text.Secondary.foreground())
|
||||
qText.alignmentX = Component.LEFT_ALIGNMENT
|
||||
row.add(qText)
|
||||
|
||||
@@ -339,7 +339,7 @@ class QuestionView(
|
||||
val joined = answers.joinToString(", ")
|
||||
val answer = text(
|
||||
joined.ifBlank { KiloBundle.message("session.question.review.notAnswered") },
|
||||
UiStyle.Colors.fg(),
|
||||
SessionUiStyle.Colors.foreground(),
|
||||
true,
|
||||
)
|
||||
answer.alignmentX = Component.LEFT_ALIGNMENT
|
||||
@@ -442,7 +442,7 @@ class QuestionView(
|
||||
addMouseListener(press)
|
||||
}
|
||||
|
||||
val label = text(KiloBundle.message("session.question.custom.label"), UiStyle.Colors.fg(), true)
|
||||
val label = text(KiloBundle.message("session.question.custom.label"), SessionUiStyle.Colors.foreground(), true)
|
||||
label.alignmentX = Component.LEFT_ALIGNMENT
|
||||
label.addMouseListener(press)
|
||||
col.add(label)
|
||||
@@ -526,7 +526,7 @@ class QuestionView(
|
||||
}
|
||||
selection?.register(ed)?.let(regs::add)
|
||||
style.applyTranscriptToField(ed)
|
||||
ed.background = style.editorBackground
|
||||
ed.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
|
||||
// Pre-fill with saved text. This call also forces lazy document creation so
|
||||
// that addDocumentListener can install on a non-null document immediately.
|
||||
@@ -700,13 +700,13 @@ class QuestionView(
|
||||
layout = if (center) GridBagLayout() else BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
addMouseListener(press)
|
||||
}
|
||||
val label = text(opt.label, UiStyle.Colors.fg(), true)
|
||||
val label = text(opt.label, SessionUiStyle.Colors.foreground(), true)
|
||||
label.alignmentX = Component.LEFT_ALIGNMENT
|
||||
label.addMouseListener(press)
|
||||
col.add(label)
|
||||
|
||||
if (opt.description.isNotBlank()) {
|
||||
val desc = text(opt.description, UiStyle.Colors.weak())
|
||||
val desc = text(opt.description, SessionUiStyle.Text.Secondary.foreground())
|
||||
desc.alignmentX = Component.LEFT_ALIGNMENT
|
||||
desc.addMouseListener(press)
|
||||
col.add(desc)
|
||||
|
||||
+16
-3
@@ -1,6 +1,7 @@
|
||||
package ai.kilocode.client.session.views.todo
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.SessionSurface
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
@@ -36,6 +37,18 @@ class TodoListPanel(
|
||||
sync()
|
||||
}
|
||||
|
||||
// The todo list is a raised content surface painted with the editor background, rounded to the
|
||||
// same block arc as the card header. It stays non-opaque so the rounded corners reveal the
|
||||
// transparent card body behind them; children are clipped to the rounded shape.
|
||||
override fun paintComponent(g: Graphics) {
|
||||
SessionSurface.fill(g, width, height)
|
||||
super.paintComponent(g)
|
||||
}
|
||||
|
||||
override fun paintChildren(g: Graphics) {
|
||||
SessionSurface.clipped(g, width, height) { super.paintChildren(it) }
|
||||
}
|
||||
|
||||
fun update(todos: List<TodoDto>, hiddenBefore: Int = 0, hiddenAfter: Int = 0) {
|
||||
val size = todos.size != items.size
|
||||
items = todos
|
||||
@@ -52,8 +65,8 @@ class TodoListPanel(
|
||||
this.style = style
|
||||
prior.font = style.smallFont
|
||||
later.font = style.smallFont
|
||||
prior.foreground = UiStyle.Colors.weak()
|
||||
later.foreground = UiStyle.Colors.weak()
|
||||
prior.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
later.foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
rows.forEachIndexed { index, row -> row.update(items[index], style) }
|
||||
syncHidden()
|
||||
}
|
||||
@@ -136,7 +149,7 @@ class TodoListPanel(
|
||||
text.font = style.regularFont
|
||||
text.foreground = when {
|
||||
!done -> style.editorForeground
|
||||
else -> UiStyle.Colors.weak()
|
||||
else -> SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-20
@@ -4,20 +4,23 @@ 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.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.base.PrimarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.rpc.dto.TodoDto
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Font
|
||||
import javax.swing.JComponent
|
||||
|
||||
class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
PrimarySessionPartView(parts.header, parts.list, expanded = true) {
|
||||
AbstractSessionPartView(parts.header, parts.list, expanded = true) {
|
||||
|
||||
override val contentId = tool.id
|
||||
|
||||
@@ -25,17 +28,9 @@ class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.left, parts.right)
|
||||
parts.list.border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()),
|
||||
)
|
||||
// Transparent list body: the base separates it from the header with the standard gap, so no
|
||||
// separator line is drawn here — only the content padding remains.
|
||||
parts.list.border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad())
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
@@ -46,6 +41,21 @@ class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
val data = rows(item)
|
||||
val present = data.todos.isNotEmpty() || data.before > 0 || data.after > 0
|
||||
return popup("part", "todo", present) { buildPopup(data) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun buildPopup(data: Rows) = componentPopupBody(
|
||||
TodoListPanel(data.todos, data.before, data.after).apply {
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad())
|
||||
applyStyle(style)
|
||||
},
|
||||
)
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
@@ -73,13 +83,11 @@ class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
|
||||
private fun sync() {
|
||||
parts.sub.text = subtitle(item)
|
||||
val view = item.todoView
|
||||
val compact = view?.mode == "compact"
|
||||
val rows = if (compact) view.todos else item.todos
|
||||
val data = rows(item)
|
||||
parts.list.update(
|
||||
rows,
|
||||
hiddenBefore = if (compact) view.hiddenBefore else 0,
|
||||
hiddenAfter = if (compact) view.hiddenAfter else 0,
|
||||
data.todos,
|
||||
hiddenBefore = data.before,
|
||||
hiddenAfter = data.after,
|
||||
)
|
||||
syncExpandable(true)
|
||||
refresh()
|
||||
@@ -103,7 +111,7 @@ class TodoParts(
|
||||
private fun todoParts(): TodoParts {
|
||||
val glyph = JBLabel(SessionViewIcons.checklist)
|
||||
val title = JBLabel(KiloBundle.message("session.part.todo.title"))
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val sub = JBLabel().apply { foreground = SessionUiStyle.Text.Secondary.foreground() }
|
||||
val header = PartHeader().apply {
|
||||
leading(glyph)
|
||||
left(title)
|
||||
@@ -120,6 +128,14 @@ private fun subtitle(tool: Tool): String {
|
||||
return "$done/$total"
|
||||
}
|
||||
|
||||
private data class Rows(val todos: List<TodoDto>, val before: Int, val after: Int)
|
||||
|
||||
private fun rows(tool: Tool): Rows {
|
||||
val view = tool.todoView
|
||||
if (view?.mode == "compact") return Rows(view.todos, view.hiddenBefore, view.hiddenAfter)
|
||||
return Rows(tool.todos, 0, 0)
|
||||
}
|
||||
|
||||
private fun setFont(component: JComponent, font: Font): Boolean {
|
||||
if (component.font == font) return false
|
||||
component.font = font
|
||||
|
||||
+15
-8
@@ -3,10 +3,11 @@ package ai.kilocode.client.session.views.tool
|
||||
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.popup.HeaderPopupRequest
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
@@ -19,7 +20,7 @@ abstract class BaseSearchToolView(
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts,
|
||||
private val repo: String? = null,
|
||||
) : SecondarySessionPartView(parts.header, { parts.scroll(tool) }) {
|
||||
) : AbstractSessionPartView(parts.header, { parts.scroll(tool) }) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
@@ -34,8 +35,6 @@ abstract class BaseSearchToolView(
|
||||
protected abstract fun viewName(): String
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.left, parts.right, parts.slot)
|
||||
parts.targets.forEach { bindHeader(it) }
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
@@ -53,7 +52,7 @@ abstract class BaseSearchToolView(
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
val height = row.preferredSize.height + expandedGap() + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@@ -66,6 +65,14 @@ abstract class BaseSearchToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
val md = toolBodyMarkdown(item)
|
||||
return popup("tool", item.name, md.isNotBlank()) {
|
||||
markdownPopupBody(style, md, options = POPUP_OPTS, foreground = bodyColor())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text).plus(targetTexts()).plus(parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
@@ -129,7 +136,7 @@ abstract class BaseSearchToolView(
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, toolTitle(item)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.sub, UiStyle.Colors.weak()) || changed
|
||||
changed = setForeground(parts.sub, SessionUiStyle.Text.Secondary.foreground()) || changed
|
||||
changed = syncTargets() || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
@@ -148,7 +155,7 @@ abstract class BaseSearchToolView(
|
||||
val text = values.getOrNull(index) ?: ""
|
||||
changed = setVisible(label, text.isNotBlank()) || changed
|
||||
changed = setTargetText(label, text) || changed
|
||||
changed = setForeground(label, UiStyle.Colors.fg()) || changed
|
||||
changed = setForeground(label, SessionUiStyle.Colors.foreground()) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
@@ -176,7 +183,7 @@ abstract class BaseSearchToolView(
|
||||
return body.applyStyle(style)
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else SessionUiStyle.Colors.foreground()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val body = parts.content ?: return 0
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.tool
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
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.layout.Stack
|
||||
import com.intellij.openapi.Disposable
|
||||
@@ -23,7 +24,7 @@ import javax.swing.JComponent
|
||||
@RequiresEdt
|
||||
internal fun diffOverflowPanel(open: () -> Unit): JComponent {
|
||||
val message = JBLabel(KiloBundle.message("diff.overflow.message")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
}
|
||||
val link = HyperlinkLabel(KiloBundle.message("diff.overflow.open")).apply {
|
||||
addHyperlinkListener { open() }
|
||||
|
||||
+17
-20
@@ -16,8 +16,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.session.views.base.AbstractSessionPartView
|
||||
import ai.kilocode.client.ui.DiffStatBadge
|
||||
import ai.kilocode.client.ui.ToolbarButtonAction
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
@@ -47,7 +46,7 @@ class EditToolView(
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool, openFile),
|
||||
private var body: EditBody = editBody(tool, selection, openFile),
|
||||
) : SecondarySessionPartView(parts.header, { body.mount(tool) }), UiDataProvider, SessionCopyTarget {
|
||||
) : AbstractSessionPartView(parts.header, { body.mount(tool) }), UiDataProvider, SessionCopyTarget {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
@@ -63,7 +62,7 @@ class EditToolView(
|
||||
)
|
||||
private val diffAnchor = hoverPlaceholder(diff)
|
||||
private val filesTag = JBLabel().apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
foreground = SessionUiStyle.Text.Secondary.foreground()
|
||||
font = JBFont.small()
|
||||
isVisible = false
|
||||
}
|
||||
@@ -75,11 +74,10 @@ class EditToolView(
|
||||
parts.left.next(parts.link)
|
||||
parts.left.next(filesTag)
|
||||
parts.left.next(PartHeader.centered(badge))
|
||||
parts.left.next(PartHeader.centered(diffAnchor))
|
||||
// parts.link is intentionally omitted: FileLinkLabel installs its own click handler that opens
|
||||
// the file, and binding it here would also toggle the card on the same click (see ReadToolView,
|
||||
// which likewise omits it). Header toggling still works via parts.left/row.
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.left, parts.right, parts.slot, filesTag, badge, diffAnchor)
|
||||
parts.left.next(diffAnchor)
|
||||
// The base binds click-to-toggle across the whole header subtree, skipping controls that own
|
||||
// a mouse listener. parts.link (FileLinkLabel) installs its own click handler that opens the
|
||||
// file, so it is skipped automatically and does not also toggle the card.
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
@@ -128,7 +126,7 @@ class EditToolView(
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + (body.panel()?.preferredSize?.height ?: 0)
|
||||
val height = row.preferredSize.height + expandedGap() + (body.panel()?.preferredSize?.height ?: 0)
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@@ -197,13 +195,8 @@ class EditToolView(
|
||||
internal fun codeEditors(): List<EditorTextField> = body.codeEditors()
|
||||
|
||||
@RequiresEdt
|
||||
override fun headerPopup(): HeaderPopupRequest? {
|
||||
if (isExpanded()) return null
|
||||
if (editDiff(item).isBlank()) return null
|
||||
return HeaderPopupRequest(row, build = { buildPopupBody() }) {
|
||||
Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "edit"))
|
||||
}
|
||||
}
|
||||
override fun headerPopup(): HeaderPopupRequest? =
|
||||
popup("tool", "edit", editDiff(item).isNotBlank()) { buildPopupBody() }
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
@@ -233,7 +226,7 @@ class EditToolView(
|
||||
val path = if (count > 1) null else editPath(item)
|
||||
changed = setFileTarget(parts, path, if (path == null) "" else tail(path)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
|
||||
changed = setForeground(parts.link, SessionUiStyle.Colors.foreground()) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
syncDiffAction(count)
|
||||
@@ -287,7 +280,7 @@ class EditToolView(
|
||||
// calls rebuild and sets its signature), so a follow-up update() here would be a no-op.
|
||||
val panel = popup.mount(item)
|
||||
popup.applyStyle(style)
|
||||
return HeaderPopupBody(panel, owner, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
|
||||
return HeaderPopupBody(panel, owner, SessionUiStyle.Colors.codeBlockBackground(), SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "EditToolView#$contentId(${labelText()})"
|
||||
@@ -342,10 +335,12 @@ private fun popupBody(tool: Tool, selection: SessionSelection?, openFile: Sessio
|
||||
|
||||
private fun diffBody(selection: SessionSelection?) = ToolMarkdownBody(
|
||||
MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.Bottom,
|
||||
border = MdCodeBlockBorder.None,
|
||||
maxLines = SessionUiStyle.View.Tool.DIFF_LINES,
|
||||
verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
editorOnly = true,
|
||||
horizontalPadding = 0,
|
||||
overlapScrollbar = true,
|
||||
),
|
||||
selection,
|
||||
render = ::diffMarkdown,
|
||||
@@ -363,6 +358,8 @@ internal val POPUP_OPTS = MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.None,
|
||||
verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
editorOnly = true,
|
||||
horizontalPadding = 0,
|
||||
overlapScrollbar = true,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+6
-9
@@ -19,7 +19,6 @@ import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.NamedColorUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Component
|
||||
@@ -184,7 +183,7 @@ class PatchBody(
|
||||
@RequiresEdt
|
||||
private fun header(file: EditFileChange): JComponent {
|
||||
val link = FileLinkLabel(openFile).apply {
|
||||
foreground = UiStyle.Colors.fg()
|
||||
foreground = SessionUiStyle.Colors.foreground()
|
||||
font = style.transcriptFont
|
||||
setTarget(file.path, tail(file.path))
|
||||
isVisible = true
|
||||
@@ -195,10 +194,6 @@ class PatchBody(
|
||||
.next(DiffStatBadge(file.additions, file.deletions))
|
||||
return JBUI.Panels.simplePanel(row).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLineBottom(NamedColorUtil.getBoundsColor()),
|
||||
JBUI.Borders.emptyLeft(SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +202,8 @@ class PatchBody(
|
||||
md.applyStyle(style)
|
||||
md.font = style.editorFont
|
||||
md.foreground = style.editorForeground
|
||||
md.background = style.editorBackground
|
||||
md.preBg = style.editorBackground
|
||||
md.background = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.preBg = SessionUiStyle.Colors.codeBlockBackground()
|
||||
md.codeFont = style.editorFamily
|
||||
md.component.border = JBUI.Borders.empty()
|
||||
rows.getOrNull(views.indexOf(md))?.let { installGutter(md, it) }
|
||||
@@ -225,10 +220,12 @@ class PatchBody(
|
||||
|
||||
private companion object {
|
||||
val DIFF_OPTS = MdCodeBlockOptions(
|
||||
border = MdCodeBlockBorder.Bottom,
|
||||
border = MdCodeBlockBorder.None,
|
||||
maxLines = SessionUiStyle.View.Tool.DIFF_LINES,
|
||||
verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
editorOnly = true,
|
||||
horizontalPadding = 0,
|
||||
overlapScrollbar = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user