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:
kirillk
2026-08-14 16:36:41 -04:00
318 changed files with 7953 additions and 2837 deletions
+6
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Remove installed agents from every writable configuration source and report removal failures.
+5
View File
@@ -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.
+5
View File
@@ -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.
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Stop memory auto-save from failing on OpenAI-compatible providers that stream by default.
-5
View File
@@ -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.
+5
View File
@@ -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.
-6
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Render the JetBrains session backdrop consistently and keep transcript hover backgrounds from getting stuck.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Stop startup from crashing on a database migration that another Kilo process already applied
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Use provider model catalogs instead of hardcoded model-name heuristics for reasoning variants.
+2 -2
View File
@@ -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
+45 -1
View File
@@ -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 }}
+1 -1
View File
@@ -21,5 +21,5 @@
"@types/react-dom": "^19.2.3",
"typescript": "^5.8.2"
},
"version": "7.4.20"
"version": "7.4.22"
}
+33 -33
View File
@@ -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
View File
@@ -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": {}
}
+1 -1
View File
@@ -38,5 +38,5 @@
"@typescript/native-preview": "catalog:",
"effect": "catalog:"
},
"version": "7.4.21"
"version": "7.4.22"
}
+6
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+11 -2
View File
@@ -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")
}),
)
})
+10
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+6 -6
View File
@@ -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 -1
View File
@@ -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",
+1 -1
View File
@@ -20,5 +20,5 @@
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:"
},
"version": "7.4.21"
"version": "7.4.22"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-console",
"version": "7.4.21",
"version": "7.4.22",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -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]
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2bb93bd5830bcfd208615e64c8b2b88855d1faa97cd7980df9d95cfada533d3
size 3561
oid sha256:dd306e8bb95d7457ef3b504ff46c44dcfa6b273a316021e2f920db29c2035021
size 4992
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c49479d1fffe96d14300617d48be269e5bffe41bc50f555b1ee3fbb09d8919e1
size 3896
oid sha256:d77e49dab17d964344cd0ca5618b1593f38c3d09f8dffffc2dca892477c5ff5c
size 5328
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f0e205d89dc56438971d53e7210f1de1b261e641666ccebf0b1a2f36c3811aa
size 4195
oid sha256:aa3a5abf6d9488d8d14db03b89e1c569dc7fc8181ee337478705d0af8bde4ea0
size 5624
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+23
View File
@@ -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.
+12
View File
@@ -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) }
}
@@ -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,
@@ -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
@@ -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)
@@ -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()
@@ -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())
}
}
@@ -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()
@@ -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() }
}
@@ -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()
@@ -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) }
}
}
@@ -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
}
@@ -149,6 +149,7 @@ open class SessionLayoutPanel(
pad: Insets = JBUI.emptyInsets(),
) : BorderLayoutPanel(), javax.swing.Scrollable {
init {
isOpaque = false
layout = SessionLayout(gap, pad)
}
@@ -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)
@@ -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()
}
}
}
@@ -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()
}
}
}
@@ -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) }
}
}
@@ -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)
@@ -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)
}
@@ -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,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 {
@@ -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)
}
@@ -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)
@@ -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)
@@ -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)
}
@@ -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()
@@ -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)
}
@@ -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)
@@ -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(),
@@ -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())
}
}
@@ -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())
}
@@ -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(
@@ -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
@@ -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)
@@ -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()
@@ -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
@@ -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,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
@@ -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)
}
}
@@ -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,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()
@@ -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()
}
}
@@ -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)
@@ -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()
}
}
@@ -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
@@ -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
@@ -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() }
@@ -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,
)
/**
@@ -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