mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(jetbrains): bound streaming reasoning resources
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Improve JetBrains reasoning blocks so active reasoning opens while streaming, empty blocks stay hidden, and adjacent reasoning renders as one block.
|
||||
Improve JetBrains reasoning blocks so active reasoning opens while streaming, completed reasoning collapses automatically, empty blocks stay hidden, and adjacent reasoning renders as one block.
|
||||
|
||||
@@ -56,6 +56,7 @@ tsconfig.tsbuildinfo
|
||||
.kilo/bun.lock
|
||||
.kilo/yarn.lock
|
||||
.kilo/node_modules
|
||||
.kilo/plans/
|
||||
.kilo/plans/*upstream-merge-report-*.md
|
||||
.kilocode/.gitignore
|
||||
.kilocode/package.json
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Plan: Repo-Relative Search Tool Paths
|
||||
|
||||
## Goal
|
||||
|
||||
Update JetBrains search-style tool headers so the path target is displayed relative to the current repo/workspace directory:
|
||||
|
||||
- If the path is inside the repo, show the relative path.
|
||||
- If the path resolves to the repo root (`.` or the repo directory), hide the path target entirely.
|
||||
- If the path is outside the repo, show the full normalized path.
|
||||
- Apply this to all current search-style tools: `glob` and `grep`/Search.
|
||||
- Use IntelliJ path utilities instead of handwritten string prefix/splitting logic.
|
||||
|
||||
## Findings
|
||||
|
||||
- Search-style target rendering is centralized in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt`:
|
||||
- `globDirectory(tool)` currently returns raw `tool.input["path"]` or `tool.title`.
|
||||
- `searchTargets(tool)` currently returns raw `tool.input["path"]`, then `pattern=...`, then `include=...`.
|
||||
- `GlobToolView` and `SearchToolView` both extend `BaseSearchToolView`, which calls `targets(item)` during base initialization.
|
||||
- `SessionUi` has the repo/workspace directory as `workspace.directory`, but the view creation path currently does not pass it to tool views.
|
||||
- In JetBrains split mode, frontend and backend may not share OS path semantics, so local `java.nio.file.Path` interpretation alone can be risky for backend-originated paths.
|
||||
- IntelliJ provides suitable path helpers in `com.intellij.openapi.util.io`:
|
||||
- `FileUtil.toSystemIndependentName(...)`
|
||||
- `FileUtil.toCanonicalPath(..., '/', true)`
|
||||
- `FileUtil.join(...)`
|
||||
- `FileUtil.getRelativePath(base, file, '/')`
|
||||
- `OSAgnosticPathUtil.isAbsolute(...)`
|
||||
- `OSAgnosticPathUtil.startsWith(...)`
|
||||
- These helpers are string/path-text utilities and do not require VFS refreshes, file existence checks, RPC, git queries, or network calls.
|
||||
|
||||
## EDT Safety Requirements
|
||||
|
||||
- All search header synchronization runs on the EDT, so path display formatting must be deterministic, local, and non-blocking.
|
||||
- Do not resolve the repo root during rendering. Use the already-known `Workspace.directory` captured by `SessionUi` and pass it down as an immutable string.
|
||||
- Do not call any API that can touch disk, VFS, git, RPC, backend services, or the network from `BaseSearchToolView.sync()` / `targets(...)` / the path formatter.
|
||||
- Specifically avoid `Files.exists`, `Path.toRealPath`, `File.getCanonicalPath`, `LocalFileSystem.refreshAndFindFileByPath`, `VfsUtil`, `Project.baseDir`, `GitRepositoryManager`, `KiloWorkspaceService`, and any coroutine/RPC call in this path.
|
||||
- The formatter should only use pure string helpers such as `FileUtil.toSystemIndependentName`, `FileUtil.toCanonicalPath(path, '/', true)`, `FileUtil.getRelativePath(base, file, '/')`, and `OSAgnosticPathUtil` checks.
|
||||
- The amount of work is bounded to a few short path strings per render/update, so no background dispatch or caching is needed unless implementation profiling later shows otherwise.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add an internal search path formatter in `ToolSupport.kt`.
|
||||
- Keep it pure and UI-independent, for example `internal fun searchPath(path: String, repo: String?): String`.
|
||||
- Normalize both `path` and `repo` with `FileUtil.toSystemIndependentName` and `FileUtil.toCanonicalPath(..., '/', true)`.
|
||||
- Use `OSAgnosticPathUtil.isAbsolute(...)` to decide whether the tool path is absolute.
|
||||
- Resolve relative tool paths against the normalized repo with `FileUtil.join(...)`, then canonicalize.
|
||||
- If no repo is available, keep the existing raw display behavior except hide `.`.
|
||||
- If the resolved target equals the repo root, return `""` so the target row is hidden.
|
||||
- If the resolved target is under the repo root, return `FileUtil.getRelativePath(repo, target, '/')`.
|
||||
- If the resolved target is outside the repo, return the full normalized target path.
|
||||
- Avoid manual prefix checks, separator splitting, or homemade `../` handling.
|
||||
- Keep this formatter string-only: no `java.nio.file.Files`, VFS, git, project model, service, or RPC access.
|
||||
|
||||
2. Update target helper functions in `ToolSupport.kt`.
|
||||
- Change `globDirectory(tool)` to accept `repo: String?` and format the path/title through `searchPath(...)`.
|
||||
- Change `searchTargets(tool)` to accept `repo: String?` and format only the `path` element through `searchPath(...)`.
|
||||
- Keep `pattern=...` and `include=...` unchanged.
|
||||
- Filter blank formatted paths so root paths disappear and pattern/include shift left as they do today when path is absent.
|
||||
|
||||
3. Pass repo context into search tool views.
|
||||
- Add `repo: String? = null` to `BaseSearchToolView` and store it in the base class.
|
||||
- Change the abstract target hook to use the base-owned repo, e.g. `targets(tool: Tool, repo: String?): List<String>`.
|
||||
- This avoids accessing subclass properties from `BaseSearchToolView.init`, which already calls `sync()`.
|
||||
- Add optional `repo` constructor parameters to `GlobToolView` and `SearchToolView` and pass them to `BaseSearchToolView`.
|
||||
- Preserve existing defaults so direct tests and call sites that do not know a repo still compile.
|
||||
- Store the repo string as provided; do not lazily resolve or refresh it from IntelliJ project state inside the view.
|
||||
|
||||
4. Propagate `workspace.directory` through the session view creation path.
|
||||
- Add optional `repo: String? = null` parameters through:
|
||||
- `SessionMessageListPanel`
|
||||
- `TurnView`
|
||||
- `MessageView`
|
||||
- `ViewFactory.create(...)`
|
||||
- `ViewFactory.createUser(...)`
|
||||
- In `SessionUi.buildUi()`, pass `repo = workspace.directory` when constructing `SessionMessageListPanel`.
|
||||
- In `ViewFactory`, pass the repo only to `GlobToolView` and `SearchToolView`; other tool views keep current behavior.
|
||||
|
||||
5. Update tests.
|
||||
- In `GlobToolViewTest`, add coverage for:
|
||||
- Absolute path inside repo displays as relative, e.g. `src`.
|
||||
- `.` and exact repo root hide the path row.
|
||||
- Absolute path outside repo stays full/normalized.
|
||||
- In `SearchToolViewTest`, add the same repo-relative/root/outside cases for the `path` target while keeping `pattern` and `include` rows unchanged.
|
||||
- Keep existing no-repo tests to prove fallback behavior remains stable.
|
||||
- Update any existing expectations that intentionally pass a repo.
|
||||
- Prefer portable test paths built from simple normalized roots; do not depend on files actually existing.
|
||||
|
||||
6. Add a patch changeset.
|
||||
- This is user-visible JetBrains UI behavior.
|
||||
- Add a `.changeset/<slug>.md` entry for `"kilo-code": patch`.
|
||||
- Suggested wording: `Display JetBrains search tool paths relative to the current repository when possible.`
|
||||
|
||||
## Verification
|
||||
|
||||
Run from `packages/kilo-jetbrains/`:
|
||||
|
||||
1. `./gradlew :frontend:test --tests ai.kilocode.client.session.views.GlobToolViewTest --tests ai.kilocode.client.session.views.SearchToolViewTest`
|
||||
2. `./gradlew typecheck`
|
||||
|
||||
If constructor propagation causes broader frontend compile errors, fix those and rerun the same checks.
|
||||
|
||||
## Notes
|
||||
|
||||
- No `kilocode_change` markers are needed because this is under `packages/kilo-jetbrains/`, a Kilo-owned package.
|
||||
- Keep the change scoped to search-style tool header paths. Do not alter read tool filename display or tool body output formatting unless requested separately.
|
||||
@@ -1,139 +0,0 @@
|
||||
# Refactor SessionUiStyle View Tokens
|
||||
|
||||
## Goal
|
||||
Refactor `SessionUiStyle.View` so tokens are grouped by semantic meaning and call sites describe what they are styling. At the same time, simplify session borders so the style layer only defines two outline colors plus one border width:
|
||||
|
||||
- Bright outline color: prompt/user prompt bubble, prompt input shell, and all question-style views.
|
||||
- Regular outline color: reasoning and all other session card borders/separators by default.
|
||||
- Border width: shared one-pixel outline width used by views when constructing their own borders.
|
||||
|
||||
## Findings
|
||||
- `SessionUiStyle.View` currently mixes transcript backgrounds, card layout constants, card surfaces, hover colors, outline colors, border factories, and nested component groups in one object.
|
||||
- Current `View.line()` is a high-contrast editor-background-derived color and is used for both bright prompt/question borders and regular card/separator borders.
|
||||
- Current `View.sessionViewOutline()` delegates to `UiStyle.Colors.contentBorder()`, which is the right softer/default outline color.
|
||||
- Existing border helpers (`sessionView`, `outline`, `topOutline`, `leftOutline`) hide whether a call site wants bright or regular outline styling and mix color decisions with border-shape decisions.
|
||||
- The main production call-site groups are:
|
||||
- Transcript backgrounds: `SessionMessageListPanel`, `SessionScroll`, `TextView`.
|
||||
- Card layout/surfaces/hover: base part views, tool/todo/question result views.
|
||||
- Bright prompt/question borders: `PromptPanel.PromptShell`, `MessageView` user prompt bubble, `BaseQuestionView`, `QuestionResultView`.
|
||||
- Default cards: `PrimarySessionPartView`, `SecondarySessionPartView`, `ReasoningView`, tool body panes, todo body panes.
|
||||
- Popup/content panel styling: `SessionAccountOverlay`.
|
||||
- Separators: `ConnectionPanel`, `Dock.banner()`, `CompactionView`.
|
||||
|
||||
## Proposed API
|
||||
Refactor `SessionUiStyle.kt` without keeping compatibility aliases for old messy names.
|
||||
|
||||
```kotlin
|
||||
object SessionUiStyle {
|
||||
object Transcript {
|
||||
fun bgColor(): Color
|
||||
}
|
||||
|
||||
object View {
|
||||
object Layout {
|
||||
const val GAP = 6
|
||||
const val VERTICAL_PADDING = 8
|
||||
const val HORIZONTAL_PADDING = 12
|
||||
const val BODY_EXTRA_HEIGHT = 16
|
||||
}
|
||||
|
||||
object Surface {
|
||||
fun bgColor(): Color
|
||||
fun headerBgColor(): Color
|
||||
fun headerHoverBgColor(): Color
|
||||
}
|
||||
|
||||
object Outline {
|
||||
fun color(): Color
|
||||
fun brightColor(): Color
|
||||
fun hoverColor(): Color
|
||||
fun width(): Int
|
||||
}
|
||||
|
||||
object Prompt { ... }
|
||||
object Reasoning { ... }
|
||||
object Message { ... }
|
||||
object Code { ... }
|
||||
object Permission { ... }
|
||||
object Tool { ... }
|
||||
}
|
||||
|
||||
object AccountPopup {
|
||||
fun bgColor(): Color
|
||||
fun outlineColor(): Color
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Concrete color mapping:
|
||||
- `View.Outline.color()` returns the softer regular outline, using `UiStyle.Colors.contentBorder()`.
|
||||
- `View.Outline.brightColor()` preserves the current bright `line()` behavior, using `UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA)`.
|
||||
- `View.Outline.hoverColor()` preserves the current hover outline calculation for `Surface.headerHoverBgColor()`.
|
||||
- `View.Outline.width()` returns `JBUI.scale(1)`.
|
||||
- `View.Surface.bgColor()` and `View.Surface.headerBgColor()` both use `UiStyle.Colors.editorBackground()`.
|
||||
- `View.Surface.headerHoverBgColor()` preserves the current `headerHover()` behavior.
|
||||
- `SessionUiStyle.Transcript.bgColor()` replaces `View.transcript()`.
|
||||
- `SessionUiStyle.AccountPopup.bgColor()` replaces `View.sessionViewBackground()`.
|
||||
- `SessionUiStyle.AccountPopup.outlineColor()` replaces `View.sessionViewOutline()`.
|
||||
|
||||
Border construction rule:
|
||||
- `SessionUiStyle` does not expose all-side/top/left border factories.
|
||||
- Views construct borders locally from `View.Outline.color()` or `View.Outline.brightColor()` plus `View.Outline.width()` according to their layout.
|
||||
- Examples: all-side cards use `JBUI.Borders.customLine(color, width)`, body separators use `JBUI.Borders.customLine(color, width, 0, 0, 0)`, reasoning uses `JBUI.Borders.customLine(color, 0, width, 0, 0)`, and rounded prompt/question shells paint using the same color and width.
|
||||
|
||||
## Implementation Plan
|
||||
1. Update `SessionUiStyle.kt`.
|
||||
- Add `Transcript`, `View.Layout`, `View.Surface`, `View.Outline`, and `AccountPopup` groups.
|
||||
- Move current `SESSION_VIEW_*` constants into `View.Layout`.
|
||||
- Move current `surface`, `header`, and `headerHover` into `View.Surface` with `*BgColor` names.
|
||||
- Replace `line` and `hoverLine` with the `View.Outline` color/width API above.
|
||||
- Remove `sessionView`, `outline`, `topOutline`, and `leftOutline` instead of replacing them with new border factory helpers.
|
||||
- Remove old methods/constants after call sites are migrated.
|
||||
|
||||
2. Update regular session-card call sites to softer outlines.
|
||||
- `PrimarySessionPartView.syncBorder()` constructs an all-side border from `SessionUiStyle.View.Outline.color()` and `width()`.
|
||||
- `SecondarySessionPartView.syncBorder()` constructs an all-side border from `SessionUiStyle.View.Outline.color()` and `width()`.
|
||||
- `ReasoningView.syncBorder()` constructs a left-only border from `SessionUiStyle.View.Outline.color()` and `width()`.
|
||||
- Tool/todo body separators construct top-only borders from `SessionUiStyle.View.Outline.color()` and `width()`.
|
||||
- Connection, dock banner, and compaction separators use `SessionUiStyle.View.Outline.color()` and `width()` as appropriate.
|
||||
|
||||
3. Update bright prompt/question call sites.
|
||||
- `PromptPanel.PromptShell.outlineColor()` -> `SessionUiStyle.View.Outline.brightColor()` when not focused.
|
||||
- `MessageView.paintComponent()` user prompt bubble outline -> `brightColor()`.
|
||||
- `BaseQuestionView.outlineColor()` -> `brightColor()`.
|
||||
- `QuestionResultView.syncBorder()` constructs an all-side border from `brightColor()` and `width()` when expanded.
|
||||
- `QuestionResultView` body separator constructs a top-only border from `brightColor()` and `width()`.
|
||||
|
||||
4. Update semantic surface/layout call sites.
|
||||
- Transcript backgrounds -> `SessionUiStyle.Transcript.bgColor()`.
|
||||
- Card backgrounds -> `SessionUiStyle.View.Surface.bgColor()`.
|
||||
- Header backgrounds -> `SessionUiStyle.View.Surface.headerBgColor()`.
|
||||
- Hover header backgrounds -> `SessionUiStyle.View.Surface.headerHoverBgColor()`.
|
||||
- Layout constants -> `SessionUiStyle.View.Layout.*`.
|
||||
- Account popup background/border test helper -> `SessionUiStyle.AccountPopup.*`.
|
||||
|
||||
5. Update tests.
|
||||
- Replace direct assertions against old names with semantic new names.
|
||||
- Add or adjust assertions so prompt/question borders use `View.Outline.brightColor()`.
|
||||
- Add or adjust assertions so reasoning/tool/regular expanded card borders use `View.Outline.color()`.
|
||||
- Keep hover tests asserting only header background changes, using `View.Surface.headerHoverBgColor()` and `headerBgColor()`.
|
||||
- Finish the pending `QuestionResultViewTest` all-side border assertions and make them check the bright outline.
|
||||
|
||||
6. Changeset.
|
||||
- Add a patch changeset for `@kilocode/kilo-jetbrains` because the visible JetBrains session border contrast changes.
|
||||
- Suggested release note: `Refine JetBrains session card borders so prompt and question surfaces use brighter outlines while reasoning and tool cards use softer default borders.`
|
||||
|
||||
## Verification
|
||||
Run from `packages/kilo-jetbrains/`:
|
||||
|
||||
```sh
|
||||
./gradlew :frontend:test --tests ai.kilocode.client.session.views.base.AbstractSessionPartViewTest --tests ai.kilocode.client.session.views.QuestionResultViewTest --tests ai.kilocode.client.session.ui.SessionMessageListPanelTest --tests ai.kilocode.client.session.views.ToolViewTest --tests ai.kilocode.client.session.views.base.BaseQuestionViewTest --tests ai.kilocode.client.session.views.question.QuestionViewTest --tests ai.kilocode.client.session.views.permission.PermissionViewTest --tests ai.kilocode.client.session.ui.account.SessionAccountOverlayTest
|
||||
./gradlew typecheck
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- This task touches only `packages/kilo-jetbrains/` and a changeset.
|
||||
- Preserve unrelated worktree changes and do not revert user/agent changes from the previous hover/border work.
|
||||
- Keep Swing UI mutations on the EDT.
|
||||
- Do not introduce Compose, JCEF, Kotlin UI DSL, services, RPC, or broad UI rewrites.
|
||||
- Prefer small, mechanical call-site updates over deeper component refactors.
|
||||
+3
@@ -53,6 +53,9 @@ class MessageView(
|
||||
get() = if (role == SessionUiStyle.View.Message.USER_ROLE) SessionView.Kind.UserPrompt else SessionView.Kind.Default
|
||||
|
||||
private val parts = LinkedHashMap<String, PartView>()
|
||||
// Adjacent reasoning parts render through the first ReasoningView. aliases maps each
|
||||
// merged child id to that owner id, and sources stores the child's latest full text
|
||||
// so snapshot updates can append only deltas.
|
||||
private val aliases = LinkedHashMap<String, String>()
|
||||
private val sources = LinkedHashMap<String, String>()
|
||||
private var hidden: ToolCallRef? = null
|
||||
|
||||
+42
-5
@@ -15,6 +15,7 @@ import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
@@ -40,7 +41,9 @@ class ReasoningView(
|
||||
|
||||
override val contentId: String = reasoning.id
|
||||
|
||||
/** Lazily creates, registers, populates, and styles the editor-backed body on first access. */
|
||||
val md: MdView
|
||||
@RequiresEdt
|
||||
get() {
|
||||
val fresh = !parts.bodyCreated()
|
||||
val view = parts.md(openUrl)
|
||||
@@ -56,6 +59,7 @@ class ReasoningView(
|
||||
private var source = reasoning.content.toString()
|
||||
private var done = reasoning.done
|
||||
private var registered = false
|
||||
private var following = false
|
||||
|
||||
init {
|
||||
row.border = JBUI.Borders.empty(
|
||||
@@ -69,6 +73,7 @@ class ReasoningView(
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
@@ -78,6 +83,7 @@ class ReasoningView(
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (!changed) return false
|
||||
@@ -85,10 +91,13 @@ class ReasoningView(
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Reasoning) return
|
||||
var changed = false
|
||||
val next = content.content.toString()
|
||||
val finished = !done && content.done
|
||||
val follow = tailVisible()
|
||||
if (done != content.done) {
|
||||
done = content.done
|
||||
changed = true
|
||||
@@ -97,36 +106,50 @@ class ReasoningView(
|
||||
source = next
|
||||
if (parts.bodyCreated()) {
|
||||
md.set(source)
|
||||
followTail()
|
||||
followTail(follow)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
if (finished) changed = collapse() || changed
|
||||
changed = sync() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun appendDelta(delta: String) {
|
||||
if (delta.isEmpty()) return
|
||||
val follow = tailVisible()
|
||||
source += delta
|
||||
if (parts.bodyCreated()) {
|
||||
md.append(delta)
|
||||
followTail()
|
||||
followTail(follow)
|
||||
}
|
||||
val changed = sync()
|
||||
if (changed || bodyVisible()) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun markdown(): String = source
|
||||
@RequiresEdt
|
||||
fun hasToggle(): Boolean = arrow.isVisible
|
||||
@RequiresEdt
|
||||
fun headerText(): String = parts.title.text
|
||||
@RequiresEdt
|
||||
internal fun headerFont() = parts.title.font
|
||||
@RequiresEdt
|
||||
internal fun bodyVisible() = parts.scrollOrNull?.parent === this
|
||||
@RequiresEdt
|
||||
internal fun horizontalPolicy() = parts.scrollOrNull?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Reasoning.BODY_LINES
|
||||
@RequiresEdt
|
||||
internal fun bodyCreated() = parts.bodyCreated()
|
||||
@RequiresEdt
|
||||
internal fun bodyScrollValue() = parts.scrollOrNull?.verticalScrollBar?.value ?: 0
|
||||
@RequiresEdt
|
||||
internal fun bodyScrollBottom() = parts.scrollOrNull?.verticalScrollBar?.let { it.maximum - it.visibleAmount } ?: 0
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
@@ -138,6 +161,7 @@ class ReasoningView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
@@ -188,11 +212,12 @@ class ReasoningView(
|
||||
return changed
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun syncBody() {
|
||||
val md = md
|
||||
registerBody(md)
|
||||
md.set(source)
|
||||
followTail()
|
||||
followTail(true)
|
||||
}
|
||||
|
||||
private fun applyBodyStyle(): Boolean {
|
||||
@@ -216,10 +241,22 @@ class ReasoningView(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
private fun followTail() {
|
||||
if (!bodyVisible()) return
|
||||
@RequiresEdt
|
||||
private fun tailVisible(): Boolean {
|
||||
if (!bodyVisible()) return false
|
||||
val scroll = parts.scrollOrNull ?: return false
|
||||
val bar = scroll.verticalScrollBar
|
||||
return bar.value >= bar.maximum - bar.visibleAmount
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun followTail(follow: Boolean) {
|
||||
if (!follow || !bodyVisible() || following) return
|
||||
val scroll = parts.scrollOrNull ?: return
|
||||
following = true
|
||||
SwingUtilities.invokeLater {
|
||||
following = false
|
||||
if (!bodyVisible()) return@invokeLater
|
||||
val bar = scroll.verticalScrollBar
|
||||
bar.value = bar.maximum - bar.visibleAmount
|
||||
}
|
||||
|
||||
+24
@@ -9,6 +9,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Dimension
|
||||
import javax.swing.Icon
|
||||
@@ -39,6 +40,7 @@ abstract class BaseSearchToolView(
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
@@ -47,6 +49,7 @@ abstract class BaseSearchToolView(
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
@@ -54,6 +57,7 @@ abstract class BaseSearchToolView(
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
item = content
|
||||
@@ -62,29 +66,49 @@ abstract class BaseSearchToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text).plus(targetTexts()).plus(parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
@RequiresEdt
|
||||
fun bodyText(): String = body(item)
|
||||
@RequiresEdt
|
||||
internal fun targetTexts(): List<String> = parts.targets.map { it.text }.filter { it.isNotBlank() }
|
||||
@RequiresEdt
|
||||
internal fun targetVisible(index: Int): Boolean = parts.targets.getOrNull(index)?.isVisible ?: false
|
||||
@RequiresEdt
|
||||
internal fun bodyVisible() = parts.scroll?.parent === this
|
||||
@RequiresEdt
|
||||
internal fun hasToggle() = arrow.isVisible
|
||||
@RequiresEdt
|
||||
internal fun bodyFont() = parts.content?.font ?: style.editorFont
|
||||
@RequiresEdt
|
||||
internal fun titleFont() = parts.title.font
|
||||
@RequiresEdt
|
||||
internal fun targetFont(index: Int) = parts.targets.getOrNull(index)?.font ?: style.regularFont
|
||||
@RequiresEdt
|
||||
internal fun stateFont() = parts.state.font
|
||||
@RequiresEdt
|
||||
internal fun bodyCreated() = parts.bodyCreated()
|
||||
@RequiresEdt
|
||||
internal fun scrollComponent() = parts.scroll
|
||||
@RequiresEdt
|
||||
internal fun bodyEditor() = parts.content?.editor
|
||||
@RequiresEdt
|
||||
internal fun horizontalPolicy() = parts.scroll?.horizontalScrollBarPolicy
|
||||
@RequiresEdt
|
||||
internal fun verticalPolicy() = parts.scroll?.verticalScrollBarPolicy
|
||||
@RequiresEdt
|
||||
internal fun bodyWrap() = parts.content?.lineWrap ?: false
|
||||
@RequiresEdt
|
||||
internal fun headerComponent() = parts.header
|
||||
@RequiresEdt
|
||||
internal fun centerComponent() = parts.center
|
||||
@RequiresEdt
|
||||
internal fun targetComponents() = parts.targets
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
|
||||
+23
@@ -9,6 +9,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Dimension
|
||||
import javax.swing.ScrollPaneConstants
|
||||
@@ -38,6 +39,7 @@ class ReadToolView(
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
@@ -45,6 +47,7 @@ class ReadToolView(
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
item = content
|
||||
@@ -53,28 +56,48 @@ class ReadToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
@RequiresEdt
|
||||
fun bodyText(): String = body(item)
|
||||
@RequiresEdt
|
||||
internal fun bodyVisible() = parts.scroll?.parent === this
|
||||
@RequiresEdt
|
||||
internal fun hasToggle() = arrow.isVisible
|
||||
@RequiresEdt
|
||||
internal fun horizontalPolicy() = parts.scroll?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
@RequiresEdt
|
||||
internal fun bodyFont() = parts.text?.font ?: style.transcriptFont
|
||||
@RequiresEdt
|
||||
internal fun bodyCreated() = parts.bodyCreated()
|
||||
@RequiresEdt
|
||||
internal fun bodyWrap() = parts.text?.lineWrap ?: false
|
||||
@RequiresEdt
|
||||
internal fun bodyEditor() = parts.content?.editor
|
||||
@RequiresEdt
|
||||
internal fun linkVisible() = parts.link.isVisible
|
||||
@RequiresEdt
|
||||
internal fun linkText() = parts.label
|
||||
@RequiresEdt
|
||||
internal fun linkMarkup() = parts.link.text ?: ""
|
||||
@RequiresEdt
|
||||
internal fun linkForeground() = parts.link.foreground
|
||||
@RequiresEdt
|
||||
internal fun linkFont() = parts.link.font
|
||||
@RequiresEdt
|
||||
internal fun subtitleForeground() = parts.sub.foreground
|
||||
@RequiresEdt
|
||||
internal fun subtitleFont() = parts.sub.font
|
||||
@RequiresEdt
|
||||
internal fun linkHref() = parts.href
|
||||
@RequiresEdt
|
||||
internal fun openLink() = parts.openLink()
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
|
||||
+39
-9
@@ -26,13 +26,14 @@ import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
@@ -65,23 +66,30 @@ class ToolParts(
|
||||
private var body: ToolBody? = null
|
||||
|
||||
val text: JBTextArea?
|
||||
@RequiresEdt
|
||||
get() = body?.area
|
||||
|
||||
val content: ToolBody?
|
||||
@RequiresEdt
|
||||
get() = body
|
||||
|
||||
val scroll: JBScrollPane?
|
||||
@RequiresEdt
|
||||
get() = body?.scroll
|
||||
|
||||
@RequiresEdt
|
||||
fun scroll(tool: Tool): JBScrollPane = body(tool).scroll
|
||||
|
||||
@RequiresEdt
|
||||
fun bodyCreated() = body != null
|
||||
|
||||
@RequiresEdt
|
||||
fun openLink() {
|
||||
val value = href ?: return
|
||||
open?.invoke(value)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun body(tool: Tool): ToolBody {
|
||||
val item = body
|
||||
if (item != null) return item
|
||||
@@ -100,7 +108,9 @@ class ToolBody private constructor(
|
||||
private val disposable: Disposable?,
|
||||
) : Disposable {
|
||||
var text: String
|
||||
@RequiresEdt
|
||||
get() = area?.text ?: ed?.text ?: ""
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
if (text == value) return
|
||||
area?.text = value
|
||||
@@ -110,7 +120,9 @@ class ToolBody private constructor(
|
||||
}
|
||||
|
||||
var font: Font
|
||||
@RequiresEdt
|
||||
get() = area?.font ?: ed?.font ?: SessionEditorStyle.current().editorFont
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
area?.font = value
|
||||
ed?.font = value
|
||||
@@ -118,7 +130,9 @@ class ToolBody private constructor(
|
||||
}
|
||||
|
||||
var foreground: Color
|
||||
@RequiresEdt
|
||||
get() = area?.foreground ?: ed?.foreground ?: UiStyle.Colors.fg()
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
area?.foreground = value
|
||||
ed?.foreground = value
|
||||
@@ -129,11 +143,13 @@ class ToolBody private constructor(
|
||||
val lineWrap: Boolean get() = area?.lineWrap ?: false
|
||||
val editor: EditorTextField? get() = ed
|
||||
|
||||
@RequiresEdt
|
||||
fun caretStart() {
|
||||
area?.caretPosition = 0
|
||||
ed?.getEditor(false)?.caretModel?.moveToOffset(0)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun applyStyle(style: SessionEditorStyle): Boolean {
|
||||
val before = font
|
||||
area?.font = style.transcriptFont
|
||||
@@ -143,6 +159,7 @@ class ToolBody private constructor(
|
||||
return before != font
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun register(selection: SessionSelection, parent: Disposable) {
|
||||
val field = ed
|
||||
if (field != null) {
|
||||
@@ -152,6 +169,7 @@ class ToolBody private constructor(
|
||||
area?.let { selection.register(it, parent) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun lineHeight(): Int = ed?.getEditor(false)?.lineHeight ?: scroll.viewport.view.getFontMetrics(font).height
|
||||
|
||||
override fun dispose() {
|
||||
@@ -162,15 +180,15 @@ class ToolBody private constructor(
|
||||
val view = scroll.viewport.view as? JComponent ?: return
|
||||
val height = height(view)
|
||||
val width = width(view)
|
||||
view.preferredSize = Dimension(width, height)
|
||||
view.minimumSize = Dimension(0, height)
|
||||
view.maximumSize = Dimension(Int.MAX_VALUE, height)
|
||||
view.preferredSize = JBUI.size(width, height)
|
||||
view.minimumSize = JBUI.size(0, height)
|
||||
view.maximumSize = JBDimension(Int.MAX_VALUE, height)
|
||||
val inset = scroll.viewportBorder?.getBorderInsets(scroll) ?: JBUI.emptyInsets()
|
||||
val pane = height + scroll.insets.top + scroll.insets.bottom + inset.top + inset.bottom +
|
||||
scroll.horizontalScrollBar.preferredSize.height
|
||||
scroll.preferredSize = Dimension(0, pane)
|
||||
scroll.minimumSize = Dimension(0, pane)
|
||||
scroll.maximumSize = Dimension(Int.MAX_VALUE, pane)
|
||||
scroll.preferredSize = JBUI.size(0, pane)
|
||||
scroll.minimumSize = JBUI.size(0, pane)
|
||||
scroll.maximumSize = JBDimension(Int.MAX_VALUE, pane)
|
||||
}
|
||||
|
||||
private fun width(view: JComponent): Int {
|
||||
@@ -186,6 +204,7 @@ class ToolBody private constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
@RequiresEdt
|
||||
fun editor(tool: Tool): ToolBody {
|
||||
val disposable = Disposer.newDisposable("Tool body")
|
||||
val body = runCatching {
|
||||
@@ -200,6 +219,7 @@ class ToolBody private constructor(
|
||||
return body
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun text(tool: Tool): ToolBody {
|
||||
val area = area(tool, true)
|
||||
val body = ToolBody(area, null, pane(area, false), null)
|
||||
@@ -275,6 +295,7 @@ private class ToolField(value: String, private var style: SessionEditorStyle) :
|
||||
private const val SUB_CARD = "sub"
|
||||
private const val LINK_CARD = "link"
|
||||
|
||||
@RequiresEdt
|
||||
internal fun toolParts(
|
||||
tool: Tool,
|
||||
openFile: ((String) -> Unit)? = null,
|
||||
@@ -318,6 +339,7 @@ internal fun toolParts(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun searchParts(count: Int): ToolParts {
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
@@ -325,7 +347,7 @@ internal fun searchParts(count: Int): ToolParts {
|
||||
val targets = List(count) {
|
||||
JBLabel().apply {
|
||||
foreground = UiStyle.Colors.fg()
|
||||
minimumSize = Dimension(0, minimumSize.height)
|
||||
minimumSize = JBUI.size(0, minimumSize.height)
|
||||
}
|
||||
}
|
||||
val link = JBLabel().apply { isVisible = false }
|
||||
@@ -339,7 +361,7 @@ internal fun searchParts(count: Int): ToolParts {
|
||||
val target = stack.align(HAlign.TRACK, VAlign.CENTER)
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
minimumSize = Dimension(0, minimumSize.height)
|
||||
minimumSize = JBUI.size(0, minimumSize.height)
|
||||
add(title, BorderLayout.WEST)
|
||||
add(target, BorderLayout.CENTER)
|
||||
}
|
||||
@@ -382,6 +404,7 @@ internal fun subtitle(tool: Tool) = when (tool.name) {
|
||||
else -> toolSubtitle(tool)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setText(label: JBLabel, text: String): Boolean {
|
||||
val value = if (text.isBlank()) "" else XmlStringUtil.wrapInHtml(XmlStringUtil.escapeString(text))
|
||||
if (label.text == value) return false
|
||||
@@ -389,12 +412,14 @@ internal fun setText(label: JBLabel, text: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setTargetText(label: JBLabel, text: String): Boolean {
|
||||
if (label.text == text) return false
|
||||
label.text = text
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setLinkText(parts: ToolParts, text: String): Boolean {
|
||||
val value = if (text.isBlank()) "" else XmlStringUtil.wrapInHtml("<u>${XmlStringUtil.escapeString(text)}</u>")
|
||||
if (parts.label == text && parts.link.text == value) return false
|
||||
@@ -403,6 +428,7 @@ internal fun setLinkText(parts: ToolParts, text: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun show(parts: ToolParts, link: Boolean): Boolean {
|
||||
if (parts.link.isVisible == link && parts.sub.isVisible != link) return false
|
||||
(parts.slot.layout as CardLayout).show(parts.slot, if (link) LINK_CARD else SUB_CARD)
|
||||
@@ -411,24 +437,28 @@ internal fun show(parts: ToolParts, link: Boolean): Boolean {
|
||||
|
||||
internal fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.label else parts.sub.text
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setIcon(label: JBLabel, icon: Icon): Boolean {
|
||||
if (label.icon === icon) return false
|
||||
label.icon = icon
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setVisible(component: JComponent, visible: Boolean): Boolean {
|
||||
if (component.isVisible == visible) return false
|
||||
component.isVisible = visible
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setForeground(component: JComponent, color: Color): Boolean {
|
||||
if (same(component.foreground, color)) return false
|
||||
component.foreground = color
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun setFont(component: JComponent, font: Font): Boolean {
|
||||
if (component.font == font) return false
|
||||
component.font = font
|
||||
|
||||
+25
@@ -9,6 +9,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Dimension
|
||||
import javax.swing.ScrollPaneConstants
|
||||
@@ -33,6 +34,7 @@ class ToolView(
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
@@ -41,6 +43,7 @@ class ToolView(
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
@@ -48,6 +51,7 @@ class ToolView(
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
val was = item.name
|
||||
@@ -59,30 +63,51 @@ class ToolView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
@RequiresEdt
|
||||
fun commandText(): String = command(item)
|
||||
@RequiresEdt
|
||||
fun outputText(): String = output(item)
|
||||
@RequiresEdt
|
||||
fun bodyText(): String = body(item)
|
||||
@RequiresEdt
|
||||
internal fun previewText(): String = parts.content?.text ?: preview(item)
|
||||
@RequiresEdt
|
||||
fun hasToggle(): Boolean = arrow.isVisible
|
||||
@RequiresEdt
|
||||
internal fun bodyFont() = parts.content?.font ?: style.editorFont
|
||||
@RequiresEdt
|
||||
internal fun titleFont() = parts.title.font
|
||||
@RequiresEdt
|
||||
internal fun subtitleFont() = parts.sub.font
|
||||
@RequiresEdt
|
||||
internal fun stateFont() = parts.state.font
|
||||
@RequiresEdt
|
||||
internal fun bodyEditable() = parts.content?.editable ?: false
|
||||
@RequiresEdt
|
||||
internal fun bodyCaretVisible() = parts.content?.caretVisible ?: false
|
||||
@RequiresEdt
|
||||
internal fun bodyVisible() = parts.scroll?.parent === this
|
||||
@RequiresEdt
|
||||
internal fun controlCount() = if (arrow.isVisible) 1 else 0
|
||||
@RequiresEdt
|
||||
internal fun horizontalPolicy() = parts.scroll?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun verticalPolicy() = parts.scroll?.verticalScrollBarPolicy ?: ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun bodyWrap() = parts.content?.lineWrap ?: false
|
||||
@RequiresEdt
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
@RequiresEdt
|
||||
internal fun bodyCreated() = parts.bodyCreated()
|
||||
@RequiresEdt
|
||||
internal fun bodyEditor() = parts.content?.editor
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Reasoning
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.components.JBHtmlPane
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Container
|
||||
import javax.swing.JPanel
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class ReasoningViewStressTest : BasePlatformTestCase() {
|
||||
|
||||
fun `test streaming reasoning retains markdown body and disposes editors`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
val view = ReasoningView(reasoning("r1", done = false, text = "intro\n\n```kotlin\n"))
|
||||
val component = view.md.component
|
||||
val scroll = scrolls(view).first()
|
||||
val editor = editors(view).single()
|
||||
val count = panel(view).componentCount
|
||||
editor.getEditor(true)
|
||||
|
||||
repeat(150) { i -> view.appendDelta("val x$i = $i\n") }
|
||||
|
||||
assertSame(component, view.md.component)
|
||||
assertSame(scroll, scrolls(view).first())
|
||||
assertSame(editor, editors(view).single())
|
||||
assertEquals(1, editors(view).size)
|
||||
assertTrue(htmls(view).size <= 1)
|
||||
assertEquals(count, panel(view).componentCount)
|
||||
|
||||
view.update(reasoning("r1", done = true, text = view.markdown() + "```"))
|
||||
assertFalse(view.bodyVisible())
|
||||
Disposer.dispose(view)
|
||||
drainEdt()
|
||||
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
private fun reasoning(id: String, done: Boolean, text: String) = Reasoning(id).also {
|
||||
it.done = done
|
||||
it.content.append(text)
|
||||
}
|
||||
|
||||
private fun panel(view: ReasoningView): JPanel = view.md.component as JPanel
|
||||
|
||||
private fun scrolls(view: ReasoningView) = descendants(view).filterIsInstance<JBScrollPane>()
|
||||
|
||||
private fun htmls(view: ReasoningView) = descendants(view).filterIsInstance<JBHtmlPane>()
|
||||
|
||||
private fun editors(view: ReasoningView) = descendants(view).filterIsInstance<EditorTextField>()
|
||||
|
||||
private fun descendants(root: Container): List<java.awt.Component> = root.components.flatMap { child ->
|
||||
listOf(child) + ((child as? Container)?.let(::descendants) ?: emptyList())
|
||||
}
|
||||
|
||||
private fun drainEdt() {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
}
|
||||
+51
@@ -5,8 +5,11 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@@ -54,6 +57,29 @@ class ReasoningViewTest : BasePlatformTestCase() {
|
||||
assertEquals("one\ntwo\nthree\nfour", view.markdown())
|
||||
}
|
||||
|
||||
fun `test live reasoning collapses when marked done`() {
|
||||
val view = ReasoningView(reasoning("p1", done = false, text = "one\ntwo\nthree\nfour"))
|
||||
|
||||
assertTrue(view.isExpanded())
|
||||
|
||||
view.update(reasoning("p1", done = true, text = "one\ntwo\nthree\nfour"))
|
||||
|
||||
assertFalse(view.isExpanded())
|
||||
assertFalse(view.bodyVisible())
|
||||
assertTrue(view.bodyCreated())
|
||||
}
|
||||
|
||||
fun `test manually expanded finished reasoning stays open on update`() {
|
||||
val view = ReasoningView(reasoning("p1", done = true, text = "one\ntwo"))
|
||||
|
||||
view.toggle()
|
||||
view.update(reasoning("p1", done = true, text = "one\ntwo\nthree"))
|
||||
|
||||
assertTrue(view.isExpanded())
|
||||
assertTrue(view.bodyVisible())
|
||||
assertEquals("one\ntwo\nthree", view.markdown())
|
||||
}
|
||||
|
||||
fun `test toggle opens and closes reasoning`() {
|
||||
val view = ReasoningView(reasoning("p1", done = true, text = "one\ntwo\nthree\nfour"))
|
||||
|
||||
@@ -184,6 +210,20 @@ class ReasoningViewTest : BasePlatformTestCase() {
|
||||
assertEquals(view.bodyScrollBottom(), view.bodyScrollValue())
|
||||
}
|
||||
|
||||
fun `test appended reasoning does not yank user scrolled above tail`() {
|
||||
val view = ReasoningView(reasoning("p1", done = false, text = (1..40).joinToString("\n") { "line $it" }))
|
||||
view.setSize(300, 80)
|
||||
view.doLayout()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
val scroll = scroll(view)
|
||||
scroll.verticalScrollBar.value = 0
|
||||
|
||||
view.appendDelta("\nline 41\nline 42")
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
assertEquals(0, scroll.verticalScrollBar.value)
|
||||
}
|
||||
|
||||
fun `test reasoning block uses vertical separator`() {
|
||||
val view = ReasoningView(reasoning("p1", done = true, text = "one"))
|
||||
|
||||
@@ -234,4 +274,15 @@ class ReasoningViewTest : BasePlatformTestCase() {
|
||||
it.done = done
|
||||
it.content.append(text)
|
||||
}
|
||||
|
||||
private fun scroll(component: Component): JBScrollPane {
|
||||
if (component is JBScrollPane) return component
|
||||
if (component is Container) {
|
||||
component.components.forEach { child ->
|
||||
val scroll = runCatching { scroll(child) }.getOrNull()
|
||||
if (scroll != null) return scroll
|
||||
}
|
||||
}
|
||||
error("scroll not found")
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -3,6 +3,8 @@ package ai.kilocode.client.session.views
|
||||
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.views.tool.GlobToolView
|
||||
import ai.kilocode.client.session.views.tool.SearchToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
@@ -26,12 +28,43 @@ class ToolBodyStressTest : BasePlatformTestCase() {
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
fun `test expanded search tool editors are disposed after churn`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
|
||||
repeat(60) { i ->
|
||||
val search = SearchToolView(search(i))
|
||||
search.toggle()
|
||||
search.bodyEditor()?.getEditor(true)
|
||||
Disposer.dispose(search)
|
||||
|
||||
val glob = GlobToolView(glob(i))
|
||||
glob.toggle()
|
||||
glob.bodyEditor()?.getEditor(true)
|
||||
Disposer.dispose(glob)
|
||||
}
|
||||
drainEdt()
|
||||
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
private fun tool(index: Int) = Tool("p$index", "bash", toolKind("bash")).also {
|
||||
it.state = ToolExecState.COMPLETED
|
||||
it.input = mapOf("command" to "log $index")
|
||||
it.output = (1..20).joinToString("\n") { line -> "line $index/$line" }
|
||||
}
|
||||
|
||||
private fun search(index: Int) = Tool("s$index", "grep", toolKind("grep")).also {
|
||||
it.state = ToolExecState.COMPLETED
|
||||
it.input = mapOf("path" to "src", "pattern" to "needle$index", "include" to "*.kt")
|
||||
it.output = (1..20).joinToString("\n") { line -> "src/File$line.kt: needle$index" }
|
||||
}
|
||||
|
||||
private fun glob(index: Int) = Tool("g$index", "glob", toolKind("glob")).also {
|
||||
it.state = ToolExecState.COMPLETED
|
||||
it.input = mapOf("path" to "src", "pattern" to "**/*$index.kt")
|
||||
it.output = (1..20).joinToString("\n") { line -> "src/File$line.kt" }
|
||||
}
|
||||
|
||||
private fun drainEdt() {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
|
||||
+33
@@ -211,6 +211,29 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
assertEquals("first second third", (mv.part("r1") as ReasoningView).markdown())
|
||||
}
|
||||
|
||||
fun `test reasoning alias maps stay bounded across churn`() {
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
|
||||
repeat(100) { i ->
|
||||
mv.upsertPart(reasoning("r${i}a", "first $i "))
|
||||
mv.upsertPart(reasoning("r${i}b", "second $i"))
|
||||
|
||||
assertEquals(listOf("r${i}a"), mv.partIds())
|
||||
assertSame(mv.part("r${i}a"), mv.part("r${i}b"))
|
||||
assertEquals(1, aliasSize(mv))
|
||||
assertEquals(1, sourceSize(mv))
|
||||
assertEquals(1, mv.componentCount)
|
||||
|
||||
mv.removePart("r${i}b")
|
||||
mv.removePart("r${i}a")
|
||||
|
||||
assertTrue(mv.partIds().isEmpty())
|
||||
assertEquals(0, aliasSize(mv))
|
||||
assertEquals(0, sourceSize(mv))
|
||||
assertEquals(0, mv.componentCount)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test text between reasoning parts keeps separate views`() {
|
||||
val message = msg("a1", "assistant")
|
||||
message.parts["r1"] = reasoning("r1", "first")
|
||||
@@ -316,6 +339,16 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
|
||||
private fun text(id: String, content: String) = Text(id).also { it.content.append(content) }
|
||||
|
||||
private fun aliasSize(view: MessageView) = mapSize(view, "aliases")
|
||||
|
||||
private fun sourceSize(view: MessageView) = mapSize(view, "sources")
|
||||
|
||||
private fun mapSize(view: MessageView, name: String): Int {
|
||||
val field = MessageView::class.java.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
return (field.get(view) as Map<*, *>).size
|
||||
}
|
||||
|
||||
private class TrackingRepaintManager(private val watched: Set<JComponent>) : RepaintManager() {
|
||||
val dirty = mutableListOf<JComponent>()
|
||||
val invalid = mutableListOf<JComponent>()
|
||||
|
||||
Reference in New Issue
Block a user