From 419ddea52a3a5d2d27578df0528d46d6c55fb039 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 14:06:37 +0000 Subject: [PATCH 001/111] chore(upstream): implement compatibility base tracking for merges Update the upstream merge script to identify and link to the previous compatibility commit. This ensures that pre-merge transformations maintain a proper commit history by using multiple parents when a prior compatibility base is found, rather than creating disconnected commits. - Add `findLatestCompatCommit` to locate previous compatibility commits - Implement `commitTree` and `updateBranch` for low-level git manipulation - Update `merge.ts` to integrate compatibility base detection and multi-parent commits --- script/upstream/merge.ts | 19 ++++++++++++- script/upstream/utils/git.ts | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 01affc03caf..967ea1d9c78 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -429,6 +429,15 @@ async function main() { await git.createBranch(opencodeBranch) logger.info(`Created opencode branch: ${opencodeBranch}`) + const prior = await git.findLatestCompatCommit(config.baseBranch, targetVersion.commit) + if (prior) { + logger.info( + `Found previous compatibility base: ${prior.message} (${prior.commit.slice(0, 8)}) from upstream ${prior.upstream.slice(0, 8)}`, + ) + } else { + logger.warn("No previous compatibility base found; merge base will remain pristine upstream") + } + // Step 6: Apply ALL transformations to opencode branch (pre-merge) // This reduces conflicts by transforming upstream code to Kilo conventions BEFORE merging logger.step(6, 8, "Applying transformations to opencode branch (pre-merge)...") @@ -524,7 +533,15 @@ async function main() { // Commit all transformations await git.stageAll() - await git.commit(`refactor: kilo compat for ${targetVersion.tag}`) + const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` + if (prior) { + const tree = await git.writeTree() + const commit = await git.commitTree(tree, compatMessage, [targetVersion.commit, prior.commit]) + await git.updateBranch(opencodeBranch, commit) + await git.checkout(opencodeBranch) + } else { + await git.commit(compatMessage) + } logger.success("Committed pre-merge transformations") // Step 7: Merge into Kilo branch diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index a781276e983..d30cb2b99ea 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -15,6 +15,12 @@ export interface RemoteInfo { url: string } +export interface CompatBase { + commit: string + upstream: string + message: string +} + export async function getCurrentBranch(): Promise { const result = await $`git rev-parse --abbrev-ref HEAD`.text() return result.trim() @@ -177,6 +183,55 @@ export async function getCommitHash(ref: string): Promise { return result.trim() } +export async function getCommitParents(ref: string): Promise { + const result = await $`git show --no-patch --format=%P ${ref}`.text() + return result + .trim() + .split(/\s+/) + .filter((parent) => parent.length > 0) +} + +export async function writeTree(): Promise { + const result = await $`git write-tree`.text() + return result.trim() +} + +export async function commitTree(tree: string, message: string, parents: string[]): Promise { + const args = parents.flatMap((parent) => ["-p", parent]) + const result = await $`git commit-tree ${tree} ${args} -m ${message}`.text() + return result.trim() +} + +export async function updateBranch(name: string, commit: string): Promise { + await $`git update-ref refs/heads/${name} ${commit}` +} + +export async function findLatestCompatCommit(base: string, target: string): Promise { + const grep = "^refactor: kilo compat for " + const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() + if (result.exitCode !== 0) { + throw new Error(`Failed to search compatibility commits: ${result.stderr.toString()}`) + } + + const lines = result.stdout + .toString() + .trim() + .split("\n") + .filter((line) => line.length > 0) + + for (const line of lines) { + const [commit, message = ""] = line.split("\0") + if (!commit) continue + + const parents = await getCommitParents(commit) + for (const parent of parents) { + if (await isAncestor(parent, target)) return { commit, upstream: parent, message } + } + } + + return null +} + /** * Check if `commit` is an ancestor of `ref` (i.e. reachable from `ref`). * Uses `git merge-base --is-ancestor`, which exits 0 for yes, 1 for no. From 4b1c1930c2fd07a4dec556f138217e4953685594 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:54:19 +0000 Subject: [PATCH 002/111] fix: preserve upstream compat merge bases --- script/upstream/merge.ts | 2 +- script/upstream/utils/git.test.ts | 55 +++++++++++++++++++++++++++++++ script/upstream/utils/git.ts | 5 ++- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 script/upstream/utils/git.test.ts diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 967ea1d9c78..db3245c1c1d 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -536,7 +536,7 @@ async function main() { const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` if (prior) { const tree = await git.writeTree() - const commit = await git.commitTree(tree, compatMessage, [targetVersion.commit, prior.commit]) + const commit = await git.createMergeCommit(tree, compatMessage, targetVersion.commit, prior.commit) await git.updateBranch(opencodeBranch, commit) await git.checkout(opencodeBranch) } else { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts new file mode 100644 index 00000000000..5636241a2f7 --- /dev/null +++ b/script/upstream/utils/git.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { $ } from "bun" +import { createMergeCommit, findLatestCompatCommit, getCommitHash, updateBranch, writeTree } from "./git" + +const cwd = process.cwd() +let dir = "" + +async function commit(message: string) { + await $`git add -A`.quiet() + await $`git -c user.name=Test -c user.email=test@example.com commit -m ${message}`.quiet() + return getCommitHash("HEAD") +} + +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "kilo-upstream-git-")) + process.chdir(dir) + await $`git init -b upstream`.quiet() + await $`git config user.name Test`.quiet() + await $`git config user.email test@example.com`.quiet() +}) + +afterEach(async () => { + process.chdir(cwd) + await rm(dir, { recursive: true, force: true }) +}) + +test("finds previous compatibility commit for transformed merge base", async () => { + await Bun.write("brand.txt", "opencode A\n") + const old = await commit("release: v1.0.0") + + await Bun.write("brand.txt", "opencode B\n") + const target = await commit("release: v1.0.1") + + await $`git checkout -b main ${old}`.quiet() + await Bun.write("brand.txt", "kilo A\n") + const prior = await commit("refactor: kilo compat for v1.0.0") + + const found = await findLatestCompatCommit("main", target) + expect(found?.commit).toBe(prior) + expect(found?.upstream).toBe(old) + + await $`git checkout ${target}`.quiet() + await $`git checkout -b opencode-v1.0.1`.quiet() + await Bun.write("brand.txt", "kilo B\n") + await $`git add -A`.quiet() + const tree = await writeTree() + const next = await createMergeCommit(tree, "refactor: kilo compat for v1.0.1", target, prior) + await updateBranch("opencode-v1.0.1", next) + + const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() + expect(base).toBe(prior) +}) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index d30cb2b99ea..4e9ccea8197 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -196,9 +196,8 @@ export async function writeTree(): Promise { return result.trim() } -export async function commitTree(tree: string, message: string, parents: string[]): Promise { - const args = parents.flatMap((parent) => ["-p", parent]) - const result = await $`git commit-tree ${tree} ${args} -m ${message}`.text() +export async function createMergeCommit(tree: string, message: string, first: string, second: string): Promise { + const result = await $`git commit-tree ${tree} -p ${first} -p ${second} -m ${message}`.text() return result.trim() } From 4b22bfbc57c2ac1a3911a910ec59e1d045e6e540 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 07:57:01 +0000 Subject: [PATCH 003/111] fix: keep compat branches linear --- script/upstream/merge.ts | 2 +- script/upstream/utils/git.test.ts | 7 ++++--- script/upstream/utils/git.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index db3245c1c1d..e4ac18f7441 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -536,7 +536,7 @@ async function main() { const compatMessage = `refactor: kilo compat for ${targetVersion.tag}` if (prior) { const tree = await git.writeTree() - const commit = await git.createMergeCommit(tree, compatMessage, targetVersion.commit, prior.commit) + const commit = await git.createCommit(tree, compatMessage, prior.commit) await git.updateBranch(opencodeBranch, commit) await git.checkout(opencodeBranch) } else { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 5636241a2f7..999510bb42b 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" import { $ } from "bun" -import { createMergeCommit, findLatestCompatCommit, getCommitHash, updateBranch, writeTree } from "./git" +import { createCommit, findLatestCompatCommit, getCommitHash, getCommitParents, updateBranch, writeTree } from "./git" const cwd = process.cwd() let dir = "" @@ -27,7 +27,7 @@ afterEach(async () => { await rm(dir, { recursive: true, force: true }) }) -test("finds previous compatibility commit for transformed merge base", async () => { +test("finds previous compatibility commit for transformed base", async () => { await Bun.write("brand.txt", "opencode A\n") const old = await commit("release: v1.0.0") @@ -47,9 +47,10 @@ test("finds previous compatibility commit for transformed merge base", async () await Bun.write("brand.txt", "kilo B\n") await $`git add -A`.quiet() const tree = await writeTree() - const next = await createMergeCommit(tree, "refactor: kilo compat for v1.0.1", target, prior) + const next = await createCommit(tree, "refactor: kilo compat for v1.0.1", prior) await updateBranch("opencode-v1.0.1", next) const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() expect(base).toBe(prior) + expect(await getCommitParents(next)).toEqual([prior]) }) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index 4e9ccea8197..b25212de02a 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -196,8 +196,8 @@ export async function writeTree(): Promise { return result.trim() } -export async function createMergeCommit(tree: string, message: string, first: string, second: string): Promise { - const result = await $`git commit-tree ${tree} -p ${first} -p ${second} -m ${message}`.text() +export async function createCommit(tree: string, message: string, parent: string): Promise { + const result = await $`git commit-tree ${tree} -p ${parent} -m ${message}`.text() return result.trim() } From fe3724605fd10da25f2a0c624049eea373b4b284 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 08:00:21 +0000 Subject: [PATCH 004/111] fix: resolve compat bases from version tags --- script/upstream/utils/git.test.ts | 2 ++ script/upstream/utils/git.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 999510bb42b..f841d74bf59 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -33,8 +33,10 @@ test("finds previous compatibility commit for transformed base", async () => { await Bun.write("brand.txt", "opencode B\n") const target = await commit("release: v1.0.1") + await $`git tag v1.0.1 ${target}`.quiet() await $`git checkout -b main ${old}`.quiet() + await $`git tag v1.0.0 ${old}`.quiet() await Bun.write("brand.txt", "kilo A\n") const prior = await commit("refactor: kilo compat for v1.0.0") diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index b25212de02a..d8730d2d378 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -205,6 +205,19 @@ export async function updateBranch(name: string, commit: string): Promise await $`git update-ref refs/heads/${name} ${commit}` } +async function compatUpstream(message: string): Promise { + const prefix = "refactor: kilo compat for " + if (!message.startsWith(prefix)) return null + + const tag = message.slice(prefix.length).trim().split(/\s+/)[0] + if (!tag) return null + + const ref = `${tag}^{commit}` + const result = await $`git rev-parse ${ref}`.quiet().nothrow() + if (result.exitCode !== 0) return null + return result.stdout.toString().trim() +} + export async function findLatestCompatCommit(base: string, target: string): Promise { const grep = "^refactor: kilo compat for " const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() @@ -222,6 +235,9 @@ export async function findLatestCompatCommit(base: string, target: string): Prom const [commit, message = ""] = line.split("\0") if (!commit) continue + const upstream = await compatUpstream(message) + if (upstream && (await isAncestor(upstream, target))) return { commit, upstream, message } + const parents = await getCommitParents(commit) for (const parent of parents) { if (await isAncestor(parent, target)) return { commit, upstream: parent, message } From 5c14ec2bed7048a24071de856976cbda4362ddc9 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 08:10:36 +0000 Subject: [PATCH 005/111] fix: preserve upstream ancestry on kilo merge --- script/upstream/merge.ts | 4 ++++ script/upstream/utils/git.test.ts | 26 ++++++++++++++++++++++++-- script/upstream/utils/git.ts | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index e4ac18f7441..77f3faa4713 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -548,6 +548,10 @@ async function main() { logger.step(7, 8, "Merging into Kilo branch...") await git.checkout(kiloBranch) + if (prior) { + const linked = await git.recordAncestor(targetVersion.commit, `merge: record upstream ${targetVersion.tag}`) + if (linked) logger.info(`Recorded upstream ${targetVersion.tag} as Kilo branch ancestry`) + } const mergeResult = await git.merge(opencodeBranch) if (!mergeResult.success) { diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index f841d74bf59..7d1f5e3c904 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -3,7 +3,16 @@ import { mkdtemp, rm } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" import { $ } from "bun" -import { createCommit, findLatestCompatCommit, getCommitHash, getCommitParents, updateBranch, writeTree } from "./git" +import { + createCommit, + findLatestCompatCommit, + getCommitHash, + getCommitParents, + isAncestor, + recordAncestor, + updateBranch, + writeTree, +} from "./git" const cwd = process.cwd() let dir = "" @@ -51,8 +60,21 @@ test("finds previous compatibility commit for transformed base", async () => { const tree = await writeTree() const next = await createCommit(tree, "refactor: kilo compat for v1.0.1", prior) await updateBranch("opencode-v1.0.1", next) - const base = (await $`git merge-base main opencode-v1.0.1`.text()).trim() expect(base).toBe(prior) expect(await getCommitParents(next)).toEqual([prior]) + + await $`git checkout main`.quiet() + expect(await recordAncestor(target, "merge: record upstream v1.0.1")).toBe(true) + const link = await getCommitHash("HEAD") + expect(await getCommitParents(link)).toEqual([prior, target]) + expect(await isAncestor(target, link)).toBe(true) + + const linked = (await $`git merge-base main opencode-v1.0.1`.text()).trim() + expect(linked).toBe(prior) + + await $`git merge opencode-v1.0.1`.quiet() + const head = await getCommitHash("HEAD") + expect(await getCommitParents(head)).toEqual([link, next]) + expect(await isAncestor(target, head)).toBe(true) }) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index d8730d2d378..35588124e00 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -205,6 +205,16 @@ export async function updateBranch(name: string, commit: string): Promise await $`git update-ref refs/heads/${name} ${commit}` } +export async function recordAncestor(ref: string, message: string): Promise { + if (await isAncestor(ref, "HEAD")) return false + + const result = await $`git merge -s ours --no-ff ${ref} -m ${message}`.nothrow() + if (result.exitCode !== 0) { + throw new Error(`Failed to record ancestor ${ref}: ${result.stderr.toString()}`) + } + return true +} + async function compatUpstream(message: string): Promise { const prefix = "refactor: kilo compat for " if (!message.startsWith(prefix)) return null From 70fde2221e3cca5624c0a728b956483b3f2e345a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 14:34:13 +0000 Subject: [PATCH 006/111] refactor(upstream): improve compatibility commit lookup using semver Update `findLatestCompatCommit` to use semantic versioning when identifying the latest compatibility commit. This ensures the correct commit is selected even when upstream tags diverge or are not strictly linear. - Add semver parsing and comparison utilities - Implement `targetSemver` to resolve versions from tags or commit messages - Filter and sort compatibility candidates by version relative to the target version - Add test case for diverging upstream tags --- script/upstream/utils/git.test.ts | 30 +++++++++++++ script/upstream/utils/git.ts | 70 +++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/script/upstream/utils/git.test.ts b/script/upstream/utils/git.test.ts index 7d1f5e3c904..b51dd35ea4a 100644 --- a/script/upstream/utils/git.test.ts +++ b/script/upstream/utils/git.test.ts @@ -78,3 +78,33 @@ test("finds previous compatibility commit for transformed base", async () => { expect(await getCommitParents(head)).toEqual([link, next]) expect(await isAncestor(target, head)).toBe(true) }) + +test("finds previous compatibility commit when upstream tags diverge", async () => { + await Bun.write("brand.txt", "opencode 1.4.9\n") + const old = await commit("release: v1.4.9") + await $`git tag v1.4.9 ${old}`.quiet() + + await $`git checkout -b release-30 ${old}`.quiet() + await Bun.write("brand.txt", "opencode 1.14.30\n") + const side = await commit("release: v1.14.30") + await $`git tag v1.14.30 ${side}`.quiet() + + await $`git checkout -b release-31 ${old}`.quiet() + await Bun.write("brand.txt", "opencode 1.14.31\n") + const target = await commit("release: v1.14.31") + await $`git tag v1.14.31 ${target}`.quiet() + + await $`git checkout -b main ${old}`.quiet() + await Bun.write("brand.txt", "kilo 1.4.9\n") + const ancient = await commit("refactor: kilo compat for v1.4.9") + await Bun.write("brand.txt", "kilo 1.14.30\n") + const prior = await commit("refactor: kilo compat for v1.14.30") + + expect(await isAncestor(side, target)).toBe(false) + expect(await isAncestor(old, target)).toBe(true) + + const found = await findLatestCompatCommit("main", target) + expect(found?.commit).toBe(prior) + expect(found?.upstream).toBe(side) + expect(found?.commit).not.toBe(ancient) +}) diff --git a/script/upstream/utils/git.ts b/script/upstream/utils/git.ts index 69d965cd553..e23823ed9a4 100644 --- a/script/upstream/utils/git.ts +++ b/script/upstream/utils/git.ts @@ -21,6 +21,12 @@ export interface CompatBase { message: string } +type Semver = readonly [number, number, number] + +interface Candidate extends CompatBase { + version: Semver +} + export async function getCurrentBranch(): Promise { const result = await $`git rev-parse --abbrev-ref HEAD`.text() return result.trim() @@ -216,10 +222,7 @@ export async function recordAncestor(ref: string, message: string): Promise { - const prefix = "refactor: kilo compat for " - if (!message.startsWith(prefix)) return null - - const tag = message.slice(prefix.length).trim().split(/\s+/)[0] + const tag = compatTag(message) if (!tag) return null const ref = `${tag}^{commit}` @@ -228,6 +231,55 @@ async function compatUpstream(message: string): Promise { return result.stdout.toString().trim() } +function compatTag(message: string): string | null { + const prefix = "refactor: kilo compat for " + if (!message.startsWith(prefix)) return null + return message.slice(prefix.length).trim().split(/\s+/)[0] ?? null +} + +function parseSemver(value: string | undefined): Semver | null { + const match = value?.match(/^v?(\d+)\.(\d+)\.(\d+)$/) + if (!match) return null + const major = Number.parseInt(match[1] ?? "0", 10) + const minor = Number.parseInt(match[2] ?? "0", 10) + const patch = Number.parseInt(match[3] ?? "0", 10) + return [major, minor, patch] +} + +function compareSemver(a: Semver, b: Semver): number { + for (const idx of [0, 1, 2] as const) { + if (a[idx] < b[idx]) return -1 + if (a[idx] > b[idx]) return 1 + } + return 0 +} + +function exists(value: T | null): value is T { + return value !== null +} + +async function targetSemver(ref: string): Promise { + const tags = await getTagsForCommit(ref) + const tag = tags.find((item) => parseSemver(item) !== null) + const parsed = parseSemver(tag) + if (parsed) return parsed + + const message = await getCommitMessage(ref) + const match = message.match(/\bv?\d+\.\d+\.\d+\b/) + return parseSemver(match?.[0]) +} + +async function candidate(line: string): Promise { + const [commit, message = ""] = line.split("\0") + if (!commit) return null + + const version = parseSemver(compatTag(message) ?? undefined) + if (!version) return null + + const upstream = (await compatUpstream(message)) ?? (await getCommitParents(commit))[0] ?? commit + return { commit, upstream, message, version } +} + export async function findLatestCompatCommit(base: string, target: string): Promise { const grep = "^refactor: kilo compat for " const result = await $`git log --format=%H%x00%s --grep=${grep} ${base}`.quiet().nothrow() @@ -241,6 +293,16 @@ export async function findLatestCompatCommit(base: string, target: string): Prom .split("\n") .filter((line) => line.length > 0) + const targetVersion = await targetSemver(target) + if (targetVersion) { + const candidates = (await Promise.all(lines.map(candidate))) + .filter(exists) + .filter((item) => compareSemver(item.version, targetVersion) < 0) + .sort((a, b) => compareSemver(b.version, a.version)) + const latest = candidates[0] + if (latest) return { commit: latest.commit, upstream: latest.upstream, message: latest.message } + } + for (const line of lines) { const [commit, message = ""] = line.split("\0") if (!commit) continue From 8645f5b880dec88e2b416b8a4050eeb4dc5d0bb1 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 12:20:28 -0400 Subject: [PATCH 007/111] fix(jetbrains): align agent steps test assertions --- .../kilocode/backend/cli/GeneratedApiModelSerializationTest.kt | 2 +- .../ai/kilocode/backend/cli/ProjectModelSerializationTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt index 9ad5e710d4b..7ddd445175f 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt @@ -188,7 +188,7 @@ class GeneratedApiModelSerializationTest { assertEquals(true, obj.native) assertEquals(false, obj.hidden) assertEquals("#FF5733", obj.color) - assertEquals(5, obj.steps) + assertEquals(5.0, obj.steps) } @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt index fdd0f9ee416..093b62e9c6a 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt @@ -171,7 +171,7 @@ class ProjectModelSerializationTest { assertEquals(true, obj.native) assertEquals(false, obj.hidden) assertEquals("#FF5733", obj.color) - assertEquals(5, obj.steps) + assertEquals(5.0, obj.steps) } @Test From 88563fa6729d9265f4bb45566659be3d5a7f9135 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 12:26:20 -0400 Subject: [PATCH 008/111] feat(jetbrains): add session history --- .../backend/app/KiloBackendSessionManager.kt | 29 +++ .../kilocode/backend/cli/KiloCliDataParser.kt | 21 +++ .../backend/rpc/KiloSessionRpcApiImpl.kt | 4 + .../app/KiloBackendSessionManagerTest.kt | 59 ++++++ .../backend/cli/KiloCliDataParserTest.kt | 27 +++ .../kilocode/backend/testing/MockCliServer.kt | 7 + .../kilocode/client/app/KiloSessionService.kt | 22 ++- .../session/history/HistoryController.kt | 109 +++++++++++ .../client/session/history/HistoryItem.kt | 15 ++ .../session/history/HistoryListRenderer.kt | 38 ++++ .../client/session/history/HistoryModel.kt | 93 ++++++++++ .../session/history/HistoryModelEvent.kt | 30 +++ .../client/session/history/HistoryPanel.kt | 174 ++++++++++++++++++ .../resources/messages/KiloBundle.properties | 14 ++ .../session/history/HistoryControllerTest.kt | 169 +++++++++++++++++ .../client/testing/FakeSessionRpcApi.kt | 25 ++- .../ai/kilocode/rpc/KiloSessionRpcApi.kt | 4 + .../kotlin/ai/kilocode/rpc/dto/HistoryDto.kt | 18 ++ 18 files changed, 853 insertions(+), 5 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt create mode 100644 packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/HistoryDto.kt diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index 80ce9ab7946..f3e410622da 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -6,6 +6,7 @@ import ai.kilocode.log.KiloLog import ai.kilocode.jetbrains.api.client.DefaultApi import ai.kilocode.jetbrains.api.model.GlobalSession import ai.kilocode.jetbrains.api.model.SessionStatus +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.SessionDto import ai.kilocode.rpc.dto.SessionListDto import ai.kilocode.rpc.dto.SessionStatusDto @@ -155,6 +156,32 @@ class KiloBackendSessionManager( directories.remove(id) } + fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto { + val h = http ?: throw IllegalStateException("Session manager not started") + val url = base ?: throw IllegalStateException("Session manager not started") + val params = listOfNotNull( + "directory=${encode(dir)}", + cursor?.let { "cursor=${encode(it)}" }, + "limit=$limit", + gitUrl?.let { "gitUrl=${encode(it)}" }, + ).joinToString("&") + val path = "$url/kilo/cloud-sessions?$params" + + val request = Request.Builder() + .url(path) + .get() + .build() + + h.newCall(request).execute().use { response -> + val raw = response.body?.string().orEmpty() + if (!response.isSuccessful) { + log.warn("Cloud sessions failed: HTTP ${response.code}, body=$raw") + throw RuntimeException("Cloud sessions failed: HTTP ${response.code} — $raw") + } + return KiloCliDataParser.parseCloudSessions(raw) + } + } + fun seed(dir: String) { try { val raw = requireClient().sessionStatus(directory = dir) @@ -227,4 +254,6 @@ class KiloBackendSessionManager( next = s.next.toLong(), requestID = s.requestID.ifBlank { null }, ) + + private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index f06cf03c2f5..5bf0f0c14af 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -1,6 +1,8 @@ package ai.kilocode.backend.cli import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.DiffFileDto import ai.kilocode.rpc.dto.MessageDto @@ -258,6 +260,25 @@ object KiloCliDataParser { } } + fun parseCloudSessions(raw: String): CloudSessionListDto { + val obj = tryParseObject(raw) ?: return CloudSessionListDto(emptyList()) + val items = obj["cliSessions"]?.jsonArray ?: JsonArray(emptyList()) + val sessions = items.mapNotNull { elem -> + val item = runCatching { elem.jsonObject }.getOrNull() ?: return@mapNotNull null + CloudSessionDto( + id = item.str("session_id") ?: return@mapNotNull null, + title = item.str("title"), + createdAt = item.str("created_at") ?: return@mapNotNull null, + updatedAt = item.str("updated_at") ?: return@mapNotNull null, + version = item.num("version") ?: return@mapNotNull null, + ) + } + return CloudSessionListDto( + sessions = sessions, + nextCursor = obj.str("nextCursor"), + ) + } + fun parseModelState(raw: String): ModelStateDto { val obj = tryParseObject(raw) ?: return ModelStateDto() return ModelStateDto( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 26b98a02a9f..ded541bee63 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -9,6 +9,7 @@ import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager import ai.kilocode.log.ChatLogSummary import ai.kilocode.rpc.KiloSessionRpcApi import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto @@ -69,6 +70,9 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi { workspaces.get(dir).deleteSession(id) } + override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = + sessions.cloudSessions(directory, cursor, limit, gitUrl) + override suspend fun statuses(): Flow> = sessions.statuses diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt index 867ad3ecbf7..8de50bdd6a4 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt @@ -185,6 +185,65 @@ class KiloBackendSessionManagerTest { } } + @Test + fun `cloudSessions returns cloud sessions from server`() = runBlocking { + mock.cloudSessions = """{ + "cliSessions": [ + {"session_id":"cloud_1","title":"Cloud One","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z","version":2} + ], + "nextCursor": "next_1" + }""" + val app = setup() + ready(app) + + val result = app.sessions.cloudSessions("/repo", null, 50, null) + + assertEquals(1, result.sessions.size) + assertEquals("cloud_1", result.sessions[0].id) + assertEquals("Cloud One", result.sessions[0].title) + assertEquals("next_1", result.nextCursor) + } + + @Test + fun `cloudSessions passes filters and cursor`() = runBlocking { + val app = setup() + ready(app) + + app.sessions.cloudSessions("/repo path", "cur 1", 25, "git@github.com:Kilo-Org/kilo.git") + + val path = mock.lastCloudSessionsPath ?: error("missing cloud sessions request") + assertTrue(path.startsWith("/kilo/cloud-sessions?")) + val decoded = URLDecoder.decode(path, "UTF-8") + assertTrue(decoded.contains("directory=/repo path"), path) + assertTrue(decoded.contains("cursor=cur 1"), path) + assertTrue(decoded.contains("limit=25"), path) + assertTrue(decoded.contains("gitUrl=git@github.com:Kilo-Org/kilo.git"), path) + } + + @Test + fun `cloudSessions throws when not started`() = runBlocking { + val app = setup() + + assertFailsWith { + app.sessions.cloudSessions("/test", null, 50, null) + } + } + + @Test + fun `cloudSessions surfaces server failure`() = runBlocking { + mock.cloudSessionsStatus = 500 + mock.cloudSessions = """{"error":"boom"}""" + val app = setup() + ready(app) + + val err = assertFailsWith { + app.sessions.cloudSessions("/test", null, 50, null) + } + + assertTrue(err.message.orEmpty().contains("HTTP 500")) + assertTrue(err.message.orEmpty().contains("boom")) + } + // ------ Session create ------ @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 63037b9dde9..10ffdaadda0 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -398,6 +398,33 @@ class KiloCliDataParserTest { assertNull(result.summary) } + @Test + fun `parseCloudSessions maps cloud session list`() { + val raw = """{ + "cliSessions": [ + {"session_id":"cloud_1","title":"Cloud One","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z","version":2}, + {"session_id":"cloud_2","title":null,"created_at":"2026-01-03T00:00:00Z","updated_at":"2026-01-04T00:00:00Z","version":3.5,"extra":true} + ], + "nextCursor": "cursor_2" + }""" + + val result = KiloCliDataParser.parseCloudSessions(raw) + + assertEquals(2, result.sessions.size) + assertEquals("cloud_1", result.sessions[0].id) + assertEquals("Cloud One", result.sessions[0].title) + assertEquals("2026-01-02T00:00:00Z", result.sessions[0].updatedAt) + assertEquals(2.0, result.sessions[0].version) + assertNull(result.sessions[1].title) + assertEquals("cursor_2", result.nextCursor) + } + + @Test + fun `parseCloudSessions tolerates malformed response`() { + assertEquals(emptyList(), KiloCliDataParser.parseCloudSessions("not json").sessions) + assertNull(KiloCliDataParser.parseCloudSessions("{}").nextCursor) + } + // ================================================================ // parseMessages // ================================================================ diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 5d794ae5f20..d88ed79108d 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -61,6 +61,9 @@ class MockCliServer : AutoCloseable { @Volatile var sessionGetStatus = 200 @Volatile var sessionDeleteStatus = 200 @Volatile var sessionStatusesStatus = 200 + @Volatile var cloudSessions = """{"cliSessions":[],"nextCursor":null}""" + @Volatile var cloudSessionsStatus = 200 + @Volatile var lastCloudSessionsPath: String? = null /** Configurable delay for all endpoint responses (ms). 0 = no delay. */ @Volatile var responseDelay: Long = 0 @@ -211,6 +214,10 @@ class MockCliServer : AutoCloseable { lastExperimentalSessionPath = path respond(output, recentSessionsStatus, recentSessions) } + bare == "/kilo/cloud-sessions" -> { + lastCloudSessionsPath = path + respond(output, cloudSessionsStatus, cloudSessions) + } bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index 7e131c93f7e..0cb381ceb02 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -5,6 +5,7 @@ package ai.kilocode.client.app import ai.kilocode.log.ChatLogSummary import ai.kilocode.rpc.KiloSessionRpcApi import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto @@ -14,6 +15,7 @@ import ai.kilocode.rpc.dto.PromptDto import ai.kilocode.rpc.dto.QuestionReplyDto import ai.kilocode.rpc.dto.QuestionRequestDto import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionListDto import ai.kilocode.rpc.dto.SessionStatusDto import com.intellij.openapi.components.Service import ai.kilocode.log.KiloLog @@ -75,14 +77,19 @@ class KiloSessionService internal constructor( fun refresh(dir: String) { cs.launch { try { - val result = call { list(dir) } - _sessions.value = result.sessions + list(dir) } catch (e: Exception) { LOG.warn("kind=session-list dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e) } } } + suspend fun list(dir: String): SessionListDto { + val result = call { list(dir) } + _sessions.value = result.sessions + return result + } + /** Load recent sessions for the current worktree family. */ suspend fun recent(dir: String, limit: Int): List = call { recent(dir, limit) }.sessions @@ -100,14 +107,21 @@ class KiloSessionService internal constructor( fun delete(id: String, dir: String) { cs.launch { try { - call { delete(id, dir) } - refresh(dir) + deleteSession(id, dir) } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(id)} kind=session delete=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e) } } } + suspend fun deleteSession(id: String, dir: String) { + call { delete(id, dir) } + list(dir) + } + + suspend fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = + call { cloudSessions(dir, cursor, limit, gitUrl) } + /** Register a worktree directory override for a session. */ fun setDirectory(id: String, dir: String) { cs.launch { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt new file mode 100644 index 00000000000..4861056eff7 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -0,0 +1,109 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.app.KiloSessionService +import ai.kilocode.client.app.Workspace +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.SessionDto +import com.intellij.openapi.application.ApplicationManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class HistoryController( + private val sessions: KiloSessionService, + private val workspace: Workspace, + private val cs: CoroutineScope, + private val open: (HistoryItem) -> Unit = {}, +) { + companion object { + const val CLOUD_LIMIT = 50 + } + + val model = HistoryModel() + + private var gitUrl: String? = null + + fun loadLocal() { + edt { model.startLocal() } + cs.launch { + try { + val result = sessions.list(workspace.directory) + val items = result.sessions.map(::localItem) + edt { model.setLocal(items) } + } catch (e: Exception) { + edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local")) } + } + } + } + + fun loadCloud(reset: Boolean = true, gitUrl: String? = null) { + if (reset) this.gitUrl = gitUrl + val cursor = if (reset) null else model.cursor + edt { model.startCloud(reset) } + cs.launch { + try { + val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, this@HistoryController.gitUrl) + val items = result.sessions.map(::cloudItem) + edt { model.setCloud(items, result.nextCursor, append = !reset) } + } catch (e: Exception) { + edt { model.error(HistorySource.CLOUD, e.message ?: KiloBundle.message("history.error.cloud")) } + } + } + } + + fun loadMoreCloud() { + if (model.cursor == null || model.cloudLoading) return + loadCloud(reset = false) + } + + fun selectSource(source: HistorySource) { + edt { model.select(source) } + } + + fun delete(item: HistoryItem) { + if (item.source == HistorySource.CLOUD) { + edt { model.error(HistorySource.CLOUD, KiloBundle.message("history.error.cloud.delete")) } + return + } + edt { model.startDelete(item.id) } + cs.launch { + try { + sessions.deleteSession(item.id, item.directory ?: workspace.directory) + edt { model.deleted(item.id) } + } catch (e: Exception) { + edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local.delete")) } + } + } + } + + fun open(item: HistoryItem) { + edt { open(item) } + } +} + +private fun edt(block: () -> Unit) { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + block() + return + } + app.invokeLater(block) +} + +private fun localItem(session: SessionDto) = HistoryItem( + id = session.id, + source = HistorySource.LOCAL, + title = session.title, + createdAt = session.time.created.toString(), + updatedAt = session.time.updated.toString(), + directory = session.directory, + local = session, +) + +private fun cloudItem(session: CloudSessionDto) = HistoryItem( + id = session.id, + source = HistorySource.CLOUD, + title = session.title.orEmpty(), + createdAt = session.createdAt, + updatedAt = session.updatedAt, +) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt new file mode 100644 index 00000000000..5d63abc696f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt @@ -0,0 +1,15 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.rpc.dto.SessionDto + +enum class HistorySource { LOCAL, CLOUD } + +data class HistoryItem( + val id: String, + val source: HistorySource, + val title: String, + val createdAt: String, + val updatedAt: String, + val directory: String? = null, + val local: SessionDto? = null, +) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt new file mode 100644 index 00000000000..d45d81b169f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -0,0 +1,38 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.ui.components.JBLabel +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 +import javax.swing.JList +import javax.swing.ListCellRenderer + +class HistoryListRenderer : BorderLayoutPanel(), ListCellRenderer { + private val title = JBLabel() + private val meta = JBLabel() + + init { + border = JBUI.Borders.empty(8, 12, 8, 12) + add(title, BorderLayout.CENTER) + add(meta, BorderLayout.EAST) + } + + override fun getListCellRendererComponent( + list: JList, + value: HistoryItem?, + index: Int, + selected: Boolean, + focus: Boolean, + ): Component { + isOpaque = selected + background = if (selected) list.selectionBackground else list.background + title.foreground = if (selected) list.selectionForeground else UIUtil.getLabelForeground() + meta.foreground = if (selected) list.selectionForeground else UIUtil.getContextHelpForeground() + title.text = value?.title?.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled") + meta.text = value?.updatedAt.orEmpty() + return this + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt new file mode 100644 index 00000000000..a5625ccff66 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -0,0 +1,93 @@ +package ai.kilocode.client.session.history + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer + +class HistoryModel { + private val listeners = mutableListOf() + + var source = HistorySource.LOCAL + private set + var local = emptyList() + private set + var cloud = emptyList() + private set + var localLoading = false + private set + var cloudLoading = false + private set + var localError: String? = null + private set + var cloudError: String? = null + private set + var cursor: String? = null + private set + + fun addListener(parent: Disposable, listener: HistoryModelEvent.Listener) { + listeners.add(listener) + Disposer.register(parent) { listeners.remove(listener) } + } + + fun items(): List = if (source == HistorySource.LOCAL) local else cloud + + fun select(value: HistorySource) { + if (source == value) return + source = value + fire(HistoryModelEvent.SourceChanged(value)) + } + + fun startLocal() { + localLoading = true + localError = null + fire(HistoryModelEvent.LocalLoading) + } + + fun setLocal(items: List) { + local = items + localLoading = false + localError = null + fire(HistoryModelEvent.LocalLoaded(items.size)) + } + + fun startCloud(reset: Boolean) { + cloudLoading = true + cloudError = null + if (reset) { + cloud = emptyList() + cursor = null + } + fire(HistoryModelEvent.CloudLoading(reset)) + } + + fun setCloud(items: List, next: String?, append: Boolean) { + cloud = if (append) cloud + items else items + cursor = next + cloudLoading = false + cloudError = null + fire(HistoryModelEvent.CloudLoaded(items.size, next)) + } + + fun startDelete(id: String) { + fire(HistoryModelEvent.DeleteStarted(id)) + } + + fun deleted(id: String) { + local = local.filterNot { it.id == id } + fire(HistoryModelEvent.Deleted(id)) + } + + fun error(source: HistorySource, message: String) { + if (source == HistorySource.LOCAL) { + localLoading = false + localError = message + } else { + cloudLoading = false + cloudError = message + } + fire(HistoryModelEvent.Error(source, message)) + } + + private fun fire(event: HistoryModelEvent) { + for (listener in listeners) listener.onEvent(event) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt new file mode 100644 index 00000000000..545bcf40e15 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt @@ -0,0 +1,30 @@ +package ai.kilocode.client.session.history + +sealed class HistoryModelEvent { + data object LocalLoading : HistoryModelEvent() + data class LocalLoaded(val count: Int) : HistoryModelEvent() { + override fun toString() = "LocalLoaded count=$count" + } + data class CloudLoading(val reset: Boolean) : HistoryModelEvent() { + override fun toString() = "CloudLoading reset=$reset" + } + data class CloudLoaded(val count: Int, val nextCursor: String?) : HistoryModelEvent() { + override fun toString() = "CloudLoaded count=$count next=$nextCursor" + } + data class DeleteStarted(val id: String) : HistoryModelEvent() { + override fun toString() = "DeleteStarted $id" + } + data class Deleted(val id: String) : HistoryModelEvent() { + override fun toString() = "Deleted $id" + } + data class Error(val source: HistorySource, val message: String) : HistoryModelEvent() { + override fun toString() = "Error $source $message" + } + data class SourceChanged(val source: HistorySource) : HistoryModelEvent() { + override fun toString() = "SourceChanged $source" + } + + fun interface Listener { + fun onEvent(event: HistoryModelEvent) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt new file mode 100644 index 00000000000..3746a6329fe --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -0,0 +1,174 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.ui.CollectionListModel +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.SearchTextField +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import java.awt.BorderLayout +import java.awt.FlowLayout +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.ListSelectionModel +import javax.swing.event.DocumentEvent + +class HistoryPanel( + parent: Disposable, + private val controller: HistoryController, + private val gitUrl: () -> String? = { null }, +) : BorderLayoutPanel(), Disposable { + private val rows = CollectionListModel() + private val search = SearchTextField(false).apply { + textEditor.emptyText.text = KiloBundle.message("history.search.placeholder") + } + private val list = JBList(rows).apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + cellRenderer = HistoryListRenderer() + emptyText.text = KiloBundle.message("history.empty") + addListSelectionListener { sync() } + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount < 2) return + selectedValue?.let(controller::open) + } + }) + } + private val local = JButton(KiloBundle.message("history.tab.local")) + private val cloud = JButton(KiloBundle.message("history.tab.cloud")) + private val repo = JBCheckBox(KiloBundle.message("history.cloud.repo.only")) + private val delete = JButton(KiloBundle.message("history.delete.text")) + private val more = JButton(KiloBundle.message("history.cloud.load.more")) + private val status = JBLabel() + + init { + Disposer.register(parent, this) + border = JBUI.Borders.empty(8) + add(header(), BorderLayout.NORTH) + add(JBScrollPane(list), BorderLayout.CENTER) + add(footer(), BorderLayout.SOUTH) + bind(parent) + sync() + controller.loadLocal() + } + + val component: JComponent get() = this + + private fun header(): JComponent { + local.addActionListener { + controller.selectSource(HistorySource.LOCAL) + controller.loadLocal() + } + cloud.addActionListener { + controller.selectSource(HistorySource.CLOUD) + controller.loadCloud(gitUrl = if (repo.isSelected) gitUrl() else null) + } + repo.addActionListener { + if (controller.model.source == HistorySource.CLOUD) { + controller.loadCloud(gitUrl = if (repo.isSelected) gitUrl() else null) + } + } + search.textEditor.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + sync() + } + }) + return BorderLayoutPanel().apply { + add(BorderLayoutPanel().apply { + add(local, BorderLayout.WEST) + add(cloud, BorderLayout.CENTER) + add(repo, BorderLayout.EAST) + }, BorderLayout.NORTH) + add(search, BorderLayout.CENTER) + } + } + + private fun footer(): JComponent { + delete.addActionListener { list.selectedValue?.let(controller::delete) } + more.addActionListener { controller.loadMoreCloud() } + return BorderLayoutPanel().apply { + add(status, BorderLayout.CENTER) + add(BorderLayoutPanel().apply { + layout = FlowLayout(FlowLayout.RIGHT, JBUI.scale(4), 0) + add(delete) + add(more) + }, BorderLayout.EAST) + } + } + + private fun bind(parent: Disposable) { + controller.model.addListener(parent) { event -> + when (event) { + is HistoryModelEvent.LocalLoading, + is HistoryModelEvent.LocalLoaded, + is HistoryModelEvent.CloudLoading, + is HistoryModelEvent.CloudLoaded, + is HistoryModelEvent.Deleted, + is HistoryModelEvent.Error, + is HistoryModelEvent.SourceChanged -> sync() + is HistoryModelEvent.DeleteStarted -> Unit + } + } + } + + private fun sync() { + val query = search.text.trim().lowercase() + val items = controller.model.items().filter { item -> + query.isEmpty() || item.title.lowercase().contains(query) || item.id.lowercase().contains(query) + } + rows.replaceAll(items) + local.isEnabled = controller.model.source != HistorySource.LOCAL + cloud.isEnabled = controller.model.source != HistorySource.CLOUD + repo.isVisible = controller.model.source == HistorySource.CLOUD + delete.isEnabled = controller.model.source == HistorySource.LOCAL && list.selectedValue != null + more.isVisible = controller.model.source == HistorySource.CLOUD + more.isEnabled = controller.model.cursor != null && !controller.model.cloudLoading + status.text = statusText() + } + + private fun statusText(): String { + val model = controller.model + if (model.localLoading || model.cloudLoading) return KiloBundle.message("history.loading") + val error = if (model.source == HistorySource.LOCAL) model.localError else model.cloudError + if (error != null) return error + return "" + } + + internal fun itemCount() = rows.size + + internal fun selectedSource() = controller.model.source + + internal fun select(index: Int) { + list.selectedIndex = index + sync() + } + + internal fun clickDelete() { + delete.doClick() + } + + internal fun clickCloud() { + cloud.doClick() + } + + internal fun clickMore() { + more.doClick() + } + + internal fun setSearch(value: String) { + search.text = value + sync() + } + + override fun dispose() { + // no-op + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index b648b527d95..6f7062d1aed 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -66,6 +66,20 @@ model.picker.free=Free model.picker.reset=Reset model to default reasoning.picker.tooltip=Select reasoning effort +history.tab.local=Local +history.tab.cloud=Cloud +history.search.placeholder=Search sessions +history.empty=No sessions +history.loading=Loading... +history.untitled=Untitled +history.delete.text=Delete +history.cloud.load.more=Load more +history.cloud.repo.only=Only this repository +history.error.local=Failed to load local history +history.error.cloud=Failed to load cloud history +history.error.local.delete=Failed to delete session +history.error.cloud.delete=Cloud sessions cannot be deleted yet + action.Kilo.Settings.text=Settings action.Kilo.Settings.description=Kilo Code settings action.Kilo.NewSession.text=New Session diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt new file mode 100644 index 00000000000..68252a2afd4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -0,0 +1,169 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.app.KiloSessionService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.app.Workspace +import ai.kilocode.client.testing.FakeSessionRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.KiloWorkspaceStateDto +import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto +import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionTimeDto +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +@Suppress("UnstableApiUsage") +class HistoryControllerTest : BasePlatformTestCase() { + private lateinit var scope: CoroutineScope + private lateinit var parent: Disposable + private lateinit var rpc: FakeSessionRpcApi + private lateinit var sessions: KiloSessionService + private lateinit var workspace: Workspace + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob()) + parent = Disposer.newDisposable("history") + rpc = FakeSessionRpcApi() + sessions = KiloSessionService(project, scope, rpc) + val workspaces = KiloWorkspaceService(scope, FakeWorkspaceRpcApi().also { + it.state.value = KiloWorkspaceStateDto(status = KiloWorkspaceStatusDto.READY) + }) + workspace = workspaces.workspace("/test") + } + + override fun tearDown() { + try { + Disposer.dispose(parent) + scope.cancel() + } finally { + super.tearDown() + } + } + + fun `test local load maps sessions and events`() { + rpc.listed += session("ses_1", "Local One") + val controller = controller() + val events = collect(controller) + + controller.loadLocal() + flush() + + assertEquals(listOf("/test"), rpc.lists) + assertEquals(1, controller.model.local.size) + assertEquals("ses_1", controller.model.local[0].id) + assertEquals("Local One", controller.model.local[0].title) + assertEquals("LocalLoading\nLocalLoaded count=1", events.joinToString("\n")) + } + + fun `test cloud load maps sessions and supports load more`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + rpc.cloudCursor = "next_1" + val controller = controller() + + controller.loadCloud(gitUrl = "git@example.com:repo.git") + flush() + + assertEquals(1, controller.model.cloud.size) + assertEquals("cloud_1", controller.model.cloud[0].id) + assertEquals("next_1", controller.model.cursor) + assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 50, "git@example.com:repo.git"), rpc.cloudCalls[0]) + + rpc.cloud.clear() + rpc.cloud += cloud("cloud_2", "Cloud Two") + rpc.cloudCursor = null + controller.loadMoreCloud() + flush() + + assertEquals(listOf("cloud_1", "cloud_2"), controller.model.cloud.map { it.id }) + assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 50, "git@example.com:repo.git"), rpc.cloudCalls[1]) + } + + fun `test local delete calls rpc and removes item`() { + rpc.listed += session("ses_1", "Local One") + val controller = controller() + controller.loadLocal() + flush() + + controller.delete(controller.model.local[0]) + flush() + + assertEquals(listOf("ses_1" to "/test"), rpc.deletes) + assertTrue(controller.model.local.isEmpty()) + } + + fun `test cloud delete emits unsupported error`() { + val controller = controller() + val item = HistoryItem("cloud_1", HistorySource.CLOUD, "Cloud", "a", "b") + + controller.delete(item) + flush() + + assertEquals(emptyList>(), rpc.deletes) + assertEquals("Cloud sessions cannot be deleted yet", controller.model.cloudError) + } + + fun `test panel filters and switches source`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + rpc.cloud += cloud("cloud_1", "Cloud") + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + assertEquals(2, panel.itemCount()) + panel.setSearch("alp") + assertEquals(1, panel.itemCount()) + + panel.clickCloud() + flush() + + assertEquals(HistorySource.CLOUD, panel.selectedSource()) + panel.setSearch("") + assertEquals(1, panel.itemCount()) + } + + private fun controller() = HistoryController(sessions, workspace, scope) + + private fun collect(controller: HistoryController): MutableList { + val events = mutableListOf() + controller.model.addListener(parent) { event -> + assertTrue(ApplicationManager.getApplication().isDispatchThread) + events.add(event) + } + return events + } + + private fun flush() = runBlocking { + repeat(5) { + delay(100) + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + } + + private fun session(id: String, title: String) = SessionDto( + id = id, + projectID = "prj", + directory = "/test", + title = title, + version = "1", + time = SessionTimeDto(created = 1.0, updated = 2.0), + ) + + private fun cloud(id: String, title: String) = CloudSessionDto( + id = id, + title = title, + createdAt = "2026-01-01T00:00:00Z", + updatedAt = "2026-01-02T00:00:00Z", + version = 1.0, + ) +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index 2bab0570a59..11f8325795b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -2,6 +2,8 @@ package ai.kilocode.client.testing import ai.kilocode.rpc.KiloSessionRpcApi import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto @@ -49,6 +51,13 @@ class FakeSessionRpcApi : KiloSessionRpcApi { var recentFailures = 0 var recentGate: CompletableDeferred? = null + /** Local sessions returned by [list]. */ + val listed = mutableListOf() + + /** Cloud sessions returned by [cloudSessions]. */ + val cloud = mutableListOf() + var cloudCursor: String? = null + /** Push chat events here; tests collect from [events]. */ val events = MutableSharedFlow(extraBufferCapacity = 64, replay = 64) @@ -73,10 +82,15 @@ class FakeSessionRpcApi : KiloSessionRpcApi { val permissionRulesSaved = mutableListOf>() val questionReplies = mutableListOf>() val questionRejects = mutableListOf>() + val deletes = mutableListOf>() + val lists = mutableListOf() val recentCalls = mutableListOf>() + val cloudCalls = mutableListOf() var creates = 0 private set + data class CloudCall(val directory: String, val cursor: String?, val limit: Int, val gitUrl: String?) + // --- Implementation --- override suspend fun create(directory: String): SessionDto { @@ -87,7 +101,8 @@ class FakeSessionRpcApi : KiloSessionRpcApi { override suspend fun list(directory: String): SessionListDto { assertNotEdt("list") - return SessionListDto(emptyList(), emptyMap()) + lists.add(directory) + return SessionListDto(listed.toList(), emptyMap()) } override suspend fun recent(directory: String, limit: Int): SessionListDto { @@ -108,6 +123,14 @@ class FakeSessionRpcApi : KiloSessionRpcApi { override suspend fun delete(id: String, directory: String) { assertNotEdt("delete") + deletes.add(id to directory) + listed.removeAll { it.id == id } + } + + override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto { + assertNotEdt("cloudSessions") + cloudCalls.add(CloudCall(directory, cursor, limit, gitUrl)) + return CloudSessionListDto(cloud.take(limit), cloudCursor) } override suspend fun statuses(): Flow> { diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index 330bfb8c113..8ad487acc24 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -1,6 +1,7 @@ package ai.kilocode.rpc import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto @@ -49,6 +50,9 @@ interface KiloSessionRpcApi : RemoteApi { /** Delete a session. */ suspend fun delete(id: String, directory: String) + /** List cloud-backed sessions. */ + suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto + /** Observe live session status changes. */ suspend fun statuses(): Flow> diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/HistoryDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/HistoryDto.kt new file mode 100644 index 00000000000..ce8115e8914 --- /dev/null +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/HistoryDto.kt @@ -0,0 +1,18 @@ +package ai.kilocode.rpc.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class CloudSessionDto( + val id: String, + val title: String? = null, + val createdAt: String, + val updatedAt: String, + val version: Double, +) + +@Serializable +data class CloudSessionListDto( + val sessions: List, + val nextCursor: String? = null, +) From e8f570cc75769434a8bbd8a05defaca0eb3a73d1 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 13:04:18 -0400 Subject: [PATCH 009/111] feat(jetbrains): add native session history --- .changeset/jetbrains-session-history.md | 5 + .../kilocode/client/KiloToolWindowFactory.kt | 3 +- .../kilocode/client/actions/HistoryAction.kt | 22 ++ .../kilocode/client/session/SessionManager.kt | 2 + .../client/session/SessionSidePanelManager.kt | 41 ++++ .../client/session/SessionUiFactory.kt | 2 +- .../session/history/HistoryController.kt | 16 +- .../session/history/HistoryListRenderer.kt | 112 +++++++-- .../client/session/history/HistoryModel.kt | 8 +- .../client/session/history/HistoryPanel.kt | 226 +++++++++++++----- .../client/session/history/HistoryTime.kt | 71 ++++++ .../resources/kilo.jetbrains.frontend.xml | 3 + .../resources/messages/KiloBundle.properties | 15 ++ .../session/SessionSidePanelManagerTest.kt | 62 ++++- .../session/history/HistoryControllerTest.kt | 53 +++- 15 files changed, 552 insertions(+), 89 deletions(-) create mode 100644 .changeset/jetbrains-session-history.md create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt diff --git a/.changeset/jetbrains-session-history.md b/.changeset/jetbrains-session-history.md new file mode 100644 index 00000000000..8aa827208cc --- /dev/null +++ b/.changeset/jetbrains-session-history.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Add a native session history panel to the JetBrains plugin. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt index b5e8cb2b5e8..52262d1ba8e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt @@ -1,5 +1,6 @@ package ai.kilocode.client +import ai.kilocode.client.actions.HistoryAction import ai.kilocode.client.actions.NewSessionAction import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace @@ -65,7 +66,7 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { manager.newSession() ActionManager.getInstance().getAction("Kilo.Settings")?.let { settings -> - toolWindow.setTitleActions(listOf(NewSessionAction(), settings)) + toolWindow.setTitleActions(listOf(NewSessionAction(), HistoryAction(), settings)) } } catch (e: Exception) { LOG.error("Failed to set up Kilo tool window content", e) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt new file mode 100644 index 00000000000..92d662e7ae5 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt @@ -0,0 +1,22 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionManager +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAware + +class HistoryAction : AnAction( + KiloBundle.message("action.Kilo.History.text"), + KiloBundle.message("action.Kilo.History.description"), + AllIcons.Vcs.History, +), DumbAware { + override fun actionPerformed(e: AnActionEvent) { + e.getData(SessionManager.KEY)?.showHistory() + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = e.getData(SessionManager.KEY) != null + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt index a65cde6df18..162fd3c6b2a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt @@ -10,5 +10,7 @@ interface SessionManager { fun newSession() + fun showHistory() + fun openSession(session: SessionDto) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 626a81eecb2..c46a006d5bc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -1,13 +1,17 @@ package ai.kilocode.client.session +import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.session.history.HistoryController +import ai.kilocode.client.session.history.HistoryPanel import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import kotlinx.coroutines.cancel import java.awt.BorderLayout import javax.swing.JComponent import javax.swing.JPanel @@ -19,6 +23,7 @@ class SessionSidePanelManager( service().create(project, workspace, manager, id, loading) }, private val resolve: (String) -> Workspace = { dir -> service().workspace(dir) }, + private val history: ((Disposable, (SessionDto) -> Unit, (String) -> Unit) -> JComponent)? = null, ) : SessionManager, Disposable { val component: JPanel = object : JPanel(BorderLayout()), DataProvider { override fun getData(dataId: String): Any? { @@ -30,6 +35,7 @@ class SessionSidePanelManager( private val opened = mutableMapOf() private val all = mutableSetOf() private var current: SessionUi? = null + private var panel: JComponent? = null val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent @@ -50,6 +56,41 @@ class SessionSidePanelManager( show(ui) } + override fun showHistory() { + register(current) + release(current) + val view = panel ?: createHistory().also { panel = it } + if (current == null && component.componentCount == 1 && component.getComponent(0) === view) return + current = null + component.removeAll() + component.add(view, BorderLayout.CENTER) + component.revalidate() + component.repaint() + } + + private fun createHistory(): JComponent { + val custom = history + if (custom != null) return custom(this, this::openSession, this::removeSession) + val factory = service() + val cs = factory.scope() + val controller = HistoryController( + sessions = project.service(), + workspace = root, + cs = cs, + open = { item -> item.local?.let(this::openSession) }, + deleted = this::removeSession, + ) + Disposer.register(this) { cs.cancel() } + return HistoryPanel(this, controller).component + } + + private fun removeSession(id: String) { + val ui = opened.remove(id) ?: return + all.remove(ui) + if (current === ui) current = null + Disposer.dispose(ui) + } + private fun show(ui: SessionUi) { all.add(ui) if (current === ui) return diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt index c55e607d926..bd8aed80e28 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt @@ -31,7 +31,7 @@ class SessionUiFactory( open = manager::openSession, ) - private fun scope(): CoroutineScope { + fun scope(): CoroutineScope { val parent = cs.coroutineContext[Job] return CoroutineScope(cs.coroutineContext + SupervisorJob(parent)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 4861056eff7..e90179897bc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -14,6 +14,7 @@ class HistoryController( private val workspace: Workspace, private val cs: CoroutineScope, private val open: (HistoryItem) -> Unit = {}, + private val deleted: (String) -> Unit = {}, ) { companion object { const val CLOUD_LIMIT = 50 @@ -28,7 +29,7 @@ class HistoryController( cs.launch { try { val result = sessions.list(workspace.directory) - val items = result.sessions.map(::localItem) + val items = HistoryTime.sorted(result.sessions.map(::localItem)) edt { model.setLocal(items) } } catch (e: Exception) { edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local")) } @@ -37,13 +38,14 @@ class HistoryController( } fun loadCloud(reset: Boolean = true, gitUrl: String? = null) { - if (reset) this.gitUrl = gitUrl val cursor = if (reset) null else model.cursor + val url = if (reset) gitUrl else this.gitUrl + if (reset) this.gitUrl = gitUrl edt { model.startCloud(reset) } cs.launch { try { - val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, this@HistoryController.gitUrl) - val items = result.sessions.map(::cloudItem) + val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, url) + val items = HistoryTime.sorted(result.sessions.map(::cloudItem)) edt { model.setCloud(items, result.nextCursor, append = !reset) } } catch (e: Exception) { edt { model.error(HistorySource.CLOUD, e.message ?: KiloBundle.message("history.error.cloud")) } @@ -69,7 +71,10 @@ class HistoryController( cs.launch { try { sessions.deleteSession(item.id, item.directory ?: workspace.directory) - edt { model.deleted(item.id) } + edt { + model.deleted(item.id) + deleted(item.id) + } } catch (e: Exception) { edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local.delete")) } } @@ -77,6 +82,7 @@ class HistoryController( } fun open(item: HistoryItem) { + if (item.source != HistorySource.LOCAL) return edt { open(item) } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index d45d81b169f..e02a8b142db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -1,23 +1,89 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.ui.PickerRow +import ai.kilocode.client.ui.UiStyle +import com.intellij.icons.AllIcons +import com.intellij.ui.CollectionListModel +import com.intellij.ui.GroupHeaderSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.EmptyIcon import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil -import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.Component +import java.awt.Point +import java.awt.Rectangle +import javax.swing.Icon import javax.swing.JList +import javax.swing.JPanel import javax.swing.ListCellRenderer +import javax.swing.SwingConstants -class HistoryListRenderer : BorderLayoutPanel(), ListCellRenderer { - private val title = JBLabel() - private val meta = JBLabel() +private const val DELETE_CLICK_AREA_WIDTH = 32 + +internal class HistoryListRenderer( + private val model: CollectionListModel, + private val source: () -> HistorySource, + private val deletable: Boolean, +) : JPanel(BorderLayout()), ListCellRenderer { + companion object { + private val icon: Icon = AllIcons.General.Remove + private val empty: Icon = EmptyIcon.create(icon) + + fun isDeleteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean { + val width = JBUI.scale(DELETE_CLICK_AREA_WIDTH) + if (list.componentOrientation.isLeftToRight) { + val right = bounds.x + bounds.width + return point.x in (right - width)..right + } + return point.x in bounds.x..(bounds.x + width) + } + + fun section(items: List, index: Int): String? { + val item = items.getOrNull(index) ?: return null + val current = HistoryTime.section(item) + val previous = items.getOrNull(index - 1)?.let(HistoryTime::section) + if (current == previous) return null + return HistoryTime.title(current) + } + } + + private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets()) + private val top = JPanel(BorderLayout()).apply { + border = JBUI.Borders.empty() + add(sep, BorderLayout.NORTH) + } + private val title = SimpleColoredComponent() + private val time = JBLabel() + private val del = JBLabel().apply { + horizontalAlignment = SwingConstants.CENTER + verticalAlignment = SwingConstants.CENTER + } + private val main = JPanel(BorderLayout()).apply { + add(title, BorderLayout.CENTER) + add(time, BorderLayout.EAST) + } + private val row = JPanel(BorderLayout()).apply { + add(main, BorderLayout.CENTER) + add(del, BorderLayout.EAST) + } + private val wrap = PickerRow() init { - border = JBUI.Borders.empty(8, 12, 8, 12) - add(title, BorderLayout.CENTER) - add(meta, BorderLayout.EAST) + isOpaque = true + top.isOpaque = true + row.border = JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG) + UiStyle.Components.transparent(row) + UiStyle.Components.transparent(main) + UiStyle.Components.transparent(title) + UiStyle.Components.transparent(time) + UiStyle.Components.transparent(del) + wrap.setContent(row) + add(top, BorderLayout.NORTH) + add(wrap, BorderLayout.CENTER) } override fun getListCellRendererComponent( @@ -26,13 +92,31 @@ class HistoryListRenderer : BorderLayoutPanel(), ListCellRenderer { index: Int, selected: Boolean, focus: Boolean, - ): Component { - isOpaque = selected - background = if (selected) list.selectionBackground else list.background - title.foreground = if (selected) list.selectionForeground else UIUtil.getLabelForeground() - meta.foreground = if (selected) list.selectionForeground else UIUtil.getContextHelpForeground() - title.text = value?.title?.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled") - meta.text = value?.updatedAt.orEmpty() + ): JPanel { + val focused = selected || list.hasFocus() || focus + val fg = UIUtil.getListForeground(selected, focused) + val weak = if (selected) fg else UIUtil.getContextHelpForeground() + val section = if (source() == HistorySource.LOCAL) section(model.items, index) else null + + background = list.background + top.background = list.background + wrap.update(list, selected, focused) + sep.caption = section + sep.setHideLine(index == 0) + top.isVisible = section != null + + title.clear() + title.append( + value?.title?.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled"), + SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg), + ) + time.text = value?.let(HistoryTime::relative).orEmpty() + time.foreground = weak + del.icon = if (deletable && selected) icon else empty + + top.invalidate() return this } + + fun deleteVisible(): Boolean = del.icon === icon } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt index a5625ccff66..54306d430b9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -22,6 +22,9 @@ class HistoryModel { private set var cursor: String? = null private set + private val deleting = mutableSetOf() + + fun deleting(id: String): Boolean = id in deleting fun addListener(parent: Disposable, listener: HistoryModelEvent.Listener) { listeners.add(listener) @@ -60,7 +63,7 @@ class HistoryModel { } fun setCloud(items: List, next: String?, append: Boolean) { - cloud = if (append) cloud + items else items + cloud = HistoryTime.sorted(if (append) cloud + items else items) cursor = next cloudLoading = false cloudError = null @@ -68,15 +71,18 @@ class HistoryModel { } fun startDelete(id: String) { + deleting.add(id) fire(HistoryModelEvent.DeleteStarted(id)) } fun deleted(id: String) { + deleting.remove(id) local = local.filterNot { it.id == id } fire(HistoryModelEvent.Deleted(id)) } fun error(source: HistorySource, message: String) { + if (source == HistorySource.LOCAL) deleting.clear() if (source == HistorySource.LOCAL) { localLoading = false localError = message diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 3746a6329fe..bf47f531289 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -1,23 +1,28 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.Disposable +import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Disposer import com.intellij.ui.CollectionListModel import com.intellij.ui.DocumentAdapter import com.intellij.ui.SearchTextField -import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane 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.FlowLayout import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import javax.swing.Box +import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.JToggleButton import javax.swing.ListSelectionModel import javax.swing.event.DocumentEvent @@ -26,35 +31,29 @@ class HistoryPanel( private val controller: HistoryController, private val gitUrl: () -> String? = { null }, ) : BorderLayoutPanel(), Disposable { - private val rows = CollectionListModel() - private val search = SearchTextField(false).apply { - textEditor.emptyText.text = KiloBundle.message("history.search.placeholder") - } - private val list = JBList(rows).apply { - selectionMode = ListSelectionModel.SINGLE_SELECTION - cellRenderer = HistoryListRenderer() - emptyText.text = KiloBundle.message("history.empty") - addListSelectionListener { sync() } - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - if (e.clickCount < 2) return - selectedValue?.let(controller::open) - } - }) - } - private val local = JButton(KiloBundle.message("history.tab.local")) - private val cloud = JButton(KiloBundle.message("history.tab.cloud")) - private val repo = JBCheckBox(KiloBundle.message("history.cloud.repo.only")) - private val delete = JButton(KiloBundle.message("history.delete.text")) - private val more = JButton(KiloBundle.message("history.cloud.load.more")) + private val local = Tab(HistorySource.LOCAL) + private val cloud = Tab(HistorySource.CLOUD) private val status = JBLabel() + private val body = BorderLayoutPanel() + private val localRows = CollectionListModel() + private val cloudRows = CollectionListModel() + private val localRenderer = HistoryListRenderer(localRows, source = { controller.model.source }, deletable = true) + private val cloudRenderer = HistoryListRenderer(cloudRows, source = { controller.model.source }, deletable = false) + private val localSearch = search() + private val cloudSearch = search() + private val localList = list(localRows, HistorySource.LOCAL, localRenderer) + private val cloudList = list(cloudRows, HistorySource.CLOUD, cloudRenderer) + private val localPanel = panel(localSearch, localList) + private val more = JButton(KiloBundle.message("history.cloud.load.more")) + private val cloudPanel = panel(cloudSearch, cloudList, more) + private var loadedCloud = false init { Disposer.register(parent, this) - border = JBUI.Borders.empty(8) + border = JBUI.Borders.empty(UiStyle.Space.LG) add(header(), BorderLayout.NORTH) - add(JBScrollPane(list), BorderLayout.CENTER) - add(footer(), BorderLayout.SOUTH) + add(body, BorderLayout.CENTER) + add(status, BorderLayout.SOUTH) bind(parent) sync() controller.loadLocal() @@ -65,43 +64,61 @@ class HistoryPanel( private fun header(): JComponent { local.addActionListener { controller.selectSource(HistorySource.LOCAL) - controller.loadLocal() } cloud.addActionListener { controller.selectSource(HistorySource.CLOUD) - controller.loadCloud(gitUrl = if (repo.isSelected) gitUrl() else null) - } - repo.addActionListener { - if (controller.model.source == HistorySource.CLOUD) { - controller.loadCloud(gitUrl = if (repo.isSelected) gitUrl() else null) + if (!loadedCloud) { + loadedCloud = true + controller.loadCloud(gitUrl = gitUrl()) } } - search.textEditor.document.addDocumentListener(object : DocumentAdapter() { + more.addActionListener { controller.loadMoreCloud() } + return BorderLayoutPanel().apply { + border = JBUI.Borders.emptyBottom(UiStyle.Space.LG) + add(JPanel().apply { + layout = BoxLayout(this, BoxLayout.X_AXIS) + add(local) + add(Box.createHorizontalStrut(JBUI.scale(UiStyle.Space.SM))) + add(cloud) + }, BorderLayout.WEST) + } + } + + private fun search() = SearchTextField(false).apply { + textEditor.emptyText.text = KiloBundle.message("history.search.placeholder") + textEditor.document.addDocumentListener(object : DocumentAdapter() { override fun textChanged(e: DocumentEvent) { sync() } }) + } + + private fun panel(search: SearchTextField, list: JBList, footer: JComponent? = null): JComponent { return BorderLayoutPanel().apply { - add(BorderLayoutPanel().apply { - add(local, BorderLayout.WEST) - add(cloud, BorderLayout.CENTER) - add(repo, BorderLayout.EAST) - }, BorderLayout.NORTH) - add(search, BorderLayout.CENTER) + add(search, BorderLayout.NORTH) + add(JBScrollPane(list), BorderLayout.CENTER) + footer?.let { + it.border = JBUI.Borders.emptyTop(UiStyle.Space.LG) + add(it, BorderLayout.SOUTH) + } } } - private fun footer(): JComponent { - delete.addActionListener { list.selectedValue?.let(controller::delete) } - more.addActionListener { controller.loadMoreCloud() } - return BorderLayoutPanel().apply { - add(status, BorderLayout.CENTER) - add(BorderLayoutPanel().apply { - layout = FlowLayout(FlowLayout.RIGHT, JBUI.scale(4), 0) - add(delete) - add(more) - }, BorderLayout.EAST) - } + private fun list(rows: CollectionListModel, source: HistorySource, renderer: HistoryListRenderer) = JBList(rows).apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + cellRenderer = renderer + emptyText.text = KiloBundle.message("history.empty") + addMouseListener(object : MouseAdapter() { + override fun mouseReleased(e: MouseEvent) { + val item = clicked(this@apply, e) ?: return + if (source == HistorySource.LOCAL && UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true) && deleteClick(this@apply, e)) { + confirm(item) + e.consume() + return + } + if (UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) controller.open(item) + } + }) } private fun bind(parent: Disposable) { @@ -114,24 +131,39 @@ class HistoryPanel( is HistoryModelEvent.Deleted, is HistoryModelEvent.Error, is HistoryModelEvent.SourceChanged -> sync() - is HistoryModelEvent.DeleteStarted -> Unit + is HistoryModelEvent.DeleteStarted -> sync() } } } private fun sync() { - val query = search.text.trim().lowercase() - val items = controller.model.items().filter { item -> - query.isEmpty() || item.title.lowercase().contains(query) || item.id.lowercase().contains(query) + syncRows(localRows, controller.model.local, localSearch.text) + syncRows(cloudRows, controller.model.cloud, cloudSearch.text) + local.isSelected = controller.model.source == HistorySource.LOCAL + cloud.isSelected = controller.model.source == HistorySource.CLOUD + val target = if (controller.model.source == HistorySource.LOCAL) localPanel else cloudPanel + if (body.componentCount != 1 || body.getComponent(0) !== target) { + body.removeAll() + body.add(target, BorderLayout.CENTER) } - rows.replaceAll(items) - local.isEnabled = controller.model.source != HistorySource.LOCAL - cloud.isEnabled = controller.model.source != HistorySource.CLOUD - repo.isVisible = controller.model.source == HistorySource.CLOUD - delete.isEnabled = controller.model.source == HistorySource.LOCAL && list.selectedValue != null - more.isVisible = controller.model.source == HistorySource.CLOUD more.isEnabled = controller.model.cursor != null && !controller.model.cloudLoading + more.isVisible = controller.model.cursor != null || controller.model.cloudLoading status.text = statusText() + revalidate() + repaint() + } + + private fun syncRows(model: CollectionListModel, items: List, value: String) { + val query = value.trim().lowercase() + val selected = if (model === localRows) localList.selectedValue?.id else cloudList.selectedValue?.id + val next = HistoryTime.sorted(items).filter { item -> + query.isEmpty() || item.title.lowercase().contains(query) || item.id.lowercase().contains(query) || item.directory?.lowercase()?.contains(query) == true + } + model.replaceAll(next) + val idx = next.indexOfFirst { it.id == selected } + if (idx >= 0) { + if (model === localRows) localList.selectedIndex = idx else cloudList.selectedIndex = idx + } } private fun statusText(): String { @@ -142,33 +174,99 @@ class HistoryPanel( return "" } - internal fun itemCount() = rows.size + private fun deleteClick(list: JBList, e: MouseEvent): Boolean { + val row = list.locationToIndex(e.point) + val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return false + if (!box.contains(e.point)) return false + return HistoryListRenderer.isDeleteClick(list, box, e.point) + } + + private fun clicked(list: JBList, e: MouseEvent): HistoryItem? { + val row = list.locationToIndex(e.point) + val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null + if (!box.contains(e.point)) return null + list.selectedIndex = row + return list.model.getElementAt(row) + } + + private fun confirm(item: HistoryItem) { + if (controller.model.deleting(item.id)) return + val result = Messages.showYesNoDialog( + this, + KiloBundle.message("history.delete.confirm.message", item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled")), + KiloBundle.message("history.delete.confirm.title"), + Messages.getWarningIcon(), + ) + if (result != Messages.YES) return + controller.delete(item) + } + + internal fun itemCount() = if (controller.model.source == HistorySource.LOCAL) localRows.size else cloudRows.size internal fun selectedSource() = controller.model.source internal fun select(index: Int) { - list.selectedIndex = index + activeList().selectedIndex = index sync() } internal fun clickDelete() { - delete.doClick() + activeList().selectedValue?.let(controller::delete) } internal fun clickCloud() { cloud.doClick() } + internal fun clickLocal() { + local.doClick() + } + internal fun clickMore() { more.doClick() } internal fun setSearch(value: String) { - search.text = value + if (controller.model.source == HistorySource.LOCAL) localSearch.text = value else cloudSearch.text = value sync() } + internal fun groupTitles(): List { + val model = if (controller.model.source == HistorySource.LOCAL) localRows else cloudRows + return model.items.indices.mapNotNull { HistoryListRenderer.section(model.items, it) } + } + + internal fun deleteVisible(index: Int, selected: Boolean = true): Boolean { + val item = localRows.getElementAt(index) + val view = localList.cellRenderer.getListCellRendererComponent(localList, item, index, selected, false) + return view is HistoryListRenderer && view.deleteVisible() + } + + internal fun cloudDeleteVisible(index: Int, selected: Boolean = true): Boolean { + val item = cloudRows.getElementAt(index) + val view = cloudList.cellRenderer.getListCellRendererComponent(cloudList, item, index, selected, false) + return view is HistoryListRenderer && view.deleteVisible() + } + + private fun activeList() = if (controller.model.source == HistorySource.LOCAL) localList else cloudList + override fun dispose() { // no-op } + + private class Tab(private val source: HistorySource) : JToggleButton( + when (source) { + HistorySource.LOCAL -> KiloBundle.message("history.tab.local") + HistorySource.CLOUD -> KiloBundle.message("history.tab.cloud") + }, + ) { + init { + isFocusable = false + } + + override fun updateUI() { + super.updateUI() + border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG) + } + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt new file mode 100644 index 00000000000..faf3e9f78c9 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt @@ -0,0 +1,71 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.plugin.KiloBundle +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import kotlin.math.abs + +private const val SECOND_MS_LIMIT = 10_000_000_000L +private const val MINUTE = 60_000L +private const val HOUR = 60 * MINUTE +private const val DAY = 24 * HOUR + +enum class HistorySection { + TODAY, + YESTERDAY, + WEEK, + MONTH, + OLDER, +} + +internal object HistoryTime { + fun millis(item: HistoryItem): Long? { + if (item.source == HistorySource.CLOUD) return runCatching { Instant.parse(item.updatedAt).toEpochMilli() }.getOrNull() + val raw = item.updatedAt.toDoubleOrNull()?.toLong() ?: return null + if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000 + return raw + } + + fun section(item: HistoryItem, now: Long = System.currentTimeMillis()): HistorySection { + val ms = millis(item) ?: return HistorySection.OLDER + val zone = ZoneId.systemDefault() + val date = Instant.ofEpochMilli(ms).atZone(zone).toLocalDate() + val today = Instant.ofEpochMilli(now).atZone(zone).toLocalDate() + if (date == today) return HistorySection.TODAY + if (date == today.minusDays(1)) return HistorySection.YESTERDAY + if (date.isAfter(today.minusDays(7)) && date.isBefore(today)) return HistorySection.WEEK + if (date.year == today.year && date.month == today.month) return HistorySection.MONTH + return HistorySection.OLDER + } + + fun title(section: HistorySection): String = when (section) { + HistorySection.TODAY -> KiloBundle.message("history.group.today") + HistorySection.YESTERDAY -> KiloBundle.message("history.group.yesterday") + HistorySection.WEEK -> KiloBundle.message("history.group.week") + HistorySection.MONTH -> KiloBundle.message("history.group.month") + HistorySection.OLDER -> KiloBundle.message("history.group.older") + } + + fun relative(item: HistoryItem, now: Long = System.currentTimeMillis()): String { + val ms = millis(item) ?: return item.updatedAt + val diff = (now - ms).coerceAtLeast(0) + if (diff < MINUTE) return KiloBundle.message("history.time.moments") + if (diff < HOUR) return KiloBundle.message("history.time.minutes", (diff / MINUTE).coerceAtLeast(1)) + if (diff < DAY) return KiloBundle.message("history.time.hours", (diff / HOUR).coerceAtLeast(1)) + if (diff < 7 * DAY) return KiloBundle.message("history.time.days", (diff / DAY).coerceAtLeast(1)) + val date = LocalDate.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault()) + val today = LocalDate.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()) + val months = ChronoUnit.MONTHS.between(date.withDayOfMonth(1), today.withDayOfMonth(1)) + if (months < 1) return KiloBundle.message("history.time.days", (diff / DAY).coerceAtLeast(1)) + if (months < 12) return KiloBundle.message("history.time.months", months) + return KiloBundle.message("history.time.years", months / 12) + } + + fun sorted(items: List): List = items.sortedWith( + compareByDescending { millis(it) ?: Long.MIN_VALUE } + .thenBy { it.title.lowercase() } + .thenBy { it.id }, + ) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index 619584623bd..5eabe4727d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -40,6 +40,9 @@ class="ai.kilocode.client.actions.KiloSettingsAction" icon="AllIcons.General.GearPlain"/> + + history }) + + manager.newSession() + manager.showHistory() + + assertSame(history, manager.component.getComponent(0)) + assertNull(manager.defaultFocusedComponent) + } + + fun `test opening local history item shows session ui`() { + lateinit var open: (SessionDto) -> Unit + val history = JLabel("History") + val manager = manager(history = { _, fn, _ -> + open = fn + history + }) + + manager.showHistory() + open(session("ses_1")) + + assertTrue(active(manager) is SessionUi) + assertEquals(listOf("/test" to "ses_1"), created) + } + + fun `test new session from history shows blank session`() { + val history = JLabel("History") + val manager = manager(history = { _, _, _ -> history }) + + manager.showHistory() + manager.newSession() + + assertTrue(active(manager) is SessionUi) + assertEquals(listOf("/test" to null), created) + } + + fun `test deleted cached history session is not reused`() { + lateinit var deleted: (String) -> Unit + val manager = manager(history = { _, _, fn -> + deleted = fn + JLabel("History") + }) + val session = session("ses_1") + + manager.openSession(session) + val first = active(manager) + manager.showHistory() + deleted("ses_1") + manager.openSession(session) + + assertNotSame(first, active(manager)) + assertEquals(listOf("/test" to "ses_1", "/test" to "ses_1"), created) + } + + private fun manager( + history: ((com.intellij.openapi.Disposable, (SessionDto) -> Unit, (String) -> Unit) -> JComponent)? = null, + ): SessionSidePanelManager { val manager = SessionSidePanelManager( project = project, root = workspace, @@ -197,6 +256,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { } }, resolve = { workspaces.workspace(it) }, + history = history, ) managers.add(manager) return manager diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 68252a2afd4..0060420b953 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -20,6 +20,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import java.time.Instant +import java.time.temporal.ChronoUnit @Suppress("UnstableApiUsage") class HistoryControllerTest : BasePlatformTestCase() { @@ -132,6 +134,53 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(1, panel.itemCount()) } + fun `test panel preserves independent search per source`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + rpc.cloud += cloud("cloud_1", "Cloud Alpha") + rpc.cloud += cloud("cloud_2", "Cloud Beta") + val panel = HistoryPanel(parent, controller()) + flush() + + panel.setSearch("alp") + assertEquals(1, panel.itemCount()) + + panel.clickCloud() + flush() + assertEquals(2, panel.itemCount()) + panel.setSearch("beta") + assertEquals(1, panel.itemCount()) + + panel.clickLocal() + assertEquals(1, panel.itemCount()) + } + + fun `test panel groups sessions by date`() { + val now = Instant.now() + rpc.listed += session("ses_today", "Today", now.toEpochMilli().toDouble()) + rpc.listed += session("ses_yesterday", "Yesterday", now.minus(1, ChronoUnit.DAYS).toEpochMilli().toDouble()) + rpc.listed += session("ses_week", "Week", now.minus(3, ChronoUnit.DAYS).toEpochMilli().toDouble()) + rpc.listed += session("ses_month", "Month", now.minus(10, ChronoUnit.DAYS).toEpochMilli().toDouble()) + rpc.listed += session("ses_older", "Older", now.minus(60, ChronoUnit.DAYS).toEpochMilli().toDouble()) + val panel = HistoryPanel(parent, controller()) + flush() + + assertTrue(panel.groupTitles().containsAll(listOf("Today", "Yesterday", "This Week", "Older"))) + } + + fun `test local renderer exposes delete and cloud renderer hides it`() { + rpc.listed += session("ses_1", "Local") + rpc.cloud += cloud("cloud_1", "Cloud") + val panel = HistoryPanel(parent, controller()) + flush() + + assertTrue(panel.deleteVisible(0)) + + panel.clickCloud() + flush() + assertFalse(panel.cloudDeleteVisible(0)) + } + private fun controller() = HistoryController(sessions, workspace, scope) private fun collect(controller: HistoryController): MutableList { @@ -150,13 +199,13 @@ class HistoryControllerTest : BasePlatformTestCase() { } } - private fun session(id: String, title: String) = SessionDto( + private fun session(id: String, title: String, updated: Double = 2.0) = SessionDto( id = id, projectID = "prj", directory = "/test", title = title, version = "1", - time = SessionTimeDto(created = 1.0, updated = 2.0), + time = SessionTimeDto(created = 1.0, updated = updated), ) private fun cloud(id: String, title: String) = CloudSessionDto( From bb304a1ed893db5944ecf86967ba8df7458f22aa Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 20:57:11 -0400 Subject: [PATCH 010/111] fix(jetbrains): refresh history panels on reopen --- .changeset/jetbrains-history-tabs.md | 5 + .../client/session/SessionSidePanelManager.kt | 6 +- .../session/history/HistoryController.kt | 118 ++++----- .../client/session/history/HistoryItem.kt | 33 ++- .../session/history/HistoryListRenderer.kt | 38 ++- .../client/session/history/HistoryModel.kt | 148 ++++++----- .../session/history/HistoryModelEvent.kt | 30 --- .../client/session/history/HistoryPanel.kt | 247 ++++++++---------- .../client/session/history/HistoryTime.kt | 8 +- .../session/history/HistoryControllerTest.kt | 75 ++++-- 10 files changed, 358 insertions(+), 350 deletions(-) create mode 100644 .changeset/jetbrains-history-tabs.md delete mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt diff --git a/.changeset/jetbrains-history-tabs.md b/.changeset/jetbrains-history-tabs.md new file mode 100644 index 00000000000..859a598e3e8 --- /dev/null +++ b/.changeset/jetbrains-history-tabs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 76777262d31..40e523b5fbe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -59,7 +59,9 @@ class SessionSidePanelManager( override fun showHistory() { register(current) release(current) - val view = panel ?: createHistory().also { panel = it } + val cached = panel + val view = cached ?: createHistory().also { panel = it } + if (cached != null && view is HistoryPanel) view.refresh() if (current == null && component.componentCount == 1 && component.getComponent(0) === view) return current = null component.removeAll() @@ -77,7 +79,7 @@ class SessionSidePanelManager( sessions = project.service(), workspace = root, cs = cs, - open = { item -> item.local?.let(this::openSession) }, + open = { item -> openSession(item.session) }, deleted = this::removeSession, ) Disposer.register(this) { cs.cancel() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index e90179897bc..0e2d0cd2562 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -13,78 +13,92 @@ class HistoryController( private val sessions: KiloSessionService, private val workspace: Workspace, private val cs: CoroutineScope, - private val open: (HistoryItem) -> Unit = {}, + private val open: (LocalHistoryItem) -> Unit = {}, private val deleted: (String) -> Unit = {}, ) { companion object { const val CLOUD_LIMIT = 50 } - val model = HistoryModel() + val local = HistoryModel() + val cloud = CloudHistoryModel() - private var gitUrl: String? = null + private val deleting = mutableSetOf() + private var git: String? = null - fun loadLocal() { - edt { model.startLocal() } + fun reload(gitUrl: String? = null) { + reloadLocal() + reloadCloud(gitUrl) + } + + fun reloadLocal() { + edt { local.start() } cs.launch { try { val result = sessions.list(workspace.directory) - val items = HistoryTime.sorted(result.sessions.map(::localItem)) - edt { model.setLocal(items) } + val items = result.sessions.map(::localItem) + edt { local.replace(items) } } catch (e: Exception) { - edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local")) } + edt { local.fail(e.message ?: KiloBundle.message("history.error.local")) } } } } - fun loadCloud(reset: Boolean = true, gitUrl: String? = null) { - val cursor = if (reset) null else model.cursor - val url = if (reset) gitUrl else this.gitUrl - if (reset) this.gitUrl = gitUrl - edt { model.startCloud(reset) } - cs.launch { - try { - val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, url) - val items = HistoryTime.sorted(result.sessions.map(::cloudItem)) - edt { model.setCloud(items, result.nextCursor, append = !reset) } - } catch (e: Exception) { - edt { model.error(HistorySource.CLOUD, e.message ?: KiloBundle.message("history.error.cloud")) } - } - } + fun reloadCloud(gitUrl: String? = null) { + git = gitUrl + loadCloud(reset = true) } fun loadMoreCloud() { - if (model.cursor == null || model.cloudLoading) return + if (cloud.cursor == null || cloud.loading) return loadCloud(reset = false) } - fun selectSource(source: HistorySource) { - edt { model.select(source) } - } - - fun delete(item: HistoryItem) { - if (item.source == HistorySource.CLOUD) { - edt { model.error(HistorySource.CLOUD, KiloBundle.message("history.error.cloud.delete")) } - return - } - edt { model.startDelete(item.id) } - cs.launch { - try { - sessions.deleteSession(item.id, item.directory ?: workspace.directory) - edt { - model.deleted(item.id) - deleted(item.id) + fun delete(item: LocalHistoryItem) { + edt { + if (item.id in deleting) return@edt + deleting.add(item.id) + local.refresh() + cs.launch { + try { + sessions.deleteSession(item.id, item.directory ?: workspace.directory) + edt { + deleting.remove(item.id) + local.remove(item.id) + deleted(item.id) + } + } catch (e: Exception) { + edt { + deleting.remove(item.id) + local.fail(e.message ?: KiloBundle.message("history.error.local.delete")) + } } - } catch (e: Exception) { - edt { model.error(HistorySource.LOCAL, e.message ?: KiloBundle.message("history.error.local.delete")) } } } } - fun open(item: HistoryItem) { - if (item.source != HistorySource.LOCAL) return + fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting + + fun open(item: LocalHistoryItem) { edt { open(item) } } + + private fun loadCloud(reset: Boolean) { + val cursor = cloud.cursor.takeUnless { reset } + edt { cloud.start(reset) } + cs.launch { + try { + val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, git) + val items = result.sessions.map(::cloudItem) + edt { + if (reset) cloud.replace(items, result.nextCursor) + else cloud.append(items, result.nextCursor) + } + } catch (e: Exception) { + edt { cloud.fail(e.message ?: KiloBundle.message("history.error.cloud")) } + } + } + } } private fun edt(block: () -> Unit) { @@ -96,20 +110,6 @@ private fun edt(block: () -> Unit) { app.invokeLater(block) } -private fun localItem(session: SessionDto) = HistoryItem( - id = session.id, - source = HistorySource.LOCAL, - title = session.title, - createdAt = session.time.created.toString(), - updatedAt = session.time.updated.toString(), - directory = session.directory, - local = session, -) +private fun localItem(session: SessionDto) = LocalHistoryItem(session) -private fun cloudItem(session: CloudSessionDto) = HistoryItem( - id = session.id, - source = HistorySource.CLOUD, - title = session.title.orEmpty(), - createdAt = session.createdAt, - updatedAt = session.updatedAt, -) +private fun cloudItem(session: CloudSessionDto) = CloudHistoryItem(session) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt index 5d63abc696f..35c9f851452 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryItem.kt @@ -1,15 +1,28 @@ package ai.kilocode.client.session.history +import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto -enum class HistorySource { LOCAL, CLOUD } +sealed interface HistoryItem { + val id: String + val title: String + val createdAt: String + val updatedAt: String +} -data class HistoryItem( - val id: String, - val source: HistorySource, - val title: String, - val createdAt: String, - val updatedAt: String, - val directory: String? = null, - val local: SessionDto? = null, -) +data class LocalHistoryItem(val session: SessionDto) : HistoryItem { + override val id: String get() = session.id + override val title: String get() = session.title + override val createdAt: String get() = session.time.created.toString() + override val updatedAt: String get() = session.time.updated.toString() + val directory: String? get() = session.directory +} + +data class CloudHistoryItem(val session: CloudSessionDto) : HistoryItem { + override val id: String get() = session.id + override val title: String get() = session.title.orEmpty() + override val createdAt: String get() = session.createdAt + override val updatedAt: String get() = session.updatedAt +} + +enum class HistorySource { LOCAL, CLOUD } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index e02a8b142db..a01aff045fd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -4,7 +4,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.PickerRow import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons -import com.intellij.ui.CollectionListModel +import com.intellij.ui.ExperimentalUI import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes @@ -13,7 +13,6 @@ import com.intellij.util.ui.EmptyIcon import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.BorderLayout -import java.awt.Component import java.awt.Point import java.awt.Rectangle import javax.swing.Icon @@ -24,22 +23,23 @@ import javax.swing.SwingConstants private const val DELETE_CLICK_AREA_WIDTH = 32 -internal class HistoryListRenderer( - private val model: CollectionListModel, - private val source: () -> HistorySource, +internal open class HistoryRenderer( + private val model: HistoryModel, private val deletable: Boolean, -) : JPanel(BorderLayout()), ListCellRenderer { +) : JPanel(BorderLayout()), ListCellRenderer { companion object { - private val icon: Icon = AllIcons.General.Remove + private val icon: Icon = AllIcons.Actions.GC private val empty: Icon = EmptyIcon.create(icon) fun isDeleteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean { val width = JBUI.scale(DELETE_CLICK_AREA_WIDTH) + val inset = deleteInset(list) if (list.componentOrientation.isLeftToRight) { - val right = bounds.x + bounds.width + val right = bounds.x + bounds.width - inset return point.x in (right - width)..right } - return point.x in bounds.x..(bounds.x + width) + val left = bounds.x + inset + return point.x in left..(left + width) } fun section(items: List, index: Int): String? { @@ -49,6 +49,13 @@ internal class HistoryListRenderer( if (current == previous) return null return HistoryTime.title(current) } + + private fun deleteInset(list: JList<*>): Int { + if (!ExperimentalUI.isNewUI()) return 0 + val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets() + val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get() + return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left + } } private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets()) @@ -87,8 +94,8 @@ internal class HistoryListRenderer( } override fun getListCellRendererComponent( - list: JList, - value: HistoryItem?, + list: JList, + value: T?, index: Int, selected: Boolean, focus: Boolean, @@ -96,14 +103,13 @@ internal class HistoryListRenderer( val focused = selected || list.hasFocus() || focus val fg = UIUtil.getListForeground(selected, focused) val weak = if (selected) fg else UIUtil.getContextHelpForeground() - val section = if (source() == HistorySource.LOCAL) section(model.items, index) else null background = list.background top.background = list.background wrap.update(list, selected, focused) - sep.caption = section + sep.caption = section(model.visibleItems, index) sep.setHideLine(index == 0) - top.isVisible = section != null + top.isVisible = sep.caption != null title.clear() title.append( @@ -120,3 +126,7 @@ internal class HistoryListRenderer( fun deleteVisible(): Boolean = del.icon === icon } + +internal class LocalHistoryRenderer(model: HistoryModel) : HistoryRenderer(model, deletable = true) + +internal class CloudHistoryRenderer(model: HistoryModel) : HistoryRenderer(model, deletable = false) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt index 54306d430b9..44543d2d37b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -1,99 +1,105 @@ package ai.kilocode.client.session.history -import com.intellij.openapi.Disposable -import com.intellij.openapi.util.Disposer +import javax.swing.AbstractListModel -class HistoryModel { - private val listeners = mutableListOf() +open class HistoryModel : AbstractListModel() { + private var all = emptyList() + private var rows = emptyList() + private var query = "" - var source = HistorySource.LOCAL + var loading = false private set - var local = emptyList() + var error: String? = null private set - var cloud = emptyList() - private set - var localLoading = false - private set - var cloudLoading = false - private set - var localError: String? = null - private set - var cloudError: String? = null - private set - var cursor: String? = null - private set - private val deleting = mutableSetOf() - fun deleting(id: String): Boolean = id in deleting + val items: List get() = all + val visibleItems: List get() = rows - fun addListener(parent: Disposable, listener: HistoryModelEvent.Listener) { - listeners.add(listener) - Disposer.register(parent) { listeners.remove(listener) } + override fun getSize(): Int = rows.size + + override fun getElementAt(index: Int): T = rows[index] + + fun start() { + loading = true + error = null + refresh() } - fun items(): List = if (source == HistorySource.LOCAL) local else cloud - - fun select(value: HistorySource) { - if (source == value) return - source = value - fire(HistoryModelEvent.SourceChanged(value)) + fun replace(items: List) { + all = HistoryTime.sorted(items) + loading = false + error = null + filter() } - fun startLocal() { - localLoading = true - localError = null - fire(HistoryModelEvent.LocalLoading) + fun append(items: List) { + all = HistoryTime.sorted(all + items) + loading = false + error = null + filter() } - fun setLocal(items: List) { - local = items - localLoading = false - localError = null - fire(HistoryModelEvent.LocalLoaded(items.size)) + fun remove(id: String) { + all = all.filterNot { it.id == id } + filter() } - fun startCloud(reset: Boolean) { - cloudLoading = true - cloudError = null - if (reset) { - cloud = emptyList() - cursor = null - } - fire(HistoryModelEvent.CloudLoading(reset)) + fun fail(message: String) { + loading = false + error = message + refresh() } - fun setCloud(items: List, next: String?, append: Boolean) { - cloud = HistoryTime.sorted(if (append) cloud + items else items) - cursor = next - cloudLoading = false - cloudError = null - fire(HistoryModelEvent.CloudLoaded(items.size, next)) + fun setFilter(value: String) { + query = value.trim().lowercase() + filter() } - fun startDelete(id: String) { - deleting.add(id) - fire(HistoryModelEvent.DeleteStarted(id)) + fun hasFilter(): Boolean = query.isNotEmpty() + + fun refresh() { + val end = size.coerceAtLeast(1) - 1 + fireContentsChanged(this, 0, end) } - fun deleted(id: String) { - deleting.remove(id) - local = local.filterNot { it.id == id } - fire(HistoryModelEvent.Deleted(id)) + protected fun clear() { + all = emptyList() + filter() } - fun error(source: HistorySource, message: String) { - if (source == HistorySource.LOCAL) deleting.clear() - if (source == HistorySource.LOCAL) { - localLoading = false - localError = message - } else { - cloudLoading = false - cloudError = message - } - fire(HistoryModelEvent.Error(source, message)) + private fun filter() { + val old = rows.size + rows = all.filter(::matches) + val end = maxOf(old, rows.size).coerceAtLeast(1) - 1 + fireContentsChanged(this, 0, end) } - private fun fire(event: HistoryModelEvent) { - for (listener in listeners) listener.onEvent(event) + private fun matches(item: T): Boolean { + if (query.isEmpty()) return true + if (item.title.lowercase().contains(query)) return true + if (item.id.lowercase().contains(query)) return true + if (item is LocalHistoryItem && item.directory?.lowercase()?.contains(query) == true) return true + return false + } +} + +class CloudHistoryModel : HistoryModel() { + var cursor: String? = null + private set + + fun start(reset: Boolean) { + cursor = cursor.takeUnless { reset } + start() + if (reset) clear() + } + + fun replace(items: List, next: String?) { + cursor = next + replace(items) + } + + fun append(items: List, next: String?) { + cursor = next + append(items) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt deleted file mode 100644 index 545bcf40e15..00000000000 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModelEvent.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ai.kilocode.client.session.history - -sealed class HistoryModelEvent { - data object LocalLoading : HistoryModelEvent() - data class LocalLoaded(val count: Int) : HistoryModelEvent() { - override fun toString() = "LocalLoaded count=$count" - } - data class CloudLoading(val reset: Boolean) : HistoryModelEvent() { - override fun toString() = "CloudLoading reset=$reset" - } - data class CloudLoaded(val count: Int, val nextCursor: String?) : HistoryModelEvent() { - override fun toString() = "CloudLoaded count=$count next=$nextCursor" - } - data class DeleteStarted(val id: String) : HistoryModelEvent() { - override fun toString() = "DeleteStarted $id" - } - data class Deleted(val id: String) : HistoryModelEvent() { - override fun toString() = "Deleted $id" - } - data class Error(val source: HistorySource, val message: String) : HistoryModelEvent() { - override fun toString() = "Error $source $message" - } - data class SourceChanged(val source: HistorySource) : HistoryModelEvent() { - override fun toString() = "SourceChanged $source" - } - - fun interface Listener { - fun onEvent(event: HistoryModelEvent) - } -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index bf47f531289..e76cd1f54f7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -5,98 +5,102 @@ import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.Disposable import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Disposer -import com.intellij.ui.CollectionListModel import com.intellij.ui.DocumentAdapter +import com.intellij.ui.ListUtil import com.intellij.ui.SearchTextField -import com.intellij.ui.components.JBLabel +import com.intellij.ui.ScrollingUtil import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.tabs.JBTabs +import com.intellij.ui.tabs.JBTabsFactory +import com.intellij.ui.tabs.JBTabsPosition +import com.intellij.ui.tabs.TabInfo +import com.intellij.ui.tabs.TabsListener import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout +import java.awt.event.HierarchyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import javax.swing.Box -import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JComponent -import javax.swing.JPanel -import javax.swing.JToggleButton +import javax.swing.JList import javax.swing.ListSelectionModel import javax.swing.event.DocumentEvent +import javax.swing.event.ListDataEvent +import javax.swing.event.ListDataListener class HistoryPanel( parent: Disposable, private val controller: HistoryController, private val gitUrl: () -> String? = { null }, ) : BorderLayoutPanel(), Disposable { - private val local = Tab(HistorySource.LOCAL) - private val cloud = Tab(HistorySource.CLOUD) - private val status = JBLabel() - private val body = BorderLayoutPanel() - private val localRows = CollectionListModel() - private val cloudRows = CollectionListModel() - private val localRenderer = HistoryListRenderer(localRows, source = { controller.model.source }, deletable = true) - private val cloudRenderer = HistoryListRenderer(cloudRows, source = { controller.model.source }, deletable = false) - private val localSearch = search() - private val cloudSearch = search() - private val localList = list(localRows, HistorySource.LOCAL, localRenderer) - private val cloudList = list(cloudRows, HistorySource.CLOUD, cloudRenderer) - private val localPanel = panel(localSearch, localList) + private val localSearch = search(controller.local) + private val cloudSearch = search(controller.cloud) + private val localList = localList() + private val cloudList = cloudList() private val more = JButton(KiloBundle.message("history.cloud.load.more")) + private val localPanel = panel(localSearch, localList) private val cloudPanel = panel(cloudSearch, cloudList, more) - private var loadedCloud = false + private val localInfo = TabInfo(localPanel).setText(KiloBundle.message("history.tab.local")) + private val cloudInfo = TabInfo(cloudPanel).setText(KiloBundle.message("history.tab.cloud")) + private var stale = false + private val tabs: JBTabs = JBTabsFactory.createTabs(null, this).apply { + presentation.setSingleRow(true) + presentation.setTabsPosition(JBTabsPosition.top) + presentation.showBorder = false + addTab(localInfo) + addTab(cloudInfo) + addListener(object : TabsListener { + override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { + sync() + } + }, this@HistoryPanel) + } init { Disposer.register(parent, this) border = JBUI.Borders.empty(UiStyle.Space.LG) - add(header(), BorderLayout.NORTH) - add(body, BorderLayout.CENTER) - add(status, BorderLayout.SOUTH) - bind(parent) + more.addActionListener { controller.loadMoreCloud() } + bind(localList, controller.local) + bind(cloudList, controller.cloud) + addHierarchyListener { e -> + if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener + if (isShowing && stale) { + refresh() + return@addHierarchyListener + } + if (!isShowing) stale = true + } + add(tabs.component, BorderLayout.CENTER) sync() - controller.loadLocal() + refresh() } val component: JComponent get() = this - private fun header(): JComponent { - local.addActionListener { - controller.selectSource(HistorySource.LOCAL) - } - cloud.addActionListener { - controller.selectSource(HistorySource.CLOUD) - if (!loadedCloud) { - loadedCloud = true - controller.loadCloud(gitUrl = gitUrl()) - } - } - more.addActionListener { controller.loadMoreCloud() } - return BorderLayoutPanel().apply { - border = JBUI.Borders.emptyBottom(UiStyle.Space.LG) - add(JPanel().apply { - layout = BoxLayout(this, BoxLayout.X_AXIS) - add(local) - add(Box.createHorizontalStrut(JBUI.scale(UiStyle.Space.SM))) - add(cloud) - }, BorderLayout.WEST) - } + fun refresh() { + stale = false + controller.reload(gitUrl()) } - private fun search() = SearchTextField(false).apply { + private fun search(model: HistoryModel) = SearchTextField(false).apply { textEditor.emptyText.text = KiloBundle.message("history.search.placeholder") textEditor.document.addDocumentListener(object : DocumentAdapter() { override fun textChanged(e: DocumentEvent) { - sync() + model.setFilter(text) } }) } - private fun panel(search: SearchTextField, list: JBList, footer: JComponent? = null): JComponent { + private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { return BorderLayoutPanel().apply { add(search, BorderLayout.NORTH) - add(JBScrollPane(list), BorderLayout.CENTER) + add(JBScrollPane(list).apply { + border = JBUI.Borders.empty() + viewportBorder = JBUI.Borders.empty() + }, BorderLayout.CENTER) footer?.let { it.border = JBUI.Borders.emptyTop(UiStyle.Space.LG) add(it, BorderLayout.SOUTH) @@ -104,84 +108,79 @@ class HistoryPanel( } } - private fun list(rows: CollectionListModel, source: HistorySource, renderer: HistoryListRenderer) = JBList(rows).apply { + private fun localList() = JBList(controller.local).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION - cellRenderer = renderer + cellRenderer = LocalHistoryRenderer(controller.local) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { override fun mouseReleased(e: MouseEvent) { + if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return val item = clicked(this@apply, e) ?: return - if (source == HistorySource.LOCAL && UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true) && deleteClick(this@apply, e)) { + if (deleteClick(this@apply, e)) { confirm(item) e.consume() return } - if (UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) controller.open(item) + controller.open(item) } }) + ListUtil.installAutoSelectOnMouseMove(this) + ScrollingUtil.installActions(this) } - private fun bind(parent: Disposable) { - controller.model.addListener(parent) { event -> - when (event) { - is HistoryModelEvent.LocalLoading, - is HistoryModelEvent.LocalLoaded, - is HistoryModelEvent.CloudLoading, - is HistoryModelEvent.CloudLoaded, - is HistoryModelEvent.Deleted, - is HistoryModelEvent.Error, - is HistoryModelEvent.SourceChanged -> sync() - is HistoryModelEvent.DeleteStarted -> sync() + private fun cloudList() = JBList(controller.cloud).apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + cellRenderer = CloudHistoryRenderer(controller.cloud) + emptyText.text = KiloBundle.message("history.empty") + addMouseListener(object : MouseAdapter() { + override fun mouseReleased(e: MouseEvent) { + if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return + clicked(this@apply, e) } + }) + ListUtil.installAutoSelectOnMouseMove(this) + ScrollingUtil.installActions(this) + } + + private fun bind(list: JBList, model: HistoryModel) { + val listener = object : ListDataListener { + override fun intervalAdded(e: ListDataEvent) = sync() + + override fun intervalRemoved(e: ListDataEvent) = sync() + + override fun contentsChanged(e: ListDataEvent) = sync() } + model.addListDataListener(listener) + Disposer.register(this) { model.removeListDataListener(listener) } + list.setPaintBusy(model.loading) } private fun sync() { - syncRows(localRows, controller.model.local, localSearch.text) - syncRows(cloudRows, controller.model.cloud, cloudSearch.text) - local.isSelected = controller.model.source == HistorySource.LOCAL - cloud.isSelected = controller.model.source == HistorySource.CLOUD - val target = if (controller.model.source == HistorySource.LOCAL) localPanel else cloudPanel - if (body.componentCount != 1 || body.getComponent(0) !== target) { - body.removeAll() - body.add(target, BorderLayout.CENTER) - } - more.isEnabled = controller.model.cursor != null && !controller.model.cloudLoading - more.isVisible = controller.model.cursor != null || controller.model.cloudLoading - status.text = statusText() + syncList(localList, controller.local) + syncList(cloudList, controller.cloud) + more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading + more.isVisible = controller.cloud.cursor != null || controller.cloud.loading revalidate() repaint() } - private fun syncRows(model: CollectionListModel, items: List, value: String) { - val query = value.trim().lowercase() - val selected = if (model === localRows) localList.selectedValue?.id else cloudList.selectedValue?.id - val next = HistoryTime.sorted(items).filter { item -> - query.isEmpty() || item.title.lowercase().contains(query) || item.id.lowercase().contains(query) || item.directory?.lowercase()?.contains(query) == true - } - model.replaceAll(next) - val idx = next.indexOfFirst { it.id == selected } - if (idx >= 0) { - if (model === localRows) localList.selectedIndex = idx else cloudList.selectedIndex = idx + private fun syncList(list: JBList, model: HistoryModel) { + list.setPaintBusy(model.loading) + list.emptyText.text = when { + model.loading -> KiloBundle.message("history.loading") + model.error != null -> model.error.orEmpty() + else -> KiloBundle.message("history.empty") } } - private fun statusText(): String { - val model = controller.model - if (model.localLoading || model.cloudLoading) return KiloBundle.message("history.loading") - val error = if (model.source == HistorySource.LOCAL) model.localError else model.cloudError - if (error != null) return error - return "" - } - - private fun deleteClick(list: JBList, e: MouseEvent): Boolean { + private fun deleteClick(list: JBList, e: MouseEvent): Boolean { val row = list.locationToIndex(e.point) val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return false if (!box.contains(e.point)) return false - return HistoryListRenderer.isDeleteClick(list, box, e.point) + return HistoryRenderer.isDeleteClick(list, box, e.point) } - private fun clicked(list: JBList, e: MouseEvent): HistoryItem? { + private fun clicked(list: JBList, e: MouseEvent): T? { val row = list.locationToIndex(e.point) val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null if (!box.contains(e.point)) return null @@ -189,8 +188,8 @@ class HistoryPanel( return list.model.getElementAt(row) } - private fun confirm(item: HistoryItem) { - if (controller.model.deleting(item.id)) return + private fun confirm(item: LocalHistoryItem) { + if (controller.deleting(item)) return val result = Messages.showYesNoDialog( this, KiloBundle.message("history.delete.confirm.message", item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled")), @@ -201,25 +200,26 @@ class HistoryPanel( controller.delete(item) } - internal fun itemCount() = if (controller.model.source == HistorySource.LOCAL) localRows.size else cloudRows.size + internal fun itemCount() = activeModel().size - internal fun selectedSource() = controller.model.source + internal fun selectedSource() = if (tabs.selectedInfo === cloudInfo) HistorySource.CLOUD else HistorySource.LOCAL internal fun select(index: Int) { activeList().selectedIndex = index - sync() } internal fun clickDelete() { - activeList().selectedValue?.let(controller::delete) + localList.selectedValue?.let(controller::delete) } internal fun clickCloud() { - cloud.doClick() + tabs.select(cloudInfo, false) + sync() } internal fun clickLocal() { - local.doClick() + tabs.select(localInfo, false) + sync() } internal fun clickMore() { @@ -227,46 +227,31 @@ class HistoryPanel( } internal fun setSearch(value: String) { - if (controller.model.source == HistorySource.LOCAL) localSearch.text = value else cloudSearch.text = value - sync() + if (tabs.selectedInfo === cloudInfo) cloudSearch.text = value else localSearch.text = value } internal fun groupTitles(): List { - val model = if (controller.model.source == HistorySource.LOCAL) localRows else cloudRows - return model.items.indices.mapNotNull { HistoryListRenderer.section(model.items, it) } + val items = activeModel().visibleItems + return items.indices.mapNotNull { HistoryRenderer.section(items, it) } } internal fun deleteVisible(index: Int, selected: Boolean = true): Boolean { - val item = localRows.getElementAt(index) + val item = controller.local.getElementAt(index) val view = localList.cellRenderer.getListCellRendererComponent(localList, item, index, selected, false) - return view is HistoryListRenderer && view.deleteVisible() + return view is HistoryRenderer<*> && view.deleteVisible() } internal fun cloudDeleteVisible(index: Int, selected: Boolean = true): Boolean { - val item = cloudRows.getElementAt(index) + val item = controller.cloud.getElementAt(index) val view = cloudList.cellRenderer.getListCellRendererComponent(cloudList, item, index, selected, false) - return view is HistoryListRenderer && view.deleteVisible() + return view is HistoryRenderer<*> && view.deleteVisible() } - private fun activeList() = if (controller.model.source == HistorySource.LOCAL) localList else cloudList + private fun activeList(): JBList = if (tabs.selectedInfo === cloudInfo) cloudList else localList + + private fun activeModel(): HistoryModel = if (tabs.selectedInfo === cloudInfo) controller.cloud else controller.local override fun dispose() { // no-op } - - private class Tab(private val source: HistorySource) : JToggleButton( - when (source) { - HistorySource.LOCAL -> KiloBundle.message("history.tab.local") - HistorySource.CLOUD -> KiloBundle.message("history.tab.cloud") - }, - ) { - init { - isFocusable = false - } - - override fun updateUI() { - super.updateUI() - border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG) - } - } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt index faf3e9f78c9..89c4687d4ae 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt @@ -22,7 +22,7 @@ enum class HistorySection { internal object HistoryTime { fun millis(item: HistoryItem): Long? { - if (item.source == HistorySource.CLOUD) return runCatching { Instant.parse(item.updatedAt).toEpochMilli() }.getOrNull() + if (item is CloudHistoryItem) return runCatching { Instant.parse(item.updatedAt).toEpochMilli() }.getOrNull() val raw = item.updatedAt.toDoubleOrNull()?.toLong() ?: return null if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000 return raw @@ -36,7 +36,7 @@ internal object HistoryTime { if (date == today) return HistorySection.TODAY if (date == today.minusDays(1)) return HistorySection.YESTERDAY if (date.isAfter(today.minusDays(7)) && date.isBefore(today)) return HistorySection.WEEK - if (date.year == today.year && date.month == today.month) return HistorySection.MONTH + if (date.isAfter(today.minusDays(30)) && date.isBefore(today)) return HistorySection.MONTH return HistorySection.OLDER } @@ -63,8 +63,8 @@ internal object HistoryTime { return KiloBundle.message("history.time.years", months / 12) } - fun sorted(items: List): List = items.sortedWith( - compareByDescending { millis(it) ?: Long.MIN_VALUE } + fun sorted(items: List): List = items.sortedWith( + compareByDescending { millis(it) ?: Long.MIN_VALUE } .thenBy { it.title.lowercase() } .thenBy { it.id }, ) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 0060420b953..328742894f6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -22,6 +22,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.time.Instant import java.time.temporal.ChronoUnit +import javax.swing.event.ListDataEvent +import javax.swing.event.ListDataListener @Suppress("UnstableApiUsage") class HistoryControllerTest : BasePlatformTestCase() { @@ -57,14 +59,14 @@ class HistoryControllerTest : BasePlatformTestCase() { val controller = controller() val events = collect(controller) - controller.loadLocal() + controller.reloadLocal() flush() assertEquals(listOf("/test"), rpc.lists) - assertEquals(1, controller.model.local.size) - assertEquals("ses_1", controller.model.local[0].id) - assertEquals("Local One", controller.model.local[0].title) - assertEquals("LocalLoading\nLocalLoaded count=1", events.joinToString("\n")) + assertEquals(1, controller.local.items.size) + assertEquals("ses_1", controller.local.items[0].id) + assertEquals("Local One", controller.local.items[0].title) + assertEquals("loading=true size=0 error=null\nloading=false size=1 error=null", events.joinToString("\n")) } fun `test cloud load maps sessions and supports load more`() { @@ -72,12 +74,12 @@ class HistoryControllerTest : BasePlatformTestCase() { rpc.cloudCursor = "next_1" val controller = controller() - controller.loadCloud(gitUrl = "git@example.com:repo.git") + controller.reloadCloud(gitUrl = "git@example.com:repo.git") flush() - assertEquals(1, controller.model.cloud.size) - assertEquals("cloud_1", controller.model.cloud[0].id) - assertEquals("next_1", controller.model.cursor) + assertEquals(1, controller.cloud.items.size) + assertEquals("cloud_1", controller.cloud.items[0].id) + assertEquals("next_1", controller.cloud.cursor) assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 50, "git@example.com:repo.git"), rpc.cloudCalls[0]) rpc.cloud.clear() @@ -86,32 +88,21 @@ class HistoryControllerTest : BasePlatformTestCase() { controller.loadMoreCloud() flush() - assertEquals(listOf("cloud_1", "cloud_2"), controller.model.cloud.map { it.id }) + assertEquals(listOf("cloud_1", "cloud_2"), controller.cloud.items.map { it.id }) assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 50, "git@example.com:repo.git"), rpc.cloudCalls[1]) } fun `test local delete calls rpc and removes item`() { rpc.listed += session("ses_1", "Local One") val controller = controller() - controller.loadLocal() + controller.reloadLocal() flush() - controller.delete(controller.model.local[0]) + controller.delete(controller.local.items[0]) flush() assertEquals(listOf("ses_1" to "/test"), rpc.deletes) - assertTrue(controller.model.local.isEmpty()) - } - - fun `test cloud delete emits unsupported error`() { - val controller = controller() - val item = HistoryItem("cloud_1", HistorySource.CLOUD, "Cloud", "a", "b") - - controller.delete(item) - flush() - - assertEquals(emptyList>(), rpc.deletes) - assertEquals("Cloud sessions cannot be deleted yet", controller.model.cloudError) + assertTrue(controller.local.items.isEmpty()) } fun `test panel filters and switches source`() { @@ -155,6 +146,22 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(1, panel.itemCount()) } + fun `test panel refresh reloads local history`() { + rpc.listed += session("ses_1", "One") + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + assertEquals(listOf("ses_1"), controller.local.items.map { it.id }) + + rpc.listed.clear() + rpc.listed += session("ses_2", "Two") + panel.refresh() + flush() + + assertEquals(listOf("ses_2"), controller.local.items.map { it.id }) + } + fun `test panel groups sessions by date`() { val now = Instant.now() rpc.listed += session("ses_today", "Today", now.toEpochMilli().toDouble()) @@ -183,12 +190,22 @@ class HistoryControllerTest : BasePlatformTestCase() { private fun controller() = HistoryController(sessions, workspace, scope) - private fun collect(controller: HistoryController): MutableList { - val events = mutableListOf() - controller.model.addListener(parent) { event -> - assertTrue(ApplicationManager.getApplication().isDispatchThread) - events.add(event) + private fun collect(controller: HistoryController): MutableList { + val events = mutableListOf() + val listener = object : ListDataListener { + override fun intervalAdded(e: ListDataEvent) = record() + + override fun intervalRemoved(e: ListDataEvent) = record() + + override fun contentsChanged(e: ListDataEvent) = record() + + private fun record() { + assertTrue(ApplicationManager.getApplication().isDispatchThread) + events.add("loading=${controller.local.loading} size=${controller.local.items.size} error=${controller.local.error}") + } } + controller.local.addListDataListener(listener) + Disposer.register(parent) { controller.local.removeListDataListener(listener) } return events } From 98ef2c9a56df5e0a9114c1b3607660e1d1430b56 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 21:13:11 -0400 Subject: [PATCH 011/111] fix(jetbrains): refresh history theme --- .../session/history/HistoryListRenderer.kt | 1 + .../client/session/history/HistoryPanel.kt | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index a01aff045fd..0fa46be66f5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -68,6 +68,7 @@ internal open class HistoryRenderer( private val del = JBLabel().apply { horizontalAlignment = SwingConstants.CENTER verticalAlignment = SwingConstants.CENTER + border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) } private val main = JPanel(BorderLayout()).apply { add(title, BorderLayout.CENTER) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index e76cd1f54f7..ab324892679 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -2,6 +2,8 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.ui.UiStyle +import com.intellij.ide.ui.LafManagerListener +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.Disposable import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Disposer @@ -27,6 +29,7 @@ import javax.swing.JButton import javax.swing.JComponent import javax.swing.JList import javax.swing.ListSelectionModel +import javax.swing.SwingUtilities import javax.swing.event.DocumentEvent import javax.swing.event.ListDataEvent import javax.swing.event.ListDataListener @@ -65,6 +68,7 @@ class HistoryPanel( more.addActionListener { controller.loadMoreCloud() } bind(localList, controller.local) bind(cloudList, controller.cloud) + bindTheme() addHierarchyListener { e -> if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener if (isShowing && stale) { @@ -82,9 +86,33 @@ class HistoryPanel( fun refresh() { stale = false + updateTheme() controller.reload(gitUrl()) } + private fun bindTheme() { + val bus = ApplicationManager.getApplication().messageBus.connect(this) + bus.subscribe(LafManagerListener.TOPIC, LafManagerListener { + ApplicationManager.getApplication().invokeLater { + updateTheme() + } + }) + } + + private fun updateTheme() { + SwingUtilities.updateComponentTreeUI(this) + SwingUtilities.updateComponentTreeUI(localPanel) + SwingUtilities.updateComponentTreeUI(cloudPanel) + updateRenderer(localList) + updateRenderer(cloudList) + sync() + } + + private fun updateRenderer(list: JBList) { + val view = list.cellRenderer + if (view is JComponent) SwingUtilities.updateComponentTreeUI(view) + } + private fun search(model: HistoryModel) = SearchTextField(false).apply { textEditor.emptyText.text = KiloBundle.message("history.search.placeholder") textEditor.document.addDocumentListener(object : DocumentAdapter() { From 5a216e5d0c03b677d4b104437d5a0d50f14c5e06 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 21:18:17 -0400 Subject: [PATCH 012/111] fix(jetbrains): format cloud history times --- .../client/session/history/HistoryTime.kt | 15 +++++++++-- .../session/history/HistoryControllerTest.kt | 25 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt index 89c4687d4ae..1b3f9516ecf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle import java.time.Instant import java.time.LocalDate +import java.time.OffsetDateTime import java.time.ZoneId import java.time.temporal.ChronoUnit import kotlin.math.abs @@ -22,8 +23,7 @@ enum class HistorySection { internal object HistoryTime { fun millis(item: HistoryItem): Long? { - if (item is CloudHistoryItem) return runCatching { Instant.parse(item.updatedAt).toEpochMilli() }.getOrNull() - val raw = item.updatedAt.toDoubleOrNull()?.toLong() ?: return null + val raw = item.updatedAt.toDoubleOrNull()?.toLong() ?: return parse(item.updatedAt) if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000 return raw } @@ -68,4 +68,15 @@ internal object HistoryTime { .thenBy { it.title.lowercase() } .thenBy { it.id }, ) + + private fun parse(value: String): Long? { + val text = value.trim().replace(' ', 'T') + val zoned = when { + text.matches(Regex(".*[+-]\\d{2}$")) -> "$text:00" + text.matches(Regex(".*[+-]\\d{4}$")) -> text.dropLast(2) + ":" + text.takeLast(2) + else -> text + } + return runCatching { Instant.parse(zoned).toEpochMilli() }.getOrNull() + ?: runCatching { OffsetDateTime.parse(zoned).toInstant().toEpochMilli() }.getOrNull() + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 328742894f6..2c77dd674df 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.session.history import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.rpc.dto.CloudSessionDto @@ -175,6 +176,24 @@ class HistoryControllerTest : BasePlatformTestCase() { assertTrue(panel.groupTitles().containsAll(listOf("Today", "Yesterday", "This Week", "Older"))) } + fun `test cloud history uses relative time and sections`() { + val now = Instant.now() + val today = CloudHistoryItem(cloud("cloud_today", "Today", now.minus(5, ChronoUnit.HOURS))) + val yesterday = CloudHistoryItem(cloud("cloud_yesterday", "Yesterday", now.minus(1, ChronoUnit.DAYS))) + val offset = CloudHistoryItem( + cloud( + "cloud_offset", + "Offset", + now.minus(10, ChronoUnit.HOURS).toString().replace("Z", "+00"), + ), + ) + + assertEquals(KiloBundle.message("history.time.hours", 5), HistoryTime.relative(today, now.toEpochMilli())) + assertEquals(HistorySection.TODAY, HistoryTime.section(today, now.toEpochMilli())) + assertEquals(HistorySection.YESTERDAY, HistoryTime.section(yesterday, now.toEpochMilli())) + assertEquals(KiloBundle.message("history.time.hours", 10), HistoryTime.relative(offset, now.toEpochMilli())) + } + fun `test local renderer exposes delete and cloud renderer hides it`() { rpc.listed += session("ses_1", "Local") rpc.cloud += cloud("cloud_1", "Cloud") @@ -225,11 +244,13 @@ class HistoryControllerTest : BasePlatformTestCase() { time = SessionTimeDto(created = 1.0, updated = updated), ) - private fun cloud(id: String, title: String) = CloudSessionDto( + private fun cloud(id: String, title: String, updated: Instant = Instant.parse("2026-01-02T00:00:00Z")) = cloud(id, title, updated.toString()) + + private fun cloud(id: String, title: String, updated: String) = CloudSessionDto( id = id, title = title, createdAt = "2026-01-01T00:00:00Z", - updatedAt = "2026-01-02T00:00:00Z", + updatedAt = updated, version = 1.0, ) } From b237dc33c70d3a113001ac9954753b4ad385bc11 Mon Sep 17 00:00:00 2001 From: kirillk Date: Wed, 6 May 2026 21:45:49 -0400 Subject: [PATCH 013/111] fix(jetbrains): focus history filter --- .../client/session/SessionSidePanelManager.kt | 18 ++++++++- .../client/session/history/HistoryPanel.kt | 36 +++++++++++++++++- .../session/SessionSidePanelManagerTest.kt | 11 ++++++ .../session/history/HistoryControllerTest.kt | 37 +++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 40e523b5fbe..e7427d4c314 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -8,9 +8,12 @@ import ai.kilocode.client.session.history.HistoryPanel import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.IdeFocusManager import kotlinx.coroutines.cancel import java.awt.BorderLayout import javax.swing.JComponent @@ -37,7 +40,7 @@ class SessionSidePanelManager( private var current: SessionUi? = null private var panel: JComponent? = null - val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent + val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent ?: (panel as? HistoryPanel)?.defaultFocusedComponent override fun newSession() { val active = current @@ -62,12 +65,23 @@ class SessionSidePanelManager( val cached = panel val view = cached ?: createHistory().also { panel = it } if (cached != null && view is HistoryPanel) view.refresh() - if (current == null && component.componentCount == 1 && component.getComponent(0) === view) return + if (current == null && component.componentCount == 1 && component.getComponent(0) === view) { + focusHistory(view) + return + } current = null component.removeAll() component.add(view, BorderLayout.CENTER) component.revalidate() component.repaint() + focusHistory(view) + } + + private fun focusHistory(view: JComponent) { + val focus = (view as? HistoryPanel)?.defaultFocusedComponent ?: return + ApplicationManager.getApplication().invokeLater({ + IdeFocusManager.getInstance(project).requestFocusInProject(focus, project) + }, ModalityState.defaultModalityState()) } private fun createHistory(): JComponent { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index ab324892679..be193ebf816 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -23,11 +23,13 @@ import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.event.HierarchyEvent +import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JButton import javax.swing.JComponent import javax.swing.JList +import javax.swing.KeyStroke import javax.swing.ListSelectionModel import javax.swing.SwingUtilities import javax.swing.event.DocumentEvent @@ -53,8 +55,8 @@ class HistoryPanel( presentation.setSingleRow(true) presentation.setTabsPosition(JBTabsPosition.top) presentation.showBorder = false - addTab(localInfo) - addTab(cloudInfo) + addTab(localInfo).setPreferredFocusableComponent(localSearch.textEditor) + addTab(cloudInfo).setPreferredFocusableComponent(cloudSearch.textEditor) addListener(object : TabsListener { override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { sync() @@ -84,6 +86,8 @@ class HistoryPanel( val component: JComponent get() = this + val defaultFocusedComponent: JComponent get() = activeSearch().textEditor + fun refresh() { stale = false updateTheme() @@ -120,6 +124,16 @@ class HistoryPanel( model.setFilter(text) } }) + textEditor.registerKeyboardAction( + { move(-1) }, + KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), + JComponent.WHEN_FOCUSED, + ) + textEditor.registerKeyboardAction( + { move(1) }, + KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), + JComponent.WHEN_FOCUSED, + ) } private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { @@ -138,6 +152,7 @@ class HistoryPanel( private fun localList() = JBList(controller.local).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION + isFocusable = false cellRenderer = LocalHistoryRenderer(controller.local) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { @@ -158,6 +173,7 @@ class HistoryPanel( private fun cloudList() = JBList(controller.cloud).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION + isFocusable = false cellRenderer = CloudHistoryRenderer(controller.cloud) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { @@ -236,6 +252,10 @@ class HistoryPanel( activeList().selectedIndex = index } + internal fun selectedIndex() = activeList().selectedIndex + + internal fun listFocusable() = activeList().isFocusable + internal fun clickDelete() { localList.selectedValue?.let(controller::delete) } @@ -279,6 +299,18 @@ class HistoryPanel( private fun activeModel(): HistoryModel = if (tabs.selectedInfo === cloudInfo) controller.cloud else controller.local + private fun activeSearch(): SearchTextField = if (tabs.selectedInfo === cloudInfo) cloudSearch else localSearch + + private fun move(step: Int) { + val list = activeList() + val size = list.model.size + if (size <= 0) return + val cur = list.selectedIndex.takeIf { it >= 0 } ?: if (step > 0) -1 else size + val idx = (cur + step).coerceIn(0, size - 1) + list.selectedIndex = idx + ScrollingUtil.ensureIndexIsVisible(list, idx, 0) + } + override fun dispose() { // no-op } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 87d3a0e6b07..6049a12ee08 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.session.history.HistoryPanel import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi @@ -109,6 +110,16 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertNotSame(first.defaultFocusedComponent, manager.defaultFocusedComponent) } + fun `test default focused component tracks history filter`() { + val manager = manager() + + manager.showHistory() + settle() + val history = active(manager) as HistoryPanel + + assertSame(history.defaultFocusedComponent, manager.defaultFocusedComponent) + } + fun `test opening same existing session reuses component`() { val manager = manager() val session = session("ses_1") diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 2c77dd674df..7d11fbebe32 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -21,8 +21,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import java.awt.event.KeyEvent import java.time.Instant import java.time.temporal.ChronoUnit +import javax.swing.JComponent +import javax.swing.KeyStroke import javax.swing.event.ListDataEvent import javax.swing.event.ListDataListener @@ -147,6 +150,35 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(1, panel.itemCount()) } + fun `test panel focuses active search and moves list selection from search`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + rpc.cloud += cloud("cloud_1", "Cloud One") + rpc.cloud += cloud("cloud_2", "Cloud Two") + val panel = HistoryPanel(parent, controller()) + flush() + + val local = panel.defaultFocusedComponent + assertFalse(panel.listFocusable()) + assertEquals(-1, panel.selectedIndex()) + key(local, KeyEvent.VK_DOWN) + assertEquals(0, panel.selectedIndex()) + key(local, KeyEvent.VK_DOWN) + assertEquals(1, panel.selectedIndex()) + key(local, KeyEvent.VK_UP) + assertEquals(0, panel.selectedIndex()) + + panel.clickCloud() + flush() + + val cloud = panel.defaultFocusedComponent + assertNotSame(local, cloud) + assertFalse(panel.listFocusable()) + assertEquals(-1, panel.selectedIndex()) + key(cloud, KeyEvent.VK_DOWN) + assertEquals(0, panel.selectedIndex()) + } + fun `test panel refresh reloads local history`() { rpc.listed += session("ses_1", "One") val controller = controller() @@ -235,6 +267,11 @@ class HistoryControllerTest : BasePlatformTestCase() { } } + private fun key(component: JComponent, code: Int) { + val action = component.getActionForKeyStroke(KeyStroke.getKeyStroke(code, 0)) + requireNotNull(action).actionPerformed(null) + } + private fun session(id: String, title: String, updated: Double = 2.0) = SessionDto( id = id, projectID = "prj", From 16e0d314cc89e4b8ef2d5ac1263d2133f3a6c86c Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 15:52:53 -0400 Subject: [PATCH 014/111] fix(jetbrains): open history panel sessions --- .../backend/app/KiloBackendSessionManager.kt | 23 +++++++++++ .../backend/rpc/KiloSessionRpcApiImpl.kt | 3 ++ .../app/KiloBackendSessionManagerTest.kt | 39 +++++++++++++++++++ .../kilocode/backend/testing/MockCliServer.kt | 9 +++++ .../kilocode/client/app/KiloSessionService.kt | 3 ++ .../session/history/HistoryController.kt | 29 +++++++++++++- .../client/session/history/HistoryPanel.kt | 26 ++++++++++++- .../session/history/HistoryControllerTest.kt | 37 ++++++++++++++++++ .../client/testing/FakeSessionRpcApi.kt | 8 ++++ .../ai/kilocode/rpc/KiloSessionRpcApi.kt | 3 ++ 10 files changed, 176 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index f3e410622da..cdfffd9911f 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -182,6 +182,25 @@ class KiloBackendSessionManager( } } + fun importCloudSession(id: String, dir: String): SessionDto { + val h = http ?: throw IllegalStateException("Session manager not started") + val url = base ?: throw IllegalStateException("Session manager not started") + val json = """{"sessionId":"${escape(id)}"}""" + val request = Request.Builder() + .url("$url/kilo/cloud/session/import?directory=${encode(dir)}") + .post(json.toRequestBody("application/json".toMediaType())) + .build() + + h.newCall(request).execute().use { response -> + val raw = response.body?.string().orEmpty() + if (!response.isSuccessful) { + log.warn("Cloud session import failed: HTTP ${response.code}, body=$raw") + throw RuntimeException("Cloud session import failed: HTTP ${response.code} — $raw") + } + return KiloCliDataParser.parseSession(raw) + } + } + fun seed(dir: String) { try { val raw = requireClient().sessionStatus(directory = dir) @@ -256,4 +275,8 @@ class KiloBackendSessionManager( ) private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8) + + private fun escape(value: String) = value + .replace("\\", "\\\\") + .replace("\"", "\\\"") } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 613c64e5e02..302e3bb9f3b 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -74,6 +74,9 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi { override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = sessions.cloudSessions(directory, cursor, limit, gitUrl) + override suspend fun importCloudSession(id: String, directory: String): SessionDto = + sessions.importCloudSession(id, directory) + override suspend fun statuses(): Flow> = sessions.statuses diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt index 8de50bdd6a4..0f653f89f11 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt @@ -244,6 +244,45 @@ class KiloBackendSessionManagerTest { assertTrue(err.message.orEmpty().contains("boom")) } + @Test + fun `importCloudSession posts cloud id and returns local session`() = runBlocking { + mock.cloudSessionImport = """{ + "id":"ses_imported", + "slug":"imported", + "projectID":"prj_test", + "directory":"/repo", + "title":"Imported Cloud", + "version":"1.0.0", + "time":{"created":1000,"updated":2000} + }""" + val app = setup() + ready(app) + + val session = app.sessions.importCloudSession("cloud_1", "/repo path") + + assertEquals("ses_imported", session.id) + assertEquals("Imported Cloud", session.title) + val path = mock.lastCloudSessionImportPath ?: error("missing cloud import request") + assertTrue(path.startsWith("/kilo/cloud/session/import?")) + assertTrue(URLDecoder.decode(path, "UTF-8").contains("directory=/repo path"), path) + assertEquals("""{"sessionId":"cloud_1"}""", mock.lastCloudSessionImportBody) + } + + @Test + fun `importCloudSession surfaces server failure`() = runBlocking { + mock.cloudSessionImportStatus = 500 + mock.cloudSessionImport = """{"error":"boom"}""" + val app = setup() + ready(app) + + val err = assertFailsWith { + app.sessions.importCloudSession("cloud_1", "/test") + } + + assertTrue(err.message.orEmpty().contains("HTTP 500")) + assertTrue(err.message.orEmpty().contains("boom")) + } + // ------ Session create ------ @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 6de93c1e9fc..5b234053a70 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -63,8 +63,12 @@ class MockCliServer : AutoCloseable { @Volatile var sessionDeleteStatus = 200 @Volatile var sessionStatusesStatus = 200 @Volatile var cloudSessions = """{"cliSessions":[],"nextCursor":null}""" + @Volatile var cloudSessionImport = """{"id":"ses_imported","slug":"imported","projectID":"prj_test","directory":"/test","title":"Imported Session","version":"1.0.0","time":{"created":1000,"updated":1000}}""" @Volatile var cloudSessionsStatus = 200 + @Volatile var cloudSessionImportStatus = 200 @Volatile var lastCloudSessionsPath: String? = null + @Volatile var lastCloudSessionImportPath: String? = null + @Volatile var lastCloudSessionImportBody: String? = null @Volatile var summarizeStatus = 200 @Volatile var lastSummarizePath: String? = null @Volatile var lastSummarizeBody: String? = null @@ -227,6 +231,11 @@ class MockCliServer : AutoCloseable { lastCloudSessionsPath = path respond(output, cloudSessionsStatus, cloudSessions) } + bare == "/kilo/cloud/session/import" && method == "POST" -> { + lastCloudSessionImportPath = path + lastCloudSessionImportBody = body + respond(output, cloudSessionImportStatus, cloudSessionImport) + } bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index 7946a067ebd..b047162ee64 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -127,6 +127,9 @@ class KiloSessionService internal constructor( suspend fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = call { cloudSessions(dir, cursor, limit, gitUrl) } + suspend fun importCloudSession(id: String, dir: String): SessionDto = + call { importCloudSession(id, dir) } + /** Register a worktree directory override for a session. */ fun setDirectory(id: String, dir: String) { cs.launch { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 0e2d0cd2562..2b0406172f0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -13,7 +13,7 @@ class HistoryController( private val sessions: KiloSessionService, private val workspace: Workspace, private val cs: CoroutineScope, - private val open: (LocalHistoryItem) -> Unit = {}, + open: (LocalHistoryItem) -> Unit = {}, private val deleted: (String) -> Unit = {}, ) { companion object { @@ -24,6 +24,8 @@ class HistoryController( val cloud = CloudHistoryModel() private val deleting = mutableSetOf() + private val importing = mutableSetOf() + private val opener = open private var git: String? = null fun reload(gitUrl: String? = null) { @@ -80,7 +82,30 @@ class HistoryController( fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting fun open(item: LocalHistoryItem) { - edt { open(item) } + edt { opener(item) } + } + + fun open(item: CloudHistoryItem) { + edt { + if (item.id in importing) return@edt + importing.add(item.id) + cloud.refresh() + cs.launch { + try { + val session = sessions.importCloudSession(item.id, workspace.directory) + edt { + importing.remove(item.id) + cloud.refresh() + opener(LocalHistoryItem(session)) + } + } catch (e: Exception) { + edt { + importing.remove(item.id) + cloud.fail(e.message ?: KiloBundle.message("history.error.cloud")) + } + } + } + } } private fun loadCloud(reset: Boolean) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index be193ebf816..ba03abed22f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -134,6 +134,11 @@ class HistoryPanel( KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), JComponent.WHEN_FOCUSED, ) + textEditor.registerKeyboardAction( + { activeList().selectedValue?.let(::activate) }, + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + JComponent.WHEN_FOCUSED, + ) } private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { @@ -164,9 +169,14 @@ class HistoryPanel( e.consume() return } - controller.open(item) + activate(item) } }) + registerKeyboardAction( + { selectedValue?.let(::activate) }, + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + JComponent.WHEN_FOCUSED, + ) ListUtil.installAutoSelectOnMouseMove(this) ScrollingUtil.installActions(this) } @@ -179,9 +189,14 @@ class HistoryPanel( addMouseListener(object : MouseAdapter() { override fun mouseReleased(e: MouseEvent) { if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return - clicked(this@apply, e) + clicked(this@apply, e)?.let(::activate) } }) + registerKeyboardAction( + { selectedValue?.let(::activate) }, + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + JComponent.WHEN_FOCUSED, + ) ListUtil.installAutoSelectOnMouseMove(this) ScrollingUtil.installActions(this) } @@ -232,6 +247,13 @@ class HistoryPanel( return list.model.getElementAt(row) } + private fun activate(item: HistoryItem) { + when (item) { + is LocalHistoryItem -> controller.open(item) + is CloudHistoryItem -> controller.open(item) + } + } + private fun confirm(item: LocalHistoryItem) { if (controller.deleting(item)) return val result = Messages.showYesNoDialog( diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 7d11fbebe32..b86f9dc69d6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -179,6 +179,39 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(0, panel.selectedIndex()) } + fun `test panel opens selected local session on enter from search`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + val opened = mutableListOf() + val panel = HistoryPanel(parent, controller(opened)) + flush() + + val search = panel.defaultFocusedComponent + key(search, KeyEvent.VK_DOWN) + key(search, KeyEvent.VK_DOWN) + key(search, KeyEvent.VK_ENTER) + + assertEquals(listOf("ses_2"), opened) + } + + fun `test panel imports and opens selected cloud session on enter from search`() { + rpc.cloud += cloud("cloud_1", "Cloud") + rpc.importedCloudSession = session("ses_imported", "Imported") + val opened = mutableListOf() + val panel = HistoryPanel(parent, controller(opened)) + flush() + + panel.clickCloud() + flush() + val search = panel.defaultFocusedComponent + key(search, KeyEvent.VK_DOWN) + key(search, KeyEvent.VK_ENTER) + flush() + + assertEquals(listOf("cloud_1" to "/test"), rpc.imports) + assertEquals(listOf("ses_imported"), opened) + } + fun `test panel refresh reloads local history`() { rpc.listed += session("ses_1", "One") val controller = controller() @@ -241,6 +274,10 @@ class HistoryControllerTest : BasePlatformTestCase() { private fun controller() = HistoryController(sessions, workspace, scope) + private fun controller(opened: MutableList) = HistoryController(sessions, workspace, scope, open = { item -> + opened.add(item.id) + }) + private fun collect(controller: HistoryController): MutableList { val events = mutableListOf() val listener = object : ListDataListener { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index 39d094d95f6..dd7c8537930 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -58,6 +58,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi { /** Cloud sessions returned by [cloudSessions]. */ val cloud = mutableListOf() var cloudCursor: String? = null + var importedCloudSession = session /** Push chat events here; tests collect from [events]. */ val events = MutableSharedFlow(extraBufferCapacity = 64, replay = 64) @@ -88,6 +89,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi { val lists = mutableListOf() val recentCalls = mutableListOf>() val cloudCalls = mutableListOf() + val imports = mutableListOf>() var creates = 0 private set @@ -135,6 +137,12 @@ class FakeSessionRpcApi : KiloSessionRpcApi { return CloudSessionListDto(cloud.take(limit), cloudCursor) } + override suspend fun importCloudSession(id: String, directory: String): SessionDto { + assertNotEdt("importCloudSession") + imports.add(id to directory) + return importedCloudSession + } + override suspend fun statuses(): Flow> { assertNotEdt("statuses") return statuses diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index 7699a7c40d2..c467af110d1 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -54,6 +54,9 @@ interface KiloSessionRpcApi : RemoteApi { /** List cloud-backed sessions. */ suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto + /** Import a cloud-backed session into local storage. */ + suspend fun importCloudSession(id: String, directory: String): SessionDto + /** Observe live session status changes. */ suspend fun statuses(): Flow> From 6b5ace20b7f7104187336b5b591723ffb4541c01 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 21:50:24 -0400 Subject: [PATCH 015/111] fix(jetbrains): unify session identity in SessionRef and gate recents behind blank controller Replace duplicated sessionId/cloud/initialSession controller fields with a single mutable ref: SessionRef? computed from the incoming target. Explicit local and cloud session loads now show the session view on completion instead of falling back to recents. Recents can only run on blank controllers (ref == null, sessionLoadState Idle, showSession false) via a canUseRecents() guard applied at all recents entry points. SessionRef gains id, type, and a from()/resolve() parser so id-only Local refs are possible. SessionSidePanelManager cache keys switch to ref.key/cacheKey and cloud-imported sessions can be reused by their local id after import. Tests cover empty-explicit-session-shows-session, no-recents- during-load, cloud import isolation, ref snapshot assertions, and cloud-to-local reuse. --- .../ai/kilocode/client/session/SessionRef.kt | 46 +++ .../client/session/SessionSidePanelManager.kt | 53 +++- .../ai/kilocode/client/session/SessionUi.kt | 79 ++--- .../client/session/SessionUiFactory.kt | 8 +- .../client/session/ui/LoadingPanel.kt | 23 ++ .../session/update/SessionController.kt | 292 +++++++++++++----- .../kilocode/client/session/SessionRefTest.kt | 62 ++++ .../session/SessionSidePanelManagerTest.kt | 104 ++++++- .../client/session/SessionUiFactoryTest.kt | 20 +- .../client/session/SessionUiLayoutTest.kt | 81 ++++- .../session/history/HistoryControllerTest.kt | 15 +- .../session/ui/EmptySessionPanelTest.kt | 3 +- .../client/session/ui/PermissionPanelTest.kt | 3 +- .../client/session/ui/QuestionPanelTest.kt | 3 +- .../session/update/DisposedStateTest.kt | 128 ++++++++ .../session/update/HistoryLoadingTest.kt | 27 ++ .../update/SessionControllerTestBase.kt | 31 +- .../session/update/ViewSwitchingTest.kt | 135 ++++++-- 18 files changed, 901 insertions(+), 212 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionRefTest.kt create mode 100644 packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt new file mode 100644 index 00000000000..323bdcee5f2 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt @@ -0,0 +1,46 @@ +package ai.kilocode.client.session + +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.SessionDto + +sealed interface SessionRef { + enum class Type { LOCAL, CLOUD } + + val type: Type + val id: String + val key: String + + data class Local(override val id: String, val session: SessionDto? = null) : SessionRef { + constructor(session: SessionDto) : this(session.id, session) + + override val type: Type get() = Type.LOCAL + override val key: String get() = id + } + + data class Cloud(override val id: String, val session: CloudSessionDto? = null) : SessionRef { + constructor(session: CloudSessionDto) : this(session.id, session) + + override val type: Type get() = Type.CLOUD + override val key: String get() = "$CLOUD_PREFIX$id" + } + + companion object { + const val CLOUD_PREFIX = "cloud:" + + fun from(id: String?): SessionRef? { + val value = id?.takeIf { it.isNotBlank() } ?: return null + if (value.startsWith(CLOUD_PREFIX)) return Cloud(value.removePrefix(CLOUD_PREFIX)) + return Local(value) + } + + fun cloud(id: String?): Cloud? { + return from(id) as? Cloud + } + + fun resolve(id: String? = null, session: SessionDto? = null, target: SessionRef? = null): SessionRef? { + if (target != null) return target + if (session != null) return Local(session) + return from(id) + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index e7427d4c314..64e7ffd4e04 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -22,11 +22,11 @@ import javax.swing.JPanel class SessionSidePanelManager( private val project: Project, private val root: Workspace, - private val create: (Project, Workspace, SessionManager, String?, Boolean, SessionDto?) -> SessionUi = { project, workspace, manager, id, loading, session -> - service().create(project, workspace, manager, id, loading, session) + private val create: (Project, Workspace, SessionManager, String?, SessionDto?, SessionRef?) -> SessionUi = { project, workspace, manager, id, session, target -> + service().create(project, workspace, manager, id, session, target) }, private val resolve: (String) -> Workspace = { dir -> service().workspace(dir) }, - private val history: ((Disposable, (SessionDto) -> Unit, (String) -> Unit) -> JComponent)? = null, + private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, ) : SessionManager, Disposable { val component: JPanel = object : JPanel(BorderLayout()), DataProvider { override fun getData(dataId: String): Any? { @@ -46,19 +46,46 @@ class SessionSidePanelManager( val active = current if (active?.blank == true) return register(active) - show(create(project, root, this, null, active == null, null)) + show(create(project, root, this, null, null, null)) } override fun openSession(session: SessionDto) { + openSession(SessionRef.Local(session)) + } + + override fun openSession(ref: SessionRef) { register(current) - val ui = opened.getOrPut(session.id) { - create(project, resolve(session.directory), this, session.id, false, session).also { - all.add(it) - } + val ui = opened[ref.key] ?: run { + val local = (ref as? SessionRef.Local)?.session?.id + val existing = local?.let { opened[it] } + if (existing != null) { + opened[ref.key] = existing + existing + } else create(ref) } show(ui) } + private fun create(ref: SessionRef): SessionUi { + val workspace = when (ref) { + is SessionRef.Local -> ref.session?.directory?.let(resolve) ?: root + is SessionRef.Cloud -> root + } + val id = when (ref) { + is SessionRef.Local -> ref.id + is SessionRef.Cloud -> ref.key + } + val session = (ref as? SessionRef.Local)?.session + return create(project, workspace, this, id, session, ref).also { + all.add(it) + opened[ref.key] = it + val local = session?.id + if (local != null) { + opened.putIfAbsent(local, it) + } + } + } + override fun showHistory() { register(current) release(current) @@ -93,7 +120,7 @@ class SessionSidePanelManager( sessions = project.service(), workspace = root, cs = cs, - open = { item -> openSession(item.session) }, + open = this::openSession, deleted = this::removeSession, ) Disposer.register(this) { cs.cancel() } @@ -102,6 +129,7 @@ class SessionSidePanelManager( private fun removeSession(id: String) { val ui = opened.remove(id) ?: return + opened.entries.removeIf { it.value === ui } all.remove(ui) if (current === ui) current = null Disposer.dispose(ui) @@ -109,6 +137,7 @@ class SessionSidePanelManager( private fun show(ui: SessionUi) { all.add(ui) + register(ui) if (current === ui) return release(current) component.removeAll() @@ -119,13 +148,13 @@ class SessionSidePanelManager( } private fun register(ui: SessionUi?) { - val id = ui?.id ?: return - opened.putIfAbsent(id, ui) + val key = ui?.cacheKey ?: return + opened.putIfAbsent(key, ui) } private fun release(ui: SessionUi?) { if (ui == null) return - if (ui.id != null) { + if (ui.cacheKey != null) { register(ui) return } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index f555b0e6377..3e583ff87ce 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -3,11 +3,11 @@ package ai.kilocode.client.session import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.ui.ConnectionPanel import ai.kilocode.client.session.ui.EmptySessionPanel +import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker @@ -32,8 +32,6 @@ import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.openapi.util.registry.Registry -import com.intellij.ui.components.JBLabel -import com.intellij.util.ui.Centerizer import kotlinx.coroutines.CoroutineScope import java.awt.BorderLayout import javax.swing.BoxLayout @@ -47,51 +45,27 @@ import javax.swing.JPanel * It builds the session panels, wires controller/model listeners, and swaps the * center body between the empty state and the message list. */ -class SessionUi private constructor( +class SessionUi( project: Project, workspace: Workspace, sessions: KiloSessionService, app: KiloAppService, cs: CoroutineScope, - id: String?, - displayMs: Long, - open: (SessionDto) -> Unit, - private val loading: Boolean, + id: String? = null, + displayMs: Long = SessionController.DISPLAY_DELAY_MS, + open: (SessionRef) -> Unit = {}, session: SessionDto? = null, + target: SessionRef? = SessionRef.resolve(id, session), ) : JPanel(BorderLayout()), Disposable, SessionStyleTarget { - constructor( - project: Project, - workspace: Workspace, - sessions: KiloSessionService, - app: KiloAppService, - cs: CoroutineScope, - id: String? = null, - displayMs: Long = SessionController.DISPLAY_DELAY_MS, - open: (SessionDto) -> Unit = {}, - session: SessionDto? = null, - ) : this(project, workspace, sessions, app, cs, session?.id ?: id, displayMs, open, id == null, session) - - internal constructor( - project: Project, - workspace: Workspace, - sessions: KiloSessionService, - app: KiloAppService, - cs: CoroutineScope, - id: String? = null, - displayMs: Long = SessionController.DISPLAY_DELAY_MS, - loading: Boolean, - open: (SessionDto) -> Unit = {}, - session: SessionDto? = null, - ) : this(project, workspace, sessions, app, cs, id, displayMs, open, loading, session) - companion object { private val LOG = KiloLog.create(SessionUi::class.java) } private val project = project private val app = app - private var opening = id != null + private val ref = SessionRef.resolve(id, session, target) + private var opening = ref != null private var pending = false private var loaded: Boolean? = null private val flushMs = @@ -101,15 +75,14 @@ class SessionUi private constructor( ?: EVENT_FLUSH_MS private val controller = SessionController( - this, id, sessions, workspace, app, cs, this, + this, ref, sessions, workspace, app, cs, this, flushMs = flushMs, condense = Registry.`is`("kilo.session.condense", true), displayMs = displayMs, - session = session, open = open, beforeUpdate = { if (opening) false else scroll.atBottom() }, afterUpdate = { if (!opening) scroll.followBottom(it) }, - loaded = ::onHistoryLoaded, + loaded = ::onSessionLoaded, ) @@ -132,16 +105,16 @@ class SessionUi private constructor( private lateinit var connection: ConnectionPanel private lateinit var prompt: PromptPanel - private lateinit var loadingLabel: JBLabel + private lateinit var load: LoadingPanel private var style = SessionStyle.current() init { buildUi() + scroll.show(body(controller.model.state)) bindUi() bindStyle() applyStyle(style) onStateChanged(controller.model.state) - scroll.show(startBody()) loaded?.let(::finishOpen) } @@ -159,6 +132,8 @@ class SessionUi private constructor( internal val id: String? get() = controller.id + internal val cacheKey: String? get() = controller.refKey + internal fun currentStyle() = style val defaultFocusedComponent: JComponent get() = prompt.defaultFocusedComponent @@ -172,14 +147,8 @@ class SessionUi private constructor( isOpaque = false } - progressBody = JPanel(BorderLayout()).apply { - isOpaque = false - loadingLabel = JBLabel(KiloBundle.message("session.empty.loading")) - add(Centerizer( - loadingLabel, - Centerizer.TYPE.BOTH, - ), BorderLayout.CENTER) - } + load = LoadingPanel() + progressBody = load messageBody = SessionMessageListPanel(controller.model, this) header = SessionHeaderPanel(controller, this) @@ -309,18 +278,18 @@ class SessionUi private constructor( }) } - private fun startBody(): JPanel { - if (controller.model.showSession) return messageBody - if (loading) return progressBody - return blankBody - } - - private fun onHistoryLoaded(show: Boolean) { + private fun onSessionLoaded(show: Boolean) { loaded = show if (!this::scroll.isInitialized) return finishOpen(show) } + private fun body(state: SessionState): JPanel { + if (controller.model.showSession) return messageBody + if (state is SessionState.Loading) return progressBody + return blankBody + } + private fun finishOpen(show: Boolean) { loaded = show if (!opening) return @@ -382,7 +351,7 @@ class SessionUi private constructor( override fun applyStyle(style: SessionStyle) { this.style = style - loadingLabel.font = style.uiFont + load.applyStyle(style) header.applyStyle(style) prompt.applyStyle(style) scroll.applyStyle(style) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt index 8464e399e46..106fe57d898 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt @@ -20,18 +20,18 @@ class SessionUiFactory( workspace: Workspace, manager: SessionManager, id: String? = null, - loading: Boolean = id == null, session: SessionDto? = null, + target: SessionRef? = SessionRef.resolve(id, session), ): SessionUi = SessionUi( project = project, workspace = workspace, sessions = project.service(), app = service(), cs = scope(), - id = session?.id ?: id, - loading = loading, + id = id, session = session, - open = manager::openSession, + target = target, + open = { ref -> manager.openSession(ref) }, ) fun scope(): CoroutineScope { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt new file mode 100644 index 00000000000..276bd7c3152 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt @@ -0,0 +1,23 @@ +package ai.kilocode.client.session.ui + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.Centerizer +import java.awt.BorderLayout +import javax.swing.JPanel + +class LoadingPanel : JPanel(BorderLayout()), SessionStyleTarget { + private val label = JBLabel(KiloBundle.message("session.empty.loading")) + + init { + isOpaque = false + add(Centerizer(label, Centerizer.TYPE.BOTH), BorderLayout.CENTER) + applyStyle(SessionStyle.current()) + } + + override fun applyStyle(style: SessionStyle) { + label.font = style.uiFont + revalidate() + repaint() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt index 447ab18a775..b96564a352d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt @@ -17,6 +17,7 @@ import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.model.QuestionItem import ai.kilocode.client.session.model.QuestionOption import ai.kilocode.client.session.model.ToolCallRef +import ai.kilocode.client.session.SessionRef import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.ConfigUpdateDto @@ -49,7 +50,7 @@ import java.awt.Component /** * Session lifecycle orchestrator for a single session. * - * Accepts an optional [id] — if non-null, loads that session immediately. + * Accepts an optional [ref] — if non-null, loads that session immediately. * If null, lazily creates a session on the first [prompt] call. This ensures * event subscription happens before the prompt is sent, eliminating races. * @@ -60,7 +61,7 @@ import java.awt.Component */ class SessionController( parent: Disposable, - id: String?, + ref: SessionRef? = null, private val sessions: KiloSessionService, private val workspace: Workspace, private val app: KiloAppService, @@ -69,8 +70,7 @@ class SessionController( private val flushMs: Long = EVENT_FLUSH_MS, private val condense: Boolean = true, private val displayMs: Long = DISPLAY_DELAY_MS, - session: SessionDto? = null, - private val open: (SessionDto) -> Unit = {}, + private val open: (SessionRef) -> Unit = {}, private val beforeUpdate: () -> Boolean = { false }, private val afterUpdate: (Boolean) -> Unit = {}, private val loaded: (Boolean) -> Unit = {}, @@ -89,8 +89,11 @@ class SessionController( val model = SessionModel() private val listeners = mutableListOf() - private var sessionId: String? = session?.id ?: id - private val initialSession = session + private var ref: SessionRef? = ref + private val local: SessionRef.Local? get() = ref as? SessionRef.Local + private val sid: String? get() = local?.id + private val key: String? get() = ref?.key + private val log: String get() = sid ?: key ?: "pending" private val directory: String get() = workspace.directory private val updates = SessionUpdateQueue( parent, @@ -98,31 +101,41 @@ class SessionController( flushMs, ::handle, condense, - id != null - ) { sessionId ?: "pending" } + ref != null + ) { log } + private var disposed = false private var partType: String? = null private var tool: String? = null private var eventJob: Job? = null - private var historyState: HistoryState = HistoryState.Idle + private var sessionLoadState: SessionLoadState = SessionLoadState.Idle private var recentsState: RecentsState = RecentsState.Idle private var viewState: SessionControllerEvent.ViewChanged? = null private var connectionState: SessionControllerEvent.ConnectionChanged? = null private var connectionTargetState: SessionControllerEvent.ConnectionChanged? = null - private val delayedState = DelayedState(displayMs) + private val connectionDelay = DelayedState(displayMs) val ready: Boolean get() = model.isReady() - internal val blank: Boolean get() = sessionId == null && model.isEmpty() && !model.showSession - internal val id: String? get() = sessionId + internal val blank: Boolean get() = ref == null && model.isEmpty() && !model.showSession + internal val id: String? get() = sid + internal val refKey: String? get() = key + internal val refType: SessionRef.Type? get() = ref?.type fun openSession(session: SessionDto) { assertEdt() - open(session) + open(SessionRef.Local(session)) + } + + fun openSession(ref: SessionRef) { + assertEdt() + open(ref) } fun addListener(parent: Disposable, listener: SessionControllerListener) { + if (disposed) return listeners.add(listener) Disposer.register(parent) { listeners.remove(listener) } + replay(listener) } internal fun snapshotState(): ControllerStateSnapshot { @@ -132,32 +145,38 @@ class SessionController( viewState = viewState, connectionState = connectionState, connectionTargetState = connectionTargetState, - historyState = historyState.toString(), + refKey = key, + refType = ref?.type?.name, + sessionLoadState = sessionLoadState.toString(), recentsState = recentsState.toString(), ) } internal fun flushEvents() { assertEdt() + if (disposed) return updates.requestFlush(true) } fun prompt(text: String) { assertEdt() - val sid = sessionId ?: "pending" + val start = log val dto = promptDto(text) - LOG.debug { "${ChatLogSummary.sid(sid)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } - showMessages() + LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } + showSession() cs.launch { try { - val id = sessionId ?: run { + val id = sid ?: run { val session = sessions.create(directory) runEdt { - sessionId = session.id + if (disposed) return@runEdt + ref = SessionRef.Local(session) + setRecentSessionsState(RecentsState.Idle) updateModel { model.setSession(session) } } + if (disposed) return@launch val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session" LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true") subscribeEvents() @@ -166,8 +185,9 @@ class SessionController( sessions.prompt(id, directory, dto) LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(sessionId ?: sid)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(log.takeUnless { it == "pending" } ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { + if (disposed) return@edt val msg = e.message ?: KiloBundle.message("session.error.prompt") updateModel { model.setState(SessionState.Error(msg)) @@ -179,8 +199,8 @@ class SessionController( fun abort() { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=abort" } - val id = sessionId ?: return + LOG.debug { "${ChatLogSummary.sid(log)} kind=abort" } + val id = sid ?: return cs.launch { try { sessions.abort(id, directory) @@ -193,7 +213,7 @@ class SessionController( fun compact() { assertEdt() - val id = sessionId ?: return + val id = sid ?: return if (model.state.isBusy()) return if (model.isEmpty()) return val parsed = model.model?.let(::parseModel) ?: return @@ -217,7 +237,7 @@ class SessionController( fun retryConnection() { assertEdt() LOG.debug { - "${ChatLogSummary.sid(sessionId ?: "pending")} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}" + "${ChatLogSummary.sid(log)} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}" } setConnectionTargetState(SessionControllerEvent.ConnectionChanged.ShowConnecting) setVisibleConnectionState(SessionControllerEvent.ConnectionChanged.ShowConnecting) @@ -238,12 +258,12 @@ class SessionController( fun selectAgent(name: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config agent=$name" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=config agent=$name" } cs.launch { try { sessions.updateConfig(directory, ConfigUpdateDto(agent = name)) } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(log)} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } fire(SessionControllerEvent.WorkspaceReady) { @@ -254,7 +274,7 @@ class SessionController( fun selectModel(provider: String, id: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config model=$provider/$id" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=config model=$provider/$id" } val agent = model.agent ?: return val key = "$provider/$id" if (item(key) == null && model.workspace.providers != null) return @@ -266,7 +286,7 @@ class SessionController( fun clearModelOverride() { assertEdt() val agent = model.agent ?: return - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config model-reset agent=$agent" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=config model-reset agent=$agent" } app.clearModel(agent) val auto = configModel(agent) ?: providerModel(agent) selectResolvedModel(auto) @@ -277,7 +297,7 @@ class SessionController( assertEdt() val key = model.model ?: return if (value !in model.variants) return - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=config variant=$key/$value" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=config variant=$key/$value" } app.selectVariant(key, value) model.variant = value } @@ -286,53 +306,63 @@ class SessionController( fun replyPermission(requestId: String, reply: PermissionReplyDto, rules: PermissionAlwaysRulesDto? = null) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=permission rid=$requestId reply=${reply.reply}" } cs.launch { try { if (rules != null) sessions.savePermissionRules(requestId, directory, rules) sessions.replyPermission(requestId, directory, reply) - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=permission rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(log)} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } fun replyQuestion(requestId: String, answers: QuestionReplyDto) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size}" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId answers=${answers.answers.size}" } cs.launch { try { sessions.replyQuestion(requestId, directory, answers) - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(log)} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } fun rejectQuestion(requestId: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId rejected=true" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId rejected=true" } cs.launch { try { sessions.rejectQuestion(requestId, directory) - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(sessionId ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(log)} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } init { - initialSession?.let { model.setSession(it) } - if (sessionId != null) { - loadHistory() - subscribeEvents() + local?.session?.let { model.setSession(it) } + when (val item = ref) { + is SessionRef.Cloud -> { + val token = SessionLoadState.Loading() + startSessionLoading(token) + importCloud(item.id, token) + } + is SessionRef.Local -> { + val token = SessionLoadState.Loading() + startSessionLoading(token) + loadSession(token) + subscribeEvents() + } + null -> Unit } model.addListener(this) { event -> - LOG.debug { "session=$sessionId model: $event" } + LOG.debug { "session=$log model: $event" } } app.connect() @@ -402,28 +432,24 @@ class SessionController( if (state.status == KiloWorkspaceStatusDto.READY) { fire(SessionControllerEvent.WorkspaceReady) edt { - if (sessionId == null) refreshRecents() + if (canUseRecents()) refreshRecents() } } } } } - private fun loadHistory() { - val id = sessionId ?: return + private fun loadSession(token: SessionLoadState.Loading) { + val target = local ?: return + val id = target.id cs.launch { - val state = HistoryState.Loading() - runEdt { - setHistoryState(state) - } - delayedState.run(state, { historyState }) { - if (!model.showSession) setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress) - } try { - val session = initialSession ?: runCatching { sessions.get(id, directory) }.getOrNull() + val session = target.session ?: runCatching { sessions.get(id, directory) }.getOrNull() val items = sessions.messages(id, directory) LOG.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(items)}" } runEdt { + if (disposed) return@runEdt + if (sid != id) return@runEdt updateModel { this@SessionController.model.loadHistory(items) if (session != null) this@SessionController.model.setSession(session) @@ -431,21 +457,27 @@ class SessionController( } recoverPending(id) runEdt { - val show = !model.isEmpty() - if (show) showMessages() - if (!show) refreshRecents(force = true) - loaded(show) + if (disposed) return@runEdt + if (sid != id) return@runEdt + showSession() + loaded(!model.isEmpty()) } } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(id)} kind=history dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { - refreshRecents(force = true) + if (disposed) return@edt + if (sid != id) return@edt + updateModel { + model.setState(SessionState.Error(e.message ?: KiloBundle.message("history.error.local"))) + } + showSession() loaded(false) } } finally { edt { - if (historyState != state) return@edt - setHistoryState(HistoryState.Idle) + if (disposed) return@edt + if (sessionLoadState != token) return@edt + setSessionLoadState(SessionLoadState.Idle) } updates.holdFlush(false) updates.requestFlush(true) @@ -453,8 +485,59 @@ class SessionController( } } + private fun importCloud(id: String, token: SessionLoadState.Loading) { + cs.launch { + try { + val session = sessions.importCloudSession(id, directory) + val items = sessions.messages(session.id, directory) + LOG.debug { "${ChatLogSummary.sid(session.id)} ${ChatLogSummary.history(items)}" } + runEdt { + if (disposed) return@runEdt + ref = SessionRef.Local(session) + setRecentSessionsState(RecentsState.Idle) + updateModel { + this@SessionController.model.loadHistory(items) + this@SessionController.model.setSession(session) + } + } + recoverPending(session.id) + runEdt { + if (disposed) return@runEdt + subscribeEvents() + showSession() + loaded(!model.isEmpty()) + } + } catch (e: Exception) { + LOG.warn("${ChatLogSummary.sid(id)} kind=cloud-import dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + edt { + if (disposed) return@edt + updateModel { + model.setState(SessionState.Error(e.message ?: KiloBundle.message("history.error.cloud"))) + } + showSession() + loaded(false) + } + } finally { + edt { + if (disposed) return@edt + if (sessionLoadState != token) return@edt + setSessionLoadState(SessionLoadState.Idle) + } + updates.holdFlush(false) + updates.requestFlush(true) + } + } + } + + private fun startSessionLoading(token: SessionLoadState.Loading) { + assertEdt() + setSessionLoadState(token) + model.setState(SessionState.Loading) + if (!model.showSession) setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress) + } + private fun subscribeEvents() { - val id = sessionId ?: return + val id = sid ?: return LOG.debug { "${ChatLogSummary.sid(id)} kind=subscription subscribe=true" } eventJob?.cancel() eventJob = cs.launch { @@ -489,6 +572,8 @@ class SessionController( "${ChatLogSummary.sid(id)} kind=recovery permissions=${permissions.size} questions=${questions.size} status=${status?.type ?: "none"} branch=$branch" } runEdt { + if (disposed) return@runEdt + if (sid != id) return@runEdt updateModel { if (permissions.isNotEmpty()) { model.setState(SessionState.AwaitingPermission(toPermission(permissions.last()))) @@ -511,7 +596,7 @@ class SessionController( * for "busy" because no more-specific state has arrived yet. */ private fun seedStatus(dto: SessionStatusDto) { - LOG.debug { "${ChatLogSummary.sid(sessionId ?: "pending")} evt=session.status ${ChatLogSummary.status(dto)}" } + LOG.debug { "${ChatLogSummary.sid(log)} evt=session.status ${ChatLogSummary.status(dto)}" } val state = when (dto.type) { "busy" -> SessionState.Busy(KiloBundle.message("session.status.considering")) "retry" -> SessionState.Retry( @@ -533,7 +618,7 @@ class SessionController( when (event) { is ChatEventDto.MessageUpdated -> { val added = model.upsertMessage(event.info) - if (added) showMessages() + if (added) showSession() } is ChatEventDto.PartUpdated -> { @@ -734,12 +819,13 @@ class SessionController( private fun updateModel(block: () -> Unit) { assertEdt() + if (disposed) return val follow = beforeUpdate() block() afterUpdate(follow) } - private fun showMessages() { + private fun showSession() { assertEdt() if (!model.showSession) { setControllerViewState(SessionControllerEvent.ViewChanged.ShowSession) @@ -764,50 +850,65 @@ class SessionController( fun refreshRecents(force: Boolean = false) { assertEdt() - if (model.showSession) return + if (!canUseRecents()) return if (recentsState is RecentsState.Loading) return if (recentsState is RecentsState.Loaded && !force) return val state = RecentsState.Loading() setRecentSessionsState(state) - delayedState.run(state, { recentsState }) { - setControllerViewState(SessionControllerEvent.ViewChanged.ShowProgress) - } cs.launch { try { val items = sessions.recent(directory, RECENT_LIMIT) edt { + if (!canUseRecents()) return@edt if (recentsState != state) return@edt setRecentSessionsState(RecentsState.Loaded) - if (model.showSession) return@edt + if (!canUseRecents()) return@edt setControllerViewState(SessionControllerEvent.ViewChanged.ShowRecents(items)) } } catch (e: Exception) { LOG.warn("kind=session-recent dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { + if (!canUseRecents()) return@edt if (recentsState != state) return@edt setRecentSessionsState(RecentsState.Loaded) - if (model.showSession) return@edt + if (!canUseRecents()) return@edt setControllerViewState(SessionControllerEvent.ViewChanged.ShowRecents(emptyList())) } } } } + private fun canUseRecents(): Boolean { + assertEdt() + if (disposed) return false + if (ref != null) return false + if (sessionLoadState !is SessionLoadState.Idle) return false + return !model.showSession + } + private fun setControllerViewState(event: SessionControllerEvent.ViewChanged) { assertEdt() + if (disposed) return + if (event is SessionControllerEvent.ViewChanged.ShowSession) openLocal() if (viewState == event) return fire(event) { viewState = event if (event is SessionControllerEvent.ViewChanged.ShowSession) { model.showSession = true - setHistoryState(HistoryState.Idle) + setSessionLoadState(SessionLoadState.Idle) setRecentSessionsState(RecentsState.Idle) } } } + private fun openLocal() { + val session = model.session ?: return + open(SessionRef.Local(session)) + } + private fun setConnectionTargetState(event: SessionControllerEvent.ConnectionChanged) { assertEdt() + if (disposed) return connectionTargetState = event val state = event if (event is SessionControllerEvent.ConnectionChanged.Hide || event is SessionControllerEvent.ConnectionChanged.ShowWarning) { @@ -817,11 +918,14 @@ class SessionController( if (connectionState == event) { return } - delayedState.run(event, { if (connectionTargetState == state) state else resolveConnectionState() }, ::setVisibleConnectionState) + connectionDelay.run(event, { if (connectionTargetState == state) state else resolveConnectionState() }) { + if (!disposed) setVisibleConnectionState(it) + } } private fun setVisibleConnectionState(event: SessionControllerEvent.ConnectionChanged) { assertEdt() + if (disposed) return if (connectionState == event) return if (connectionState == null && event is SessionControllerEvent.ConnectionChanged.Hide) { connectionState = event @@ -837,9 +941,9 @@ class SessionController( setConnectionTargetState(resolveConnectionState()) } - private fun setHistoryState(state: HistoryState) { + private fun setSessionLoadState(state: SessionLoadState) { assertEdt() - historyState = state + sessionLoadState = state } private fun setRecentSessionsState(state: RecentsState) { @@ -882,19 +986,41 @@ class SessionController( } private fun fire(event: SessionControllerEvent, before: (() -> Unit)? = null) { - LOG.debug { "session=$sessionId controller: $event" } + LOG.debug { "session=$log controller: $event" } val application = ApplicationManager.getApplication() if (application.isDispatchThread) { + if (disposed) return before?.invoke() for (l in listeners) l.onEvent(event) return } application.invokeLater { + if (disposed) return@invokeLater before?.invoke() for (l in listeners) l.onEvent(event) } } + private fun replay(listener: SessionControllerListener) { + if (disposed) return + val app = ApplicationManager.getApplication() + val block: () -> Unit = { + if (!disposed) { + viewState?.let(listener::onEvent) + connectionState?.let(listener::onEvent) + } + } + if (app.isDispatchThread) { + if (disposed) return + block() + return + } + app.invokeLater { + if (disposed) return@invokeLater + block() + } + } + private fun assertEdt() { check(ApplicationManager.getApplication().isDispatchThread) { "SessionController state must be accessed on EDT" } } @@ -913,7 +1039,8 @@ class SessionController( } override fun dispose() { - delayedState.dispose() + disposed = true + connectionDelay.dispose() eventJob?.cancel() cs.cancel() } @@ -940,6 +1067,7 @@ class SessionController( when (val state = model.state) { is SessionState.Idle -> out.add("[idle]") + is SessionState.Loading -> out.add("[loading]") is SessionState.Busy -> { out.add("[busy]") out.add("[${state.text.toDumpText()}]") @@ -1046,9 +1174,9 @@ private sealed interface RecentsState { data object Loaded : RecentsState } -private sealed interface HistoryState { - data object Idle : HistoryState - data class Loading(val id: Any = Any()) : HistoryState +private sealed interface SessionLoadState { + data object Idle : SessionLoadState + data class Loading(val id: Any = Any()) : SessionLoadState } internal data class ControllerStateSnapshot( @@ -1056,7 +1184,9 @@ internal data class ControllerStateSnapshot( val viewState: SessionControllerEvent.ViewChanged?, val connectionState: SessionControllerEvent.ConnectionChanged?, val connectionTargetState: SessionControllerEvent.ConnectionChanged?, - val historyState: String, + val refKey: String?, + val refType: String?, + val sessionLoadState: String, val recentsState: String, ) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionRefTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionRefTest.kt new file mode 100644 index 00000000000..6b6d1a7079d --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionRefTest.kt @@ -0,0 +1,62 @@ +package ai.kilocode.client.session + +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionTimeDto +import junit.framework.TestCase + +class SessionRefTest : TestCase() { + + fun `test local ref exposes type id and key`() { + val ref = SessionRef.Local("ses_test") + + assertEquals(SessionRef.Type.LOCAL, ref.type) + assertEquals("ses_test", ref.id) + assertEquals("ses_test", ref.key) + assertNull(ref.session) + } + + fun `test metadata local ref keeps id identity`() { + val dto = session("ses_test") + val ref = SessionRef.Local(dto) + + assertEquals(SessionRef.Type.LOCAL, ref.type) + assertEquals("ses_test", ref.id) + assertEquals("ses_test", ref.key) + assertSame(dto, ref.session) + } + + fun `test cloud ref exposes type id and prefixed key`() { + val dto = cloud("cloud_1") + val ref = SessionRef.Cloud(dto) + + assertEquals(SessionRef.Type.CLOUD, ref.type) + assertEquals("cloud_1", ref.id) + assertEquals("cloud:cloud_1", ref.key) + assertSame(dto, ref.session) + } + + fun `test ref parser resolves cloud and local ids`() { + assertEquals(SessionRef.Cloud("cloud_1"), SessionRef.from("cloud:cloud_1")) + assertEquals(SessionRef.Local("ses_test"), SessionRef.from("ses_test")) + assertNull(SessionRef.from(null)) + assertNull(SessionRef.from("")) + } + + private fun session(id: String) = SessionDto( + id = id, + projectID = "prj", + directory = "/test", + title = "Session $id", + version = "1", + time = SessionTimeDto(created = 1.0, updated = 2.0), + ) + + private fun cloud(id: String) = CloudSessionDto( + id = id, + title = "Cloud $id", + createdAt = "2026-01-01T00:00:00Z", + updatedAt = "2026-01-02T00:00:00Z", + version = 1.0, + ) +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 6049a12ee08..1c037300745 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -8,6 +8,7 @@ import ai.kilocode.client.session.history.HistoryPanel import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto @@ -34,7 +35,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { private lateinit var app: KiloAppService private val managers = mutableListOf() private val created = mutableListOf>() - private val loading = mutableListOf() + private val targets = mutableListOf() private val ui = mutableListOf() override fun setUp() { @@ -81,7 +82,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertNotSame(first, second) assertEquals(listOf("/test" to null, "/test" to null), created) - assertEquals(listOf(true, false), loading) } fun `test new session on blank session keeps active component`() { @@ -94,7 +94,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertSame(first, second) assertEquals(listOf("/test" to null), created) - assertEquals(listOf(true), loading) } fun `test default focused component tracks active session`() { @@ -132,7 +131,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertSame(first, second) assertEquals(listOf("/test" to "ses_1", "/test" to null), created) - assertEquals(listOf(false, false), loading) } fun `test prompted blank session is reused from recents`() { @@ -170,7 +168,6 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1", "/repo")) assertEquals(listOf("/repo" to "ses_1"), created) - assertEquals(listOf(false), loading) } fun `test open session seeds session metadata into ui`() { @@ -218,7 +215,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { } fun `test opening local history item shows session ui`() { - lateinit var open: (SessionDto) -> Unit + lateinit var open: (SessionRef) -> Unit val history = JLabel("History") val manager = manager(history = { _, fn, _ -> open = fn @@ -226,12 +223,69 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { }) manager.showHistory() - open(session("ses_1")) + open(SessionRef.Local(session("ses_1"))) assertTrue(active(manager) is SessionUi) assertEquals(listOf("/test" to "ses_1"), created) } + fun `test opening cloud history item shows session ui before import`() { + lateinit var open: (SessionRef) -> Unit + rpc.historyGate = kotlinx.coroutines.CompletableDeferred() + rpc.importedCloudSession = session("ses_imported") + val manager = manager(history = { _, fn, _ -> + open = fn + JLabel("History") + }) + + manager.showHistory() + open(SessionRef.Cloud(cloud("cloud_1"))) + settle() + + assertTrue(active(manager) is SessionUi) + assertEquals(listOf("/test" to "cloud:cloud_1"), created) + assertEquals("cloud:cloud_1", targets.single()?.key) + assertEquals(listOf("cloud_1" to "/test"), rpc.imports) + + rpc.historyGate?.complete(Unit) + settle() + assertSame(active(manager), ui.single()) + assertEquals(listOf("/test" to "cloud:cloud_1"), created) + } + + fun `test opening same cloud session while in-flight reuses existing ui`() { + rpc.historyGate = kotlinx.coroutines.CompletableDeferred() + rpc.importedCloudSession = session("ses_imported") + val manager = manager() + val ref = SessionRef.Cloud(cloud("cloud_1")) + + manager.openSession(ref) + val first = active(manager) + manager.newSession() + manager.openSession(ref) + val second = active(manager) + + assertSame(first, second) + assertEquals(listOf("/test" to "cloud:cloud_1", "/test" to null), created) + + rpc.historyGate!!.complete(Unit) + settle() + } + + fun `test imported cloud session is reused when opened as local`() { + rpc.importedCloudSession = session("ses_imported") + val manager = manager() + + manager.openSession(SessionRef.Cloud(cloud("cloud_1"))) + settle() + val first = active(manager) + manager.openSession(session("ses_imported")) + val second = active(manager) + + assertSame(first, second) + assertEquals(listOf("/test" to "cloud:cloud_1"), created) + } + fun `test new session from history shows blank session`() { val history = JLabel("History") val manager = manager(history = { _, _, _ -> history }) @@ -243,6 +297,26 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertEquals(listOf("/test" to null), created) } + fun `test opening same local session while in-flight reuses existing ui`() { + val gate = kotlinx.coroutines.CompletableDeferred() + rpc.historyGate = gate + val manager = manager() + val session = session("ses_1") + + manager.openSession(session) + val first = active(manager) + // Open the same session again while history is still loading + manager.newSession() + manager.openSession(session) + val second = active(manager) + + assertSame("in-flight session must be reused", first, second) + assertEquals(listOf("/test" to "ses_1", "/test" to null), created) + + gate.complete(Unit) + settle() + } + fun `test deleted cached history session is not reused`() { lateinit var deleted: (String) -> Unit val manager = manager(history = { _, _, fn -> @@ -262,15 +336,15 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { } private fun manager( - history: ((com.intellij.openapi.Disposable, (SessionDto) -> Unit, (String) -> Unit) -> JComponent)? = null, + history: ((com.intellij.openapi.Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, ): SessionSidePanelManager { val manager = SessionSidePanelManager( project = project, root = workspace, - create = { project, workspace, owner, id, show, session -> + create = { project, workspace, owner, id, session, target -> created.add(workspace.directory to id) - loading.add(show) - SessionUi(project, workspace, sessions, app, scope, id = id, loading = show, open = owner::openSession, session = session).also { + targets.add(target) + SessionUi(project, workspace, sessions, app, scope, id = id, open = { item -> owner.openSession(item) }, session = session, target = target).also { ui.add(it) Disposer.register(it) { ui.remove(it) } } @@ -308,4 +382,12 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { time = SessionTimeDto(created = 1.0, updated = 2.0), ) + private fun cloud(id: String) = CloudSessionDto( + id = id, + title = "Cloud $id", + createdAt = "2026-01-01T00:00:00Z", + updatedAt = "2026-01-02T00:00:00Z", + version = 1.0, + ) + } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt index cad1b00915c..19c0478291c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt @@ -48,7 +48,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { } fun `test factory creates blank session ui`() { - val ui = direct().create(project, workspace, FakeManager(), null, true) + val ui = direct().create(project, workspace, FakeManager(), null) assertNotNull(ui) } @@ -56,7 +56,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { fun `test factory wires open callback`() { val manager = FakeManager() val rpc = session("ses_1") - val ui = SessionUi(project, workspace, sessions, app, scope, open = manager::openSession) + val ui = SessionUi(project, workspace, sessions, app, scope, open = { ref -> manager.openSession(ref) }) val controller = controller(ui) com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait { @@ -66,15 +66,16 @@ class SessionUiFactoryTest : BasePlatformTestCase() { assertEquals(listOf("ses_1"), manager.opened) } - fun `test empty panel opens through controller`() { + fun `test empty panel opens through SessionRef via controller`() { val manager = FakeManager() val rpc = session("ses_1") - val ui = SessionUi(project, workspace, sessions, app, scope, open = manager::openSession) + val ui = SessionUi(project, workspace, sessions, app, scope, open = { ref -> manager.openSession(ref) }) val controller = controller(ui) val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, listOf(rpc)) panel.clickRecent(0) + // Recent click routes through SessionRef.Local path assertEquals(listOf("ses_1"), manager.opened) } @@ -104,8 +105,15 @@ class SessionUiFactoryTest : BasePlatformTestCase() { override fun newSession() { } - override fun openSession(session: SessionDto) { - opened.add(session.id) + override fun showHistory() { + } + + override fun openSession(ref: SessionRef) { + val id = when (ref) { + is SessionRef.Local -> ref.id + is SessionRef.Cloud -> ref.key + } + opened.add(id) } } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 547f7c3a881..0d3e6d04bf9 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionMeta import ai.kilocode.client.session.model.Question @@ -8,6 +9,7 @@ import ai.kilocode.client.session.model.QuestionOption import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.ui.ConnectionPanel import ai.kilocode.client.session.ui.EmptySessionPanel +import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.PermissionPanel import ai.kilocode.client.session.ui.prompt.PromptPanel import ai.kilocode.client.session.ui.QuestionPanel @@ -132,23 +134,35 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertSame(find(ui), scrollView()) } - fun `test new session starts with loading body`() { + fun `test new session starts neutral before controller view state`() { ui = newUi(displayMs = 1_000) assertFalse(scrollView() is EmptySessionPanel) + assertFalse(scrollView() is LoadingPanel) } fun `test action-created new session starts blank`() { - ui = newUi(displayMs = 1_000, loading = false) + ui = newUi(displayMs = 1_000) assertFalse(scrollView() is EmptySessionPanel) assertFalse(scrollView() is SessionMessageListPanel) + assertFalse(scrollView() is LoadingPanel) } - fun `test clicking recent session calls opener`() { + fun `test existing session id shows loading body immediately`() { + rpc.historyGate = kotlinx.coroutines.CompletableDeferred() + + ui = newUi(id = "ses_test", displayMs = 1_000) + + assertSame(find(ui), scrollView()) + assertEquals(SessionState.Loading, controller().model.state) + rpc.historyGate?.complete(Unit) + } + + fun `test clicking recent session calls opener via SessionRef`() { val opened = mutableListOf() rpc.recent.add(session("ses_1")) - ui = newUi(open = { opened.add(it.id) }) + ui = newUi(open = { ref -> if (ref is SessionRef.Local) opened.add(ref.id) }) settle() layout() @@ -166,6 +180,54 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertSame(find(ui), scrollView()) } + fun `test empty explicit session id shows message body`() { + rpc.recent.add(session("ses_recent")) + + ui = newUi(id = "ses_test") + settle() + + assertSame(find(ui), scrollView()) + assertNull(find(ui, EmptySessionPanel::class.java)) + assertTrue(rpc.recentCalls.isEmpty()) + } + + fun `test explicit session id loading does not show recents`() { + rpc.historyGate = kotlinx.coroutines.CompletableDeferred() + rpc.recent.add(session("ses_recent")) + + ui = newUi(id = "ses_test", displayMs = 50) + settleShort(100) + + assertSame(find(ui), scrollView()) + assertNull(find(ui, EmptySessionPanel::class.java)) + assertTrue(rpc.recentCalls.isEmpty()) + + rpc.historyGate!!.complete(Unit) + settle() + + assertSame(find(ui), scrollView()) + assertTrue(rpc.recentCalls.isEmpty()) + } + + fun `test explicit cloud session loading does not show recents`() { + rpc.importedCloudSession = session("ses_imported") + rpc.historyGate = kotlinx.coroutines.CompletableDeferred() + rpc.recent.add(session("ses_recent")) + + ui = newUi(id = "cloud:cloud_1", displayMs = 50) + settleShort(100) + + assertSame(find(ui), scrollView()) + assertNull(find(ui, EmptySessionPanel::class.java)) + assertTrue(rpc.recentCalls.isEmpty()) + + rpc.historyGate!!.complete(Unit) + settle() + + assertSame(find(ui), scrollView()) + assertTrue(rpc.recentCalls.isEmpty()) + } + fun `test existing session history shows header above scroll pane`() { rpc.history.add(MessageWithPartsDto(message("msg1"), emptyList())) @@ -181,25 +243,32 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertTrue(header.y + header.height <= scroll.y) } - fun `test new session keeps loading body before recents delay`() { + fun `test new session shows blank body while recents are loading`() { rpc.recentGate = kotlinx.coroutines.CompletableDeferred() ui = newUi(displayMs = 1_000) settleShort(100) + // A new session (no id) shows blank body while recents are pending, not loading body assertFalse(scrollView() is EmptySessionPanel) + assertFalse(scrollView() is LoadingPanel) + rpc.recentGate!!.complete(Unit) } - fun `test slow recents switch to loading body only after progress event`() { + fun `test slow recents never show loading body and show recents when complete`() { rpc.recentGate = kotlinx.coroutines.CompletableDeferred() rpc.recent.add(session("ses_1")) ui = newUi(displayMs = 50) settleShort(20) + // No loading body — recents do not trigger progress indicator assertFalse(scrollView() is EmptySessionPanel) + assertFalse(scrollView() is LoadingPanel) settleShort(80) + // Still no loading body even after the delay interval passes assertFalse(scrollView() is EmptySessionPanel) + assertFalse(scrollView() is LoadingPanel) rpc.recentGate!!.complete(Unit) settle() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index b86f9dc69d6..b9dd7452712 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.rpc.dto.CloudSessionDto @@ -194,7 +195,7 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(listOf("ses_2"), opened) } - fun `test panel imports and opens selected cloud session on enter from search`() { + fun `test panel opens selected cloud session immediately on enter from search`() { rpc.cloud += cloud("cloud_1", "Cloud") rpc.importedCloudSession = session("ses_imported", "Imported") val opened = mutableListOf() @@ -208,8 +209,8 @@ class HistoryControllerTest : BasePlatformTestCase() { key(search, KeyEvent.VK_ENTER) flush() - assertEquals(listOf("cloud_1" to "/test"), rpc.imports) - assertEquals(listOf("ses_imported"), opened) + assertTrue(rpc.imports.isEmpty()) + assertEquals(listOf("cloud:cloud_1"), opened) } fun `test panel refresh reloads local history`() { @@ -274,8 +275,12 @@ class HistoryControllerTest : BasePlatformTestCase() { private fun controller() = HistoryController(sessions, workspace, scope) - private fun controller(opened: MutableList) = HistoryController(sessions, workspace, scope, open = { item -> - opened.add(item.id) + private fun controller(opened: MutableList) = HistoryController(sessions, workspace, scope, open = { open -> + val id = when (open) { + is SessionRef.Local -> open.id + is SessionRef.Cloud -> "cloud:${open.id}" + } + opened.add(id) }) private fun collect(controller: HistoryController): MutableList { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 8cd526a4010..ea90be3a397 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi @@ -41,7 +42,7 @@ class EmptySessionPanelTest : BasePlatformTestCase() { workspace = workspaces.workspace("/test") controller = SessionController( parent = testRootDisposable, - id = null, + ref = null, sessions = KiloSessionService(project, scope, FakeSessionRpcApi()), workspace = workspace, app = app, diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt index b44519ce852..39bec08835d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt @@ -6,6 +6,7 @@ import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionMeta +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi @@ -53,7 +54,7 @@ class PermissionPanelTest : BasePlatformTestCase() { app = KiloAppService(scope, api) workspaces = KiloWorkspaceService(scope, work) workspace = workspaces.workspace("/test") - controller = SessionController(parent, "ses_test", sessions, workspace, app, scope, BorderLayoutPanel()) + controller = SessionController(parent, SessionRef.Local("ses_test"), sessions, workspace, app, scope, BorderLayoutPanel()) panel = PermissionPanel(controller) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt index e861ecca773..8a90882732e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.model.QuestionItem @@ -55,7 +56,7 @@ class QuestionPanelTest : BasePlatformTestCase() { workspaces = KiloWorkspaceService(scope, workspaceRpc) workspace = workspaces.workspace("/test") val root = BorderLayoutPanel() - controller = SessionController(parent, "ses_test", sessions, workspace, app, scope, root) + controller = SessionController(parent, SessionRef.Local("ses_test"), sessions, workspace, app, scope, root) panel = QuestionPanel(controller) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt new file mode 100644 index 00000000000..c3225a809dc --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt @@ -0,0 +1,128 @@ +package ai.kilocode.client.session.update + +import ai.kilocode.client.session.model.SessionModelEvent +import ai.kilocode.client.session.model.SessionState +import ai.kilocode.rpc.dto.MessageWithPartsDto +import com.intellij.openapi.util.Disposer +import kotlinx.coroutines.CompletableDeferred + +/** + * Tests that disposed [SessionController] ignores all late async completions. + */ +class DisposedStateTest : SessionControllerTestBase() { + + fun `test dispose during local session loading emits no late model events`() { + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test") + val modelEvents = collectModelEvents(m) + flush() + modelEvents.clear() + + Disposer.dispose(m) + gate.complete(Unit) + flush() + + assertTrue("expected no model events after dispose, got: $modelEvents", modelEvents.isEmpty()) + } + + fun `test dispose during local session loading emits no controller events`() { + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test") + val events = collect(m) + flush() + events.clear() + + Disposer.dispose(m) + gate.complete(Unit) + flush() + + assertTrue("expected no controller events after dispose, got: $events", events.isEmpty()) + } + + fun `test dispose during cloud import emits no model events`() { + val gate = CompletableDeferred() + rpc.historyGate = gate + rpc.importedCloudSession = session("ses_imported") + val m = controller("cloud:cloud_1") + val modelEvents = collectModelEvents(m) + flush() + modelEvents.clear() + + Disposer.dispose(m) + gate.complete(Unit) + flush() + + assertTrue("expected no model events after cloud dispose, got: $modelEvents", modelEvents.isEmpty()) + } + + fun `test dispose during recents loading emits no recents event`() { + projectRpc.state.value = workspaceReady() + val gate = CompletableDeferred() + rpc.recentGate = gate + val m = controller() + val events = collect(m) + flush() + events.clear() + + Disposer.dispose(m) + gate.complete(Unit) + flush() + + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) + } + + fun `test disposed controller addListener is no-op`() { + val m = controller("ses_test") + flush() + Disposer.dispose(m) + + val extra = mutableListOf() + val disposable = Disposer.newDisposable("extra") + Disposer.register(parent, disposable) + m.addListener(disposable) { extra.add(it) } + + assertTrue(extra.isEmpty()) + } + + fun `test dispose marks controller disposed and blocks model updates`() { + val m = controller("ses_test") + flush() + val modelEvents = collectModelEvents(m) + + Disposer.dispose(m) + edt { + // Simulate a late model mutation attempt on EDT after dispose + // updateModel is no-op when disposed + m.flushEvents() + } + + assertTrue(modelEvents.isEmpty()) + } + + fun `test session load state is idle after normal load completes`() { + rpc.history.add(MessageWithPartsDto(msg("msg1", "ses_test", "user"), emptyList())) + val m = controller("ses_test") + flush() + + var snap: ControllerStateSnapshot? = null + edt { snap = m.snapshotState() } + assertEquals("Idle", snap!!.sessionLoadState) + } + + fun `test session load state is loading before history arrives`() { + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test") + + edt { + assertEquals(SessionState.Loading, m.model.state) + val snap = m.snapshotState() + assertTrue("expected Loading in: ${snap.sessionLoadState}", snap.sessionLoadState.startsWith("Loading")) + } + + gate.complete(Unit) + flush() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt index 6f0cf34d24f..4098f5ed8f7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt @@ -32,9 +32,12 @@ class HistoryLoadingTest : SessionControllerTestBase() { val events = collect(c) flush() + // ViewChanged progress fires immediately on controller construction (step 3 of plan). + // ViewChanged session fires after non-empty history is loaded. assertControllerEvents(""" AppChanged WorkspaceChanged + ViewChanged progress ViewChanged session """, events) @@ -47,4 +50,28 @@ class HistoryLoadingTest : SessionControllerTestBase() { c, ) } + + fun `test empty explicit session history shows messages view`() { + rpc.recent.add(session("ses_recent")) + + val c = controller("ses_test") + val events = collect(c) + val modelEvents = collectModelEvents(c) + flush() + + assertTrue(rpc.recentCalls.isEmpty()) + assertModelEvents("HistoryLoaded", modelEvents) + assertControllerEvents(""" + AppChanged + WorkspaceChanged + ViewChanged progress + ViewChanged session + """, events) + assertSession( + """ + [app: DISCONNECTED] [workspace: PENDING] + """, + c, + ) + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt index 13b30a8355b..d56af31d373 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt @@ -10,6 +10,7 @@ import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.session.SessionRef import ai.kilocode.rpc.dto.AgentDto import ai.kilocode.rpc.dto.AgentsDto import ai.kilocode.rpc.dto.ChatEventDto @@ -131,6 +132,14 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { return controller(id, flushMs, true, displayMs = displayMs) } + protected fun controller( + ref: SessionRef, + flushMs: Long = Long.MAX_VALUE, + displayMs: Long = Long.MAX_VALUE, + ): SessionController { + return controller(ref = ref, flushMs = flushMs, condense = true, displayMs = displayMs) + } + protected fun controller( id: String? = null, flushMs: Long, @@ -139,20 +148,20 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { session: SessionDto? = null, beforeUpdate: () -> Boolean = { false }, afterUpdate: (Boolean) -> Unit = {}, + ref: SessionRef? = SessionRef.resolve(id, session), ): SessionController { val root = Root() val m = SessionController( - parent, - id, - sessions, - workspace, - app, - scope, - root, - flushMs, - condense, - displayMs, - session = session, + parent, + ref, + sessions, + workspace, + app, + scope, + root, + flushMs, + condense, + displayMs, beforeUpdate = beforeUpdate, afterUpdate = afterUpdate, ) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt index 1f5de63b397..a7b75edb514 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt @@ -1,5 +1,7 @@ package ai.kilocode.client.session.update +import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.session.model.SessionState import kotlinx.coroutines.CompletableDeferred class ViewSwitchingTest : SessionControllerTestBase() { @@ -84,21 +86,84 @@ class ViewSwitchingTest : SessionControllerTestBase() { """, events) } - fun `test empty history transitions to recents`() { + fun `test empty explicit session history shows session view`() { rpc.recent.add(session("ses_1")) val m = controller("ses_test", displayMs = 1_000) val events = collect(m) flush() + assertTrue(rpc.recentCalls.isEmpty()) assertControllerEvents(""" AppChanged WorkspaceChanged - ViewChanged recents=1 + ViewChanged progress + ViewChanged session """, events) + assertTrue(m.model.showSession) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) } - fun `test slow recents show progress after delay then recents`() { + fun `test workspace ready does not load recents during explicit local load`() { + projectRpc.state.value = workspaceReady() + rpc.recent.add(session("ses_1")) + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test", displayMs = 50) + val events = collect(m) + + pause(80) + + assertTrue(rpc.recentCalls.isEmpty()) + assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) + + gate.complete(Unit) + flush() + + assertTrue(rpc.recentCalls.isEmpty()) + assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowSession }) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) + } + + fun `test workspace ready does not load recents during cloud import`() { + projectRpc.state.value = workspaceReady() + rpc.recent.add(session("ses_1")) + rpc.importedCloudSession = session("ses_imported") + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("cloud:cloud_1", displayMs = 50) + val events = collect(m) + + pause(80) + + assertTrue(rpc.recentCalls.isEmpty()) + assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) + + gate.complete(Unit) + flush() + + assertTrue(rpc.recentCalls.isEmpty()) + assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowSession }) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) + } + + fun `test existing session load shows progress immediately`() { + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test", displayMs = 1_000) + val events = collect(m) + + assertEquals(SessionState.Loading, m.model.state) + pause(20) + + assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) + gate.complete(Unit) + flush() + } + + fun `test slow recents do not show progress then emit recents when complete`() { projectRpc.state.value = workspaceReady() rpc.recent.add(session("ses_1")) val gate = CompletableDeferred() @@ -111,21 +176,20 @@ class ViewSwitchingTest : SessionControllerTestBase() { assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) pause(80) - assertTrue(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) + // Slow recents must NOT show progress even after the delay interval + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) gate.complete(Unit) flush() - assertEquals( - """ - ViewChanged progress - ViewChanged recents=1 - """.trimIndent().trim(), - events.filterIsInstance().joinToString("\n"), - ) + // After completing, recents must fire directly (no prior progress event) + assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowProgress }) + assertEquals(1, events.count { it is SessionControllerEvent.ViewChanged.ShowRecents }) + val recentsView = events.filterIsInstance() + assertEquals("ViewChanged recents=1", recentsView.last().toString()) } - fun `test recents progress event sees loading state on EDT`() { + fun `test recents loaded state visible on EDT when recents event fires`() { projectRpc.state.value = workspaceReady() rpc.recent.add(session("ses_1")) val gate = CompletableDeferred() @@ -133,14 +197,12 @@ class ViewSwitchingTest : SessionControllerTestBase() { val m = controller(displayMs = 50) val states = collectStates(m) - pause(80) - - val state = states.single { it.first is SessionControllerEvent.ViewChanged.ShowProgress }.second - assertEquals(SessionControllerEvent.ViewChanged.ShowProgress, state.viewState) - assertTrue(state.recentsState.startsWith("Loading")) - gate.complete(Unit) flush() + + assertFalse(states.any { it.first is SessionControllerEvent.ViewChanged.ShowProgress }) + val state = states.single { it.first is SessionControllerEvent.ViewChanged.ShowRecents }.second + assertEquals("Loaded", state.recentsState) } fun `test fast recents suppress progress`() { @@ -201,6 +263,43 @@ class ViewSwitchingTest : SessionControllerTestBase() { assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) } + fun `test id-only local ref starts with local identity`() { + val m = controller(SessionRef.Local("ses_test")) + + flush() + + val snap = m.snapshotState() + assertEquals("ses_test", snap.refKey) + assertEquals("LOCAL", snap.refType) + } + + fun `test cloud ref becomes local after import`() { + rpc.importedCloudSession = session("ses_imported") + val m = controller(SessionRef.Cloud("cloud_1")) + + val start = m.snapshotState() + assertEquals("cloud:cloud_1", start.refKey) + assertEquals("CLOUD", start.refType) + + flush() + + val end = m.snapshotState() + assertEquals("ses_imported", end.refKey) + assertEquals("LOCAL", end.refType) + } + + fun `test prompt updates blank controller to local ref`() { + val m = controller() + + flush() + edt { m.prompt("hello") } + flush() + + val snap = m.snapshotState() + assertEquals("ses_test", snap.refKey) + assertEquals("LOCAL", snap.refType) + } + private fun session(id: String) = ai.kilocode.rpc.dto.SessionDto( id = id, projectID = "prj", From 3d6586c883f17e0ea72cc4f99fb407e6d4e60c4e Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 21:52:31 -0400 Subject: [PATCH 016/111] fix(jetbrains): session loading state, history panel loading indicator, and model dedup - Add SessionState.Loading and handle it in ProgressPanel and isBusy() - Deduplicate setState calls in SessionModel (no-op when state unchanged) - HistoryController now opens cloud items as SessionRef.Cloud instead of importing inline - HistoryModel tracks loaded flag; HistoryPanel shows a loading card before first load - SessionManager interface takes SessionRef instead of raw SessionDto - build.gradle.kts: set splitModeServerPort for runIdeBackend task - Test: NewSessionActionTest, SessionUiTestBase, SessionModelTest updated accordingly --- .changeset/jetbrains-session-loading-state.md | 5 ++++ packages/kilo-jetbrains/build.gradle.kts | 6 +++++ .../kilocode/client/session/SessionManager.kt | 6 ++++- .../session/history/HistoryController.kt | 27 +++---------------- .../client/session/history/HistoryModel.kt | 7 ++++- .../client/session/history/HistoryPanel.kt | 24 ++++++++++++++++- .../client/session/model/SessionModel.kt | 1 + .../client/session/model/SessionState.kt | 4 ++- .../client/session/ui/EmptySessionPanel.kt | 5 ++-- .../client/session/ui/ProgressPanel.kt | 3 ++- .../client/actions/NewSessionActionTest.kt | 7 +++-- .../client/session/SessionUiTestBase.kt | 6 ++--- .../client/session/model/SessionModelTest.kt | 11 ++++++++ 13 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 .changeset/jetbrains-session-loading-state.md diff --git a/.changeset/jetbrains-session-loading-state.md b/.changeset/jetbrains-session-loading-state.md new file mode 100644 index 00000000000..bc930ebcbca --- /dev/null +++ b/.changeset/jetbrains-session-loading-state.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions. diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index 3ecc8be04b6..00b4bd5ea2b 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -66,6 +66,12 @@ intellijPlatform { } } +tasks { + runIdeBackend { + splitModeServerPort.set(12345) + } +} + tasks.named("runIde") { dependsOn(":backend:processResources") jvmArgumentProviders += CommandLineArgumentProvider { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt index 162fd3c6b2a..62d8ec94074 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionManager.kt @@ -12,5 +12,9 @@ interface SessionManager { fun showHistory() - fun openSession(session: SessionDto) + fun openSession(ref: SessionRef) + + fun openSession(session: SessionDto) { + openSession(SessionRef.Local(session)) + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 2b0406172f0..08d1865c8b7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.session.history import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionRef import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.application.ApplicationManager @@ -13,7 +14,7 @@ class HistoryController( private val sessions: KiloSessionService, private val workspace: Workspace, private val cs: CoroutineScope, - open: (LocalHistoryItem) -> Unit = {}, + open: (SessionRef) -> Unit = {}, private val deleted: (String) -> Unit = {}, ) { companion object { @@ -24,7 +25,6 @@ class HistoryController( val cloud = CloudHistoryModel() private val deleting = mutableSetOf() - private val importing = mutableSetOf() private val opener = open private var git: String? = null @@ -82,30 +82,11 @@ class HistoryController( fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting fun open(item: LocalHistoryItem) { - edt { opener(item) } + edt { opener(SessionRef.Local(item.session)) } } fun open(item: CloudHistoryItem) { - edt { - if (item.id in importing) return@edt - importing.add(item.id) - cloud.refresh() - cs.launch { - try { - val session = sessions.importCloudSession(item.id, workspace.directory) - edt { - importing.remove(item.id) - cloud.refresh() - opener(LocalHistoryItem(session)) - } - } catch (e: Exception) { - edt { - importing.remove(item.id) - cloud.fail(e.message ?: KiloBundle.message("history.error.cloud")) - } - } - } - } + edt { opener(SessionRef.Cloud(item.session)) } } private fun loadCloud(reset: Boolean) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt index 44543d2d37b..f6aa89d3be3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -11,6 +11,8 @@ open class HistoryModel : AbstractListModel() { private set var error: String? = null private set + var loaded = false + private set val items: List get() = all val visibleItems: List get() = rows @@ -28,6 +30,7 @@ open class HistoryModel : AbstractListModel() { fun replace(items: List) { all = HistoryTime.sorted(items) loading = false + loaded = true error = null filter() } @@ -35,6 +38,7 @@ open class HistoryModel : AbstractListModel() { fun append(items: List) { all = HistoryTime.sorted(all + items) loading = false + loaded = true error = null filter() } @@ -46,6 +50,7 @@ open class HistoryModel : AbstractListModel() { fun fail(message: String) { loading = false + loaded = true error = message refresh() } @@ -90,7 +95,7 @@ class CloudHistoryModel : HistoryModel() { fun start(reset: Boolean) { cursor = cursor.takeUnless { reset } start() - if (reset) clear() + if (reset && !loaded) clear() } fun replace(items: List, next: String?) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index ba03abed22f..6cfe54fffc5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -1,6 +1,8 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.ui.LoadingPanel +import ai.kilocode.client.session.ui.SessionStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ide.ui.LafManagerListener import com.intellij.openapi.application.ApplicationManager @@ -22,6 +24,7 @@ import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout +import java.awt.CardLayout import java.awt.event.HierarchyEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter @@ -48,6 +51,9 @@ class HistoryPanel( private val more = JButton(KiloBundle.message("history.cloud.load.more")) private val localPanel = panel(localSearch, localList) private val cloudPanel = panel(cloudSearch, cloudList, more) + private val cards = CardLayout() + private val body = BorderLayoutPanel().apply { layout = cards } + private val load = LoadingPanel() private val localInfo = TabInfo(localPanel).setText(KiloBundle.message("history.tab.local")) private val cloudInfo = TabInfo(cloudPanel).setText(KiloBundle.message("history.tab.cloud")) private var stale = false @@ -79,7 +85,9 @@ class HistoryPanel( } if (!isShowing) stale = true } - add(tabs.component, BorderLayout.CENTER) + body.add(load, CARD_LOAD) + body.add(tabs.component, CARD_TABS) + add(body, BorderLayout.CENTER) sync() refresh() } @@ -107,6 +115,7 @@ class HistoryPanel( SwingUtilities.updateComponentTreeUI(this) SwingUtilities.updateComponentTreeUI(localPanel) SwingUtilities.updateComponentTreeUI(cloudPanel) + load.applyStyle(SessionStyle.current()) updateRenderer(localList) updateRenderer(cloudList) sync() @@ -219,10 +228,16 @@ class HistoryPanel( syncList(cloudList, controller.cloud) more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading more.isVisible = controller.cloud.cursor != null || controller.cloud.loading + cards.show(body, if (loading()) CARD_LOAD else CARD_TABS) revalidate() repaint() } + private fun loading(): Boolean { + if (controller.local.loaded || controller.cloud.loaded) return false + return controller.local.loading || controller.cloud.loading + } + private fun syncList(list: JBList, model: HistoryModel) { list.setPaintBusy(model.loading) list.emptyText.text = when { @@ -336,4 +351,11 @@ class HistoryPanel( override fun dispose() { // no-op } + + internal fun showingLoading() = !controller.local.loaded && !controller.cloud.loaded && (controller.local.loading || controller.cloud.loading) + + private companion object { + const val CARD_LOAD = "load" + const val CARD_TABS = "tabs" + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 798d2bf3c6f..230cda662fe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -175,6 +175,7 @@ class SessionModel { } fun setState(state: SessionState) { + if (this.state == state) return this.state = state fire(SessionModelEvent.StateChanged(state)) updateHeader() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt index 7d0f5f1b67b..156411aaf88 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt @@ -4,6 +4,8 @@ package ai.kilocode.client.session.model sealed class SessionState { data object Idle : SessionState() + data object Loading : SessionState() + data class Busy(val text: String) : SessionState() data class AwaitingQuestion(val question: Question) : SessionState() @@ -17,7 +19,7 @@ sealed class SessionState { data class Error(val message: String, val kind: String? = null) : SessionState() fun isBusy(): Boolean = when (this) { - is Idle, is Error -> false + is Idle, is Loading, is Error -> false else -> true } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 1caf26b538d..6f693c36e44 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView @@ -67,7 +68,7 @@ class EmptySessionPanel( val index = row(e) if (index < 0) return selectedIndex = index - controller.openSession(model.getElementAt(index)) + controller.openSession(SessionRef.Local(model.getElementAt(index))) } override fun mouseExited(e: MouseEvent) { @@ -153,7 +154,7 @@ class EmptySessionPanel( internal fun clickRecent(index: Int) { list.selectedIndex = index - controller.openSession(model.getElementAt(index)) + controller.openSession(SessionRef.Local(model.getElementAt(index))) } internal fun recentVisible() = true diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt index 2712a4e2952..d7c3357d1cb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt @@ -15,7 +15,7 @@ import java.awt.FlowLayout * * Reacts to [SessionModelEvent.StateChanged]: * - [SessionState.Busy] → shows an animated spinner and [SessionState.Busy.text] - * - Any other state → hidden + * - Any other state -> hidden * * Owned by [SessionMessageListPanel], which always re-anchors it as the last child so it * appears below all turn views inside the scroll pane. @@ -52,6 +52,7 @@ class ProgressPanel( label.text = state.text isVisible = true } + is SessionState.Loading -> isVisible = false else -> isVisible = false } revalidate() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt index d9da97616d8..3de571b0d8f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.session.SessionManager -import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.client.session.SessionRef import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.Presentation @@ -49,7 +49,10 @@ class NewSessionActionTest : BasePlatformTestCase() { created++ } - override fun openSession(session: SessionDto) { + override fun showHistory() { + } + + override fun openSession(ref: SessionRef) { } } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index 96ba3937824..96b8b3b5d6d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -10,6 +10,7 @@ import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.client.session.SessionRef import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto @@ -76,10 +77,9 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { protected fun newUi( id: String? = null, displayMs: Long = 0, - loading: Boolean = id == null, - open: (SessionDto) -> Unit = {}, + open: (SessionRef) -> Unit = {}, ): SessionUi { - return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, loading = loading, open = open).apply { + return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, open = open).apply { setSize(800, 600) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt index f468ff973af..38373fe9a80 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt @@ -328,6 +328,17 @@ class SessionModelTest : UsefulTestCase() { assertEquals(state, (events.single() as SessionModelEvent.StateChanged).state) } + fun `test setState duplicate is no-op`() { + val state = SessionState.Busy("thinking") + model.setState(state) + events.clear() + + model.setState(state) + + assertTrue(events.isEmpty()) + assertEquals(state, model.state) + } + fun `test setState to Error stores error data`() { model.setState(SessionState.Error("something broke", "timeout")) From c7f9add7d9a3c67a8cbc6c16bb5d5ce01f88beab Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 22:06:36 -0400 Subject: [PATCH 017/111] refactor(jetbrains): remove local/key/log helpers from SessionController, keep only ref and sid --- .../session/update/SessionController.kt | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt index b96564a352d..2d2369f46ac 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt @@ -90,10 +90,7 @@ class SessionController( private val listeners = mutableListOf() private var ref: SessionRef? = ref - private val local: SessionRef.Local? get() = ref as? SessionRef.Local - private val sid: String? get() = local?.id - private val key: String? get() = ref?.key - private val log: String get() = sid ?: key ?: "pending" + private val sid: String? get() = (ref as? SessionRef.Local)?.id private val directory: String get() = workspace.directory private val updates = SessionUpdateQueue( parent, @@ -102,7 +99,7 @@ class SessionController( ::handle, condense, ref != null - ) { log } + ) { sid ?: ref?.key ?: "pending" } private var disposed = false private var partType: String? = null @@ -118,7 +115,7 @@ class SessionController( val ready: Boolean get() = model.isReady() internal val blank: Boolean get() = ref == null && model.isEmpty() && !model.showSession internal val id: String? get() = sid - internal val refKey: String? get() = key + internal val refKey: String? get() = ref?.key internal val refType: SessionRef.Type? get() = ref?.type fun openSession(session: SessionDto) { @@ -145,7 +142,7 @@ class SessionController( viewState = viewState, connectionState = connectionState, connectionTargetState = connectionTargetState, - refKey = key, + refKey = ref?.key, refType = ref?.type?.name, sessionLoadState = sessionLoadState.toString(), recentsState = recentsState.toString(), @@ -160,7 +157,7 @@ class SessionController( fun prompt(text: String) { assertEdt() - val start = log + val start = sid ?: ref?.key ?: "pending" val dto = promptDto(text) LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } showSession() @@ -185,7 +182,7 @@ class SessionController( sessions.prompt(id, directory, dto) LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(log.takeUnless { it == "pending" } ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { if (disposed) return@edt val msg = e.message ?: KiloBundle.message("session.error.prompt") @@ -199,7 +196,7 @@ class SessionController( fun abort() { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=abort" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=abort" } val id = sid ?: return cs.launch { try { @@ -237,7 +234,7 @@ class SessionController( fun retryConnection() { assertEdt() LOG.debug { - "${ChatLogSummary.sid(log)} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}" + "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}" } setConnectionTargetState(SessionControllerEvent.ConnectionChanged.ShowConnecting) setVisibleConnectionState(SessionControllerEvent.ConnectionChanged.ShowConnecting) @@ -258,12 +255,12 @@ class SessionController( fun selectAgent(name: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=config agent=$name" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config agent=$name" } cs.launch { try { sessions.updateConfig(directory, ConfigUpdateDto(agent = name)) } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(log)} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config agent=$name dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } fire(SessionControllerEvent.WorkspaceReady) { @@ -274,7 +271,7 @@ class SessionController( fun selectModel(provider: String, id: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=config model=$provider/$id" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config model=$provider/$id" } val agent = model.agent ?: return val key = "$provider/$id" if (item(key) == null && model.workspace.providers != null) return @@ -286,7 +283,7 @@ class SessionController( fun clearModelOverride() { assertEdt() val agent = model.agent ?: return - LOG.debug { "${ChatLogSummary.sid(log)} kind=config model-reset agent=$agent" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config model-reset agent=$agent" } app.clearModel(agent) val auto = configModel(agent) ?: providerModel(agent) selectResolvedModel(auto) @@ -297,7 +294,7 @@ class SessionController( assertEdt() val key = model.model ?: return if (value !in model.variants) return - LOG.debug { "${ChatLogSummary.sid(log)} kind=config variant=$key/$value" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config variant=$key/$value" } app.selectVariant(key, value) model.variant = value } @@ -306,46 +303,46 @@ class SessionController( fun replyPermission(requestId: String, reply: PermissionReplyDto, rules: PermissionAlwaysRulesDto? = null) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=permission rid=$requestId reply=${reply.reply}" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" } cs.launch { try { if (rules != null) sessions.savePermissionRules(requestId, directory, rules) sessions.replyPermission(requestId, directory, reply) - LOG.debug { "${ChatLogSummary.sid(log)} kind=permission rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(log)} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } fun replyQuestion(requestId: String, answers: QuestionReplyDto) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId answers=${answers.answers.size}" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size}" } cs.launch { try { sessions.replyQuestion(requestId, directory, answers) - LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(log)} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } fun rejectQuestion(requestId: String) { assertEdt() - LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId rejected=true" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true" } cs.launch { try { sessions.rejectQuestion(requestId, directory) - LOG.debug { "${ChatLogSummary.sid(log)} kind=question rid=$requestId ok=true" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" } } catch (e: Exception) { - LOG.warn("${ChatLogSummary.sid(log)} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } } init { - local?.session?.let { model.setSession(it) } + (ref as? SessionRef.Local)?.session?.let { model.setSession(it) } when (val item = ref) { is SessionRef.Cloud -> { val token = SessionLoadState.Loading() @@ -362,7 +359,7 @@ class SessionController( } model.addListener(this) { event -> - LOG.debug { "session=$log model: $event" } + LOG.debug { "session=${sid ?: ref?.key ?: "pending"} model: $event" } } app.connect() @@ -440,7 +437,7 @@ class SessionController( } private fun loadSession(token: SessionLoadState.Loading) { - val target = local ?: return + val target = ref as? SessionRef.Local ?: return val id = target.id cs.launch { try { @@ -596,7 +593,7 @@ class SessionController( * for "busy" because no more-specific state has arrived yet. */ private fun seedStatus(dto: SessionStatusDto) { - LOG.debug { "${ChatLogSummary.sid(log)} evt=session.status ${ChatLogSummary.status(dto)}" } + LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} evt=session.status ${ChatLogSummary.status(dto)}" } val state = when (dto.type) { "busy" -> SessionState.Busy(KiloBundle.message("session.status.considering")) "retry" -> SessionState.Retry( @@ -986,7 +983,7 @@ class SessionController( } private fun fire(event: SessionControllerEvent, before: (() -> Unit)? = null) { - LOG.debug { "session=$log controller: $event" } + LOG.debug { "session=${sid ?: ref?.key ?: "pending"} controller: $event" } val application = ApplicationManager.getApplication() if (application.isDispatchThread) { if (disposed) return From bb5b63b2369cde99031bf5adf3f226174b622c68 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 22:16:42 -0400 Subject: [PATCH 018/111] refactor(jetbrains): collapse SessionUi id/session/target into single ref: SessionRef? --- .../ai/kilocode/client/session/SessionRef.kt | 6 ------ .../client/session/SessionSidePanelManager.kt | 20 ++++++------------- .../ai/kilocode/client/session/SessionUi.kt | 8 ++------ .../client/session/SessionUiFactory.kt | 11 +++------- .../session/SessionSidePanelManagerTest.kt | 15 +++++++++----- .../client/session/SessionUiLayoutTest.kt | 2 +- .../client/session/SessionUiTestBase.kt | 2 +- .../update/SessionControllerTestBase.kt | 2 +- 8 files changed, 24 insertions(+), 42 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt index 323bdcee5f2..73bb72c2943 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionRef.kt @@ -36,11 +36,5 @@ sealed interface SessionRef { fun cloud(id: String?): Cloud? { return from(id) as? Cloud } - - fun resolve(id: String? = null, session: SessionDto? = null, target: SessionRef? = null): SessionRef? { - if (target != null) return target - if (session != null) return Local(session) - return from(id) - } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 64e7ffd4e04..410e698ac87 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -5,7 +5,6 @@ import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.history.HistoryController import ai.kilocode.client.session.history.HistoryPanel -import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.application.ApplicationManager @@ -22,8 +21,8 @@ import javax.swing.JPanel class SessionSidePanelManager( private val project: Project, private val root: Workspace, - private val create: (Project, Workspace, SessionManager, String?, SessionDto?, SessionRef?) -> SessionUi = { project, workspace, manager, id, session, target -> - service().create(project, workspace, manager, id, session, target) + private val create: (Project, Workspace, SessionManager, SessionRef?) -> SessionUi = { project, workspace, manager, ref -> + service().create(project, workspace, manager, ref) }, private val resolve: (String) -> Workspace = { dir -> service().workspace(dir) }, private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, @@ -46,7 +45,7 @@ class SessionSidePanelManager( val active = current if (active?.blank == true) return register(active) - show(create(project, root, this, null, null, null)) + show(create(project, root, this, null)) } override fun openSession(session: SessionDto) { @@ -71,18 +70,11 @@ class SessionSidePanelManager( is SessionRef.Local -> ref.session?.directory?.let(resolve) ?: root is SessionRef.Cloud -> root } - val id = when (ref) { - is SessionRef.Local -> ref.id - is SessionRef.Cloud -> ref.key - } - val session = (ref as? SessionRef.Local)?.session - return create(project, workspace, this, id, session, ref).also { + return create(project, workspace, this, ref).also { all.add(it) opened[ref.key] = it - val local = session?.id - if (local != null) { - opened.putIfAbsent(local, it) - } + val local = (ref as? SessionRef.Local)?.session?.id + if (local != null) opened.putIfAbsent(local, it) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 3e583ff87ce..b853783a39d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -22,7 +22,6 @@ import ai.kilocode.client.session.ui.SessionStyleTarget import ai.kilocode.client.session.update.EVENT_FLUSH_MS import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.update.SessionControllerEvent -import ai.kilocode.rpc.dto.SessionDto import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import com.intellij.ide.ui.LafManagerListener @@ -51,11 +50,9 @@ class SessionUi( sessions: KiloSessionService, app: KiloAppService, cs: CoroutineScope, - id: String? = null, + ref: SessionRef? = null, displayMs: Long = SessionController.DISPLAY_DELAY_MS, open: (SessionRef) -> Unit = {}, - session: SessionDto? = null, - target: SessionRef? = SessionRef.resolve(id, session), ) : JPanel(BorderLayout()), Disposable, SessionStyleTarget { companion object { @@ -64,7 +61,6 @@ class SessionUi( private val project = project private val app = app - private val ref = SessionRef.resolve(id, session, target) private var opening = ref != null private var pending = false private var loaded: Boolean? = null @@ -75,7 +71,7 @@ class SessionUi( ?: EVENT_FLUSH_MS private val controller = SessionController( - this, ref, sessions, workspace, app, cs, this, + this, ref, sessions, workspace, app, cs, comp = this, flushMs = flushMs, condense = Registry.`is`("kilo.session.condense", true), displayMs = displayMs, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt index 106fe57d898..bf313665478 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.session import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace -import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -19,19 +18,15 @@ class SessionUiFactory( project: Project, workspace: Workspace, manager: SessionManager, - id: String? = null, - session: SessionDto? = null, - target: SessionRef? = SessionRef.resolve(id, session), + ref: SessionRef? = null, ): SessionUi = SessionUi( project = project, workspace = workspace, sessions = project.service(), app = service(), cs = scope(), - id = id, - session = session, - target = target, - open = { ref -> manager.openSession(ref) }, + ref = ref, + open = { item -> manager.openSession(item) }, ) fun scope(): CoroutineScope { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 1c037300745..aec57adf2c8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -35,7 +35,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { private lateinit var app: KiloAppService private val managers = mutableListOf() private val created = mutableListOf>() - private val targets = mutableListOf() + private val refs = mutableListOf() private val ui = mutableListOf() override fun setUp() { @@ -244,7 +244,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertTrue(active(manager) is SessionUi) assertEquals(listOf("/test" to "cloud:cloud_1"), created) - assertEquals("cloud:cloud_1", targets.single()?.key) + assertEquals("cloud:cloud_1", refs.single()?.key) assertEquals(listOf("cloud_1" to "/test"), rpc.imports) rpc.historyGate?.complete(Unit) @@ -341,10 +341,15 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { val manager = SessionSidePanelManager( project = project, root = workspace, - create = { project, workspace, owner, id, session, target -> + create = { project, workspace, owner, ref -> + val id = when (ref) { + is SessionRef.Local -> ref.id + is SessionRef.Cloud -> ref.key + null -> null + } created.add(workspace.directory to id) - targets.add(target) - SessionUi(project, workspace, sessions, app, scope, id = id, open = { item -> owner.openSession(item) }, session = session, target = target).also { + refs.add(ref) + SessionUi(project, workspace, sessions, app, scope, ref = ref, open = { item -> owner.openSession(item) }).also { ui.add(it) Disposer.register(it) { ui.remove(it) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 0d3e6d04bf9..4b9ab0e5db7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -231,7 +231,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { fun `test existing session history shows header above scroll pane`() { rpc.history.add(MessageWithPartsDto(message("msg1"), emptyList())) - ui = SessionUi(project, workspace, sessions, app, scope, id = "ses_test", displayMs = 0).apply { + ui = SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.Local("ses_test"), displayMs = 0).apply { setSize(800, 600) } settle() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index 96b8b3b5d6d..4e902d0fc02 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -79,7 +79,7 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { displayMs: Long = 0, open: (SessionRef) -> Unit = {}, ): SessionUi { - return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, open = open).apply { + return SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.from(id), displayMs = displayMs, open = open).apply { setSize(800, 600) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt index d56af31d373..091364ef108 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt @@ -148,7 +148,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { session: SessionDto? = null, beforeUpdate: () -> Boolean = { false }, afterUpdate: (Boolean) -> Unit = {}, - ref: SessionRef? = SessionRef.resolve(id, session), + ref: SessionRef? = if (session != null) SessionRef.Local(session) else SessionRef.from(id), ): SessionController { val root = Root() val m = SessionController( From b2be20950796d9b24c61a071f519128479878210 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 22:23:50 -0400 Subject: [PATCH 019/111] refactor(jetbrains): replace open callback in SessionUi with manager: SessionManager? --- .../kotlin/ai/kilocode/client/session/SessionUi.kt | 4 ++-- .../ai/kilocode/client/session/SessionUiFactory.kt | 2 +- .../client/session/SessionSidePanelManagerTest.kt | 2 +- .../kilocode/client/session/SessionUiFactoryTest.kt | 4 ++-- .../ai/kilocode/client/session/SessionUiTestBase.kt | 11 +++++++++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index b853783a39d..87dbd714187 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -52,7 +52,7 @@ class SessionUi( cs: CoroutineScope, ref: SessionRef? = null, displayMs: Long = SessionController.DISPLAY_DELAY_MS, - open: (SessionRef) -> Unit = {}, + manager: SessionManager? = null, ) : JPanel(BorderLayout()), Disposable, SessionStyleTarget { companion object { @@ -75,7 +75,7 @@ class SessionUi( flushMs = flushMs, condense = Registry.`is`("kilo.session.condense", true), displayMs = displayMs, - open = open, + open = { item -> manager?.openSession(item) }, beforeUpdate = { if (opening) false else scroll.atBottom() }, afterUpdate = { if (!opening) scroll.followBottom(it) }, loaded = ::onSessionLoaded, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt index bf313665478..52735be09d5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt @@ -26,7 +26,7 @@ class SessionUiFactory( app = service(), cs = scope(), ref = ref, - open = { item -> manager.openSession(item) }, + manager = manager, ) fun scope(): CoroutineScope { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index aec57adf2c8..99c1c044312 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -349,7 +349,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { } created.add(workspace.directory to id) refs.add(ref) - SessionUi(project, workspace, sessions, app, scope, ref = ref, open = { item -> owner.openSession(item) }).also { + SessionUi(project, workspace, sessions, app, scope, ref = ref, manager = owner).also { ui.add(it) Disposer.register(it) { ui.remove(it) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt index 19c0478291c..6e21d8c719c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt @@ -56,7 +56,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { fun `test factory wires open callback`() { val manager = FakeManager() val rpc = session("ses_1") - val ui = SessionUi(project, workspace, sessions, app, scope, open = { ref -> manager.openSession(ref) }) + val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager) val controller = controller(ui) com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait { @@ -69,7 +69,7 @@ class SessionUiFactoryTest : BasePlatformTestCase() { fun `test empty panel opens through SessionRef via controller`() { val manager = FakeManager() val rpc = session("ses_1") - val ui = SessionUi(project, workspace, sessions, app, scope, open = { ref -> manager.openSession(ref) }) + val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager) val controller = controller(ui) val panel = ai.kilocode.client.session.ui.EmptySessionPanel(testRootDisposable, controller, listOf(rpc)) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index 4e902d0fc02..95d35e1f7ba 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -77,9 +77,16 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { protected fun newUi( id: String? = null, displayMs: Long = 0, - open: (SessionRef) -> Unit = {}, + open: ((SessionRef) -> Unit)? = null, ): SessionUi { - return SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.from(id), displayMs = displayMs, open = open).apply { + val manager = open?.let { fn -> + object : SessionManager { + override fun newSession() {} + override fun showHistory() {} + override fun openSession(ref: SessionRef) = fn(ref) + } + } + return SessionUi(project, workspace, sessions, app, scope, ref = SessionRef.from(id), displayMs = displayMs, manager = manager).apply { setSize(800, 600) } } From d630d66d89929cf0b273b9051a4cd3e858a98fa2 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 22:40:02 -0400 Subject: [PATCH 020/111] fix(jetbrains): remove redundant session overload --- .../ai/kilocode/client/session/SessionSidePanelManager.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 410e698ac87..f91e16af5f7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -48,10 +48,6 @@ class SessionSidePanelManager( show(create(project, root, this, null)) } - override fun openSession(session: SessionDto) { - openSession(SessionRef.Local(session)) - } - override fun openSession(ref: SessionRef) { register(current) val ui = opened[ref.key] ?: run { From 2faa74d8b67b38d412e2345b232542876b0fe955 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 23:16:12 -0400 Subject: [PATCH 021/111] feat(jetbrains): add history back button --- .../client/session/history/HistoryPanel.kt | 26 +++++++++++++++++-- .../resources/messages/KiloBundle.properties | 1 + .../session/history/HistoryControllerTest.kt | 12 +++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 6cfe54fffc5..4759da0426c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -4,7 +4,9 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.SessionStyle import ai.kilocode.client.ui.UiStyle +import com.intellij.icons.AllIcons import com.intellij.ide.ui.LafManagerListener +import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.Disposable import com.intellij.openapi.ui.Messages @@ -54,8 +56,12 @@ class HistoryPanel( private val cards = CardLayout() private val body = BorderLayoutPanel().apply { layout = cards } private val load = LoadingPanel() - private val localInfo = TabInfo(localPanel).setText(KiloBundle.message("history.tab.local")) - private val cloudInfo = TabInfo(cloudPanel).setText(KiloBundle.message("history.tab.cloud")) + private val localInfo = TabInfo(localPanel) + .setText(KiloBundle.message("history.tab.local")) + .setForeSideComponent(back()) + private val cloudInfo = TabInfo(cloudPanel) + .setText(KiloBundle.message("history.tab.cloud")) + .setForeSideComponent(back()) private var stale = false private val tabs: JBTabs = JBTabsFactory.createTabs(null, this).apply { presentation.setSingleRow(true) @@ -150,6 +156,15 @@ class HistoryPanel( ) } + private fun back() = BorderLayoutPanel().apply { + add(JButton(KiloBundle.message("history.back"), AllIcons.Actions.Back).apply { + putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) + isFocusable = false + addActionListener { Unit } + }, BorderLayout.WEST) + border = JBUI.Borders.emptyRight(UiStyle.Space.LG) + } + private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { return BorderLayoutPanel().apply { add(search, BorderLayout.NORTH) @@ -293,6 +308,11 @@ class HistoryPanel( internal fun listFocusable() = activeList().isFocusable + internal fun backText(): String? { + val view = activeInfo().foreSideComponent ?: return null + return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.text + } + internal fun clickDelete() { localList.selectedValue?.let(controller::delete) } @@ -338,6 +358,8 @@ class HistoryPanel( private fun activeSearch(): SearchTextField = if (tabs.selectedInfo === cloudInfo) cloudSearch else localSearch + private fun activeInfo(): TabInfo = if (tabs.selectedInfo === cloudInfo) cloudInfo else localInfo + private fun move(step: Int) { val list = activeList() val size = list.model.size diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index c775534617f..238488e8566 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -86,6 +86,7 @@ reasoning.picker.tooltip=Select reasoning effort history.tab.local=Local history.tab.cloud=Cloud +history.back=Back history.search.placeholder=Search sessions history.empty=No sessions history.loading=Loading... diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index b9dd7452712..81aba4a276e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -130,6 +130,18 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(1, panel.itemCount()) } + fun `test panel shows back button with tabs`() { + val panel = HistoryPanel(parent, controller()) + flush() + + assertEquals(KiloBundle.message("history.back"), panel.backText()) + + panel.clickCloud() + flush() + + assertEquals(KiloBundle.message("history.back"), panel.backText()) + } + fun `test panel preserves independent search per source`() { rpc.listed += session("ses_1", "Alpha") rpc.listed += session("ses_2", "Beta") From 373b90a5c3930f141cb69fdd3145bdcb3ffafc63 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 23:28:59 -0400 Subject: [PATCH 022/111] refactor(jetbrains): share history helpers with empty state --- .../session/history/HistoryListRenderer.kt | 3 +- .../client/session/history/HistoryListUi.kt | 15 +++++ .../client/session/history/HistoryPanel.kt | 10 +-- .../client/session/ui/EmptySessionPanel.kt | 63 +++++++------------ .../resources/messages/KiloBundle.properties | 4 -- .../session/ui/EmptySessionPanelTest.kt | 21 +++++-- 6 files changed, 53 insertions(+), 63 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index 0fa46be66f5..8b5bcacc901 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -1,6 +1,5 @@ package ai.kilocode.client.session.history -import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.PickerRow import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons @@ -114,7 +113,7 @@ internal open class HistoryRenderer( title.clear() title.append( - value?.title?.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled"), + value?.let(::title).orEmpty(), SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg), ) time.text = value?.let(HistoryTime::relative).orEmpty() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt new file mode 100644 index 00000000000..c5c9b2a6be4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt @@ -0,0 +1,15 @@ +package ai.kilocode.client.session.history + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.ui.components.JBList +import java.awt.event.MouseEvent + +internal fun title(item: HistoryItem): String = item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled") + +internal fun clicked(list: JBList, e: MouseEvent): T? { + val row = list.locationToIndex(e.point) + val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null + if (!box.contains(e.point)) return null + list.selectedIndex = row + return list.model.getElementAt(row) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 4759da0426c..f1d14aba3bd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -269,14 +269,6 @@ class HistoryPanel( return HistoryRenderer.isDeleteClick(list, box, e.point) } - private fun clicked(list: JBList, e: MouseEvent): T? { - val row = list.locationToIndex(e.point) - val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null - if (!box.contains(e.point)) return null - list.selectedIndex = row - return list.model.getElementAt(row) - } - private fun activate(item: HistoryItem) { when (item) { is LocalHistoryItem -> controller.open(item) @@ -288,7 +280,7 @@ class HistoryPanel( if (controller.deleting(item)) return val result = Messages.showYesNoDialog( this, - KiloBundle.message("history.delete.confirm.message", item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled")), + KiloBundle.message("history.delete.confirm.message", title(item)), KiloBundle.message("history.delete.confirm.title"), Messages.getWarningIcon(), ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 6f693c36e44..646770f1e68 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -2,6 +2,10 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.session.history.HistoryTime +import ai.kilocode.client.session.history.LocalHistoryItem +import ai.kilocode.client.session.history.clicked +import ai.kilocode.client.session.history.title import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView @@ -28,7 +32,6 @@ import javax.swing.DefaultListModel import javax.swing.JList import javax.swing.ListCellRenderer import javax.swing.ListSelectionModel -import kotlin.math.abs /** * Centered empty-session panel. @@ -42,13 +45,9 @@ class EmptySessionPanel( companion object { internal val LIMIT = UiStyle.Size.LIMIT internal val MAX_WIDTH = UiStyle.Size.WIDTH - private const val SECOND_MS_LIMIT = 10_000_000_000L - private const val MINUTE = 60_000L - private const val HOUR = 60 * MINUTE - private const val DAY = 24 * HOUR } - private val model = DefaultListModel() + private val model = DefaultListModel() private var hover = -1 private var style = SessionStyle.current() private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply { @@ -65,10 +64,8 @@ class EmptySessionPanel( emptyText.clear() addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - val index = row(e) - if (index < 0) return - selectedIndex = index - controller.openSession(SessionRef.Local(model.getElementAt(index))) + val item = clicked(this@apply, e) ?: return + controller.openSession(SessionRef.Local(item.session)) } override fun mouseExited(e: MouseEvent) { @@ -78,7 +75,7 @@ class EmptySessionPanel( }) addMouseMotionListener(object : MouseMotionAdapter() { override fun mouseMoved(e: MouseEvent) { - val index = row(e) + val index = index(e) if (hover == index) return hover = index repaint() @@ -105,7 +102,7 @@ class EmptySessionPanel( private fun setSessions(sessions: List) { model.clear() - sessions.take(LIMIT).forEach(model::addElement) + sessions.take(LIMIT).map(::LocalHistoryItem).forEach(model::addElement) revalidate() repaint() } @@ -154,7 +151,7 @@ class EmptySessionPanel( internal fun clickRecent(index: Int) { list.selectedIndex = index - controller.openSession(SessionRef.Local(model.getElementAt(index))) + controller.openSession(SessionRef.Local(model.getElementAt(index).session)) } internal fun recentVisible() = true @@ -169,13 +166,7 @@ class EmptySessionPanel( internal fun activeView() = getComponent(0) - internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = time(session, now) - - internal fun normalize(value: Double): Long { - val raw = value.toLong() - if (abs(raw) < SECOND_MS_LIMIT) return raw * 1000 - return raw - } + internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = HistoryTime.relative(LocalHistoryItem(session), now) internal fun rendererComponent( session: SessionDto, @@ -184,20 +175,20 @@ class EmptySessionPanel( ): Component { val old = this.hover this.hover = if (hover) 0 else -1 - return list.cellRenderer.getListCellRendererComponent(list, session, 0, selected, false).also { + return list.cellRenderer.getListCellRendererComponent(list, LocalHistoryItem(session), 0, selected, false).also { this.hover = old } } - private fun row(e: MouseEvent): Int { - val index = list.locationToIndex(e.point) - if (index < 0) return -1 - val box = list.getCellBounds(index, index) ?: return -1 + private fun index(e: MouseEvent): Int { + val idx = list.locationToIndex(e.point) + if (idx < 0) return -1 + val box = list.getCellBounds(idx, idx) ?: return -1 if (!box.contains(e.point)) return -1 - return index + return idx } - private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer { + private inner class SessionRenderer : BorderLayoutPanel(), ListCellRenderer { private val title = JBLabel() private val time = JBLabel() @@ -208,8 +199,8 @@ class EmptySessionPanel( } override fun getListCellRendererComponent( - list: JList, - value: SessionDto?, + list: JList, + value: LocalHistoryItem?, index: Int, selected: Boolean, focus: Boolean, @@ -220,23 +211,11 @@ class EmptySessionPanel( title.foreground = if (active) list.selectionForeground else UIUtil.getLabelForeground() time.foreground = if (active) list.selectionForeground else UIUtil.getContextHelpForeground() title.text = value?.let(::title) ?: "" - time.text = value?.let { time(it) } ?: "" + time.text = value?.let(HistoryTime::relative) ?: "" return this } } - private fun title(session: SessionDto) = - session.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("session.tab.untitled") - - private fun time(session: SessionDto, now: Long = System.currentTimeMillis()): String { - val ms = normalize(session.time.updated) - val diff = (now - ms).coerceAtLeast(0) - if (diff < MINUTE) return KiloBundle.message("session.empty.time.moments") - if (diff < HOUR) return KiloBundle.message("session.empty.time.minutes", (diff / MINUTE).coerceAtLeast(1)) - if (diff < DAY) return KiloBundle.message("session.empty.time.hours", (diff / HOUR).coerceAtLeast(1)) - return KiloBundle.message("session.empty.time.days", (diff / DAY).coerceAtLeast(1)) - } - override fun dispose() { // no-op } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 238488e8566..b346e789d0b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -8,10 +8,6 @@ session.connection.warning.config=Configuration warnings session.empty.welcome=Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase. session.empty.loading=Loading... session.empty.recent=RECENT -session.empty.time.moments=Moments ago -session.empty.time.minutes={0} min ago -session.empty.time.hours={0}h ago -session.empty.time.days={0}d ago session.scroll.bottom=Scroll to bottom session.tab.new=New Session session.tab.untitled=Untitled Session diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index ea90be3a397..788bdfe869f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -5,6 +5,8 @@ import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.session.history.HistoryTime +import ai.kilocode.client.session.history.LocalHistoryItem import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi @@ -16,6 +18,8 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto import ai.kilocode.rpc.dto.SessionDto import ai.kilocode.rpc.dto.SessionTimeDto import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -138,11 +142,16 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals(selected.background, hovered.background) } - fun `test timestamp normalization handles seconds and milliseconds`() { - val panel = panel() + fun `test renderer reuses history title fallback`() { + val cell = panel().rendererComponent(session("ses_1", title = "")) as BorderLayoutPanel + val label = UIUtil.uiTraverser(cell).filter(JBLabel::class.java).firstOrNull() - assertEquals(1_700_000_000_000L, panel.normalize(1_700_000_000.0)) - assertEquals(1_700_000_000_000L, panel.normalize(1_700_000_000_000.0)) + assertEquals("Untitled", label?.text) + } + + fun `test timestamp normalization handles seconds and milliseconds`() { + assertEquals(1_700_000_000_000L, HistoryTime.millis(LocalHistoryItem(session("ses_1", 1_700_000_000)))) + assertEquals(1_700_000_000_000L, HistoryTime.millis(LocalHistoryItem(session("ses_1", 1_700_000_000_000)))) } fun `test timestamp renders coarse relative text`() { @@ -158,11 +167,11 @@ class EmptySessionPanelTest : BasePlatformTestCase() { private fun panel(recents: List = emptyList()) = EmptySessionPanel(testRootDisposable, controller, recents) - private fun session(id: String, updated: Long = 2_000L) = SessionDto( + private fun session(id: String, updated: Long = 2_000L, title: String = "Title $id") = SessionDto( id = id, projectID = "prj", directory = "/repo/$id", - title = "Title $id", + title = title, version = "1", time = SessionTimeDto(created = 1.0, updated = updated.toDouble()), ) From a7e5c4f0a98d22bb099d50f73f7bf5bc7f0a55a0 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 7 May 2026 23:51:31 -0400 Subject: [PATCH 023/111] feat(jetbrains): improve history navigation --- .changeset/jetbrains-history-navigation.md | 5 ++ .../client/session/SessionSidePanelManager.kt | 16 ++++- .../ai/kilocode/client/session/SessionUi.kt | 4 +- .../client/session/history/HistoryPanel.kt | 19 ++++- .../client/session/ui/EmptySessionPanel.kt | 72 +++++++++++++++++++ .../resources/messages/KiloBundle.properties | 1 + .../session/SessionSidePanelManagerTest.kt | 45 ++++++++++++ .../client/session/SessionUiFactoryTest.kt | 17 +++++ .../session/history/HistoryControllerTest.kt | 25 +++++++ .../session/ui/EmptySessionPanelTest.kt | 28 +++++++- 10 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 .changeset/jetbrains-history-navigation.md diff --git a/.changeset/jetbrains-history-navigation.md b/.changeset/jetbrains-history-navigation.md new file mode 100644 index 00000000000..92f361fc775 --- /dev/null +++ b/.changeset/jetbrains-history-navigation.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve JetBrains history navigation with Back support and a Show History action in recent sessions. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index f91e16af5f7..2fa5d531c56 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -37,6 +37,7 @@ class SessionSidePanelManager( private val opened = mutableMapOf() private val all = mutableSetOf() private var current: SessionUi? = null + private var latest: SessionUi? = null private var panel: JComponent? = null val defaultFocusedComponent: JComponent? get() = current?.defaultFocusedComponent ?: (panel as? HistoryPanel)?.defaultFocusedComponent @@ -112,7 +113,17 @@ class SessionSidePanelManager( deleted = this::removeSession, ) Disposer.register(this) { cs.cancel() } - return HistoryPanel(this, controller).component + return HistoryPanel(this, controller, nav = this::back).component + } + + private fun back() { + val ui = latest + if (ui != null && ui in all) { + show(ui) + return + } + latest = null + newSession() } private fun removeSession(id: String) { @@ -120,12 +131,14 @@ class SessionSidePanelManager( opened.entries.removeIf { it.value === ui } all.remove(ui) if (current === ui) current = null + if (latest === ui) latest = null Disposer.dispose(ui) } private fun show(ui: SessionUi) { all.add(ui) register(ui) + latest = ui if (current === ui) return release(current) component.removeAll() @@ -155,6 +168,7 @@ class SessionSidePanelManager( opened.clear() all.clear() current = null + latest = null component.removeAll() items.forEach { Disposer.dispose(it) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 87dbd714187..a73b7f2038e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -52,7 +52,7 @@ class SessionUi( cs: CoroutineScope, ref: SessionRef? = null, displayMs: Long = SessionController.DISPLAY_DELAY_MS, - manager: SessionManager? = null, + private val manager: SessionManager? = null, ) : JPanel(BorderLayout()), Disposable, SessionStyleTarget { companion object { @@ -218,7 +218,7 @@ class SessionUi( } is SessionControllerEvent.ViewChanged.ShowRecents -> { - val panel = EmptySessionPanel(this, controller, event.recents) + val panel = EmptySessionPanel(this, controller, event.recents) { manager?.showHistory() } scroll.show(panel) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index f1d14aba3bd..88f7299d24b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -27,6 +27,7 @@ import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.CardLayout +import java.awt.Cursor import java.awt.event.HierarchyEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter @@ -45,6 +46,7 @@ class HistoryPanel( parent: Disposable, private val controller: HistoryController, private val gitUrl: () -> String? = { null }, + private val nav: () -> Unit = {}, ) : BorderLayoutPanel(), Disposable { private val localSearch = search(controller.local) private val cloudSearch = search(controller.cloud) @@ -160,7 +162,8 @@ class HistoryPanel( add(JButton(KiloBundle.message("history.back"), AllIcons.Actions.Back).apply { putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) isFocusable = false - addActionListener { Unit } + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addActionListener { nav() } }, BorderLayout.WEST) border = JBUI.Borders.emptyRight(UiStyle.Space.LG) } @@ -183,6 +186,7 @@ class HistoryPanel( selectionMode = ListSelectionModel.SINGLE_SELECTION isFocusable = false cellRenderer = LocalHistoryRenderer(controller.local) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { override fun mouseReleased(e: MouseEvent) { @@ -209,6 +213,7 @@ class HistoryPanel( selectionMode = ListSelectionModel.SINGLE_SELECTION isFocusable = false cellRenderer = CloudHistoryRenderer(controller.cloud) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { override fun mouseReleased(e: MouseEvent) { @@ -300,11 +305,23 @@ class HistoryPanel( internal fun listFocusable() = activeList().isFocusable + internal fun listCursor() = activeList().cursor.type + internal fun backText(): String? { val view = activeInfo().foreSideComponent ?: return null return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.text } + internal fun backCursor(): Int? { + val view = activeInfo().foreSideComponent ?: return null + return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.cursor?.type + } + + internal fun clickBack() { + val view = activeInfo().foreSideComponent ?: return + UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.doClick() + } + internal fun clickDelete() { localList.selectedValue?.let(controller::delete) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 646770f1e68..0fb25f0b15f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -10,6 +10,7 @@ import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView import ai.kilocode.rpc.dto.SessionDto +import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.IconLoader @@ -22,13 +23,18 @@ import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.Component +import java.awt.Cursor import java.awt.Dimension +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.RenderingHints import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseMotionAdapter import javax.swing.Box import javax.swing.BoxLayout import javax.swing.DefaultListModel +import javax.swing.JButton import javax.swing.JList import javax.swing.ListCellRenderer import javax.swing.ListSelectionModel @@ -40,6 +46,7 @@ class EmptySessionPanel( parent: Disposable, private val controller: SessionController, recents: List, + private val history: () -> Unit = {}, ) : BorderLayoutPanel(), Disposable, SessionStyleTarget { companion object { @@ -61,6 +68,7 @@ class EmptySessionPanel( selectionMode = ListSelectionModel.SINGLE_SELECTION visibleRowCount = LIMIT cellRenderer = SessionRenderer() + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.clear() addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { @@ -82,6 +90,10 @@ class EmptySessionPanel( } }) } + private val historyButton = ShowHistoryButton().apply { + alignmentX = CENTER_ALIGNMENT + addActionListener { history() } + } private val md = MdView.html().apply { // MdView uses an HTML component; transparency keeps the centered panel seamless. opaque = false @@ -122,6 +134,10 @@ class EmptySessionPanel( alignmentX = CENTER_ALIGNMENT add(recentTitle, BorderLayout.NORTH) add(list, BorderLayout.CENTER) + add(BorderLayoutPanel().apply { + border = JBUI.Borders.emptyTop(UiStyle.Space.LG) + add(historyButton, BorderLayout.CENTER) + }, BorderLayout.SOUTH) } val stack = BorderLayoutPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) @@ -154,6 +170,18 @@ class EmptySessionPanel( controller.openSession(SessionRef.Local(model.getElementAt(index).session)) } + internal fun clickShowHistory() { + historyButton.doClick() + } + + internal fun showHistoryText() = historyButton.text + + internal fun showHistoryBorderPainted() = historyButton.isBorderPainted + + internal fun showHistoryCursor() = historyButton.cursor.type + + internal fun recentCursor() = list.cursor.type + internal fun recentVisible() = true internal fun explanationMarkdown() = md.markdown() @@ -216,6 +244,50 @@ class EmptySessionPanel( } } + private inner class ShowHistoryButton : JButton(KiloBundle.message("session.showHistory"), AllIcons.Vcs.History) { + private var over = false + + init { + isFocusable = false + setRequestFocusEnabled(false) + isContentAreaFilled = false + isBorderPainted = false + isOpaque = false + border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun paintComponent(g: Graphics) { + if (isEnabled && over) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + } + super.paintComponent(g) + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } + } + override fun dispose() { // no-op } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index b346e789d0b..9549719e60d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -8,6 +8,7 @@ session.connection.warning.config=Configuration warnings session.empty.welcome=Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase. session.empty.loading=Loading... session.empty.recent=RECENT +session.showHistory=Show History session.scroll.bottom=Scroll to bottom session.tab.new=New Session session.tab.untitled=Untitled Session diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 99c1c044312..205c0eb244c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -214,6 +214,39 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertNull(manager.defaultFocusedComponent) } + fun `test history back restores latest open session`() { + val manager = manager() + + manager.openSession(session("ses_1")) + val first = active(manager) + manager.showHistory() + back(manager) + + assertSame(first, active(manager)) + } + + fun `test history back without latest session opens new session`() { + val manager = manager() + + manager.showHistory() + back(manager) + + assertTrue(active(manager) is SessionUi) + assertEquals(listOf("/test" to null), created) + } + + fun `test history back ignores deleted latest session`() { + val manager = manager() + + manager.openSession(session("ses_1")) + manager.showHistory() + remove(manager, "ses_1") + back(manager) + + assertTrue(active(manager) is SessionUi) + assertEquals(listOf("/test" to "ses_1", "/test" to null), created) + } + fun `test opening local history item shows session ui`() { lateinit var open: (SessionRef) -> Unit val history = JLabel("History") @@ -369,6 +402,18 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { return field.get(this) as ai.kilocode.client.session.update.SessionController } + private fun remove(manager: SessionSidePanelManager, id: String) { + val method = SessionSidePanelManager::class.java.getDeclaredMethod("removeSession", String::class.java) + method.isAccessible = true + method.invoke(manager, id) + } + + private fun back(manager: SessionSidePanelManager) { + val method = SessionSidePanelManager::class.java.getDeclaredMethod("back") + method.isAccessible = true + method.invoke(manager) + } + private fun settle() = kotlinx.coroutines.runBlocking { repeat(5) { kotlinx.coroutines.delay(100) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt index 6e21d8c719c..b5acef14090 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt @@ -79,6 +79,21 @@ class SessionUiFactoryTest : BasePlatformTestCase() { assertEquals(listOf("ses_1"), manager.opened) } + fun `test empty panel show history routes through manager`() { + val manager = FakeManager() + val ui = SessionUi(project, workspace, sessions, app, scope, manager = manager) + val controller = controller(ui) + val panel = ai.kilocode.client.session.ui.EmptySessionPanel( + testRootDisposable, + controller, + emptyList(), + ) { manager.showHistory() } + + panel.clickShowHistory() + + assertEquals(1, manager.history) + } + private fun controller(ui: SessionUi): ai.kilocode.client.session.update.SessionController { val field = SessionUi::class.java.getDeclaredField("controller") field.isAccessible = true @@ -102,10 +117,12 @@ class SessionUiFactoryTest : BasePlatformTestCase() { private class FakeManager : SessionManager { val opened = mutableListOf() + var history = 0 override fun newSession() { } override fun showHistory() { + history++ } override fun openSession(ref: SessionRef) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 81aba4a276e..4458552722b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import java.awt.Cursor import java.awt.event.KeyEvent import java.time.Instant import java.time.temporal.ChronoUnit @@ -142,6 +143,30 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(KiloBundle.message("history.back"), panel.backText()) } + fun `test panel back button invokes callback`() { + var calls = 0 + val panel = HistoryPanel(parent, controller(), nav = { calls++ }) + flush() + + panel.clickBack() + + assertEquals(1, calls) + } + + fun `test history actions use hand cursor`() { + val panel = HistoryPanel(parent, controller()) + flush() + + assertEquals(Cursor.HAND_CURSOR, panel.backCursor()) + assertEquals(Cursor.HAND_CURSOR, panel.listCursor()) + + panel.clickCloud() + flush() + + assertEquals(Cursor.HAND_CURSOR, panel.backCursor()) + assertEquals(Cursor.HAND_CURSOR, panel.listCursor()) + } + fun `test panel preserves independent search per source`() { rpc.listed += session("ses_1", "Alpha") rpc.listed += session("ses_2", "Beta") diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 788bdfe869f..948352b610a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import java.awt.BorderLayout +import java.awt.Cursor @Suppress("UnstableApiUsage") class EmptySessionPanelTest : BasePlatformTestCase() { @@ -123,6 +124,29 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals(listOf("ses_2"), opened) } + fun `test show history button uses localized text`() { + val panel = panel() + + assertEquals(ai.kilocode.client.plugin.KiloBundle.message("session.showHistory"), panel.showHistoryText()) + } + + fun `test action controls use hand cursor and no show history outline`() { + val panel = panel() + + assertFalse(panel.showHistoryBorderPainted()) + assertEquals(Cursor.HAND_CURSOR, panel.showHistoryCursor()) + assertEquals(Cursor.HAND_CURSOR, panel.recentCursor()) + } + + fun `test clicking show history delegates callback`() { + var calls = 0 + val panel = panel(history = { calls++ }) + + panel.clickShowHistory() + + assertEquals(1, calls) + } + fun `test renderer aligns title center and time east`() { val cell = panel().rendererComponent(session("ses_1")) as BorderLayoutPanel val layout = cell.layout as BorderLayout @@ -164,8 +188,8 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals("4d ago", panel.text(session("ses_1", now - 345_600_000), now)) } - private fun panel(recents: List = emptyList()) = - EmptySessionPanel(testRootDisposable, controller, recents) + private fun panel(recents: List = emptyList(), history: () -> Unit = {}) = + EmptySessionPanel(testRootDisposable, controller, recents, history) private fun session(id: String, updated: Long = 2_000L, title: String = "Title $id") = SessionDto( id = id, From 391379f1611ac5b23c6284cc20c85cb5de89dabe Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 14:38:32 -0400 Subject: [PATCH 024/111] fix(jetbrains): refine empty session layout --- .changeset/jetbrains-empty-panel-layout.md | 5 + .../ai/kilocode/client/session/SessionUi.kt | 2 +- .../client/session/ui/EmptySessionPanel.kt | 136 ++++++++++-------- .../kilocode/client/ui/CenterShrinkPanel.kt | 38 +++++ .../session/ui/EmptySessionPanelTest.kt | 23 ++- 5 files changed, 138 insertions(+), 66 deletions(-) create mode 100644 .changeset/jetbrains-empty-panel-layout.md create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/CenterShrinkPanel.kt diff --git a/.changeset/jetbrains-empty-panel-layout.md b/.changeset/jetbrains-empty-panel-layout.md new file mode 100644 index 00000000000..3f13739eeb8 --- /dev/null +++ b/.changeset/jetbrains-empty-panel-layout.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index a73b7f2038e..91a9e18ed4a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -219,7 +219,7 @@ class SessionUi( is SessionControllerEvent.ViewChanged.ShowRecents -> { val panel = EmptySessionPanel(this, controller, event.recents) { manager?.showHistory() } - scroll.show(panel) + scroll.show(panel.view) } is SessionControllerEvent.ViewChanged.ShowSession -> { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 0fb25f0b15f..f7aa3291725 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -7,8 +7,8 @@ import ai.kilocode.client.session.history.LocalHistoryItem import ai.kilocode.client.session.history.clicked import ai.kilocode.client.session.history.title import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.ui.CenterShrinkPanel import ai.kilocode.client.ui.UiStyle -import ai.kilocode.client.ui.md.MdView import ai.kilocode.rpc.dto.SessionDto import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable @@ -17,10 +17,10 @@ import com.intellij.openapi.util.IconLoader import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList import com.intellij.util.ui.Centerizer -import com.intellij.util.ui.JBDimension import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel +import com.intellij.xml.util.XmlStringUtil import java.awt.BorderLayout import java.awt.Component import java.awt.Cursor @@ -31,8 +31,6 @@ import java.awt.RenderingHints import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseMotionAdapter -import javax.swing.Box -import javax.swing.BoxLayout import javax.swing.DefaultListModel import javax.swing.JButton import javax.swing.JList @@ -40,7 +38,10 @@ import javax.swing.ListCellRenderer import javax.swing.ListSelectionModel /** - * Centered empty-session panel. + * Empty-session panel. + * + * The content is a BorderLayout panel, wrapped in a + * [CenterShrinkPanel] (exposed as [view]) so callers need not know about centering. */ class EmptySessionPanel( parent: Disposable, @@ -51,19 +52,20 @@ class EmptySessionPanel( companion object { internal val LIMIT = UiStyle.Size.LIMIT - internal val MAX_WIDTH = UiStyle.Size.WIDTH + internal val DESCRIPTION_WIDTH = 250 } + val view: CenterShrinkPanel = CenterShrinkPanel(this) + private val model = DefaultListModel() private var hover = -1 private var style = SessionStyle.current() + private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply { foreground = UIUtil.getContextHelpForeground() - border = JBUI.Borders.emptyLeft(UiStyle.Space.LG) } private val list = JBList(model).apply { - // Blend the recent-session list into the centered empty-state surface. isOpaque = false selectionMode = ListSelectionModel.SINGLE_SELECTION visibleRowCount = LIMIT @@ -90,26 +92,67 @@ class EmptySessionPanel( } }) } + private val historyButton = ShowHistoryButton().apply { - alignmentX = CENTER_ALIGNMENT addActionListener { history() } } - private val md = MdView.html().apply { - // MdView uses an HTML component; transparency keeps the centered panel seamless. - opaque = false + + private val welcomeLabel = JBLabel(welcomeHtml()).apply { foreground = UIUtil.getContextHelpForeground() - set(KiloBundle.message("session.empty.welcome")) + horizontalAlignment = JBLabel.CENTER + setAllowAutoWrapping(true) + } + + private val description = object : BorderLayoutPanel() { + override fun getPreferredSize(): Dimension { + val size = super.getPreferredSize() + return Dimension(JBUI.scale(DESCRIPTION_WIDTH), size.height) + } + + override fun getMaximumSize(): Dimension { + val size = super.getMaximumSize() + return Dimension(JBUI.scale(DESCRIPTION_WIDTH), size.height) + } + }.apply { + isOpaque = false + border = JBUI.Borders.empty(UiStyle.Space.LG, 0, UiStyle.Space.LG, 0) + add(welcomeLabel, BorderLayout.CENTER) } - private val content = createContent() init { Disposer.register(parent, this) - // The empty state floats on the tool-window background. isOpaque = false - border = UiStyle.Insets.empty() applyStyle(SessionStyle.current()) setSessions(recents) - add(Centerizer(content, Centerizer.TYPE.BOTH), BorderLayout.CENTER) + + val gap = JBUI.scale(UiStyle.Gap.turn()) + layout = BorderLayout(0, gap) + + val logo = JBLabel( + IconLoader.getIcon("/icons/kilo-content.svg", EmptySessionPanel::class.java), + ).apply { + horizontalAlignment = JBLabel.CENTER + } + val header = BorderLayoutPanel(0, gap).apply { + isOpaque = false + add(logo, BorderLayout.NORTH) + add(CenterShrinkPanel(description), BorderLayout.CENTER) + } + + val recent = BorderLayoutPanel().apply { + isOpaque = false + add(recentTitle, BorderLayout.NORTH) + add(list, BorderLayout.CENTER) + } + + val south = BorderLayoutPanel().apply { + isOpaque = false + add(Centerizer(historyButton, Centerizer.TYPE.HORIZONTAL), BorderLayout.CENTER) + } + + add(header, BorderLayout.NORTH) + add(recent, BorderLayout.CENTER) + add(south, BorderLayout.SOUTH) } private fun setSessions(sessions: List) { @@ -119,44 +162,6 @@ class EmptySessionPanel( repaint() } - private fun createContent(): BorderLayoutPanel { - val logo = JBLabel( - IconLoader.getIcon("/icons/kilo-content.svg", EmptySessionPanel::class.java), - ).apply { - alignmentX = CENTER_ALIGNMENT - } - val intro = BorderLayoutPanel().apply { - alignmentX = CENTER_ALIGNMENT - add(md.component, BorderLayout.CENTER) - border = JBUI.Borders.empty(0, UiStyle.Space.PAD, 0, UiStyle.Space.PAD) - } - val recent = BorderLayoutPanel().apply { - alignmentX = CENTER_ALIGNMENT - add(recentTitle, BorderLayout.NORTH) - add(list, BorderLayout.CENTER) - add(BorderLayoutPanel().apply { - border = JBUI.Borders.emptyTop(UiStyle.Space.LG) - add(historyButton, BorderLayout.CENTER) - }, BorderLayout.SOUTH) - } - val stack = BorderLayoutPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(logo) - add(Box.createVerticalStrut(JBUI.scale(UiStyle.Space.LOGO))) - add(intro) - add(Box.createVerticalStrut(JBUI.scale(UiStyle.Space.RECENT))) - add(recent) - } - return object : BorderLayoutPanel() { - override fun getPreferredSize(): Dimension { - val size = super.getPreferredSize() - return JBDimension(JBUI.scale(MAX_WIDTH), size.height) - } - }.apply { - add(stack, BorderLayout.NORTH) - } - } - internal fun recentCount() = model.size() internal fun selectRecent(index: Int) { @@ -184,9 +189,15 @@ class EmptySessionPanel( internal fun recentVisible() = true - internal fun explanationMarkdown() = md.markdown() + internal fun explanationText() = KiloBundle.message("session.empty.welcome") - internal fun contentPreferredSize() = content.preferredSize + internal fun welcomeLabelAlignment() = welcomeLabel.horizontalAlignment + + internal fun descriptionPreferredSize() = description.preferredSize + + internal fun descriptionMaximumSize() = description.maximumSize + + internal fun historyButtonPreferredWidth() = historyButton.preferredSize.width internal fun initialized() = true @@ -194,7 +205,8 @@ class EmptySessionPanel( internal fun activeView() = getComponent(0) - internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = HistoryTime.relative(LocalHistoryItem(session), now) + internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = + HistoryTime.relative(LocalHistoryItem(session), now) internal fun rendererComponent( session: SessionDto, @@ -221,6 +233,7 @@ class EmptySessionPanel( private val time = JBLabel() init { + layout = BorderLayout(JBUI.scale(UiStyle.Space.LG), 0) border = JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG) add(title, BorderLayout.CENTER) add(time, BorderLayout.EAST) @@ -253,7 +266,6 @@ class EmptySessionPanel( isContentAreaFilled = false isBorderPainted = false isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG) cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) addMouseListener(object : MouseAdapter() { override fun mouseEntered(e: MouseEvent) { @@ -294,9 +306,13 @@ class EmptySessionPanel( override fun applyStyle(style: SessionStyle) { this.style = style - md.font = style.uiFont + welcomeLabel.font = style.uiFont recentTitle.font = style.smallUiFont revalidate() repaint() } + + private fun welcomeHtml() = XmlStringUtil.wrapInHtml( + "
${XmlStringUtil.escapeString(KiloBundle.message("session.empty.welcome"))}
" + ) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/CenterShrinkPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/CenterShrinkPanel.kt new file mode 100644 index 00000000000..57e7d95e5f4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/CenterShrinkPanel.kt @@ -0,0 +1,38 @@ +package ai.kilocode.client.ui + +import java.awt.Component +import java.awt.Dimension +import javax.swing.JPanel + +/** + * Centers its single child and shrinks it to available space when needed. + * If available space is larger than the child's maximum size, the child is not expanded. + */ +class CenterShrinkPanel(child: Component) : JPanel(null) { + init { + isOpaque = false + add(child) + } + + override fun doLayout() { + if (componentCount == 0) return + val child = getComponent(0) + val insets = getInsets() + val availW = width - insets.left - insets.right + val availH = height - insets.top - insets.bottom + val pref = child.preferredSize + val max = child.maximumSize + val w = minOf(pref.width, max.width, availW) + val h = minOf(pref.height, max.height, availH) + val x = insets.left + (availW - w) / 2 + val y = insets.top + (availH - h) / 2 + child.setBounds(x, y, w, h) + } + + override fun getPreferredSize(): Dimension { + if (componentCount == 0) return super.getPreferredSize() + val pref = getComponent(0).preferredSize + val insets = getInsets() + return Dimension(pref.width + insets.left + insets.right, pref.height + insets.top + insets.bottom) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 948352b610a..1955144ea5a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -84,11 +84,24 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertTrue(panel.preferredSize.height > 0) } - fun `test content has fixed preferred width`() { + fun `test description width is capped at DESCRIPTION_WIDTH`() { val panel = panel() - assertEquals(com.intellij.util.ui.JBUI.scale(EmptySessionPanel.MAX_WIDTH), panel.contentPreferredSize().width) - assertTrue(panel.contentPreferredSize().height > 0) + assertEquals(com.intellij.util.ui.JBUI.scale(EmptySessionPanel.DESCRIPTION_WIDTH), panel.descriptionPreferredSize().width) + assertEquals(com.intellij.util.ui.JBUI.scale(EmptySessionPanel.DESCRIPTION_WIDTH), panel.descriptionMaximumSize().width) + } + + fun `test description label is centered`() { + val panel = panel() + + assertEquals(javax.swing.SwingConstants.CENTER, panel.welcomeLabelAlignment()) + } + + fun `test show history button has its own preferred width`() { + val panel = panel() + val btn = panel.historyButtonPreferredWidth() + + assertTrue(btn > 0) } fun `test recent sessions are capped at five`() { @@ -98,12 +111,12 @@ class EmptySessionPanelTest : BasePlatformTestCase() { assertEquals(5, panel.recentCount()) } - fun `test explanation uses markdown view`() { + fun `test explanation uses welcome message`() { val panel = panel() assertEquals( "Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.", - panel.explanationMarkdown(), + panel.explanationText(), ) } From 5461f51b3e42c164241894c3281c04cfb6fb2779 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 15:58:38 -0400 Subject: [PATCH 025/111] refactor(jetbrains): remove UiStyle.Space and replace with Gap calls Replace all UiStyle.Space.* constant usages with the corresponding UiStyle.Gap.*() functions throughout the JetBrains plugin UI code. Also fixes a pre-existing double-scale bug in EmptySessionPanel where JBUI.scale(UiStyle.Gap.pad()) was called instead of UiStyle.Gap.pad(). --- .../kilocode/client/session/SessionScroll.kt | 2 +- .../session/history/HistoryListRenderer.kt | 2 +- .../client/session/history/HistoryPanel.kt | 6 +-- .../client/session/ui/ConnectionPanel.kt | 6 +-- .../client/session/ui/EmptySessionPanel.kt | 8 +-- .../client/session/ui/header/ContextBar.kt | 4 +- .../session/ui/header/SessionHeaderPanel.kt | 14 ++--- .../session/ui/mode/ModePickerRenderer.kt | 14 ++--- .../session/ui/model/ModelPickerRenderer.kt | 8 +-- .../client/session/ui/prompt/PromptPanel.kt | 6 +-- .../client/session/views/GenericView.kt | 2 +- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 52 +++++++------------ 12 files changed, 56 insertions(+), 68 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt index 3a4b2ce7cab..5a4a4616a29 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt @@ -71,7 +71,7 @@ internal class SessionScroll( component.verticalScrollBar.addAdjustmentListener { onScroll() } root.addOverlay(jump) { _, child -> val size = child.preferredSize - val gap = JBUI.scale(UiStyle.Space.PAD) + val gap = UiStyle.Gap.pad() Rectangle( host.x + host.width - size.width - gap, host.y + host.height - size.height - gap, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index 8b5bcacc901..b99f592cac3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -82,7 +82,7 @@ internal open class HistoryRenderer( init { isOpaque = true top.isOpaque = true - row.border = JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG) + row.border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) UiStyle.Components.transparent(row) UiStyle.Components.transparent(main) UiStyle.Components.transparent(title) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 88f7299d24b..69ac57689d7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -80,7 +80,7 @@ class HistoryPanel( init { Disposer.register(parent, this) - border = JBUI.Borders.empty(UiStyle.Space.LG) + border = JBUI.Borders.empty(UiStyle.Gap.lg()) more.addActionListener { controller.loadMoreCloud() } bind(localList, controller.local) bind(cloudList, controller.cloud) @@ -165,7 +165,7 @@ class HistoryPanel( cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) addActionListener { nav() } }, BorderLayout.WEST) - border = JBUI.Borders.emptyRight(UiStyle.Space.LG) + border = JBUI.Borders.emptyRight(UiStyle.Gap.lg()) } private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { @@ -176,7 +176,7 @@ class HistoryPanel( viewportBorder = JBUI.Borders.empty() }, BorderLayout.CENTER) footer?.let { - it.border = JBUI.Borders.emptyTop(UiStyle.Space.LG) + it.border = JBUI.Borders.emptyTop(UiStyle.Gap.lg()) add(it, BorderLayout.SOUTH) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 422b6daa9d9..a9a30917d89 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -39,11 +39,11 @@ class ConnectionPanel( } private val header = BorderLayoutPanel().apply { - border = JBUI.Borders.empty(UiStyle.Space.SM, UiStyle.Space.LG, 0, UiStyle.Space.LG) + border = JBUI.Borders.empty(UiStyle.Gap.small(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()) } private val left = BorderLayoutPanel().apply { - layout = UiStyle.Gap.layout(UiStyle.Space.SM) + layout = UiStyle.Gap.layout(UiStyle.Gap.small()) addMouseListener(click) } @@ -76,7 +76,7 @@ class ConnectionPanel( } private val scroll = JBScrollPane(details).apply { - border = JBUI.Borders.empty(0, UiStyle.Space.LG, UiStyle.Space.SM, 0) + border = JBUI.Borders.empty(0, UiStyle.Gap.lg(), UiStyle.Gap.small(), 0) // Match the banner background while retaining platform scroll behavior. isOpaque = false viewport.isOpaque = false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index f7aa3291725..c0509cea688 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -115,7 +115,7 @@ class EmptySessionPanel( } }.apply { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.LG, 0, UiStyle.Space.LG, 0) + border = JBUI.Borders.empty(UiStyle.Gap.lg(), 0, UiStyle.Gap.lg(), 0) add(welcomeLabel, BorderLayout.CENTER) } @@ -125,7 +125,7 @@ class EmptySessionPanel( applyStyle(SessionStyle.current()) setSessions(recents) - val gap = JBUI.scale(UiStyle.Gap.turn()) + val gap = UiStyle.Gap.pad() layout = BorderLayout(0, gap) val logo = JBLabel( @@ -233,8 +233,8 @@ class EmptySessionPanel( private val time = JBLabel() init { - layout = BorderLayout(JBUI.scale(UiStyle.Space.LG), 0) - border = JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG, UiStyle.Space.LG) + layout = BorderLayout(UiStyle.Gap.lg(), 0) + border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) add(title, BorderLayout.CENTER) add(time, BorderLayout.EAST) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt index f053a2d28d8..d1091ebc572 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt @@ -15,14 +15,14 @@ import java.awt.RenderingHints import javax.swing.JComponent import javax.swing.JPanel -internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.inline(), 0)) { +internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) { private val used = JBLabel() private val limit = JBLabel() private val meter = Meter() init { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) add(used, BorderLayout.WEST) add(meter, BorderLayout.CENTER) add(limit, BorderLayout.EAST) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index cfadf9f113e..98964e7af2d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -86,7 +86,7 @@ class SessionHeaderPanel( iconTextGap = UiStyle.Gap.xs() } private val top = BorderLayoutPanel() - private val right = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.inline(), 0)).apply { + private val right = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.md(), 0)).apply { isOpaque = false add(cost) add(context) @@ -95,24 +95,24 @@ class SessionHeaderPanel( } private val tokens = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) add(tokenTitle) - add(Box.createHorizontalStrut(UiStyle.Gap.inline())) + add(Box.createHorizontalStrut(UiStyle.Gap.md())) add(input) add(Box.createHorizontalStrut(UiStyle.Gap.small())) add(output) add(Box.createHorizontalStrut(UiStyle.Gap.small())) add(cacheRead) } - private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.inline(), 0)).apply { + private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0)).apply { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) add(todos) } private val body = JPanel().apply { isOpaque = false layout = BoxLayout(this, BoxLayout.Y_AXIS) - border = JBUI.Borders.empty(UiStyle.Space.SM, 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) add(viewport) add(tokens) add(bar) @@ -178,7 +178,7 @@ class SessionHeaderPanel( super.updateUI() border = JBUI.Borders.compound( JBUI.Borders.customLine(JBUI.CurrentTheme.ToolWindow.borderColor(), 1, 0, 1, 0), - JBUI.Borders.empty(UiStyle.Space.LG, UiStyle.Space.PAD, UiStyle.Space.SM, UiStyle.Space.PAD), + JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad(), UiStyle.Gap.small(), UiStyle.Gap.pad()), ) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt index fbb3ba6c667..6d30dcfe76e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt @@ -57,12 +57,12 @@ internal class ModePickerRenderer( init { UiStyle.Components.transparent(this) UiStyle.Components.transparent(row) - (row.layout as BorderLayout).hgap = UiStyle.Gap.inline() + (row.layout as BorderLayout).hgap = UiStyle.Gap.md() row.border = JBUI.Borders.empty( - UiStyle.Space.MD, - UiStyle.Space.LG, - UiStyle.Space.MD, - UiStyle.Space.LG, + UiStyle.Gap.md(), + UiStyle.Gap.lg(), + UiStyle.Gap.md(), + UiStyle.Gap.lg(), ) body.add(head, BorderLayout.NORTH) body.add(desc, BorderLayout.CENTER) @@ -98,8 +98,8 @@ internal class ModePickerRenderer( badge.border = JBUI.Borders.compound( JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()), JBUI.Borders.compound( - RoundedLineBorder(warn, JBUI.scale(UiStyle.Space.SM)), - JBUI.Borders.empty(0, UiStyle.Space.MD), + RoundedLineBorder(warn, UiStyle.Gap.small()), + JBUI.Borders.empty(0, UiStyle.Gap.md()), ), ) icon.icon = icon(value) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index fea99069103..64889ecc9d8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -100,10 +100,10 @@ internal class ModelPickerRenderer( UiStyle.Components.transparent(provider) UiStyle.Components.transparent(star) row.border = JBUI.Borders.empty( - UiStyle.Space.MD, - UiStyle.Space.LG, - UiStyle.Space.MD, - UiStyle.Space.LG + UiStyle.Space.SM, + UiStyle.Gap.md(), + UiStyle.Gap.lg(), + UiStyle.Gap.md(), + UiStyle.Gap.pad(), ) wrap.setContent(row) add(top, BorderLayout.NORTH) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 0c6436f9759..d0c0eeb130f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -151,7 +151,7 @@ class PromptPanel( val bar = BorderLayoutPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) isOpaque = false - border = JBUI.Borders.emptyTop(UiStyle.Space.SM) + border = JBUI.Borders.emptyTop(UiStyle.Gap.small()) } bar.add(mode) bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) @@ -354,12 +354,12 @@ class PromptPanel( init { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Space.MD, UiStyle.Space.LG) + border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.lg()) } override fun updateUI() { super.updateUI() - border = JBUI.Borders.empty(UiStyle.Space.MD, UiStyle.Space.LG) + border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.lg()) } override fun paintComponent(g: Graphics) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt index fc87d42d0b5..61d15f91f5a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt @@ -20,7 +20,7 @@ class GenericView(content: Generic) : PartView() { private val label = JBLabel("[${content.type}]").apply { foreground = UiStyle.Colors.weak() - border = com.intellij.util.ui.JBUI.Borders.empty(UiStyle.Space.XS, 0) + border = com.intellij.util.ui.JBUI.Borders.empty(UiStyle.Gap.xs(), 0) } init { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index b4ffe3717a4..d8e8670d052 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -37,16 +37,6 @@ object UiStyle { fun toolBodyLimit(): Int = TOOL_BODY } - object Space { - const val XS = 2 - const val SM = 4 - const val MD = 6 - const val LG = 8 - const val PAD = 12 - const val LOGO = 14 - const val RECENT = 28 - } - object Colors { internal const val BORDER_DELTA = 64 internal const val HOVER_ALPHA = 0.35f @@ -127,17 +117,17 @@ object UiStyle { object Insets { fun none(): java.awt.Insets = JBUI.emptyInsets() - fun transcript(): java.awt.Insets = JBUI.insets(Space.PAD, Space.PAD, Space.PAD, Space.PAD) + fun transcript(): java.awt.Insets = JBUI.insets(Gap.pad(), Gap.pad(), Gap.pad(), Gap.pad()) fun userPrompt(): Int = Size.userPromptMin() - fun empty(): Border = JBUI.Borders.empty(Space.PAD) + fun empty(): Border = JBUI.Borders.empty(Gap.pad()) - fun prompt(): Border = JBUI.Borders.empty(Space.LG, Space.PAD, Space.LG, Space.PAD) + fun prompt(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad(), Gap.lg(), Gap.pad()) - fun header(): Border = JBUI.Borders.empty(Space.LG, Space.LG) + fun header(): Border = JBUI.Borders.empty(Gap.lg(), Gap.lg()) - fun body(): Border = JBUI.Borders.empty(Space.LG, Space.PAD) + fun body(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) } object Borders { @@ -149,11 +139,11 @@ object UiStyle { fun warning(): Border = JBUI.Borders.customLine(Colors.warning(), 1) - fun picker(): Border = JBUI.Borders.empty(Space.XS, Space.LG) + fun picker(): Border = JBUI.Borders.empty(Gap.xs(), Gap.lg()) fun user(): Border = JBUI.Borders.compound( - RoundedLineBorder(Colors.line(), JBUI.scale(Space.LG)), - JBUI.Borders.empty(Space.LG, Space.PAD), + RoundedLineBorder(Colors.line(), Gap.lg()), + JBUI.Borders.empty(Gap.lg(), Gap.pad()), )!! fun assistant(): Border = JBUI.Borders.empty() @@ -164,13 +154,13 @@ object UiStyle { const val LINES = 15 const val REASONING_LINES = 5 - fun layout(): BorderLayout = Gap.layout(Space.MD) + fun layout(): BorderLayout = Gap.layout(Gap.md()) fun groupGap(): Int = Gap.small() - fun headerInsets(): Border = JBUI.Borders.empty(Space.LG, Space.PAD) + fun headerInsets(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) - fun bodyInsets(): Border = JBUI.Borders.empty(Space.LG, Space.PAD) + fun bodyInsets(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) fun border(): Border = Borders.card() @@ -182,36 +172,34 @@ object UiStyle { object Dock { fun banner(): Border = JBUI.Borders.compound( JBUI.Borders.customLineTop(Colors.line()), - JBUI.Borders.empty(Space.SM, Space.LG, 0, Space.LG), + JBUI.Borders.empty(Gap.small(), Gap.lg(), 0, Gap.lg()), )!! fun neutral(): Border = JBUI.Borders.compound( JBUI.Borders.customLine(Colors.line(), 1), - JBUI.Borders.empty(Space.LG, Space.PAD), + JBUI.Borders.empty(Gap.lg(), Gap.pad()), )!! fun warning(): Border = JBUI.Borders.compound( Borders.warning(), - JBUI.Borders.empty(Space.LG, Space.PAD), + JBUI.Borders.empty(Gap.lg(), Gap.pad()), )!! } object Gap { - fun xs() = JBUI.scale(Space.XS) + fun xs() = JBUI.scale(2) - fun inline() = JBUI.scale(Space.MD) + fun md() = JBUI.scale(6) - fun regular() = JBUI.scale(Space.LG) + fun lg() = JBUI.scale(8) - fun small() = JBUI.scale(Space.SM) + fun small() = JBUI.scale(4) - fun turn() = JBUI.scale(Space.PAD) - - fun part() = JBUI.scale(Space.SM) + fun pad() = JBUI.scale(12) fun scroll() = JBUI.scale(Size.SCROLL) - fun layout(gap: Int = Space.LG) = BorderLayout(JBUI.scale(gap), 0) + fun layout(gap: Int = lg()) = BorderLayout(gap, 0) } object Buttons { From 2448e1f0695783c6516ef9aac2ffc894faffa1c2 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 16:02:07 -0400 Subject: [PATCH 026/111] refactor(jetbrains): update remaining Gap alias usages after Space removal Update ProgressPanel, SessionLayout, and CompactionView to use the canonical Gap method names (md, small, lg) now that the old aliases (inline, part, regular) were removed along with UiStyle.Space. --- .../kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt | 2 +- .../kotlin/ai/kilocode/client/session/ui/SessionLayout.kt | 4 ++-- .../kotlin/ai/kilocode/client/session/views/CompactionView.kt | 2 +- .../src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt index d7c3357d1cb..aa4ebf4a25d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt @@ -32,7 +32,7 @@ class ProgressPanel( init { isOpaque = false isVisible = false - layout = FlowLayout(FlowLayout.LEFT, UiStyle.Gap.inline(), 0) + layout = FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0) applyStyle(SessionStyle.current()) add(JBLabel(AnimatedIcon.Default())) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 25c22ee7689..de0a4fffd95 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -29,7 +29,7 @@ import java.awt.LayoutManager * the panel width and the layout always has a valid width to work with. */ class SessionLayout( - private val gap: Int = UiStyle.Gap.part(), + private val gap: Int = UiStyle.Gap.small(), private val pad: Insets = UiStyle.Insets.none(), ) : LayoutManager { @@ -103,7 +103,7 @@ class SessionLayout( * [SessionLayout] a valid width to measure against. */ open class SessionLayoutPanel( - gap: Int = UiStyle.Gap.part(), + gap: Int = UiStyle.Gap.small(), pad: Insets = UiStyle.Insets.none(), ) : BorderLayoutPanel(), javax.swing.Scrollable { init { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt index cfe1b5e39b9..aefb0243f90 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt @@ -29,7 +29,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par private val text = JBLabel(KiloBundle.message("session.part.compaction")).apply { foreground = UiStyle.Colors.weak() horizontalAlignment = SwingConstants.CENTER - border = JBUI.Borders.empty(0, UiStyle.Gap.regular()) + border = JBUI.Borders.empty(0, UiStyle.Gap.lg()) } init { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt index 025fb5787c7..8a40caf7337 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewTest.kt @@ -414,4 +414,5 @@ class MdViewTest : BasePlatformTestCase() { view.resetStyles() assertTrue(view.html().contains("")) } + } From c9c1f999abcc48f3055e582918a459a9f13d8b37 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 16:02:12 -0400 Subject: [PATCH 027/111] chore: bump plugin lock files to @kilocode/plugin 7.2.40 --- .kilo/package-lock.json | 46 ++++++++++++++++++------------------- .kilocode/package-lock.json | 46 ++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.kilo/package-lock.json b/.kilo/package-lock.json index ce556edce27..f288fc518c7 100644 --- a/.kilo/package-lock.json +++ b/.kilo/package-lock.json @@ -5,22 +5,22 @@ "packages": { "": { "dependencies": { - "@kilocode/plugin": "7.2.31" + "@kilocode/plugin": "7.2.40" } }, "node_modules/@kilocode/plugin": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz", - "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==", + "version": "7.2.40", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.40.tgz", + "integrity": "sha512-m0/5LnQdKW+FJnv9sVbri4Cqw7vq2jy5xS7hdkWO0mbyPkjROcrXklnwuHk3bi32F5InnO4DUyKrkqfMFoMDgQ==", "license": "MIT", "dependencies": { - "@kilocode/sdk": "7.2.31", - "effect": "4.0.0-beta.48", + "@kilocode/sdk": "7.2.40", + "effect": "4.0.0-beta.57", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" + "@opentui/core": ">=0.1.105", + "@opentui/solid": ">=0.1.105" }, "peerDependenciesMeta": { "@opentui/core": { @@ -32,9 +32,9 @@ } }, "node_modules/@kilocode/sdk": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz", - "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==", + "version": "7.2.40", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.40.tgz", + "integrity": "sha512-I/AWGE2EnM26/lWD/gf9T8QxuCB/YPZBQJ14M8D/SN4+rZKonyqz2Q6iIgQWqcX3B4LQLMWNFNh9rdvPaHN66Q==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -149,9 +149,9 @@ } }, "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "version": "4.0.0-beta.57", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.57.tgz", + "integrity": "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -216,9 +216,9 @@ "license": "Apache-2.0" }, "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -323,9 +323,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -351,9 +351,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/.kilocode/package-lock.json b/.kilocode/package-lock.json index 7a0e27b435c..22942939cd1 100644 --- a/.kilocode/package-lock.json +++ b/.kilocode/package-lock.json @@ -5,22 +5,22 @@ "packages": { "": { "dependencies": { - "@kilocode/plugin": "7.2.31" + "@kilocode/plugin": "7.2.40" } }, "node_modules/@kilocode/plugin": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz", - "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==", + "version": "7.2.40", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.40.tgz", + "integrity": "sha512-m0/5LnQdKW+FJnv9sVbri4Cqw7vq2jy5xS7hdkWO0mbyPkjROcrXklnwuHk3bi32F5InnO4DUyKrkqfMFoMDgQ==", "license": "MIT", "dependencies": { - "@kilocode/sdk": "7.2.31", - "effect": "4.0.0-beta.48", + "@kilocode/sdk": "7.2.40", + "effect": "4.0.0-beta.57", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" + "@opentui/core": ">=0.1.105", + "@opentui/solid": ">=0.1.105" }, "peerDependenciesMeta": { "@opentui/core": { @@ -32,9 +32,9 @@ } }, "node_modules/@kilocode/sdk": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz", - "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==", + "version": "7.2.40", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.40.tgz", + "integrity": "sha512-I/AWGE2EnM26/lWD/gf9T8QxuCB/YPZBQJ14M8D/SN4+rZKonyqz2Q6iIgQWqcX3B4LQLMWNFNh9rdvPaHN66Q==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -149,9 +149,9 @@ } }, "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "version": "4.0.0-beta.57", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.57.tgz", + "integrity": "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -216,9 +216,9 @@ "license": "Apache-2.0" }, "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -323,9 +323,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -351,9 +351,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" From cf22a6ede8f6a0b39ef209d0012499628bb260d8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 16:04:06 -0400 Subject: [PATCH 028/111] refactor(jetbrains): rename SessionStyle to SessionEditorStyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename SessionStyle → SessionEditorStyle and SessionStyleTarget → SessionEditorStyleTarget throughout to better reflect that this type carries editor-derived styling (fonts, colors, scheme) applied to session UI components. --- .../ai/kilocode/client/session/SessionScroll.kt | 12 ++++++------ .../kotlin/ai/kilocode/client/session/SessionUi.kt | 14 +++++++------- .../client/session/history/HistoryPanel.kt | 4 ++-- .../client/session/ui/EmptySessionPanel.kt | 8 ++++---- .../ai/kilocode/client/session/ui/LoadingPanel.kt | 6 +++--- .../ai/kilocode/client/session/ui/ProgressPanel.kt | 6 +++--- .../ui/{SessionStyle.kt => SessionEditorStyle.kt} | 12 ++++++------ .../client/session/ui/SessionMessageListPanel.kt | 6 +++--- .../client/session/ui/header/ContextBar.kt | 4 ++-- .../client/session/ui/header/SessionHeaderPanel.kt | 10 +++++----- .../client/session/ui/prompt/PromptPanel.kt | 10 +++++----- .../client/session/views/CompactionView.kt | 6 +++--- .../kilocode/client/session/views/GenericView.kt | 6 +++--- .../kilocode/client/session/views/MessageView.kt | 12 ++++++------ .../ai/kilocode/client/session/views/PartView.kt | 8 ++++---- .../kilocode/client/session/views/ReasoningView.kt | 8 ++++---- .../ai/kilocode/client/session/views/TextView.kt | 6 +++--- .../ai/kilocode/client/session/views/ToolView.kt | 8 ++++---- .../ai/kilocode/client/session/views/TurnView.kt | 12 ++++++------ .../client/session/SessionSidePanelManagerTest.kt | 2 +- .../kilocode/client/session/ui/PromptPanelTest.kt | 6 +++--- ...ssionStyleTest.kt => SessionEditorStyleTest.kt} | 10 +++++----- .../session/ui/SessionMessageListPanelTest.kt | 4 ++-- .../session/ui/header/SessionHeaderPanelTest.kt | 8 ++++---- .../client/session/views/ReasoningViewTest.kt | 12 ++++++------ .../kilocode/client/session/views/TextViewTest.kt | 6 +++--- .../kilocode/client/session/views/ToolViewTest.kt | 12 ++++++------ 27 files changed, 109 insertions(+), 109 deletions(-) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/{SessionStyle.kt => SessionEditorStyle.kt} (91%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/{SessionStyleTest.kt => SessionEditorStyleTest.kt} (86%) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt index 5a4a4616a29..cc4bd60e006 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt @@ -3,8 +3,8 @@ package ai.kilocode.client.session import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.SessionRootPanel -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.IconLoader @@ -50,7 +50,7 @@ internal class SessionScroll( internal val jump: JBLabel val view: JComponent? get() = component.viewport.view as? JComponent - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() private var tail = true private var auto = false private var opening = false @@ -83,7 +83,7 @@ internal class SessionScroll( fun show(panel: JPanel) { if (component.viewport.view === panel) return - (panel as? SessionStyleTarget)?.applyStyle(style) + (panel as? SessionEditorStyleTarget)?.applyStyle(style) component.viewport.setView(panel) component.repaint() updateJump() @@ -128,12 +128,12 @@ internal class SessionScroll( updateJump() } - fun applyStyle(style: SessionStyle) { + fun applyStyle(style: SessionEditorStyle) { this.style = style jump.icon = patchedIcon(ICON) messages.applyStyle(style) val view = component.viewport.view - if (view !== messages) (view as? SessionStyleTarget)?.applyStyle(style) + if (view !== messages) (view as? SessionEditorStyleTarget)?.applyStyle(style) refresh() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 91a9e18ed4a..9e33d856046 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -17,8 +17,8 @@ import ai.kilocode.client.session.ui.QuestionPanel import ai.kilocode.client.session.ui.SessionRootPanel import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.header.SessionHeaderPanel -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.session.update.EVENT_FLUSH_MS import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.update.SessionControllerEvent @@ -53,7 +53,7 @@ class SessionUi( ref: SessionRef? = null, displayMs: Long = SessionController.DISPLAY_DELAY_MS, private val manager: SessionManager? = null, -) : JPanel(BorderLayout()), Disposable, SessionStyleTarget { +) : JPanel(BorderLayout()), Disposable, SessionEditorStyleTarget { companion object { private val LOG = KiloLog.create(SessionUi::class.java) @@ -102,7 +102,7 @@ class SessionUi( private lateinit var prompt: PromptPanel private lateinit var load: LoadingPanel - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() init { buildUi() @@ -264,12 +264,12 @@ class SessionUi( val bus = ApplicationManager.getApplication().messageBus.connect(this) bus.subscribe(EditorColorsManager.TOPIC, EditorColorsListener { ApplicationManager.getApplication().invokeLater { - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) } }) bus.subscribe(LafManagerListener.TOPIC, LafManagerListener { ApplicationManager.getApplication().invokeLater { - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) } }) } @@ -345,7 +345,7 @@ class SessionUi( root.repaint() } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style load.applyStyle(style) header.applyStyle(style) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 69ac57689d7..332bce6b754 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.LoadingPanel -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ide.ui.LafManagerListener @@ -123,7 +123,7 @@ class HistoryPanel( SwingUtilities.updateComponentTreeUI(this) SwingUtilities.updateComponentTreeUI(localPanel) SwingUtilities.updateComponentTreeUI(cloudPanel) - load.applyStyle(SessionStyle.current()) + load.applyStyle(SessionEditorStyle.current()) updateRenderer(localList) updateRenderer(cloudList) sync() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index c0509cea688..18decafb5f1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -48,7 +48,7 @@ class EmptySessionPanel( private val controller: SessionController, recents: List, private val history: () -> Unit = {}, -) : BorderLayoutPanel(), Disposable, SessionStyleTarget { +) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget { companion object { internal val LIMIT = UiStyle.Size.LIMIT @@ -59,7 +59,7 @@ class EmptySessionPanel( private val model = DefaultListModel() private var hover = -1 - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() private val recentTitle = JBLabel(KiloBundle.message("session.empty.recent")).apply { foreground = UIUtil.getContextHelpForeground() @@ -122,7 +122,7 @@ class EmptySessionPanel( init { Disposer.register(parent, this) isOpaque = false - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) setSessions(recents) val gap = UiStyle.Gap.pad() @@ -304,7 +304,7 @@ class EmptySessionPanel( // no-op } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style welcomeLabel.font = style.uiFont recentTitle.font = style.smallUiFont diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt index 276bd7c3152..f8ca17f30da 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt @@ -6,16 +6,16 @@ import com.intellij.util.ui.Centerizer import java.awt.BorderLayout import javax.swing.JPanel -class LoadingPanel : JPanel(BorderLayout()), SessionStyleTarget { +class LoadingPanel : JPanel(BorderLayout()), SessionEditorStyleTarget { private val label = JBLabel(KiloBundle.message("session.empty.loading")) init { isOpaque = false add(Centerizer(label, Centerizer.TYPE.BOTH), BorderLayout.CENTER) - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { label.font = style.uiFont revalidate() repaint() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt index aa4ebf4a25d..a57e91dec7b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt @@ -23,7 +23,7 @@ import java.awt.FlowLayout class ProgressPanel( model: SessionModel, parent: Disposable, -) : SessionLayoutPanel(), SessionStyleTarget { +) : SessionLayoutPanel(), SessionEditorStyleTarget { private val label = JBLabel().apply { foreground = UiStyle.Colors.weak() @@ -33,7 +33,7 @@ class ProgressPanel( isOpaque = false isVisible = false layout = FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0) - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) add(JBLabel(AnimatedIcon.Default())) add(label) @@ -59,7 +59,7 @@ class ProgressPanel( repaint() } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { label.font = style.uiFont revalidate() repaint() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt similarity index 91% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionStyle.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt index 550b3741746..617be4e1b42 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt @@ -10,7 +10,7 @@ import java.awt.Font import kotlin.math.roundToInt /** Snapshot of editor-derived transcript styling applied to live session components. */ -data class SessionStyle( +data class SessionEditorStyle( val editorScheme: EditorColorsScheme, val editorFamily: String, val editorSize: Int, @@ -30,7 +30,7 @@ data class SessionStyle( } companion object { - fun current(): SessionStyle { + fun current(): SessionEditorStyle { val scheme = EditorColorsManager.getInstance().globalScheme return create(scheme, scheme.editorFontName, scheme.editorFontSize) } @@ -39,11 +39,11 @@ data class SessionStyle( scheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme, family: String = scheme.editorFontName, size: Int = scheme.editorFontSize, - ): SessionStyle { + ): SessionEditorStyle { val small = scaledSize(size, JBFont.small()) val ui = JBUI.Fonts.label().deriveFont(size.toFloat()) val smallUi = JBFont.small().deriveFont(small.toFloat()) - return SessionStyle( + return SessionEditorStyle( editorScheme = scheme, editorFamily = family, editorSize = size, @@ -67,6 +67,6 @@ data class SessionStyle( } /** Implemented by session components that can update styling in place. */ -interface SessionStyleTarget { - fun applyStyle(style: SessionStyle) +interface SessionEditorStyleTarget { + fun applyStyle(style: SessionEditorStyle) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index 44afdeda402..f6de23d94a9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -31,12 +31,12 @@ import com.intellij.openapi.Disposable class SessionMessageListPanel( private val model: SessionModel, parent: Disposable, -) : SessionLayoutPanel(UiStyle.Card.groupGap(), UiStyle.Insets.transcript()), SessionStyleTarget { +) : SessionLayoutPanel(UiStyle.Card.groupGap(), UiStyle.Insets.transcript()), SessionEditorStyleTarget { private val turnViews = LinkedHashMap() private val msgToTurn = HashMap() private val msgToView = HashMap() - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() /** Progress footer — always the last child inside the scroll. */ val progress = ProgressPanel(model, parent) @@ -240,7 +240,7 @@ class SessionMessageListPanel( repaint() } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style for (view in turnViews.values) view.applyStyle(style) progress.applyStyle(style) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt index d1091ebc572..f1a97e05c93 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.ui.header import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.ContextUsage -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI @@ -40,7 +40,7 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) { repaint() } - fun applyStyle(style: SessionStyle) { + fun applyStyle(style: SessionEditorStyle) { background = style.editorBackground foreground = style.editorForeground meter.background = style.editorBackground diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index 98964e7af2d..64c4f00421c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -3,8 +3,8 @@ package ai.kilocode.client.session.ui.header import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.SessionHeaderSnapshot import ai.kilocode.client.session.model.SessionModelEvent -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.TokensDto @@ -33,7 +33,7 @@ import javax.swing.SwingUtilities class SessionHeaderPanel( private val controller: SessionController, parent: Disposable, -) : BorderLayoutPanel(), SessionStyleTarget { +) : BorderLayoutPanel(), SessionEditorStyleTarget { companion object { private val COMPRESS_ICON: Icon = IconLoader.getIcon("/icons/compress.svg", SessionHeaderPanel::class.java) @@ -118,7 +118,7 @@ class SessionHeaderPanel( add(bar) add(todoRow) } - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() init { isOpaque = true @@ -212,7 +212,7 @@ class SessionHeaderPanel( refresh() } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style background = style.editorBackground foreground = style.editorForeground diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index d0c0eeb130f..ca12335b4ca 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -4,8 +4,8 @@ import ai.kilocode.client.actions.SendPromptAction import ai.kilocode.client.actions.StopSessionAction import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.ReasoningPicker -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker import ai.kilocode.client.ui.UiStyle @@ -58,7 +58,7 @@ class PromptPanel( private val project: Project, private val onSend: (String) -> Unit, private val onAbort: () -> Unit, -) : BorderLayoutPanel(), SessionStyleTarget, SendPromptContext { +) : BorderLayoutPanel(), SessionEditorStyleTarget, SendPromptContext { companion object { private val LOG = KiloLog.create(PromptPanel::class.java) @@ -72,7 +72,7 @@ class PromptPanel( val model = ModelPicker() val reasoning = ReasoningPicker() var onReset: () -> Unit = {} - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() private val shell = PromptShell() private var bus: MessageBusConnection? = null @@ -206,7 +206,7 @@ class PromptPanel( internal val defaultFocusedComponent: JComponent get() = editor - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style editor.font = style.transcriptFont editor.getEditor(false)?.let(style::applyToEditor) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt index aefb0243f90..b397a5eb4d3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt @@ -3,7 +3,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Compaction import ai.kilocode.client.session.model.Content import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import java.awt.BorderLayout @@ -35,7 +35,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par init { layout = BorderLayout() isOpaque = false - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) val line = { JPanel().apply { background = UiStyle.Colors.line() @@ -65,7 +65,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par override fun update(content: Content) {} // compaction has no mutable state - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { if (text.font == style.smallUiFont) return text.font = style.smallUiFont revalidate() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt index 61d15f91f5a..c6950be3b19 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Generic -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import java.awt.BorderLayout @@ -26,7 +26,7 @@ class GenericView(content: Generic) : PartView() { init { layout = BorderLayout() isOpaque = false - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) add(label, BorderLayout.CENTER) } @@ -35,7 +35,7 @@ class GenericView(content: Generic) : PartView() { /** Exposed for tests. */ fun labelText(): String = label.text - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { if (label.font == style.smallUiFont) return label.font = style.smallUiFont revalidate() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index 17322436fb2..7f2e26e06b9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -4,8 +4,8 @@ import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.model.StepFinish import ai.kilocode.client.session.ui.SessionView -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.ui.UiStyle /** @@ -21,10 +21,10 @@ import ai.kilocode.client.ui.UiStyle */ class MessageView( val msg: Message, - private var style: SessionStyle = SessionStyle.current(), -) : ai.kilocode.client.session.ui.SessionLayoutPanel(UiStyle.Card.groupGap()), SessionStyleTarget, SessionView { + private var style: SessionEditorStyle = SessionEditorStyle.current(), +) : ai.kilocode.client.session.ui.SessionLayoutPanel(UiStyle.Card.groupGap()), SessionEditorStyleTarget, SessionView { - constructor(msg: Message) : this(msg, SessionStyle.current()) + constructor(msg: Message) : this(msg, SessionEditorStyle.current()) val role: String get() = msg.info.role @@ -97,7 +97,7 @@ class MessageView( /** Compact dump for test assertions. */ fun dump(): String = parts.values.joinToString(", ") { it.dumpLabel() } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style for (view in parts.values) view.applyStyle(style) refresh() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt index 330f099ddb5..c6be3209eb4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt @@ -1,8 +1,8 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import javax.swing.JPanel /** @@ -14,7 +14,7 @@ import javax.swing.JPanel * * All methods must be called on the EDT. */ -abstract class PartView : JPanel(), SessionStyleTarget { +abstract class PartView : JPanel(), SessionEditorStyleTarget { /** Stable [Content.id] this renderer was created for. */ abstract val contentId: String @@ -32,7 +32,7 @@ abstract class PartView : JPanel(), SessionStyleTarget { */ open fun appendDelta(delta: String) {} - override fun applyStyle(style: SessionStyle) {} + override fun applyStyle(style: SessionEditorStyle) {} /** Readable name for test dumps. */ open fun dumpLabel(): String = javaClass.simpleName diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index 4e2f143e1ae..776eaca47dc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -5,7 +5,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Reasoning -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView import com.intellij.icons.AllIcons @@ -56,7 +56,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { foreground = UiStyle.Colors.weak() } - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() private var source = reasoning.content.toString() private val click = object : MouseAdapter() { @@ -95,7 +95,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { it.addMouseListener(mouse) } - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) md.opaque = false md.set(source) body.add(md.component, BorderLayout.CENTER) @@ -146,7 +146,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { internal fun bodyCreated() = true - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style var changed = false if (title.font != style.smallEditorFont) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt index 1954aaa8a48..2aa7f9fb075 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Text -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.md.MdView import java.awt.BorderLayout @@ -20,7 +20,7 @@ class TextView(text: Text) : PartView() { init { layout = BorderLayout() isOpaque = false - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) add(md.component, BorderLayout.CENTER) if (text.content.isNotEmpty()) md.set(text.content.toString()) } @@ -40,7 +40,7 @@ class TextView(text: Text) : PartView() { /** Current markdown source — used by tests to assert rendered content. */ fun markdown(): String = md.markdown() - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { val changed = md.font != style.transcriptFont || md.codeFont != style.editorFamily if (md.font != style.transcriptFont) md.font = style.transcriptFont if (md.codeFont != style.editorFamily) md.codeFont = style.editorFamily diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index 70d1e13b9ce..bf3d68d8fcf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -6,7 +6,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ui.components.JBLabel @@ -34,7 +34,7 @@ class ToolView(tool: Tool) : PartView() { override val contentId: String = tool.id private var item = tool - private var style = SessionStyle.current() + private var style = SessionEditorStyle.current() private val root = JPanel(BorderLayout()).apply { isOpaque = true @@ -114,7 +114,7 @@ class ToolView(tool: Tool) : PartView() { it.addMouseListener(click) } text.text = preview(item) - applyStyle(SessionStyle.current()) + applyStyle(SessionEditorStyle.current()) add(root, BorderLayout.CENTER) sync() } @@ -175,7 +175,7 @@ class ToolView(tool: Tool) : PartView() { internal fun bodyCreated() = true - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style var changed = false changed = setFont(title, style.boldEditorFont) || changed diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index 2f132f7f084..0c7a6a4369b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -2,8 +2,8 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.ui.SessionLayoutPanel -import ai.kilocode.client.session.ui.SessionStyle -import ai.kilocode.client.session.ui.SessionStyleTarget +import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.ui.UiStyle /** @@ -17,10 +17,10 @@ import ai.kilocode.client.ui.UiStyle */ class TurnView( val id: String, - private var style: SessionStyle = SessionStyle.current(), -) : SessionLayoutPanel(UiStyle.Card.groupGap()), SessionStyleTarget { + private var style: SessionEditorStyle = SessionEditorStyle.current(), +) : SessionLayoutPanel(UiStyle.Card.groupGap()), SessionEditorStyleTarget { - constructor(id: String) : this(id, SessionStyle.current()) + constructor(id: String) : this(id, SessionEditorStyle.current()) private val messages = LinkedHashMap() @@ -53,7 +53,7 @@ class TurnView( /** Compact dump for test assertions. */ fun dump(): String = messages.entries.joinToString(", ") { (id, mv) -> "${mv.role}#$id" } - override fun applyStyle(style: SessionStyle) { + override fun applyStyle(style: SessionEditorStyle) { this.style = style for (view in messages.values) view.applyStyle(style) revalidate() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 205c0eb244c..70c38163543 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -184,7 +184,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1")) val first = active(manager) as SessionUi manager.openSession(session("ses_2")) - val style = ai.kilocode.client.session.ui.SessionStyle.create(family = "Courier New", size = 24) + val style = ai.kilocode.client.session.ui.SessionEditorStyle.create(family = "Courier New", size = 24) first.applyStyle(style) manager.openSession(session("ses_1")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index 253e6758e8a..249accefe48 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -14,7 +14,7 @@ import javax.swing.SwingUtilities class PromptPanelTest : BasePlatformTestCase() { fun `test prompt input uses editor font settings`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val panel = PromptPanel(project, {}, {}) val font = panel.inputFont() @@ -23,7 +23,7 @@ class PromptPanelTest : BasePlatformTestCase() { } fun `test prompt input uses editor background`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val panel = PromptPanel(project, {}, {}) assertEquals(style.editorScheme.defaultBackground, panel.defaultFocusedComponent.background) @@ -31,7 +31,7 @@ class PromptPanelTest : BasePlatformTestCase() { fun `test applyStyle updates prompt input and height`() { val panel = PromptPanel(project, {}, {}) - val style = SessionStyle.create(family = "Courier New", size = 26) + val style = SessionEditorStyle.create(family = "Courier New", size = 26) panel.applyStyle(style) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt similarity index 86% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionStyleTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt index 35ba549f76e..fa2a3aee7a8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt @@ -5,11 +5,11 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase import java.awt.Font @Suppress("UnstableApiUsage") -class SessionStyleTest : BasePlatformTestCase() { +class SessionEditorStyleTest : BasePlatformTestCase() { fun `test transcript font uses editor settings`() { val scheme = EditorColorsManager.getInstance().globalScheme - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val font = style.transcriptFont assertEquals(scheme.editorFontName, font.name) @@ -20,7 +20,7 @@ class SessionStyleTest : BasePlatformTestCase() { } fun `test bold editor font uses editor family and size`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val font = style.boldEditorFont assertEquals(style.editorFamily, font.name) @@ -29,7 +29,7 @@ class SessionStyleTest : BasePlatformTestCase() { } fun `test small editor font uses editor family with smaller editor-derived size`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val font = style.smallEditorFont assertEquals(style.editorFamily, font.name) @@ -37,7 +37,7 @@ class SessionStyleTest : BasePlatformTestCase() { } fun `test custom style derives fonts from supplied editor baseline`() { - val style = SessionStyle.create(family = "Courier New", size = 22) + val style = SessionEditorStyle.create(family = "Courier New", size = 22) assertEquals("Courier New", style.editorFamily) assertEquals(22, style.editorSize) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index 40085e5b4d9..821567ec5ee 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -232,7 +232,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { val message = panel.findMessage("a1")!! val text = message.part("p1") as TextView val comp = text.md.component - val style = SessionStyle.create(family = "Courier New", size = 24) + val style = SessionEditorStyle.create(family = "Courier New", size = 24) panel.applyStyle(style) @@ -246,7 +246,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { fun `test new content after applyStyle uses queued style`() { model.upsertMessage(msg("a1", "assistant")) - val style = SessionStyle.create(family = "Courier New", size = 25) + val style = SessionEditorStyle.create(family = "Courier New", size = 25) panel.applyStyle(style) model.updateContent("a1", part("p1", "a1", "text", text = "hello")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt index e36e31ed254..c4918205737 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt @@ -5,7 +5,7 @@ import ai.kilocode.client.session.model.StepFinish import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.ToolKind -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.session.update.SessionControllerTestBase import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.MessageDto @@ -51,7 +51,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { fun `test shows populated session header`() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) - val style = SessionStyle.current() + val style = SessionEditorStyle.current() assertTrue(panel.isVisible) assertTrue(panel.isExpanded()) @@ -108,7 +108,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { fun `test apply style updates header colors`() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) - val style = SessionStyle.current().copy( + val style = SessionEditorStyle.current().copy( editorForeground = Color(1, 2, 3), editorBackground = Color(4, 5, 6), ) @@ -129,7 +129,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { fun `test expanded body shows timeline context and token metrics`() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val body = panel.bodyPanel() val timeline = panel.timelinePanel() val bar = panel.contextBar() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt index c3d62fd07c8..e51781c9f9e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Reasoning -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import javax.swing.ScrollPaneConstants @@ -127,7 +127,7 @@ class ReasoningViewTest : BasePlatformTestCase() { } fun `test reasoning markdown uses editor font settings`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val view = ReasoningView(reasoning("p1", done = true, text = "one\ntwo\nthree\nfour")) assertSmallItalicSheet(view.md.overrideSheet(), style) @@ -135,7 +135,7 @@ class ReasoningViewTest : BasePlatformTestCase() { } fun `test reasoning header uses smaller editor-derived font`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val view = ReasoningView(reasoning("p1", done = true, text = "one")) val font = view.headerFont() @@ -146,7 +146,7 @@ class ReasoningViewTest : BasePlatformTestCase() { fun `test applyStyle updates reasoning in place`() { val view = ReasoningView(reasoning("p1", done = true, text = "one\ntwo\nthree\nfour")) val component = view.md.component - val style = SessionStyle.create(family = "Courier New", size = 24) + val style = SessionEditorStyle.create(family = "Courier New", size = 24) view.applyStyle(style) @@ -163,12 +163,12 @@ class ReasoningViewTest : BasePlatformTestCase() { assertTrue(view.preferredSize.height > 0) } - private fun assertEditorSheet(sheet: String, style: SessionStyle) { + private fun assertEditorSheet(sheet: String, style: SessionEditorStyle) { assertTrue(sheet.contains(style.editorFamily)) assertTrue(sheet.contains("${style.editorSize}pt")) } - private fun assertSmallItalicSheet(sheet: String, style: SessionStyle) { + private fun assertSmallItalicSheet(sheet: String, style: SessionEditorStyle) { assertTrue(sheet.contains(style.editorFamily)) assertFalse(sheet.contains("${style.editorSize}pt")) assertTrue(sheet.contains("font-style: italic")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt index 3bd72a98a03..0d3938a5bee 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Text -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase /** @@ -78,7 +78,7 @@ class TextViewTest : BasePlatformTestCase() { } fun `test markdown uses editor font settings`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val view = TextView(Text("p1")) val sheet = view.md.overrideSheet() @@ -89,7 +89,7 @@ class TextViewTest : BasePlatformTestCase() { fun `test applyStyle updates markdown in place`() { val view = TextView(Text("p1")) val component = view.md.component - val style = SessionStyle.create(family = "Courier New", size = 23) + val style = SessionEditorStyle.create(family = "Courier New", size = 23) view.applyStyle(style) val sheet = view.md.overrideSheet() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt index e5579fda720..36fd940e3e7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt @@ -4,7 +4,7 @@ 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.model.toolKind -import ai.kilocode.client.session.ui.SessionStyle +import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import javax.swing.ScrollPaneConstants @@ -208,7 +208,7 @@ class ToolViewTest : BasePlatformTestCase() { } fun `test bash output uses editor font settings`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val view = ToolView(tool("p1", "bash", ToolExecState.COMPLETED)) assertEditorFont(view.bodyFont(), style) @@ -219,7 +219,7 @@ class ToolViewTest : BasePlatformTestCase() { } fun `test tool header uses editor-derived fonts`() { - val style = SessionStyle.current() + val style = SessionEditorStyle.current() val view = ToolView(tool("p1", "bash", ToolExecState.COMPLETED)) assertEditorFont(view.titleFont(), style) @@ -230,7 +230,7 @@ class ToolViewTest : BasePlatformTestCase() { fun `test applyStyle updates tool fonts in place`() { val view = ToolView(tool("p1", "bash", ToolExecState.COMPLETED)) - val style = SessionStyle.create(family = "Courier New", size = 25) + val style = SessionEditorStyle.create(family = "Courier New", size = 25) view.applyStyle(style) @@ -329,12 +329,12 @@ class ToolViewTest : BasePlatformTestCase() { private fun tool(id: String, name: String, state: ToolExecState, title: String? = null): Tool = Tool(id, name, toolKind(name)).also { it.state = state; it.title = title } - private fun assertEditorFont(font: java.awt.Font, style: SessionStyle) { + private fun assertEditorFont(font: java.awt.Font, style: SessionEditorStyle) { assertEquals(style.editorFamily, font.name) assertEquals(style.editorSize, font.size) } - private fun assertSmallEditorFont(font: java.awt.Font, style: SessionStyle) { + private fun assertSmallEditorFont(font: java.awt.Font, style: SessionEditorStyle) { assertEquals(style.editorFamily, font.name) assertTrue(font.size < style.editorSize) } From 3930fb61cd89a963b47be95b5db600be13812f62 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 16:59:18 -0400 Subject: [PATCH 029/111] refactor(jetbrains): move timeline styles into session ui --- .../client/session/ui/SessionUiStyle.kt | 18 +++++++++++++++++ .../client/session/ui/header/TimelinePanel.kt | 20 +++++++++---------- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 8 -------- 3 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt new file mode 100644 index 00000000000..128807347a4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -0,0 +1,18 @@ +package ai.kilocode.client.session.ui + +import com.intellij.ui.JBColor +import com.intellij.util.ui.UIUtil +import java.awt.Color + +/** Static UI tokens and helpers for session-specific Swing surfaces. */ +object SessionUiStyle { + object Timeline { + val read: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) + val write: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) + val tool: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc)) + val success: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground()) + val error: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71)) + val text: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d)) + val step: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d)) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt index 3906138b5de..822b02f19a6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt @@ -9,7 +9,7 @@ import ai.kilocode.client.session.model.TimelineItem import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.ToolKind -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.session.ui.SessionUiStyle import com.intellij.util.ui.JBUI import java.awt.Color import java.awt.Dimension @@ -138,14 +138,14 @@ internal class TimelinePanel : JPanel() { private fun color(item: TimelineItem): Color { val part = item.part - if (part is Tool && part.state == ToolExecState.ERROR) return UiStyle.Colors.timelineError - if (part is Text) return UiStyle.Colors.timelineText - if (part is Reasoning) return UiStyle.Colors.timelineText - if (part is Compaction) return UiStyle.Colors.timelineStep - if (part is StepFinish) return UiStyle.Colors.timelineSuccess - if (part !is Tool) return UiStyle.Colors.timelineStep - if (part.kind == ToolKind.READ) return UiStyle.Colors.timelineRead - if (part.kind == ToolKind.WRITE) return UiStyle.Colors.timelineWrite - return UiStyle.Colors.timelineTool + if (part is Tool && part.state == ToolExecState.ERROR) return SessionUiStyle.Timeline.error + if (part is Text) return SessionUiStyle.Timeline.text + if (part is Reasoning) return SessionUiStyle.Timeline.text + if (part is Compaction) return SessionUiStyle.Timeline.step + if (part is StepFinish) return SessionUiStyle.Timeline.success + if (part !is Tool) return SessionUiStyle.Timeline.step + if (part.kind == ToolKind.READ) return SessionUiStyle.Timeline.read + if (part.kind == ToolKind.WRITE) return SessionUiStyle.Timeline.write + return SessionUiStyle.Timeline.tool } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index d8e8670d052..9ef4bd2ca19 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -41,14 +41,6 @@ object UiStyle { internal const val BORDER_DELTA = 64 internal const val HOVER_ALPHA = 0.35f - val timelineRead: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) - val timelineWrite: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) - val timelineTool: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc)) - val timelineSuccess: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground()) - val timelineError: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71)) - val timelineText: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d)) - val timelineStep: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d)) - fun bg(): Color = UIUtil.getPanelBackground() fun fg(): Color = UIUtil.getLabelForeground() From c169cff09f7dfb53dcddef33d3a848743c1b96c9 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:02:34 -0400 Subject: [PATCH 030/111] styles --- .../client/session/ui/PermissionPanel.kt | 3 +-- .../client/session/ui/QuestionPanel.kt | 3 +-- .../client/session/ui/SessionUiStyle.kt | 20 +++++++++++++++++++ .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 17 ---------------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt index 7d87fdd90ea..40d8e2027b7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.model.Permission -import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.PermissionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap @@ -36,7 +35,7 @@ class PermissionPanel( private lateinit var requestId: String init { - border = UiStyle.Dock.warning() + border = Dock.warning() isVisible = false } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt index 9d7db8c0d78..fa820f6aeeb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.model.Question -import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.QuestionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap @@ -35,7 +34,7 @@ class QuestionPanel( private var requestId: String? = null init { - border = UiStyle.Dock.neutral() + border = Dock.neutral() isVisible = false } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt index 128807347a4..ef4fe1a2ae7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -1,8 +1,11 @@ package ai.kilocode.client.session.ui +import ai.kilocode.client.ui.UiStyle import com.intellij.ui.JBColor +import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.Color +import javax.swing.border.Border /** Static UI tokens and helpers for session-specific Swing surfaces. */ object SessionUiStyle { @@ -16,3 +19,20 @@ object SessionUiStyle { val step: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d)) } } + +object Dock { + fun banner(): Border = JBUI.Borders.compound( + JBUI.Borders.customLineTop(UiStyle.Colors.line()), + JBUI.Borders.empty(UiStyle.Gap.small(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()), + )!! + + fun neutral(): Border = JBUI.Borders.compound( + JBUI.Borders.customLine(UiStyle.Colors.line(), 1), + JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()), + )!! + + fun warning(): Border = JBUI.Borders.compound( + UiStyle.Borders.warning(), + JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()), + )!! +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 9ef4bd2ca19..f3e97c5a9e1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -161,23 +161,6 @@ object UiStyle { fun scrollChrome(): Int = JBUI.scale(Size.CHROME) } - object Dock { - fun banner(): Border = JBUI.Borders.compound( - JBUI.Borders.customLineTop(Colors.line()), - JBUI.Borders.empty(Gap.small(), Gap.lg(), 0, Gap.lg()), - )!! - - fun neutral(): Border = JBUI.Borders.compound( - JBUI.Borders.customLine(Colors.line(), 1), - JBUI.Borders.empty(Gap.lg(), Gap.pad()), - )!! - - fun warning(): Border = JBUI.Borders.compound( - Borders.warning(), - JBUI.Borders.empty(Gap.lg(), Gap.pad()), - )!! - } - object Gap { fun xs() = JBUI.scale(2) From 6fbcfa76071065a4f97c308a418a76ebc261859e Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:07:16 -0400 Subject: [PATCH 031/111] refactor(jetbrains): move recent session styles --- .../client/session/ui/EmptySessionPanel.kt | 14 ++++---------- .../kilocode/client/session/ui/SessionUiStyle.kt | 5 +++++ .../main/kotlin/ai/kilocode/client/ui/UiStyle.kt | 1 - .../client/session/ui/EmptySessionPanelTest.kt | 10 ++++++++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 18decafb5f1..6bc8977f6ff 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -49,12 +49,6 @@ class EmptySessionPanel( recents: List, private val history: () -> Unit = {}, ) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget { - - companion object { - internal val LIMIT = UiStyle.Size.LIMIT - internal val DESCRIPTION_WIDTH = 250 - } - val view: CenterShrinkPanel = CenterShrinkPanel(this) private val model = DefaultListModel() @@ -68,7 +62,7 @@ class EmptySessionPanel( private val list = JBList(model).apply { isOpaque = false selectionMode = ListSelectionModel.SINGLE_SELECTION - visibleRowCount = LIMIT + visibleRowCount = SessionUiStyle.RecentSessions.LIMIT cellRenderer = SessionRenderer() cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.clear() @@ -106,12 +100,12 @@ class EmptySessionPanel( private val description = object : BorderLayoutPanel() { override fun getPreferredSize(): Dimension { val size = super.getPreferredSize() - return Dimension(JBUI.scale(DESCRIPTION_WIDTH), size.height) + return Dimension(JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), size.height) } override fun getMaximumSize(): Dimension { val size = super.getMaximumSize() - return Dimension(JBUI.scale(DESCRIPTION_WIDTH), size.height) + return Dimension(JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), size.height) } }.apply { isOpaque = false @@ -157,7 +151,7 @@ class EmptySessionPanel( private fun setSessions(sessions: List) { model.clear() - sessions.take(LIMIT).map(::LocalHistoryItem).forEach(model::addElement) + sessions.take(SessionUiStyle.RecentSessions.LIMIT).map(::LocalHistoryItem).forEach(model::addElement) revalidate() repaint() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt index ef4fe1a2ae7..c1b4b680715 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -9,6 +9,11 @@ import javax.swing.border.Border /** Static UI tokens and helpers for session-specific Swing surfaces. */ object SessionUiStyle { + object RecentSessions { + const val LIMIT = 5 + const val DESCRIPTION_WIDTH = 250 + } + object Timeline { val read: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) val write: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index f3e97c5a9e1..6b5342529df 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -23,7 +23,6 @@ import javax.swing.border.Border object UiStyle { object Size { const val WIDTH = 350 - const val LIMIT = 5 const val LINES = 3 const val CHROME = 16 const val BUTTON_WIDTH = 28 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 1955144ea5a..e0f1ee0f802 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -87,8 +87,14 @@ class EmptySessionPanelTest : BasePlatformTestCase() { fun `test description width is capped at DESCRIPTION_WIDTH`() { val panel = panel() - assertEquals(com.intellij.util.ui.JBUI.scale(EmptySessionPanel.DESCRIPTION_WIDTH), panel.descriptionPreferredSize().width) - assertEquals(com.intellij.util.ui.JBUI.scale(EmptySessionPanel.DESCRIPTION_WIDTH), panel.descriptionMaximumSize().width) + assertEquals( + com.intellij.util.ui.JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), + panel.descriptionPreferredSize().width, + ) + assertEquals( + com.intellij.util.ui.JBUI.scale(SessionUiStyle.RecentSessions.DESCRIPTION_WIDTH), + panel.descriptionMaximumSize().width, + ) } fun `test description label is centered`() { From 7594a05b774046e281d028295b89fcd5a4386395 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:20:58 -0400 Subject: [PATCH 032/111] refactor(jetbrains): move prompt styles into session ui --- .../client/session/ui/SessionLayout.kt | 5 ++- .../client/session/ui/SessionUiStyle.kt | 14 ++++++ .../client/session/ui/prompt/PromptPanel.kt | 43 ++++++++++++------- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 8 ---- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index de0a4fffd95..7047244b430 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.ui.UiStyle import com.intellij.util.ui.JBDimension +import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.Component import java.awt.Container @@ -76,9 +77,9 @@ class SessionLayout( private fun bounds(ins: Insets, width: Int, comp: Component): Bounds { val view = comp as? SessionView ?: return Bounds(ins.left, width) if (view.sessionViewKind != SessionView.Kind.UserPrompt) return Bounds(ins.left, width) - val shift = UiStyle.Insets.userPrompt() + val shift = JBUI.scale(SessionUiStyle.Prompt.USER_PROMPT_INDENT) val next = width - shift - if (next < UiStyle.Size.userPromptMin()) return Bounds(ins.left, width) + if (next < JBUI.scale(SessionUiStyle.Prompt.USER_PROMPT_INDENT)) return Bounds(ins.left, width) return Bounds(ins.left + shift, next) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt index c1b4b680715..33359063ab1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -9,6 +9,20 @@ import javax.swing.border.Border /** Static UI tokens and helpers for session-specific Swing surfaces. */ object SessionUiStyle { + object Prompt { + const val EDITOR_LINES = 3 + const val EDITOR_CHROME = 16 + const val SEND_BUTTON_SIZE = 24 + const val USER_PROMPT_INDENT = 100 + const val CORNER_ARC = 6 + const val FOCUS_WIDTH = 2 + const val PANEL_VERTICAL_PADDING = 8 + const val PANEL_HORIZONTAL_PADDING = 12 + const val CONTROL_GAP = 4 + const val SHELL_VERTICAL_PADDING = 6 + const val SHELL_HORIZONTAL_PADDING = 8 + } + object RecentSessions { const val LIMIT = 5 const val DESCRIPTION_WIDTH = 250 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index ca12335b4ca..7c3a9a0571f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -6,6 +6,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker import ai.kilocode.client.ui.UiStyle @@ -64,8 +65,6 @@ class PromptPanel( private val LOG = KiloLog.create(PromptPanel::class.java) private val SEND_ICON: Icon = IconLoader.getIcon("/icons/send.svg", PromptPanel::class.java) private val STOP_ICON: Icon = IconLoader.getIcon("/icons/stop.svg", PromptPanel::class.java) - private const val ARC = 6 - private const val FOCUS = 2 } val mode = ModePicker() @@ -142,7 +141,12 @@ class PromptPanel( init { border = JBUI.Borders.compound( JBUI.Borders.customLineTop(JBUI.CurrentTheme.ToolWindow.borderColor()), - UiStyle.Insets.prompt(), + JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.Prompt.PANEL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.Prompt.PANEL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.Prompt.PANEL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.Prompt.PANEL_HORIZONTAL_PADDING), + ), ) applyStyle(style) @@ -151,14 +155,14 @@ class PromptPanel( val bar = BorderLayoutPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) isOpaque = false - border = JBUI.Borders.emptyTop(UiStyle.Gap.small()) + border = JBUI.Borders.emptyTop(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP)) } bar.add(mode) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) bar.add(model) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) bar.add(reasoning) - bar.add(Box.createHorizontalStrut(UiStyle.Gap.small())) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) bar.add(reset) bar.add(Box.createHorizontalGlue()) bar.add(button) @@ -211,8 +215,8 @@ class PromptPanel( editor.font = style.transcriptFont editor.getEditor(false)?.let(style::applyToEditor) editor.background = style.editorScheme.defaultBackground - val height = style.transcriptFont.size * UiStyle.Size.LINES + JBUI.scale( - UiStyle.Size.CHROME) + val height = style.transcriptFont.size * SessionUiStyle.Prompt.EDITOR_LINES + JBUI.scale( + SessionUiStyle.Prompt.EDITOR_CHROME) editor.preferredSize = JBDimension(0, height) editor.minimumSize = JBDimension(0, height) revalidate() @@ -316,7 +320,10 @@ class PromptPanel( }) } - override fun getPreferredSize() = JBUI.size(UiStyle.Size.BUTTON, UiStyle.Size.BUTTON) + override fun getPreferredSize() = JBUI.size( + SessionUiStyle.Prompt.SEND_BUTTON_SIZE, + SessionUiStyle.Prompt.SEND_BUTTON_SIZE, + ) override fun uiDataSnapshot(sink: DataSink) { sink.set(PromptDataKeys.SEND, this@PromptPanel) @@ -332,7 +339,7 @@ class PromptPanel( try { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() - val arc = JBUI.scale(JBUI.getInt("Button.arc", ARC)) + val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.Prompt.CORNER_ARC)) g2.fillRoundRect(0, 0, width, height, arc, arc) } finally { g2.dispose() @@ -349,17 +356,23 @@ class PromptPanel( } private inner class PromptShell : BorderLayoutPanel() { - private val arc = JBValue.UIInteger("Button.arc", ARC) - private val focus = JBValue.UIInteger("Component.focusWidth", FOCUS) + private val arc = JBValue.UIInteger("Button.arc", SessionUiStyle.Prompt.CORNER_ARC) + private val focus = JBValue.UIInteger("Component.focusWidth", SessionUiStyle.Prompt.FOCUS_WIDTH) init { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.lg()) + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.Prompt.SHELL_HORIZONTAL_PADDING), + ) } override fun updateUI() { super.updateUI() - border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.lg()) + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.Prompt.SHELL_HORIZONTAL_PADDING), + ) } override fun paintComponent(g: Graphics) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 6b5342529df..b5ca9f09147 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -23,16 +23,12 @@ import javax.swing.border.Border object UiStyle { object Size { const val WIDTH = 350 - const val LINES = 3 const val CHROME = 16 const val BUTTON_WIDTH = 28 const val BUTTON = 24 const val SCROLL = 16 - const val USER_PROMPT = 100 const val TOOL_BODY = 20_000 - fun userPromptMin(): Int = JBUI.scale(USER_PROMPT) - fun toolBodyLimit(): Int = TOOL_BODY } @@ -110,12 +106,8 @@ object UiStyle { fun transcript(): java.awt.Insets = JBUI.insets(Gap.pad(), Gap.pad(), Gap.pad(), Gap.pad()) - fun userPrompt(): Int = Size.userPromptMin() - fun empty(): Border = JBUI.Borders.empty(Gap.pad()) - fun prompt(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad(), Gap.lg(), Gap.pad()) - fun header(): Border = JBUI.Borders.empty(Gap.lg(), Gap.lg()) fun body(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) From 42924e8044f5851ce76908c3a8ffa8d2f6f93df4 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:34:10 -0400 Subject: [PATCH 033/111] refactor(jetbrains): move session layout styles --- .../client/session/ui/SessionLayout.kt | 15 +++++----- .../session/ui/SessionMessageListPanel.kt | 12 ++++++-- .../client/session/ui/SessionUiStyle.kt | 14 +++++++++- .../client/session/views/MessageView.kt | 6 +++- .../client/session/views/ReasoningView.kt | 26 +++++++++++------ .../kilocode/client/session/views/ToolView.kt | 25 +++++++++++------ .../kilocode/client/session/views/TurnView.kt | 5 ++-- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 28 ------------------- .../client/session/views/TurnViewTest.kt | 10 +++++-- .../ai/kilocode/client/ui/UiStyleTest.kt | 16 ++++++----- 10 files changed, 88 insertions(+), 69 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 7047244b430..9aaa91b2f45 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -1,6 +1,5 @@ package ai.kilocode.client.session.ui -import ai.kilocode.client.ui.UiStyle import com.intellij.util.ui.JBDimension import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel @@ -30,8 +29,8 @@ import java.awt.LayoutManager * the panel width and the layout always has a valid width to work with. */ class SessionLayout( - private val gap: Int = UiStyle.Gap.small(), - private val pad: Insets = UiStyle.Insets.none(), + private val gap: Int = JBUI.scale(SessionUiStyle.SessionLayout.GAP), + private val pad: Insets = JBUI.emptyInsets(), ) : LayoutManager { override fun addLayoutComponent(name: String, comp: Component) = Unit @@ -77,9 +76,9 @@ class SessionLayout( private fun bounds(ins: Insets, width: Int, comp: Component): Bounds { val view = comp as? SessionView ?: return Bounds(ins.left, width) if (view.sessionViewKind != SessionView.Kind.UserPrompt) return Bounds(ins.left, width) - val shift = JBUI.scale(SessionUiStyle.Prompt.USER_PROMPT_INDENT) + val shift = JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT) val next = width - shift - if (next < JBUI.scale(SessionUiStyle.Prompt.USER_PROMPT_INDENT)) return Bounds(ins.left, width) + if (next < JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT)) return Bounds(ins.left, width) return Bounds(ins.left + shift, next) } @@ -104,8 +103,8 @@ class SessionLayout( * [SessionLayout] a valid width to measure against. */ open class SessionLayoutPanel( - gap: Int = UiStyle.Gap.small(), - pad: Insets = UiStyle.Insets.none(), + gap: Int = JBUI.scale(SessionUiStyle.SessionLayout.GAP), + pad: Insets = JBUI.emptyInsets(), ) : BorderLayoutPanel(), javax.swing.Scrollable { init { layout = SessionLayout(gap, pad) @@ -118,7 +117,7 @@ open class SessionLayoutPanel( visibleRect: java.awt.Rectangle, @Suppress("UNUSED_PARAMETER") orientation: Int, @Suppress("UNUSED_PARAMETER") direction: Int, - ): Int = UiStyle.Gap.scroll() + ): Int = JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_INCREMENT) override fun getScrollableBlockIncrement( visibleRect: java.awt.Rectangle, @Suppress("UNUSED_PARAMETER") orientation: Int, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index f6de23d94a9..d21b0241565 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -4,8 +4,8 @@ import ai.kilocode.client.session.model.SessionModel import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.views.MessageView import ai.kilocode.client.session.views.TurnView -import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.Disposable +import com.intellij.util.ui.JBUI /** * Scrollable transcript panel that maps the model's turn grouping to @@ -31,7 +31,15 @@ import com.intellij.openapi.Disposable class SessionMessageListPanel( private val model: SessionModel, parent: Disposable, -) : SessionLayoutPanel(UiStyle.Card.groupGap(), UiStyle.Insets.transcript()), SessionEditorStyleTarget { +) : SessionLayoutPanel( + JBUI.scale(SessionUiStyle.SessionLayout.GAP), + JBUI.insets( + SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING, + SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING, + SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING, + SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING, + ), +), SessionEditorStyleTarget { private val turnViews = LinkedHashMap() private val msgToTurn = HashMap() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt index 33359063ab1..c0d7ade8e8e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -13,7 +13,6 @@ object SessionUiStyle { const val EDITOR_LINES = 3 const val EDITOR_CHROME = 16 const val SEND_BUTTON_SIZE = 24 - const val USER_PROMPT_INDENT = 100 const val CORNER_ARC = 6 const val FOCUS_WIDTH = 2 const val PANEL_VERTICAL_PADDING = 8 @@ -23,6 +22,19 @@ object SessionUiStyle { const val SHELL_HORIZONTAL_PADDING = 8 } + object SessionLayout { + const val GAP = 4 + const val TRANSCRIPT_PADDING = 12 + const val CARD_LINES = 15 + const val REASONING_LINES = 5 + const val CARD_LAYOUT_GAP = 6 + const val CARD_VERTICAL_PADDING = 8 + const val CARD_HORIZONTAL_PADDING = 12 + const val USER_PROMPT_INDENT = 100 + const val SCROLL_CHROME = 16 + const val SCROLL_INCREMENT = 16 + } + object RecentSessions { const val LIMIT = 5 const val DESCRIPTION_WIDTH = 250 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index 7f2e26e06b9..b7768387129 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -6,7 +6,9 @@ import ai.kilocode.client.session.model.StepFinish import ai.kilocode.client.session.ui.SessionView import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.client.ui.UiStyle +import com.intellij.util.ui.JBUI /** * A single message container inside a [TurnView]. @@ -22,7 +24,9 @@ import ai.kilocode.client.ui.UiStyle class MessageView( val msg: Message, private var style: SessionEditorStyle = SessionEditorStyle.current(), -) : ai.kilocode.client.session.ui.SessionLayoutPanel(UiStyle.Card.groupGap()), SessionEditorStyleTarget, SessionView { +) : ai.kilocode.client.session.ui.SessionLayoutPanel( + JBUI.scale(SessionUiStyle.SessionLayout.GAP), +), SessionEditorStyleTarget, SessionView { constructor(msg: Message) : this(msg, SessionEditorStyle.current()) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index 776eaca47dc..e680f8d62cf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -6,11 +6,13 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Reasoning import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView import com.intellij.icons.AllIcons import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI import java.awt.BorderLayout import java.awt.Cursor import java.awt.Dimension @@ -34,20 +36,26 @@ class ReasoningView(reasoning: Reasoning) : PartView() { private val body = TrackPanel().apply { isOpaque = true background = UiStyle.Colors.surface() - border = UiStyle.Card.bodyInsets() + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + ) } private val scroll = JBScrollPane(body).apply { - border = UiStyle.Card.divider() + border = UiStyle.Borders.cardTop() isOpaque = true background = UiStyle.Colors.surface() viewport.background = UiStyle.Colors.surface() horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED } - private val header = JPanel(UiStyle.Card.layout()).apply { + private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true background = UiStyle.Colors.header() - border = UiStyle.Card.headerInsets() + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + ) } private val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = UiStyle.Colors.weak() @@ -80,9 +88,9 @@ class ReasoningView(reasoning: Reasoning) : PartView() { init { layout = BorderLayout() isOpaque = false - border = UiStyle.Card.border() + border = UiStyle.Borders.card() - val left = JPanel(UiStyle.Card.layout()).apply { + val left = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false add(icon, BorderLayout.WEST) add(title, BorderLayout.CENTER) @@ -142,7 +150,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { internal fun horizontalPolicy() = scroll.horizontalScrollBarPolicy - internal fun bodyMaxRows() = UiStyle.Card.REASONING_LINES + internal fun bodyMaxRows() = SessionUiStyle.SessionLayout.REASONING_LINES internal fun bodyCreated() = true @@ -254,7 +262,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { } private fun bodyMaxHeight(): Int = md.component.getFontMetrics(md.font).height * bodyMaxRows() + - UiStyle.Card.scrollChrome() + JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_CHROME) override fun dumpLabel(): String { val state = if (bodyVisible()) "open" else "closed" @@ -270,7 +278,7 @@ private class TrackPanel : JPanel(BorderLayout()), Scrollable { visibleRect: Rectangle, orientation: Int, direction: Int, - ) = UiStyle.Gap.scroll() + ) = JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_INCREMENT) override fun getScrollableBlockIncrement( visibleRect: Rectangle, orientation: Int, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index bf3d68d8fcf..40736152e43 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -7,11 +7,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.SessionEditorStyle +import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI import com.intellij.xml.util.XmlStringUtil import java.awt.BorderLayout import java.awt.Color @@ -39,12 +41,15 @@ class ToolView(tool: Tool) : PartView() { private val root = JPanel(BorderLayout()).apply { isOpaque = true background = UiStyle.Colors.surface() - border = UiStyle.Card.border() + border = UiStyle.Borders.card() } - private val header = JPanel(UiStyle.Card.layout()).apply { + private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true background = UiStyle.Colors.header() - border = UiStyle.Card.headerInsets() + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + ) } private val glyph = JBLabel() private val title = JBLabel() @@ -55,7 +60,7 @@ class ToolView(tool: Tool) : PartView() { foreground = UiStyle.Colors.weak() } private val arrow = JBLabel() - private val center = JPanel(UiStyle.Card.layout()).apply { + private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false } private val controls: JComponent = Box.createHorizontalBox().apply { @@ -70,10 +75,13 @@ class ToolView(tool: Tool) : PartView() { wrapStyleWord = true foreground = bodyColor() background = UiStyle.Colors.surface() - border = UiStyle.Card.bodyInsets() + border = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + ) } private val scroll = JBScrollPane(text).apply { - border = UiStyle.Card.divider() + border = UiStyle.Borders.cardTop() isOpaque = true background = UiStyle.Colors.surface() viewport.background = UiStyle.Colors.surface() @@ -171,7 +179,7 @@ class ToolView(tool: Tool) : PartView() { internal fun bodyWrap() = text.lineWrap - internal fun bodyMaxRows() = UiStyle.Card.LINES + internal fun bodyMaxRows() = SessionUiStyle.SessionLayout.CARD_LINES internal fun bodyCreated() = true @@ -282,7 +290,8 @@ class ToolView(tool: Tool) : PartView() { private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.error() else UiStyle.Colors.fg() private fun bodyMaxHeight(): Int { - return text.getFontMetrics(text.font).height * bodyMaxRows() + UiStyle.Card.scrollChrome() + return text.getFontMetrics(text.font).height * bodyMaxRows() + + JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_CHROME) } override fun dumpLabel() = "ToolView#$contentId(${labelText()})" diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index 0c7a6a4369b..a766634c2c4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -4,7 +4,8 @@ import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.ui.SessionLayoutPanel import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.session.ui.SessionEditorStyleTarget -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.session.ui.SessionUiStyle +import com.intellij.util.ui.JBUI /** * Top-level transcript item representing one conversational turn. @@ -18,7 +19,7 @@ import ai.kilocode.client.ui.UiStyle class TurnView( val id: String, private var style: SessionEditorStyle = SessionEditorStyle.current(), -) : SessionLayoutPanel(UiStyle.Card.groupGap()), SessionEditorStyleTarget { +) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), SessionEditorStyleTarget { constructor(id: String) : this(id, SessionEditorStyle.current()) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index b5ca9f09147..da03b86e1dd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -23,10 +23,8 @@ import javax.swing.border.Border object UiStyle { object Size { const val WIDTH = 350 - const val CHROME = 16 const val BUTTON_WIDTH = 28 const val BUTTON = 24 - const val SCROLL = 16 const val TOOL_BODY = 20_000 fun toolBodyLimit(): Int = TOOL_BODY @@ -102,10 +100,6 @@ object UiStyle { } object Insets { - fun none(): java.awt.Insets = JBUI.emptyInsets() - - fun transcript(): java.awt.Insets = JBUI.insets(Gap.pad(), Gap.pad(), Gap.pad(), Gap.pad()) - fun empty(): Border = JBUI.Borders.empty(Gap.pad()) fun header(): Border = JBUI.Borders.empty(Gap.lg(), Gap.lg()) @@ -132,26 +126,6 @@ object UiStyle { fun assistant(): Border = JBUI.Borders.empty() } - /** Shared geometry for collapsible transcript cards such as tools and reasoning. */ - object Card { - const val LINES = 15 - const val REASONING_LINES = 5 - - fun layout(): BorderLayout = Gap.layout(Gap.md()) - - fun groupGap(): Int = Gap.small() - - fun headerInsets(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) - - fun bodyInsets(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) - - fun border(): Border = Borders.card() - - fun divider(): Border = Borders.cardTop() - - fun scrollChrome(): Int = JBUI.scale(Size.CHROME) - } - object Gap { fun xs() = JBUI.scale(2) @@ -163,8 +137,6 @@ object UiStyle { fun pad() = JBUI.scale(12) - fun scroll() = JBUI.scale(Size.SCROLL) - fun layout(gap: Int = lg()) = BorderLayout(gap, 0) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt index ca961ee6650..21d718140fe 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt @@ -6,10 +6,11 @@ import ai.kilocode.client.session.model.Text import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.toolKind -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.JBUI /** * Tests for [TurnView] and [MessageView]. @@ -170,7 +171,10 @@ class TurnViewTest : BasePlatformTestCase() { mv.setSize(400, 200) mv.doLayout() - assertEquals(UiStyle.Card.groupGap(), mv.part("t1")!!.y - mv.part("r1")!!.bounds.maxY.toInt()) + assertEquals( + JBUI.scale(SessionUiStyle.SessionLayout.GAP), + mv.part("t1")!!.y - mv.part("r1")!!.bounds.maxY.toInt(), + ) } fun `test consecutive messages use shared compact gap`() { @@ -187,7 +191,7 @@ class TurnViewTest : BasePlatformTestCase() { val first = tv.messageView("u1")!! val second = tv.messageView("a2")!! - assertEquals(UiStyle.Card.groupGap(), second.y - first.bounds.maxY.toInt()) + assertEquals(JBUI.scale(SessionUiStyle.SessionLayout.GAP), second.y - first.bounds.maxY.toInt()) } // ------ helpers ------ diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index df7b02889e4..e83c0d87e61 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -1,6 +1,8 @@ package ai.kilocode.client.ui +import ai.kilocode.client.session.ui.SessionUiStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.JBUI import java.awt.Color @Suppress("UnstableApiUsage") @@ -35,12 +37,12 @@ class UiStyleTest : BasePlatformTestCase() { assertEquals(hover.green, hover.blue) } - fun `test card helpers provide shared geometry`() { - assertNotNull(UiStyle.Card.layout()) - assertNotNull(UiStyle.Card.border()) - assertNotNull(UiStyle.Card.divider()) - assertNotNull(UiStyle.Card.headerInsets()) - assertNotNull(UiStyle.Card.bodyInsets()) - assertTrue(UiStyle.Card.groupGap() > 0) + fun `test session layout constants provide shared geometry`() { + assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.GAP) > 0) + assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP) > 0) + assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING) > 0) + assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING) > 0) + assertTrue(SessionUiStyle.SessionLayout.CARD_LINES > 0) + assertTrue(SessionUiStyle.SessionLayout.REASONING_LINES > 0) } } From 7d51a1203cc372d39ce228f620dbe9043a7c831f Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:39:30 -0400 Subject: [PATCH 034/111] refactor(jetbrains): extract shared ui controls --- .../client/session/ui/ReasoningPicker.kt | 4 +- .../session/ui/header/SessionHeaderPanel.kt | 5 +- .../client/session/ui/mode/ModePicker.kt | 4 +- .../client/session/ui/model/ModelPicker.kt | 4 +- .../client/session/ui/prompt/PromptPanel.kt | 6 +- .../kotlin/ai/kilocode/client/ui/HoverIcon.kt | 65 ++++++++++ .../ai/kilocode/client/ui/PickerButton.kt | 54 ++++++++ .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 117 ------------------ 8 files changed, 132 insertions(+), 127 deletions(-) create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt index ea19f7e5e49..60b925da60a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.PickerButton import com.intellij.icons.AllIcons import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.ListPopup @@ -21,7 +21,7 @@ import javax.swing.Icon * opens a list popup above the picker. Disabled (greyed out, not * clickable) when no items are loaded. */ -class ReasoningPicker : UiStyle.Pickers.Label() { +class ReasoningPicker : PickerButton() { private companion object { val checked: Icon = AllIcons.Actions.Checked diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index 64c4f00421c..1ede7921e1e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.ui.SessionEditorStyle import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.TokensDto import com.intellij.ide.util.PropertiesComponent @@ -51,13 +52,13 @@ class SessionHeaderPanel( private val cost = JBLabel() private val context = JBLabel() private val todos = JBLabel() - private val compact = UiStyle.Buttons.HoverIcon().apply { + private val compact = HoverIcon().apply { icon = COMPRESS_ICON toolTipText = KiloBundle.message("session.header.compact.description") accessibleContext.accessibleName = KiloBundle.message("session.header.compact") addActionListener { controller.compact() } } - private val expand = UiStyle.Buttons.HoverIcon().apply { + private val expand = HoverIcon().apply { icon = CHEVRON_ICON toolTipText = KiloBundle.message("session.header.expand") accessibleContext.accessibleName = KiloBundle.message("session.header.expand") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt index d42e76aa2e0..6c9a03b6565 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.ui.mode import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.PickerButton import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupShowOptions import java.awt.Cursor @@ -9,7 +9,7 @@ import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.ListSelectionModel -class ModePicker : UiStyle.Pickers.Label() { +class ModePicker : PickerButton() { data class Item( val id: String, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index 9a86d0a85d7..4423679e9ef 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.ui.model import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.PickerButton import ai.kilocode.rpc.dto.ModelSelectionDto import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory @@ -45,7 +45,7 @@ private const val MODEL_PICKER_MAX_WIDTH = 760 private const val MODEL_PICKER_MAX_VISIBLE_ROWS = 10 private const val MODEL_PICKER_EMPTY_LIST_HEIGHT = 120 -class ModelPicker : UiStyle.Pickers.Label() { +class ModelPicker : PickerButton() { data class Item( val id: String, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 7c3a9a0571f..57ea47797be 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -9,7 +9,9 @@ import ai.kilocode.client.session.ui.SessionEditorStyleTarget import ai.kilocode.client.session.ui.SessionUiStyle import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker +import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.iconButton import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import com.intellij.icons.AllIcons @@ -120,7 +122,7 @@ class PromptPanel( } } - private val reset = UiStyle.Buttons.HoverIcon().apply { + private val reset = HoverIcon().apply { icon = AllIcons.Actions.Cancel toolTipText = KiloBundle.message("model.picker.reset") accessibleContext.accessibleName = KiloBundle.message("model.picker.reset") @@ -307,7 +309,7 @@ class PromptPanel( private var over = false init { - UiStyle.Buttons.icon(this) + iconButton(this) cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) addMouseListener(object : MouseAdapter() { override fun mouseEntered(e: MouseEvent) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt new file mode 100644 index 00000000000..14f3ce49b45 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt @@ -0,0 +1,65 @@ +package ai.kilocode.client.ui + +import com.intellij.util.ui.JBUI +import java.awt.Dimension +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.RenderingHints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JButton + +class HoverIcon : JButton() { + private var over = false + + init { + iconButton(this) + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun getPreferredSize(): Dimension = JBUI.size(UiStyle.Size.BUTTON, UiStyle.Size.BUTTON) + + override fun getMinimumSize(): Dimension = preferredSize + + override fun getMaximumSize(): Dimension = preferredSize + + override fun paintComponent(g: Graphics) { + if (isEnabled && over) paintHover(g) + super.paintComponent(g) + } + + private fun paintHover(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } +} + +fun iconButton(button: JButton) { + button.isFocusable = false + button.setRequestFocusEnabled(false) + button.isContentAreaFilled = false + button.isBorderPainted = false + button.isOpaque = false + button.border = JBUI.Borders.empty() +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt new file mode 100644 index 00000000000..a080ebdbcf4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt @@ -0,0 +1,54 @@ +package ai.kilocode.client.ui + +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.JBUI +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.RenderingHints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent + +open class PickerButton : JBLabel() { + private var over = false + + init { + border = UiStyle.Borders.picker() + background = UiStyle.Colors.picker() + // The custom rounded fill needs parent background around the corners. + isOpaque = false + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun updateUI() { + super.updateUI() + border = UiStyle.Borders.picker() + background = UiStyle.Colors.picker() + } + + override fun paintComponent(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = if (isEnabled && over) UiStyle.Colors.pickerHover() else UiStyle.Colors.picker() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + super.paintComponent(g) + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index da03b86e1dd..436d7b0e30b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -3,18 +3,10 @@ package ai.kilocode.client.ui import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.JBColor import com.intellij.ui.RoundedLineBorder -import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.BorderLayout import java.awt.Color -import java.awt.Dimension -import java.awt.Graphics -import java.awt.Graphics2D -import java.awt.RenderingHints -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import javax.swing.JButton import javax.swing.JComponent import javax.swing.UIManager import javax.swing.border.Border @@ -140,115 +132,6 @@ object UiStyle { fun layout(gap: Int = lg()) = BorderLayout(gap, 0) } - object Buttons { - class HoverIcon : JButton() { - private var over = false - - init { - isFocusable = false - setRequestFocusEnabled(false) - isContentAreaFilled = false - isBorderPainted = false - isOpaque = false - border = JBUI.Borders.empty() - addMouseListener(object : MouseAdapter() { - override fun mouseEntered(e: MouseEvent) { - sync(true) - } - - override fun mouseExited(e: MouseEvent) { - sync(false) - } - }) - } - - override fun getPreferredSize(): Dimension = JBUI.size(Size.BUTTON, Size.BUTTON) - - override fun getMinimumSize(): Dimension = preferredSize - - override fun getMaximumSize(): Dimension = preferredSize - - override fun paintComponent(g: Graphics) { - if (isEnabled && over) paintHover(g) - super.paintComponent(g) - } - - private fun paintHover(g: Graphics) { - val g2 = g.create() as Graphics2D - try { - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() - val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) - g2.fillRoundRect(0, 0, width, height, arc, arc) - } finally { - g2.dispose() - } - } - - private fun sync(value: Boolean) { - if (over == value) return - over = value - repaint() - } - } - - fun icon(button: JButton) { - button.isFocusable = false - button.setRequestFocusEnabled(false) - button.isContentAreaFilled = false - button.isBorderPainted = false - button.isOpaque = false - button.border = JBUI.Borders.empty() - } - } - - object Pickers { - open class Label : JBLabel() { - private var over = false - - init { - border = Borders.picker() - background = Colors.picker() - // The custom rounded fill needs parent background around the corners. - isOpaque = false - addMouseListener(object : MouseAdapter() { - override fun mouseEntered(e: MouseEvent) { - sync(true) - } - - override fun mouseExited(e: MouseEvent) { - sync(false) - } - }) - } - - override fun updateUI() { - super.updateUI() - border = Borders.picker() - background = Colors.picker() - } - - override fun paintComponent(g: Graphics) { - val g2 = g.create() as Graphics2D - try { - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g2.color = if (isEnabled && over) Colors.pickerHover() else Colors.picker() - val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) - g2.fillRoundRect(0, 0, width, height, arc, arc) - } finally { - g2.dispose() - } - super.paintComponent(g) - } - - private fun sync(value: Boolean) { - if (over == value) return - over = value - repaint() - } - } - } - object Components { fun transparent(component: JComponent) { component.isOpaque = false From 368f73c87ce48fd606c4cc8b17061889c038625d Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:41:23 -0400 Subject: [PATCH 035/111] refactor(jetbrains): inline hover icon size --- .../frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt | 2 +- .../frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt index 14f3ce49b45..70f8b27af8e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/HoverIcon.kt @@ -25,7 +25,7 @@ class HoverIcon : JButton() { }) } - override fun getPreferredSize(): Dimension = JBUI.size(UiStyle.Size.BUTTON, UiStyle.Size.BUTTON) + override fun getPreferredSize(): Dimension = JBUI.size(24, 24) override fun getMinimumSize(): Dimension = preferredSize diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 436d7b0e30b..1a96629672c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -16,7 +16,6 @@ object UiStyle { object Size { const val WIDTH = 350 const val BUTTON_WIDTH = 28 - const val BUTTON = 24 const val TOOL_BODY = 20_000 fun toolBodyLimit(): Int = TOOL_BODY From 94721182a37885de96fb4fb4a0c2ecbfaaca30a7 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 17:43:11 -0400 Subject: [PATCH 036/111] refactor(jetbrains): uppercase timeline style constants --- .../client/session/ui/SessionUiStyle.kt | 14 +++++++------- .../client/session/ui/header/TimelinePanel.kt | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt index c0d7ade8e8e..9783c97b142 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt @@ -41,13 +41,13 @@ object SessionUiStyle { } object Timeline { - val read: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) - val write: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) - val tool: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc)) - val success: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground()) - val error: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71)) - val text: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d)) - val step: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d)) + val READ: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) + val WRITE: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) + val TOOL: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc)) + val SUCCESS: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground()) + val ERROR: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71)) + val TEXT: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d)) + val STEP: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d)) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt index 822b02f19a6..a437055a8a7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt @@ -138,14 +138,14 @@ internal class TimelinePanel : JPanel() { private fun color(item: TimelineItem): Color { val part = item.part - if (part is Tool && part.state == ToolExecState.ERROR) return SessionUiStyle.Timeline.error - if (part is Text) return SessionUiStyle.Timeline.text - if (part is Reasoning) return SessionUiStyle.Timeline.text - if (part is Compaction) return SessionUiStyle.Timeline.step - if (part is StepFinish) return SessionUiStyle.Timeline.success - if (part !is Tool) return SessionUiStyle.Timeline.step - if (part.kind == ToolKind.READ) return SessionUiStyle.Timeline.read - if (part.kind == ToolKind.WRITE) return SessionUiStyle.Timeline.write - return SessionUiStyle.Timeline.tool + if (part is Tool && part.state == ToolExecState.ERROR) return SessionUiStyle.Timeline.ERROR + if (part is Text) return SessionUiStyle.Timeline.TEXT + if (part is Reasoning) return SessionUiStyle.Timeline.TEXT + if (part is Compaction) return SessionUiStyle.Timeline.STEP + if (part is StepFinish) return SessionUiStyle.Timeline.SUCCESS + if (part !is Tool) return SessionUiStyle.Timeline.STEP + if (part.kind == ToolKind.READ) return SessionUiStyle.Timeline.READ + if (part.kind == ToolKind.WRITE) return SessionUiStyle.Timeline.WRITE + return SessionUiStyle.Timeline.TOOL } } From 7ee0f093c09e5908bda17301c77d58f2da478e04 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:00:00 -0400 Subject: [PATCH 037/111] refactor(jetbrains): move session styles into style package --- .../main/kotlin/ai/kilocode/client/session/SessionScroll.kt | 4 ++-- .../src/main/kotlin/ai/kilocode/client/session/SessionUi.kt | 4 ++-- .../ai/kilocode/client/session/history/HistoryPanel.kt | 2 +- .../ai/kilocode/client/session/ui/EmptySessionPanel.kt | 3 +++ .../kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt | 2 ++ .../kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt | 3 ++- .../kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt | 2 ++ .../kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt | 3 ++- .../kotlin/ai/kilocode/client/session/ui/SessionLayout.kt | 1 + .../kilocode/client/session/ui/SessionMessageListPanel.kt | 3 +++ .../ai/kilocode/client/session/ui/header/ContextBar.kt | 2 +- .../kilocode/client/session/ui/header/SessionHeaderPanel.kt | 4 ++-- .../ai/kilocode/client/session/ui/header/TimelinePanel.kt | 2 +- .../ai/kilocode/client/session/ui/prompt/PromptPanel.kt | 6 +++--- .../client/session/ui/{ => style}/SessionEditorStyle.kt | 2 +- .../client/session/ui/{ => style}/SessionUiStyle.kt | 2 +- .../ai/kilocode/client/session/views/CompactionView.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/GenericView.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/MessageView.kt | 6 +++--- .../kotlin/ai/kilocode/client/session/views/PartView.kt | 4 ++-- .../ai/kilocode/client/session/views/ReasoningView.kt | 4 ++-- .../kotlin/ai/kilocode/client/session/views/TextView.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/ToolView.kt | 4 ++-- .../kotlin/ai/kilocode/client/session/views/TurnView.kt | 6 +++--- .../kilocode/client/session/SessionSidePanelManagerTest.kt | 2 +- .../ai/kilocode/client/session/ui/EmptySessionPanelTest.kt | 1 + .../kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt | 1 + .../ai/kilocode/client/session/ui/SessionEditorStyleTest.kt | 1 + .../client/session/ui/SessionMessageListPanelTest.kt | 1 + .../client/session/ui/header/SessionHeaderPanelTest.kt | 2 +- .../ai/kilocode/client/session/views/ReasoningViewTest.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/TextViewTest.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/ToolViewTest.kt | 2 +- .../kotlin/ai/kilocode/client/session/views/TurnViewTest.kt | 2 +- .../src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt | 2 +- 35 files changed, 55 insertions(+), 38 deletions(-) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/{ => style}/SessionEditorStyle.kt (98%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/{ => style}/SessionUiStyle.kt (98%) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt index cc4bd60e006..eef78776d83 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt @@ -3,8 +3,8 @@ package ai.kilocode.client.session import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.SessionRootPanel -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.IconLoader diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 9e33d856046..dfd2e8ab37c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -17,8 +17,8 @@ import ai.kilocode.client.session.ui.QuestionPanel import ai.kilocode.client.session.ui.SessionRootPanel import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.header.SessionHeaderPanel -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.update.EVENT_FLUSH_MS import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.update.SessionControllerEvent diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 332bce6b754..8753c4d9889 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.LoadingPanel -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ide.ui.LafManagerListener diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 6bc8977f6ff..9ea187446e8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -6,6 +6,9 @@ import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem import ai.kilocode.client.session.history.clicked import ai.kilocode.client.session.history.title +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.CenterShrinkPanel import ai.kilocode.client.ui.UiStyle diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt index f8ca17f30da..04acd9f6b44 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/LoadingPanel.kt @@ -1,6 +1,8 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import com.intellij.ui.components.JBLabel import com.intellij.util.ui.Centerizer import java.awt.BorderLayout diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt index 40d8e2027b7..d7a444abfcf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt @@ -1,8 +1,9 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.model.Permission +import ai.kilocode.client.session.ui.style.Dock +import ai.kilocode.client.session.update.SessionController import ai.kilocode.rpc.dto.PermissionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt index a57e91dec7b..376816612de 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ProgressPanel.kt @@ -3,6 +3,8 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.session.model.SessionModel import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.Disposable import com.intellij.ui.AnimatedIcon diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt index fa820f6aeeb..b84439d765d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt @@ -1,8 +1,9 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.model.Question +import ai.kilocode.client.session.ui.style.Dock +import ai.kilocode.client.session.update.SessionController import ai.kilocode.rpc.dto.QuestionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 9aaa91b2f45..296a87ef9e7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.ui +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.util.ui.JBDimension import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index d21b0241565..bc348f09079 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -2,6 +2,9 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.session.model.SessionModel import ai.kilocode.client.session.model.SessionModelEvent +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.MessageView import ai.kilocode.client.session.views.TurnView import com.intellij.openapi.Disposable diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt index f1a97e05c93..baad66fa87d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.ui.header import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.ContextUsage -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index 1ede7921e1e..cd53a8edbd5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -3,8 +3,8 @@ package ai.kilocode.client.session.ui.header import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.SessionHeaderSnapshot import ai.kilocode.client.session.model.SessionModelEvent -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.UiStyle diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt index a437055a8a7..b0e1cf49fc3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/TimelinePanel.kt @@ -9,7 +9,7 @@ import ai.kilocode.client.session.model.TimelineItem import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.ToolKind -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.util.ui.JBUI import java.awt.Color import java.awt.Dimension diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 57ea47797be..4473eec2c6e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -4,9 +4,9 @@ import ai.kilocode.client.actions.SendPromptAction import ai.kilocode.client.actions.StopSessionAction import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.ReasoningPicker -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker import ai.kilocode.client.ui.HoverIcon diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt index 617be4e1b42..e32fcaafe71 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionEditorStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.ui +package ai.kilocode.client.session.ui.style import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.colors.EditorColorsScheme diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 9783c97b142..6289af52fd4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.ui +package ai.kilocode.client.session.ui.style import ai.kilocode.client.ui.UiStyle import com.intellij.ui.JBColor diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt index b397a5eb4d3..07e4707c66f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt @@ -3,7 +3,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Compaction import ai.kilocode.client.session.model.Content import ai.kilocode.client.plugin.KiloBundle -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import java.awt.BorderLayout diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt index c6950be3b19..31db00f4ecd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/GenericView.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Generic -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import java.awt.BorderLayout diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index b7768387129..b07710fa7c0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -4,9 +4,9 @@ import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.model.StepFinish import ai.kilocode.client.session.ui.SessionView -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget -import ai.kilocode.client.session.ui.SessionUiStyle +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.util.ui.JBUI diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt index c6be3209eb4..1d32c764b7d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PartView.kt @@ -1,8 +1,8 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import javax.swing.JPanel /** diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index e680f8d62cf..3f30376fcce 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -5,8 +5,8 @@ package ai.kilocode.client.session.views import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Reasoning -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView import com.intellij.icons.AllIcons diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt index 2aa7f9fb075..e08880b9017 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt @@ -2,7 +2,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Content import ai.kilocode.client.session.model.Text -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.md.MdView import java.awt.BorderLayout diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index 40736152e43..f4ac323296f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -6,8 +6,8 @@ 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.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ui.components.JBLabel diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index a766634c2c4..57538a8f19d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -2,9 +2,9 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.ui.SessionLayoutPanel -import ai.kilocode.client.session.ui.SessionEditorStyle -import ai.kilocode.client.session.ui.SessionEditorStyleTarget -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.util.ui.JBUI /** diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 70c38163543..92d5d5d5d45 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -184,7 +184,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1")) val first = active(manager) as SessionUi manager.openSession(session("ses_2")) - val style = ai.kilocode.client.session.ui.SessionEditorStyle.create(family = "Courier New", size = 24) + val style = ai.kilocode.client.session.ui.style.SessionEditorStyle.create(family = "Courier New", size = 24) first.applyStyle(style) manager.openSession(session("ses_1")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index e0f1ee0f802..78a714cd909 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -7,6 +7,7 @@ import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index 249accefe48..1b210456b57 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.ui +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.prompt.PromptDataKeys import ai.kilocode.client.session.ui.prompt.PromptPanel import com.intellij.icons.AllIcons diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt index fa2a3aee7a8..b11df3d3320 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.session.ui +import ai.kilocode.client.session.ui.style.SessionEditorStyle import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.testFramework.fixtures.BasePlatformTestCase import java.awt.Font diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index 821567ec5ee..96fbc965366 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.session.model.SessionModel +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.views.TextView import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt index c4918205737..7d4af0f3fff 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt @@ -5,7 +5,7 @@ import ai.kilocode.client.session.model.StepFinish import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.ToolKind -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.update.SessionControllerTestBase import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.MessageDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt index e51781c9f9e..6cae7834def 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Reasoning -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import javax.swing.ScrollPaneConstants diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt index 0d3938a5bee..a457b45d9e2 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt @@ -1,7 +1,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Text -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase /** diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt index 36fd940e3e7..29dd8278999 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt @@ -4,7 +4,7 @@ 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.model.toolKind -import ai.kilocode.client.session.ui.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import javax.swing.ScrollPaneConstants diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt index 21d718140fe..8427cce067e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TurnViewTest.kt @@ -6,7 +6,7 @@ import ai.kilocode.client.session.model.Text import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.toolKind -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto import com.intellij.testFramework.fixtures.BasePlatformTestCase diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index e83c0d87e61..6955ed97d52 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -1,6 +1,6 @@ package ai.kilocode.client.ui -import ai.kilocode.client.session.ui.SessionUiStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.JBUI import java.awt.Color From 776d0b5b66474466c2be8bec793a98d9fe196820 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:07:53 -0400 Subject: [PATCH 038/111] refactor(jetbrains): group view style constants --- .../client/session/ui/style/SessionUiStyle.kt | 17 ++++++++++++----- .../client/session/views/ReasoningView.kt | 16 ++++++++-------- .../kilocode/client/session/views/ToolView.kt | 18 +++++++++--------- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 3 --- .../client/session/views/ToolViewTest.kt | 6 +++--- .../ai/kilocode/client/ui/UiStyleTest.kt | 10 +++++----- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 6289af52fd4..b742269887c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -25,14 +25,21 @@ object SessionUiStyle { object SessionLayout { const val GAP = 4 const val TRANSCRIPT_PADDING = 12 - const val CARD_LINES = 15 - const val REASONING_LINES = 5 + const val USER_PROMPT_INDENT = 100 + const val SCROLL_INCREMENT = 16 + } + + object View { const val CARD_LAYOUT_GAP = 6 const val CARD_VERTICAL_PADDING = 8 const val CARD_HORIZONTAL_PADDING = 12 - const val USER_PROMPT_INDENT = 100 - const val SCROLL_CHROME = 16 - const val SCROLL_INCREMENT = 16 + const val CARD_BODY_EXTRA_HEIGHT = 16 + const val REASONING_LINES = 5 + + object Tool { + const val BODY_LINES = 15 + const val PREVIEW_LIMIT = 20_000 + } } object RecentSessions { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index 3f30376fcce..b0740e61df0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -37,8 +37,8 @@ class ReasoningView(reasoning: Reasoning) : PartView() { isOpaque = true background = UiStyle.Colors.surface() border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val scroll = JBScrollPane(body).apply { @@ -49,12 +49,12 @@ class ReasoningView(reasoning: Reasoning) : PartView() { horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED } - private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { + private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true background = UiStyle.Colors.header() border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { @@ -90,7 +90,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { isOpaque = false border = UiStyle.Borders.card() - val left = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { + val left = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false add(icon, BorderLayout.WEST) add(title, BorderLayout.CENTER) @@ -150,7 +150,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { internal fun horizontalPolicy() = scroll.horizontalScrollBarPolicy - internal fun bodyMaxRows() = SessionUiStyle.SessionLayout.REASONING_LINES + internal fun bodyMaxRows() = SessionUiStyle.View.REASONING_LINES internal fun bodyCreated() = true @@ -262,7 +262,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { } private fun bodyMaxHeight(): Int = md.component.getFontMetrics(md.font).height * bodyMaxRows() + - JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_CHROME) + JBUI.scale(SessionUiStyle.View.CARD_BODY_EXTRA_HEIGHT) override fun dumpLabel(): String { val state = if (bodyVisible()) "open" else "closed" diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index f4ac323296f..01b00f28a88 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -43,12 +43,12 @@ class ToolView(tool: Tool) : PartView() { background = UiStyle.Colors.surface() border = UiStyle.Borders.card() } - private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { + private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true background = UiStyle.Colors.header() border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val glyph = JBLabel() @@ -60,7 +60,7 @@ class ToolView(tool: Tool) : PartView() { foreground = UiStyle.Colors.weak() } private val arrow = JBLabel() - private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP), 0)).apply { + private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false } private val controls: JComponent = Box.createHorizontalBox().apply { @@ -76,8 +76,8 @@ class ToolView(tool: Tool) : PartView() { foreground = bodyColor() background = UiStyle.Colors.surface() border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val scroll = JBScrollPane(text).apply { @@ -179,7 +179,7 @@ class ToolView(tool: Tool) : PartView() { internal fun bodyWrap() = text.lineWrap - internal fun bodyMaxRows() = SessionUiStyle.SessionLayout.CARD_LINES + internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES internal fun bodyCreated() = true @@ -291,7 +291,7 @@ class ToolView(tool: Tool) : PartView() { private fun bodyMaxHeight(): Int { return text.getFontMetrics(text.font).height * bodyMaxRows() + - JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_CHROME) + JBUI.scale(SessionUiStyle.View.CARD_BODY_EXTRA_HEIGHT) } override fun dumpLabel() = "ToolView#$contentId(${labelText()})" @@ -484,7 +484,7 @@ private class Preview { fun append(value: String): Preview { if (cut) return this - val rem = UiStyle.Size.toolBodyLimit() - text.length + val rem = SessionUiStyle.View.Tool.PREVIEW_LIMIT - text.length if (value.length <= rem) { text.append(value) return this diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 1a96629672c..43250b005de 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -16,9 +16,6 @@ object UiStyle { object Size { const val WIDTH = 350 const val BUTTON_WIDTH = 28 - const val TOOL_BODY = 20_000 - - fun toolBodyLimit(): Int = TOOL_BODY } object Colors { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt index 29dd8278999..1486718c479 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt @@ -5,7 +5,7 @@ import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.toolKind import ai.kilocode.client.session.ui.style.SessionEditorStyle -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.testFramework.fixtures.BasePlatformTestCase import javax.swing.ScrollPaneConstants @@ -266,7 +266,7 @@ class ToolViewTest : BasePlatformTestCase() { } fun `test large tool output is truncated in preview`() { - val out = "x".repeat(UiStyle.Size.toolBodyLimit() + 1_000) + val out = "x".repeat(SessionUiStyle.View.Tool.PREVIEW_LIMIT + 1_000) val t = tool("p1", "bash", ToolExecState.COMPLETED).also { it.input = mapOf("command" to "log") it.output = out @@ -281,7 +281,7 @@ class ToolViewTest : BasePlatformTestCase() { } fun `test large generic tool output is truncated in preview`() { - val out = "x".repeat(UiStyle.Size.toolBodyLimit() + 1_000) + val out = "x".repeat(SessionUiStyle.View.Tool.PREVIEW_LIMIT + 1_000) val t = tool("p1", "glob", ToolExecState.COMPLETED).also { it.output = out } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index 6955ed97d52..8d73354e798 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -39,10 +39,10 @@ class UiStyleTest : BasePlatformTestCase() { fun `test session layout constants provide shared geometry`() { assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.GAP) > 0) - assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_LAYOUT_GAP) > 0) - assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_VERTICAL_PADDING) > 0) - assertTrue(JBUI.scale(SessionUiStyle.SessionLayout.CARD_HORIZONTAL_PADDING) > 0) - assertTrue(SessionUiStyle.SessionLayout.CARD_LINES > 0) - assertTrue(SessionUiStyle.SessionLayout.REASONING_LINES > 0) + assertTrue(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP) > 0) + assertTrue(JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING) > 0) + assertTrue(JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING) > 0) + assertTrue(SessionUiStyle.View.Tool.BODY_LINES > 0) + assertTrue(SessionUiStyle.View.REASONING_LINES > 0) } } From 2acbadd04548ffb65980a792ea01f33a2585d8bf Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:14:29 -0400 Subject: [PATCH 039/111] refactor(jetbrains): group reasoning view styles --- .../ai/kilocode/client/session/ui/style/SessionUiStyle.kt | 5 ++++- .../kotlin/ai/kilocode/client/session/views/ReasoningView.kt | 2 +- .../src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index b742269887c..4a234a7fe5d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -34,7 +34,10 @@ object SessionUiStyle { const val CARD_VERTICAL_PADDING = 8 const val CARD_HORIZONTAL_PADDING = 12 const val CARD_BODY_EXTRA_HEIGHT = 16 - const val REASONING_LINES = 5 + + object Reasoning { + const val BODY_LINES = 5 + } object Tool { const val BODY_LINES = 15 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index b0740e61df0..a4bc47579b4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -150,7 +150,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { internal fun horizontalPolicy() = scroll.horizontalScrollBarPolicy - internal fun bodyMaxRows() = SessionUiStyle.View.REASONING_LINES + internal fun bodyMaxRows() = SessionUiStyle.View.Reasoning.BODY_LINES internal fun bodyCreated() = true diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index 8d73354e798..b9d61d73909 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -43,6 +43,6 @@ class UiStyleTest : BasePlatformTestCase() { assertTrue(JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING) > 0) assertTrue(JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING) > 0) assertTrue(SessionUiStyle.View.Tool.BODY_LINES > 0) - assertTrue(SessionUiStyle.View.REASONING_LINES > 0) + assertTrue(SessionUiStyle.View.Reasoning.BODY_LINES > 0) } } From bc9b98b7418310764b452a2fdf287026a670b5a8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:18:27 -0400 Subject: [PATCH 040/111] refactor(jetbrains): group message view styles --- .../client/session/ui/style/SessionUiStyle.kt | 8 +++++++ .../client/session/views/MessageView.kt | 23 ++++++++++++++----- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 8 ------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 4a234a7fe5d..1fb01ae28b4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -39,6 +39,14 @@ object SessionUiStyle { const val BODY_LINES = 5 } + object Message { + const val USER_ROLE = "user" + const val ASSISTANT_ROLE = "assistant" + const val USER_BORDER_ARC = 8 + const val USER_BORDER_VERTICAL_PADDING = 8 + const val USER_BORDER_HORIZONTAL_PADDING = 12 + } + object Tool { const val BODY_LINES = 15 const val PREVIEW_LIMIT = 20_000 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index b07710fa7c0..3f072556340 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -8,6 +8,7 @@ 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.RoundedLineBorder import com.intellij.util.ui.JBUI /** @@ -33,16 +34,16 @@ class MessageView( val role: String get() = msg.info.role override val sessionViewKind: SessionView.Kind - get() = if (role == "user") SessionView.Kind.UserPrompt else SessionView.Kind.Default + get() = if (role == SessionUiStyle.View.Message.USER_ROLE) SessionView.Kind.UserPrompt else SessionView.Kind.Default private val parts = LinkedHashMap() init { isOpaque = false - border = if (msg.info.role == "user") { - UiStyle.Borders.user() + border = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) { + userBorder() } else { - UiStyle.Borders.assistant() + assistantBorder() } // Populate content that already exists (e.g. after loadHistory) @@ -81,8 +82,8 @@ class MessageView( } private fun syncBorder() { - if (msg.info.role != "assistant") return - border = UiStyle.Borders.assistant() + if (msg.info.role != SessionUiStyle.View.Message.ASSISTANT_ROLE) return + border = assistantBorder() } /** Append a streaming delta to the renderer for [contentId]. */ @@ -111,4 +112,14 @@ class MessageView( revalidate() repaint() } + + private fun userBorder() = JBUI.Borders.compound( + RoundedLineBorder(UiStyle.Colors.line(), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_ARC)), + JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_HORIZONTAL_PADDING), + ), + )!! + + private fun assistantBorder() = JBUI.Borders.empty() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 43250b005de..1c999bdbc1f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -2,7 +2,6 @@ package ai.kilocode.client.ui import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.JBColor -import com.intellij.ui.RoundedLineBorder import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.BorderLayout @@ -105,13 +104,6 @@ object UiStyle { fun warning(): Border = JBUI.Borders.customLine(Colors.warning(), 1) fun picker(): Border = JBUI.Borders.empty(Gap.xs(), Gap.lg()) - - fun user(): Border = JBUI.Borders.compound( - RoundedLineBorder(Colors.line(), Gap.lg()), - JBUI.Borders.empty(Gap.lg(), Gap.pad()), - )!! - - fun assistant(): Border = JBUI.Borders.empty() } object Gap { From e9a73cc6fb13fa4b384c0db863b920e5fc8079d3 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:31:34 -0400 Subject: [PATCH 041/111] refactor(jetbrains): group session view style tokens --- .../client/session/ui/ConnectionPanel.kt | 3 +- .../client/session/ui/prompt/PromptPanel.kt | 40 +++++++-------- .../client/session/ui/style/SessionUiStyle.kt | 49 +++++++++++++------ .../client/session/views/CompactionView.kt | 3 +- .../client/session/views/MessageView.kt | 3 +- .../client/session/views/ReasoningView.kt | 14 +++--- .../kilocode/client/session/views/ToolView.kt | 16 +++--- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 34 +------------ .../ai/kilocode/client/ui/UiStyleTest.kt | 8 +-- 9 files changed, 80 insertions(+), 90 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index a9a30917d89..d7c74c3f3f8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.update.SessionController import ai.kilocode.client.session.update.SessionControllerEvent import ai.kilocode.client.session.update.SessionControllerListener @@ -93,7 +94,7 @@ class ConnectionPanel( // Keep the banner solid so expanded details cover transcript content beneath it. isOpaque = true background = UiStyle.Colors.bg() - border = JBUI.Borders.customLine(UiStyle.Colors.line(), 1, 0, 0, 0) + border = JBUI.Borders.customLine(SessionUiStyle.View.line(), 1, 0, 0, 0) left.add(toggle, BorderLayout.WEST) left.add(label, BorderLayout.CENTER) header.add(left, BorderLayout.CENTER) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 4473eec2c6e..ac4bc25f6e2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -144,10 +144,10 @@ class PromptPanel( border = JBUI.Borders.compound( JBUI.Borders.customLineTop(JBUI.CurrentTheme.ToolWindow.borderColor()), JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.Prompt.PANEL_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.Prompt.PANEL_HORIZONTAL_PADDING), - JBUI.scale(SessionUiStyle.Prompt.PANEL_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.Prompt.PANEL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING), ), ) @@ -157,14 +157,14 @@ class PromptPanel( val bar = BorderLayoutPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) isOpaque = false - border = JBUI.Borders.emptyTop(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP)) + border = JBUI.Borders.emptyTop(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)) } bar.add(mode) - bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP))) bar.add(model) - bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP))) bar.add(reasoning) - bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.Prompt.CONTROL_GAP))) + bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP))) bar.add(reset) bar.add(Box.createHorizontalGlue()) bar.add(button) @@ -217,8 +217,8 @@ class PromptPanel( editor.font = style.transcriptFont editor.getEditor(false)?.let(style::applyToEditor) editor.background = style.editorScheme.defaultBackground - val height = style.transcriptFont.size * SessionUiStyle.Prompt.EDITOR_LINES + JBUI.scale( - SessionUiStyle.Prompt.EDITOR_CHROME) + val height = style.transcriptFont.size * SessionUiStyle.View.Prompt.EDITOR_LINES + JBUI.scale( + SessionUiStyle.View.Prompt.EDITOR_CHROME) editor.preferredSize = JBDimension(0, height) editor.minimumSize = JBDimension(0, height) revalidate() @@ -323,8 +323,8 @@ class PromptPanel( } override fun getPreferredSize() = JBUI.size( - SessionUiStyle.Prompt.SEND_BUTTON_SIZE, - SessionUiStyle.Prompt.SEND_BUTTON_SIZE, + SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE, + SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE, ) override fun uiDataSnapshot(sink: DataSink) { @@ -341,7 +341,7 @@ class PromptPanel( try { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() - val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.Prompt.CORNER_ARC)) + val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC)) g2.fillRoundRect(0, 0, width, height, arc, arc) } finally { g2.dispose() @@ -358,22 +358,22 @@ class PromptPanel( } private inner class PromptShell : BorderLayoutPanel() { - private val arc = JBValue.UIInteger("Button.arc", SessionUiStyle.Prompt.CORNER_ARC) - private val focus = JBValue.UIInteger("Component.focusWidth", SessionUiStyle.Prompt.FOCUS_WIDTH) + private val arc = JBValue.UIInteger("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC) + private val focus = JBValue.UIInteger("Component.focusWidth", SessionUiStyle.View.Prompt.FOCUS_WIDTH) init { isOpaque = false border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.Prompt.SHELL_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.Prompt.SHELL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), ) } override fun updateUI() { super.updateUI() border = JBUI.Borders.empty( - JBUI.scale(SessionUiStyle.Prompt.SHELL_VERTICAL_PADDING), - JBUI.scale(SessionUiStyle.Prompt.SHELL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), ) } @@ -391,7 +391,7 @@ class PromptPanel( g2.color = if (active) { JBUI.CurrentTheme.Focus.focusColor() } else { - UiStyle.Colors.line() + SessionUiStyle.View.line() } val bw = if (active) focus.get() else JBUI.scale(1) for (idx in 0 until bw) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 1fb01ae28b4..9e8daa96a5a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -9,19 +9,6 @@ import javax.swing.border.Border /** Static UI tokens and helpers for session-specific Swing surfaces. */ object SessionUiStyle { - object Prompt { - const val EDITOR_LINES = 3 - const val EDITOR_CHROME = 16 - const val SEND_BUTTON_SIZE = 24 - const val CORNER_ARC = 6 - const val FOCUS_WIDTH = 2 - const val PANEL_VERTICAL_PADDING = 8 - const val PANEL_HORIZONTAL_PADDING = 12 - const val CONTROL_GAP = 4 - const val SHELL_VERTICAL_PADDING = 6 - const val SHELL_HORIZONTAL_PADDING = 8 - } - object SessionLayout { const val GAP = 4 const val TRANSCRIPT_PADDING = 12 @@ -35,6 +22,38 @@ object SessionUiStyle { const val CARD_HORIZONTAL_PADDING = 12 const val CARD_BODY_EXTRA_HEIGHT = 16 + internal const val BORDER_DELTA = 64 + internal const val HOVER_ALPHA = 0.35f + + /** Creates a visible separator against editor-derived transcript surfaces. */ + fun line(): Color = JBColor.lazy { UiStyle.Colors.contrast(UiStyle.Colors.panel(), BORDER_DELTA) } + + fun surface(): Color = UiStyle.Colors.panel() + + fun header(): Color = UiStyle.Colors.panel() + + /** Local hover color for collapsible transcript card headers. */ + fun headerHover(): Color = JBColor.lazy { UiStyle.Colors.blend(header(), line(), HOVER_ALPHA) } + + fun card(): Border = cardBorder() + + fun cardBorder(): Border = JBUI.Borders.customLine(line(), 1) + + fun cardTop(): Border = JBUI.Borders.customLineTop(line()) + + object Prompt { + const val EDITOR_LINES = 3 + const val EDITOR_CHROME = 16 + const val SEND_BUTTON_SIZE = 24 + const val CORNER_ARC = 6 + const val FOCUS_WIDTH = 2 + const val PANEL_VERTICAL_PADDING = 8 + const val PANEL_HORIZONTAL_PADDING = 12 + const val CONTROL_GAP = 4 + const val SHELL_VERTICAL_PADDING = 6 + const val SHELL_HORIZONTAL_PADDING = 8 + } + object Reasoning { const val BODY_LINES = 5 } @@ -71,12 +90,12 @@ object SessionUiStyle { object Dock { fun banner(): Border = JBUI.Borders.compound( - JBUI.Borders.customLineTop(UiStyle.Colors.line()), + JBUI.Borders.customLineTop(SessionUiStyle.View.line()), JBUI.Borders.empty(UiStyle.Gap.small(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()), )!! fun neutral(): Border = JBUI.Borders.compound( - JBUI.Borders.customLine(UiStyle.Colors.line(), 1), + JBUI.Borders.customLine(SessionUiStyle.View.line(), 1), JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()), )!! diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt index 07e4707c66f..df406de4762 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/CompactionView.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.session.model.Compaction import ai.kilocode.client.session.model.Content import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle import com.intellij.ui.components.JBLabel import java.awt.BorderLayout @@ -38,7 +39,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par applyStyle(SessionEditorStyle.current()) val line = { JPanel().apply { - background = UiStyle.Colors.line() + background = SessionUiStyle.View.line() isOpaque = true preferredSize = JBDimension(0, JBUI.scale(1)) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index 3f072556340..dbba074cdb2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -7,7 +7,6 @@ import ai.kilocode.client.session.ui.SessionView 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.RoundedLineBorder import com.intellij.util.ui.JBUI @@ -114,7 +113,7 @@ class MessageView( } private fun userBorder() = JBUI.Borders.compound( - RoundedLineBorder(UiStyle.Colors.line(), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_ARC)), + RoundedLineBorder(SessionUiStyle.View.line(), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_ARC)), JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.Message.USER_BORDER_HORIZONTAL_PADDING), diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt index a4bc47579b4..bec8c6b6fad 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ReasoningView.kt @@ -35,23 +35,23 @@ class ReasoningView(reasoning: Reasoning) : PartView() { private val arrow = JBLabel() private val body = TrackPanel().apply { isOpaque = true - background = UiStyle.Colors.surface() + background = SessionUiStyle.View.surface() border = JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val scroll = JBScrollPane(body).apply { - border = UiStyle.Borders.cardTop() + border = SessionUiStyle.View.cardTop() isOpaque = true - background = UiStyle.Colors.surface() - viewport.background = UiStyle.Colors.surface() + background = SessionUiStyle.View.surface() + viewport.background = SessionUiStyle.View.surface() horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED } private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true - background = UiStyle.Colors.header() + background = SessionUiStyle.View.header() border = JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), @@ -88,7 +88,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { init { layout = BorderLayout() isOpaque = false - border = UiStyle.Borders.card() + border = SessionUiStyle.View.card() val left = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = false @@ -180,7 +180,7 @@ class ReasoningView(reasoning: Reasoning) : PartView() { } private fun setHover(value: Boolean) { - val color = if (value) UiStyle.Colors.headerHover() else UiStyle.Colors.header() + val color = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header() if (header.background?.rgb == color.rgb) return header.background = color header.repaint() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index 01b00f28a88..6159f68ff40 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -40,12 +40,12 @@ class ToolView(tool: Tool) : PartView() { private val root = JPanel(BorderLayout()).apply { isOpaque = true - background = UiStyle.Colors.surface() - border = UiStyle.Borders.card() + background = SessionUiStyle.View.surface() + border = SessionUiStyle.View.card() } private val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.CARD_LAYOUT_GAP), 0)).apply { isOpaque = true - background = UiStyle.Colors.header() + background = SessionUiStyle.View.header() border = JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), @@ -74,17 +74,17 @@ class ToolView(tool: Tool) : PartView() { lineWrap = true wrapStyleWord = true foreground = bodyColor() - background = UiStyle.Colors.surface() + background = SessionUiStyle.View.surface() border = JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.CARD_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.CARD_HORIZONTAL_PADDING), ) } private val scroll = JBScrollPane(text).apply { - border = UiStyle.Borders.cardTop() + border = SessionUiStyle.View.cardTop() isOpaque = true - background = UiStyle.Colors.surface() - viewport.background = UiStyle.Colors.surface() + background = SessionUiStyle.View.surface() + viewport.background = SessionUiStyle.View.surface() horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED } @@ -201,7 +201,7 @@ class ToolView(tool: Tool) : PartView() { } private fun setHover(value: Boolean) { - val color = if (value) UiStyle.Colors.headerHover() else UiStyle.Colors.header() + val color = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header() if (same(header.background, color)) return header.background = color header.repaint() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 1c999bdbc1f..69aa6003ee4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -8,7 +8,6 @@ import java.awt.BorderLayout import java.awt.Color import javax.swing.JComponent import javax.swing.UIManager -import javax.swing.border.Border /** Static UI tokens and helpers for JetBrains session Swing surfaces. */ object UiStyle { @@ -18,32 +17,17 @@ object UiStyle { } object Colors { - internal const val BORDER_DELTA = 64 - internal const val HOVER_ALPHA = 0.35f - fun bg(): Color = UIUtil.getPanelBackground() fun fg(): Color = UIUtil.getLabelForeground() fun weak(): Color = UIUtil.getContextHelpForeground() - /** Creates a visible separator against editor-derived transcript surfaces. */ - fun line(): Color = JBColor.lazy { contrast(panel(), BORDER_DELTA) } - - fun surface(): Color = panel() - /** Uses the editor background so chat cards feel native beside editor content. */ fun panel(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground } - fun panelHover(): Color = JBColor.lazy { blend(panel(), line(), HOVER_ALPHA) } - - fun header(): Color = panel() - fun headerBar(): Color = JBUI.CurrentTheme.ToolWindow.headerBackground(false) - /** Local hover color for collapsible transcript card headers. */ - fun headerHover(): Color = panelHover() - fun error(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground()) fun warning(): Color = JBColor.lazy { @@ -86,24 +70,10 @@ object UiStyle { (color.red * 0.299 + color.green * 0.587 + color.blue * 0.114) >= 128 } - object Insets { - fun empty(): Border = JBUI.Borders.empty(Gap.pad()) - - fun header(): Border = JBUI.Borders.empty(Gap.lg(), Gap.lg()) - - fun body(): Border = JBUI.Borders.empty(Gap.lg(), Gap.pad()) - } - object Borders { - fun card(): Border = cardBorder() + fun warning() = JBUI.Borders.customLine(Colors.warning(), 1) - fun cardBorder(): Border = JBUI.Borders.customLine(Colors.line(), 1) - - fun cardTop(): Border = JBUI.Borders.customLineTop(Colors.line()) - - fun warning(): Border = JBUI.Borders.customLine(Colors.warning(), 1) - - fun picker(): Border = JBUI.Borders.empty(Gap.xs(), Gap.lg()) + fun picker() = JBUI.Borders.empty(Gap.xs(), Gap.lg()) } object Gap { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt index b9d61d73909..73269d10499 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt @@ -10,7 +10,7 @@ class UiStyleTest : BasePlatformTestCase() { fun `test border is lighter than dark panel`() { val panel = Color(0, 0, 0) - val border = UiStyle.Colors.contrast(panel, UiStyle.Colors.BORDER_DELTA) + val border = UiStyle.Colors.contrast(panel, SessionUiStyle.View.BORDER_DELTA) assertTrue(border.red > panel.red) assertTrue(border.green > panel.green) @@ -19,7 +19,7 @@ class UiStyleTest : BasePlatformTestCase() { fun `test border is darker than light panel`() { val panel = Color(255, 255, 255) - val border = UiStyle.Colors.contrast(panel, UiStyle.Colors.BORDER_DELTA) + val border = UiStyle.Colors.contrast(panel, SessionUiStyle.View.BORDER_DELTA) assertTrue(border.red < panel.red) assertTrue(border.green < panel.green) @@ -28,8 +28,8 @@ class UiStyleTest : BasePlatformTestCase() { fun `test hover blends from panel toward border`() { val panel = Color(0, 0, 0) - val border = UiStyle.Colors.contrast(panel, UiStyle.Colors.BORDER_DELTA) - val hover = UiStyle.Colors.blend(panel, border, UiStyle.Colors.HOVER_ALPHA) + val border = UiStyle.Colors.contrast(panel, SessionUiStyle.View.BORDER_DELTA) + val hover = UiStyle.Colors.blend(panel, border, SessionUiStyle.View.HOVER_ALPHA) assertTrue(hover.red > panel.red) assertTrue(hover.red < border.red) From d40c1713241ba41397f4e8f45d17ed1e8c4ee400 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:46:51 -0400 Subject: [PATCH 042/111] refactor(jetbrains): colocate component style tokens --- .../client/session/ui/ConnectionPanel.kt | 4 +-- .../client/session/ui/header/ContextBar.kt | 2 +- .../session/ui/mode/ModePickerRenderer.kt | 2 +- .../client/session/ui/style/SessionUiStyle.kt | 14 +++++++--- .../kilocode/client/session/views/ToolView.kt | 12 ++++----- .../ai/kilocode/client/ui/PickerButton.kt | 21 +++++++++++---- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 26 +++---------------- .../client/session/ui/ConnectionPanelTest.kt | 4 +-- 8 files changed, 43 insertions(+), 42 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index d7c74c3f3f8..7feab1bafa5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -136,7 +136,7 @@ class ConnectionPanel( } private fun showError(text: String, detail: String?) { - label.foreground = UiStyle.Colors.error() + label.foreground = UiStyle.Colors.errorLabelForeground() label.text = text retry.isVisible = true this.detail = detail?.takeIf { it.isNotBlank() } @@ -146,7 +146,7 @@ class ConnectionPanel( } private fun showWarning(text: String, detail: String?) { - label.foreground = UiStyle.Colors.warning() + label.foreground = UiStyle.Colors.warningLabelForeground() label.text = text retry.isVisible = true this.detail = detail?.takeIf { it.isNotBlank() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt index baad66fa87d..6d1053489b3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt @@ -143,7 +143,7 @@ private class Meter : JComponent() { fun reservedColor(): Color = shade(0.28f) private fun shade(alpha: Float): Color { - val base = background ?: UiStyle.Colors.panel() + val base = background ?: UiStyle.Colors.editorBackground() val grey = if (UiStyle.Colors.bright(base)) Color.BLACK else Color.WHITE return UiStyle.Colors.blend(base, grey, alpha) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt index 6d30dcfe76e..30d1b5c1615 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt @@ -82,7 +82,7 @@ internal class ModePickerRenderer( val focus = selected || list.hasFocus() || focused val fg = UIUtil.getListForeground(selected, focus) val weak = if (selected) fg else UiStyle.Colors.weak() - val warn = if (selected) fg else UiStyle.Colors.warning() + val warn = if (selected) fg else UiStyle.Colors.warningLabelForeground() background = list.background wrap.update(list, selected, focus) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 9e8daa96a5a..05865c446b0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -26,11 +26,11 @@ object SessionUiStyle { internal const val HOVER_ALPHA = 0.35f /** Creates a visible separator against editor-derived transcript surfaces. */ - fun line(): Color = JBColor.lazy { UiStyle.Colors.contrast(UiStyle.Colors.panel(), BORDER_DELTA) } + fun line(): Color = JBColor.lazy { UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA) } - fun surface(): Color = UiStyle.Colors.panel() + fun surface(): Color = UiStyle.Colors.editorBackground() - fun header(): Color = UiStyle.Colors.panel() + fun header(): Color = UiStyle.Colors.editorBackground() /** Local hover color for collapsible transcript card headers. */ fun headerHover(): Color = JBColor.lazy { UiStyle.Colors.blend(header(), line(), HOVER_ALPHA) } @@ -69,6 +69,14 @@ object SessionUiStyle { object Tool { const val BODY_LINES = 15 const val PREVIEW_LIMIT = 20_000 + + fun pending(): Color = UiStyle.Colors.weak() + + fun running(): Color = UiStyle.Colors.fg() + + fun completed(): Color = UiStyle.Colors.weak() + + fun error(): Color = UiStyle.Colors.errorLabelForeground() } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt index 6159f68ff40..39048dd38af 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ToolView.kt @@ -287,7 +287,7 @@ class ToolView(tool: Tool) : PartView() { repaint() } - private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.error() else UiStyle.Colors.fg() + private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg() private fun bodyMaxHeight(): Int { return text.getFontMetrics(text.font).height * bodyMaxRows() + @@ -354,14 +354,14 @@ private fun setFont(component: JComponent, font: Font): Boolean { private fun same(a: Color?, b: Color): Boolean = a?.rgb == b.rgb private fun color(tool: Tool) = when (tool.state) { - ToolExecState.PENDING -> UiStyle.Colors.weak() - ToolExecState.RUNNING -> UiStyle.Colors.running() - ToolExecState.COMPLETED -> UiStyle.Colors.weak() - ToolExecState.ERROR -> UiStyle.Colors.error() + ToolExecState.PENDING -> SessionUiStyle.View.Tool.pending() + ToolExecState.RUNNING -> SessionUiStyle.View.Tool.running() + ToolExecState.COMPLETED -> SessionUiStyle.View.Tool.completed() + ToolExecState.ERROR -> SessionUiStyle.View.Tool.error() } private fun titleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) { - UiStyle.Colors.error() + UiStyle.Colors.errorLabelForeground() } else { UiStyle.Colors.fg() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt index a080ebdbcf4..6a08c5bb9ce 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/PickerButton.kt @@ -1,19 +1,22 @@ package ai.kilocode.client.ui +import com.intellij.ui.JBColor import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import java.awt.Graphics import java.awt.Graphics2D import java.awt.RenderingHints import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import javax.swing.UIManager open class PickerButton : JBLabel() { private var over = false init { - border = UiStyle.Borders.picker() - background = UiStyle.Colors.picker() + border = pickerBorder() + background = picker() // The custom rounded fill needs parent background around the corners. isOpaque = false addMouseListener(object : MouseAdapter() { @@ -29,15 +32,15 @@ open class PickerButton : JBLabel() { override fun updateUI() { super.updateUI() - border = UiStyle.Borders.picker() - background = UiStyle.Colors.picker() + border = pickerBorder() + background = picker() } override fun paintComponent(g: Graphics) { val g2 = g.create() as Graphics2D try { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g2.color = if (isEnabled && over) UiStyle.Colors.pickerHover() else UiStyle.Colors.picker() + g2.color = if (isEnabled && over) JBUI.CurrentTheme.ActionButton.hoverBackground() else picker() val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) g2.fillRoundRect(0, 0, width, height, arc, arc) } finally { @@ -51,4 +54,12 @@ open class PickerButton : JBLabel() { over = value repaint() } + + private fun picker() = JBColor.lazy { + UIManager.getColor("ComboBoxButton.background") + ?: UIManager.getColor("ComboBox.nonEditableBackground") + ?: UIUtil.getPanelBackground() + } + + private fun pickerBorder() = JBUI.Borders.empty(UiStyle.Gap.xs(), UiStyle.Gap.lg()) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 69aa6003ee4..c9ac2b4e890 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -11,10 +11,6 @@ import javax.swing.UIManager /** Static UI tokens and helpers for JetBrains session Swing surfaces. */ object UiStyle { - object Size { - const val WIDTH = 350 - const val BUTTON_WIDTH = 28 - } object Colors { fun bg(): Color = UIUtil.getPanelBackground() @@ -24,28 +20,16 @@ object UiStyle { fun weak(): Color = UIUtil.getContextHelpForeground() /** Uses the editor background so chat cards feel native beside editor content. */ - fun panel(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground } + fun editorBackground(): Color = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground } - fun headerBar(): Color = JBUI.CurrentTheme.ToolWindow.headerBackground(false) + fun errorLabelForeground(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground()) - fun error(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground()) - - fun warning(): Color = JBColor.lazy { + fun warningLabelForeground(): Color = JBColor.lazy { UIManager.getColor("Component.warningFocusColor") ?: UIManager.getColor("Label.warningForeground") ?: UIUtil.getContextHelpForeground() } - fun running(): Color = JBColor.namedColor("ProgressBar.foreground", UIUtil.getLabelForeground()) - - fun picker(): Color = JBColor.lazy { - UIManager.getColor("ComboBoxButton.background") - ?: UIManager.getColor("ComboBox.nonEditableBackground") - ?: UIUtil.getPanelBackground() - } - - fun pickerHover(): Color = JBUI.CurrentTheme.ActionButton.hoverBackground() - internal fun contrast(base: Color, delta: Int): Color { val step = if (bright(base)) -delta else delta return Color( @@ -71,9 +55,7 @@ object UiStyle { } object Borders { - fun warning() = JBUI.Borders.customLine(Colors.warning(), 1) - - fun picker() = JBUI.Borders.empty(Gap.xs(), Gap.lg()) + fun warning() = JBUI.Borders.customLine(Colors.warningLabelForeground(), 1) } object Gap { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index a2c12370def..68196e0280c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -45,7 +45,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertTrue(panel.isVisible) assertEquals("CLI startup failed", panel.summaryText()) - assertEquals(UiStyle.Colors.error(), panel.summaryColor()) + assertEquals(UiStyle.Colors.errorLabelForeground(), panel.summaryColor()) assertTrue(panel.toggleVisible()) assertFalse(panel.toggleExpanded()) assertFalse(panel.detailsVisible()) @@ -103,7 +103,7 @@ class ConnectionPanelTest : SessionControllerTestBase() { assertTrue(panel.isVisible) assertEquals("Configuration warnings", panel.summaryText()) - assertEquals(UiStyle.Colors.warning(), panel.summaryColor()) + assertEquals(UiStyle.Colors.warningLabelForeground(), panel.summaryColor()) assertTrue(panel.toggleVisible()) assertFalse(panel.toggleExpanded()) assertFalse(panel.detailsVisible()) From 6f0b15b7cd6ade3623aa13d432dde98ad17c753e Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:50:53 -0400 Subject: [PATCH 043/111] refactor(jetbrains): simplify ui gap tokens --- .../client/session/ui/ConnectionPanel.kt | 6 ++-- .../client/session/ui/header/ContextBar.kt | 2 +- .../session/ui/header/SessionHeaderPanel.kt | 12 ++++---- .../session/ui/mode/ModePickerRenderer.kt | 2 +- .../client/session/ui/style/SessionUiStyle.kt | 5 ++-- .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 30 ++++++++----------- 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 7feab1bafa5..7b1a8787ee2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -40,11 +40,11 @@ class ConnectionPanel( } private val header = BorderLayoutPanel().apply { - border = JBUI.Borders.empty(UiStyle.Gap.small(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()) } private val left = BorderLayoutPanel().apply { - layout = UiStyle.Gap.layout(UiStyle.Gap.small()) + layout = BorderLayout(UiStyle.Gap.sm(), 0) addMouseListener(click) } @@ -77,7 +77,7 @@ class ConnectionPanel( } private val scroll = JBScrollPane(details).apply { - border = JBUI.Borders.empty(0, UiStyle.Gap.lg(), UiStyle.Gap.small(), 0) + border = JBUI.Borders.empty(0, UiStyle.Gap.lg(), UiStyle.Gap.sm(), 0) // Match the banner background while retaining platform scroll behavior. isOpaque = false viewport.isOpaque = false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt index 6d1053489b3..e15a525fdd1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/ContextBar.kt @@ -22,7 +22,7 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) { init { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) add(used, BorderLayout.WEST) add(meter, BorderLayout.CENTER) add(limit, BorderLayout.EAST) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index cd53a8edbd5..6ccb1df9ab3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -96,24 +96,24 @@ class SessionHeaderPanel( } private val tokens = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) add(tokenTitle) add(Box.createHorizontalStrut(UiStyle.Gap.md())) add(input) - add(Box.createHorizontalStrut(UiStyle.Gap.small())) + add(Box.createHorizontalStrut(UiStyle.Gap.sm())) add(output) - add(Box.createHorizontalStrut(UiStyle.Gap.small())) + add(Box.createHorizontalStrut(UiStyle.Gap.sm())) add(cacheRead) } private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0)).apply { isOpaque = false - border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) add(todos) } private val body = JPanel().apply { isOpaque = false layout = BoxLayout(this, BoxLayout.Y_AXIS) - border = JBUI.Borders.empty(UiStyle.Gap.small(), 0, 0, 0) + border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) add(viewport) add(tokens) add(bar) @@ -179,7 +179,7 @@ class SessionHeaderPanel( super.updateUI() border = JBUI.Borders.compound( JBUI.Borders.customLine(JBUI.CurrentTheme.ToolWindow.borderColor(), 1, 0, 1, 0), - JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad(), UiStyle.Gap.small(), UiStyle.Gap.pad()), + JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad(), UiStyle.Gap.sm(), UiStyle.Gap.pad()), ) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt index 30d1b5c1615..d1c882174c8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt @@ -98,7 +98,7 @@ internal class ModePickerRenderer( badge.border = JBUI.Borders.compound( JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()), JBUI.Borders.compound( - RoundedLineBorder(warn, UiStyle.Gap.small()), + RoundedLineBorder(warn, UiStyle.Gap.sm()), JBUI.Borders.empty(0, UiStyle.Gap.md()), ), ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 05865c446b0..2233043107c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.session.ui.style import ai.kilocode.client.ui.UiStyle import com.intellij.ui.JBColor import com.intellij.util.ui.JBUI +import com.intellij.util.ui.JBUI.Borders.customLine import com.intellij.util.ui.UIUtil import java.awt.Color import javax.swing.border.Border @@ -99,7 +100,7 @@ object SessionUiStyle { object Dock { fun banner(): Border = JBUI.Borders.compound( JBUI.Borders.customLineTop(SessionUiStyle.View.line()), - JBUI.Borders.empty(UiStyle.Gap.small(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()), + JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()), )!! fun neutral(): Border = JBUI.Borders.compound( @@ -108,7 +109,7 @@ object Dock { )!! fun warning(): Border = JBUI.Borders.compound( - UiStyle.Borders.warning(), + customLine(UiStyle.Colors.warningLabelForeground(), 1), JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.pad()), )!! } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index c9ac2b4e890..2d8097ef756 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -4,7 +4,6 @@ import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.JBColor import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil -import java.awt.BorderLayout import java.awt.Color import javax.swing.JComponent import javax.swing.UIManager @@ -12,6 +11,18 @@ import javax.swing.UIManager /** Static UI tokens and helpers for JetBrains session Swing surfaces. */ object UiStyle { + object Gap { + fun xs() = JBUI.scale(2) + + fun md() = JBUI.scale(6) + + fun lg() = JBUI.scale(8) + + fun sm() = JBUI.scale(4) + + fun pad() = JBUI.scale(12) + } + object Colors { fun bg(): Color = UIUtil.getPanelBackground() @@ -54,23 +65,6 @@ object UiStyle { (color.red * 0.299 + color.green * 0.587 + color.blue * 0.114) >= 128 } - object Borders { - fun warning() = JBUI.Borders.customLine(Colors.warningLabelForeground(), 1) - } - - object Gap { - fun xs() = JBUI.scale(2) - - fun md() = JBUI.scale(6) - - fun lg() = JBUI.scale(8) - - fun small() = JBUI.scale(4) - - fun pad() = JBUI.scale(12) - - fun layout(gap: Int = lg()) = BorderLayout(gap, 0) - } object Components { fun transparent(component: JComponent) { From 8326a5a8061e5b312b563b5864aab18a02c26325 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:55:12 -0400 Subject: [PATCH 044/111] docs(jetbrains): document ui style tokens --- .../client/session/ui/style/SessionEditorStyle.kt | 10 ++++++++-- .../client/session/ui/style/SessionUiStyle.kt | 11 ++++++++++- .../src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt | 6 ++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt index e32fcaafe71..d510a0fdebe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionEditorStyle.kt @@ -9,7 +9,12 @@ import java.awt.Color import java.awt.Font import kotlin.math.roundToInt -/** Snapshot of editor-derived transcript styling applied to live session components. */ +/** + * Immutable snapshot of editor-derived fonts and colors for transcript components. + * + * Session UI uses this instead of reading editor globals in every component so font and color changes can be applied + * consistently through [SessionEditorStyleTarget]. + */ data class SessionEditorStyle( val editorScheme: EditorColorsScheme, val editorFamily: String, @@ -30,6 +35,7 @@ data class SessionEditorStyle( } companion object { + /** Builds a style snapshot from the current global editor color scheme. */ fun current(): SessionEditorStyle { val scheme = EditorColorsManager.getInstance().globalScheme return create(scheme, scheme.editorFontName, scheme.editorFontSize) @@ -66,7 +72,7 @@ data class SessionEditorStyle( } } -/** Implemented by session components that can update styling in place. */ +/** Session component contract for applying a refreshed [SessionEditorStyle] without rebuilding Swing nodes. */ interface SessionEditorStyleTarget { fun applyStyle(style: SessionEditorStyle) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index 2233043107c..9ace86801c3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -8,8 +8,9 @@ import com.intellij.util.ui.UIUtil import java.awt.Color import javax.swing.border.Border -/** Static UI tokens and helpers for session-specific Swing surfaces. */ +/** Static style tokens owned by the chat session UI. */ object SessionUiStyle { + /** Geometry for the transcript list and its scroll behavior. */ object SessionLayout { const val GAP = 4 const val TRANSCRIPT_PADDING = 12 @@ -17,6 +18,7 @@ object SessionUiStyle { const val SCROLL_INCREMENT = 16 } + /** Shared tokens for individual transcript views and cards. */ object View { const val CARD_LAYOUT_GAP = 6 const val CARD_VERTICAL_PADDING = 8 @@ -42,6 +44,7 @@ object SessionUiStyle { fun cardTop(): Border = JBUI.Borders.customLineTop(line()) + /** Prompt input dimensions and chrome inside the session view. */ object Prompt { const val EDITOR_LINES = 3 const val EDITOR_CHROME = 16 @@ -55,10 +58,12 @@ object SessionUiStyle { const val SHELL_HORIZONTAL_PADDING = 8 } + /** Reasoning block preview sizing. */ object Reasoning { const val BODY_LINES = 5 } + /** Message container roles and user bubble geometry. */ object Message { const val USER_ROLE = "user" const val ASSISTANT_ROLE = "assistant" @@ -67,6 +72,7 @@ object SessionUiStyle { const val USER_BORDER_HORIZONTAL_PADDING = 12 } + /** Tool card preview limits and state colors. */ object Tool { const val BODY_LINES = 15 const val PREVIEW_LIMIT = 20_000 @@ -81,11 +87,13 @@ object SessionUiStyle { } } + /** Limits for the empty-state recent sessions list. */ object RecentSessions { const val LIMIT = 5 const val DESCRIPTION_WIDTH = 250 } + /** Colors for timeline/activity indicators in the session header. */ object Timeline { val READ: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff)) val WRITE: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4)) @@ -97,6 +105,7 @@ object SessionUiStyle { } } +/** Border presets for question, permission, and connection dock panels. */ object Dock { fun banner(): Border = JBUI.Borders.compound( JBUI.Borders.customLineTop(SessionUiStyle.View.line()), diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 2d8097ef756..5c648e6ed9c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -8,9 +8,10 @@ import java.awt.Color import javax.swing.JComponent import javax.swing.UIManager -/** Static UI tokens and helpers for JetBrains session Swing surfaces. */ +/** Shared Swing style tokens that are not tied to one session component. */ object UiStyle { + /** DPI-aware spacing primitives used across custom Swing layouts. */ object Gap { fun xs() = JBUI.scale(2) @@ -23,6 +24,7 @@ object UiStyle { fun pad() = JBUI.scale(12) } + /** Theme-aware colors and color math used by multiple UI surfaces. */ object Colors { fun bg(): Color = UIUtil.getPanelBackground() @@ -65,7 +67,7 @@ object UiStyle { (color.red * 0.299 + color.green * 0.587 + color.blue * 0.114) >= 128 } - + /** Small component helpers that keep repeated Swing setup in one place. */ object Components { fun transparent(component: JComponent) { component.isOpaque = false From d63c752998aec01efb9c35d1edf4952dc65d8ba6 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 18:57:12 -0400 Subject: [PATCH 045/111] refactor(jetbrains): batch transparent component setup --- .../session/history/HistoryListRenderer.kt | 6 +----- .../session/ui/mode/ModePickerRenderer.kt | 21 +++++-------------- .../session/ui/model/ModelPickerRenderer.kt | 7 +------ .../kotlin/ai/kilocode/client/ui/UiStyle.kt | 4 ++-- 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index b99f592cac3..a7fb4e2c961 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -83,11 +83,7 @@ internal open class HistoryRenderer( isOpaque = true top.isOpaque = true row.border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) - UiStyle.Components.transparent(row) - UiStyle.Components.transparent(main) - UiStyle.Components.transparent(title) - UiStyle.Components.transparent(time) - UiStyle.Components.transparent(del) + UiStyle.Components.transparent(row, main, title, time, del) wrap.setContent(row) add(top, BorderLayout.NORTH) add(wrap, BorderLayout.CENTER) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt index d1c882174c8..fae243c8e12 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt @@ -32,31 +32,20 @@ internal class ModePickerRenderer( private val icon = JBLabel().apply { horizontalAlignment = SwingConstants.CENTER verticalAlignment = SwingConstants.CENTER - UiStyle.Components.transparent(this) - } - private val title = SimpleColoredComponent().apply { - UiStyle.Components.transparent(this) - } - private val desc = SimpleColoredComponent().apply { - UiStyle.Components.transparent(this) - } - private val badge = JBLabel(KiloBundle.message("mode.picker.deprecated")).apply { - UiStyle.Components.transparent(this) } + private val title = SimpleColoredComponent() + private val desc = SimpleColoredComponent() + private val badge = JBLabel(KiloBundle.message("mode.picker.deprecated")) private val head = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply { - UiStyle.Components.transparent(this) add(title) add(badge) } - private val body = JPanel(BorderLayout()).apply { - UiStyle.Components.transparent(this) - } + private val body = JPanel(BorderLayout()) private val row = JPanel(BorderLayout()) private val wrap = PickerRow() init { - UiStyle.Components.transparent(this) - UiStyle.Components.transparent(row) + UiStyle.Components.transparent(this, icon, title, desc, badge, head, body, row) (row.layout as BorderLayout).hgap = UiStyle.Gap.md() row.border = JBUI.Borders.empty( UiStyle.Gap.md(), diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 64889ecc9d8..0a51cbaee15 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -93,12 +93,7 @@ internal class ModelPickerRenderer( init { isOpaque = true top.isOpaque = true - UiStyle.Components.transparent(row) - UiStyle.Components.transparent(check) - UiStyle.Components.transparent(title) - UiStyle.Components.transparent(head) - UiStyle.Components.transparent(provider) - UiStyle.Components.transparent(star) + UiStyle.Components.transparent(row, check, title, head, provider, star) row.border = JBUI.Borders.empty( UiStyle.Gap.md(), UiStyle.Gap.lg(), diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index 5c648e6ed9c..850ab9f84dd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -69,8 +69,8 @@ object UiStyle { /** Small component helpers that keep repeated Swing setup in one place. */ object Components { - fun transparent(component: JComponent) { - component.isOpaque = false + fun transparent(vararg components: JComponent) { + components.forEach { it.isOpaque = false } } } } From 73e6b40fb9d3a4cef1cb61ad263aedca4694a5ff Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 20:18:23 -0400 Subject: [PATCH 046/111] refactor(jetbrains): rename session controller package --- .../kotlin/ai/kilocode/client/app/KiloSessionService.kt | 2 +- .../src/main/kotlin/ai/kilocode/client/session/SessionUi.kt | 6 +++--- .../client/session/{update => controller}/DelayedState.kt | 2 +- .../session/{update => controller}/SessionController.kt | 2 +- .../{update => controller}/SessionControllerEvent.kt | 2 +- .../session/{update => controller}/SessionQueueCondenser.kt | 2 +- .../session/{update => controller}/SessionUpdateQueue.kt | 2 +- .../kotlin/ai/kilocode/client/session/model/SessionModel.kt | 2 +- .../kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt | 6 +++--- .../ai/kilocode/client/session/ui/EmptySessionPanel.kt | 2 +- .../kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt | 2 +- .../kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt | 2 +- .../kilocode/client/session/ui/header/SessionHeaderPanel.kt | 2 +- .../kilocode/client/session/SessionSidePanelManagerTest.kt | 4 ++-- .../ai/kilocode/client/session/SessionUiFactoryTest.kt | 4 ++-- .../ai/kilocode/client/session/SessionUiLayoutTest.kt | 2 +- .../kotlin/ai/kilocode/client/session/SessionUiTestBase.kt | 2 +- .../session/{update => controller}/AppWatchingTest.kt | 2 +- .../session/{update => controller}/ChatLoggingFlowTest.kt | 2 +- .../session/{update => controller}/ConfigSelectionTest.kt | 2 +- .../session/{update => controller}/ConnectionDelayTest.kt | 2 +- .../session/{update => controller}/DelayedStateTest.kt | 2 +- .../session/{update => controller}/DisposedStateTest.kt | 2 +- .../session/{update => controller}/HistoryLoadingTest.kt | 2 +- .../session/{update => controller}/ListenerLifecycleTest.kt | 2 +- .../session/{update => controller}/MessageListTest.kt | 2 +- .../session/{update => controller}/ProgressTrackingTest.kt | 2 +- .../session/{update => controller}/PromptLifecycleTest.kt | 2 +- .../session/{update => controller}/SessionArtifactsTest.kt | 2 +- .../{update => controller}/SessionControllerTestBase.kt | 4 ++-- .../session/{update => controller}/SessionCreationTest.kt | 2 +- .../{update => controller}/SessionHeaderControllerTest.kt | 2 +- .../{update => controller}/SessionQueueCondenserTest.kt | 2 +- .../session/{update => controller}/SessionRecoveryTest.kt | 4 ++-- .../{update => controller}/SessionUpdateQueueTest.kt | 2 +- .../session/{update => controller}/StatusComputationTest.kt | 2 +- .../session/{update => controller}/TurnLifecycleTest.kt | 2 +- .../session/{update => controller}/ViewSwitchingTest.kt | 2 +- .../session/{update => controller}/WorkspaceWatchingTest.kt | 2 +- .../ai/kilocode/client/session/ui/ConnectionPanelTest.kt | 6 +++--- .../ai/kilocode/client/session/ui/EmptySessionPanelTest.kt | 2 +- .../ai/kilocode/client/session/ui/PermissionPanelTest.kt | 2 +- .../ai/kilocode/client/session/ui/QuestionPanelTest.kt | 2 +- .../client/session/ui/header/SessionHeaderPanelTest.kt | 4 ++-- 44 files changed, 55 insertions(+), 55 deletions(-) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/{update => controller}/DelayedState.kt (97%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/{update => controller}/SessionController.kt (99%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/{update => controller}/SessionControllerEvent.kt (97%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/{update => controller}/SessionQueueCondenser.kt (98%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/{update => controller}/SessionUpdateQueue.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/AppWatchingTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ChatLoggingFlowTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ConfigSelectionTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ConnectionDelayTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/DelayedStateTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/DisposedStateTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/HistoryLoadingTest.kt (97%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ListenerLifecycleTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/MessageListTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ProgressTrackingTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/PromptLifecycleTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionArtifactsTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionControllerTestBase.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionCreationTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionHeaderControllerTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionQueueCondenserTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionRecoveryTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/SessionUpdateQueueTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/StatusComputationTest.kt (96%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/TurnLifecycleTest.kt (98%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/ViewSwitchingTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/{update => controller}/WorkspaceWatchingTest.kt (97%) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index b047162ee64..a011ad885e6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.launch * Project-level frontend service for session management. * * Stateless with respect to "active session" — callers pass explicit - * session IDs. [ai.kilocode.client.session.update.SessionController] owns the + * session IDs. [ai.kilocode.client.session.controller.SessionController] owns the * active session concept. */ @Service(Service.Level.PROJECT) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index dfd2e8ab37c..cb316ab2d93 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -19,9 +19,9 @@ import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.header.SessionHeaderPanel import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget -import ai.kilocode.client.session.update.EVENT_FLUSH_MS -import ai.kilocode.client.session.update.SessionController -import ai.kilocode.client.session.update.SessionControllerEvent +import ai.kilocode.client.session.controller.EVENT_FLUSH_MS +import ai.kilocode.client.session.controller.SessionController +import ai.kilocode.client.session.controller.SessionControllerEvent import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import com.intellij.ide.ui.LafManagerListener diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/DelayedState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt similarity index 97% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/DelayedState.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt index 2dba541f6b1..623524e2c0a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/DelayedState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 2d2369f46ac..3c0da06ba82 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionControllerEvent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionControllerEvent.kt similarity index 97% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionControllerEvent.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionControllerEvent.kt index 121af83b5d8..ba6217af592 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionControllerEvent.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionControllerEvent.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModel import ai.kilocode.client.session.model.SessionModelEvent diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionQueueCondenser.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenser.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionQueueCondenser.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenser.kt index dcb06f4e6ca..4c8ca98a74f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionQueueCondenser.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenser.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionUpdateQueue.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionUpdateQueue.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt index 770c9ee388c..7c35f9dde2e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/update/SessionUpdateQueue.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueue.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 230cda662fe..65e04de0e40 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -18,7 +18,7 @@ import kotlin.math.roundToInt /** * Pure session model — single source of truth for session content and runtime state. * - * **EDT-only access** — no synchronization. [ai.kilocode.client.session.update.SessionController] guarantees all + * **EDT-only access** — no synchronization. [ai.kilocode.client.session.controller.SessionController] guarantees all * reads and writes happen on the EDT. * * In addition to the flat message list, the model maintains a derived diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt index 7b1a8787ee2..9b6beb55e8c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ConnectionPanel.kt @@ -2,9 +2,9 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.style.SessionUiStyle -import ai.kilocode.client.session.update.SessionController -import ai.kilocode.client.session.update.SessionControllerEvent -import ai.kilocode.client.session.update.SessionControllerListener +import ai.kilocode.client.session.controller.SessionController +import ai.kilocode.client.session.controller.SessionControllerEvent +import ai.kilocode.client.session.controller.SessionControllerListener import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 9ea187446e8..968f2754236 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -9,7 +9,7 @@ import ai.kilocode.client.session.history.title import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.ui.style.SessionUiStyle -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.ui.CenterShrinkPanel import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.SessionDto diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt index d7a444abfcf..49e8b14261c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PermissionPanel.kt @@ -3,7 +3,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.ui.style.Dock -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.rpc.dto.PermissionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt index b84439d765d..144a703cbbe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/QuestionPanel.kt @@ -3,7 +3,7 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.ui.style.Dock -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.rpc.dto.QuestionReplyDto import com.intellij.icons.AllIcons import com.intellij.ui.dsl.builder.RightGap diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index 6ccb1df9ab3..9ad1c33d7a4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -5,7 +5,7 @@ import ai.kilocode.client.session.model.SessionHeaderSnapshot import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.TokensDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 92d5d5d5d45..74272ac98ca 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -396,10 +396,10 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { private fun active(manager: SessionSidePanelManager) = manager.component.getComponent(0) as JPanel - private fun JPanel.controller(): ai.kilocode.client.session.update.SessionController { + private fun JPanel.controller(): ai.kilocode.client.session.controller.SessionController { val field = SessionUi::class.java.getDeclaredField("controller") field.isAccessible = true - return field.get(this) as ai.kilocode.client.session.update.SessionController + return field.get(this) as ai.kilocode.client.session.controller.SessionController } private fun remove(manager: SessionSidePanelManager, id: String) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt index b5acef14090..75b3c17d393 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiFactoryTest.kt @@ -94,10 +94,10 @@ class SessionUiFactoryTest : BasePlatformTestCase() { assertEquals(1, manager.history) } - private fun controller(ui: SessionUi): ai.kilocode.client.session.update.SessionController { + private fun controller(ui: SessionUi): ai.kilocode.client.session.controller.SessionController { val field = SessionUi::class.java.getDeclaredField("controller") field.isAccessible = true - return field.get(ui) as ai.kilocode.client.session.update.SessionController + return field.get(ui) as ai.kilocode.client.session.controller.SessionController } fun `test application service is available`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 4b9ab0e5db7..be781893329 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -16,7 +16,7 @@ import ai.kilocode.client.session.ui.QuestionPanel import ai.kilocode.client.session.ui.SessionMessageListPanel import ai.kilocode.client.session.ui.SessionRootPanel import ai.kilocode.client.session.ui.header.SessionHeaderPanel -import ai.kilocode.client.session.update.SessionControllerEvent +import ai.kilocode.client.session.controller.SessionControllerEvent import ai.kilocode.rpc.dto.MessageWithPartsDto import com.intellij.ui.components.JBScrollPane import javax.swing.JLayeredPane diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index 95d35e1f7ba..a1d28529c71 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -6,7 +6,7 @@ import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.ui.SessionRootPanel import ai.kilocode.client.session.ui.prompt.PromptPanel -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/AppWatchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/AppWatchingTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/AppWatchingTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/AppWatchingTest.kt index efda159ca97..0309baec13d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/AppWatchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/AppWatchingTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ChatLoggingFlowTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ChatLoggingFlowTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ChatLoggingFlowTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ChatLoggingFlowTest.kt index a2852df403f..312913c29c1 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ChatLoggingFlowTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ChatLoggingFlowTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConfigSelectionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConfigSelectionTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConfigSelectionTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConfigSelectionTest.kt index 03b6681cf06..1a986bc6c9a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConfigSelectionTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConfigSelectionTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.AgentDto import ai.kilocode.rpc.dto.AgentConfigDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConnectionDelayTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConnectionDelayTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConnectionDelayTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConnectionDelayTest.kt index d5269a7ca86..4c4c7172c71 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ConnectionDelayTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ConnectionDelayTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.KiloAppStateDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DelayedStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DelayedStateTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt index 8f6e5cfc3e0..349171f6549 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DelayedStateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DisposedStateTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DisposedStateTest.kt index c3225a809dc..e4920f663f1 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/DisposedStateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DisposedStateTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/HistoryLoadingTest.kt similarity index 97% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/HistoryLoadingTest.kt index 4098f5ed8f7..5d8d5350a81 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/HistoryLoadingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/HistoryLoadingTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.rpc.dto.MessageWithPartsDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ListenerLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ListenerLifecycleTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ListenerLifecycleTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ListenerLifecycleTest.kt index e5d61e373fc..fa0e118ba2c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ListenerLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ListenerLifecycleTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionState import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/MessageListTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/MessageListTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/MessageListTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/MessageListTest.kt index b09da2064e1..381995d06fd 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/MessageListTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/MessageListTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ProgressTrackingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ProgressTrackingTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ProgressTrackingTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ProgressTrackingTest.kt index e6885853521..dbe7c98d4ea 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ProgressTrackingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ProgressTrackingTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/PromptLifecycleTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index 8e3cea3ba9f..727f527b25f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionState import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionArtifactsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionArtifactsTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionArtifactsTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionArtifactsTest.kt index d2afd1387e5..33a2620a502 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionArtifactsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionArtifactsTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.DiffFileDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt index 091364ef108..214df032c3b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService @@ -40,7 +40,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking /** - * Base class for [ai.kilocode.client.session.update.SessionController] tests. + * Base class for [ai.kilocode.client.session.controller.SessionController] tests. * * Provides real IntelliJ Application/EDT/Disposer via [BasePlatformTestCase], * real frontend services wired to fake RPC backends, and shared helpers. diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionCreationTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionCreationTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt index 936c8851466..698dec3f5e0 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionCreationTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.KiloAppStateDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionHeaderControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionHeaderControllerTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionHeaderControllerTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionHeaderControllerTest.kt index 8ebea6e5676..b9b3c4fe21e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionHeaderControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionHeaderControllerTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionQueueCondenserTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenserTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionQueueCondenserTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenserTest.kt index c7871d4332e..7db99809802 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionQueueCondenserTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionQueueCondenserTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.DiffFileDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionRecoveryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionRecoveryTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt index e407fb61b49..80229177999 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionRecoveryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionState import ai.kilocode.rpc.dto.PermissionRequestDto @@ -10,7 +10,7 @@ import ai.kilocode.rpc.dto.SessionStatusDto * Tests for pending permission/question recovery after history load. * * VS Code rehydrates pending prompts by calling list endpoints after - * reconnect. JetBrains now does the same in [ai.kilocode.client.session.update.SessionController.recoverPending]. + * reconnect. JetBrains now does the same in [ai.kilocode.client.session.controller.SessionController.recoverPending]. */ class SessionRecoveryTest : SessionControllerTestBase() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionUpdateQueueTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionUpdateQueueTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt index 871db12db3f..1a10efeabc6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/SessionUpdateQueueTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/StatusComputationTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/StatusComputationTest.kt similarity index 96% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/StatusComputationTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/StatusComputationTest.kt index 27adf704416..a629dc76876 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/StatusComputationTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/StatusComputationTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/TurnLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt similarity index 98% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/TurnLifecycleTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt index 80aa8367511..bff862e380d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/TurnLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.model.SessionState import ai.kilocode.rpc.dto.ChatEventDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt similarity index 99% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt index a7b75edb514..b54c344e74a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/ViewSwitchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.model.SessionState diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/WorkspaceWatchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt similarity index 97% rename from packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/WorkspaceWatchingTest.kt rename to packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt index 595bbebb300..96f63afe9b5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/update/WorkspaceWatchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt @@ -1,4 +1,4 @@ -package ai.kilocode.client.session.update +package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.AgentDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index 68196e0280c..16536b70276 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -1,8 +1,8 @@ package ai.kilocode.client.session.ui -import ai.kilocode.client.session.update.SessionController -import ai.kilocode.client.session.update.SessionControllerEvent -import ai.kilocode.client.session.update.SessionControllerTestBase +import ai.kilocode.client.session.controller.SessionController +import ai.kilocode.client.session.controller.SessionControllerEvent +import ai.kilocode.client.session.controller.SessionControllerTestBase import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.KiloAppStateDto diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt index 78a714cd909..ca717ded514 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/EmptySessionPanelTest.kt @@ -8,7 +8,7 @@ import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem import ai.kilocode.client.session.ui.style.SessionUiStyle -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt index 39bec08835d..4f39bb9810a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PermissionPanelTest.kt @@ -7,7 +7,7 @@ import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionMeta import ai.kilocode.client.session.SessionRef -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt index 8a90882732e..50d44ea348d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/QuestionPanelTest.kt @@ -5,7 +5,7 @@ import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionRef -import ai.kilocode.client.session.update.SessionController +import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.model.QuestionItem import ai.kilocode.client.session.model.QuestionOption diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt index 7d4af0f3fff..d829d6bb329 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt @@ -6,7 +6,7 @@ import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.ToolKind import ai.kilocode.client.session.ui.style.SessionEditorStyle -import ai.kilocode.client.session.update.SessionControllerTestBase +import ai.kilocode.client.session.controller.SessionControllerTestBase import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto @@ -464,7 +464,7 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { assertEquals(0, panel.timelineViewport().viewPosition.y) } - private fun promptedHeader(): ai.kilocode.client.session.update.SessionController { + private fun promptedHeader(): ai.kilocode.client.session.controller.SessionController { appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto( ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"), From e7126ad8e05d75c2df435643c4557913ff6fb308 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 8 May 2026 22:07:30 -0400 Subject: [PATCH 047/111] docs(jetbrains): document ui style constant ownership --- packages/kilo-jetbrains/AGENTS.md | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index e9d9d3f71f1..7d7b12d3bf0 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -118,6 +118,20 @@ The chat session feature uses a three-layer Model / Controller / View architectu 3. Handle the new `ChatEventDto` in `SessionController.handle()` by calling the model mutation method. 4. Add `-> Unit` stubs for the new event in any existing exhaustive `when` blocks in view code. +### Editor-Dependent Session Styling + +Session UI components that render text using editor fonts or colors must not read global editor settings directly on every paint. Instead they must: + +1. Implement `SessionEditorStyleTarget` (`session/ui/style/SessionEditorStyle.kt`). +2. Hold a snapshot field initialised with `SessionEditorStyle.current()`. +3. Override `applyStyle(style: SessionEditorStyle)` and update all fonts/colors in one place without rebuilding Swing nodes. + +`SessionUi` propagates a refreshed `SessionEditorStyle` to every registered `SessionEditorStyleTarget` child when the global editor scheme changes. New session elements that depend on editor settings must be registered through this flow, not through ad hoc `EditorColorsManager` listeners. + +**Anti-patterns to avoid:** +- Do not pass `SessionEditorStyle` fields through constructors or method parameters when the component can implement the interface and receive updates via `applyStyle`. +- Do not store individual style properties (e.g. a separate `font` field copied from the style) when holding the full `SessionEditorStyle` snapshot is cleaner. + ### Testing Controller tests extend `SessionControllerTestBase` (`test/…/session/SessionControllerTestBase.kt`), @@ -714,6 +728,28 @@ Inspection `Plugin DevKit | Code | Undesirable class usage` highlights when you - Always create via `JBUI.Borders.empty(top, left, bottom, right)` and `JBUI.insets()` — DPI-aware and auto-update on zoom. - Use `JBUI.scale(int)` for any pixel dimension to ensure proper HiDPI scaling. +### UI Style Constants + +Style tokens and layout constants are split across three files. Put new constants in the right place — do not scatter them into ad hoc fields or pass them through constructors. + +**`UiStyle`** (`frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt`) — generic Swing style tokens that are not tied to any single session component: +- `UiStyle.Gap` — DPI-aware spacing primitives (`xs`, `sm`, `md`, `lg`, `pad`). Use these for borders, gaps, and insets anywhere in the plugin. +- `UiStyle.Colors` — theme-aware generic colors (`bg`, `fg`, `weak`, `editorBackground`, `errorLabelForeground`, `warningLabelForeground`). +- `UiStyle.Components` — small reusable Swing helpers (e.g. `transparent()`). + +**`SessionUiStyle`** (`frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt`) — static tokens that are specific to the chat/session UI: +- `SessionUiStyle.SessionLayout` — transcript list geometry and scroll increments. +- `SessionUiStyle.View` — card sizing, card borders, surfaces, hover colors, and nested objects for `Prompt`, `Reasoning`, `Message`, and `Tool`. +- `SessionUiStyle.RecentSessions` — recent sessions list limits. +- `SessionUiStyle.Timeline` — activity-indicator colors for the session header timeline. +- `Dock` — border presets for question, permission, and connection dock panels. + +**Rules for adding new constants:** +- Generic layout constants (gaps, generic colors, reusable helpers) → `UiStyle`. +- Session-specific constants (transcript layout, prompt chrome, card geometry, session colors) → `SessionUiStyle`. +- Do not create extra fields whose only purpose is to hold a constant value. Reference the constant object directly, e.g. `UiStyle.Gap.lg()` or `SessionUiStyle.View.Prompt.EDITOR_LINES`. +- Do not pass style constants through constructors or method parameters. Using the constant directly at the call site is correct and preferred. Only introduce parameters for values that are genuinely variable or test-controlled. + ### Icons - **Reuse platform icons**: browse at https://intellij-icons.jetbrains.design. Access via `AllIcons.*` constants. From 50381a1d5d0f1f40b31fe655f55b40f99082cfed Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 9 May 2026 15:00:46 -0400 Subject: [PATCH 048/111] test(jetbrains): cover history route state --- .../client/session/SessionUiLayoutTest.kt | 10 ++++- .../session/controller/ViewSwitchingTest.kt | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index be781893329..ce71341b4b7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -121,7 +121,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { fun `test empty and message bodies share the same scroll pane`() { settle() val pane = scrollComponent() - val empty = find(ui) + val empty = find(ui).view assertSame(empty, scrollView()) @@ -182,6 +182,8 @@ class SessionUiLayoutTest : SessionUiTestBase() { fun `test empty explicit session id shows message body`() { rpc.recent.add(session("ses_recent")) + settle() + rpc.recentCalls.clear() ui = newUi(id = "ses_test") settle() @@ -194,6 +196,8 @@ class SessionUiLayoutTest : SessionUiTestBase() { fun `test explicit session id loading does not show recents`() { rpc.historyGate = kotlinx.coroutines.CompletableDeferred() rpc.recent.add(session("ses_recent")) + settle() + rpc.recentCalls.clear() ui = newUi(id = "ses_test", displayMs = 50) settleShort(100) @@ -213,6 +217,8 @@ class SessionUiLayoutTest : SessionUiTestBase() { rpc.importedCloudSession = session("ses_imported") rpc.historyGate = kotlinx.coroutines.CompletableDeferred() rpc.recent.add(session("ses_recent")) + settle() + rpc.recentCalls.clear() ui = newUi(id = "cloud:cloud_1", displayMs = 50) settleShort(100) @@ -274,7 +280,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { settle() val panel = find(ui) - assertSame(panel, scrollView()) + assertSame(panel.view, scrollView()) assertEquals(1, panel.recentCount()) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt index b54c344e74a..ae2a2f2ea72 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ViewSwitchingTest.kt @@ -149,6 +149,45 @@ class ViewSwitchingTest : SessionControllerTestBase() { assertFalse(events.any { it is SessionControllerEvent.ViewChanged.ShowRecents }) } + fun `test local history session event sees loaded state on EDT`() { + projectRpc.state.value = workspaceReady() + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("ses_test", displayMs = 50) + val states = collectStates(m) + + gate.complete(Unit) + flush() + + val state = states.single { it.first is SessionControllerEvent.ViewChanged.ShowSession }.second + assertTrue(state.showSession) + assertEquals(SessionControllerEvent.ViewChanged.ShowSession, state.viewState) + assertEquals("Idle", state.sessionLoadState) + assertEquals("Idle", state.recentsState) + assertEquals("ses_test", state.refKey) + assertEquals("LOCAL", state.refType) + } + + fun `test cloud history session event sees imported local state on EDT`() { + projectRpc.state.value = workspaceReady() + rpc.importedCloudSession = session("ses_imported") + val gate = CompletableDeferred() + rpc.historyGate = gate + val m = controller("cloud:cloud_1", displayMs = 50) + val states = collectStates(m) + + gate.complete(Unit) + flush() + + val state = states.single { it.first is SessionControllerEvent.ViewChanged.ShowSession }.second + assertTrue(state.showSession) + assertEquals(SessionControllerEvent.ViewChanged.ShowSession, state.viewState) + assertEquals("Idle", state.sessionLoadState) + assertEquals("Idle", state.recentsState) + assertEquals("ses_imported", state.refKey) + assertEquals("LOCAL", state.refType) + } + fun `test existing session load shows progress immediately`() { val gate = CompletableDeferred() rpc.historyGate = gate From ed1af58cf652e6f41f5dbda0e08859592a2720c3 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 9 May 2026 15:09:32 -0400 Subject: [PATCH 049/111] fix(jetbrains): widen recent timestamp gap --- .../kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index 968f2754236..d2811072a28 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -230,7 +230,7 @@ class EmptySessionPanel( private val time = JBLabel() init { - layout = BorderLayout(UiStyle.Gap.lg(), 0) + layout = BorderLayout(UiStyle.Gap.pad(), 0) border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg()) add(title, BorderLayout.CENTER) add(time, BorderLayout.EAST) From f7e73778582da909fda3baaca6526a9ba604158d Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 9 May 2026 15:24:47 -0400 Subject: [PATCH 050/111] fix(jetbrains): improve cloud history loading --- .../session/history/HistoryController.kt | 2 +- .../client/session/history/HistoryPanel.kt | 54 +++++++++++++++++-- .../session/history/HistoryControllerTest.kt | 4 +- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 08d1865c8b7..04f98148caa 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -18,7 +18,7 @@ class HistoryController( private val deleted: (String) -> Unit = {}, ) { companion object { - const val CLOUD_LIMIT = 50 + const val CLOUD_LIMIT = 150 } val local = HistoryModel() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 8753c4d9889..4c4460acd28 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -22,12 +22,16 @@ import com.intellij.ui.tabs.JBTabsFactory import com.intellij.ui.tabs.JBTabsPosition import com.intellij.ui.tabs.TabInfo import com.intellij.ui.tabs.TabsListener +import com.intellij.util.ui.Centerizer import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.CardLayout import java.awt.Cursor +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.RenderingHints import java.awt.event.HierarchyEvent import java.awt.event.KeyEvent import java.awt.event.MouseAdapter @@ -52,7 +56,7 @@ class HistoryPanel( private val cloudSearch = search(controller.cloud) private val localList = localList() private val cloudList = cloudList() - private val more = JButton(KiloBundle.message("history.cloud.load.more")) + private val more = LoadMoreButton() private val localPanel = panel(localSearch, localList) private val cloudPanel = panel(cloudSearch, cloudList, more) private val cards = CardLayout() @@ -176,8 +180,9 @@ class HistoryPanel( viewportBorder = JBUI.Borders.empty() }, BorderLayout.CENTER) footer?.let { - it.border = JBUI.Borders.emptyTop(UiStyle.Gap.lg()) - add(it, BorderLayout.SOUTH) + add(Centerizer(it, Centerizer.TYPE.HORIZONTAL).apply { + border = JBUI.Borders.emptyTop(UiStyle.Gap.lg()) + }, BorderLayout.SOUTH) } } } @@ -383,6 +388,49 @@ class HistoryPanel( // no-op } + private class LoadMoreButton : JButton(KiloBundle.message("history.cloud.load.more")) { + private var over = false + + init { + isFocusable = false + setRequestFocusEnabled(false) + isContentAreaFilled = false + isBorderPainted = false + isOpaque = false + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + sync(true) + } + + override fun mouseExited(e: MouseEvent) { + sync(false) + } + }) + } + + override fun paintComponent(g: Graphics) { + if (isEnabled && over) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + } + super.paintComponent(g) + } + + private fun sync(value: Boolean) { + if (over == value) return + over = value + repaint() + } + } + internal fun showingLoading() = !controller.local.loaded && !controller.cloud.loaded && (controller.local.loading || controller.cloud.loading) private companion object { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 4458552722b..250877766f5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -86,7 +86,7 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(1, controller.cloud.items.size) assertEquals("cloud_1", controller.cloud.items[0].id) assertEquals("next_1", controller.cloud.cursor) - assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 50, "git@example.com:repo.git"), rpc.cloudCalls[0]) + assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 150, "git@example.com:repo.git"), rpc.cloudCalls[0]) rpc.cloud.clear() rpc.cloud += cloud("cloud_2", "Cloud Two") @@ -95,7 +95,7 @@ class HistoryControllerTest : BasePlatformTestCase() { flush() assertEquals(listOf("cloud_1", "cloud_2"), controller.cloud.items.map { it.id }) - assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 50, "git@example.com:repo.git"), rpc.cloudCalls[1]) + assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 150, "git@example.com:repo.git"), rpc.cloudCalls[1]) } fun `test local delete calls rpc and removes item`() { From d1b48b4f1465b78117b98f90960e1b96a45440af Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 9 May 2026 15:49:04 -0400 Subject: [PATCH 051/111] fix(jetbrains): address bot review warnings (json escape, data race, list events) --- .../backend/app/KiloBackendSessionManager.kt | 15 ++++++++++++--- .../client/session/history/HistoryController.kt | 3 ++- .../client/session/history/HistoryModel.kt | 14 ++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index cdfffd9911f..eb0af2025ec 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -276,7 +276,16 @@ class KiloBackendSessionManager( private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8) - private fun escape(value: String) = value - .replace("\\", "\\\\") - .replace("\"", "\\\"") + private fun escape(value: String) = value.buildString { + for (c in value) { + when (c) { + '\\' -> append("\\\\") + '"' -> append("\\\"") + '\n' -> append("\\n") + '\r' -> append("\\r") + '\t' -> append("\\t") + else -> if (c < '\u0020') append("\\u%04x".format(c.code)) else append(c) + } + } + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 04f98148caa..81993348e78 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -91,10 +91,11 @@ class HistoryController( private fun loadCloud(reset: Boolean) { val cursor = cloud.cursor.takeUnless { reset } + val gitUrl = git edt { cloud.start(reset) } cs.launch { try { - val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, git) + val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, gitUrl) val items = result.sessions.map(::cloudItem) edt { if (reset) cloud.replace(items, result.nextCursor) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt index f6aa89d3be3..a9d0af9e774 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -75,8 +75,18 @@ open class HistoryModel : AbstractListModel() { private fun filter() { val old = rows.size rows = all.filter(::matches) - val end = maxOf(old, rows.size).coerceAtLeast(1) - 1 - fireContentsChanged(this, 0, end) + val new = rows.size + when { + old > new -> { + if (new > 0) fireContentsChanged(this, 0, new - 1) + fireIntervalRemoved(this, new, old - 1) + } + new > old -> { + if (old > 0) fireContentsChanged(this, 0, old - 1) + fireIntervalAdded(this, old, new - 1) + } + new > 0 -> fireContentsChanged(this, 0, new - 1) + } } private fun matches(item: T): Boolean { From f4c7e77601bb986e5f24c76c8d6d62187b9168ba Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 9 May 2026 19:07:44 -0400 Subject: [PATCH 052/111] fix(jetbrains): use top-level buildString --- .../kilocode/backend/app/KiloBackendSessionManager.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index eb0af2025ec..b5e014623e8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -232,8 +232,8 @@ class KiloBackendSessionManager( title = s.title, version = s.version, time = SessionTimeDto( - created = s.time.created, - updated = s.time.updated, + created = s.time.created.toDouble(), + updated = s.time.updated.toDouble(), archived = s.time.archived, ), summary = s.summary?.let { @@ -253,8 +253,8 @@ class KiloBackendSessionManager( title = s.title, version = s.version, time = SessionTimeDto( - created = s.time.created, - updated = s.time.updated, + created = s.time.created.toDouble(), + updated = s.time.updated.toDouble(), archived = s.time.archived, ), summary = s.summary?.let { @@ -276,7 +276,7 @@ class KiloBackendSessionManager( private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8) - private fun escape(value: String) = value.buildString { + private fun escape(value: String) = buildString { for (c in value) { when (c) { '\\' -> append("\\\\") From 9963b0271a78244f773e6192721376618d0a3549 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Sun, 10 May 2026 00:50:32 -0400 Subject: [PATCH 053/111] fix(cli): apply `--auto` flag to subagents from Task tool --- .changeset/auto-subagent-permissions.md | 5 + packages/opencode/src/cli/cmd/run.ts | 33 ++-- .../opencode/src/kilocode/cli/run-auto.ts | 47 +++++ .../test/kilocode/cli-run-auto-helper.test.ts | 59 ++++++ .../opencode/test/kilocode/run-auto.test.ts | 177 ++++++++++++++++++ 5 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 .changeset/auto-subagent-permissions.md create mode 100644 packages/opencode/src/kilocode/cli/run-auto.ts create mode 100644 packages/opencode/test/kilocode/cli-run-auto-helper.test.ts create mode 100644 packages/opencode/test/kilocode/run-auto.test.ts diff --git a/.changeset/auto-subagent-permissions.md b/.changeset/auto-subagent-permissions.md new file mode 100644 index 00000000000..73c865fdfbb --- /dev/null +++ b/.changeset/auto-subagent-permissions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Auto-approve Task subagent tool permissions when running `kilo run --auto`. diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 43fc4a96212..da5c533d35b 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -28,6 +28,7 @@ import { TodoWriteTool } from "../../tool/todo" import { Locale } from "@/util/locale" import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change import { AppRuntime } from "@/effect/app-runtime" +import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change type ToolProps = { input: Tool.InferParameters @@ -477,6 +478,9 @@ export const RunCommand = cmd({ if (event.type === "message.part.updated") { const part = event.properties.part + // kilocode_change start - track Task child sessions for --auto permission replies + if (args.auto) KiloRunAuto.track(auto, part) + // kilocode_change end if (part.sessionID !== sessionID) continue if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) { @@ -571,25 +575,27 @@ export const RunCommand = cmd({ if (event.type === "permission.asked") { const permission = event.properties - if (permission.sessionID !== sessionID) continue - + // kilocode_change start - In auto mode, approve root and tracked Task child permissions only if (args.auto) { - // kilocode_change - In auto mode, automatically approve all permissions without prompting + if (!KiloRunAuto.allowed(auto, permission.sessionID)) continue await sdk.permission.reply({ requestID: permission.id, reply: "once", }) - } else { - UI.println( - UI.Style.TEXT_WARNING_BOLD + "!", - UI.Style.TEXT_NORMAL + - `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, - ) - await sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - }) + continue } + + if (permission.sessionID !== sessionID) continue + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL + + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, + ) + await sdk.permission.reply({ + requestID: permission.id, + reply: "reject", + }) + // kilocode_change end } // kilocode_change start - network retry handling if (event.type === "session.network.asked") { @@ -682,6 +688,7 @@ export const RunCommand = cmd({ UI.error("Session not found") process.exit(1) } + const auto = KiloRunAuto.create(sessionID) // kilocode_change await share(sdk, sessionID) loop().catch((e) => { diff --git a/packages/opencode/src/kilocode/cli/run-auto.ts b/packages/opencode/src/kilocode/cli/run-auto.ts new file mode 100644 index 00000000000..84014997ef3 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/run-auto.ts @@ -0,0 +1,47 @@ +// kilocode_change - new file +export namespace KiloRunAuto { + export interface State { + root: string + sessions: Set + } + + export interface Part { + type?: string + tool?: string + sessionID?: string + state?: unknown + } + + export function create(root: string): State { + return { + root, + sessions: new Set([root]), + } + } + + export function allowed(state: State, sessionID: string) { + return state.sessions.has(sessionID) + } + + export function track(state: State, part: Part) { + if (part.type !== "tool") return + if (part.tool !== "task") return + if (part.sessionID !== state.root) return + const id = child(meta(part.state)) + if (!id) return + state.sessions.add(id) + } + + function meta(state: unknown) { + if (!state || typeof state !== "object") return + return (state as Record).metadata + } + + function child(meta: unknown) { + if (!meta || typeof meta !== "object") return + const id = (meta as Record).sessionId + if (typeof id !== "string") return + if (!id) return + return id + } +} diff --git a/packages/opencode/test/kilocode/cli-run-auto-helper.test.ts b/packages/opencode/test/kilocode/cli-run-auto-helper.test.ts new file mode 100644 index 00000000000..2b843848ac4 --- /dev/null +++ b/packages/opencode/test/kilocode/cli-run-auto-helper.test.ts @@ -0,0 +1,59 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { KiloRunAuto } from "../../src/kilocode/cli/run-auto" + +describe("KiloRunAuto", () => { + test("tracks task child sessions without allowing unrelated sessions", () => { + const state = KiloRunAuto.create("ses_root") + + expect(KiloRunAuto.allowed(state, "ses_root")).toBe(true) + expect(KiloRunAuto.allowed(state, "ses_child")).toBe(false) + + KiloRunAuto.track(state, { + type: "tool", + tool: "task", + sessionID: "ses_root", + state: { + metadata: { + sessionId: "ses_child", + }, + }, + }) + + expect(KiloRunAuto.allowed(state, "ses_child")).toBe(true) + expect(KiloRunAuto.allowed(state, "ses_other")).toBe(false) + }) + + test("ignores malformed or non-root task metadata", () => { + const state = KiloRunAuto.create("ses_root") + + KiloRunAuto.track(state, { + type: "tool", + tool: "task", + sessionID: "ses_root", + state: { + metadata: { + sessionId: "", + }, + }, + }) + KiloRunAuto.track(state, { + type: "tool", + tool: "task", + sessionID: "ses_other", + state: { + metadata: { + sessionId: "ses_wrong", + }, + }, + }) + KiloRunAuto.track(state, { + type: "text", + sessionID: "ses_root", + state: {}, + }) + + expect(KiloRunAuto.allowed(state, "ses_wrong")).toBe(false) + expect(KiloRunAuto.allowed(state, "")).toBe(false) + }) +}) diff --git a/packages/opencode/test/kilocode/run-auto.test.ts b/packages/opencode/test/kilocode/run-auto.test.ts new file mode 100644 index 00000000000..4b6788d2a50 --- /dev/null +++ b/packages/opencode/test/kilocode/run-auto.test.ts @@ -0,0 +1,177 @@ +// kilocode_change - new file +import { afterEach, describe, expect, mock, test } from "bun:test" + +type Event = { + type: string + properties: Record +} + +function feed() { + const list: T[] = [] + const wait: Array<() => void> = [] + const state = { done: false } + + return { + push(item: T) { + list.push(item) + while (wait.length) wait.shift()?.() + }, + end() { + state.done = true + while (wait.length) wait.shift()?.() + }, + async *stream() { + while (!state.done || list.length) { + if (list.length) { + yield list.shift() as T + continue + } + await new Promise((resolve) => wait.push(resolve)) + } + }, + } +} + +function task(child: string): Event { + return { + type: "message.part.updated", + properties: { + part: { + id: "prt_task", + type: "tool", + tool: "task", + sessionID: "ses_root", + state: { + status: "running", + input: { + description: "inspect bug", + prompt: "check child permissions", + subagent_type: "general", + }, + metadata: { + sessionId: child, + }, + time: { start: 0 }, + }, + }, + }, + } +} + +function permission(id: string, sessionID: string): Event { + return { + type: "permission.asked", + properties: { + id, + sessionID, + permission: "bash", + patterns: ["npm test"], + metadata: { command: "npm test" }, + always: ["npm *"], + }, + } +} + +function idle(): Event { + return { + type: "session.status", + properties: { + sessionID: "ses_root", + status: { type: "idle" }, + }, + } +} + +function args() { + return { + _: [], + $0: "kilo", + message: ["hi"], + command: undefined, + continue: false, + session: "ses_root", + fork: false, + "cloud-fork": false, + cloudFork: false, + share: false, + model: undefined, + agent: undefined, + format: "json", + file: undefined, + title: undefined, + attach: "http://127.0.0.1:4096", + password: undefined, + dir: undefined, + port: undefined, + variant: undefined, + thinking: false, + auto: true, + "dangerously-skip-permissions": false, + dangerouslySkipPermissions: false, + "--": [], + } +} + +const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY") + +afterEach(() => { + if (tty) { + Object.defineProperty(process.stdin, "isTTY", tty) + return + } + delete (process.stdin as { isTTY?: boolean }).isTTY +}) + +async function run(sdk: Record) { + mock.module("@kilocode/sdk/v2", () => ({ + createKiloClient: () => sdk, + })) + + Object.defineProperty(process.stdin, "isTTY", { + configurable: true, + value: true, + }) + + const key = JSON.stringify({ time: Date.now(), rand: Math.random() }) + const { RunCommand } = await import(`../../src/cli/cmd/run?${key}`) + return RunCommand.handler(args() as never) +} + +describe("cli run auto permissions", () => { + test("auto approves tracked subagent permissions and ignores unrelated sessions", async () => { + const q = feed() + const calls: Array<{ requestID: string; reply: string }> = [] + const done = Promise.withResolvers() + + const sdk = { + config: { + get: async () => ({ data: { share: "manual" } }), + }, + event: { + subscribe: async () => ({ stream: q.stream() }), + }, + permission: { + reply: async (input: { requestID: string; reply: string }) => { + calls.push(input) + if (input.requestID === "perm_child") done.resolve() + return { data: true } + }, + }, + session: { + prompt: async () => { + q.push(task("ses_child")) + q.push(permission("perm_other", "ses_other")) + q.push(permission("perm_child", "ses_child")) + q.push(idle()) + await Promise.race([done.promise, new Promise((resolve) => setTimeout(resolve, 25))]) + q.end() + return { data: undefined } + }, + }, + } + + await run(sdk) + + expect(calls).toEqual([{ requestID: "perm_child", reply: "once" }]) + }) +}) From 3b44205a386d670e0d6971aa0e4ea83e7e591d17 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 10 May 2026 22:20:24 -0600 Subject: [PATCH 054/111] feat(cli): improve model info panel with modality and description support --- .../components/model-info-panel-utils.ts | 9 +- .../kilocode/components/model-info-panel.tsx | 137 ++++++++++++++---- 2 files changed, 113 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts index 582db55650d..ee7c596e747 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts +++ b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts @@ -13,11 +13,11 @@ export function fmtPrice(n: number): string { return `$${n.toFixed(2)}/1M` } -export function fmtCachedPrice(cost: Cost): string { +export function fmtCachedPrice(cost: Cost): string | null { const read = cost.cache.read if (read > 0) return fmtPrice(read) if (cost.input === 0) return fmtPrice(0) - return "N/A" + return null } export function avgPrice(cost: Cost): number { @@ -31,3 +31,8 @@ export function fmtContext(n: number): string { if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K` return String(n) } + +export function fmtDate(s: string): string { + const d = new Date(s) + return d.toLocaleDateString(undefined, { year: "numeric", month: "short" }) +} diff --git a/packages/opencode/src/kilocode/components/model-info-panel.tsx b/packages/opencode/src/kilocode/components/model-info-panel.tsx index ceb9e860445..c3d49b1ebcb 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel.tsx +++ b/packages/opencode/src/kilocode/components/model-info-panel.tsx @@ -1,7 +1,9 @@ import { TextAttributes } from "@opentui/core" +import { useTerminalDimensions } from "@opentui/solid" +import { createMemo } from "solid-js" import { useTheme } from "@tui/context/theme" import type { Model } from "@kilocode/sdk/v2" -import { avgPrice, fmtCachedPrice, fmtContext, fmtPrice } from "./model-info-panel-utils" +import { avgPrice, fmtCachedPrice, fmtContext, fmtDate, fmtPrice } from "./model-info-panel-utils" import { Show } from "solid-js" interface Props { @@ -12,6 +14,40 @@ interface Props { export function ModelInfoPanel(props: Props) { const { theme } = useTheme() const m = () => props.model + const dimensions = useTerminalDimensions() + + const maxHeight = createMemo(() => Math.floor(dimensions().height / 2) - 3) + + const cost = () => m().cost + const cached = () => (cost() ? fmtCachedPrice(cost()) : null) + const avg = () => (cost() ? avgPrice(cost()) : undefined) + const caps = () => m().capabilities + const inputs = () => caps()?.input + const outputs = () => caps()?.output + const activeInputModalities = () => { + if (!inputs()) return [] as string[] + return Object.entries(inputs()) + .filter(([k, v]) => v && k !== "text") + .map(([k]) => k.charAt(0).toUpperCase() + k.slice(1)) + } + const activeOutputModalities = () => { + if (!outputs()) return [] as string[] + return Object.entries(outputs()) + .filter(([k, v]) => v && k !== "text") + .map(([k]) => k.charAt(0).toUpperCase() + k.slice(1)) + } + const inputLine = () => { + const mods = activeInputModalities() + return mods.length > 0 ? `In: ${mods.join(", ")}` : null + } + const outputLine = () => { + const mods = activeOutputModalities() + return mods.length > 0 ? `Out: ${mods.join(", ")}` : null + } + const desc = () => { + const d = m().options?.description + return typeof d === "string" && d.trim() ? d : null + } return ( - - - {m().name ?? m().id ?? "Model"} - - {props.provider ?? m().providerID ?? ""} - - + + + + {m().name ?? m().id ?? "Model"} + + {props.provider ?? m().providerID ?? ""} + - + Free - + - Input - {m() ? fmtPrice(m().cost.input) : "—"} - - - Output - {m() ? fmtPrice(m().cost.output) : "—"} - - - Cache Read - {m() ? fmtCachedPrice(m().cost) : "—"} - - - - Context Size - {m() ? fmtContext(m().limit.context) : "—"} - - - Average Cost - {m() ? fmtPrice(avgPrice(m().cost)) : "—"} + Family + {m().family!.charAt(0).toUpperCase() + m().family!.slice(1)} - + + + Released + {fmtDate(m().release_date)} + + + + + + Input + {m() ? fmtPrice(m().cost.input) : "—"} + + + Output + {m() ? fmtPrice(m().cost.output) : "—"} + + + + Cached + {cached()} + + + + + Avg Cost + {m() ? fmtPrice(avg()!) : "—"} + + + + Context + {m() ? fmtContext(m().limit.context) : "—"} + + + + + + + + [Reasoning] + + + {inputLine()} + + + {outputLine()} + + + + + + + {desc()} + + + ) -} +} \ No newline at end of file From 51b35d0b9490532a3f2174086f06340a89f99116 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 10 May 2026 22:29:08 -0600 Subject: [PATCH 055/111] fix(model-info-panel): resolve review comments - merge imports, guard fmtDate, use Show render-prop --- .../src/kilocode/components/model-info-panel-utils.ts | 1 + .../src/kilocode/components/model-info-panel.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts index ee7c596e747..152c23a50e6 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts +++ b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts @@ -34,5 +34,6 @@ export function fmtContext(n: number): string { export function fmtDate(s: string): string { const d = new Date(s) + if (isNaN(d.getTime())) return s return d.toLocaleDateString(undefined, { year: "numeric", month: "short" }) } diff --git a/packages/opencode/src/kilocode/components/model-info-panel.tsx b/packages/opencode/src/kilocode/components/model-info-panel.tsx index c3d49b1ebcb..b726d606433 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel.tsx +++ b/packages/opencode/src/kilocode/components/model-info-panel.tsx @@ -1,10 +1,9 @@ import { TextAttributes } from "@opentui/core" import { useTerminalDimensions } from "@opentui/solid" -import { createMemo } from "solid-js" +import { createMemo, Show } from "solid-js" import { useTheme } from "@tui/context/theme" import type { Model } from "@kilocode/sdk/v2" import { avgPrice, fmtCachedPrice, fmtContext, fmtDate, fmtPrice } from "./model-info-panel-utils" -import { Show } from "solid-js" interface Props { model: Model @@ -121,10 +120,10 @@ export function ModelInfoPanel(props: Props) { [Reasoning] - {inputLine()} + {(line) => {line()}} - {outputLine()} + {(line) => {line()}} @@ -137,4 +136,4 @@ export function ModelInfoPanel(props: Props) { ) -} \ No newline at end of file +} From fc6ce735c9dde64a544e0952d789397fa9fb7815 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 11 May 2026 09:21:37 +0200 Subject: [PATCH 056/111] fix(vscode): hide empty task header graph (#10125) --- .changeset/hide-empty-task-header-graph.md | 5 +++++ .../webview-ui/src/components/chat/TaskHeader.tsx | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/hide-empty-task-header-graph.md diff --git a/.changeset/hide-empty-task-header-graph.md b/.changeset/hide-empty-task-header-graph.md new file mode 100644 index 00000000000..06609ec09aa --- /dev/null +++ b/.changeset/hide-empty-task-header-graph.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Hide the task header graph area until timeline data is available. diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 409bc62bbcb..e04f51dc7d3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -78,6 +78,14 @@ export const TaskHeader: Component = (props) => { return undefined }) + const hasTimeline = createMemo(() => { + for (const m of session.messages()) { + if (m.role !== "assistant") continue + if (session.getParts(m.id).some((p) => p.type !== "step-start")) return true + } + return false + }) + const fmtNum = (n: number): string => { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` @@ -166,7 +174,7 @@ export const TaskHeader: Component = (props) => { {/* Expanded graph section: timeline + context bar + token breakdown */} - m.role === "assistant")}> +
From 3259008074cafe232a30d380e9ecc73c63ca0a35 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 11 May 2026 09:27:41 +0200 Subject: [PATCH 057/111] fix(vscode): stabilize shell command streaming (#10127) --- .changeset/stable-shell-streaming.md | 5 ++++ .../src/components/chat/AssistantMessage.tsx | 29 ++++++++++++------- .../webview-ui/src/context/session.tsx | 9 ++++-- 3 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 .changeset/stable-shell-streaming.md diff --git a/.changeset/stable-shell-streaming.md b/.changeset/stable-shell-streaming.md new file mode 100644 index 00000000000..bdae1f250a5 --- /dev/null +++ b/.changeset/stable-shell-streaming.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep shell command blocks stable while output streams so pending animations and expanded state do not reset. diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 998d610d579..9acf713193d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -67,19 +67,26 @@ interface AssistantMessageProps { feedback?: MessageFeedbackControls } +type ToolStateProps = { + input?: Record + metadata?: Record + output?: string + status?: string +} + function TodoToolCard(props: { part: ToolPart }) { const render = ToolRegistry.render(props.part.tool) - const state = props.part.state as any + const state = () => props.part.state as ToolStateProps return ( {(renderFn) => ( @@ -90,23 +97,23 @@ function TodoToolCard(props: { part: ToolPart }) { function BashToolCard(props: { part: ToolPart; defaultOpen: boolean }) { const render = ToolRegistry.render(props.part.tool) - const state = props.part.state as any + const state = () => props.part.state as ToolStateProps return ( {(card) => ( >} - input={state?.input ?? {}} - metadata={state?.metadata ?? {}} + input={state()?.input ?? {}} + metadata={state()?.metadata ?? {}} partMetadata={props.part.metadata ?? {}} tool={props.part.tool} partID={props.part.id} callID={props.part.callID} - output={state?.output} - status={state?.status} + output={state()?.output} + status={state()?.status} defaultOpen={props.defaultOpen} animate - reveal={state?.status === "pending" || state?.status === "running"} + reveal={state()?.status === "pending" || state()?.status === "running"} /> )} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 8ebe0d41edf..2da1f032c68 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1192,8 +1192,13 @@ export const SessionProvider: ParentComponent = (props) => { // Append text delta to text or reasoning parts ;(existing as { text: string }).text += delta.textDelta } else { - // Replace entire part - parts[effectiveMessageID][existingIndex] = part + // Preserve the proxy identity so Solid does not remount tool UI + // during streaming updates and restart pending animations. + const target = existing as unknown as Record + for (const key of Object.keys(target)) { + if (!(key in part)) delete target[key] + } + Object.assign(existing, part) } } else { // Add new part From 27da09614efa15375cd2d32ce2e8cfdbc12e60a2 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 7 May 2026 12:26:10 +0200 Subject: [PATCH 058/111] fix: restore root package.json entries dropped by upstream-compat The automated kilo compat transform keeps upstream's root scripts/catalog wholesale and only re-applies a handful of Kilo-specific scripts, which silently dropped: - postinstall's `&& bun run script/setup-git.ts` tail (needed to set merge.conflictStyle=zdiff3 locally, which upstream merges rely on) - the `dev-setup` script shorthand - kept dead `dev:desktop` / `dev:web` / `dev:console` scripts whose target packages aren't tracked in Kilo - kept `@sentry/solid` / `@sentry/vite-plugin` catalog entries that have zero consumers in our tree --- package.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d5cbbaf4375..e4f650194da 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,11 @@ "packageManager": "bun@1.3.13", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", - "dev:desktop": "bun --cwd packages/desktop-electron dev", - "dev:web": "bun --cwd packages/app dev", - "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", + "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", "typecheck": "bun turbo typecheck", - "postinstall": "bun run --cwd packages/opencode fix-node-pty", + "postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts", "prepare": "husky", "random": "echo 'Random script'", "hello": "echo 'Hello World!'", @@ -76,8 +74,6 @@ "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", - "@sentry/solid": "10.36.0", - "@sentry/vite-plugin": "4.6.0", "solid-js": "1.9.12", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.10" From a156e7a98d01a7b343a2669dac85128c0f463346 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 7 May 2026 13:41:00 +0200 Subject: [PATCH 059/111] chore(upstream): preserve & prune root package.json entries on merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transform that materialises the 'kilo compat for vX.Y.Z' commit during an upstream merge takes upstream's package.json wholesale and re-applies a hand-picked list of Kilo scripts. That list was incomplete: it only covered `extension`, `changeset`, `changeset:version`, `test`, and `test:ci`, so every merge silently dropped Kilo's `postinstall` tail (`&& bun run script/setup-git.ts`) and the `dev-setup` root shortcut, and it never pruned upstream-only scripts (`dev:desktop` / `dev:web` / `dev:console`) or upstream-only catalog entries (`@sentry/solid`, `@sentry/vite-plugin`) whose target packages Kilo doesn't ship. - replace the per-script if-blocks with data-driven PRESERVE_SCRIPTS - add DELETE_UPSTREAM_SCRIPTS for scripts that reference packages Kilo doesn't ship - add DELETE_UPSTREAM_CATALOG for catalog entries with zero Kilo consumers - apply the same policy in both transformPackageJson (conflict path) and transformAllPackageJson (pre-merge sweep path) — previously the pre-merge sweep was missing `changeset` / `changeset:version` preservation - new tests covering preservation, deletion, opencode test scripts, and the catalog pruning --- .../transforms/transform-package-json.test.ts | 86 ++++++++++++ .../transforms/transform-package-json.ts | 131 +++++++++--------- 2 files changed, 155 insertions(+), 62 deletions(-) create mode 100644 script/upstream/transforms/transform-package-json.test.ts diff --git a/script/upstream/transforms/transform-package-json.test.ts b/script/upstream/transforms/transform-package-json.test.ts new file mode 100644 index 00000000000..11016f0e135 --- /dev/null +++ b/script/upstream/transforms/transform-package-json.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test" +import { fixCatalog, fixScripts } from "./transform-package-json" + +test("fixScripts preserves Kilo-only root scripts from base", () => { + const ours = { + scripts: { + "dev-setup": "kilo dev-setup", + "postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts", + "extension": "bun --cwd packages/kilo-vscode script/launch.ts", + }, + } + const pkg: Record = { + scripts: { postinstall: "bun run --cwd packages/opencode fix-node-pty" }, + } + const changes: string[] = [] + fixScripts(pkg, "package.json", ours, changes) + const scripts = pkg.scripts as Record + expect(scripts.postinstall).toBe(ours.scripts.postinstall) + expect(scripts["dev-setup"]).toBe(ours.scripts["dev-setup"]) + expect(scripts.extension).toBe(ours.scripts.extension) + expect(changes.some((c) => c.includes("postinstall"))).toBe(true) + expect(changes.some((c) => c.includes("dev-setup"))).toBe(true) +}) + +test("fixScripts removes upstream-only dead scripts from root", () => { + const pkg: Record = { + scripts: { + "dev": "bun run --cwd packages/opencode src/index.ts", + "dev:desktop": "bun --cwd packages/desktop-electron dev", + "dev:web": "bun --cwd packages/app dev", + "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", + }, + } + const changes: string[] = [] + fixScripts(pkg, "package.json", null, changes) + const scripts = pkg.scripts as Record + expect(scripts.dev).toBeDefined() + expect(scripts["dev:desktop"]).toBeUndefined() + expect(scripts["dev:web"]).toBeUndefined() + expect(scripts["dev:console"]).toBeUndefined() + expect(changes.length).toBe(3) +}) + +test("fixScripts preserves opencode test scripts", () => { + const ours = { scripts: { test: "bun test", "test:ci": "bun test --ci" } } + const pkg: Record = { scripts: { test: "vitest" } } + const changes: string[] = [] + fixScripts(pkg, "packages/opencode/package.json", ours, changes) + const scripts = pkg.scripts as Record + expect(scripts.test).toBe("bun test") + expect(scripts["test:ci"]).toBe("bun test --ci") +}) + +test("fixScripts leaves unknown packages untouched", () => { + const pkg: Record = { scripts: { build: "tsc" } } + const changes: string[] = [] + fixScripts(pkg, "packages/some-unknown/package.json", null, changes) + expect((pkg.scripts as Record).build).toBe("tsc") + expect(changes.length).toBe(0) +}) + +test("fixCatalog removes upstream-only desktop sentry entries", () => { + const pkg: Record = { + workspaces: { + catalog: { + "@sentry/solid": "10.36.0", + "@sentry/vite-plugin": "4.6.0", + "solid-js": "1.9.12", + }, + }, + } + const changes: string[] = [] + fixCatalog(pkg, "package.json", changes) + const cat = (pkg.workspaces as { catalog: Record }).catalog + expect(cat["@sentry/solid"]).toBeUndefined() + expect(cat["@sentry/vite-plugin"]).toBeUndefined() + expect(cat["solid-js"]).toBe("1.9.12") + expect(changes.length).toBe(2) +}) + +test("fixCatalog is a no-op when catalog is absent", () => { + const pkg: Record = {} + const changes: string[] = [] + fixCatalog(pkg, "package.json", changes) + expect(changes.length).toBe(0) +}) diff --git a/script/upstream/transforms/transform-package-json.ts b/script/upstream/transforms/transform-package-json.ts index bbc812ea958..327ee30facc 100644 --- a/script/upstream/transforms/transform-package-json.ts +++ b/script/upstream/transforms/transform-package-json.ts @@ -204,6 +204,69 @@ const TRANSFORM_PACKAGE_NAMES: Record = { "packages/sdk/js/package.json": "@kilocode/sdk", } +// Kilo-specific scripts to preserve from the base branch per package.json. +// Upstream's version wholesale-replaces the scripts block, so anything listed +// here gets re-applied from ours after taking theirs. +const PRESERVE_SCRIPTS: Record = { + "package.json": ["extension", "changeset", "changeset:version", "dev-setup", "postinstall"], + "packages/opencode/package.json": ["test", "test:ci"], +} + +// Upstream-only scripts to delete per package.json. These reference packages +// Kilo doesn't ship (desktop-electron, console/app, app) and would otherwise +// reappear on every merge. +const DELETE_UPSTREAM_SCRIPTS: Record = { + "package.json": ["dev:desktop", "dev:web", "dev:console"], +} + +// Upstream-only catalog entries to delete per package.json. These are pulled +// in by upstream features (e.g. desktop Sentry integration) that Kilo doesn't +// ship, so they add install weight with zero consumers in our tree. +const DELETE_UPSTREAM_CATALOG: Record = { + "package.json": ["@sentry/solid", "@sentry/vite-plugin"], +} + +/** + * Re-apply Kilo-specific scripts on top of the upstream-shaped scripts block, + * and prune upstream-only scripts that target packages Kilo doesn't ship. + */ +export function fixScripts(pkg: Record, path: string, ours: Record | null, changes: string[]): void { + const theirs = (pkg.scripts as Record | undefined) || {} + const oursScripts = (ours?.scripts as Record | undefined) || {} + + for (const name of PRESERVE_SCRIPTS[path] || []) { + const val = oursScripts[name] + if (val && theirs[name] !== val) { + theirs[name] = val + changes.push(`scripts.${name}: preserved from base`) + } + } + + for (const name of DELETE_UPSTREAM_SCRIPTS[path] || []) { + if (theirs[name]) { + delete theirs[name] + changes.push(`scripts.${name}: removed (upstream-only, no Kilo target)`) + } + } + + if (Object.keys(theirs).length > 0) pkg.scripts = theirs +} + +/** + * Prune upstream-only catalog entries that have no consumers in Kilo. + */ +export function fixCatalog(pkg: Record, path: string, changes: string[]): void { + const ws = pkg.workspaces as { catalog?: Record } | undefined + const cat = ws?.catalog + if (!cat) return + for (const name of DELETE_UPSTREAM_CATALOG[path] || []) { + if (cat[name]) { + delete cat[name] + changes.push(`workspaces.catalog.${name}: removed (upstream-only, no Kilo consumer)`) + } + } +} + /** * Check if file is a package.json */ @@ -358,46 +421,7 @@ export async function transformPackageJson(file: string, options: PackageJsonOpt changes.push(`workspaces.packages: preserved Kilo's workspace configuration`) } - const ourScripts = ourPkg.scripts as Record | undefined - if (relativePath === "package.json" && ourScripts?.extension && pkg.scripts?.extension !== ourScripts.extension) { - pkg.scripts = pkg.scripts || {} - pkg.scripts.extension = ourScripts.extension - changes.push(`scripts.extension: preserved Kilo's extension script`) - } - if (relativePath === "package.json" && ourScripts?.changeset && pkg.scripts?.changeset !== ourScripts.changeset) { - pkg.scripts = pkg.scripts || {} - pkg.scripts.changeset = ourScripts.changeset - changes.push(`scripts.changeset: preserved Kilo's changeset script`) - } - if ( - relativePath === "package.json" && - ourScripts?.["changeset:version"] && - pkg.scripts?.["changeset:version"] !== ourScripts["changeset:version"] - ) { - pkg.scripts = pkg.scripts || {} - pkg.scripts["changeset:version"] = ourScripts["changeset:version"] - changes.push(`scripts.changeset:version: preserved Kilo's changeset:version script`) - } - - // Preserve Kilo's test runner scripts for packages/opencode - if ( - relativePath === "packages/opencode/package.json" && - ourScripts?.test && - pkg.scripts?.test !== ourScripts.test - ) { - pkg.scripts = pkg.scripts || {} - pkg.scripts.test = ourScripts.test - changes.push(`scripts.test: preserved Kilo's test runner script`) - } - if ( - relativePath === "packages/opencode/package.json" && - ourScripts?.["test:ci"] && - pkg.scripts?.["test:ci"] !== ourScripts["test:ci"] - ) { - pkg.scripts = pkg.scripts || {} - pkg.scripts["test:ci"] = ourScripts["test:ci"] - changes.push(`scripts.test:ci: preserved Kilo's CI test runner script`) - } + fixScripts(pkg, relativePath, ourPkg, changes) // Merge catalog with "newest wins" strategy if (ourWorkspaces?.catalog || theirWorkspaces?.catalog) { @@ -409,6 +433,8 @@ export async function transformPackageJson(file: string, options: PackageJsonOpt "workspaces.catalog", ) } + + fixCatalog(pkg, relativePath, changes) } // 7. Transform dependency names (opencode -> kilo) @@ -617,28 +643,7 @@ export async function transformAllPackageJson(options: PackageJsonOptions = {}): changes.push(`workspaces.packages: preserved Kilo's workspace configuration`) } - const kiloScripts = kiloPkg.scripts as Record | undefined - if (path === "package.json" && kiloScripts?.extension && pkg.scripts?.extension !== kiloScripts.extension) { - pkg.scripts = pkg.scripts || {} - pkg.scripts.extension = kiloScripts.extension - changes.push(`scripts.extension: preserved Kilo's extension script`) - } - - // Preserve Kilo's test runner scripts for packages/opencode - if (path === "packages/opencode/package.json" && kiloScripts?.test && pkg.scripts?.test !== kiloScripts.test) { - pkg.scripts = pkg.scripts || {} - pkg.scripts.test = kiloScripts.test - changes.push(`scripts.test: preserved Kilo's test runner script`) - } - if ( - path === "packages/opencode/package.json" && - kiloScripts?.["test:ci"] && - pkg.scripts?.["test:ci"] !== kiloScripts["test:ci"] - ) { - pkg.scripts = pkg.scripts || {} - pkg.scripts["test:ci"] = kiloScripts["test:ci"] - changes.push(`scripts.test:ci: preserved Kilo's CI test runner script`) - } + fixScripts(pkg, path, kiloPkg, changes) // Merge catalog with "newest wins" strategy if (kiloWorkspaces?.catalog || upstreamWorkspaces?.catalog) { @@ -650,6 +655,8 @@ export async function transformAllPackageJson(options: PackageJsonOptions = {}): "workspaces.catalog", ) } + + fixCatalog(pkg, path, changes) } // 7. Transform dependency names (opencode -> kilo) From d79664f0841f93dace85c35239c12a8d64bec35f Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 7 May 2026 16:44:50 +0200 Subject: [PATCH 060/111] fix(upstream-merge): preserve ours' key order in package.json dep merge Seeding the result with theirs' keys and appending ours-only keys at the end caused kilo-only deps (e.g. rotating-file-stream in packages/core) to relocate from the middle of the deps block to the end during the pre-merge transform. Git's textual 3-way merge then saw ours keeping the line in place and theirs adding the same key elsewhere, producing a duplicate JSON key in the merged file. Iterate ours first so kilo-only deps stay in their original position, then append any theirs-only keys at the end. --- .../transforms/transform-package-json.test.ts | 32 ++++++++- .../transforms/transform-package-json.ts | 66 ++++++++++--------- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/script/upstream/transforms/transform-package-json.test.ts b/script/upstream/transforms/transform-package-json.test.ts index 11016f0e135..a239bcc5edf 100644 --- a/script/upstream/transforms/transform-package-json.test.ts +++ b/script/upstream/transforms/transform-package-json.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { fixCatalog, fixScripts } from "./transform-package-json" +import { fixCatalog, fixScripts, mergeWithNewestVersions } from "./transform-package-json" test("fixScripts preserves Kilo-only root scripts from base", () => { const ours = { @@ -84,3 +84,33 @@ test("fixCatalog is a no-op when catalog is absent", () => { fixCatalog(pkg, "package.json", changes) expect(changes.length).toBe(0) }) + +test("mergeWithNewestVersions preserves ours' key order so kilo-only deps don't relocate", () => { + // Regression: when ours has a kilo-only dep in the middle (e.g. rotating-file-stream + // alphabetically between npm-package-arg and semver) and theirs lacks it, the merge + // result must keep that key in its original position. Previously this function + // started from theirs' keys and appended ours-only keys at the end, causing git's + // textual 3-way merge to produce a duplicate JSON key. + const ours = { + "npm-package-arg": "13.0.2", + "rotating-file-stream": "3.2.9", + semver: "^7.6.3", + zod: "catalog:", + } + const theirs = { + "npm-package-arg": "13.0.2", + semver: "^7.6.3", + zod: "catalog:", + } + const changes: string[] = [] + const result = mergeWithNewestVersions(ours, theirs, changes, "dependencies") + expect(Object.keys(result)).toEqual(["npm-package-arg", "rotating-file-stream", "semver", "zod"]) +}) + +test("mergeWithNewestVersions appends theirs-only keys at the end", () => { + const ours = { a: "1.0.0", b: "1.0.0" } + const theirs = { a: "1.0.0", c: "1.0.0" } + const changes: string[] = [] + const result = mergeWithNewestVersions(ours, theirs, changes, "dependencies") + expect(Object.keys(result)).toEqual(["a", "b", "c"]) +}) diff --git a/script/upstream/transforms/transform-package-json.ts b/script/upstream/transforms/transform-package-json.ts index 327ee30facc..45f396a18ff 100644 --- a/script/upstream/transforms/transform-package-json.ts +++ b/script/upstream/transforms/transform-package-json.ts @@ -108,8 +108,14 @@ function compareVersions(a: string, b: string): number | null { /** * Merge two dependency objects using "newest wins" strategy * For non-comparable versions (URLs, catalog:, workspace:*), upstream (theirs) wins + * + * Key order preserves ours' order first (so kilo-only deps stay in their + * original position), then appends theirs-only keys at the end. This avoids + * relocating existing keys, which would otherwise let git's textual merge + * produce duplicate JSON keys (ours keeps the line in place, theirs appears + * to "add" the same key elsewhere → both survive the merge). */ -function mergeWithNewestVersions( +export function mergeWithNewestVersions( ours: Record | undefined, theirs: Record | undefined, changes: string[], @@ -117,38 +123,38 @@ function mergeWithNewestVersions( ): Record { const result: Record = {} - // Start with all of theirs - if (theirs) { - for (const [name, version] of Object.entries(theirs)) { - result[name] = version + // Seed with ours' keys in ours' order, applying newest-wins per key. + if (ours) { + for (const [name, ourVersion] of Object.entries(ours)) { + const theirVersion = theirs?.[name] + if (theirVersion === undefined) { + result[name] = ourVersion + changes.push(`${section}: preserved ${name}@${ourVersion} (kilo-only)`) + continue + } + if (ourVersion === theirVersion) { + result[name] = theirVersion + continue + } + const cmp = compareVersions(ourVersion, theirVersion) + if (cmp === null) { + result[name] = theirVersion + changes.push(`${section}: ${name} kept upstream ${theirVersion} (special format)`) + } else if (cmp > 0) { + result[name] = ourVersion + changes.push(`${section}: ${name} ${theirVersion} -> ${ourVersion} (kilo newer)`) + } else { + result[name] = theirVersion + if (cmp < 0) changes.push(`${section}: ${name} kept upstream ${theirVersion} (upstream newer)`) + } } } - // Merge in ours, keeping newer versions - if (ours) { - for (const [name, ourVersion] of Object.entries(ours)) { - const theirVersion = result[name] - - if (!theirVersion) { - // Dependency only exists in ours - keep it - result[name] = ourVersion - changes.push(`${section}: preserved ${name}@${ourVersion} (kilo-only)`) - } else if (ourVersion !== theirVersion) { - // Both have it with different versions - compare - const comparison = compareVersions(ourVersion, theirVersion) - - if (comparison === null) { - // Can't compare (special format) - upstream wins per user preference - changes.push(`${section}: ${name} kept upstream ${theirVersion} (special format)`) - } else if (comparison > 0) { - // Ours is newer - result[name] = ourVersion - changes.push(`${section}: ${name} ${theirVersion} -> ${ourVersion} (kilo newer)`) - } else if (comparison < 0) { - // Theirs is newer - already in result - changes.push(`${section}: ${name} kept upstream ${theirVersion} (upstream newer)`) - } - // If equal, keep theirs (already in result) + // Append any theirs-only keys at the end, preserving theirs' relative order. + if (theirs) { + for (const [name, version] of Object.entries(theirs)) { + if (result[name] === undefined) { + result[name] = version } } } From ee3ee5b06b9b8c06d2ef2a5ce0799c92fc5bdb91 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 7 May 2026 17:41:58 +0200 Subject: [PATCH 061/111] fix(upstream-merge): always reconcile package.json post-merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git rerere learns conflict resolutions from past upstream merges and replays them on the next merge. When past resolutions used the buggy mergeWithNewestVersions ordering, rerere auto-resolves package.json files with stale content before transformConflictedPackageJson ever gets a chance to run — so the fixed merge logic never reaches the file. Add reconcileAllPackageJson, which runs after every merge (clean, auto-resolved, or partially conflicted) and rewrites every package.json that the merge touched from the kilo branch's pre-merge HEAD and the opencode compat branch using the same merge logic. Files that are still conflicted are skipped so manual resolution isn't silently overwritten. This makes our merge logic the source of truth for package.json content, regardless of what rerere or git's textual merge produced. --- script/upstream/merge.ts | 35 ++- .../transforms/transform-package-json.ts | 240 ++++++++++++++++++ 2 files changed, 274 insertions(+), 1 deletion(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 02933816c1f..332e7795450 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -33,7 +33,11 @@ import { skipFiles } from "./transforms/skip-files" import { transformConflictedI18n, transformAllI18n } from "./transforms/transform-i18n" // New transforms for auto-resolving more conflict types import { transformConflictedTakeTheirs, transformAllTakeTheirs } from "./transforms/transform-take-theirs" -import { transformConflictedPackageJson, transformAllPackageJson } from "./transforms/transform-package-json" +import { + transformConflictedPackageJson, + transformAllPackageJson, + reconcileAllPackageJson, +} from "./transforms/transform-package-json" import { transformConflictedScripts, transformAllScripts } from "./transforms/transform-scripts" import { transformConflictedExtensions, transformAllExtensions } from "./transforms/transform-extensions" import { transformConflictedWeb, transformAllWeb } from "./transforms/transform-web" @@ -707,6 +711,24 @@ async function main() { } } + // Reconcile every package.json that the merge touched, regardless of + // whether it was conflicted, auto-resolved by rerere, or merged textually. + // rerere can replay stale resolutions that bypass our package.json + // transform entirely, so always run our merge logic as the final word for + // package.json content. Skip files that are still conflicted so the user + // can resolve them manually instead of silently overwriting markers. + const stillConflicted = new Set(await git.getConflictedFiles()) + const reconcileResults = await reconcileAllPackageJson({ + oursRef: baseSha, + theirsRef: opencodeBranch, + verbose: options.verbose, + skip: stillConflicted, + }) + const reconcileCount = reconcileResults.filter((r) => r.action === "transformed" && r.changes.length > 0).length + if (reconcileCount > 0) { + logger.success(`Reconciled ${reconcileCount} package.json file(s) post-merge`) + } + // Check remaining conflicts const remaining = await git.getConflictedFiles() // Combine git-reported conflicts with files flagged due to kilocode_change markers @@ -764,6 +786,17 @@ async function main() { } } else { logger.success("Merge completed without conflicts!") + // Same reconcile pass as the conflict path: ensure rerere or git's textual + // merge can't slip stale package.json resolutions through. + const reconcileResults = await reconcileAllPackageJson({ + oursRef: baseSha, + theirsRef: opencodeBranch, + verbose: options.verbose, + }) + const reconcileCount = reconcileResults.filter((r) => r.action === "transformed" && r.changes.length > 0).length + if (reconcileCount > 0) { + logger.success(`Reconciled ${reconcileCount} package.json file(s) post-merge`) + } await git.stageAll() const hasChanges = await git.hasUncommittedChanges() if (hasChanges) { diff --git a/script/upstream/transforms/transform-package-json.ts b/script/upstream/transforms/transform-package-json.ts index 45f396a18ff..501786e1453 100644 --- a/script/upstream/transforms/transform-package-json.ts +++ b/script/upstream/transforms/transform-package-json.ts @@ -175,6 +175,17 @@ export interface PackageJsonOptions { preserveVersion?: boolean } +export interface ReconcileOptions extends PackageJsonOptions { + oursRef: string + theirsRef: string + /** + * Files to skip (e.g. still-conflicted files where the user is going to + * resolve manually). The reconciler would otherwise overwrite the conflict + * markers and silently auto-resolve. + */ + skip?: Set +} + // Package name mappings const PACKAGE_NAME_MAP: Record = { "opencode-ai": "@kilocode/cli", @@ -729,6 +740,235 @@ export async function transformAllPackageJson(options: PackageJsonOptions = {}): return results } +/** + * Reconcile a single package.json after a merge has finished, regardless of + * whether it was conflicted or auto-resolved (by rerere or git's textual + * merge). Reads ours from `oursRef` and theirs from `theirsRef`, then applies + * the same merge logic used for conflict resolution and writes the result to + * the working tree. Stages the file. + * + * This is needed because rerere can replay stale resolutions for files like + * `package.json` that include cosmetic reordering — those resolutions bypass + * `transformConflictedPackageJson` entirely. Running this reconciler after + * the merge guarantees our merge logic always wins. + * + * Returns "skipped" if neither side touched the file (or both sides match) so + * callers can avoid unnecessary churn. Returns "flagged" if ours has + * kilocode_change markers (manual review needed). + */ +export async function reconcilePackageJsonFromRefs( + file: string, + options: ReconcileOptions, +): Promise { + const changes: string[] = [] + const dryRun = options.dryRun ?? false + + if (await oursHasKilocodeChanges(file)) { + warn(`${file} has kilocode_change markers — skipping reconcile, needs manual resolution`) + return { file, action: "flagged", changes: [], dryRun } + } + + let ourPkg: Record | null = null + try { + const ourContent = await $`git show ${options.oursRef}:${file}`.text() + ourPkg = JSON.parse(ourContent) + } catch { + // file didn't exist in ours - that's fine + } + + let pkg: Record | null = null + try { + const theirContent = await $`git show ${options.theirsRef}:${file}`.text() + pkg = JSON.parse(theirContent) + } catch { + // file didn't exist in theirs either - nothing to reconcile + } + + if (!pkg) { + if (!ourPkg) return { file, action: "skipped", changes: [], dryRun } + pkg = JSON.parse(JSON.stringify(ourPkg)) + } + + const relativePath = file.replace(process.cwd() + "/", "") + const newName = TRANSFORM_PACKAGE_NAMES[relativePath] + if (newName && pkg.name !== newName) { + changes.push(`name: ${pkg.name} -> ${newName}`) + pkg.name = newName + } + + if (options.preserveVersion !== false) { + const kiloVersion = await getCurrentVersion() + if (pkg.version !== kiloVersion) { + changes.push(`version: ${pkg.version} -> ${kiloVersion}`) + pkg.version = kiloVersion + } + } + + if (ourPkg) { + pkg.dependencies = mergeWithNewestVersions( + ourPkg.dependencies as Record | undefined, + pkg.dependencies as Record | undefined, + changes, + "dependencies", + ) + pkg.devDependencies = mergeWithNewestVersions( + ourPkg.devDependencies as Record | undefined, + pkg.devDependencies as Record | undefined, + changes, + "devDependencies", + ) + pkg.peerDependencies = mergeWithNewestVersions( + ourPkg.peerDependencies as Record | undefined, + pkg.peerDependencies as Record | undefined, + changes, + "peerDependencies", + ) + + const ourOverrides = ourPkg.overrides as Record | undefined + if (ourOverrides || pkg.overrides) { + pkg.overrides = mergeWithNewestVersions( + ourOverrides, + pkg.overrides as Record | undefined, + changes, + "overrides", + ) + } + + const ourPatched = ourPkg.patchedDependencies as Record | undefined + if (ourPatched) { + pkg.patchedDependencies = (pkg.patchedDependencies as Record) || {} + const patched = pkg.patchedDependencies as Record + for (const [name, patch] of Object.entries(ourPatched)) { + if (!patched[name]) { + patched[name] = patch + changes.push(`patchedDependencies: preserved ${name}`) + } + } + } + + const ourRepo = ourPkg.repository + if (ourRepo && JSON.stringify(pkg.repository) !== JSON.stringify(ourRepo)) { + pkg.repository = ourRepo + changes.push(`repository: preserved Kilo's repository configuration`) + } + + const ourWs = ourPkg.workspaces as { packages?: string[]; catalog?: Record } | undefined + const theirWs = pkg.workspaces as { packages?: string[]; catalog?: Record } | undefined + + if (relativePath === "package.json" && ourWs?.packages) { + pkg.workspaces = (pkg.workspaces as Record) || {} + ;(pkg.workspaces as { packages: string[] }).packages = ourWs.packages + changes.push(`workspaces.packages: preserved Kilo's workspace configuration`) + } + + fixScripts(pkg, relativePath, ourPkg, changes) + + if (ourWs?.catalog || theirWs?.catalog) { + pkg.workspaces = (pkg.workspaces as Record) || {} + ;(pkg.workspaces as { catalog: Record }).catalog = mergeWithNewestVersions( + ourWs?.catalog, + theirWs?.catalog, + changes, + "workspaces.catalog", + ) + } + + fixCatalog(pkg, relativePath, changes) + } + + if (pkg.dependencies) { + const { result, changes: depChanges } = transformDependencies(pkg.dependencies as Record) + pkg.dependencies = result + changes.push(...depChanges.map((c) => `dependencies: ${c}`)) + } + if (pkg.devDependencies) { + const { result, changes: devChanges } = transformDependencies(pkg.devDependencies as Record) + if (devChanges.length > 0) { + pkg.devDependencies = result + changes.push(...devChanges.map((c) => `devDependencies: ${c}`)) + } + } + if (pkg.peerDependencies) { + const { result, changes: peerChanges } = transformDependencies(pkg.peerDependencies as Record) + if (peerChanges.length > 0) { + pkg.peerDependencies = result + changes.push(...peerChanges.map((c) => `peerDependencies: ${c}`)) + } + } + + const kiloDeps = KILO_DEPENDENCIES[relativePath] + if (kiloDeps) { + pkg.dependencies = (pkg.dependencies as Record) || {} + const deps = pkg.dependencies as Record + for (const [name, version] of Object.entries(kiloDeps)) { + if (!deps[name]) { + deps[name] = version + changes.push(`injected: ${name}`) + } + } + } + + const kiloBin = KILO_BIN[relativePath] + if (kiloBin) { + pkg.bin = kiloBin + changes.push(`bin: set Kilo bin entries`) + } + + if (dryRun) { + info(`[DRY-RUN] Would reconcile ${file}: ${changes.length} changes`) + return { file, action: "transformed", changes, dryRun: true } + } + + const newContent = JSON.stringify(pkg, null, 2) + "\n" + await Bun.write(file, newContent) + await $`git add ${file}`.quiet().nothrow() + + if (changes.length > 0) { + success(`Reconciled ${file}: ${changes.length} changes`) + if (options.verbose) { + for (const change of changes) debug(` - ${change}`) + } + } + + return { file, action: "transformed", changes, dryRun: false } +} + +/** + * Reconcile every package.json that differs between `oursRef` and `theirsRef` + * after a merge. This is meant to run after `git merge` (whether the merge + * was clean, conflict-resolved, or rerere-replayed) to ensure our merge logic + * is the source of truth for package.json content. + */ +export async function reconcileAllPackageJson(options: ReconcileOptions): Promise { + // Collect every package.json that differs in either direction so we cover + // upstream-only and kilo-only files alike. + const diffOurs = await $`git diff --name-only ${options.oursRef} -- '*package.json'`.text() + const diffTheirs = await $`git diff --name-only ${options.theirsRef} -- '*package.json'`.text() + const candidates = new Set() + for (const line of [...diffOurs.split("\n"), ...diffTheirs.split("\n")]) { + const path = line.trim() + if (!path) continue + if (path.includes("node_modules")) continue + if (!path.endsWith("package.json")) continue + candidates.add(path) + } + + const results: PackageJsonResult[] = [] + for (const file of candidates) { + if (options.skip?.has(file)) { + results.push({ file, action: "skipped", changes: [], dryRun: options.dryRun ?? false }) + continue + } + const f = Bun.file(file) + if (!(await f.exists())) { + // file was removed by the merge - nothing to reconcile + continue + } + results.push(await reconcilePackageJsonFromRefs(file, options)) + } + return results +} + // CLI entry point if (import.meta.main) { const args = process.argv.slice(2) From 8710e9d9de7fbc02cb073919fad3f9925c8bd621 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:11:40 +0000 Subject: [PATCH 062/111] feat(upstream): record last merged tag in .opencode-version Avoids the ls-remote + merge-base --is-ancestor walk in last() for the common case by reading the recorded tag from .opencode-version. merge.ts writes the file as part of the pre-merge compat commit so future runs of fix-kilocode-markers, reset-to-upstream, and find-reset-candidates can resolve the base tag instantly. Seeded with v1.14.33 (PR #9978). --- .opencode-version | 1 + script/upstream/README.md | 4 +- script/upstream/merge.ts | 6 +++ script/upstream/utils/upstream.ts | 62 +++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .opencode-version diff --git a/.opencode-version b/.opencode-version new file mode 100644 index 00000000000..9866d345648 --- /dev/null +++ b/.opencode-version @@ -0,0 +1 @@ +v1.14.33 diff --git a/script/upstream/README.md b/script/upstream/README.md index ed37cd23422..2024a11ad6b 100644 --- a/script/upstream/README.md +++ b/script/upstream/README.md @@ -243,7 +243,9 @@ Options: --dry-run Show what would change without writing the file ``` -The command finds the newest upstream tag already merged into `HEAD`, reads that upstream version of the file, applies the same branding transforms used by upstream merge automation, strips existing `kilocode_change` markers from the current file, and adds fresh markers around the remaining lines that differ from upstream. +The command finds the newest upstream tag already merged into `HEAD` (read from `.opencode-version` at the repo root, falling back to an `ls-remote` + `merge-base --is-ancestor` walk), reads that upstream version of the file, applies the same branding transforms used by upstream merge automation, strips existing `kilocode_change` markers from the current file, and adds fresh markers around the remaining lines that differ from upstream. + +The `.opencode-version` file is a single-line tag (e.g. `v1.14.33`) recorded by `merge.ts` after every successful upstream merge. Editing it by hand pins the "last merged" tag for the per-file commands above; delete it to fall back to the slower automatic discovery. ### reset-to-upstream.ts diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 24587c9ecec..bca7a4b497a 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -38,6 +38,7 @@ import { transformConflictedScripts, transformAllScripts } from "./transforms/tr import { transformConflictedExtensions, transformAllExtensions } from "./transforms/transform-extensions" import { transformConflictedWeb, transformAllWeb } from "./transforms/transform-web" import { resolveLockFileConflicts, regenerateLockFiles } from "./transforms/lock-files" +import { writeVersion } from "./utils/upstream" interface MergeOptions { version?: string @@ -508,6 +509,11 @@ async function main() { const keepOursResults = await resetToOurs(config.keepOurs, { dryRun: false, verbose: options.verbose }) logger.success(`Reset ${keepOursResults.length} files to Kilo's version`) + // 6k. Record the last merged upstream tag so future automation can find it + // without walking ls-remote + isAncestor for every tag. + const versionFile = await writeVersion(targetVersion.tag) + logger.success(`Recorded ${targetVersion.tag} in ${versionFile.split("/").pop()}`) + // Clean untracked build artifacts from Kilo-specific directories. // These packages don't exist in upstream, so their .gitignore files are absent // on the opencode branch. Artifacts like bin/, out/, .next/ etc. would otherwise diff --git a/script/upstream/utils/upstream.ts b/script/upstream/utils/upstream.ts index 2ab0c1b3537..c63425eed48 100644 --- a/script/upstream/utils/upstream.ts +++ b/script/upstream/utils/upstream.ts @@ -15,6 +15,12 @@ import { isAncestor } from "./git" const url = "https://github.com/anomalyco/opencode.git" const workflows = [".github/workflows/publish.yml", ".github/workflows/beta.yml"] +/** + * Repo-relative path of the file that records the last merged upstream tag. + * Single line containing the upstream tag (e.g. `v1.14.33`). + */ +export const versionFile = ".opencode-version" + export async function root() { return (await $`git rev-parse --show-toplevel`.text()).trim() } @@ -39,6 +45,9 @@ export async function remote() { } export async function last(): Promise { + const recorded = await readVersionFile() + if (recorded) return recorded + const source = await remote() info(`Fetching upstream tags from ${source}...`) @@ -53,6 +62,59 @@ export async function last(): Promise { throw new Error("Could not find a merged upstream tag in HEAD") } +/** + * Read the recorded last-merged upstream tag from `.opencode-version`. Returns + * null if the file is missing/empty, or if the recorded tag cannot be resolved + * to a commit (e.g. tags have not been fetched yet). Falls back to the + * isAncestor-based discovery in `last()`. + */ +async function readVersionFile(): Promise { + const repo = await root() + const file = Bun.file(`${repo}/${versionFile}`) + if (!(await file.exists())) return null + + const tag = (await file.text()).trim() + if (!tag) return null + + const version = parseVersion(tag) + if (!version) { + warn(`${versionFile} contains '${tag}' which is not a valid version tag; ignoring`) + return null + } + + const commit = await resolveTag(tag) + if (!commit) return null + + return { version, tag, commit } +} + +async function resolveTag(tag: string): Promise { + const local = await $`git rev-parse --verify --quiet ${tag}^{commit}`.quiet().nothrow() + if (local.exitCode === 0) return local.stdout.toString().trim() + + const source = await remote() + info(`Tag ${tag} not present locally; fetching from ${source}...`) + const fetch = await $`git fetch ${source} tag ${tag} --no-tags`.quiet().nothrow() + if (fetch.exitCode !== 0) { + warn(`Failed to fetch tag ${tag}: ${fetch.stderr.toString()}`) + return null + } + + const after = await $`git rev-parse --verify --quiet ${tag}^{commit}`.quiet().nothrow() + return after.exitCode === 0 ? after.stdout.toString().trim() : null +} + +/** + * Record the merged upstream tag in `.opencode-version` so subsequent runs of + * `last()` resolve instantly without an `ls-remote` walk. + */ +export async function writeVersion(tag: string): Promise { + const repo = await root() + const path = `${repo}/${versionFile}` + await Bun.write(path, `${tag}\n`) + return path +} + export async function versions(source: string): Promise { const result = await $`git ls-remote --tags ${source}`.quiet().nothrow() if (result.exitCode !== 0) throw new Error(`Failed to list upstream tags: ${result.stderr.toString()}`) From 5482cc0d09f7c22e15063c75dac223f13b1fa4f9 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 11 May 2026 10:13:36 +0200 Subject: [PATCH 063/111] fix(vscode): improve review comment icon contrast (#10130) --- .changeset/review-plus-contrast.md | 6 ++++++ packages/kilo-ui/src/components/diff.tsx | 7 ++----- .../kilo-vscode/webview-ui/agent-manager/agent-manager.css | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .changeset/review-plus-contrast.md diff --git a/.changeset/review-plus-contrast.md b/.changeset/review-plus-contrast.md new file mode 100644 index 00000000000..49414a2be15 --- /dev/null +++ b/.changeset/review-plus-contrast.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/kilo-ui": patch +--- + +Improve review comment plus icon contrast in diff viewers. diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index 43abaaf2459..febb70ca813 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -256,10 +256,7 @@ export function Diff(props: DiffProps) { host.removeAttribute("data-color-scheme") } - // Patch a bug in @pierre/diffs where `grid-template-columns: 100% auto` is set - // for `line-info-basic` separators under `@media (pointer: fine)`, causing the - // expand button to consume 100% of the gutter width and overlap the separator - // content text. We inject into `@layer unsafe` which overrides `@layer base`. + // Patch Pierre shadow styles for Kilo-specific layout and contrast tweaks. let separatorPatchSheet: CSSStyleSheet | null = null const patchSeparatorLayout = () => { const root = getRoot() @@ -267,7 +264,7 @@ export function Diff(props: DiffProps) { if (!separatorPatchSheet) { separatorPatchSheet = new CSSStyleSheet() separatorPatchSheet.replaceSync( - `@layer unsafe { @media (pointer: fine) { [data-separator='line-info-basic'][data-expand-index] [data-separator-wrapper] { grid-template-columns: 34px auto; } } }`, + `@layer unsafe { @media (pointer: fine) { [data-separator='line-info-basic'][data-expand-index] [data-separator-wrapper] { grid-template-columns: 34px auto; } } [data-utility-button] { background-color: var(--vscode-button-background, var(--button-primary-base, var(--icon-interactive-base))); color: var(--vscode-button-foreground, var(--icon-invert-base, #fff)); } }`, ) } if (!root.adoptedStyleSheets.includes(separatorPatchSheet)) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 909688ed72d..5181f537af1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1991,8 +1991,8 @@ body.am-wt-dragging-active * { .am-markdown-comment-button:hover, .am-markdown-comment-button:focus-visible { - color: var(--text-base); - background: var(--surface-interactive-base); + color: var(--vscode-button-foreground, var(--icon-invert-base, #fff)); + background: var(--vscode-button-background, var(--button-primary-base, var(--icon-interactive-base))); } .am-markdown-line-number { From 1988db53e32f2a91a8ea1e21f3a87cef975c9b64 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 11 May 2026 10:29:20 +0200 Subject: [PATCH 064/111] chore: ignore generated Kilo config artifacts (#10128) * chore: ignore generated Kilo config artifacts * chore: ignore Kilo config node modules --- .../check-kilo-generated-artifacts.yml | 27 ++ .gitignore | 16 +- .kilo/package-lock.json | 376 ------------------ .kilocode/package-lock.json | 376 ------------------ script/check-kilo-generated-artifacts.ts | 51 +++ script/check-workflows.ts | 1 + 6 files changed, 94 insertions(+), 753 deletions(-) create mode 100644 .github/workflows/check-kilo-generated-artifacts.yml delete mode 100644 .kilo/package-lock.json delete mode 100644 .kilocode/package-lock.json create mode 100644 script/check-kilo-generated-artifacts.ts diff --git a/.github/workflows/check-kilo-generated-artifacts.yml b/.github/workflows/check-kilo-generated-artifacts.yml new file mode 100644 index 00000000000..1fa1c083f28 --- /dev/null +++ b/.github/workflows/check-kilo-generated-artifacts.yml @@ -0,0 +1,27 @@ +# kilocode_change - new file +name: Check Kilo generated artifacts + +on: + pull_request: + paths: + - ".gitignore" + - ".github/workflows/check-kilo-generated-artifacts.yml" + - ".kilo/**" + - ".kilocode/**" + - "script/check-kilo-generated-artifacts.ts" + workflow_dispatch: + +jobs: + check: + name: Check Kilo generated artifacts + if: github.repository == 'Kilo-Org/kilocode' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 # kilocode_change + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - uses: oven-sh/setup-bun@v2 + + - name: Run check + run: bun run script/check-kilo-generated-artifacts.ts diff --git a/.gitignore b/.gitignore index 95a44149d17..91bf7230c02 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,22 @@ telemetry-id tsconfig.tsbuildinfo # Kilo +.kilo/.gitignore +.kilo/package.json +.kilo/package-lock.json +.kilo/pnpm-lock.yaml +.kilo/bun.lock +.kilo/yarn.lock +.kilo/node_modules .kilo/plans/*upstream-merge-report-*.md +.kilocode/.gitignore +.kilocode/package.json +.kilocode/package-lock.json +.kilocode/pnpm-lock.yaml +.kilocode/bun.lock +.kilocode/yarn.lock +.kilocode/node_modules # Test Artifacts packages/app/.artifacts/ -packages/opencode/.artifacts/ \ No newline at end of file +packages/opencode/.artifacts/ diff --git a/.kilo/package-lock.json b/.kilo/package-lock.json deleted file mode 100644 index ce556edce27..00000000000 --- a/.kilo/package-lock.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "name": ".kilo", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@kilocode/plugin": "7.2.31" - } - }, - "node_modules/@kilocode/plugin": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz", - "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==", - "license": "MIT", - "dependencies": { - "@kilocode/sdk": "7.2.31", - "effect": "4.0.0-beta.48", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@kilocode/sdk": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz", - "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" - } - }, - "node_modules/fast-check": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", - "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "license": "MIT" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "license": "Apache-2.0" - }, - "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/.kilocode/package-lock.json b/.kilocode/package-lock.json deleted file mode 100644 index 32676c56243..00000000000 --- a/.kilocode/package-lock.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "name": ".kilocode", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@kilocode/plugin": "7.2.31" - } - }, - "node_modules/@kilocode/plugin": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz", - "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==", - "license": "MIT", - "dependencies": { - "@kilocode/sdk": "7.2.31", - "effect": "4.0.0-beta.48", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@kilocode/sdk": { - "version": "7.2.31", - "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz", - "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" - } - }, - "node_modules/fast-check": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", - "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "license": "MIT" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "license": "Apache-2.0" - }, - "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/script/check-kilo-generated-artifacts.ts b/script/check-kilo-generated-artifacts.ts new file mode 100644 index 00000000000..e7d377b0eef --- /dev/null +++ b/script/check-kilo-generated-artifacts.ts @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +// kilocode_change - new file + +/** + * Guards generated Kilo config dependency artifacts. + * + * Kilo loads project config from .kilo/ and .kilocode/ and installs + * @kilocode/plugin there at runtime. npm writes package.json, lockfiles, + * .gitignore, and node_modules as generated local state. These paths must stay + * untracked so background installs do not create recurring branch diffs. + */ + +import { spawnSync } from "node:child_process" + +const paths = [ + ".kilo/.gitignore", + ".kilo/package.json", + ".kilo/package-lock.json", + ".kilo/pnpm-lock.yaml", + ".kilo/bun.lock", + ".kilo/yarn.lock", + ".kilo/node_modules", + ".kilocode/.gitignore", + ".kilocode/package.json", + ".kilocode/package-lock.json", + ".kilocode/pnpm-lock.yaml", + ".kilocode/bun.lock", + ".kilocode/yarn.lock", + ".kilocode/node_modules", +] + +const git = spawnSync("git", ["ls-files", "-z", "--", ...paths], { encoding: "utf8" }) + +if (git.status !== 0) { + console.error(git.stderr.trim() || "git ls-files failed") + process.exit(1) +} + +const bad = git.stdout.split("\0").filter(Boolean).sort() + +if (bad.length === 0) { + console.log("check-kilo-generated-artifacts: ok") + process.exit(0) +} + +console.error("Generated Kilo config dependency artifacts are tracked:") +for (const file of bad) console.error(` ${file}`) +console.error("") +console.error("These files are created by runtime dependency installs in .kilo/ and .kilocode/.") +console.error("Remove them from git and keep them ignored.") +process.exit(1) diff --git a/script/check-workflows.ts b/script/check-workflows.ts index a7536cd0799..487b03f20a5 100644 --- a/script/check-workflows.ts +++ b/script/check-workflows.ts @@ -29,6 +29,7 @@ const DIR = path.join(ROOT, ".github", "workflows") const active = new Set([ "auto-docs.yml", "beta.yml", + "check-kilo-generated-artifacts.yml", "check-md-table-padding.yml", "check-opencode-annotations.yml", "check-org-member.yml", From d8582521c3d0b1daef7ca6da64cc2a8340786ca9 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 11 May 2026 10:30:01 +0200 Subject: [PATCH 065/111] fix(vscode): keep revert checkpoints accessible (#10129) * fix(vscode): keep revert checkpoints accessible * fix(vscode): format revert checkpoint guard --- .changeset/revert-checkpoints-stay.md | 5 +++++ .../tests/unit/revert-checkpoints.test.ts | 20 +++++++++++++++++++ .../src/components/chat/VscodeSessionTurn.tsx | 8 +++----- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 .changeset/revert-checkpoints-stay.md create mode 100644 packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts diff --git a/.changeset/revert-checkpoints-stay.md b/.changeset/revert-checkpoints-stay.md new file mode 100644 index 00000000000..bb505f02d57 --- /dev/null +++ b/.changeset/revert-checkpoints-stay.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep older revert checkpoints available after reverting a message in the VS Code extension. diff --git a/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts b/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts new file mode 100644 index 00000000000..83f03b0fa76 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") +const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx") + +const src = fs.readFileSync(TURN_FILE, "utf-8") + +describe("message revert checkpoints", () => { + it("keeps revert actions available after a session is already reverted", () => { + expect(src).toMatch(/onRevert=\{\s*assistantMessages\(\)\.length > 0\s*\? \(\) =>/) + expect(src).not.toMatch(/onRevert=\{[\s\S]*?&& !session\.revert\(\)[\s\S]*?\? \(\) =>/) + }) + + it("only marks revert disabled while the agent is busy", () => { + expect(src).toMatch(/data-revert-disabled=\{\s*assistantMessages\(\)\.length > 0 && session\.status\(\) !== "idle"/) + expect(src).not.toMatch(/data-revert-disabled=\{[\s\S]*?!session\.revert\(\)/) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index c4de848fa2a..c667615beae 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -122,11 +122,9 @@ export const VscodeSessionTurn: Component = (props) => {
0 && !session.revert() && session.status() !== "idle" ? "" : undefined - } + data-revert-disabled={assistantMessages().length > 0 && session.status() !== "idle" ? "" : undefined} title={ - assistantMessages().length > 0 && !session.revert() && session.status() !== "idle" + assistantMessages().length > 0 && session.status() !== "idle" ? language.t("revert.disabled.agentBusy") : undefined } @@ -138,7 +136,7 @@ export const VscodeSessionTurn: Component = (props) => { queued={props.queued} onFork={props.onForkMessage ? () => props.onForkMessage?.(msg().sessionID, msg().id) : undefined} onRevert={ - assistantMessages().length > 0 && !session.revert() + assistantMessages().length > 0 ? () => { if (session.status() !== "idle") return session.revertSession(msg().id) From 5b8121ea5c70e161a2596a3350b8d489ef903c89 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:51:59 +0000 Subject: [PATCH 066/111] fix(upstream): rename shadowing path local in writeVersion --- script/upstream/utils/upstream.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/upstream/utils/upstream.ts b/script/upstream/utils/upstream.ts index c63425eed48..ab4107c40db 100644 --- a/script/upstream/utils/upstream.ts +++ b/script/upstream/utils/upstream.ts @@ -110,9 +110,9 @@ async function resolveTag(tag: string): Promise { */ export async function writeVersion(tag: string): Promise { const repo = await root() - const path = `${repo}/${versionFile}` - await Bun.write(path, `${tag}\n`) - return path + const dest = `${repo}/${versionFile}` + await Bun.write(dest, `${tag}\n`) + return dest } export async function versions(source: string): Promise { From c11f63391d1d85919ff092a67f1c49a5c9d69608 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Mon, 11 May 2026 10:59:43 +0200 Subject: [PATCH 067/111] Fix cursor positioning when text ends with newline (#10133) * fix(vscode): fix cursor positioning when text ends with newline A trailing newline is collapsed in a
with white-space: pre-wrap but renders as a real empty line in the