Merge remote-tracking branch 'origin/main' into marius-kilocode/kilo-opencode-v1.14.51
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Support reading .ods (OpenDocument Spreadsheet) files in the read tool
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix agent-manager model sync on config change
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Show a pointer cursor for clickable controls and links in Kilo webviews.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-gateway": minor
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Show Terminal Bench completion scores and per-attempt costs in supported model details.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Preserve image attachments when Photon is unavailable, enforce attachment limits for user images, and correlate shell lifecycle events correctly.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep task timelines in place while reviewing earlier activity across Kilo chat surfaces, and resume following updates from the latest bar.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Restore automatic session titles for models that require reasoning without assuming a supported effort level.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Restore cloud session filesystem changes from synced session diffs when importing sessions, including inherited changes across imported session forks.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix JetBrains session scrolling so mouse wheel and keyboard scrolling no longer snap back or bounce near the transcript bottom.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Discover project-installed skills in Agent Manager worktree sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Prevent the JetBrains session scrollbar from covering transcript content.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render glob search results in the JetBrains chat as collapsible tool output with separate directory and pattern rows.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep subagent sessions isolated when forking sessions through editor clients.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Use matching VS Code-style icons for JetBrains session views.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Avoid copying visible planning chat into new sessions started from the plan follow-up prompt.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager streaming responsive when multiple sessions run concurrently.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Hide thematic separator lines in JetBrains chat markdown while preserving surrounding prose and code blocks.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refine JetBrains session card borders so prompt and question surfaces use brighter outlines while reasoning and tool cards use softer default borders.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Display JetBrains search tool paths relative to the current repository when possible.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Rename Mercury autocomplete model labels to distinguish FIM and Next Edit modes.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Start forked sessions at zero cost instead of carrying over the source session's spend.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Reset stale hover styling when moving between JetBrains session cards and draw card outlines only while expanded.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Re-enable free-model session and Git workspace data export.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Route Agent Manager permission approvals to the worktree that created the request.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render grep searches in the JetBrains chat with a dedicated search header that shows stacked, clipped targets.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent memory leak in KiloSessionPromptQueue.cancel for sessions without active tails
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve mouse wheel scrolling speed in the JetBrains session view.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Sync CLI sessions to Kilo session history when authenticated with `KILO_API_KEY` when no stored Kilo auth is present.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Compact sessions at the configured context percentage before sending an oversized provider request.
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
close:
|
||||
if: github.repository == 'anomalyco/opencode' # kilocode_change - Kilo uses kilo-auto-close.yml
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
close-stale-prs:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
if: github.repository == 'anomalyco/opencode' # kilocode_change - Kilo uses kilo-auto-close.yml
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
# kilocode_change - new file
|
||||
name: kilo-auto-close
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dryRun:
|
||||
description: "Log actions without closing items"
|
||||
type: boolean
|
||||
default: true
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
close:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Close inactive PRs and issues
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
KILO_AUTO_CLOSE_ENABLED: ${{ vars.KILO_AUTO_CLOSE_ENABLED }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const PR_DAYS_INACTIVE = 30
|
||||
const ISSUE_DAYS_INACTIVE = 60
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
// Adaptive delay: fast for small batches, slower for large to respect
|
||||
// GitHub's 80 content-generating requests/minute limit
|
||||
const SMALL_BATCH_THRESHOLD = 10
|
||||
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 items)
|
||||
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 items) = ~30 ops/min, well under 80 limit
|
||||
|
||||
const startTime = Date.now()
|
||||
const prCutoff = new Date(Date.now() - PR_DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||
const issueCutoff = new Date(Date.now() - ISSUE_DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||
const { owner, repo } = context.repo
|
||||
const dryRunInput = context.payload.inputs?.dryRun
|
||||
const enabled = process.env.KILO_AUTO_CLOSE_ENABLED === "true"
|
||||
const dryRun = dryRunInput === undefined
|
||||
? !enabled
|
||||
: dryRunInput !== "false"
|
||||
|
||||
core.info(`Dry run mode: ${dryRun}`)
|
||||
core.info(`PR cutoff date: ${prCutoff.toISOString()}`)
|
||||
core.info(`Issue cutoff date: ${issueCutoff.toISOString()}`)
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function withRetry(fn, description = 'API call') {
|
||||
let lastError
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const result = await fn()
|
||||
return result
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
const isRateLimited = error.status === 403 &&
|
||||
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
|
||||
|
||||
if (!isRateLimited) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (attempt === MAX_RETRIES - 1) {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse retry-after header, default to 60 seconds
|
||||
const retryAfter = error.response?.headers?.['retry-after']
|
||||
? parseInt(error.response.headers['retry-after'])
|
||||
: 60
|
||||
|
||||
// Exponential backoff: retryAfter * 2^attempt
|
||||
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
|
||||
|
||||
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
|
||||
|
||||
await sleep(backoffMs)
|
||||
}
|
||||
}
|
||||
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
|
||||
throw lastError
|
||||
}
|
||||
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $cursor: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(first: 100, states: OPEN, after: $cursor) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
number
|
||||
title
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
committedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
comments(last: 1) {
|
||||
nodes {
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
reviews(last: 1) {
|
||||
nodes {
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const allPrs = []
|
||||
let cursor = null
|
||||
let hasNextPage = true
|
||||
let pageCount = 0
|
||||
|
||||
while (hasNextPage) {
|
||||
pageCount++
|
||||
core.info(`Fetching page ${pageCount} of open PRs...`)
|
||||
|
||||
const result = await withRetry(
|
||||
() => github.graphql(query, { owner, repo, cursor }),
|
||||
`GraphQL page ${pageCount}`
|
||||
)
|
||||
|
||||
allPrs.push(...result.repository.pullRequests.nodes)
|
||||
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
||||
cursor = result.repository.pullRequests.pageInfo.endCursor
|
||||
|
||||
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
|
||||
|
||||
// Delay between pagination requests (use small batch delay for reads)
|
||||
if (hasNextPage) {
|
||||
await sleep(SMALL_BATCH_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Found ${allPrs.length} open pull requests`)
|
||||
|
||||
const stalePrs = allPrs.filter((pr) => {
|
||||
const dates = [
|
||||
new Date(pr.createdAt),
|
||||
pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null,
|
||||
pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null,
|
||||
pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null,
|
||||
].filter((d) => d !== null)
|
||||
|
||||
const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||
|
||||
if (!lastActivity || lastActivity > prCutoff) {
|
||||
core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`)
|
||||
return false
|
||||
}
|
||||
|
||||
core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`)
|
||||
return true
|
||||
})
|
||||
|
||||
core.info(`Found ${stalePrs.length} stale pull requests`)
|
||||
|
||||
const prDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
|
||||
? LARGE_BATCH_DELAY_MS
|
||||
: SMALL_BATCH_DELAY_MS
|
||||
|
||||
core.info(`Using ${prDelayMs}ms delay between PR operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
|
||||
|
||||
let closedPrCount = 0
|
||||
let skippedPrCount = 0
|
||||
|
||||
for (const pr of stalePrs) {
|
||||
const issue_number = pr.number
|
||||
const closeComment = `To stay organized pull requests are automatically closed after ${PR_DAYS_INACTIVE} days of inactivity. If the pull request is still relevant please open a new one.`
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Add comment
|
||||
await withRetry(
|
||||
() => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: closeComment,
|
||||
}),
|
||||
`Comment on PR #${issue_number}`
|
||||
)
|
||||
|
||||
// Close PR
|
||||
await withRetry(
|
||||
() => github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
state: "closed",
|
||||
}),
|
||||
`Close PR #${issue_number}`
|
||||
)
|
||||
|
||||
closedPrCount++
|
||||
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||
|
||||
// Delay before processing next PR
|
||||
await sleep(prDelayMs)
|
||||
} catch (error) {
|
||||
skippedPrCount++
|
||||
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
let page = 1
|
||||
let stop = false
|
||||
const staleIssues = []
|
||||
|
||||
while (!stop) {
|
||||
core.info(`Fetching page ${page} of open issues...`)
|
||||
|
||||
const result = await withRetry(
|
||||
() => github.rest.issues.listForRepo({
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
sort: "updated",
|
||||
direction: "asc",
|
||||
per_page: 100,
|
||||
page,
|
||||
}),
|
||||
`Issues page ${page}`
|
||||
)
|
||||
|
||||
if (!result.data.length) {
|
||||
break
|
||||
}
|
||||
|
||||
core.info(`Page ${page}: fetched ${result.data.length} issues`)
|
||||
|
||||
for (const issue of result.data) {
|
||||
const updated = new Date(issue.updated_at)
|
||||
|
||||
if (updated >= issueCutoff) {
|
||||
core.info(`Found fresh issue/PR #${issue.number} (${updated.toISOString()}), stopping issue scan`)
|
||||
stop = true
|
||||
break
|
||||
}
|
||||
|
||||
if (issue.pull_request) {
|
||||
core.info(`Skipping PR #${issue.number} in issue scan`)
|
||||
continue
|
||||
}
|
||||
|
||||
staleIssues.push(issue)
|
||||
}
|
||||
|
||||
if (!stop) {
|
||||
page++
|
||||
await sleep(SMALL_BATCH_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Found ${staleIssues.length} stale issues`)
|
||||
|
||||
const issueDelayMs = staleIssues.length > SMALL_BATCH_THRESHOLD
|
||||
? LARGE_BATCH_DELAY_MS
|
||||
: SMALL_BATCH_DELAY_MS
|
||||
|
||||
core.info(`Using ${issueDelayMs}ms delay between issue operations (${staleIssues.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
|
||||
|
||||
let closedIssueCount = 0
|
||||
let skippedIssueCount = 0
|
||||
|
||||
for (const issue of staleIssues) {
|
||||
const closeComment = `To stay organized issues are automatically closed after ${ISSUE_DAYS_INACTIVE} days of no activity. If the issue is still relevant please open a new one.`
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[dry-run] Would close issue #${issue.number}: ${issue.title}`)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await withRetry(
|
||||
() => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: closeComment,
|
||||
}),
|
||||
`Comment on issue #${issue.number}`
|
||||
)
|
||||
|
||||
await withRetry(
|
||||
() => github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
}),
|
||||
`Close issue #${issue.number}`
|
||||
)
|
||||
|
||||
closedIssueCount++
|
||||
core.info(`Closed issue #${issue.number}: ${issue.title}`)
|
||||
|
||||
await sleep(issueDelayMs)
|
||||
} catch (error) {
|
||||
skippedIssueCount++
|
||||
core.error(`Failed to close issue #${issue.number}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
||||
core.info(`\n========== Summary ==========`)
|
||||
core.info(`Total open PRs found: ${allPrs.length}`)
|
||||
core.info(`Stale PRs identified: ${stalePrs.length}`)
|
||||
core.info(`PRs closed: ${closedPrCount}`)
|
||||
core.info(`PRs skipped (errors): ${skippedPrCount}`)
|
||||
core.info(`Stale issues identified: ${staleIssues.length}`)
|
||||
core.info(`Issues closed: ${closedIssueCount}`)
|
||||
core.info(`Issues skipped (errors): ${skippedIssueCount}`)
|
||||
core.info(`Elapsed time: ${elapsed}s`)
|
||||
core.info(`=============================`)
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"packages/kilo-console": {
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -161,7 +161,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -198,7 +198,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -208,7 +208,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -240,11 +240,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -258,7 +258,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -295,7 +295,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -361,7 +361,7 @@
|
||||
},
|
||||
"packages/kilo-web-ui": {
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -376,7 +376,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -394,7 +394,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -562,7 +562,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -590,7 +590,7 @@
|
||||
},
|
||||
"packages/plugin-atomic-chat": {
|
||||
"name": "@kilocode/plugin-atomic-chat",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
},
|
||||
@@ -604,7 +604,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -615,7 +615,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -630,7 +630,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -653,7 +653,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
|
||||
@@ -153,6 +153,6 @@
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.3.40"
|
||||
version = "7.3.41"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.40/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.41/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.40/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.41/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.40/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.41/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.40/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.41/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.40/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.41/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -10,6 +10,7 @@ exclude_path = ["node_modules"]
|
||||
|
||||
accept = [
|
||||
"200",
|
||||
"202",
|
||||
"403",
|
||||
"429",
|
||||
"301",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -8,11 +8,11 @@ sidebar_label: DeepSeek
|
||||
|
||||
Kilo Code supports accessing models through the DeepSeek API, including `deepseek-chat` and `deepseek-reasoner`.
|
||||
|
||||
**Website:** [DeepSeek API documentation](https://api-docs.deepseek.com/)
|
||||
**Website:** [https://platform.deepseek.com/](https://platform.deepseek.com/)
|
||||
|
||||
## Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [DeepSeek API keys page](https://platform.deepseek.com/api_keys). Create an account or sign in.
|
||||
1. **Sign Up/Sign In:** Go to the [DeepSeek Platform](https://platform.deepseek.com/). Create an account or sign in.
|
||||
2. **Navigate to API Keys:** Find your API keys in the [API keys](https://platform.deepseek.com/api_keys) section of the platform.
|
||||
3. **Create a Key:** Click "Create new API key". Give your key a descriptive name (e.g., "Kilo Code").
|
||||
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.40",
|
||||
"version": "7.3.41",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -268,6 +268,22 @@ Tests for retained Swing components should assert:
|
||||
- `update(model)` changes existing labels/body text without duplicating components.
|
||||
- Updates while collapsed do not eagerly create lazy bodies.
|
||||
- No-op updates, empty deltas, repeated hover values, and toggling non-expandable cards do not repaint/revalidate the whole view.
|
||||
- Streaming/rebuilding surfaces additionally require stress + leak tests (see below).
|
||||
|
||||
### Stress and Leak Tests for Streaming UI
|
||||
|
||||
Session/transcript UI that streams updates or rebuilds its component tree (markdown
|
||||
views, code blocks, transcript parts, collapsible cards) must ship stress + leak tests in
|
||||
addition to behavior tests. These tests must:
|
||||
|
||||
- Drive many updates (hundreds of streamed deltas or `set` cycles) through the public API.
|
||||
- Assert that retained component instances stay identical across updates (`assertSame`).
|
||||
- Assert the component count stays bounded — no growth per update.
|
||||
- Assert disposable-backed resources return to baseline after churn + clear/dispose.
|
||||
For code editors, compare `EditorFactory.getInstance().allEditors.size` against a
|
||||
baseline captured before the loop.
|
||||
|
||||
See `MdViewHybridStressTest` for the reference pattern.
|
||||
|
||||
### Platform Components and Utilities
|
||||
|
||||
|
||||
@@ -6,6 +6,26 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [7.0.1-rc.8] - 2026-06-09
|
||||
|
||||
### Added
|
||||
|
||||
- Display search results and tool output in clearer, more readable JetBrains session cards.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve session transcript scrolling so streaming updates, expanded cards, reasoning blocks, and mouse wheel scrolling preserve the user's position more reliably.
|
||||
- Make session transcripts easier to scan with tighter spacing, aligned icons, cleaner card outlines, relative search paths, and less visual noise.
|
||||
- Keep completed reasoning blocks expanded after a response finishes.
|
||||
- Improve session stability during long-running or cancelled prompts.
|
||||
- Restore automatic session titles, project skill discovery, and subagent isolation in forked sessions.
|
||||
- Restore imported cloud session diffs.
|
||||
- Compact sessions before the configured context limit is exceeded.
|
||||
|
||||
### Changed
|
||||
|
||||
- Update the bundled Kilo CLI runtime with the latest fixes used by the JetBrains plugin.
|
||||
|
||||
## [7.0.1-rc.7] - 2026-06-04
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -287,7 +287,18 @@ class SessionUi(
|
||||
dismiss = { controller.dismissLoginRequired() },
|
||||
selection = selection,
|
||||
)
|
||||
messageBody = SessionMessageListPanel(controller.model, this, question, permission, login, ::openFile, ::openUrl, selection)
|
||||
messageBody = SessionMessageListPanel(
|
||||
controller.model,
|
||||
this,
|
||||
question,
|
||||
permission,
|
||||
login,
|
||||
::openFile,
|
||||
::openUrl,
|
||||
selection,
|
||||
repo = workspace.directory,
|
||||
resize = { anchor, fn -> scroll.preserve(anchor, fn) },
|
||||
)
|
||||
header = SessionHeaderPanel(controller, this)
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
|
||||
@@ -51,10 +51,10 @@ internal class SessionScroll(
|
||||
private var opening = false
|
||||
private var stable = -1
|
||||
private var seq = 0
|
||||
private var pause = false
|
||||
private var user = false
|
||||
private var value = 0
|
||||
private var question = false
|
||||
private var restoring = false
|
||||
|
||||
init {
|
||||
jump = JBLabel(ScrollButtonIcon.create()).apply {
|
||||
@@ -73,7 +73,6 @@ internal class SessionScroll(
|
||||
user = true
|
||||
}
|
||||
})
|
||||
component.viewport.addChangeListener { onViewport() }
|
||||
component.verticalScrollBar.addAdjustmentListener { onScroll() }
|
||||
root.addOverlay(jump) { _, child ->
|
||||
val size = child.preferredSize
|
||||
@@ -98,11 +97,10 @@ internal class SessionScroll(
|
||||
|
||||
@RequiresEdt
|
||||
fun atBottom(): Boolean {
|
||||
val bar = component.verticalScrollBar
|
||||
return when {
|
||||
component.viewport.view !== messages -> tail
|
||||
bar.maximum <= bar.visibleAmount -> true
|
||||
else -> bar.value + bar.visibleAmount >= bar.maximum - JBUI.scale(THRESHOLD)
|
||||
!tail -> false
|
||||
else -> near()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +112,7 @@ internal class SessionScroll(
|
||||
return
|
||||
}
|
||||
user = false
|
||||
pause = false
|
||||
tail = true
|
||||
stable = -1
|
||||
auto = true
|
||||
@@ -139,11 +138,44 @@ internal class SessionScroll(
|
||||
return component.viewport.view === messages && tail
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun preserve(anchor: JComponent, action: () -> Unit) {
|
||||
if (component.viewport.view !== messages) {
|
||||
action()
|
||||
return
|
||||
}
|
||||
val pos = SwingUtilities.convertPoint(anchor, Point(0, 0), messages)
|
||||
val delta = pos.y - component.viewport.viewPosition.y
|
||||
seq++
|
||||
stable = -1
|
||||
user = false
|
||||
pause = false
|
||||
auto = true
|
||||
try {
|
||||
action()
|
||||
layoutScroll()
|
||||
val next = SwingUtilities.convertPoint(anchor, Point(0, 0), messages)
|
||||
val y = (next.y - delta).coerceIn(0, bottom())
|
||||
component.viewport.viewPosition = Point(0, y)
|
||||
bar.value = y
|
||||
} finally {
|
||||
auto = false
|
||||
}
|
||||
tail = atBottom()
|
||||
syncValue()
|
||||
updateJump()
|
||||
if (tail) {
|
||||
stable = -1
|
||||
seq++
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun openBottom(done: () -> Unit) {
|
||||
opening = true
|
||||
stable = -1
|
||||
user = false
|
||||
pause = false
|
||||
tail = true
|
||||
auto = true
|
||||
show(messages)
|
||||
@@ -169,8 +201,8 @@ internal class SessionScroll(
|
||||
@RequiresEdt
|
||||
fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
component.background = SessionUiStyle.View.transcript()
|
||||
component.viewport.background = SessionUiStyle.View.transcript()
|
||||
component.background = SessionUiStyle.Transcript.bgColor()
|
||||
component.viewport.background = SessionUiStyle.Transcript.bgColor()
|
||||
syncIcon()
|
||||
messages.applyStyle(style)
|
||||
val view = component.viewport.view
|
||||
@@ -189,6 +221,7 @@ internal class SessionScroll(
|
||||
opening = false
|
||||
stable = -1
|
||||
user = false
|
||||
pause = false
|
||||
tail = true
|
||||
auto = true
|
||||
show(messages)
|
||||
@@ -271,49 +304,62 @@ internal class SessionScroll(
|
||||
bar.value = bottom()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun onViewport() {
|
||||
if (restoring || auto || opening || user || tail || component.viewport.view !== messages) return
|
||||
val y = value.coerceIn(0, bottom())
|
||||
if (component.viewport.viewPosition.y == y && bar.value == y) return
|
||||
restoring = true
|
||||
try {
|
||||
component.viewport.viewPosition = Point(0, y)
|
||||
bar.value = y
|
||||
} finally {
|
||||
restoring = false
|
||||
}
|
||||
updateJump()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun bottom(): Int {
|
||||
val bar = component.verticalScrollBar
|
||||
return (bar.maximum - bar.visibleAmount).coerceAtLeast(bar.minimum)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun near(): Boolean {
|
||||
val bar = component.verticalScrollBar
|
||||
return bar.maximum <= bar.visibleAmount || bar.value + bar.visibleAmount >= bar.maximum - JBUI.scale(THRESHOLD)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun onScroll() {
|
||||
val prev = value
|
||||
val moved = bar.value != value
|
||||
val down = bar.value > value
|
||||
syncValue()
|
||||
if (auto || opening) {
|
||||
updateJump()
|
||||
return
|
||||
}
|
||||
if (component.viewport.view === messages) {
|
||||
val bottom = atBottom()
|
||||
val bottom = near()
|
||||
if (bottom) {
|
||||
tail = true
|
||||
if (user && moved && !down) {
|
||||
tail = false
|
||||
pause = true
|
||||
} else if (!tail && !user) {
|
||||
if (moved) {
|
||||
auto = true
|
||||
try {
|
||||
bar.value = prev.coerceIn(bar.minimum, bottom())
|
||||
} finally {
|
||||
auto = false
|
||||
}
|
||||
syncValue()
|
||||
}
|
||||
tail = false
|
||||
} else if (pause && !user) {
|
||||
tail = false
|
||||
} else {
|
||||
tail = true
|
||||
pause = false
|
||||
}
|
||||
user = false
|
||||
updateJump()
|
||||
return
|
||||
}
|
||||
if (tail && (!user || !moved)) {
|
||||
if (tail && !user && !moved) {
|
||||
user = false
|
||||
followBottom(true)
|
||||
return
|
||||
}
|
||||
tail = false
|
||||
pause = false
|
||||
user = false
|
||||
seq++
|
||||
}
|
||||
|
||||
@@ -94,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(SessionUiStyle.View.line(), 1, 0, 0, 0)
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width(), 0, 0, 0)
|
||||
left.add(toggle, BorderLayout.WEST)
|
||||
left.add(label, BorderLayout.CENTER)
|
||||
header.add(left, BorderLayout.CENTER)
|
||||
|
||||
@@ -5,12 +5,14 @@ import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.StackAxis
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
|
||||
/**
|
||||
* Progress footer rendered at the bottom of the session transcript while the
|
||||
@@ -35,6 +37,12 @@ class ProgressPanel(
|
||||
init {
|
||||
isOpaque = false
|
||||
isVisible = false
|
||||
border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.sm(),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
|
||||
next(JBLabel(AnimatedIcon.Default()))
|
||||
|
||||
@@ -13,9 +13,11 @@ import ai.kilocode.client.session.views.MessageView
|
||||
import ai.kilocode.client.session.views.permission.PermissionView
|
||||
import ai.kilocode.client.session.views.question.QuestionView
|
||||
import ai.kilocode.client.session.views.TurnView
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.ui.JBUI
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Scrollable transcript panel that maps the model's turn grouping to
|
||||
@@ -51,13 +53,15 @@ class SessionMessageListPanel(
|
||||
private val openFile: (String) -> Unit,
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
private val selection: SessionSelection? = null,
|
||||
private val repo: String? = null,
|
||||
private val resize: ((JComponent, () -> Unit) -> Unit)? = null,
|
||||
) : SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
JBUI.insets(
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
|
||||
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING + SessionUiStyle.SessionLayout.TRANSCRIPT_SCROLLBAR_PADDING,
|
||||
),
|
||||
), Disposable, SessionEditorStyleTarget {
|
||||
|
||||
@@ -66,13 +70,14 @@ class SessionMessageListPanel(
|
||||
private val msgToView = HashMap<String, MessageView>()
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var hiddenTool: ToolCallRef? = null
|
||||
private var hovered: PartView? = null
|
||||
|
||||
/** Progress footer — always the last child inside the scroll. */
|
||||
val progress = ProgressPanel(model, parent)
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.transcript()
|
||||
background = SessionUiStyle.Transcript.bgColor()
|
||||
Disposer.register(parent, this)
|
||||
|
||||
model.addListener(parent) { event ->
|
||||
@@ -177,7 +182,7 @@ class SessionMessageListPanel(
|
||||
// ------ private event handlers ------
|
||||
|
||||
private fun onTurnAdded(turn: ai.kilocode.client.session.model.Turn) {
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl, selection)
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl, selection, resize, repo, ::hover)
|
||||
turnViews[turn.id] = tv
|
||||
for (msgId in turn.messageIds) {
|
||||
val msg = model.message(msgId) ?: continue
|
||||
@@ -223,6 +228,7 @@ class SessionMessageListPanel(
|
||||
}
|
||||
|
||||
private fun rebuild() {
|
||||
clearHover()
|
||||
turnViews.values.forEach {
|
||||
remove(it)
|
||||
Disposer.dispose(it)
|
||||
@@ -233,7 +239,7 @@ class SessionMessageListPanel(
|
||||
removeAll()
|
||||
|
||||
for (turn in model.turns()) {
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl, selection)
|
||||
val tv = TurnView(turn.id, openFile, style, openUrl, selection, resize, repo, ::hover)
|
||||
turnViews[turn.id] = tv
|
||||
for (msgId in turn.messageIds) {
|
||||
val msg = model.message(msgId) ?: continue
|
||||
@@ -249,6 +255,7 @@ class SessionMessageListPanel(
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
clearHover()
|
||||
turnViews.values.forEach {
|
||||
remove(it)
|
||||
Disposer.dispose(it)
|
||||
@@ -338,9 +345,26 @@ class SessionMessageListPanel(
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun hover(view: PartView, value: Boolean) {
|
||||
if (value) {
|
||||
val prev = hovered
|
||||
if (prev === view) return
|
||||
hovered = view
|
||||
prev?.setHovered(false)
|
||||
return
|
||||
}
|
||||
if (hovered === view) hovered = null
|
||||
}
|
||||
|
||||
private fun clearHover() {
|
||||
val view = hovered ?: return
|
||||
hovered = null
|
||||
view.setHovered(false)
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
background = SessionUiStyle.View.transcript()
|
||||
background = SessionUiStyle.Transcript.bgColor()
|
||||
for (view in turnViews.values) view.applyStyle(style)
|
||||
question?.applyStyle(style)
|
||||
permission?.applyStyle(style)
|
||||
@@ -350,6 +374,7 @@ class SessionMessageListPanel(
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
clearHover()
|
||||
turnViews.values.forEach {
|
||||
remove(it)
|
||||
Disposer.dispose(it)
|
||||
|
||||
@@ -194,7 +194,7 @@ internal class SessionAccountOverlay(
|
||||
|
||||
@RequiresEdt
|
||||
private fun showPopup() {
|
||||
val bg = SessionUiStyle.View.sessionViewBackground()
|
||||
val bg = SessionUiStyle.AccountPopup.bgColor()
|
||||
val model = CollectionListModel(choices)
|
||||
val list = JBList(model).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
@@ -285,7 +285,7 @@ internal class SessionAccountOverlay(
|
||||
internal fun choiceCount() = choices.size
|
||||
internal fun selectedIndex() = choices.indexOfFirst { it.org == currentOrgId }.takeIf { it >= 0 } ?: 0
|
||||
internal fun panelBackground() = panel.background
|
||||
internal fun panelBorderColor() = SessionUiStyle.View.sessionViewOutline()
|
||||
internal fun panelBorderColor() = SessionUiStyle.AccountPopup.outlineColor()
|
||||
internal fun balanceVisible() = balance.isVisible
|
||||
internal fun balanceIcon() = balance.icon
|
||||
internal fun balanceText() = balanceText
|
||||
|
||||
@@ -491,10 +491,10 @@ class PromptPanel(
|
||||
override fun outlineColor() = if (UIUtil.isFocusAncestor(editor)) {
|
||||
JBUI.CurrentTheme.Focus.focusColor()
|
||||
} else {
|
||||
SessionUiStyle.View.line()
|
||||
SessionUiStyle.View.Outline.brightColor()
|
||||
}
|
||||
|
||||
override fun outlineWidth() = if (UIUtil.isFocusAncestor(editor)) focus.get() else JBUI.scale(1)
|
||||
override fun outlineWidth() = if (UIUtil.isFocusAncestor(editor)) focus.get() else SessionUiStyle.View.Outline.width()
|
||||
|
||||
override fun cornerArc() = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class SessionSelection : Disposable {
|
||||
|
||||
override fun clearSelection() {
|
||||
val pos = component.selectionStart.coerceIn(0, component.document.length)
|
||||
component.select(pos, pos)
|
||||
component.caretPosition = pos
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
|
||||
@@ -9,51 +9,57 @@ import javax.swing.border.Border
|
||||
|
||||
/** Static style tokens owned by the chat session UI. */
|
||||
object SessionUiStyle {
|
||||
object Transcript {
|
||||
fun bgColor(): Color = UiStyle.Colors.bg()
|
||||
}
|
||||
|
||||
/** Geometry for the transcript list and its scroll behavior. */
|
||||
object SessionLayout {
|
||||
const val GAP = 4
|
||||
const val GAP = 3
|
||||
const val TRANSCRIPT_PADDING = 12
|
||||
const val TRANSCRIPT_SCROLLBAR_PADDING = 10
|
||||
const val USER_PROMPT_INDENT = 100
|
||||
const val SCROLL_INCREMENT = 16
|
||||
const val SCROLL_INCREMENT = 48
|
||||
}
|
||||
|
||||
/** Shared tokens for individual transcript views and session views. */
|
||||
object View {
|
||||
const val SESSION_VIEW_GAP = 6
|
||||
const val SESSION_VIEW_VERTICAL_PADDING = 8
|
||||
const val SESSION_VIEW_HORIZONTAL_PADDING = 12
|
||||
const val SESSION_VIEW_BODY_EXTRA_HEIGHT = 16
|
||||
object Layout {
|
||||
const val GAP = 5
|
||||
const val VERTICAL_PADDING = 7
|
||||
const val HORIZONTAL_PADDING = 12
|
||||
const val BODY_EXTRA_HEIGHT = 16
|
||||
}
|
||||
|
||||
internal const val BORDER_DELTA = 80
|
||||
internal const val HOVER_BORDER_ALPHA = 0.18f
|
||||
internal const val HOVER_FILL_ALPHA = 0.10f
|
||||
|
||||
/** Creates a visible separator against editor-derived transcript surfaces. */
|
||||
fun line(): Color = JBColor.lazy { UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA) }
|
||||
object Surface {
|
||||
fun bgColor(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
fun transcript(): Color = UiStyle.Colors.bg()
|
||||
fun headerBgColor(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
fun sessionViewBackground(): Color = UiStyle.Colors.contentBackground()
|
||||
|
||||
fun sessionViewOutline(): Color = UiStyle.Colors.contentBorder()
|
||||
|
||||
fun surface(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
fun header(): Color = UiStyle.Colors.editorBackground()
|
||||
|
||||
/** Subtle hover fill, softer than the session-view outline. */
|
||||
fun headerHover(): Color = JBColor.lazy { UiStyle.Colors.blend(header(), hoverLine(), HOVER_FILL_ALPHA) }
|
||||
|
||||
/** Subtle hover outline, stronger than the hover fill. */
|
||||
fun hoverLine(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.blend(line(), JBUI.CurrentTheme.ActionButton.hoverBackground(), HOVER_BORDER_ALPHA)
|
||||
/** Subtle hover fill, softer than the session-view outline. */
|
||||
fun headerHoverBgColor(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.blend(headerBgColor(), Outline.hoverColor(), HOVER_FILL_ALPHA)
|
||||
}
|
||||
}
|
||||
|
||||
fun sessionView(color: Color = line()): Border = outline(color)
|
||||
object Outline {
|
||||
fun color(): Color = UiStyle.Colors.contentBorder()
|
||||
|
||||
fun outline(color: Color = line()): Border = JBUI.Borders.customLine(color, 1)
|
||||
fun brightColor(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.contrast(UiStyle.Colors.editorBackground(), BORDER_DELTA)
|
||||
}
|
||||
|
||||
fun topOutline(): Border = JBUI.Borders.customLineTop(line())
|
||||
/** Subtle hover outline, stronger than the hover fill. */
|
||||
fun hoverColor(): Color = JBColor.lazy {
|
||||
UiStyle.Colors.blend(brightColor(), JBUI.CurrentTheme.ActionButton.hoverBackground(), HOVER_BORDER_ALPHA)
|
||||
}
|
||||
|
||||
fun width(): Int = JBUI.scale(1)
|
||||
}
|
||||
|
||||
/** Prompt input dimensions and chrome inside the session view. */
|
||||
object Prompt {
|
||||
@@ -74,6 +80,9 @@ object SessionUiStyle {
|
||||
/** Reasoning block preview sizing. */
|
||||
object Reasoning {
|
||||
const val BODY_LINES = 5
|
||||
const val HEADER_VERTICAL_PADDING = 5
|
||||
const val BODY_VERTICAL_PADDING = 4
|
||||
const val BODY_HORIZONTAL_PADDING = 8
|
||||
}
|
||||
|
||||
/** Message container roles and user bubble geometry. */
|
||||
@@ -119,6 +128,12 @@ object SessionUiStyle {
|
||||
}
|
||||
}
|
||||
|
||||
object AccountPopup {
|
||||
fun bgColor(): Color = UiStyle.Colors.contentBackground()
|
||||
|
||||
fun outlineColor(): Color = UiStyle.Colors.contentBorder()
|
||||
}
|
||||
|
||||
/** Limits for the empty-state recent sessions list. */
|
||||
object RecentSessions {
|
||||
const val LIMIT = 5
|
||||
@@ -140,7 +155,13 @@ object SessionUiStyle {
|
||||
/** Border presets for connection dock panel. */
|
||||
object Dock {
|
||||
fun banner(): Border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLineTop(SessionUiStyle.View.line()),
|
||||
JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.lg(), 0, UiStyle.Gap.lg()),
|
||||
)!!
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
|
||||
val line = { JPanel().apply {
|
||||
background = SessionUiStyle.View.line()
|
||||
background = SessionUiStyle.View.Outline.color()
|
||||
isOpaque = true
|
||||
preferredSize = JBDimension(0, JBUI.scale(1))
|
||||
} }
|
||||
|
||||
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Message
|
||||
import ai.kilocode.client.session.model.Reasoning
|
||||
import ai.kilocode.client.session.model.StepFinish
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolCallRef
|
||||
@@ -18,6 +19,7 @@ import com.intellij.util.ui.JBUI
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* A single message container inside a [TurnView].
|
||||
@@ -36,6 +38,9 @@ class MessageView(
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
private val selection: SessionSelection? = null,
|
||||
private val resize: ((JComponent, () -> Unit) -> Unit)? = null,
|
||||
private val repo: String? = null,
|
||||
private val hover: ((PartView, Boolean) -> Unit)? = null,
|
||||
) : ai.kilocode.client.session.ui.SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
), Disposable, SessionEditorStyleTarget, SessionView {
|
||||
@@ -48,6 +53,11 @@ 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
|
||||
|
||||
init {
|
||||
@@ -59,10 +69,7 @@ class MessageView(
|
||||
for ((_, content) in msg.parts) {
|
||||
if (content is StepFinish) continue
|
||||
if (isHidden(content)) continue
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
addPart(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +88,11 @@ class MessageView(
|
||||
if (content is StepFinish) return
|
||||
if (isHidden(content)) {
|
||||
// Remove any stale view for this content so it disappears when suppressed
|
||||
val stale = parts.remove(content.id)
|
||||
val id = aliases.remove(content.id)
|
||||
sources.remove(content.id)
|
||||
val stale = if (id == null) parts.remove(content.id) else null
|
||||
if (stale != null) {
|
||||
detach(stale)
|
||||
remove(stale)
|
||||
Disposer.dispose(stale)
|
||||
syncBorder()
|
||||
@@ -90,6 +100,16 @@ class MessageView(
|
||||
}
|
||||
return
|
||||
}
|
||||
val id = aliases[content.id]
|
||||
if (id != null && content is Reasoning) {
|
||||
updateAlias(content, id)
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
if (id != null) {
|
||||
aliases.remove(content.id)
|
||||
sources.remove(content.id)
|
||||
}
|
||||
val existing = parts[content.id]
|
||||
if (existing != null) {
|
||||
if (ViewFactory.shouldReplace(existing, content)) {
|
||||
@@ -100,20 +120,56 @@ class MessageView(
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
addPart(content)
|
||||
syncBorder()
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun addPart(content: Content) {
|
||||
if (content is Reasoning) {
|
||||
val previous = parts.values.lastOrNull()
|
||||
if (previous is ReasoningView) {
|
||||
aliases[content.id] = previous.contentId
|
||||
sources[content.id] = content.content.toString()
|
||||
previous.update(merged(previous, content, content.content.toString()))
|
||||
return
|
||||
}
|
||||
}
|
||||
val view = view(content)
|
||||
view.resize = resize
|
||||
view.hover = hover
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
syncBorder()
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun updateAlias(content: Reasoning, id: String) {
|
||||
val view = parts[id] as? ReasoningView ?: return
|
||||
val prev = sources[content.id].orEmpty()
|
||||
val next = content.content.toString()
|
||||
val delta = if (next.startsWith(prev)) next.removePrefix(prev) else next
|
||||
sources[content.id] = next
|
||||
if (delta.isEmpty()) return
|
||||
view.update(merged(view, content, delta))
|
||||
}
|
||||
|
||||
private fun merged(view: ReasoningView, content: Reasoning, delta: String) = Reasoning(view.contentId).also {
|
||||
it.done = content.done
|
||||
it.content.append(view.markdown())
|
||||
it.content.append(delta)
|
||||
}
|
||||
|
||||
private fun replacePart(content: Content, existing: PartView) {
|
||||
val at = components.indexOfFirst { it === existing }.takeIf { it >= 0 } ?: componentCount
|
||||
parts.remove(content.id)
|
||||
aliases.values.removeAll { it == content.id }
|
||||
sources.keys.removeAll { it !in aliases }
|
||||
detach(existing)
|
||||
remove(existing)
|
||||
Disposer.dispose(existing)
|
||||
val view = view(content)
|
||||
view.resize = resize
|
||||
view.hover = hover
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view, at)
|
||||
@@ -123,7 +179,14 @@ class MessageView(
|
||||
|
||||
/** Remove the renderer for [contentId] if present. */
|
||||
fun removePart(contentId: String) {
|
||||
if (aliases.remove(contentId) != null) {
|
||||
sources.remove(contentId)
|
||||
return
|
||||
}
|
||||
val view = parts.remove(contentId) ?: return
|
||||
aliases.values.removeAll { it == contentId }
|
||||
sources.keys.removeAll { it !in aliases }
|
||||
detach(view)
|
||||
remove(view)
|
||||
Disposer.dispose(view)
|
||||
syncBorder()
|
||||
@@ -150,17 +213,17 @@ class MessageView(
|
||||
*/
|
||||
private fun rebuildParts() {
|
||||
parts.values.forEach {
|
||||
detach(it)
|
||||
remove(it)
|
||||
Disposer.dispose(it)
|
||||
}
|
||||
parts.clear()
|
||||
aliases.clear()
|
||||
sources.clear()
|
||||
for ((_, content) in msg.parts) {
|
||||
if (content is StepFinish) continue
|
||||
if (isHidden(content)) continue
|
||||
val view = view(content)
|
||||
view.applyStyle(style)
|
||||
parts[content.id] = view
|
||||
add(view)
|
||||
addPart(content)
|
||||
}
|
||||
syncBorder()
|
||||
refresh()
|
||||
@@ -172,20 +235,22 @@ class MessageView(
|
||||
}
|
||||
|
||||
private fun view(content: Content) = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) {
|
||||
ViewFactory.createUser(content, openFile, openUrl, selection)
|
||||
ViewFactory.createUser(content, openFile, openUrl, selection, repo)
|
||||
} else {
|
||||
ViewFactory.create(content, openFile, openUrl, selection)
|
||||
ViewFactory.create(content, openFile, openUrl, selection, repo)
|
||||
}
|
||||
|
||||
/** Append a streaming delta to the renderer for [contentId]. */
|
||||
fun appendDelta(contentId: String, delta: String): Boolean {
|
||||
val part = parts[contentId] ?: return false
|
||||
val id = aliases[contentId]
|
||||
if (id != null) sources[contentId] = sources[contentId].orEmpty() + delta
|
||||
val part = parts[id ?: contentId] ?: return false
|
||||
part.appendDelta(delta)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Look up a renderer by part id. */
|
||||
fun part(id: String): PartView? = parts[id]
|
||||
fun part(id: String): PartView? = parts[aliases[id] ?: id]
|
||||
|
||||
/** Ordered part ids — stable for test assertions. */
|
||||
fun partIds(): List<String> = parts.keys.toList()
|
||||
@@ -202,10 +267,13 @@ class MessageView(
|
||||
|
||||
override fun dispose() {
|
||||
parts.values.forEach {
|
||||
detach(it)
|
||||
remove(it)
|
||||
Disposer.dispose(it)
|
||||
}
|
||||
parts.clear()
|
||||
aliases.clear()
|
||||
sources.clear()
|
||||
hidden = null
|
||||
}
|
||||
|
||||
@@ -220,7 +288,7 @@ class MessageView(
|
||||
val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
g2.color = style.editorScheme.defaultBackground
|
||||
g2.fillRoundRect(0, 0, width, height, arc, arc)
|
||||
g2.color = SessionUiStyle.View.line()
|
||||
g2.color = SessionUiStyle.View.Outline.brightColor()
|
||||
val w = width - 1
|
||||
val h = height - 1
|
||||
if (w > 0 && h > 0) g2.drawRoundRect(0, 0, w, h, arc, arc)
|
||||
@@ -235,5 +303,10 @@ class MessageView(
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun detach(view: PartView) {
|
||||
view.setHovered(false)
|
||||
view.hover = null
|
||||
}
|
||||
|
||||
private fun assistantBorder() = JBUI.Borders.empty()
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
import ai.kilocode.client.ui.md.MdViewFactory
|
||||
import com.intellij.icons.AllIcons
|
||||
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
|
||||
@@ -24,6 +24,7 @@ import java.awt.Rectangle
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/** Renders reasoning as a secondary collapsible block. */
|
||||
class ReasoningView(
|
||||
@@ -32,11 +33,17 @@ class ReasoningView(
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ReasoningParts = reasoningParts(selection),
|
||||
) :
|
||||
SecondarySessionPartView(parts.header, { parts.scroll(openUrl) }) {
|
||||
SecondarySessionPartView(
|
||||
parts.header,
|
||||
{ parts.scroll(openUrl) },
|
||||
expanded = reasoning.content.isNotBlank() && !reasoning.done,
|
||||
) {
|
||||
|
||||
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)
|
||||
@@ -50,52 +57,97 @@ class ReasoningView(
|
||||
|
||||
private var style = SessionEditorStyle.current()
|
||||
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(
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.HEADER_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
bindHeader(parts.title, parts.icon)
|
||||
applyStyle(style)
|
||||
if (bodyVisible()) syncBody()
|
||||
syncBorder()
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
syncBorder()
|
||||
syncBody()
|
||||
applyBodyStyle()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (!changed) return false
|
||||
syncBorder()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Reasoning) return
|
||||
var changed = false
|
||||
val next = content.content.toString()
|
||||
val follow = tailVisible()
|
||||
if (done != content.done) {
|
||||
done = content.done
|
||||
changed = true
|
||||
}
|
||||
if (source != next) {
|
||||
source = next
|
||||
if (parts.bodyCreated()) md.set(source)
|
||||
if (parts.bodyCreated()) {
|
||||
md.set(source)
|
||||
followTail(follow)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
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)
|
||||
if (parts.bodyCreated()) {
|
||||
md.append(delta)
|
||||
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
|
||||
@@ -107,6 +159,7 @@ class ReasoningView(
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
@@ -116,7 +169,34 @@ class ReasoningView(
|
||||
|
||||
private fun canExpand(): Boolean = source.isNotBlank()
|
||||
|
||||
private fun sync(): Boolean = syncExpandable(canExpand())
|
||||
private fun sync(): Boolean {
|
||||
var changed = false
|
||||
val visible = source.isNotBlank()
|
||||
if (isVisible != visible) {
|
||||
isVisible = visible
|
||||
changed = true
|
||||
}
|
||||
changed = syncExpandable(canExpand()) || changed
|
||||
if (visible && !done && !parts.bodyCreated()) {
|
||||
changed = expand() || changed
|
||||
changed = syncExpandable(canExpand()) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
0,
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(0, 1, 0, 0)
|
||||
}
|
||||
|
||||
private fun apply(md: MdView): Boolean {
|
||||
var changed = false
|
||||
@@ -130,10 +210,12 @@ class ReasoningView(
|
||||
return changed
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun syncBody() {
|
||||
val md = md
|
||||
registerBody(md)
|
||||
md.set(source)
|
||||
followTail(true)
|
||||
}
|
||||
|
||||
private fun applyBodyStyle(): Boolean {
|
||||
@@ -154,7 +236,28 @@ class ReasoningView(
|
||||
if (!parts.bodyCreated()) return 0
|
||||
val md = md
|
||||
return md.component.getFontMetrics(md.font).height * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_BODY_EXTRA_HEIGHT)
|
||||
JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
override fun dumpLabel(): String {
|
||||
@@ -187,18 +290,18 @@ class ReasoningParts(
|
||||
}
|
||||
val panel = TrackPanel().apply {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Reasoning.BODY_HORIZONTAL_PADDING),
|
||||
)
|
||||
add(md.component, BorderLayout.CENTER)
|
||||
}
|
||||
val scroll = JBScrollPane(panel).apply {
|
||||
border = SessionUiStyle.View.topOutline()
|
||||
border = JBUI.Borders.empty()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
viewport.background = SessionUiStyle.View.surface()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
viewport.background = SessionUiStyle.View.Surface.bgColor()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
@@ -214,8 +317,8 @@ class ReasoningBody(
|
||||
|
||||
private fun reasoningParts(selection: SessionSelection? = null): ReasoningParts {
|
||||
val title = JBLabel(KiloBundle.message("session.part.reasoning")).apply { foreground = UiStyle.Colors.weak() }
|
||||
val icon = JBLabel(AllIcons.General.InspectionsEye).apply { foreground = UiStyle.Colors.weak() }
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply {
|
||||
val icon = JBLabel(SessionViewIcons.eye).apply { foreground = UiStyle.Colors.weak() }
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
add(icon, BorderLayout.WEST)
|
||||
add(title, BorderLayout.CENTER)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ai.kilocode.client.session.views
|
||||
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
|
||||
object SessionViewIcons {
|
||||
val brain = icon("brain")
|
||||
val bubble = icon("bubble-5")
|
||||
val bulletList = icon("bullet-list")
|
||||
val checklist = icon("checklist")
|
||||
val chevronDown = icon("chevron-down")
|
||||
val chevronLeft = icon("chevron-left")
|
||||
val chevronRight = icon("chevron-right")
|
||||
val code = icon("code")
|
||||
val codeLines = icon("code-lines")
|
||||
val console = icon("console")
|
||||
val eye = icon("eye")
|
||||
val glasses = icon("glasses")
|
||||
val mcp = icon("mcp")
|
||||
val search = icon("magnifying-glass-menu")
|
||||
val task = icon("task")
|
||||
val warning = icon("warning")
|
||||
val windowCursor = icon("window-cursor")
|
||||
|
||||
private fun icon(name: String) = IconLoader.getIcon("/icons/views/$name.svg", SessionViewIcons::class.java)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ open class TextView(
|
||||
|
||||
protected open fun styleFont(style: SessionEditorStyle) = style.transcriptFont
|
||||
|
||||
protected open fun styleBackground(style: SessionEditorStyle) = SessionUiStyle.View.transcript()
|
||||
protected open fun styleBackground(style: SessionEditorStyle) = SessionUiStyle.Transcript.bgColor()
|
||||
|
||||
private fun refresh() {
|
||||
revalidate()
|
||||
|
||||
@@ -1,655 +0,0 @@
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
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.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.ui.selection.SessionSelection
|
||||
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.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.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
|
||||
import javax.swing.Box
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
/** Renders non-read tool calls with VS Code-inspired rows/cards. */
|
||||
class ToolView(
|
||||
tool: Tool,
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool),
|
||||
) :
|
||||
SecondarySessionPartView(parts.header, { parts.scroll(tool) }) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var registered = false
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
syncBody()
|
||||
applyBodyStyle()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
val was = item.name
|
||||
item = content
|
||||
var changed = false
|
||||
if (was != content.name || !canExpand(content)) changed = collapse() || changed
|
||||
changed = sync() || changed
|
||||
changed = syncBody() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
fun commandText(): String = command(item)
|
||||
|
||||
fun outputText(): String = output(item)
|
||||
fun bodyText(): String = body(item)
|
||||
internal fun previewText(): String = parts.text?.text ?: preview(item)
|
||||
fun hasToggle(): Boolean = arrow.isVisible
|
||||
internal fun bodyFont() = parts.text?.font ?: style.transcriptFont
|
||||
internal fun titleFont() = parts.title.font
|
||||
internal fun subtitleFont() = parts.sub.font
|
||||
internal fun stateFont() = parts.state.font
|
||||
internal fun bodyEditable() = parts.text?.isEditable ?: false
|
||||
internal fun bodyCaretVisible() = parts.text?.caret?.isVisible ?: false
|
||||
internal fun bodyVisible() = parts.scroll?.parent === this
|
||||
internal fun controlCount() = if (arrow.isVisible) 1 else 0
|
||||
internal fun horizontalPolicy() = parts.scroll?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
internal fun bodyWrap() = parts.text?.lineWrap ?: true
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
internal fun bodyCreated() = parts.bodyCreated()
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.link, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = applyBodyStyle() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
val expand = canExpand(item)
|
||||
var changed = false
|
||||
changed = syncExpandable(expand) || changed
|
||||
changed = setVisible(parts.state, !expand) || changed
|
||||
changed = syncLabels() || changed
|
||||
val text = parts.text
|
||||
if (text != null) changed = setForeground(text, bodyColor()) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncLabels(): Boolean {
|
||||
var changed = false
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
var changed = false
|
||||
val text = parts.text ?: return false
|
||||
val value = preview(item)
|
||||
if (text.text != value) {
|
||||
text.text = value
|
||||
text.caretPosition = 0
|
||||
changed = true
|
||||
}
|
||||
changed = setForeground(text, bodyColor()) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun applyBodyStyle(): Boolean {
|
||||
val text = parts.text ?: return false
|
||||
if (!registered && selection != null && text.parent != null) {
|
||||
registered = true
|
||||
selection.register(text, this)
|
||||
}
|
||||
return setFont(text, style.transcriptFont)
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val text = parts.text ?: return 0
|
||||
return text.getFontMetrics(text.font).height * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "ToolView#$contentId(${labelText()})"
|
||||
}
|
||||
|
||||
/** Renders read calls with secondary, borderless chrome. */
|
||||
class ReadToolView(
|
||||
tool: Tool,
|
||||
openFile: (String) -> Unit = {},
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool, openFile),
|
||||
) : SecondarySessionPartView(parts.header, parts.scroll(tool), expandable = false) {
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = tool.kind == ToolKind.READ
|
||||
}
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
parts.text?.let { selection?.register(it, this) }
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
parts.text?.text = preview(item)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
item = content
|
||||
var changed = sync()
|
||||
changed = syncBody() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
fun bodyText(): String = body(item)
|
||||
internal fun bodyVisible() = parts.scroll?.parent === this
|
||||
internal fun hasToggle() = arrow.isVisible
|
||||
internal fun horizontalPolicy() = parts.scroll?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.BODY_LINES
|
||||
internal fun bodyFont() = parts.text?.font ?: style.transcriptFont
|
||||
internal fun linkVisible() = parts.link.isVisible
|
||||
internal fun linkText() = parts.label
|
||||
internal fun linkMarkup() = parts.link.text ?: ""
|
||||
internal fun linkForeground() = parts.link.foreground
|
||||
internal fun linkFont() = parts.link.font
|
||||
internal fun subtitleForeground() = parts.sub.foreground
|
||||
internal fun subtitleFont() = parts.sub.font
|
||||
internal fun linkHref() = parts.href
|
||||
internal fun openLink() = parts.openLink()
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.transcriptFont) || changed
|
||||
changed = setFont(parts.link, style.transcriptFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
parts.text?.let { changed = setFont(it, style.transcriptFont) || changed }
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
var changed = false
|
||||
changed = syncExpandable(false) || changed
|
||||
changed = setVisible(parts.state, true) || changed
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = syncSubtitle() || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.sub, UiStyle.Colors.fg()) || changed
|
||||
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
parts.text?.let { changed = setForeground(it, bodyColor()) || changed }
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncSubtitle(): Boolean {
|
||||
val target = target(item)?.takeIf { it.type == "file" }
|
||||
if (target != null) {
|
||||
var changed = false
|
||||
if (parts.href != target.path) {
|
||||
parts.href = target.path
|
||||
changed = true
|
||||
}
|
||||
changed = setLinkText(parts, tail(target.path).ifBlank { target.path }) || changed
|
||||
changed = show(parts, true) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
var changed = false
|
||||
if (parts.href != null) {
|
||||
parts.href = null
|
||||
changed = true
|
||||
}
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = show(parts, false) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
val value = preview(item)
|
||||
val text = parts.text ?: return false
|
||||
if (text.text == value) return false
|
||||
text.text = value
|
||||
text.caretPosition = 0
|
||||
return true
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val text = parts.text ?: return 0
|
||||
return text.getFontMetrics(text.font).height * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "ReadToolView#$contentId(${labelText()})"
|
||||
}
|
||||
|
||||
class ToolParts(
|
||||
val header: JPanel,
|
||||
val glyph: JBLabel,
|
||||
val title: JBLabel,
|
||||
val sub: JBLabel,
|
||||
val link: JBLabel,
|
||||
val slot: JPanel,
|
||||
val state: JBLabel,
|
||||
val center: JPanel,
|
||||
val controls: JComponent,
|
||||
private val open: ((String) -> Unit)? = null,
|
||||
) {
|
||||
var href: String? = null
|
||||
var label: String = ""
|
||||
private var body: ToolBody? = null
|
||||
|
||||
val text: JBTextArea?
|
||||
get() = body?.text
|
||||
|
||||
val scroll: JBScrollPane?
|
||||
get() = body?.scroll
|
||||
|
||||
fun scroll(tool: Tool): JBScrollPane = body(tool).scroll
|
||||
|
||||
fun bodyCreated() = body != null
|
||||
|
||||
fun openLink() {
|
||||
val value = href ?: return
|
||||
open?.invoke(value)
|
||||
}
|
||||
|
||||
private fun body(tool: Tool): ToolBody {
|
||||
val item = body
|
||||
if (item != null) return item
|
||||
val text = JBTextArea().apply {
|
||||
isEditable = false
|
||||
caret.isVisible = false
|
||||
caret.isSelectionVisible = true
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
foreground = if (tool.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
val scroll = JBScrollPane(text).apply {
|
||||
border = SessionUiStyle.View.topOutline()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
viewport.background = SessionUiStyle.View.surface()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
return ToolBody(text, scroll).also { body = it }
|
||||
}
|
||||
}
|
||||
|
||||
class ToolBody(
|
||||
val text: JBTextArea,
|
||||
val scroll: JBScrollPane,
|
||||
)
|
||||
|
||||
private const val SUB_CARD = "sub"
|
||||
private const val LINK_CARD = "link"
|
||||
|
||||
private fun toolParts(tool: Tool, openFile: ((String) -> Unit)? = null): ToolParts {
|
||||
lateinit var parts: ToolParts
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val link = JBLabel().apply {
|
||||
isVisible = false
|
||||
isFocusable = false
|
||||
foreground = UiStyle.Colors.fg()
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
setRequestFocusEnabled(false)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
parts.openLink()
|
||||
}
|
||||
})
|
||||
}
|
||||
val slot = JPanel(CardLayout()).apply {
|
||||
isOpaque = false
|
||||
add(sub, SUB_CARD)
|
||||
add(link, LINK_CARD)
|
||||
}
|
||||
val state = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply { isOpaque = false }
|
||||
val controls = Box.createHorizontalBox()
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
center.add(title, BorderLayout.WEST)
|
||||
center.add(slot, BorderLayout.CENTER)
|
||||
add(glyph, BorderLayout.WEST)
|
||||
add(center, BorderLayout.CENTER)
|
||||
add(controls, BorderLayout.EAST)
|
||||
}
|
||||
parts = ToolParts(header, glyph, title, sub, link, slot, state, center, controls, openFile)
|
||||
return parts.also {
|
||||
controls.add(it.state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun icon(tool: Tool) = when (tool.name) {
|
||||
"read" -> AllIcons.Actions.Preview
|
||||
"bash" -> AllIcons.Debugger.Console
|
||||
else -> when (tool.state) {
|
||||
ToolExecState.PENDING -> AllIcons.Process.Step_1
|
||||
ToolExecState.RUNNING -> AllIcons.Process.Step_2
|
||||
ToolExecState.COMPLETED -> AllIcons.Actions.Checked
|
||||
ToolExecState.ERROR -> AllIcons.General.Error
|
||||
}
|
||||
}
|
||||
|
||||
private fun title(tool: Tool) = when (tool.name) {
|
||||
"read" -> KiloBundle.message("session.part.tool.read")
|
||||
"bash" -> KiloBundle.message("session.part.tool.shell")
|
||||
else -> toolTitle(tool)
|
||||
}
|
||||
|
||||
private fun subtitle(tool: Tool) = when (tool.name) {
|
||||
"read" -> readPath(tool)
|
||||
"bash" -> shellTitle(tool)
|
||||
else -> toolSubtitle(tool)
|
||||
}
|
||||
|
||||
private fun setText(label: JBLabel, text: String): Boolean {
|
||||
val value = if (text.isBlank()) "" else XmlStringUtil.wrapInHtml(XmlStringUtil.escapeString(text))
|
||||
if (label.text == value) return false
|
||||
label.text = value
|
||||
return true
|
||||
}
|
||||
|
||||
private 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
|
||||
parts.label = text
|
||||
parts.link.text = value
|
||||
return true
|
||||
}
|
||||
|
||||
private 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)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.label else parts.sub.text
|
||||
|
||||
private fun setIcon(label: JBLabel, icon: Icon): Boolean {
|
||||
if (label.icon === icon) return false
|
||||
label.icon = icon
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setVisible(component: JComponent, visible: Boolean): Boolean {
|
||||
if (component.isVisible == visible) return false
|
||||
component.isVisible = visible
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setForeground(component: JComponent, color: Color): Boolean {
|
||||
if (same(component.foreground, color)) return false
|
||||
component.foreground = color
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setFont(component: JComponent, font: Font): Boolean {
|
||||
if (component.font == font) return false
|
||||
component.font = font
|
||||
return true
|
||||
}
|
||||
|
||||
private fun same(a: Color?, b: Color): Boolean = a?.rgb == b.rgb
|
||||
|
||||
private fun color(tool: Tool) = when (tool.state) {
|
||||
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.errorLabelForeground()
|
||||
} else {
|
||||
UiStyle.Colors.fg()
|
||||
}
|
||||
|
||||
private fun stateText(tool: Tool) = when (tool.state) {
|
||||
ToolExecState.PENDING -> KiloBundle.message("session.part.tool.pending")
|
||||
ToolExecState.RUNNING -> KiloBundle.message("session.part.tool.running")
|
||||
ToolExecState.COMPLETED -> ""
|
||||
ToolExecState.ERROR -> KiloBundle.message("session.part.tool.error")
|
||||
}
|
||||
|
||||
private fun readPath(tool: Tool): String {
|
||||
val target = target(tool)
|
||||
if (target != null) {
|
||||
if (target.type == "file") return tail(target.path).ifBlank { target.path }
|
||||
return target.path
|
||||
}
|
||||
val path = tool.input["filePath"] ?: tool.input["path"] ?: tool.title ?: return tool.name
|
||||
return tail(path).ifBlank { path }
|
||||
}
|
||||
|
||||
private data class Target(
|
||||
val path: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
private fun target(tool: Tool): Target? {
|
||||
val out = output(tool)
|
||||
if (out.isBlank()) return null
|
||||
val path = tag(out, "path") ?: return null
|
||||
val type = tag(out, "type") ?: return null
|
||||
return Target(path, type.lowercase())
|
||||
}
|
||||
|
||||
private fun tag(text: String, name: String): String? =
|
||||
Regex("<$name>\\s*([\\s\\S]*?)\\s*</$name>")
|
||||
.find(text)
|
||||
?.groupValues
|
||||
?.getOrNull(1)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun shellTitle(tool: Tool): String =
|
||||
tool.input["description"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["description"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.title?.takeIf { it.isNotBlank() }
|
||||
?: command(tool).lineSequence().firstOrNull { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
private fun command(tool: Tool): String =
|
||||
tool.input["command"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["command"]?.takeIf { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
private fun output(tool: Tool): String =
|
||||
tool.output?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["output"]?.takeIf { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
private fun preview(tool: Tool): String = if (tool.name == "bash") shellPreview(tool) else plainPreview(tool)
|
||||
|
||||
private fun body(tool: Tool): String = if (tool.name == "bash") shellBody(tool) else plainBody(tool)
|
||||
|
||||
private fun shellPreview(tool: Tool): String {
|
||||
val cmd = command(tool)
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return Preview().apply {
|
||||
if (cmd.isNotBlank()) append("$ ").append(cmd)
|
||||
if (out.isNotBlank()) {
|
||||
sep()
|
||||
append(out)
|
||||
}
|
||||
if (err != null) {
|
||||
sep()
|
||||
append(err)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun shellBody(tool: Tool): String {
|
||||
val cmd = command(tool)
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return buildString {
|
||||
if (cmd.isNotBlank()) append("$ ").append(cmd)
|
||||
if (out.isNotBlank()) {
|
||||
if (isNotEmpty()) append("\n\n")
|
||||
append(out)
|
||||
}
|
||||
if (err != null) {
|
||||
if (isNotEmpty()) append("\n\n")
|
||||
append(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun plainPreview(tool: Tool): String {
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return Preview().apply {
|
||||
if (out.isNotBlank()) append(out)
|
||||
if (err != null) {
|
||||
sep()
|
||||
append(err)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun plainBody(tool: Tool): String {
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return listOf(out, err).filter { !it.isNullOrBlank() }.joinToString("\n\n")
|
||||
}
|
||||
|
||||
private fun canExpand(tool: Tool): Boolean {
|
||||
if (tool.name == "bash") return command(tool).isNotBlank() || output(tool).isNotBlank() || !tool.error.isNullOrBlank()
|
||||
return output(tool).isNotBlank() || !tool.error.isNullOrBlank()
|
||||
}
|
||||
|
||||
private fun toolTitle(tool: Tool): String =
|
||||
tool.title?.takeIf { it.isNotBlank() }
|
||||
?: tool.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
|
||||
private fun toolSubtitle(tool: Tool): String {
|
||||
val base = listOf("description", "query", "url", "filePath", "path", "name")
|
||||
.mapNotNull { tool.input[it]?.takeIf { value -> value.isNotBlank() } }
|
||||
.firstOrNull()
|
||||
val args = listOf("pattern", "include", "offset", "limit")
|
||||
.mapNotNull { key -> tool.input[key]?.takeIf { it.isNotBlank() }?.let { "$key=$it" } }
|
||||
return listOfNotNull(base).plus(args).joinToString(" ")
|
||||
}
|
||||
|
||||
private fun tail(path: String): String {
|
||||
val value = path.trimEnd('/', '\\')
|
||||
val index = maxOf(value.lastIndexOf('/'), value.lastIndexOf('\\'))
|
||||
if (index < 0) return value
|
||||
return value.substring(index + 1)
|
||||
}
|
||||
|
||||
private class Preview {
|
||||
private val text = StringBuilder()
|
||||
private var cut = false
|
||||
|
||||
fun append(value: String): Preview {
|
||||
if (cut) return this
|
||||
val rem = SessionUiStyle.View.Tool.PREVIEW_LIMIT - text.length
|
||||
if (value.length <= rem) {
|
||||
text.append(value)
|
||||
return this
|
||||
}
|
||||
if (rem > 0) text.append(value, 0, rem)
|
||||
cut = true
|
||||
return this
|
||||
}
|
||||
|
||||
fun sep(): Preview {
|
||||
if (text.isNotEmpty()) append("\n\n")
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
if (!cut) return text.toString()
|
||||
if (text.isNotEmpty()) text.append("\n\n")
|
||||
text.append(KiloBundle.message("session.part.tool.truncated"))
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.ui.JBUI
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Top-level transcript item representing one conversational turn.
|
||||
@@ -25,6 +27,9 @@ class TurnView(
|
||||
private var style: SessionEditorStyle = SessionEditorStyle.current(),
|
||||
private val openUrl: (String) -> Unit = {},
|
||||
private val selection: SessionSelection? = null,
|
||||
private val resize: ((JComponent, () -> Unit) -> Unit)? = null,
|
||||
private val repo: String? = null,
|
||||
private val hover: ((PartView, Boolean) -> Unit)? = null,
|
||||
) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), Disposable, SessionEditorStyleTarget {
|
||||
|
||||
constructor(id: String, openFile: (String) -> Unit) : this(id, openFile, SessionEditorStyle.current())
|
||||
@@ -37,7 +42,7 @@ class TurnView(
|
||||
|
||||
/** Add a new [MessageView] for [msg] at the end of this turn. */
|
||||
fun addMessage(msg: Message): MessageView {
|
||||
val view = MessageView(msg, openFile, style, openUrl, selection)
|
||||
val view = MessageView(msg, openFile, style, openUrl, selection, resize, repo, hover)
|
||||
messages[msg.info.id] = view
|
||||
add(view)
|
||||
revalidate()
|
||||
|
||||
@@ -3,6 +3,10 @@ package ai.kilocode.client.session.views
|
||||
import ai.kilocode.client.session.views.base.GenericView
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.session.views.question.QuestionResultView
|
||||
import ai.kilocode.client.session.views.tool.GlobToolView
|
||||
import ai.kilocode.client.session.views.tool.ReadToolView
|
||||
import ai.kilocode.client.session.views.tool.SearchToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.model.Compaction
|
||||
import ai.kilocode.client.session.model.Content
|
||||
@@ -25,19 +29,20 @@ object ViewFactory {
|
||||
fun create(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
): PartView = create(content, openFile, openUrl = {}, selection = null)
|
||||
): PartView = create(content, openFile, openUrl = {}, selection = null, repo = null)
|
||||
|
||||
fun create(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit,
|
||||
): PartView = create(content, openFile, openUrl, selection = null)
|
||||
): PartView = create(content, openFile, openUrl, selection = null, repo = null)
|
||||
|
||||
fun create(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit = {},
|
||||
selection: SessionSelection? = null,
|
||||
repo: String? = null,
|
||||
): PartView = when (content) {
|
||||
is Text -> TextView(content, openUrl = openUrl, selection = selection)
|
||||
is Reasoning -> ReasoningView(content, openUrl = openUrl, selection = selection)
|
||||
@@ -45,6 +50,8 @@ object ViewFactory {
|
||||
TodoWriteView.canRender(content) -> TodoWriteView(content)
|
||||
PlanExitView.canRender(content) -> PlanExitView(content, openFile, selection)
|
||||
QuestionResultView.canRender(content) -> QuestionResultView(content, selection)
|
||||
GlobToolView.canRender(content) -> GlobToolView(content, selection = selection, repo = repo)
|
||||
SearchToolView.canRender(content) -> SearchToolView(content, selection = selection, repo = repo)
|
||||
ReadToolView.canRender(content) -> ReadToolView(content, openFile, selection = selection)
|
||||
else -> ToolView(content, selection = selection)
|
||||
}
|
||||
@@ -56,22 +63,23 @@ object ViewFactory {
|
||||
fun createUser(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
): PartView = createUser(content, openFile, openUrl = {}, selection = null)
|
||||
): PartView = createUser(content, openFile, openUrl = {}, selection = null, repo = null)
|
||||
|
||||
fun createUser(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit,
|
||||
): PartView = createUser(content, openFile, openUrl, selection = null)
|
||||
): PartView = createUser(content, openFile, openUrl, selection = null, repo = null)
|
||||
|
||||
fun createUser(
|
||||
content: Content,
|
||||
openFile: (String) -> Unit,
|
||||
openUrl: (String) -> Unit = {},
|
||||
selection: SessionSelection? = null,
|
||||
repo: String? = null,
|
||||
): PartView = when (content) {
|
||||
is Text -> PromptView(content, openUrl = openUrl, selection = selection)
|
||||
else -> create(content, openFile, openUrl, selection)
|
||||
else -> create(content, openFile, openUrl, selection, repo)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +94,10 @@ object ViewFactory {
|
||||
if (view is PlanExitView) return !PlanExitView.canRender(content)
|
||||
if (view !is PlanExitView && PlanExitView.canRender(content)) return true
|
||||
if (view is QuestionResultView) return !QuestionResultView.canRender(content)
|
||||
if (view is GlobToolView) return !GlobToolView.canRender(content) || QuestionResultView.canRender(content)
|
||||
if (view !is GlobToolView && GlobToolView.canRender(content)) return true
|
||||
if (view is SearchToolView) return !SearchToolView.canRender(content) || QuestionResultView.canRender(content)
|
||||
if (view !is SearchToolView && SearchToolView.canRender(content)) return true
|
||||
if (view is ReadToolView) return !ReadToolView.canRender(content) || QuestionResultView.canRender(content)
|
||||
if (view is ToolView && ReadToolView.canRender(content)) return true
|
||||
if (view is ToolView) return QuestionResultView.canRender(content)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
@@ -29,7 +29,7 @@ abstract class AbstractSessionPartView(
|
||||
) : this(header, { body }, expanded, expandable)
|
||||
|
||||
protected val arrow = JBLabel()
|
||||
protected val row = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0))
|
||||
protected val row = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0))
|
||||
private val bound = linkedSetOf<Component>()
|
||||
private var body: JComponent? = null
|
||||
|
||||
@@ -41,12 +41,12 @@ abstract class AbstractSessionPartView(
|
||||
}
|
||||
private val mouse = object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
setHover(true)
|
||||
setHovered(true)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
if (inside(e)) return
|
||||
setHover(false)
|
||||
setHovered(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ abstract class AbstractSessionPartView(
|
||||
|
||||
fun toggle() {
|
||||
if (!expandable || !arrow.isVisible) return
|
||||
val changed = if (isExpanded()) collapse() else expand()
|
||||
val changed = toggleLocal()
|
||||
if (!changed) return
|
||||
syncArrow()
|
||||
refresh()
|
||||
@@ -78,7 +78,7 @@ abstract class AbstractSessionPartView(
|
||||
return true
|
||||
}
|
||||
|
||||
fun collapse(): Boolean {
|
||||
open fun collapse(): Boolean {
|
||||
val item = body ?: return false
|
||||
if (item.parent !== this) return false
|
||||
remove(item)
|
||||
@@ -89,6 +89,15 @@ abstract class AbstractSessionPartView(
|
||||
|
||||
protected fun bodyComponent(): JComponent = body()
|
||||
|
||||
private fun toggleLocal(): Boolean {
|
||||
val fn = resize ?: return toggleBody()
|
||||
val expanded = isExpanded()
|
||||
fn(this) { toggleBody() }
|
||||
return expanded != isExpanded()
|
||||
}
|
||||
|
||||
private fun toggleBody(): Boolean = if (isExpanded()) collapse() else expand()
|
||||
|
||||
fun syncExpandable(expandable: Boolean): Boolean {
|
||||
val active = this.expandable && expandable
|
||||
val changed = setVisible(arrow, active)
|
||||
@@ -110,13 +119,11 @@ abstract class AbstractSessionPartView(
|
||||
|
||||
protected open fun hoverColor(value: Boolean): Color? = null
|
||||
|
||||
protected open fun applyHover(value: Boolean, color: Color) {}
|
||||
|
||||
private fun setHover(value: Boolean) {
|
||||
override fun setHovered(value: Boolean) {
|
||||
hover?.invoke(this, value)
|
||||
val color = hoverColor(value) ?: return
|
||||
if (row.background?.rgb == color.rgb) return
|
||||
row.background = color
|
||||
applyHover(value, color)
|
||||
row.repaint()
|
||||
}
|
||||
|
||||
@@ -150,7 +157,7 @@ abstract class AbstractSessionPartView(
|
||||
}
|
||||
|
||||
private fun syncArrow(): Boolean {
|
||||
val icon = if (isExpanded()) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
|
||||
val icon = if (isExpanded()) SessionViewIcons.chevronDown else SessionViewIcons.chevronRight
|
||||
if (arrow.icon === icon) return false
|
||||
arrow.icon = icon
|
||||
return true
|
||||
|
||||
@@ -41,6 +41,8 @@ import javax.swing.JPanel
|
||||
class BaseQuestionView(
|
||||
private val selection: SessionSelection? = null,
|
||||
) : RoundedContentPanel(
|
||||
UiStyle.Gap.pad(),
|
||||
UiStyle.Gap.pad(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.pad(),
|
||||
), SessionEditorStyleTarget {
|
||||
@@ -95,6 +97,7 @@ class BaseQuestionView(
|
||||
private var top: JComponent? = null
|
||||
private var content: JComponent? = null
|
||||
private var actionLeft: JComponent? = null
|
||||
private var gap = UiStyle.Gap.lg()
|
||||
|
||||
// action buttons keyed by id for retained updates
|
||||
private val actionButtons = mutableMapOf<String, JButton>()
|
||||
@@ -184,6 +187,15 @@ class BaseQuestionView(
|
||||
repaint()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setSpacing(top: Int, gap: Int) {
|
||||
this.gap = gap
|
||||
border = JBUI.Borders.empty(top, UiStyle.Gap.pad(), UiStyle.Gap.lg(), UiStyle.Gap.pad())
|
||||
syncNorth()
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the action buttons shown in the card's right-aligned footer.
|
||||
*
|
||||
@@ -275,9 +287,9 @@ class BaseQuestionView(
|
||||
|
||||
// ---- contentColor override ----
|
||||
|
||||
override fun contentColor(): Color = SessionUiStyle.View.surface()
|
||||
override fun contentColor(): Color = SessionUiStyle.View.Surface.bgColor()
|
||||
|
||||
override fun outlineColor(): Color = SessionUiStyle.View.line()
|
||||
override fun outlineColor(): Color = SessionUiStyle.View.Outline.brightColor()
|
||||
|
||||
// ---- private helpers ----
|
||||
|
||||
@@ -285,7 +297,7 @@ class BaseQuestionView(
|
||||
north.removeAll()
|
||||
top?.let { north.next(it) }
|
||||
north.next(header)
|
||||
if (content != null) north.fill(UiStyle.Gap.md())
|
||||
if (content != null) north.fill(gap)
|
||||
north.revalidate()
|
||||
north.repaint()
|
||||
}
|
||||
@@ -383,7 +395,7 @@ class BaseQuestionView(
|
||||
}
|
||||
|
||||
private fun syncBackground() {
|
||||
background = SessionUiStyle.View.surface()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
}
|
||||
}
|
||||
btn.addActionListener { actionHandlers[id]?.invoke() }
|
||||
|
||||
@@ -26,6 +26,7 @@ class GenericView private constructor(
|
||||
label.foreground = UiStyle.Colors.weak()
|
||||
applyStyle(SessionEditorStyle.current())
|
||||
syncExpandable(false)
|
||||
border = null
|
||||
}
|
||||
|
||||
override fun update(content: Content) {} // generic content has no updatable state
|
||||
|
||||
@@ -4,6 +4,7 @@ import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import com.intellij.openapi.Disposable
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,10 @@ abstract class PartView : JPanel(), Disposable, SessionEditorStyleTarget {
|
||||
/** Stable [Content.id] this renderer was created for. */
|
||||
abstract val contentId: String
|
||||
|
||||
var resize: ((JComponent, () -> Unit) -> Unit)? = null
|
||||
|
||||
var hover: ((PartView, Boolean) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Apply a full content update — replace, not append.
|
||||
* Called when [ai.kilocode.client.session.model.SessionModelEvent.ContentUpdated] fires.
|
||||
@@ -33,6 +38,8 @@ abstract class PartView : JPanel(), Disposable, SessionEditorStyleTarget {
|
||||
*/
|
||||
open fun appendDelta(delta: String) {}
|
||||
|
||||
open fun setHovered(value: Boolean) {}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {}
|
||||
|
||||
override fun dispose() {}
|
||||
|
||||
@@ -2,7 +2,6 @@ package ai.kilocode.client.session.views.base
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import javax.swing.JComponent
|
||||
|
||||
abstract class PrimarySessionPartView(
|
||||
@@ -13,20 +12,36 @@ abstract class PrimarySessionPartView(
|
||||
) : AbstractSessionPartView(header, content, expanded, expandable) {
|
||||
init {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = SessionUiStyle.View.sessionView()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
row.isOpaque = true
|
||||
row.background = SessionUiStyle.View.header()
|
||||
row.background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
row.border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header()
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun applyHover(value: Boolean, color: Color) {
|
||||
border = if (value) SessionUiStyle.View.sessionView(SessionUiStyle.View.hoverLine()) else SessionUiStyle.View.sessionView()
|
||||
repaint()
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width())
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,34 @@ abstract class SecondarySessionPartView(
|
||||
) : this(header, { content }, expanded, expandable)
|
||||
init {
|
||||
row.isOpaque = true
|
||||
row.background = SessionUiStyle.View.header()
|
||||
row.background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
row.border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header()
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun collapse(): Boolean {
|
||||
val changed = super.collapse()
|
||||
if (changed) syncBorder()
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun hoverColor(value: Boolean) =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
border = JBUI.Borders.customLine(SessionUiStyle.View.Outline.color(), SessionUiStyle.View.Outline.width())
|
||||
return
|
||||
}
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle.View.SESSION_VIEW_GAP
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.ColorUtil
|
||||
@@ -68,7 +67,7 @@ class PermissionView(
|
||||
isOpaque = false
|
||||
isVisible = false
|
||||
|
||||
card.setHeaderIcon(AllIcons.General.Warning, KiloBundle.message("session.permission.title"))
|
||||
card.setHeaderIcon(SessionViewIcons.warning, KiloBundle.message("session.permission.title"))
|
||||
card.setContent(body)
|
||||
card.setActions(listOf(
|
||||
BaseQuestionView.Action(ID_DENY, KiloBundle.message("session.permission.deny"), primary = false) { decide("reject") },
|
||||
@@ -128,7 +127,7 @@ class PermissionView(
|
||||
|
||||
/** Adds a three-column permission detail row: tool, target, and changes. */
|
||||
private fun addDetailRow(action: String, target: String?, diffs: List<PermissionFileDiff>) {
|
||||
val row = JPanel(BorderLayout(SESSION_VIEW_GAP, 0)).apply {
|
||||
val row = JPanel(BorderLayout(SessionUiStyle.View.Layout.GAP, 0)).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
@@ -174,7 +173,7 @@ class PermissionView(
|
||||
private fun applyTargetPane(pane: JBHtmlPane) {
|
||||
pane.font = style.transcriptFont
|
||||
pane.foreground = style.editorForeground
|
||||
pane.background = SessionUiStyle.View.headerHover()
|
||||
pane.background = SessionUiStyle.View.Surface.headerHoverBgColor()
|
||||
pane.reloadCssStylesheets()
|
||||
}
|
||||
|
||||
@@ -182,7 +181,7 @@ class PermissionView(
|
||||
val sheet = StyleSheet()
|
||||
val font = style.transcriptFont
|
||||
val fg = ColorUtil.toHtmlColor(style.editorForeground)
|
||||
val bg = ColorUtil.toHtmlColor(SessionUiStyle.View.headerHover())
|
||||
val bg = ColorUtil.toHtmlColor(SessionUiStyle.View.Surface.headerHoverBgColor())
|
||||
val family = font.name.replace("\\", "\\\\").replace("'", "\\'")
|
||||
sheet.addRule("body { margin: 0; padding: 0 ${UiStyle.Gap.xs()}px; color: $fg; background: $bg; font-family: '$family', monospace; font-size: ${font.size}pt }")
|
||||
sheet.addRule("pre { margin: 0; white-space: pre-wrap; font-family: '$family', monospace; font-size: ${font.size}pt }")
|
||||
|
||||
@@ -6,10 +6,10 @@ import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.session.views.ToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBLabel
|
||||
@@ -41,26 +41,26 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = SessionUiStyle.View.sessionView()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.empty(1)
|
||||
}
|
||||
}
|
||||
private val header = object : JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)) {
|
||||
private val header = object : JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)) {
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.header()
|
||||
background = SessionUiStyle.View.Surface.headerBgColor()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
}
|
||||
private val glyph = JBLabel(AllIcons.General.Balloon)
|
||||
private val glyph = JBLabel(SessionViewIcons.bubble)
|
||||
private val title = JBLabel()
|
||||
private val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
private val arrow = JBLabel()
|
||||
private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply {
|
||||
private val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
private var pane: JPanel? = null
|
||||
@@ -70,10 +70,10 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
}
|
||||
|
||||
private val mouse = object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) { setHover(true) }
|
||||
override fun mouseEntered(e: MouseEvent) { setHovered(true) }
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
if (inside(e)) return
|
||||
setHover(false)
|
||||
setHovered(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
add(root, BorderLayout.CENTER)
|
||||
syncLabels()
|
||||
syncArrow()
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
override fun update(content: Content) {
|
||||
@@ -122,13 +123,18 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
resize?.invoke(this) { toggleBody() } ?: toggleBody()
|
||||
syncArrow()
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun toggleBody() {
|
||||
if (isExpanded()) {
|
||||
pane?.let { root.remove(it) }
|
||||
} else {
|
||||
root.add(body(), BorderLayout.CENTER)
|
||||
}
|
||||
syncArrow()
|
||||
refresh()
|
||||
syncBorder()
|
||||
}
|
||||
|
||||
fun isExpanded(): Boolean = pane?.parent === root
|
||||
@@ -164,10 +170,19 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.surface()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.SESSION_VIEW_HORIZONTAL_PADDING),
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.brightColor(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.apply {
|
||||
@@ -270,16 +285,28 @@ class QuestionResultView(tool: Tool, private val selection: SessionSelection? =
|
||||
}
|
||||
|
||||
private fun syncArrow() {
|
||||
arrow.icon = if (isExpanded()) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
|
||||
arrow.icon = if (isExpanded()) SessionViewIcons.chevronDown else SessionViewIcons.chevronRight
|
||||
}
|
||||
|
||||
private fun setHover(value: Boolean) {
|
||||
val color = if (value) SessionUiStyle.View.headerHover() else SessionUiStyle.View.header()
|
||||
if (header.background?.rgb == color.rgb) return
|
||||
header.background = color
|
||||
root.border = if (value) SessionUiStyle.View.sessionView(SessionUiStyle.View.hoverLine()) else SessionUiStyle.View.sessionView()
|
||||
header.repaint()
|
||||
root.repaint()
|
||||
override fun setHovered(value: Boolean) {
|
||||
hover?.invoke(this, value)
|
||||
val color =
|
||||
if (value) SessionUiStyle.View.Surface.headerHoverBgColor() else SessionUiStyle.View.Surface.headerBgColor()
|
||||
if (header.background?.rgb != color.rgb) {
|
||||
header.background = color
|
||||
header.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncBorder() {
|
||||
if (isExpanded()) {
|
||||
root.border = JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.brightColor(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
)
|
||||
return
|
||||
}
|
||||
root.border = JBUI.Borders.empty(1)
|
||||
}
|
||||
|
||||
private fun inside(e: MouseEvent): Boolean {
|
||||
|
||||
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.QuestionItem
|
||||
import ai.kilocode.client.session.model.QuestionOption
|
||||
import ai.kilocode.client.session.ui.SessionView
|
||||
import ai.kilocode.client.session.ui.editor.SessionEditorTextField
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
@@ -13,7 +14,6 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -79,14 +79,14 @@ class QuestionView(
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
}
|
||||
private val back = HoverIcon().apply {
|
||||
val ico = AllIcons.Actions.Back
|
||||
val ico = SessionViewIcons.chevronLeft
|
||||
icon = ico
|
||||
disabledIcon = IconLoader.getDisabledIcon(ico)
|
||||
toolTipText = KiloBundle.message("session.question.back")
|
||||
addActionListener { goBack() }
|
||||
}
|
||||
private val fwd = HoverIcon().apply {
|
||||
val ico = AllIcons.Actions.Forward
|
||||
val ico = SessionViewIcons.chevronRight
|
||||
icon = ico
|
||||
disabledIcon = IconLoader.getDisabledIcon(ico)
|
||||
toolTipText = KiloBundle.message("session.question.next")
|
||||
@@ -94,7 +94,7 @@ class QuestionView(
|
||||
}
|
||||
private val topPanel = JPanel(BorderLayout()).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.lg())
|
||||
border = JBUI.Borders.empty()
|
||||
alignmentX = Component.LEFT_ALIGNMENT
|
||||
}
|
||||
private val body = JPanel().apply {
|
||||
@@ -207,6 +207,12 @@ class QuestionView(
|
||||
summary.isVisible = total > 1
|
||||
nav.isVisible = total > 1
|
||||
topPanel.isVisible = total > 1
|
||||
if (total > 1) {
|
||||
topPanel.border = JBUI.Borders.empty(0, 0, UiStyle.Gap.sm(), 0)
|
||||
card.setSpacing(UiStyle.Gap.sm(), UiStyle.Gap.pad())
|
||||
return
|
||||
}
|
||||
card.setSpacing(UiStyle.Gap.xl(), UiStyle.Gap.pad())
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
|
||||
@@ -6,9 +6,9 @@ import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.PrimarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
@@ -28,7 +28,13 @@ class TodoWriteView(tool: Tool, private val parts: TodoParts = todoParts()) :
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.center, parts.controls)
|
||||
parts.list.border = JBUI.Borders.compound(
|
||||
SessionUiStyle.View.topOutline(),
|
||||
JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.md()),
|
||||
)
|
||||
applyStyle(style)
|
||||
@@ -92,16 +98,16 @@ class TodoParts(
|
||||
)
|
||||
|
||||
private fun todoParts(): TodoParts {
|
||||
val glyph = JBLabel(AllIcons.Actions.Checked)
|
||||
val glyph = JBLabel(SessionViewIcons.checklist)
|
||||
val title = JBLabel(KiloBundle.message("session.part.todo.title"))
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply {
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
add(title, BorderLayout.WEST)
|
||||
add(sub, BorderLayout.CENTER)
|
||||
}
|
||||
val controls = Box.createHorizontalBox()
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.SESSION_VIEW_GAP), 0)).apply {
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
add(glyph, BorderLayout.WEST)
|
||||
add(center, BorderLayout.CENTER)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.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
|
||||
|
||||
abstract class BaseSearchToolView(
|
||||
tool: Tool,
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts,
|
||||
private val repo: String? = null,
|
||||
) : SecondarySessionPartView(parts.header, { parts.scroll(tool) }) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
protected var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var registered = false
|
||||
private var disposed = false
|
||||
|
||||
protected abstract fun toolIcon(tool: Tool): Icon
|
||||
protected abstract fun toolTitle(tool: Tool): String
|
||||
protected abstract fun targets(tool: Tool, repo: String?): List<String>
|
||||
protected abstract fun viewName(): String
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
parts.targets.forEach { bindHeader(it) }
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
syncBody()
|
||||
applyBodyStyle()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
item = content
|
||||
var changed = sync()
|
||||
changed = syncBody() || changed
|
||||
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
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
parts.targets.forEach { changed = setFont(it, style.regularFont) || changed }
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = applyBodyStyle() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
val expand = canExpand(item)
|
||||
var changed = false
|
||||
changed = syncExpandable(expand) || changed
|
||||
changed = setVisible(parts.state, item.state != ToolExecState.COMPLETED) || changed
|
||||
changed = setIcon(parts.glyph, toolIcon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, toolTitle(item)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.sub, UiStyle.Colors.weak()) || changed
|
||||
changed = syncTargets() || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
val body = parts.content
|
||||
if (body != null && body.foreground != bodyColor()) {
|
||||
body.foreground = bodyColor()
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncTargets(): Boolean {
|
||||
val values = targets(item, repo)
|
||||
var changed = false
|
||||
parts.targets.forEachIndexed { index, label ->
|
||||
val text = values.getOrNull(index) ?: ""
|
||||
changed = setVisible(label, text.isNotBlank()) || changed
|
||||
changed = setTargetText(label, text) || changed
|
||||
changed = setForeground(label, UiStyle.Colors.fg()) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
val body = parts.content ?: return false
|
||||
val value = plainBody(item)
|
||||
if (body.text != value) {
|
||||
body.text = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun applyBodyStyle(): Boolean {
|
||||
val body = parts.content ?: return false
|
||||
if (!disposed) {
|
||||
Disposer.register(this, body)
|
||||
disposed = true
|
||||
}
|
||||
if (!registered && selection != null && parts.scroll?.parent != null) {
|
||||
registered = true
|
||||
body.register(selection, this)
|
||||
}
|
||||
return body.applyStyle(style)
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val body = parts.content ?: return 0
|
||||
return body.lineHeight() * SessionUiStyle.View.Tool.BODY_LINES +
|
||||
JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "${viewName()}#$contentId(${labelText()})"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
|
||||
/** Renders glob calls with a stacked, collapsible search-result header. */
|
||||
class GlobToolView(
|
||||
tool: Tool,
|
||||
selection: SessionSelection? = null,
|
||||
parts: ToolParts = searchParts(2),
|
||||
repo: String? = null,
|
||||
) : BaseSearchToolView(tool, selection, parts, repo) {
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = tool.name == "glob"
|
||||
}
|
||||
|
||||
override fun toolIcon(tool: Tool) = icon(tool)
|
||||
override fun toolTitle(tool: Tool) = KiloBundle.message("session.part.tool.glob")
|
||||
override fun targets(tool: Tool, repo: String?) = listOf(globDirectory(tool, repo), globPattern(tool)).filter { it.isNotBlank() }
|
||||
override fun viewName() = "GlobToolView"
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.ToolKind
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Dimension
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
/** Renders read calls with secondary, borderless chrome. */
|
||||
class ReadToolView(
|
||||
tool: Tool,
|
||||
openFile: (String) -> Unit = {},
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool, openFile),
|
||||
) : SecondarySessionPartView(parts.header, parts.scroll(tool), expandable = false) {
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = tool.kind == ToolKind.READ
|
||||
}
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
|
||||
init {
|
||||
parts.text?.let { selection?.register(it, this) }
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
parts.text?.text = preview(item)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
item = content
|
||||
var changed = sync()
|
||||
changed = syncBody() || changed
|
||||
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
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.transcriptFont) || changed
|
||||
changed = setFont(parts.link, style.transcriptFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
parts.text?.let { changed = setFont(it, style.transcriptFont) || changed }
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
var changed = false
|
||||
changed = syncExpandable(false) || changed
|
||||
changed = setVisible(parts.state, true) || changed
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = syncSubtitle() || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setForeground(parts.sub, UiStyle.Colors.fg()) || changed
|
||||
changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
parts.text?.let { changed = setForeground(it, bodyColor()) || changed }
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncSubtitle(): Boolean {
|
||||
val target = target(item)?.takeIf { it.type == "file" }
|
||||
if (target != null) {
|
||||
var changed = false
|
||||
if (parts.href != target.path) {
|
||||
parts.href = target.path
|
||||
changed = true
|
||||
}
|
||||
changed = setLinkText(parts, tail(target.path).ifBlank { target.path }) || changed
|
||||
changed = show(parts, true) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
var changed = false
|
||||
if (parts.href != null) {
|
||||
parts.href = null
|
||||
changed = true
|
||||
}
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = show(parts, false) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
val value = preview(item)
|
||||
val text = parts.text ?: return false
|
||||
if (text.text == value) return false
|
||||
text.text = value
|
||||
text.caretPosition = 0
|
||||
return true
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val text = parts.text ?: return 0
|
||||
return text.getFontMetrics(text.font).height * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "ReadToolView#$contentId(${labelText()})"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
|
||||
/** Renders grep/content-search calls with stacked, clipped search targets. */
|
||||
class SearchToolView(
|
||||
tool: Tool,
|
||||
selection: SessionSelection? = null,
|
||||
parts: ToolParts = searchParts(3),
|
||||
repo: String? = null,
|
||||
) : BaseSearchToolView(tool, selection, parts, repo) {
|
||||
|
||||
companion object {
|
||||
fun canRender(tool: Tool): Boolean = tool.name == "grep"
|
||||
}
|
||||
|
||||
override fun toolIcon(tool: Tool) = SessionViewIcons.search
|
||||
override fun toolTitle(tool: Tool) = KiloBundle.message("session.part.tool.search")
|
||||
override fun targets(tool: Tool, repo: String?) = searchTargets(tool, repo)
|
||||
override fun viewName() = "SearchToolView"
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.io.OSAgnosticPathUtil
|
||||
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.Font
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
private val LOG = KiloLog.create(ToolParts::class.java)
|
||||
|
||||
enum class ToolBodyMode { EDITOR, TEXT }
|
||||
|
||||
class ToolParts(
|
||||
val header: JPanel,
|
||||
val glyph: JBLabel,
|
||||
val title: JBLabel,
|
||||
val sub: JBLabel,
|
||||
val link: JBLabel,
|
||||
val slot: JPanel,
|
||||
val state: JBLabel,
|
||||
val center: JPanel,
|
||||
val controls: JComponent,
|
||||
private val open: ((String) -> Unit)? = null,
|
||||
val extra: JBLabel? = null,
|
||||
val targets: List<JBLabel> = emptyList(),
|
||||
private val mode: ToolBodyMode = ToolBodyMode.EDITOR,
|
||||
) {
|
||||
var href: String? = null
|
||||
var label: String = ""
|
||||
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
|
||||
val body = when (mode) {
|
||||
ToolBodyMode.EDITOR -> ToolBody.editor(tool)
|
||||
ToolBodyMode.TEXT -> ToolBody.text(tool)
|
||||
}
|
||||
return body.also { this.body = it }
|
||||
}
|
||||
}
|
||||
|
||||
class ToolBody private constructor(
|
||||
val area: JBTextArea?,
|
||||
val ed: EditorTextField?,
|
||||
val scroll: JBScrollPane,
|
||||
private val disposable: Disposable?,
|
||||
) : Disposable {
|
||||
var text: String
|
||||
@RequiresEdt
|
||||
get() = area?.text ?: ed?.text ?: ""
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
if (text == value) return
|
||||
area?.text = value
|
||||
ed?.text = value
|
||||
caretStart()
|
||||
size()
|
||||
}
|
||||
|
||||
var font: Font
|
||||
@RequiresEdt
|
||||
get() = area?.font ?: ed?.font ?: SessionEditorStyle.current().editorFont
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
area?.font = value
|
||||
ed?.font = value
|
||||
size()
|
||||
}
|
||||
|
||||
var foreground: Color
|
||||
@RequiresEdt
|
||||
get() = area?.foreground ?: ed?.foreground ?: UiStyle.Colors.fg()
|
||||
@RequiresEdt
|
||||
set(value) {
|
||||
area?.foreground = value
|
||||
ed?.foreground = value
|
||||
}
|
||||
|
||||
val editable: Boolean get() = area?.isEditable ?: false
|
||||
val caretVisible: Boolean get() = area?.caret?.isVisible ?: false
|
||||
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
|
||||
ed?.font = style.editorFont
|
||||
ed?.getEditor(false)?.let(style::applyToEditor)
|
||||
size()
|
||||
return before != font
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun register(selection: SessionSelection, parent: Disposable) {
|
||||
val field = ed
|
||||
if (field != null) {
|
||||
selection.register(field, parent)
|
||||
return
|
||||
}
|
||||
area?.let { selection.register(it, parent) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun lineHeight(): Int = ed?.getEditor(false)?.lineHeight ?: scroll.viewport.view.getFontMetrics(font).height
|
||||
|
||||
override fun dispose() {
|
||||
disposable?.let(Disposer::dispose)
|
||||
}
|
||||
|
||||
private fun size() {
|
||||
val view = scroll.viewport.view as? JComponent ?: return
|
||||
val height = height(view)
|
||||
val width = width(view)
|
||||
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 = JBUI.size(0, pane)
|
||||
scroll.minimumSize = JBUI.size(0, pane)
|
||||
scroll.maximumSize = JBDimension(Int.MAX_VALUE, pane)
|
||||
}
|
||||
|
||||
private fun width(view: JComponent): Int {
|
||||
val metrics = view.getFontMetrics(font)
|
||||
return (text.lineSequence().maxOfOrNull { metrics.stringWidth(it) } ?: 0) +
|
||||
JBUI.scale(SessionUiStyle.View.Code.WIDTH_PADDING)
|
||||
}
|
||||
|
||||
private fun height(view: JComponent): Int {
|
||||
ed?.ensureWillComputePreferredSize()
|
||||
val rows = text.lineSequence().count().coerceAtLeast(SessionUiStyle.View.Code.MIN_ROWS)
|
||||
return maxOf(view.preferredSize.height, lineHeight() * rows)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@RequiresEdt
|
||||
fun editor(tool: Tool): ToolBody {
|
||||
val disposable = Disposer.newDisposable("Tool body")
|
||||
val body = runCatching {
|
||||
val field = ToolField(preview(tool), SessionEditorStyle.current()).also { ed ->
|
||||
ed.setDisposedWith(disposable)
|
||||
Disposer.register(disposable) {
|
||||
ed.getEditor(false)?.let(EditorFactory.getInstance()::releaseEditor)
|
||||
}
|
||||
}
|
||||
ToolBody(null, field, pane(field, true), disposable)
|
||||
}.getOrElse { err ->
|
||||
LOG.warn("kind=tool codeEditor=true failed message=${err.message}", err)
|
||||
val area = area(tool, false)
|
||||
ToolBody(area, null, pane(area, true), disposable)
|
||||
}
|
||||
body.size()
|
||||
return body
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun text(tool: Tool): ToolBody {
|
||||
val area = area(tool, true)
|
||||
val body = ToolBody(area, null, pane(area, false), null)
|
||||
body.size()
|
||||
return body
|
||||
}
|
||||
|
||||
private fun area(tool: Tool, wrap: Boolean) = JBTextArea().apply {
|
||||
isEditable = false
|
||||
caret.isVisible = false
|
||||
caret.isSelectionVisible = true
|
||||
lineWrap = wrap
|
||||
wrapStyleWord = wrap
|
||||
foreground = if (tool.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
|
||||
private fun pane(view: JComponent, scrolls: Boolean) = JBScrollPane(view).apply {
|
||||
border = JBUI.Borders.customLine(
|
||||
SessionUiStyle.View.Outline.color(),
|
||||
SessionUiStyle.View.Outline.width(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
viewportBorder = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Layout.VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING),
|
||||
).takeIf { scrolls }
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
viewport.background = SessionUiStyle.View.Surface.bgColor()
|
||||
horizontalScrollBarPolicy = if (scrolls) {
|
||||
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED
|
||||
} else {
|
||||
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
}
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ToolField(value: String, private var style: SessionEditorStyle) : EditorTextField(
|
||||
EditorFactory.getInstance().createDocument(value.trimEnd('\n')),
|
||||
ProjectManager.getInstance().defaultProject,
|
||||
PlainTextFileType.INSTANCE,
|
||||
true,
|
||||
false,
|
||||
) {
|
||||
init {
|
||||
setFontInheritedFromLAF(false)
|
||||
font = style.editorFont
|
||||
addSettingsProvider { ed ->
|
||||
style.applyToEditor(ed)
|
||||
ed.setBorder(JBUI.Borders.empty())
|
||||
ed.scrollPane.border = JBUI.Borders.empty()
|
||||
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
|
||||
ed.backgroundColor = SessionUiStyle.View.Surface.bgColor()
|
||||
ed.scrollPane.background = SessionUiStyle.View.Surface.bgColor()
|
||||
ed.scrollPane.viewport.background = SessionUiStyle.View.Surface.bgColor()
|
||||
ed.settings.isUseSoftWraps = false
|
||||
ed.settings.isAdditionalPageAtBottom = false
|
||||
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUB_CARD = "sub"
|
||||
private const val LINK_CARD = "link"
|
||||
|
||||
@RequiresEdt
|
||||
internal fun toolParts(
|
||||
tool: Tool,
|
||||
openFile: ((String) -> Unit)? = null,
|
||||
mode: ToolBodyMode = ToolBodyMode.TEXT,
|
||||
): ToolParts {
|
||||
lateinit var parts: ToolParts
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val link = JBLabel().apply {
|
||||
isVisible = false
|
||||
isFocusable = false
|
||||
foreground = UiStyle.Colors.fg()
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
setRequestFocusEnabled(false)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
parts.openLink()
|
||||
}
|
||||
})
|
||||
}
|
||||
val slot = JPanel(CardLayout()).apply {
|
||||
isOpaque = false
|
||||
add(sub, SUB_CARD)
|
||||
add(link, LINK_CARD)
|
||||
}
|
||||
val state = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply { isOpaque = false }
|
||||
val controls = Stack.horizontal()
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
center.add(title, BorderLayout.WEST)
|
||||
center.add(slot, BorderLayout.CENTER)
|
||||
add(glyph, BorderLayout.WEST)
|
||||
add(center, BorderLayout.CENTER)
|
||||
add(controls, BorderLayout.EAST)
|
||||
}
|
||||
parts = ToolParts(header, glyph, title, sub, link, slot, state, center, controls, openFile, mode = mode)
|
||||
return parts.also {
|
||||
controls.add(it.state)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun searchParts(count: Int): ToolParts {
|
||||
val glyph = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val targets = List(count) {
|
||||
JBLabel().apply {
|
||||
foreground = UiStyle.Colors.fg()
|
||||
minimumSize = JBUI.size(0, minimumSize.height)
|
||||
}
|
||||
}
|
||||
val link = JBLabel().apply { isVisible = false }
|
||||
val slot = JPanel(CardLayout()).apply {
|
||||
isOpaque = false
|
||||
add(sub, SUB_CARD)
|
||||
add(link, LINK_CARD)
|
||||
}
|
||||
val state = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val stack = Stack.fitHorizontal(UiStyle.Gap.md()).apply { targets.forEach { next(it) } }
|
||||
val target = stack.align(HAlign.TRACK, VAlign.CENTER)
|
||||
val center = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
minimumSize = JBUI.size(0, minimumSize.height)
|
||||
add(title, BorderLayout.WEST)
|
||||
add(target, BorderLayout.CENTER)
|
||||
}
|
||||
val controls = Stack.horizontal()
|
||||
val header = JPanel(BorderLayout(JBUI.scale(SessionUiStyle.View.Layout.GAP), 0)).apply {
|
||||
isOpaque = false
|
||||
add(glyph, BorderLayout.WEST)
|
||||
add(center, BorderLayout.CENTER)
|
||||
add(controls, BorderLayout.EAST)
|
||||
}
|
||||
return ToolParts(header, glyph, title, sub, link, slot, state, center, controls, targets = targets, mode = ToolBodyMode.EDITOR).also {
|
||||
controls.add(it.state)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun icon(tool: Tool) = when (tool.name) {
|
||||
"read" -> SessionViewIcons.glasses
|
||||
"list" -> SessionViewIcons.bulletList
|
||||
"glob", "grep" -> SessionViewIcons.search
|
||||
"webfetch", "websearch" -> SessionViewIcons.windowCursor
|
||||
"codesearch" -> SessionViewIcons.code
|
||||
"task" -> SessionViewIcons.task
|
||||
"bash" -> SessionViewIcons.console
|
||||
"edit", "write", "apply_patch" -> SessionViewIcons.codeLines
|
||||
"todowrite", "todoread" -> SessionViewIcons.checklist
|
||||
"question" -> SessionViewIcons.bubble
|
||||
"skill" -> SessionViewIcons.brain
|
||||
else -> SessionViewIcons.mcp
|
||||
}
|
||||
|
||||
internal fun title(tool: Tool) = when (tool.name) {
|
||||
"read" -> KiloBundle.message("session.part.tool.read")
|
||||
"bash" -> KiloBundle.message("session.part.tool.shell")
|
||||
else -> toolTitle(tool)
|
||||
}
|
||||
|
||||
internal fun subtitle(tool: Tool) = when (tool.name) {
|
||||
"read" -> readPath(tool)
|
||||
"bash" -> shellTitle(tool)
|
||||
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
|
||||
label.text = value
|
||||
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
|
||||
parts.label = text
|
||||
parts.link.text = value
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
return true
|
||||
}
|
||||
|
||||
private fun same(a: Color?, b: Color): Boolean = a?.rgb == b.rgb
|
||||
|
||||
internal fun color(tool: Tool) = when (tool.state) {
|
||||
ToolExecState.PENDING -> SessionUiStyle.View.Tool.pending()
|
||||
ToolExecState.RUNNING -> SessionUiStyle.View.Tool.running()
|
||||
ToolExecState.COMPLETED -> SessionUiStyle.View.Tool.completed()
|
||||
ToolExecState.ERROR -> SessionUiStyle.View.Tool.error()
|
||||
}
|
||||
|
||||
internal fun titleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) {
|
||||
UiStyle.Colors.errorLabelForeground()
|
||||
} else {
|
||||
UiStyle.Colors.fg()
|
||||
}
|
||||
|
||||
internal fun stateText(tool: Tool) = when (tool.state) {
|
||||
ToolExecState.PENDING -> KiloBundle.message("session.part.tool.pending")
|
||||
ToolExecState.RUNNING -> KiloBundle.message("session.part.tool.running")
|
||||
ToolExecState.COMPLETED -> ""
|
||||
ToolExecState.ERROR -> KiloBundle.message("session.part.tool.error")
|
||||
}
|
||||
|
||||
private fun readPath(tool: Tool): String {
|
||||
val target = target(tool)
|
||||
if (target != null) {
|
||||
if (target.type == "file") return tail(target.path).ifBlank { target.path }
|
||||
return target.path
|
||||
}
|
||||
val path = tool.input["filePath"] ?: tool.input["path"] ?: tool.title ?: return tool.name
|
||||
return tail(path).ifBlank { path }
|
||||
}
|
||||
|
||||
internal fun searchPath(path: String, repo: String?): String {
|
||||
val text = path.takeIf { it.isNotBlank() } ?: return ""
|
||||
val root = repo?.takeIf { it.isNotBlank() }?.let(::norm)
|
||||
if (root == null) return text.takeUnless { it == "." } ?: ""
|
||||
val full = if (OSAgnosticPathUtil.isAbsolute(text)) norm(text) else norm(FileUtil.join(root, text))
|
||||
if (full == root) return ""
|
||||
if (!OSAgnosticPathUtil.startsWith(full, root)) return full
|
||||
return FileUtil.getRelativePath(root, full, '/') ?: full
|
||||
}
|
||||
|
||||
private fun norm(path: String): String = FileUtil.toCanonicalPath(FileUtil.toSystemIndependentName(path), '/', true)
|
||||
|
||||
internal fun globDirectory(tool: Tool, repo: String?): String =
|
||||
searchPath(
|
||||
tool.input["path"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.title?.takeIf { it.isNotBlank() }
|
||||
?: "",
|
||||
repo,
|
||||
)
|
||||
|
||||
internal fun globPattern(tool: Tool): String =
|
||||
tool.input["pattern"]?.takeIf { it.isNotBlank() }?.let { "pattern=$it" } ?: ""
|
||||
|
||||
internal fun searchTargets(tool: Tool, repo: String?): List<String> = listOfNotNull(
|
||||
tool.input["path"]?.takeIf { it.isNotBlank() }?.let { searchPath(it, repo) }?.takeIf { it.isNotBlank() },
|
||||
tool.input["pattern"]?.takeIf { it.isNotBlank() }?.let { "pattern=$it" },
|
||||
tool.input["include"]?.takeIf { it.isNotBlank() }?.let { "include=$it" },
|
||||
)
|
||||
|
||||
internal data class Target(
|
||||
val path: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
internal fun target(tool: Tool): Target? {
|
||||
val out = output(tool)
|
||||
if (out.isBlank()) return null
|
||||
val path = tag(out, "path") ?: return null
|
||||
val type = tag(out, "type") ?: return null
|
||||
return Target(path, type.lowercase())
|
||||
}
|
||||
|
||||
private fun tag(text: String, name: String): String? =
|
||||
Regex("<$name>\\s*([\\s\\S]*?)\\s*</$name>")
|
||||
.find(text)
|
||||
?.groupValues
|
||||
?.getOrNull(1)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun shellTitle(tool: Tool): String =
|
||||
tool.input["description"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["description"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.title?.takeIf { it.isNotBlank() }
|
||||
?: command(tool).lineSequence().firstOrNull { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
internal fun command(tool: Tool): String =
|
||||
tool.input["command"]?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["command"]?.takeIf { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
internal fun output(tool: Tool): String =
|
||||
tool.output?.takeIf { it.isNotBlank() }
|
||||
?: tool.metadata["output"]?.takeIf { it.isNotBlank() }
|
||||
?: ""
|
||||
|
||||
internal fun preview(tool: Tool): String = if (tool.name == "bash") shellPreview(tool) else plainPreview(tool)
|
||||
|
||||
internal fun body(tool: Tool): String = if (tool.name == "bash") shellBody(tool) else plainBody(tool)
|
||||
|
||||
private fun shellPreview(tool: Tool): String {
|
||||
val cmd = command(tool)
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return Preview().apply {
|
||||
if (cmd.isNotBlank()) append("$ ").append(cmd)
|
||||
if (out.isNotBlank()) {
|
||||
sep()
|
||||
append(out)
|
||||
}
|
||||
if (err != null) {
|
||||
sep()
|
||||
append(err)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun shellBody(tool: Tool): String {
|
||||
val cmd = command(tool)
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return buildString {
|
||||
if (cmd.isNotBlank()) append("$ ").append(cmd)
|
||||
if (out.isNotBlank()) {
|
||||
if (isNotEmpty()) append("\n\n")
|
||||
append(out)
|
||||
}
|
||||
if (err != null) {
|
||||
if (isNotEmpty()) append("\n\n")
|
||||
append(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun plainPreview(tool: Tool): String {
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return Preview().apply {
|
||||
if (out.isNotBlank()) append(out)
|
||||
if (err != null) {
|
||||
sep()
|
||||
append(err)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
internal fun plainBody(tool: Tool): String {
|
||||
val out = output(tool)
|
||||
val err = tool.error?.takeIf { it.isNotBlank() }
|
||||
return listOf(out, err).filter { !it.isNullOrBlank() }.joinToString("\n\n")
|
||||
}
|
||||
|
||||
internal fun canExpand(tool: Tool): Boolean {
|
||||
if (tool.name == "bash") return command(tool).isNotBlank() || output(tool).isNotBlank() || !tool.error.isNullOrBlank()
|
||||
return output(tool).isNotBlank() || !tool.error.isNullOrBlank()
|
||||
}
|
||||
|
||||
private fun toolTitle(tool: Tool): String =
|
||||
tool.title?.takeIf { it.isNotBlank() }
|
||||
?: tool.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
|
||||
private fun toolSubtitle(tool: Tool): String {
|
||||
val base = listOf("description", "query", "url", "filePath", "path", "name")
|
||||
.mapNotNull { tool.input[it]?.takeIf { value -> value.isNotBlank() } }
|
||||
.firstOrNull()
|
||||
val args = listOf("pattern", "include", "offset", "limit")
|
||||
.mapNotNull { key -> tool.input[key]?.takeIf { it.isNotBlank() }?.let { "$key=$it" } }
|
||||
return listOfNotNull(base).plus(args).joinToString(" ")
|
||||
}
|
||||
|
||||
internal fun tail(path: String): String {
|
||||
val value = path.trimEnd('/', '\\')
|
||||
val index = maxOf(value.lastIndexOf('/'), value.lastIndexOf('\\'))
|
||||
if (index < 0) return value
|
||||
return value.substring(index + 1)
|
||||
}
|
||||
|
||||
private class Preview {
|
||||
private val text = StringBuilder()
|
||||
private var cut = false
|
||||
|
||||
fun append(value: String): Preview {
|
||||
if (cut) return this
|
||||
val rem = SessionUiStyle.View.Tool.PREVIEW_LIMIT - text.length
|
||||
if (value.length <= rem) {
|
||||
text.append(value)
|
||||
return this
|
||||
}
|
||||
if (rem > 0) text.append(value, 0, rem)
|
||||
cut = true
|
||||
return this
|
||||
}
|
||||
|
||||
fun sep(): Preview {
|
||||
if (text.isNotEmpty()) append("\n\n")
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
if (!cut) return text.toString()
|
||||
if (text.isNotEmpty()) text.append("\n\n")
|
||||
text.append(KiloBundle.message("session.part.tool.truncated"))
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.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
|
||||
|
||||
/** Renders non-read tool calls with VS Code-inspired rows/cards. */
|
||||
class ToolView(
|
||||
tool: Tool,
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool, mode = ToolBodyMode.EDITOR),
|
||||
) : SecondarySessionPartView(parts.header, { parts.scroll(tool) }) {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
private var registered = false
|
||||
private var disposed = false
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
val changed = super.expand()
|
||||
if (!changed) return false
|
||||
syncBody()
|
||||
applyBodyStyle()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
val was = item.name
|
||||
item = content
|
||||
var changed = false
|
||||
if (was != content.name || !canExpand(content)) changed = collapse() || changed
|
||||
changed = sync() || changed
|
||||
changed = syncBody() || changed
|
||||
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
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.link, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
changed = applyBodyStyle() || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
val expand = canExpand(item)
|
||||
var changed = false
|
||||
changed = syncExpandable(expand) || changed
|
||||
changed = setVisible(parts.state, !expand) || changed
|
||||
changed = syncLabels() || changed
|
||||
val body = parts.content
|
||||
if (body != null && body.foreground != bodyColor()) {
|
||||
body.foreground = bodyColor()
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncLabels(): Boolean {
|
||||
var changed = false
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, title(item)) || changed
|
||||
changed = setText(parts.sub, subtitle(item)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncBody(): Boolean {
|
||||
var changed = false
|
||||
val body = parts.content ?: return false
|
||||
val value = preview(item)
|
||||
if (body.text != value) {
|
||||
body.text = value
|
||||
changed = true
|
||||
}
|
||||
if (body.foreground != bodyColor()) {
|
||||
body.foreground = bodyColor()
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun applyBodyStyle(): Boolean {
|
||||
val body = parts.content ?: return false
|
||||
if (!disposed) {
|
||||
Disposer.register(this, body)
|
||||
disposed = true
|
||||
}
|
||||
if (!registered && selection != null && parts.scroll?.parent != null) {
|
||||
registered = true
|
||||
body.register(selection, this)
|
||||
}
|
||||
return body.applyStyle(style)
|
||||
}
|
||||
|
||||
private fun bodyColor() = if (item.state == ToolExecState.ERROR) UiStyle.Colors.errorLabelForeground() else UiStyle.Colors.fg()
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val body = parts.content ?: return 0
|
||||
return body.lineHeight() * bodyMaxRows() +
|
||||
JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "ToolView#$contentId(${labelText()})"
|
||||
}
|
||||
@@ -51,11 +51,19 @@ open class Stack(
|
||||
private val mgr: Layout
|
||||
get() = getLayout() as Layout
|
||||
|
||||
internal fun fit(): Stack {
|
||||
mgr.fit = true
|
||||
revalidate()
|
||||
return this
|
||||
}
|
||||
|
||||
private class Layout(
|
||||
private val axis: StackAxis,
|
||||
private val gap: Int,
|
||||
) : LayoutManager2 {
|
||||
|
||||
var fit = false
|
||||
|
||||
private val entries = mutableListOf<Entry>()
|
||||
|
||||
fun gap(size: Int) {
|
||||
@@ -83,6 +91,10 @@ open class Stack(
|
||||
val ins = parent.insets
|
||||
val w = maxOf(0, parent.width - ins.left - ins.right)
|
||||
val h = maxOf(0, parent.height - ins.top - ins.bottom)
|
||||
if (axis == StackAxis.HORIZONTAL && fit) {
|
||||
fit(parent, ins.left, ins.top, w, h)
|
||||
return
|
||||
}
|
||||
var x = ins.left
|
||||
var y = ins.top
|
||||
var seen = false
|
||||
@@ -131,6 +143,48 @@ open class Stack(
|
||||
}
|
||||
}
|
||||
|
||||
private fun fit(parent: Container, left: Int, top: Int, w: Int, h: Int) {
|
||||
val items = children(parent, h)
|
||||
var x = left
|
||||
var rest = w
|
||||
items.forEach { item ->
|
||||
val gap = minOf(item.gap, rest)
|
||||
x += gap
|
||||
rest -= gap
|
||||
val width = minOf(item.width, rest)
|
||||
item.comp.setBounds(x, top, width, h)
|
||||
x += width
|
||||
rest -= width
|
||||
}
|
||||
}
|
||||
|
||||
private fun children(parent: Container, h: Int): List<Item> {
|
||||
val items = mutableListOf<Item>()
|
||||
var seen = false
|
||||
var ready = false
|
||||
var pending: Int? = null
|
||||
for (entry in entries) {
|
||||
when (entry) {
|
||||
is Entry.Gap -> if (ready) pending = safe(pending ?: 0, entry.size)
|
||||
is Entry.Child -> {
|
||||
val space = pending
|
||||
pending = null
|
||||
ready = false
|
||||
if (entry.comp.isVisible) {
|
||||
entry.comp.setSize(entry.comp.width.coerceAtLeast(1), h)
|
||||
val pref = entry.comp.preferredSize
|
||||
val min = entry.comp.minimumSize
|
||||
val max = entry.comp.maximumSize
|
||||
items.add(Item(entry.comp, if (seen) space ?: gap else 0, bound(pref.width, min.width, max.width)))
|
||||
seen = true
|
||||
ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
override fun minimumLayoutSize(parent: Container) = size(parent, Size.MIN)
|
||||
override fun preferredLayoutSize(parent: Container) = size(parent, Size.PREF)
|
||||
override fun maximumLayoutSize(target: Container) = size(target, Size.MAX)
|
||||
@@ -204,6 +258,8 @@ open class Stack(
|
||||
data class Child(val comp: Component) : Entry
|
||||
data class Gap(val size: Int) : Entry
|
||||
}
|
||||
|
||||
private data class Item(val comp: Component, val gap: Int, val width: Int)
|
||||
}
|
||||
|
||||
private enum class Size { MIN, PREF, MAX }
|
||||
@@ -211,6 +267,7 @@ open class Stack(
|
||||
companion object {
|
||||
fun vertical(gap: Int = 0) = Stack(StackAxis.VERTICAL, gap)
|
||||
fun horizontal(gap: Int = 0) = Stack(StackAxis.HORIZONTAL, gap)
|
||||
fun fitHorizontal(gap: Int = 0) = Stack(StackAxis.HORIZONTAL, gap).fit()
|
||||
fun verticalFiller(size: Int): Component = filler(StackAxis.VERTICAL, size)
|
||||
fun horizontalFiller(size: Int): Component = filler(StackAxis.HORIZONTAL, size)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.commonmark.node.Document
|
||||
import org.commonmark.node.FencedCodeBlock
|
||||
import org.commonmark.node.IndentedCodeBlock
|
||||
import org.commonmark.node.Node
|
||||
import org.commonmark.node.ThematicBreak
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import java.awt.Color
|
||||
@@ -498,6 +499,9 @@ internal class MdViewHybrid(
|
||||
val field = runCatching {
|
||||
CodeField(file, opts, text).also { ed ->
|
||||
ed.setDisposedWith(disposable)
|
||||
Disposer.register(disposable) {
|
||||
ed.getEditor(false)?.let(EditorFactory.getInstance()::releaseEditor)
|
||||
}
|
||||
selection?.register(ed, disposable)
|
||||
}
|
||||
}.getOrElse { err ->
|
||||
@@ -682,8 +686,14 @@ internal class MdViewHybrid(
|
||||
fun flush() {
|
||||
if (md.isEmpty()) return
|
||||
val doc = parser.parse(md.toString())
|
||||
html.append(renderer.render(doc))
|
||||
blocks.addAll(collect(doc))
|
||||
val descs = collect(doc)
|
||||
blocks.addAll(descs)
|
||||
for (desc in descs) {
|
||||
when (desc) {
|
||||
is Desc.Html -> html.append(desc.body)
|
||||
is Desc.Code -> html.append(codeHtml(desc.text))
|
||||
}
|
||||
}
|
||||
md.clear()
|
||||
}
|
||||
|
||||
@@ -896,27 +906,39 @@ internal class MdViewHybrid(
|
||||
|
||||
private inner class Visitor : AbstractVisitor() {
|
||||
val blocks = mutableListOf<Desc>()
|
||||
private val run = StringBuilder()
|
||||
|
||||
override fun visit(document: Document) {
|
||||
visitChildren(document)
|
||||
flush()
|
||||
}
|
||||
|
||||
override fun visit(code: FencedCodeBlock) {
|
||||
flush()
|
||||
blocks.add(Desc.Code(code.literal, file(code.info)))
|
||||
}
|
||||
|
||||
override fun visit(code: IndentedCodeBlock) {
|
||||
flush()
|
||||
blocks.add(Desc.Code(code.literal, file(null)))
|
||||
}
|
||||
|
||||
private fun flush() {
|
||||
if (run.isEmpty()) return
|
||||
blocks.add(Desc.Html(run.toString()))
|
||||
run.clear()
|
||||
}
|
||||
|
||||
public override fun visitChildren(parent: Node) {
|
||||
var child = parent.firstChild
|
||||
while (child != null) {
|
||||
val next = child.next
|
||||
if (child is FencedCodeBlock || child is IndentedCodeBlock) child.accept(this)
|
||||
if (child is Block && child !is FencedCodeBlock && child !is IndentedCodeBlock) {
|
||||
blocks.add(Desc.Html(renderer.render(child)))
|
||||
if (child is ThematicBreak) {
|
||||
child = next
|
||||
continue
|
||||
}
|
||||
if (child is FencedCodeBlock || child is IndentedCodeBlock) child.accept(this)
|
||||
if (child is Block && child !is FencedCodeBlock && child !is IndentedCodeBlock) run.append(renderer.render(child))
|
||||
child = next
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15L7 10L12 5" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15L7 10L12 5" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 15L13 10L8 5" stroke="#6C707E" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 15L13 10L8 5" stroke="#CED0D6" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.08325 3.75H11.2499M14.5833 3.75H17.9166M2.08325 10L7.08325 10M10.4166 10L17.9166 10M2.08325 16.25L8.74992 16.25M12.0833 16.25L17.9166 16.25" stroke="#6C707E" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.08325 3.75H11.2499M14.5833 3.75H17.9166M2.08325 10L7.08325 10M10.4166 10L17.9166 10M2.08325 16.25L8.74992 16.25M12.0833 16.25L17.9166 16.25" stroke="#CED0D6" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.7513 7.5013L6.2513 10.0013L8.7513 12.5013M11.2513 7.5013L13.7513 10.0013L11.2513 12.5013M2.91797 2.91797H17.0846V17.0846H2.91797V2.91797Z" stroke="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |