mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
Merge remote-tracking branch 'origin/main' into prairie-crocus
# Conflicts: # packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show detected ports for tracked background processes in the TUI sidebar and process detail dialog.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support tracked background processes in the CLI and VS Code so agents can start long-running dev servers and clean them up when sessions change or end. The CLI also includes process management UI, status, and logs.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains model picker with sections, favorites, and smart filtering.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Use the fallback logo in old Windows terminal emulators while keeping the Unicode logo available over SSH.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a JetBrains tool window action for starting new sessions and let recent sessions reopen in the side panel without duplicate views.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support typed custom responses to question prompts in the JetBrains plugin.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Scale JetBrains chat transcript fonts from the editor font settings.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains model picker at the same row position when models are favorited or unfavorited.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix JetBrains startup when command templates are returned as lazy objects.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show JetBrains session connection status inline above the prompt, cap expanded connection details, and keep empty sessions in the shared chat scroll area.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show detailed JetBrains session connection and configuration issues in the connection panel, and make retry recover app-side problems more reliably.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Match VS Code-style reasoning and tool run rendering in JetBrains chat.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Open existing JetBrains sessions scrolled to the latest message after history loads.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains history navigation with Back support and a Show History action in recent sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show the logged-out account status in the same rounded panel as the logged-in account overlay, with a "Not logged in" label, hidden picker/balance, and a profile icon to open settings.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a Dismiss button to the paid-model sign-in prompt so users can close it and choose a different model.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains sign-in UI: step labels are left-aligned, the URL field selects all on click, copying the URL or device code shows a confirmation balloon, and the click-to-copy code card and balance card share the same themed background and border.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains mode picker with descriptions, deprecated badges, and native IntelliJ selection behavior.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show a sign-in prompt in JetBrains sessions when a paid model requires login.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains permission prompts with compact action rows and diff badges.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Polish the JetBrains user profile settings layout with a compact account stack, copyable email, simplified organization names, and a refreshable balance card.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add native Kilo profile settings page to JetBrains plugin. Settings > Tools > Kilo > User Profile shows login/logout, balance, personal/org account switching, and a dashboard link, and refreshes immediately after login, logout, or active account changes. A new Profile button in the tool window toolbar opens the page directly. The profile settings page now keeps its native UI mounted while login state and active account details change.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show JetBrains question prompts one question at a time with aligned native option rows.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains chat transcript pinned to bottom reliably while responses stream.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show a scroll-to-bottom button when JetBrains chat sessions are scrolled away from the latest message.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render active question and permission prompts inside the scrollable JetBrains chat transcript.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support rebinding the JetBrains prompt Send action in the IntelliJ keymap.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show account login, switching, and balance controls on the empty JetBrains session screen.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a native session history panel to the JetBrains plugin.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Control JetBrains chat transcript spacing from the session layout.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Reduce redundant JetBrains chat scroll layout work while keeping bottom-follow behavior.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show completed assistant steps in the JetBrains session timeline.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support stopping active JetBrains sessions from the prompt action button.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Avoid leaving mouse and advanced keyboard modes enabled after exiting the TUI in mintty and MINGW terminals.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Size the JetBrains model picker from its rendered model rows instead of fixed popup dimensions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support reasoning effort selection and resetting JetBrains chat model overrides to the default model.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Align checked and unchecked reasoning picker rows in the JetBrains plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Start expandable session sections collapsed by default.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve question-based session views so UI text uses editor-sized interface fonts, actions align consistently, and permission prompts show a header icon.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains session connection status above the prompt without shifting the chat layout.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refine JetBrains session loading UI and settings actions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Preserve JetBrains chat scroll position when reviewing earlier session output.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Avoid flickering transient loading and connection error states in JetBrains sessions.
|
||||
@@ -34,6 +34,7 @@ a.out
|
||||
target
|
||||
.scripts
|
||||
.direnv/
|
||||
packages/kilo-jetbrains/build-tasks/.gradle/
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v1.14.34
|
||||
v1.14.41
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -176,10 +176,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.3.8",
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -193,7 +194,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -230,7 +231,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -293,7 +294,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -301,7 +302,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -451,7 +452,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -476,7 +477,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -487,7 +488,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -502,7 +503,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -525,7 +526,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -626,7 +627,7 @@
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "24.12.2",
|
||||
"@types/semver": "7.7.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
||||
"ai": "6.0.168",
|
||||
@@ -672,7 +673,7 @@
|
||||
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="],
|
||||
|
||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||
|
||||
@@ -2164,7 +2165,7 @@
|
||||
|
||||
"@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
"@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
@@ -4338,7 +4339,7 @@
|
||||
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
@@ -4552,6 +4553,8 @@
|
||||
|
||||
"@antfu/install-pkg/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
@@ -4656,6 +4659,8 @@
|
||||
|
||||
"@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
|
||||
"@manypkg/find-root/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||
@@ -4804,6 +4809,30 @@
|
||||
|
||||
"@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@types/cacache/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/cross-spawn/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/mssql/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/node-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/npm-registry-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/npmcli__arborist/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/npmlog/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/pacote/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/qrcode/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/readable-stream/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/ssri/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@types/ws/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
@@ -4840,6 +4869,8 @@
|
||||
|
||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
||||
|
||||
"apache-arrow/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@@ -4874,6 +4905,8 @@
|
||||
|
||||
"buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
@@ -4966,6 +4999,8 @@
|
||||
|
||||
"gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"image-q/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"isomorphic-git/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -5088,6 +5123,8 @@
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"protobufjs/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||
@@ -5154,6 +5191,8 @@
|
||||
|
||||
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"tedious/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"test-exclude/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
@@ -5212,6 +5251,8 @@
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
@@ -5250,10 +5291,14 @@
|
||||
|
||||
"@kilocode/kilo-gateway/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
|
||||
|
||||
"@manypkg/find-root/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
||||
|
||||
"@morphllm/morphsdk/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
@@ -5360,6 +5405,30 @@
|
||||
|
||||
"@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@types/cacache/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/cross-spawn/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/mssql/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/npm-registry-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/npmcli__arborist/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/npmlog/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/pacote/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/qrcode/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/readable-stream/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/ssri/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@vscode/ripgrep/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
@@ -5402,6 +5471,8 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"apache-arrow/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
@@ -5432,6 +5503,8 @@
|
||||
|
||||
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"c8/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
@@ -5536,10 +5609,14 @@
|
||||
|
||||
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"image-q/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"kilo-code/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
@@ -5602,6 +5679,8 @@
|
||||
|
||||
"posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="],
|
||||
|
||||
"protobufjs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||
|
||||
"qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
@@ -5632,6 +5711,8 @@
|
||||
|
||||
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"tedious/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
@@ -5664,6 +5745,8 @@
|
||||
|
||||
"@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
|
||||
"@morphllm/morphsdk/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
|
||||
@@ -5776,6 +5859,8 @@
|
||||
|
||||
"gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"kilo-code/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"mocha/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-1GI3/cmf3ne+dxfEHzp7EzvEO0GzjgXtlilFGzV73hc=",
|
||||
"aarch64-linux": "sha256-lHO7dmc1inyzFjAgoU7hyNUHldcrWOFu6eeBL1NaB14=",
|
||||
"aarch64-darwin": "sha256-9j9ZdIZ6TD9F1GioHSRynaPjNHENdrl2sgMI93P2QhA=",
|
||||
"x86_64-darwin": "sha256-3qKN9xzg2TzhnxX/zhwtP5V0152d5bfnn3hlctv8IJI="
|
||||
"x86_64-linux": "sha256-EbXkvlbexL1Gyy4vvGG5j+JWjv7SsVk6T5YnDfo3AQY=",
|
||||
"aarch64-linux": "sha256-+MuJE4XGr0dArD/HuozaoK9Oymfgvx1CisE/Sm4ZstY=",
|
||||
"aarch64-darwin": "sha256-Qdk+tZLydGld471ApL1cxfd85QQNFelTfqq7uAznfK4=",
|
||||
"x86_64-darwin": "sha256-Fo6W65MfAcXaulBGCaE7+GfHtq3VVJyVDJpIENvR7lQ="
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "24.12.2",
|
||||
"@types/semver": "7.7.1",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
@@ -145,6 +145,6 @@
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
},
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -87,6 +87,8 @@ export const layer = Layer.effect(
|
||||
Effect.sync(() => Service.of(make())),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export const layerWith = (input: Partial<Interface>) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.3.7"
|
||||
version = "7.3.8"
|
||||
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.7/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/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.7/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/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.7/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/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.7/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/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.7/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18d394d726ebee312e54db5d45f55cca3a1728d2b41f9f73bb9b610096bf53e0
|
||||
size 41004
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.7",
|
||||
"version": "7.3.8",
|
||||
"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.7",
|
||||
"version": "7.3.8",
|
||||
"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.7",
|
||||
"version": "7.3.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -171,6 +171,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
- **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/` (requires CLI binaries already present in `backend/build/generated/cli/`; run `bun run build --prepare-cli` first).
|
||||
- **Via Turbo**: `bun turbo build --filter=@kilocode/kilo-jetbrains` from repo root.
|
||||
- **Run in sandbox**: `./gradlew runIde` — launches sandboxed IntelliJ with the plugin. Does NOT build CLI binaries.
|
||||
- **Run split backend**: `./gradlew runIdeBackend` — if it exits shortly after startup, check for an orphaned Java process from a previous backend run and kill it before restarting.
|
||||
- **Test split mode**: `./gradlew generateSplitModeRunConfigurations` creates a "Run IDE (Split Mode)" config that starts both frontend and backend processes locally. Emulate latency via the Split Mode widget (requires internal mode: `-Didea.is.internal=true`).
|
||||
|
||||
## UI Guidelines
|
||||
@@ -365,6 +366,39 @@ For common spacing lookups, prefer `JBUI.CurrentTheme` area-specific insets (e.g
|
||||
| Simple `BorderLayout` panels | `JBUI.Panels.simplePanel(...)`, `BorderLayoutPanel` |
|
||||
| Simple vertical custom Swing groups | `VerticalLayout` |
|
||||
| Fluent platform panels | `JBPanel.withBorder(...)`, `.andTransparent()`, `.andOpaque()`, `.withBackground(...)` |
|
||||
| Single-component alignment wrapper | `ai.kilocode.client.ui.layout.Align` — see section below |
|
||||
|
||||
### Align — Single-Component Alignment Wrapper
|
||||
|
||||
Use `Align` (`ai.kilocode.client.ui.layout.Align`) when a single Swing component must be positioned inside available space without adding visual chrome. It is a transparent, no-border, no-color `JPanel(null)` that lays out its one child according to independent horizontal (`HAlign`) and vertical (`VAlign`) modes. `CenterShrinkPanel` has been removed; use `child.align(HAlign.CENTER, VAlign.CENTER)` as a direct replacement.
|
||||
|
||||
**Alignment modes:**
|
||||
|
||||
| Mode | Axis | Layout behavior | Wrapper size contribution |
|
||||
|---|---|---|---|
|
||||
| `HAlign.TRACK` / `VAlign.TRACK` | either | Child always fills all available space; ignores child min/preferred/max | Zero (wrapper reports insets only on that axis) |
|
||||
| `HAlign.FIT` / `VAlign.FIT` | either | Child fills available space clamped to child's effective `[min, max]` range | Child min/preferred/max respected |
|
||||
| `HAlign.LEFT` / `VAlign.TOP` | H / V | Child placed at left/top edge at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
|
||||
| `HAlign.CENTER` / `VAlign.CENTER` | H / V | Child centered at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
|
||||
| `HAlign.RIGHT` / `VAlign.BOTTOM` | H / V | Child placed at right/bottom edge at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
|
||||
|
||||
"Bounded preferred" means the child's preferred size coerced into the effective `[min, max]` range. If available space is smaller than the effective minimum, the layout shrinks the child to available space to avoid overflow.
|
||||
|
||||
**Factory extension** on `Component`:
|
||||
|
||||
```kotlin
|
||||
child.align(HAlign.LEFT, VAlign.TOP) // left-aligned, top-pinned
|
||||
child.align(HAlign.CENTER, VAlign.CENTER) // centered (replaces CenterShrinkPanel)
|
||||
child.align(HAlign.TRACK, VAlign.CENTER) // fill width, center vertically
|
||||
child.align(HAlign.TRACK, VAlign.TRACK) // fill all available space
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Prefer `child.align(h, v)` over creating one-off `JPanel(FlowLayout(...))` or `BorderLayoutPanel` wrappers just to control alignment.
|
||||
- Use `TRACK` when the child must occupy all available space on an axis and must not reserve any space in the parent's size negotiation on that axis. Use `FIT` when you want to fill available space but still respect child min/max constraints.
|
||||
- All non-TRACK modes include the child's min, preferred, and max sizes in the wrapper's own min/preferred/max size. This means parent layout managers see the child constraints through the wrapper.
|
||||
- Do not use `Align` for spacing, padding, borders, colors, or multi-child layout — use `JBUI.Borders.empty(...)`, `UiStyle.Gap`, or an appropriate layout manager for those concerns.
|
||||
|
||||
### IntelliJ UI Surfaces
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ Production packaging still requires running `bun run build:production` so all pl
|
||||
|
||||
Use the checked-in `Run IDE (Backend)` run configuration (or `./gradlew runIdeBackend`) to launch just the backend half of a split-mode session. It prepares the local-platform CLI binary automatically when `backend/build/generated/cli/cli/` does not contain the expected binary.
|
||||
|
||||
If `Run IDE (Backend)` exits shortly after startup, check for an orphaned Java process from a previous backend run and kill it before restarting the backend.
|
||||
|
||||
Use `Run IDE (Split Mode)` to launch both halves at once (composes `Run IDE (Backend)` + `Run IDE (Frontend)`).
|
||||
|
||||
### Backend Gradle properties
|
||||
|
||||
+95
-1
@@ -13,6 +13,9 @@ import ai.kilocode.jetbrains.api.model.Config
|
||||
import ai.kilocode.jetbrains.api.model.ConfigWarnings200ResponseInner
|
||||
import ai.kilocode.jetbrains.api.model.KiloNotifications200ResponseInner
|
||||
import ai.kilocode.jetbrains.api.model.KiloProfile200Response
|
||||
import ai.kilocode.jetbrains.api.model.ProviderOauthAuthorizeRequest
|
||||
import ai.kilocode.jetbrains.api.model.ProviderOauthCallbackRequest
|
||||
import ai.kilocode.rpc.dto.DeviceAuthDto
|
||||
import ai.kilocode.rpc.dto.HealthDto
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.Service
|
||||
@@ -27,10 +30,18 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
@@ -550,6 +561,89 @@ class KiloBackendAppService private constructor(
|
||||
_appState.value = KiloAppState.Disconnected
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user profile from the CLI backend.
|
||||
* Returns the latest profile data, or null when not logged in.
|
||||
* Updates the current [KiloAppState.Ready] profile in-place if the app is ready.
|
||||
*/
|
||||
suspend fun refreshProfile(): KiloProfile200Response? {
|
||||
val result = fetchProfile()
|
||||
val fresh = result.value
|
||||
val current = _appState.value
|
||||
if (current is KiloAppState.Ready) {
|
||||
setAppReady(current.data.copy(profile = fresh))
|
||||
}
|
||||
profile = fresh
|
||||
return fresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Kilo device auth login flow.
|
||||
* Returns [DeviceAuthDto] containing the verification URL and code for display in the UI.
|
||||
*/
|
||||
suspend fun startLogin(directory: String?): DeviceAuthDto {
|
||||
val client = connection.api ?: throw IllegalStateException("Not connected")
|
||||
val body = ProviderOauthAuthorizeRequest(method = 0.0)
|
||||
val response = client.providerOauthAuthorize(providerID = "kilo", directory = directory, providerOauthAuthorizeRequest = body)
|
||||
val match = response.instructions.let { Regex("""code:\s*(\S+)""", RegexOption.IGNORE_CASE).find(it) }
|
||||
return DeviceAuthDto(
|
||||
code = match?.groupValues?.get(1),
|
||||
verificationUrl = response.url,
|
||||
expiresIn = 900,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the Kilo device auth login flow.
|
||||
* Blocks until the user completes authentication on the browser side.
|
||||
* Returns the user profile on success, or null if the login could not be completed.
|
||||
*/
|
||||
suspend fun completeLogin(directory: String?): KiloProfile200Response? {
|
||||
val client = connection.api ?: throw IllegalStateException("Not connected")
|
||||
client.providerOauthCallback(providerID = "kilo", directory = directory, providerOauthCallbackRequest = ProviderOauthCallbackRequest(method = 0.0))
|
||||
return refreshProfile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out from Kilo Gateway.
|
||||
* Removes credentials and clears the profile from app state.
|
||||
*/
|
||||
suspend fun logout(): Boolean {
|
||||
val client = connection.api ?: throw IllegalStateException("Not connected")
|
||||
val result = client.authRemove(providerID = "kilo")
|
||||
val current = _appState.value
|
||||
if (current is KiloAppState.Ready) {
|
||||
profile = null
|
||||
setAppReady(current.data.copy(profile = null))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the active account context.
|
||||
* Pass null for personal account, an organization ID for org context.
|
||||
* Returns the updated profile after the switch.
|
||||
*/
|
||||
suspend fun setOrganization(organizationId: String?): KiloProfile200Response? {
|
||||
val http = connection.apiClient ?: throw IllegalStateException("Not connected")
|
||||
val body = JsonObject(
|
||||
mapOf("organizationId" to (organizationId?.let { JsonPrimitive(it) } ?: JsonNull)),
|
||||
).toString()
|
||||
val request = Request.Builder()
|
||||
.url("http://127.0.0.1:$port/kilo/organization")
|
||||
.header("Accept", "application/json")
|
||||
.post(body.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
withContext(Dispatchers.IO) {
|
||||
http.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IllegalStateException("Organization switch failed: HTTP ${response.code} ${response.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
return refreshProfile()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
watcher?.cancel()
|
||||
watcher = null
|
||||
|
||||
+1
-7
@@ -9,10 +9,6 @@ import ai.kilocode.rpc.dto.ModelStateDto
|
||||
import ai.kilocode.rpc.dto.ModelVariantUpdateDto
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.nio.file.Files
|
||||
@@ -29,7 +25,6 @@ class KiloBackendModelStateManager(
|
||||
private val DEFAULT_DIR = Path.of(System.getProperty("user.home"), ".local", "state", "kilo")
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val mutex = Mutex()
|
||||
|
||||
private var client: OkHttpClient? = null
|
||||
@@ -122,8 +117,7 @@ class KiloBackendModelStateManager(
|
||||
return null
|
||||
}
|
||||
val raw = response.body?.string() ?: return null
|
||||
val state = json.parseToJsonElement(raw).jsonObject["state"]?.jsonPrimitive?.contentOrNull
|
||||
val dir = state?.let(Path::of) ?: DEFAULT_DIR
|
||||
val dir = KiloCliDataParser.parsePathState(raw)?.let(Path::of) ?: DEFAULT_DIR
|
||||
Files.createDirectories(dir)
|
||||
dir.resolve("model.json").also { file = it }
|
||||
}
|
||||
|
||||
+197
-7
@@ -1,5 +1,10 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import ai.kilocode.backend.workspace.CommandInfo
|
||||
import ai.kilocode.backend.workspace.ModelInfo
|
||||
import ai.kilocode.backend.workspace.ModelLimitInfo
|
||||
import ai.kilocode.backend.workspace.ProviderData
|
||||
import ai.kilocode.backend.workspace.ProviderInfo
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.CloudSessionListDto
|
||||
@@ -13,6 +18,7 @@ import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.ModelStateDto
|
||||
import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
|
||||
import ai.kilocode.rpc.dto.PermissionFileDiffDto
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
import ai.kilocode.rpc.dto.PartTimeDto
|
||||
@@ -286,6 +292,45 @@ object KiloCliDataParser {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a provider catalog response (`GET /provider`) into [ProviderData].
|
||||
* Throws if [raw] is not a valid JSON object (lets the workspace loading
|
||||
* catch the exception and surface it as a LoadError).
|
||||
*/
|
||||
fun parseProviders(raw: String): ProviderData {
|
||||
val obj = json.parseToJsonElement(raw).jsonObject
|
||||
return ProviderData(
|
||||
providers = obj["all"]?.jsonArray?.map { parseProvider(it.jsonObject) } ?: emptyList(),
|
||||
connected = obj["connected"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(),
|
||||
defaults = obj["default"]?.jsonObject?.mapValues { (_, v) -> v.jsonPrimitive.content } ?: emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a command list response (`GET /command`) into a list of [CommandInfo].
|
||||
* The `template` field is intentionally ignored — CLI commands can return lazy
|
||||
* promise objects (`{}`) for that field, which must not crash JetBrains startup.
|
||||
*/
|
||||
fun parseCommands(raw: String): List<CommandInfo> =
|
||||
json.parseToJsonElement(raw).jsonArray.map { item ->
|
||||
val obj = item.jsonObject
|
||||
CommandInfo(
|
||||
name = obj.str("name") ?: "",
|
||||
description = obj.str("description"),
|
||||
source = obj.str("source"),
|
||||
hints = obj["hints"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the `state` directory path from a `/path` response.
|
||||
* Returns `null` when the field is missing, not a JSON string, or the JSON is malformed.
|
||||
*/
|
||||
fun parsePathState(raw: String): String? {
|
||||
val prim = runCatching { tryParseObject(raw)?.get("state")?.jsonPrimitive }.getOrNull() ?: return null
|
||||
return if (prim.isString) prim.content else null
|
||||
}
|
||||
|
||||
fun parseModelState(raw: String): ModelStateDto {
|
||||
val obj = tryParseObject(raw) ?: return ModelStateDto()
|
||||
return ModelStateDto(
|
||||
@@ -327,6 +372,14 @@ object KiloCliDataParser {
|
||||
}
|
||||
val sb = StringBuilder()
|
||||
sb.append("""{"parts":[$parts]""")
|
||||
val msg = prompt.messageID
|
||||
if (msg != null) {
|
||||
sb.append(""","messageID":${escape(msg)}""")
|
||||
}
|
||||
val reply = prompt.noReply
|
||||
if (reply != null) {
|
||||
sb.append(""","noReply":$reply""")
|
||||
}
|
||||
val pid = prompt.providerID
|
||||
val mid = prompt.modelID
|
||||
if (pid != null && mid != null) {
|
||||
@@ -441,10 +494,16 @@ object KiloCliDataParser {
|
||||
|
||||
internal fun parseError(obj: JsonObject): MessageErrorDto {
|
||||
val type = obj.str("type") ?: obj.str("name") ?: "unknown"
|
||||
val data = obj["data"]?.jsonObject
|
||||
val msg = obj.str("message")
|
||||
?: obj["data"]?.jsonObject?.str("message")
|
||||
?: data?.str("message")
|
||||
?: obj.str("error")
|
||||
return MessageErrorDto(type, msg)
|
||||
return MessageErrorDto(
|
||||
type,
|
||||
msg,
|
||||
statusCode = data?.long("statusCode")?.safeInt(),
|
||||
responseBody = data?.str("responseBody"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parsePermissionRequest(obj: JsonObject): PermissionRequestDto? {
|
||||
@@ -453,11 +512,27 @@ object KiloCliDataParser {
|
||||
val permission = obj.str("permission") ?: return null
|
||||
val patterns = obj["patterns"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList()
|
||||
val always = obj["always"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList()
|
||||
val meta = obj["metadata"]?.jsonObject?.let { m ->
|
||||
m.entries.associate { (k, v) -> k to (v.jsonPrimitive.contentOrNull ?: "") }
|
||||
} ?: emptyMap()
|
||||
val ref = toolRef(obj)
|
||||
return PermissionRequestDto(id, sid, permission, patterns, meta, always, ref)
|
||||
val metaObj = obj["metadata"].obj()
|
||||
val meta = metaObj?.entries?.mapNotNull { (key, value) ->
|
||||
val text = value.scalar() ?: return@mapNotNull null
|
||||
key to text
|
||||
}?.toMap() ?: emptyMap()
|
||||
val path = metaObj.path()
|
||||
val diffs = metaObj.permissionDiffs(path)
|
||||
return PermissionRequestDto(
|
||||
id = id,
|
||||
sessionID = sid,
|
||||
permission = permission,
|
||||
patterns = patterns,
|
||||
metadata = meta,
|
||||
always = always,
|
||||
tool = toolRef(obj),
|
||||
message = obj.str("message") ?: metaObj?.str("message"),
|
||||
command = metaObj?.str("command") ?: obj.str("command"),
|
||||
rules = metaObj.rules(),
|
||||
filePath = path,
|
||||
fileDiffs = diffs,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun parseQuestionRequest(obj: JsonObject): QuestionRequestDto? {
|
||||
@@ -520,6 +595,49 @@ object KiloCliDataParser {
|
||||
"modelID" to JsonPrimitive(item.modelID),
|
||||
))
|
||||
|
||||
// ================================================================
|
||||
// Internal — provider/catalog parsing
|
||||
// ================================================================
|
||||
|
||||
private val EFFORT_ORDER = listOf("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
||||
.withIndex().associate { it.value to it.index }
|
||||
|
||||
private fun parseProvider(obj: JsonObject) = ProviderInfo(
|
||||
id = obj.str("id") ?: "",
|
||||
name = obj.str("name") ?: "",
|
||||
source = obj.str("source"),
|
||||
models = obj["models"]?.jsonObject?.mapValues { (id, v) -> parseModel(id, v.jsonObject) } ?: emptyMap(),
|
||||
)
|
||||
|
||||
private fun parseModel(id: String, obj: JsonObject): ModelInfo {
|
||||
val cap = obj["capabilities"]?.jsonObject
|
||||
val limit = obj["limit"]?.jsonObject
|
||||
return ModelInfo(
|
||||
id = obj.str("id") ?: id,
|
||||
name = obj.str("name") ?: id,
|
||||
attachment = cap.bool("attachment"),
|
||||
reasoning = cap.bool("reasoning"),
|
||||
temperature = cap.bool("temperature"),
|
||||
toolCall = cap.bool("toolcall"),
|
||||
free = obj.bool("isFree"),
|
||||
status = obj.str("status"),
|
||||
recommendedIndex = obj.num("recommendedIndex"),
|
||||
variants = parseVariants(obj),
|
||||
limit = limit?.let {
|
||||
ModelLimitInfo(
|
||||
context = it.long("context") ?: 0,
|
||||
input = it.long("input"),
|
||||
output = it.long("output") ?: 0,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseVariants(obj: JsonObject): List<String> {
|
||||
val keys = obj["variants"]?.jsonObject?.keys?.toList() ?: return emptyList()
|
||||
return keys.sortedWith(compareBy<String> { EFFORT_ORDER[it] ?: Int.MAX_VALUE }.thenBy { it })
|
||||
}
|
||||
|
||||
private fun parseSessionObject(obj: JsonObject): SessionDto {
|
||||
val time = obj["time"]?.jsonObject
|
||||
val summary = obj["summary"]?.jsonObject
|
||||
@@ -601,6 +719,11 @@ object KiloCliDataParser {
|
||||
/**
|
||||
* Build the JSON body for `POST /permission/{requestID}/reply`.
|
||||
*/
|
||||
internal fun parseRulesJson(text: String): List<String> {
|
||||
val arr = runCatching { json.parseToJsonElement(text).jsonArray }.getOrNull() ?: return listOf(text)
|
||||
return arr.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
||||
}
|
||||
|
||||
fun buildPermissionReplyJson(reply: PermissionReplyDto): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("""{"reply":${escape(reply.reply)}""")
|
||||
@@ -669,6 +792,70 @@ object KiloCliDataParser {
|
||||
}
|
||||
}
|
||||
|
||||
// Permission metadata helpers
|
||||
|
||||
private fun JsonElement?.obj(): JsonObject? = runCatching { this?.jsonObject }.getOrNull()
|
||||
private fun JsonElement?.arr(): JsonArray? = runCatching { this?.jsonArray }.getOrNull()
|
||||
|
||||
private fun JsonObject?.path(): String? {
|
||||
if (this == null) return null
|
||||
return str("filepath") ?: str("filePath") ?: str("file") ?: str("path")
|
||||
}
|
||||
|
||||
private fun JsonObject?.rules(): List<String> {
|
||||
if (this == null) return emptyList()
|
||||
val raw = this["rules"] ?: return emptyList()
|
||||
val arr = raw.arr()
|
||||
if (arr != null) {
|
||||
return arr.mapNotNull { it.jsonPrimitive.contentOrNull }
|
||||
}
|
||||
val text = runCatching { raw.jsonPrimitive.contentOrNull }.getOrNull() ?: return emptyList()
|
||||
if (text.startsWith("[")) {
|
||||
return runCatching {
|
||||
KiloCliDataParser.parseRulesJson(text)
|
||||
}.getOrElse { listOf(text) }
|
||||
}
|
||||
return listOf(text)
|
||||
}
|
||||
|
||||
private fun JsonObject?.permissionDiffs(path: String?): List<PermissionFileDiffDto> {
|
||||
if (this == null) return emptyList()
|
||||
val filediff = this["filediff"].obj()
|
||||
if (filediff != null) {
|
||||
val file = filediff.str("file") ?: filediff.str("relativePath") ?: path ?: return emptyList()
|
||||
return listOf(
|
||||
PermissionFileDiffDto(
|
||||
file = file,
|
||||
patch = filediff.str("patch"),
|
||||
before = filediff.str("before"),
|
||||
after = filediff.str("after"),
|
||||
additions = filediff.long("additions")?.safeInt() ?: 0,
|
||||
deletions = filediff.long("deletions")?.safeInt() ?: 0,
|
||||
)
|
||||
)
|
||||
}
|
||||
val files = this["files"].arr()
|
||||
if (files != null) {
|
||||
return files.mapNotNull { elem ->
|
||||
val item = elem.obj() ?: return@mapNotNull null
|
||||
val file = item.str("relativePath") ?: item.str("filePath") ?: item.str("file") ?: return@mapNotNull null
|
||||
PermissionFileDiffDto(
|
||||
file = file,
|
||||
patch = item.str("patch"),
|
||||
before = item.str("before"),
|
||||
after = item.str("after"),
|
||||
additions = item.long("additions")?.safeInt() ?: 0,
|
||||
deletions = item.long("deletions")?.safeInt() ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
val diff = str("diff")
|
||||
if (diff != null) {
|
||||
return listOf(PermissionFileDiffDto(file = path ?: "patch", patch = diff))
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// JsonObject convenience extensions
|
||||
private fun JsonObject.str(key: String): String? =
|
||||
this[key]?.jsonPrimitive?.contentOrNull
|
||||
@@ -679,6 +866,9 @@ private fun JsonObject.num(key: String): Double? =
|
||||
private fun JsonObject.long(key: String): Long? =
|
||||
this[key]?.jsonPrimitive?.longOrNull
|
||||
|
||||
private fun JsonObject?.bool(key: String): Boolean =
|
||||
this?.get(key)?.jsonPrimitive?.booleanOrNull ?: false
|
||||
|
||||
private fun Long.safeInt() = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
|
||||
|
||||
private fun JsonObject?.map(key: String): Map<String, String> {
|
||||
|
||||
+27
@@ -11,10 +11,12 @@ import ai.kilocode.backend.app.ProfileResult
|
||||
import ai.kilocode.jetbrains.api.model.AgentConfig
|
||||
import ai.kilocode.jetbrains.api.model.Config
|
||||
import ai.kilocode.jetbrains.api.model.ConfigAgent
|
||||
import ai.kilocode.jetbrains.api.model.KiloProfile200Response
|
||||
import ai.kilocode.rpc.dto.AgentConfigDto
|
||||
import ai.kilocode.rpc.dto.ConfigDto
|
||||
import ai.kilocode.rpc.KiloAppRpcApi
|
||||
import ai.kilocode.rpc.dto.ConfigWarningDto
|
||||
import ai.kilocode.rpc.dto.DeviceAuthDto
|
||||
import ai.kilocode.rpc.dto.HealthDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
@@ -24,6 +26,9 @@ import ai.kilocode.rpc.dto.ModelFavoriteUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelStateDto
|
||||
import ai.kilocode.rpc.dto.ModelVariantUpdateDto
|
||||
import ai.kilocode.rpc.dto.ProfileBalanceDto
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.ProfileOrganizationDto
|
||||
import ai.kilocode.rpc.dto.ProfileStatusDto
|
||||
import com.intellij.openapi.components.service
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -63,6 +68,17 @@ class KiloAppRpcApiImpl : KiloAppRpcApi {
|
||||
|
||||
override suspend fun updateModelVariant(update: ModelVariantUpdateDto): ModelStateDto = app.models.variant(update)
|
||||
|
||||
override suspend fun refreshProfile(): ProfileDto? = app.refreshProfile()?.let(::profileDto)
|
||||
|
||||
override suspend fun startLogin(directory: String?): DeviceAuthDto = app.startLogin(directory)
|
||||
|
||||
override suspend fun completeLogin(directory: String?): ProfileDto? = app.completeLogin(directory)?.let(::profileDto)
|
||||
|
||||
override suspend fun logout(): Boolean = app.logout()
|
||||
|
||||
override suspend fun setOrganization(organizationId: String?): ProfileDto? =
|
||||
app.setOrganization(organizationId)?.let(::profileDto)
|
||||
|
||||
private fun dto(state: KiloAppState): KiloAppStateDto =
|
||||
appStateDto(state)
|
||||
}
|
||||
@@ -85,6 +101,7 @@ internal fun appStateDto(state: KiloAppState): KiloAppStateDto =
|
||||
),
|
||||
warnings = state.data.warnings.map(::warning),
|
||||
config = config(state.data.config),
|
||||
profile = state.data.profile?.let(::profileDto),
|
||||
)
|
||||
is KiloAppState.Error -> KiloAppStateDto(
|
||||
status = KiloAppStatusDto.ERROR,
|
||||
@@ -93,6 +110,16 @@ internal fun appStateDto(state: KiloAppState): KiloAppStateDto =
|
||||
)
|
||||
}
|
||||
|
||||
internal fun profileDto(p: KiloProfile200Response): ProfileDto = ProfileDto(
|
||||
email = p.profile.email,
|
||||
name = p.profile.name,
|
||||
organizations = p.profile.organizations.orEmpty().map { org ->
|
||||
ProfileOrganizationDto(id = org.id, name = org.name, role = org.role)
|
||||
},
|
||||
balance = p.balance?.let { ProfileBalanceDto(balance = it.balance) },
|
||||
currentOrgId = p.currentOrgId,
|
||||
)
|
||||
|
||||
private fun progress(p: LoadProgress) = LoadProgressDto(
|
||||
config = p.config,
|
||||
notifications = p.notifications,
|
||||
|
||||
+3
-71
@@ -3,20 +3,12 @@ package ai.kilocode.backend.workspace
|
||||
import ai.kilocode.backend.app.KiloBackendSessionManager
|
||||
import ai.kilocode.backend.app.LoadError
|
||||
import ai.kilocode.backend.app.SseEvent
|
||||
import ai.kilocode.backend.cli.KiloCliDataParser
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.jetbrains.api.client.DefaultApi
|
||||
import ai.kilocode.jetbrains.api.model.Agent
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -57,10 +49,6 @@ class KiloBackendWorkspace(
|
||||
companion object {
|
||||
private const val MAX_RETRIES = 3
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val EFFORT_ORDER = listOf("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
||||
.withIndex()
|
||||
.associate { it.value to it.index }
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow<KiloWorkspaceState>(KiloWorkspaceState.Pending)
|
||||
@@ -219,7 +207,7 @@ class KiloBackendWorkspace(
|
||||
|
||||
private fun fetchProviders(): FetchResult<ProviderData> =
|
||||
try {
|
||||
FetchResult.ok(parseProviders(fetch("/provider?directory=${encode(directory)}")))
|
||||
FetchResult.ok(KiloCliDataParser.parseProviders(fetch("/provider?directory=${encode(directory)}")))
|
||||
} catch (e: Exception) {
|
||||
log.warn("Providers fetch failed: ${e.message}", e)
|
||||
FetchResult.fail("providers", e)
|
||||
@@ -242,14 +230,7 @@ class KiloBackendWorkspace(
|
||||
|
||||
private fun fetchCommands(): FetchResult<List<CommandInfo>> =
|
||||
try {
|
||||
FetchResult.ok(api.commandList(directory = directory).map { c ->
|
||||
CommandInfo(
|
||||
name = c.name,
|
||||
description = c.description,
|
||||
source = c.source?.value,
|
||||
hints = c.hints,
|
||||
)
|
||||
})
|
||||
FetchResult.ok(KiloCliDataParser.parseCommands(fetch("/command?directory=${encode(directory)}")))
|
||||
} catch (e: Exception) {
|
||||
log.warn("Commands fetch failed: ${e.message}", e)
|
||||
FetchResult.fail("commands", e)
|
||||
@@ -282,51 +263,6 @@ class KiloBackendWorkspace(
|
||||
deprecated = a.deprecated,
|
||||
)
|
||||
|
||||
private fun parseProviders(raw: String): ProviderData {
|
||||
val obj = json.parseToJsonElement(raw).jsonObject
|
||||
return ProviderData(
|
||||
providers = obj["all"]?.jsonArray?.map { provider(it.jsonObject) } ?: emptyList(),
|
||||
connected = obj["connected"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(),
|
||||
defaults = obj["default"]?.jsonObject?.mapValues { (_, value) -> value.jsonPrimitive.content } ?: emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun provider(obj: JsonObject) = ProviderInfo(
|
||||
id = obj.str("id") ?: "",
|
||||
name = obj.str("name") ?: "",
|
||||
source = obj.str("source"),
|
||||
models = obj["models"]?.jsonObject?.mapValues { (id, value) -> model(id, value.jsonObject) } ?: emptyMap(),
|
||||
)
|
||||
|
||||
private fun model(id: String, obj: JsonObject): ModelInfo {
|
||||
val cap = obj["capabilities"]?.jsonObject
|
||||
val limit = obj["limit"]?.jsonObject
|
||||
return ModelInfo(
|
||||
id = obj.str("id") ?: id,
|
||||
name = obj.str("name") ?: id,
|
||||
attachment = cap.bool("attachment"),
|
||||
reasoning = cap.bool("reasoning"),
|
||||
temperature = cap.bool("temperature"),
|
||||
toolCall = cap.bool("toolcall"),
|
||||
free = obj.bool("isFree"),
|
||||
status = obj.str("status"),
|
||||
recommendedIndex = obj.num("recommendedIndex"),
|
||||
variants = variants(obj),
|
||||
limit = limit?.let {
|
||||
ModelLimitInfo(
|
||||
context = it.long("context") ?: 0,
|
||||
input = it.long("input"),
|
||||
output = it.long("output") ?: 0,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun variants(obj: JsonObject): List<String> {
|
||||
val raw = obj["variants"]?.jsonObject?.keys?.toList() ?: return emptyList()
|
||||
return raw.sortedWith(compareBy<String> { EFFORT_ORDER[it] ?: Int.MAX_VALUE }.thenBy { it })
|
||||
}
|
||||
|
||||
private fun fetch(path: String): String {
|
||||
val request = Request.Builder().url("http://localhost:$port$path").get().build()
|
||||
http.newCall(request).execute().use { response ->
|
||||
@@ -371,7 +307,3 @@ class KiloBackendWorkspace(
|
||||
}
|
||||
|
||||
private fun encode(value: String) = java.net.URLEncoder.encode(value, Charsets.UTF_8)
|
||||
private fun JsonObject.str(key: String) = this[key]?.jsonPrimitive?.contentOrNull
|
||||
private fun JsonObject?.bool(key: String) = this?.get(key)?.jsonPrimitive?.booleanOrNull ?: false
|
||||
private fun JsonObject.num(key: String) = this[key]?.jsonPrimitive?.doubleOrNull
|
||||
private fun JsonObject.long(key: String) = this[key]?.jsonPrimitive?.longOrNull
|
||||
|
||||
+197
@@ -164,6 +164,22 @@ class KiloBackendAppServiceTest {
|
||||
assertEquals("alice@test.com", svc.profile!!.profile.email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set organization sends explicit null body for personal account`() = runBlocking {
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
svc.setOrganization("org_1")
|
||||
assertEquals("""{"organizationId":"org_1"}""", mock.lastOrganizationSetBody)
|
||||
|
||||
svc.setOrganization(null)
|
||||
assertEquals("""{"organizationId":null}""", mock.lastOrganizationSetBody)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `profile 401 does not prevent Ready`() = runBlocking {
|
||||
mock.profileStatus = 401
|
||||
@@ -440,6 +456,41 @@ class KiloBackendAppServiceTest {
|
||||
assertTrue((svc.appState.value as KiloAppState.Ready).data.warnings.isEmpty())
|
||||
}
|
||||
|
||||
// ------ Auth mapping tests ------
|
||||
|
||||
@Test
|
||||
fun `start login maps device auth response`() = runBlocking<Unit> {
|
||||
// Default authorizeResponse: url=https://auth.kilo.ai/device, code=TEST-1234
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
val auth = svc.startLogin(null)
|
||||
assertEquals("https://auth.kilo.ai/device", auth.verificationUrl)
|
||||
assertEquals("TEST-1234", auth.code)
|
||||
assertEquals(900, auth.expiresIn)
|
||||
assertNotNull(mock.lastAuthorizeBody)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complete login calls callback and refreshes profile`() = runBlocking<Unit> {
|
||||
mock.profile = """{"profile":{"email":"alice@test.com","name":"Alice"},"balance":null,"currentOrgId":null}"""
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
val profile = svc.completeLogin(null)
|
||||
assertNotNull(profile)
|
||||
assertEquals("alice@test.com", profile.profile.email)
|
||||
assertNotNull(mock.lastCallbackBody)
|
||||
}
|
||||
|
||||
// ------ Concurrency & lifecycle tests ------
|
||||
|
||||
@Test
|
||||
@@ -519,4 +570,150 @@ class KiloBackendAppServiceTest {
|
||||
|
||||
assertIs<KiloAppState.Ready>(svc.appState.value)
|
||||
}
|
||||
|
||||
// ------ Profile DTO mapping tests ------
|
||||
|
||||
@Test
|
||||
fun `ready dto maps profile fields`() = runBlocking {
|
||||
mock.profile = """{
|
||||
"profile":{
|
||||
"email":"alice@test.com",
|
||||
"name":"Alice",
|
||||
"organizations":[{"id":"org_1","name":"Acme","role":"ADMIN"}]
|
||||
},
|
||||
"balance":{"balance":42.5},
|
||||
"currentOrgId":"org_1"
|
||||
}""".trimIndent()
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
val dto = appStateDto(svc.appState.value)
|
||||
assertEquals("alice@test.com", dto.profile?.email)
|
||||
assertEquals("Alice", dto.profile?.name)
|
||||
assertEquals("ADMIN", dto.profile?.organizations?.firstOrNull()?.role)
|
||||
assertEquals(42.5, dto.profile?.balance?.balance)
|
||||
assertEquals("org_1", dto.profile?.currentOrgId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh profile updates ready dto profile`() = runBlocking {
|
||||
mock.profile = """{"profile":{"email":"alice@test.com","name":"Alice"},"balance":null,"currentOrgId":null}"""
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
// Update mock to return different profile
|
||||
mock.profile = """{"profile":{"email":"alice@test.com","name":"Updated Alice"},"balance":{"balance":99.0},"currentOrgId":null}"""
|
||||
|
||||
val fresh = svc.refreshProfile()
|
||||
assertNotNull(fresh)
|
||||
assertEquals("Updated Alice", fresh.profile.name)
|
||||
assertEquals("Updated Alice", appStateDto(svc.appState.value).profile?.name)
|
||||
assertEquals(99.0, appStateDto(svc.appState.value).profile?.balance?.balance)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout clears ready profile on success`() = runBlocking {
|
||||
mock.profile = """{"profile":{"email":"alice@test.com","name":"Alice"},"balance":null,"currentOrgId":null}"""
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
assertNotNull(svc.profile)
|
||||
mock.authRemoveStatus = 200
|
||||
val ok = svc.logout()
|
||||
|
||||
assertTrue(ok)
|
||||
assertNull(svc.profile)
|
||||
assertNull(appStateDto(svc.appState.value).profile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set organization failure leaves profile unchanged`() = runBlocking {
|
||||
mock.profile = """{"profile":{"email":"alice@test.com","name":"Alice"},"balance":null,"currentOrgId":null}"""
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
val before = svc.profile
|
||||
assertNotNull(before)
|
||||
|
||||
mock.organizationSetStatus = 500
|
||||
var thrown = false
|
||||
try {
|
||||
svc.setOrganization("org_1")
|
||||
} catch (_: Exception) {
|
||||
thrown = true
|
||||
}
|
||||
assertTrue(thrown, "setOrganization with 500 should throw")
|
||||
// Profile should remain unchanged because organization switch failed before refreshProfile
|
||||
assertEquals(before.profile.email, svc.profile?.profile?.email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `start login failure propagates`() = runBlocking {
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
mock.authorizeStatus = 500
|
||||
var thrown = false
|
||||
try {
|
||||
svc.startLogin(null)
|
||||
} catch (_: Exception) {
|
||||
thrown = true
|
||||
}
|
||||
assertTrue(thrown, "startLogin with 500 status should throw")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `start login without code returns null code but url present`() = runBlocking {
|
||||
// Instructions without 'code:' — the regex match should return null
|
||||
mock.authorizeResponse = """{"url":"https://auth.kilo.ai/device","method":"code","instructions":"Open the URL in your browser to sign in"}"""
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
val auth = svc.startLogin(null)
|
||||
assertNull(auth.code, "code should be null when instructions have no code: prefix")
|
||||
assertEquals("https://auth.kilo.ai/device", auth.verificationUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complete login callback failure propagates`() = runBlocking {
|
||||
val svc = create()
|
||||
svc.connect()
|
||||
|
||||
withTimeout(10_000) {
|
||||
svc.appState.first { it is KiloAppState.Ready }
|
||||
}
|
||||
|
||||
mock.callbackStatus = 500
|
||||
var thrown = false
|
||||
try {
|
||||
svc.completeLogin(null)
|
||||
} catch (_: Exception) {
|
||||
thrown = true
|
||||
}
|
||||
assertTrue(thrown, "completeLogin with 500 callback status should throw")
|
||||
}
|
||||
}
|
||||
|
||||
+1339
-966
File diff suppressed because it is too large
Load Diff
+26
-1
@@ -40,6 +40,16 @@ class MockCliServer : AutoCloseable {
|
||||
@Volatile var warningsStatus = 200
|
||||
@Volatile var notificationsStatus = 200
|
||||
|
||||
// Auth / OAuth responses
|
||||
@Volatile var authorizeResponse = """{"url":"https://auth.kilo.ai/device","method":"code","instructions":"Open URL and enter code: TEST-1234"}"""
|
||||
@Volatile var authorizeStatus = 200
|
||||
@Volatile var callbackStatus = 200
|
||||
@Volatile var authRemoveStatus = 200
|
||||
@Volatile var organizationSetStatus = 200
|
||||
@Volatile var lastAuthorizeBody: String? = null
|
||||
@Volatile var lastCallbackBody: String? = null
|
||||
@Volatile var lastOrganizationSetBody: String? = null
|
||||
|
||||
// Project-scoped REST responses
|
||||
@Volatile var providers = """{"all":[],"default":{},"connected":[],"failed":[]}"""
|
||||
@Volatile var agents = "[]"
|
||||
@@ -219,13 +229,28 @@ class MockCliServer : AutoCloseable {
|
||||
path == "/global/config" -> respond(output, configStatus, config)
|
||||
path.startsWith("/config/warnings") -> respond(output, warningsStatus, warnings)
|
||||
path.startsWith("/kilo/notifications") -> respond(output, notificationsStatus, notifications)
|
||||
path.startsWith("/kilo/profile") -> {
|
||||
path.startsWith("/kilo/profile") && method == "GET" -> {
|
||||
if (profileStatus == 401) {
|
||||
respond(output, 401, """{"message":"Unauthorized"}""")
|
||||
} else {
|
||||
respond(output, profileStatus, profile)
|
||||
}
|
||||
}
|
||||
path.matches(Regex("/provider/[^/]+/oauth/authorize.*")) && method == "POST" -> {
|
||||
lastAuthorizeBody = body
|
||||
respond(output, authorizeStatus, authorizeResponse)
|
||||
}
|
||||
path.matches(Regex("/provider/[^/]+/oauth/callback.*")) && method == "POST" -> {
|
||||
lastCallbackBody = body
|
||||
respond(output, callbackStatus, "true")
|
||||
}
|
||||
bare.matches(Regex("/auth/[^/]+")) && method == "DELETE" -> {
|
||||
respond(output, authRemoveStatus, "true")
|
||||
}
|
||||
bare == "/kilo/organization" && method == "POST" -> {
|
||||
lastOrganizationSetBody = body
|
||||
respond(output, organizationSetStatus, "true")
|
||||
}
|
||||
path == "/global/event" -> handleSse(output)
|
||||
path == "/path" -> respond(output, 200, this.path)
|
||||
bare == "/provider" -> respond(output, providersStatus, providers)
|
||||
|
||||
+7
-15
@@ -274,9 +274,11 @@ class KiloBackendWorkspaceTest {
|
||||
}
|
||||
|
||||
// ------ Data mapping ------
|
||||
// Detailed provider/command/path parsing correctness is covered in KiloCliDataParserTest.
|
||||
// These integration tests verify end-to-end data flow: server → workspace state.
|
||||
|
||||
@Test
|
||||
fun `providers response maps models correctly`() = runBlocking {
|
||||
fun `providers response reaches state with expected provider and model`() = runBlocking {
|
||||
mock.providers = PROVIDERS_JSON
|
||||
val app = setup()
|
||||
val ws = ready(app)
|
||||
@@ -286,20 +288,10 @@ class KiloBackendWorkspaceTest {
|
||||
}
|
||||
|
||||
val state = ws.state.value as KiloWorkspaceState.Ready
|
||||
val provider = state.providers.providers[0]
|
||||
assertEquals("anthropic", provider.id)
|
||||
assertEquals("Anthropic", provider.name)
|
||||
val model = provider.models["claude-4"]
|
||||
assertNotNull(model)
|
||||
assertEquals("Claude 4", model.name)
|
||||
assertTrue(model.attachment)
|
||||
assertTrue(model.reasoning)
|
||||
assertTrue(model.toolCall)
|
||||
assertEquals(2.0, model.recommendedIndex)
|
||||
assertEquals(listOf("low", "medium", "high"), model.variants)
|
||||
assertEquals(200000L, model.limit?.context)
|
||||
assertEquals(100000L, model.limit?.input)
|
||||
assertEquals(16000L, model.limit?.output)
|
||||
assertEquals(1, state.providers.providers.size)
|
||||
assertEquals("anthropic", state.providers.providers[0].id)
|
||||
assertNotNull(state.providers.providers[0].models["claude-4"])
|
||||
assertEquals(listOf("anthropic"), state.providers.connected)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -46,7 +46,7 @@ val ver = if (release) checked(
|
||||
|
||||
val notes = providers.gradleProperty("kilo.changeNotes").orElse("Release candidate build.")
|
||||
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").map(::port).orElse(providers.provider(::fallback))
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").orNull?.let(::port) ?: fallback()
|
||||
val isolated = providers.gradleProperty("kilo.dev.storage.isolated").map { it.toBoolean() }.orElse(false)
|
||||
val worktreeRoot = providers.gradleProperty("kilo.dev.worktree.root").orElse(
|
||||
providers.provider { rootProject.layout.projectDirectory.asFile.parentFile.parentFile.canonicalPath }
|
||||
|
||||
@@ -23,6 +23,7 @@ dependencies {
|
||||
implementation(libs.commonmark.autolink)
|
||||
implementation(libs.commonmark.tables)
|
||||
implementation(libs.commonmark.strikethrough)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
|
||||
+5
-4
@@ -1,11 +1,10 @@
|
||||
package ai.kilocode.client
|
||||
|
||||
import ai.kilocode.client.actions.HistoryAction
|
||||
import ai.kilocode.client.actions.NewSessionAction
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.session.SessionSidePanelManager
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
@@ -65,8 +64,10 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||
toolWindow.contentManager.setSelectedContent(content)
|
||||
manager.newSession()
|
||||
|
||||
ActionManager.getInstance().getAction("Kilo.Settings")?.let { settings ->
|
||||
toolWindow.setTitleActions(listOf(NewSessionAction(), HistoryAction(), settings))
|
||||
val toolbar = ActionManager.getInstance().getAction("Kilo.ToolWindowToolbar")
|
||||
if (toolbar is ActionGroup) {
|
||||
val actions = toolbar.getChildren(null).toList()
|
||||
toolWindow.setTitleActions(actions)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to set up Kilo tool window content", e)
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.remoting.ActionRemoteBehaviorSpecification
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableWithId
|
||||
import com.intellij.openapi.options.ShowSettingsUtil
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import java.util.function.Predicate
|
||||
|
||||
/**
|
||||
* Toolbar action that opens the Kilo User Profile settings page.
|
||||
*
|
||||
* Uses a predicate-based lookup so settings open correctly in JetBrains
|
||||
* Remote Development where configurables may be wrapped.
|
||||
*/
|
||||
class ShowProfileAction : DumbAwareAction(
|
||||
KiloBundle.message("action.Kilo.ShowProfile.text"),
|
||||
KiloBundle.message("action.Kilo.ShowProfile.description"),
|
||||
AllIcons.General.User,
|
||||
), ActionRemoteBehaviorSpecification.Frontend {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(
|
||||
e.project,
|
||||
Predicate { cfg: Configurable ->
|
||||
cfg is ConfigurableWithId && cfg.getId() == UserProfileConfigurable.ID
|
||||
},
|
||||
{ cfg: Configurable -> cfg.focusOn(UserProfileConfigurable.FOCUS_ACCOUNT_COMBO) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
+64
@@ -3,6 +3,7 @@
|
||||
package ai.kilocode.client.app
|
||||
|
||||
import ai.kilocode.rpc.KiloAppRpcApi
|
||||
import ai.kilocode.rpc.dto.DeviceAuthDto
|
||||
import ai.kilocode.rpc.dto.HealthDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
@@ -11,6 +12,8 @@ import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelStateDto
|
||||
import ai.kilocode.rpc.dto.ModelVariantUpdateDto
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.ProfileStatusDto
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.openapi.components.Service
|
||||
import fleet.rpc.client.durable
|
||||
@@ -216,6 +219,59 @@ class KiloAppService internal constructor(
|
||||
_favorites.value = state.favorite
|
||||
}
|
||||
|
||||
/** Refresh the user profile and return the latest data. Null = not logged in. */
|
||||
suspend fun refreshProfile(): ProfileDto? = try {
|
||||
call { refreshProfile() }.also { setProfile(it) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("profile refresh failed", e)
|
||||
null
|
||||
}
|
||||
|
||||
/** Refresh profile in fire-and-forget fashion from non-suspend context. */
|
||||
fun refreshProfileAsync() {
|
||||
cs.launch { refreshProfile() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Kilo device auth login flow.
|
||||
* Returns [DeviceAuthDto] with the URL/code to display.
|
||||
* Throws on failure.
|
||||
*/
|
||||
suspend fun startLogin(directory: String? = null): DeviceAuthDto = call { startLogin(directory) }
|
||||
|
||||
/**
|
||||
* Complete the login flow. Blocks until authentication finishes.
|
||||
* Returns the user profile, or null if unavailable.
|
||||
*/
|
||||
suspend fun completeLogin(directory: String? = null): ProfileDto? = try {
|
||||
call { completeLogin(directory) }.also { setProfile(it) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("login completion failed", e)
|
||||
null
|
||||
}
|
||||
|
||||
/** Log out and clear the user profile. */
|
||||
suspend fun logout(): Boolean = try {
|
||||
call { logout() }.also { ok ->
|
||||
if (ok) setProfile(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("logout failed", e)
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch active account context.
|
||||
* Pass null for personal account, organization ID for org context.
|
||||
* Returns the updated profile, or null if not logged in.
|
||||
*/
|
||||
suspend fun setOrganization(organizationId: String?): ProfileDto? = try {
|
||||
call { setOrganization(organizationId) }.also { setProfile(it) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("organization switch failed", e)
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect app state changes and invoke [fn] for each update.
|
||||
*/
|
||||
@@ -227,4 +283,12 @@ class KiloAppService internal constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setProfile(profile: ProfileDto?) {
|
||||
val current = _state.value
|
||||
val progress = current.progress?.copy(
|
||||
profile = if (profile == null) ProfileStatusDto.NOT_LOGGED_IN else ProfileStatusDto.LOADED,
|
||||
)
|
||||
_state.value = current.copy(profile = profile, progress = progress)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-3
@@ -17,6 +17,7 @@ import ai.kilocode.client.session.ui.ReasoningPicker
|
||||
import ai.kilocode.client.session.ui.mode.ModePicker
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.session.ui.account.SessionAccountOverlay
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.SessionMessageListPanel
|
||||
import ai.kilocode.client.session.ui.header.SessionHeaderPanel
|
||||
@@ -25,9 +26,13 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.controller.EVENT_FLUSH_MS
|
||||
import ai.kilocode.client.session.controller.SessionController
|
||||
import ai.kilocode.client.session.controller.SessionControllerEvent
|
||||
import ai.kilocode.client.session.views.PermissionView
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.LoginRequiredView
|
||||
import ai.kilocode.client.session.views.permission.PermissionView
|
||||
import ai.kilocode.client.session.views.question.QuestionView
|
||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
@@ -36,8 +41,13 @@ import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.colors.EditorColorsListener
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableWithId
|
||||
import com.intellij.openapi.options.ShowSettingsUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import java.util.function.Predicate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -58,7 +68,7 @@ class SessionUi(
|
||||
workspace: Workspace,
|
||||
sessions: KiloSessionService,
|
||||
app: KiloAppService,
|
||||
cs: CoroutineScope,
|
||||
private val cs: CoroutineScope,
|
||||
ref: SessionRef? = null,
|
||||
displayMs: Long = SessionController.DISPLAY_DELAY_MS,
|
||||
private val manager: SessionManager? = null,
|
||||
@@ -92,10 +102,12 @@ class SessionUi(
|
||||
beforeUpdate = { if (opening) false else scroll.atBottom() },
|
||||
afterUpdate = { if (!opening) scroll.followBottom(it) },
|
||||
loaded = ::onSessionLoaded,
|
||||
openProfileAction = ::openProfileSettings,
|
||||
)
|
||||
|
||||
|
||||
private lateinit var root: SessionRootPanel
|
||||
private lateinit var account: SessionAccountOverlay
|
||||
|
||||
private lateinit var sessionContent: JPanel
|
||||
|
||||
@@ -111,6 +123,7 @@ class SessionUi(
|
||||
|
||||
private lateinit var question: QuestionView
|
||||
private lateinit var permission: PermissionView
|
||||
private lateinit var login: LoginRequiredView
|
||||
private lateinit var connection: ConnectionPanel
|
||||
|
||||
private lateinit var prompt: PromptPanel
|
||||
@@ -168,6 +181,22 @@ class SessionUi(
|
||||
}
|
||||
root.setBlocker(migrationOverlay)
|
||||
|
||||
account = SessionAccountOverlay(
|
||||
select = { org -> controller.selectOrganization(org) },
|
||||
profile = { controller.openProfile() },
|
||||
)
|
||||
root.addOverlay(account) { pane, child ->
|
||||
val size = child.preferredSize
|
||||
val top = JBUI.scale(SessionUiStyle.View.Prompt.PANEL_VERTICAL_PADDING)
|
||||
val right = JBUI.scale(SessionUiStyle.View.Prompt.PANEL_HORIZONTAL_PADDING)
|
||||
java.awt.Rectangle(
|
||||
pane.width - size.width - right,
|
||||
top,
|
||||
size.width,
|
||||
size.height,
|
||||
)
|
||||
}
|
||||
|
||||
sessionContent = JPanel(BorderLayout())
|
||||
|
||||
blankBody = JPanel(BorderLayout()).apply {
|
||||
@@ -177,6 +206,7 @@ class SessionUi(
|
||||
load = LoadingPanel()
|
||||
progressBody = load
|
||||
question = QuestionView(
|
||||
project = project,
|
||||
reply = { id, dto -> controller.replyQuestion(id, dto) },
|
||||
reject = { id -> controller.rejectQuestion(id) },
|
||||
scroll = { scroll.followBottom(true) },
|
||||
@@ -184,7 +214,8 @@ class SessionUi(
|
||||
permission = PermissionView(
|
||||
reply = { id, dto -> controller.replyPermission(id, dto) },
|
||||
)
|
||||
messageBody = SessionMessageListPanel(controller.model, this, question, permission)
|
||||
login = LoginRequiredView(openProfile = { controller.openProfile() }, dismiss = { controller.dismissLoginRequired() })
|
||||
messageBody = SessionMessageListPanel(controller.model, this, question, permission, login)
|
||||
header = SessionHeaderPanel(controller, this)
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
@@ -271,6 +302,8 @@ class SessionUi(
|
||||
}
|
||||
|
||||
is SessionControllerEvent.ConnectionChanged -> Unit
|
||||
|
||||
is SessionControllerEvent.AccountOverlayChanged -> account.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +432,16 @@ class SessionUi(
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun openProfileSettings() {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(
|
||||
project,
|
||||
Predicate { cfg: Configurable ->
|
||||
cfg is ConfigurableWithId && cfg.getId() == UserProfileConfigurable.ID
|
||||
},
|
||||
{ cfg: Configurable -> cfg.focusOn(UserProfileConfigurable.FOCUS_ACCOUNT_COMBO) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.rpc.dto.MessageErrorDto
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
private const val PAID_MODEL_AUTH_REQUIRED = "PAID_MODEL_AUTH_REQUIRED"
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
/**
|
||||
* Returns true when [error] signals that the user must sign in to use a paid model.
|
||||
*
|
||||
* Conditions (all must hold):
|
||||
* - error type is "APIError"
|
||||
* - statusCode is 401
|
||||
* - response body contains `error.code` or `code` equal to "PAID_MODEL_AUTH_REQUIRED"
|
||||
*
|
||||
* Malformed or missing response body returns false rather than throwing.
|
||||
*/
|
||||
internal fun isPaidModelAuthRequired(error: MessageErrorDto?): Boolean {
|
||||
if (error == null) return false
|
||||
if (error.type != "APIError") return false
|
||||
if (error.statusCode != 401) return false
|
||||
val body = error.responseBody ?: return false
|
||||
return runCatching {
|
||||
val obj = json.parseToJsonElement(body).jsonObject
|
||||
val nested = obj["error"]?.jsonObject?.get("code")?.jsonPrimitive?.content
|
||||
val top = obj["code"]?.jsonPrimitive?.content
|
||||
nested == PAID_MODEL_AUTH_REQUIRED || top == PAID_MODEL_AUTH_REQUIRED
|
||||
}.getOrNull() == true
|
||||
}
|
||||
+278
-7
@@ -11,6 +11,7 @@ import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.model.Permission
|
||||
import ai.kilocode.client.session.model.PermissionFileDiff
|
||||
import ai.kilocode.client.session.model.PermissionMeta
|
||||
import ai.kilocode.client.session.model.PermissionRequestState
|
||||
import ai.kilocode.client.session.model.Question
|
||||
@@ -21,10 +22,13 @@ import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigWarningDto
|
||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||
import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.LoadErrorDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.ProfileStatusDto
|
||||
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
|
||||
import ai.kilocode.rpc.dto.PermissionReplyDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
@@ -74,8 +78,11 @@ class SessionController(
|
||||
private val beforeUpdate: () -> Boolean = { false },
|
||||
private val afterUpdate: (Boolean) -> Unit = {},
|
||||
private val loaded: (Boolean) -> Unit = {},
|
||||
private val openProfileAction: () -> Unit = {},
|
||||
) : Disposable {
|
||||
|
||||
private data class OrganizationTarget(val org: String?)
|
||||
|
||||
companion object {
|
||||
private val LOG = KiloLog.create(SessionController::class.java)
|
||||
internal const val RECENT_LIMIT = 5
|
||||
@@ -105,12 +112,20 @@ class SessionController(
|
||||
private var partType: String? = null
|
||||
private var tool: String? = null
|
||||
private var eventJob: Job? = null
|
||||
private val childJobs: MutableMap<String, Job> = mutableMapOf()
|
||||
private val childIds: MutableSet<String> = mutableSetOf()
|
||||
private var sessionLoadState: SessionLoadState = SessionLoadState.Idle
|
||||
private var recentsState: RecentsState = RecentsState.Idle
|
||||
private var viewState: SessionControllerEvent.ViewChanged? = null
|
||||
private var connectionState: SessionControllerEvent.ConnectionChanged? = null
|
||||
private var connectionTargetState: SessionControllerEvent.ConnectionChanged? = null
|
||||
private val connectionDelay = DelayedState(displayMs)
|
||||
private var acctState: SessionControllerEvent.AccountOverlayChanged =
|
||||
SessionControllerEvent.AccountOverlayChanged.Hide
|
||||
private var acctAllowed = false
|
||||
private var lastProfile: ProfileDto? = null
|
||||
private var target: OrganizationTarget? = null
|
||||
private var loginRetry: PromptDto? = null
|
||||
|
||||
val ready: Boolean get() = model.isReady()
|
||||
internal val blank: Boolean get() = ref == null && model.isEmpty() && !model.showSession
|
||||
@@ -304,6 +319,7 @@ class SessionController(
|
||||
fun replyPermission(requestId: String, reply: PermissionReplyDto, rules: PermissionAlwaysRulesDto? = null) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" }
|
||||
updatePermission(requestId, PermissionRequestState.RESPONDING)
|
||||
cs.launch {
|
||||
try {
|
||||
if (rules != null) sessions.savePermissionRules(requestId, directory, rules)
|
||||
@@ -311,10 +327,29 @@ class SessionController(
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId ok=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
updatePermission(
|
||||
requestId,
|
||||
PermissionRequestState.ERROR,
|
||||
e.message ?: KiloBundle.message("session.permission.error"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePermission(id: String, state: PermissionRequestState, message: String? = null) {
|
||||
assertEdt()
|
||||
val current = model.state
|
||||
if (current !is SessionState.AwaitingPermission) return
|
||||
if (current.permission.id != id) return
|
||||
val perm = current.permission.copy(
|
||||
state = state,
|
||||
message = message ?: current.permission.message,
|
||||
)
|
||||
updateModel { model.setState(SessionState.AwaitingPermission(perm)) }
|
||||
}
|
||||
|
||||
fun replyQuestion(requestId: String, answers: QuestionReplyDto) {
|
||||
assertEdt()
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId answers=${answers.answers.size}" }
|
||||
@@ -369,8 +404,12 @@ class SessionController(
|
||||
fire(SessionControllerEvent.AppChanged) {
|
||||
model.app = state
|
||||
model.version = app.version
|
||||
if (model.state is SessionState.LoginRequired && state.profile != null) {
|
||||
resumeAfterLogin()
|
||||
}
|
||||
syncModelSelection()
|
||||
syncConnectionState()
|
||||
refreshAccountOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,6 +483,7 @@ class SessionController(
|
||||
val session = target.session ?: runCatching { sessions.get(id, directory) }.getOrNull()
|
||||
val items = sessions.messages(id, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(items)}" }
|
||||
val discovered = items.flatMap { it.parts }.mapNotNull { childID(it) }.toSet()
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
if (sid != id) return@runEdt
|
||||
@@ -456,6 +496,7 @@ class SessionController(
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
if (sid != id) return@runEdt
|
||||
for (child in discovered) trackChild(child)
|
||||
showSession()
|
||||
loaded(!model.isEmpty())
|
||||
}
|
||||
@@ -488,6 +529,7 @@ class SessionController(
|
||||
val session = sessions.importCloudSession(id, directory)
|
||||
val items = sessions.messages(session.id, directory)
|
||||
LOG.debug { "${ChatLogSummary.sid(session.id)} ${ChatLogSummary.history(items)}" }
|
||||
val discovered = items.flatMap { it.parts }.mapNotNull { childID(it) }.toSet()
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
ref = SessionRef.Local(session)
|
||||
@@ -500,6 +542,7 @@ class SessionController(
|
||||
recoverPending(session.id)
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
for (child in discovered) trackChild(child)
|
||||
subscribeEvents()
|
||||
showSession()
|
||||
loaded(!model.isEmpty())
|
||||
@@ -537,6 +580,9 @@ class SessionController(
|
||||
val id = sid ?: return
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=subscription subscribe=true" }
|
||||
eventJob?.cancel()
|
||||
childJobs.values.forEach { it.cancel() }
|
||||
childJobs.clear()
|
||||
childIds.clear()
|
||||
eventJob = cs.launch {
|
||||
try {
|
||||
sessions.events(id, directory).collect { event ->
|
||||
@@ -553,6 +599,46 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeChild(child: String) {
|
||||
if (childJobs.containsKey(child)) return
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-subscription child=$child subscribe=true" }
|
||||
val job = cs.launch {
|
||||
try {
|
||||
sessions.events(child, directory).collect { event ->
|
||||
if (!isChildPermissionEvent(event, child)) return@collect
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-event child=$child ${ChatLogSummary.eventBody(event)}" }
|
||||
updates.enqueue(event)
|
||||
}
|
||||
} finally {
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-subscription child=$child subscribe=false" }
|
||||
}
|
||||
}
|
||||
childJobs[child] = job
|
||||
}
|
||||
|
||||
private fun trackChild(child: String) {
|
||||
if (!childIds.add(child)) return
|
||||
subscribeChild(child)
|
||||
cs.launch { recoverChildPermissions(child) }
|
||||
}
|
||||
|
||||
private suspend fun recoverChildPermissions(child: String) {
|
||||
try {
|
||||
val permissions = sessions.pendingPermissions(directory).filter { it.sessionID == child }
|
||||
if (permissions.isEmpty()) return
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-recovery child=$child permissions=${permissions.size}" }
|
||||
val last = toPermission(permissions.last())
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
// Do not overwrite an existing root or other child AwaitingPermission state
|
||||
if (model.state is SessionState.AwaitingPermission) return@runEdt
|
||||
updateModel { model.setState(SessionState.AwaitingPermission(last)) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: "pending")} kind=child-recovery child=$child dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Rehydrate pending permissions/questions and current session status after history load. */
|
||||
private suspend fun recoverPending(id: String) {
|
||||
try {
|
||||
@@ -625,6 +711,7 @@ class SessionController(
|
||||
if (model.state is SessionState.Busy) {
|
||||
model.setState(SessionState.Busy(status()))
|
||||
}
|
||||
childID(event.part)?.let { child -> trackChild(child) }
|
||||
}
|
||||
|
||||
is ChatEventDto.PartDelta -> {
|
||||
@@ -648,7 +735,7 @@ class SessionController(
|
||||
tool = null
|
||||
// "completed" always transitions to idle.
|
||||
// Other reasons: don't clobber a more specific terminal state (Error,
|
||||
// AwaitingPermission, AwaitingQuestion) that arrived just before close.
|
||||
// AwaitingPermission, AwaitingQuestion, LoginRequired) that arrived just before close.
|
||||
val current = model.state
|
||||
val clobberOk = event.reason == "completed"
|
||||
|| current is SessionState.Busy
|
||||
@@ -660,8 +747,14 @@ class SessionController(
|
||||
is ChatEventDto.Error -> {
|
||||
partType = null
|
||||
tool = null
|
||||
val msg = event.error?.message ?: event.error?.type ?: KiloBundle.message("session.error.unknown")
|
||||
model.setState(SessionState.Error(msg, event.error?.type))
|
||||
if (isPaidModelAuthRequired(event.error)) {
|
||||
loginRetry = retryPrompt()
|
||||
showSession()
|
||||
model.setState(SessionState.LoginRequired(KiloBundle.message("session.login.required.description")))
|
||||
} else {
|
||||
val msg = event.error?.message ?: event.error?.type ?: KiloBundle.message("session.error.unknown")
|
||||
model.setState(SessionState.Error(msg, event.error?.type))
|
||||
}
|
||||
}
|
||||
|
||||
is ChatEventDto.MessageRemoved -> {
|
||||
@@ -669,7 +762,8 @@ class SessionController(
|
||||
}
|
||||
|
||||
is ChatEventDto.PermissionAsked -> {
|
||||
model.setState(SessionState.AwaitingPermission(toPermission(event.request)))
|
||||
val perm = toPermission(event.request)
|
||||
model.setState(SessionState.AwaitingPermission(perm))
|
||||
}
|
||||
|
||||
is ChatEventDto.PermissionReplied -> {
|
||||
@@ -699,7 +793,11 @@ class SessionController(
|
||||
|
||||
is ChatEventDto.SessionStatusChanged -> {
|
||||
val state = when (event.status.type) {
|
||||
"idle" -> SessionState.Idle
|
||||
"idle" -> {
|
||||
val current = model.state
|
||||
if (current is SessionState.LoginRequired) return
|
||||
SessionState.Idle
|
||||
}
|
||||
"busy" -> {
|
||||
val current = model.state
|
||||
if (current is SessionState.Idle || current is SessionState.Error)
|
||||
@@ -729,6 +827,7 @@ class SessionController(
|
||||
if (current !is SessionState.Error
|
||||
&& current !is SessionState.AwaitingPermission
|
||||
&& current !is SessionState.AwaitingQuestion
|
||||
&& current !is SessionState.LoginRequired
|
||||
) {
|
||||
model.setState(SessionState.Idle)
|
||||
}
|
||||
@@ -740,6 +839,48 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryPrompt(): PromptDto? {
|
||||
val msg = model.messages().lastOrNull { it.info.role == "user" } ?: return null
|
||||
return PromptDto(
|
||||
parts = emptyList(),
|
||||
messageID = msg.info.id,
|
||||
providerID = msg.info.providerID,
|
||||
modelID = msg.info.modelID,
|
||||
agent = msg.info.agent,
|
||||
variant = model.variant?.takeIf { it in model.variants },
|
||||
noReply = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resumeAfterLogin() {
|
||||
assertEdt()
|
||||
val retry = loginRetry
|
||||
loginRetry = null
|
||||
if (retry == null) {
|
||||
model.setState(SessionState.Idle)
|
||||
return
|
||||
}
|
||||
val id = sid
|
||||
if (id == null) {
|
||||
model.setState(SessionState.Idle)
|
||||
return
|
||||
}
|
||||
model.setState(SessionState.Busy(KiloBundle.message("session.status.considering")))
|
||||
cs.launch {
|
||||
try {
|
||||
sessions.prompt(id, directory, retry)
|
||||
LOG.debug { "${ChatLogSummary.sid(id)} kind=login-resume dispatched=true" }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(id)} kind=login-resume dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
edt {
|
||||
if (disposed) return@edt
|
||||
val msg = e.message ?: KiloBundle.message("session.error.prompt")
|
||||
model.setState(SessionState.Error(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptDto(text: String): PromptDto {
|
||||
val full = model.model
|
||||
val sel = full?.let(::parseModel)
|
||||
@@ -845,6 +986,87 @@ class SessionController(
|
||||
else -> KiloBundle.message("session.status.considering")
|
||||
}
|
||||
|
||||
fun selectOrganization(org: String?) {
|
||||
assertEdt()
|
||||
val next = OrganizationTarget(org)
|
||||
if (target == next) return
|
||||
target = next
|
||||
refreshAccountOverlay()
|
||||
cs.launch {
|
||||
try {
|
||||
app.setOrganization(org)
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("account switch failed org=$org message=${e.message}", e)
|
||||
edt {
|
||||
if (disposed) return@edt
|
||||
target = null
|
||||
refreshAccountOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openProfile() {
|
||||
assertEdt()
|
||||
openProfileAction()
|
||||
}
|
||||
|
||||
fun dismissLoginRequired() {
|
||||
assertEdt()
|
||||
loginRetry = null
|
||||
if (model.state is SessionState.LoginRequired) {
|
||||
updateModel { model.setState(SessionState.Idle) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun accountSnapshot(): SessionControllerEvent.AccountOverlaySnapshot {
|
||||
val state = model.app
|
||||
val prof = state.profile
|
||||
val pending = prof == null && state.progress?.profile == ProfileStatusDto.PENDING
|
||||
val current = when {
|
||||
prof != null -> prof
|
||||
pending -> lastProfile
|
||||
else -> null
|
||||
}
|
||||
if (prof != null) {
|
||||
lastProfile = prof
|
||||
if (target?.org == prof.currentOrgId) target = null
|
||||
}
|
||||
if (!pending && prof == null) {
|
||||
lastProfile = null
|
||||
target = null
|
||||
}
|
||||
return SessionControllerEvent.AccountOverlaySnapshot(
|
||||
status = state.status,
|
||||
profile = current,
|
||||
transient = pending,
|
||||
switching = target != null,
|
||||
targetOrgId = target?.org,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showAccountOverlay() {
|
||||
acctAllowed = true
|
||||
setAccountOverlayState(SessionControllerEvent.AccountOverlayChanged.Show(accountSnapshot()))
|
||||
}
|
||||
|
||||
private fun hideAccountOverlay() {
|
||||
acctAllowed = false
|
||||
setAccountOverlayState(SessionControllerEvent.AccountOverlayChanged.Hide)
|
||||
}
|
||||
|
||||
private fun refreshAccountOverlay() {
|
||||
if (!acctAllowed) return
|
||||
setAccountOverlayState(SessionControllerEvent.AccountOverlayChanged.Show(accountSnapshot()))
|
||||
}
|
||||
|
||||
private fun setAccountOverlayState(event: SessionControllerEvent.AccountOverlayChanged) {
|
||||
if (acctState == event) return
|
||||
fire(event) {
|
||||
acctState = event
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshRecents(force: Boolean = false) {
|
||||
assertEdt()
|
||||
if (!canUseRecents()) return
|
||||
@@ -896,6 +1118,11 @@ class SessionController(
|
||||
setRecentSessionsState(RecentsState.Idle)
|
||||
}
|
||||
}
|
||||
when (event) {
|
||||
is SessionControllerEvent.ViewChanged.ShowRecents -> showAccountOverlay()
|
||||
is SessionControllerEvent.ViewChanged.ShowProgress -> hideAccountOverlay()
|
||||
is SessionControllerEvent.ViewChanged.ShowSession -> hideAccountOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLocal() {
|
||||
@@ -1004,6 +1231,7 @@ class SessionController(
|
||||
val block: () -> Unit = {
|
||||
if (!disposed) {
|
||||
viewState?.let(listener::onEvent)
|
||||
listener.onEvent(acctState)
|
||||
connectionState?.let(listener::onEvent)
|
||||
}
|
||||
}
|
||||
@@ -1039,6 +1267,9 @@ class SessionController(
|
||||
disposed = true
|
||||
connectionDelay.dispose()
|
||||
eventJob?.cancel()
|
||||
childJobs.values.forEach { it.cancel() }
|
||||
childJobs.clear()
|
||||
childIds.clear()
|
||||
cs.cancel()
|
||||
}
|
||||
|
||||
@@ -1083,12 +1314,29 @@ class SessionController(
|
||||
out.add("[error]")
|
||||
out.add("[${state.message}]")
|
||||
}
|
||||
is SessionState.LoginRequired -> {
|
||||
out.add("[login-required]")
|
||||
out.add("[${state.message}]")
|
||||
}
|
||||
}
|
||||
|
||||
return out.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts the child session ID from a task tool part's metadata, or null if not a task part. */
|
||||
private fun childID(part: PartDto): String? {
|
||||
if (part.type != "tool" || part.tool != "task") return null
|
||||
return part.metadata["sessionId"]
|
||||
}
|
||||
|
||||
/** Returns true when [event] is a permission event for [child] (used by child subscriptions). */
|
||||
private fun isChildPermissionEvent(event: ChatEventDto, child: String): Boolean = when (event) {
|
||||
is ChatEventDto.PermissionAsked -> event.sessionID == child
|
||||
is ChatEventDto.PermissionReplied -> event.sessionID == child
|
||||
else -> false
|
||||
}
|
||||
|
||||
/** Returns true when [event]'s sessionID matches [id] (or event has no sessionID, like Error). */
|
||||
private fun matchesSession(event: ChatEventDto, id: String): Boolean = when (event) {
|
||||
is ChatEventDto.MessageUpdated -> event.sessionID == id
|
||||
@@ -1213,17 +1461,40 @@ private fun ConfigWarningDto.toDetailLine(): String {
|
||||
|
||||
private fun toPermission(dto: PermissionRequestDto): Permission {
|
||||
val ref = dto.tool?.let { ToolCallRef(it.messageID, it.callID) }
|
||||
val file = dto.metadata["file"] ?: dto.metadata["path"]
|
||||
val state = dto.metadata["state"]?.let { raw ->
|
||||
PermissionRequestState.values().firstOrNull { item -> item.name.equals(raw, ignoreCase = true) }
|
||||
} ?: PermissionRequestState.PENDING
|
||||
val diffs = dto.fileDiffs.map {
|
||||
PermissionFileDiff(
|
||||
file = it.file,
|
||||
patch = it.patch,
|
||||
before = it.before,
|
||||
after = it.after,
|
||||
additions = it.additions,
|
||||
deletions = it.deletions,
|
||||
)
|
||||
}
|
||||
val file = dto.filePath
|
||||
?: dto.metadata["filepath"]
|
||||
?: dto.metadata["filePath"]
|
||||
?: dto.metadata["file"]
|
||||
?: dto.metadata["path"]
|
||||
return Permission(
|
||||
id = dto.id,
|
||||
sessionId = dto.sessionID,
|
||||
name = dto.permission,
|
||||
patterns = dto.patterns,
|
||||
always = dto.always,
|
||||
meta = PermissionMeta(filePath = file, raw = dto.metadata),
|
||||
meta = PermissionMeta(
|
||||
command = dto.command ?: dto.metadata["command"],
|
||||
rules = dto.rules,
|
||||
diff = dto.metadata["diff"],
|
||||
filePath = file,
|
||||
fileDiff = diffs.firstOrNull(),
|
||||
fileDiffs = diffs,
|
||||
raw = dto.metadata,
|
||||
),
|
||||
message = dto.message ?: dto.metadata["message"],
|
||||
tool = ref,
|
||||
state = state,
|
||||
)
|
||||
|
||||
+20
@@ -2,6 +2,8 @@ package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.ProfileDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
|
||||
/**
|
||||
@@ -34,6 +36,24 @@ sealed class SessionControllerEvent {
|
||||
}
|
||||
}
|
||||
|
||||
data class AccountOverlaySnapshot(
|
||||
val status: KiloAppStatusDto,
|
||||
val profile: ProfileDto?,
|
||||
val transient: Boolean = false,
|
||||
val switching: Boolean = false,
|
||||
val targetOrgId: String? = null,
|
||||
)
|
||||
|
||||
sealed class AccountOverlayChanged : SessionControllerEvent() {
|
||||
data class Show(val account: AccountOverlaySnapshot) : AccountOverlayChanged() {
|
||||
override fun toString() = "AccountOverlayChanged show loggedIn=${account.profile != null}"
|
||||
}
|
||||
|
||||
data object Hide : AccountOverlayChanged() {
|
||||
override fun toString() = "AccountOverlayChanged hide"
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ConnectionChanged : SessionControllerEvent() {
|
||||
data object Hide : ConnectionChanged() {
|
||||
override fun toString() = "ConnectionChanged hide"
|
||||
|
||||
+2
@@ -17,10 +17,12 @@ data class Permission(
|
||||
)
|
||||
|
||||
data class PermissionMeta(
|
||||
val command: String? = null,
|
||||
val rules: List<String> = emptyList(),
|
||||
val diff: String? = null,
|
||||
val filePath: String? = null,
|
||||
val fileDiff: PermissionFileDiff? = null,
|
||||
val fileDiffs: List<PermissionFileDiff> = emptyList(),
|
||||
val raw: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
|
||||
+3
-1
@@ -18,8 +18,10 @@ sealed class SessionState {
|
||||
|
||||
data class Error(val message: String, val kind: String? = null) : SessionState()
|
||||
|
||||
data class LoginRequired(val message: String) : SessionState()
|
||||
|
||||
fun isBusy(): Boolean = when (this) {
|
||||
is Idle, is Loading, is Error -> false
|
||||
is Idle, is Loading, is Error, is LoginRequired -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
+19
-4
@@ -28,6 +28,7 @@ internal class SessionScroll(
|
||||
companion object {
|
||||
private const val THRESHOLD = 32
|
||||
private const val OPEN_PASSES = 12
|
||||
private const val FOLLOW_PASSES = 6
|
||||
}
|
||||
|
||||
val component = JBScrollPane(body).apply {
|
||||
@@ -95,10 +96,14 @@ internal class SessionScroll(
|
||||
return
|
||||
}
|
||||
tail = true
|
||||
stable = -1
|
||||
auto = true
|
||||
show(messages)
|
||||
auto = false
|
||||
followPass(++seq, 2)
|
||||
val id = ++seq
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
followPass(id, FOLLOW_PASSES)
|
||||
}
|
||||
}
|
||||
|
||||
fun openBottom(done: () -> Unit) {
|
||||
@@ -134,12 +139,16 @@ internal class SessionScroll(
|
||||
auto = true
|
||||
show(messages)
|
||||
auto = false
|
||||
followPass(++seq, 2)
|
||||
val id = ++seq
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
followPass(id, FOLLOW_PASSES)
|
||||
}
|
||||
}
|
||||
|
||||
private fun followPass(id: Int, remaining: Int) {
|
||||
if (id != seq || !tail) return
|
||||
auto = true
|
||||
val prev = bottom()
|
||||
try {
|
||||
layoutScroll()
|
||||
scrollToBottom()
|
||||
@@ -147,9 +156,15 @@ internal class SessionScroll(
|
||||
} finally {
|
||||
auto = false
|
||||
}
|
||||
if (remaining <= 0) return
|
||||
if (remaining <= 0) {
|
||||
stable = -1
|
||||
return
|
||||
}
|
||||
val next = bottom()
|
||||
val left = if (next == prev && next == stable) remaining - 1 else FOLLOW_PASSES
|
||||
stable = next
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
followPass(id, remaining - 1)
|
||||
followPass(id, left)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-6
@@ -10,8 +10,11 @@ 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.controller.SessionController
|
||||
import ai.kilocode.client.ui.CenterShrinkPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Align
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
@@ -44,7 +47,7 @@ import javax.swing.ListSelectionModel
|
||||
* Empty-session panel.
|
||||
*
|
||||
* The content is a BorderLayout panel, wrapped in a
|
||||
* [CenterShrinkPanel] (exposed as [view]) so callers need not know about centering.
|
||||
* [Align] (exposed as [view]) so callers need not know about centering.
|
||||
*/
|
||||
class EmptySessionPanel(
|
||||
parent: Disposable,
|
||||
@@ -52,7 +55,7 @@ class EmptySessionPanel(
|
||||
recents: List<SessionDto>,
|
||||
private val history: () -> Unit = {},
|
||||
) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget {
|
||||
val view: CenterShrinkPanel = CenterShrinkPanel(this)
|
||||
val view: Align = align(HAlign.CENTER, VAlign.CENTER)
|
||||
|
||||
private val model = DefaultListModel<LocalHistoryItem>()
|
||||
private var hover = -1
|
||||
@@ -133,7 +136,7 @@ class EmptySessionPanel(
|
||||
val header = BorderLayoutPanel(0, gap).apply {
|
||||
isOpaque = false
|
||||
add(logo, BorderLayout.NORTH)
|
||||
add(CenterShrinkPanel(description), BorderLayout.CENTER)
|
||||
add(description.align(HAlign.CENTER, VAlign.CENTER), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val recent = BorderLayoutPanel().apply {
|
||||
@@ -303,8 +306,8 @@ class EmptySessionPanel(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
welcomeLabel.font = style.uiFont
|
||||
recentTitle.font = style.smallUiFont
|
||||
welcomeLabel.font = style.regularFont
|
||||
recentTitle.font = style.smallFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class LoadingPanel : JPanel(BorderLayout()), SessionEditorStyleTarget {
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
label.font = style.uiFont
|
||||
label.font = style.regularFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class ProgressPanel(
|
||||
}
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
label.font = style.uiFont
|
||||
label.font = style.regularFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
+19
-5
@@ -7,8 +7,9 @@ import ai.kilocode.client.session.model.ToolCallRef
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.LoginRequiredView
|
||||
import ai.kilocode.client.session.views.MessageView
|
||||
import ai.kilocode.client.session.views.PermissionView
|
||||
import ai.kilocode.client.session.views.permission.PermissionView
|
||||
import ai.kilocode.client.session.views.question.QuestionView
|
||||
import ai.kilocode.client.session.views.TurnView
|
||||
import com.intellij.openapi.Disposable
|
||||
@@ -44,6 +45,7 @@ class SessionMessageListPanel(
|
||||
parent: Disposable,
|
||||
private val question: QuestionView? = null,
|
||||
private val permission: PermissionView? = null,
|
||||
private val login: LoginRequiredView? = null,
|
||||
) : SessionLayoutPanel(
|
||||
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
|
||||
JBUI.insets(
|
||||
@@ -248,8 +250,8 @@ class SessionMessageListPanel(
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide active question/permission views based on [state].
|
||||
* Both views are always kept as children of this panel (added in [anchorFooter]),
|
||||
* Show or hide active question/permission/login views based on [state].
|
||||
* All views are always kept as children of this panel (added in [anchorFooter]),
|
||||
* but visibility is controlled here.
|
||||
*/
|
||||
private fun syncActive(state: SessionState = model.state) {
|
||||
@@ -257,17 +259,26 @@ class SessionMessageListPanel(
|
||||
is SessionState.AwaitingQuestion -> {
|
||||
setHiddenQuestionTool(state.question.tool)
|
||||
permission?.hideView()
|
||||
login?.hideView()
|
||||
question?.show(state.question)
|
||||
}
|
||||
is SessionState.AwaitingPermission -> {
|
||||
setHiddenQuestionTool(null)
|
||||
question?.hideView()
|
||||
login?.hideView()
|
||||
permission?.show(state.permission)
|
||||
}
|
||||
is SessionState.LoginRequired -> {
|
||||
setHiddenQuestionTool(null)
|
||||
question?.hideView()
|
||||
permission?.hideView()
|
||||
login?.show(state.message)
|
||||
}
|
||||
else -> {
|
||||
setHiddenQuestionTool(null)
|
||||
question?.hideView()
|
||||
permission?.hideView()
|
||||
login?.hideView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,19 +291,21 @@ class SessionMessageListPanel(
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-insert [question], [permission], and [progress] as the last children
|
||||
* Re-insert [question], [permission], [login], and [progress] as the last children
|
||||
* so active views always render after all turn views, and progress is last.
|
||||
*
|
||||
* Both active views are added even when invisible — [SessionLayout] skips
|
||||
* All active views are added even when invisible — [SessionLayout] skips
|
||||
* invisible children, so no extra space is consumed, and the component tree
|
||||
* remains stable for tests.
|
||||
*/
|
||||
private fun anchorFooter() {
|
||||
if (question != null) remove(question)
|
||||
if (permission != null) remove(permission)
|
||||
if (login != null) remove(login)
|
||||
remove(progress)
|
||||
if (question != null) add(question)
|
||||
if (permission != null) add(permission)
|
||||
if (login != null) add(login)
|
||||
add(progress)
|
||||
}
|
||||
|
||||
@@ -317,6 +330,7 @@ class SessionMessageListPanel(
|
||||
for (view in turnViews.values) view.applyStyle(style)
|
||||
question?.applyStyle(style)
|
||||
permission?.applyStyle(style)
|
||||
login?.applyStyle(style)
|
||||
progress.applyStyle(style)
|
||||
refresh()
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ai.kilocode.client.session.ui.account
|
||||
|
||||
internal data class AccountChoice(val org: String?, val title: String) {
|
||||
override fun toString() = title
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package ai.kilocode.client.session.ui.account
|
||||
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
internal class AccountPickerRenderer(
|
||||
private val active: () -> String?,
|
||||
) : JPanel(BorderLayout()), ListCellRenderer<AccountChoice> {
|
||||
companion object {
|
||||
val checked: javax.swing.Icon = AllIcons.Actions.Checked
|
||||
val empty: javax.swing.Icon = EmptyIcon.create(checked)
|
||||
}
|
||||
|
||||
private val icon = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
}
|
||||
private val title = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.LEFT
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
}
|
||||
private val row = JPanel(BorderLayout(UiStyle.Gap.md(), 0))
|
||||
private val wrap = PickerRow()
|
||||
|
||||
init {
|
||||
UiStyle.Components.transparent(this, icon, title, row)
|
||||
row.border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
)
|
||||
row.add(icon, BorderLayout.WEST)
|
||||
row.add(title, BorderLayout.CENTER)
|
||||
wrap.setContent(row)
|
||||
add(wrap, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<out AccountChoice>,
|
||||
value: AccountChoice,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
focused: Boolean,
|
||||
): Component {
|
||||
val focus = selected || list.hasFocus() || focused
|
||||
val fg = UIUtil.getListForeground(selected, focus)
|
||||
background = list.background
|
||||
wrap.update(list, selected, focus)
|
||||
icon.icon = icon(value)
|
||||
title.text = value.title
|
||||
title.foreground = fg
|
||||
return this
|
||||
}
|
||||
|
||||
internal fun icon(value: AccountChoice): javax.swing.Icon =
|
||||
if (value.org == active()) checked else empty
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package ai.kilocode.client.session.ui.account
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.controller.SessionControllerEvent
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.PickerButton
|
||||
import ai.kilocode.client.ui.RoundedContentPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.ui.CollectionListModel
|
||||
import com.intellij.ui.ListUtil
|
||||
import com.intellij.ui.ScrollPaneFactory
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import ai.kilocode.client.settings.profile.formatBalance
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
/**
|
||||
* Compact account overlay shown in the top-right of the empty session screen.
|
||||
*
|
||||
* Only visible when logged in. Hidden when not logged in or no profile is available.
|
||||
* Visibility is controlled entirely by [onEvent] — never set [isVisible] externally.
|
||||
*/
|
||||
internal class SessionAccountOverlay(
|
||||
private val select: (String?) -> Unit,
|
||||
private val profile: () -> Unit,
|
||||
) : BorderLayoutPanel() {
|
||||
|
||||
private val picker = PickerButton().apply {
|
||||
isEnabled = false
|
||||
text = " "
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (!isEnabled || choices.isEmpty()) return
|
||||
showPopup()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var balanceText: String? = null
|
||||
|
||||
private val balance = JBLabel().apply {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
private val profileBtn = HoverIcon().apply {
|
||||
icon = AllIcons.General.User
|
||||
toolTipText = KiloBundle.message("action.Kilo.ShowProfile.description")
|
||||
accessibleContext.accessibleName = KiloBundle.message("action.Kilo.ShowProfile.text")
|
||||
addActionListener { profile() }
|
||||
}
|
||||
|
||||
private val row = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
isOpaque = false
|
||||
add(picker)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.md()))
|
||||
add(balance)
|
||||
add(Box.createHorizontalStrut(UiStyle.Gap.md()))
|
||||
add(profileBtn)
|
||||
}
|
||||
|
||||
private val panel = RoundedContentPanel(UiStyle.Gap.lg(), UiStyle.Gap.lg()).apply {
|
||||
addToCenter(row)
|
||||
}
|
||||
|
||||
private var choices: List<AccountChoice> = emptyList()
|
||||
private var currentOrgId: String? = null
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
isVisible = false
|
||||
addToCenter(panel)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun onEvent(event: SessionControllerEvent.AccountOverlayChanged) {
|
||||
var layout = false
|
||||
var paint = false
|
||||
when (event) {
|
||||
is SessionControllerEvent.AccountOverlayChanged.Hide -> {
|
||||
if (isVisible) {
|
||||
isVisible = false
|
||||
layout = true
|
||||
paint = true
|
||||
}
|
||||
}
|
||||
is SessionControllerEvent.AccountOverlayChanged.Show -> {
|
||||
val snap = event.account
|
||||
val prof = snap.profile
|
||||
if (prof == null) {
|
||||
if (!snap.transient && isVisible) {
|
||||
isVisible = false
|
||||
layout = true
|
||||
paint = true
|
||||
}
|
||||
} else {
|
||||
layout = updateLoggedIn(prof, snap.switching, snap.targetOrgId) || layout
|
||||
if (!isVisible) {
|
||||
isVisible = true
|
||||
layout = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (layout) revalidate()
|
||||
if (layout || paint) repaint()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun updateLoggedIn(prof: ai.kilocode.rpc.dto.ProfileDto, switching: Boolean, target: String?): Boolean {
|
||||
var layout = false
|
||||
|
||||
val orgs = prof.organizations
|
||||
val next = listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) +
|
||||
orgs.map { org -> AccountChoice(org.id, org.name) }
|
||||
if (next != choices) {
|
||||
choices = next
|
||||
layout = true
|
||||
}
|
||||
|
||||
if (currentOrgId != prof.currentOrgId) currentOrgId = prof.currentOrgId
|
||||
|
||||
val activeId = if (switching) target else prof.currentOrgId
|
||||
val active = choices.firstOrNull { it.org == activeId } ?: choices.firstOrNull()
|
||||
val title = "${active?.title ?: " "} ▾"
|
||||
if (picker.text != title) {
|
||||
picker.text = title
|
||||
layout = true
|
||||
}
|
||||
|
||||
val enabled = !switching
|
||||
if (picker.isEnabled != enabled) {
|
||||
picker.isEnabled = enabled
|
||||
picker.repaint()
|
||||
}
|
||||
|
||||
val tip = if (switching) {
|
||||
KiloBundle.message("profile.switchingAccount")
|
||||
} else {
|
||||
KiloBundle.message("session.account.switcher")
|
||||
}
|
||||
if (picker.toolTipText != tip) picker.toolTipText = tip
|
||||
|
||||
layout = syncBalance(prof) || layout
|
||||
return layout
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun syncBalance(prof: ai.kilocode.rpc.dto.ProfileDto): Boolean {
|
||||
var layout = false
|
||||
val next = prof.balance?.let { formatBalance(it.balance) }
|
||||
if (next == null) {
|
||||
if (balance.isVisible) {
|
||||
balance.isVisible = false
|
||||
layout = true
|
||||
}
|
||||
if (balance.icon != null) {
|
||||
balance.icon = null
|
||||
}
|
||||
if (balance.toolTipText != null) balance.toolTipText = null
|
||||
balanceText = null
|
||||
} else {
|
||||
if (!balance.isVisible) {
|
||||
balance.isVisible = true
|
||||
layout = true
|
||||
}
|
||||
if (balanceText != next || balance.icon == null) {
|
||||
balance.icon = FilledBadgeIcon(
|
||||
next,
|
||||
UiStyle.Colors.badgeBg(),
|
||||
UiStyle.Colors.badgeFg(),
|
||||
)
|
||||
layout = true
|
||||
}
|
||||
val tip = KiloBundle.message("session.account.balance", next)
|
||||
if (balance.toolTipText != tip) balance.toolTipText = tip
|
||||
balanceText = next
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun showPopup() {
|
||||
val bg = UiStyle.Colors.cardBg()
|
||||
val model = CollectionListModel(choices)
|
||||
val list = JBList(model).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
background = bg
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.xs(), 0)
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
list.cellRenderer = AccountPickerRenderer { currentOrgId }
|
||||
|
||||
val idx = choices.indexOfFirst { it.org == currentOrgId }.takeIf { it >= 0 } ?: 0
|
||||
if (idx >= 0) {
|
||||
list.selectedIndex = idx
|
||||
ScrollingUtil.ensureIndexIsVisible(list, idx, 0)
|
||||
}
|
||||
|
||||
lateinit var popup: com.intellij.openapi.ui.popup.JBPopup
|
||||
|
||||
fun activate(choice: AccountChoice) {
|
||||
if (choice.org != currentOrgId) select(choice.org)
|
||||
popup.closeOk(null)
|
||||
}
|
||||
|
||||
list.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return
|
||||
val row = list.locationToIndex(e.point)
|
||||
val bounds = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return
|
||||
if (!bounds.contains(e.point)) return
|
||||
activate(model.getElementAt(row))
|
||||
}
|
||||
})
|
||||
|
||||
ListUtil.installAutoSelectOnMouseMove(list)
|
||||
ScrollingUtil.installActions(list)
|
||||
|
||||
list.registerKeyboardAction(
|
||||
{ list.selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
list.registerKeyboardAction(
|
||||
{ popup.cancel() },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
|
||||
val scroll = ScrollPaneFactory.createScrollPane(list).apply {
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
border = JBUI.Borders.empty()
|
||||
viewportBorder = JBUI.Borders.empty()
|
||||
background = bg
|
||||
viewport.background = bg
|
||||
viewport.isOpaque = true
|
||||
}
|
||||
val content = RoundedContentPanel(UiStyle.Gap.sm(), UiStyle.Gap.sm()).apply {
|
||||
addToCenter(scroll)
|
||||
}
|
||||
|
||||
popup = JBPopupFactory.getInstance()
|
||||
.createComponentPopupBuilder(content, list)
|
||||
.setRequestFocus(true)
|
||||
.setFocusable(true)
|
||||
.setCancelOnClickOutside(true)
|
||||
.setCancelKeyEnabled(true)
|
||||
.setCancelOnWindowDeactivation(true)
|
||||
.setResizable(false)
|
||||
.setMovable(false)
|
||||
.createPopup()
|
||||
|
||||
popup.showUnderneathOf(picker)
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate an account choice without showing the popup.
|
||||
* Only calls [select] when the choice differs from [currentOrgId].
|
||||
* Used by tests and by the popup's confirm action.
|
||||
*/
|
||||
@RequiresEdt
|
||||
internal fun activate(choice: AccountChoice) {
|
||||
if (choice.org != currentOrgId) select(choice.org)
|
||||
}
|
||||
|
||||
internal fun loggedInVisible() = isVisible
|
||||
internal fun accountTitle(): String? = picker.text?.removeSuffix(" ▾")?.ifBlank { null }
|
||||
internal fun pickerEnabled() = picker.isEnabled
|
||||
internal fun pickerVisible() = picker.isVisible
|
||||
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() = UiStyle.Colors.cardBorder()
|
||||
internal fun balanceVisible() = balance.isVisible
|
||||
internal fun balanceIcon() = balance.icon
|
||||
internal fun balanceText() = balanceText
|
||||
internal fun profileIcon() = profileBtn.icon
|
||||
internal fun clickProfile() = profileBtn.doClick()
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package ai.kilocode.client.session.ui.editor
|
||||
|
||||
import ai.kilocode.client.session.ui.prompt.PromptDataKeys
|
||||
import ai.kilocode.client.session.ui.prompt.SendPromptContext
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.EditorTextField
|
||||
|
||||
/**
|
||||
* A session-scoped [EditorTextField] for plain-text input.
|
||||
*
|
||||
* When [ctx] is non-null the component injects it into the data context so
|
||||
* shortcut-based send/stop actions work (prompt use-case). When [ctx] is null
|
||||
* the component does not expose [PromptDataKeys.SEND], preventing accidental
|
||||
* `SendPromptAction` dispatch from question custom-answer editors.
|
||||
*
|
||||
* Both instances are created on the EDT. The underlying [EditorTextField]
|
||||
* lazily initializes its IntelliJ editor the first time the component becomes
|
||||
* visible; that initialization calls `EditorThreading.compute` internally,
|
||||
* satisfying the platform's read-context requirement without additional
|
||||
* wrapping here.
|
||||
*/
|
||||
internal open class SessionEditorTextField(
|
||||
project: Project,
|
||||
private val ctx: SendPromptContext? = null,
|
||||
) : EditorTextField(project, PlainTextFileType.INSTANCE) {
|
||||
override fun uiDataSnapshot(sink: DataSink) {
|
||||
super.uiDataSnapshot(sink)
|
||||
ctx?.let { sink.set(PromptDataKeys.SEND, it) }
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -44,9 +44,9 @@ internal class ContextBar : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) {
|
||||
background = style.editorBackground
|
||||
foreground = style.editorForeground
|
||||
meter.background = style.editorBackground
|
||||
used.font = style.smallUiFont
|
||||
used.font = style.smallFont
|
||||
used.foreground = style.editorForeground
|
||||
limit.font = style.smallUiFont
|
||||
limit.font = style.smallFont
|
||||
limit.foreground = style.editorForeground
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -229,23 +229,23 @@ class SessionHeaderPanel(
|
||||
todoRow.background = style.editorBackground
|
||||
body.background = style.editorBackground
|
||||
viewport.background = style.editorBackground
|
||||
title.font = style.boldUiFont
|
||||
title.font = style.boldFont
|
||||
title.foreground = style.editorForeground
|
||||
cost.font = style.uiFont
|
||||
cost.font = style.regularFont
|
||||
cost.foreground = style.editorForeground
|
||||
context.font = style.uiFont
|
||||
context.font = style.regularFont
|
||||
context.foreground = style.editorForeground
|
||||
todos.font = style.smallUiFont
|
||||
todos.font = style.smallFont
|
||||
todos.foreground = style.editorForeground
|
||||
tokenTitle.font = style.smallUiFont
|
||||
tokenTitle.font = style.smallFont
|
||||
tokenTitle.foreground = style.editorForeground
|
||||
input.font = style.smallUiFont
|
||||
input.font = style.smallFont
|
||||
input.foreground = style.editorForeground
|
||||
output.font = style.smallUiFont
|
||||
output.font = style.smallFont
|
||||
output.foreground = style.editorForeground
|
||||
cacheRead.font = style.smallUiFont
|
||||
cacheRead.font = style.smallFont
|
||||
cacheRead.foreground = style.editorForeground
|
||||
cacheWrite.font = style.smallUiFont
|
||||
cacheWrite.font = style.smallFont
|
||||
cacheWrite.foreground = style.editorForeground
|
||||
bar.applyStyle(style)
|
||||
refresh()
|
||||
@@ -391,7 +391,7 @@ class SessionHeaderPanel(
|
||||
expand.accessibleContext.accessibleName = KiloBundle.message(key)
|
||||
}
|
||||
|
||||
private fun expanded() = PropertiesComponent.getInstance().getBoolean(EXPANDED_KEY, true)
|
||||
private fun expanded() = PropertiesComponent.getInstance().getBoolean(EXPANDED_KEY, false)
|
||||
|
||||
private fun sizeTimeline() {
|
||||
val size = timeline.preferredSize
|
||||
|
||||
+6
-36
@@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.model
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.CollectionListModel
|
||||
@@ -12,18 +13,12 @@ import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.font.FontRenderContext
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
@@ -70,7 +65,11 @@ internal class ModelPickerRenderer(
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
}
|
||||
private val title = SimpleColoredComponent()
|
||||
private val badge = BadgeIcon
|
||||
private val badge = FilledBadgeIcon(
|
||||
KiloBundle.message("model.picker.free"),
|
||||
ModelText.freeBg(),
|
||||
JBColor.namedColor("Kilo.ModelPicker.freeBadgeForeground", JBColor.WHITE),
|
||||
)
|
||||
private val provider = JBLabel()
|
||||
private val head = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
add(title)
|
||||
@@ -157,33 +156,4 @@ internal class ModelPickerRenderer(
|
||||
internal fun badgeVisible(): Boolean = head.getComponent(1).isVisible
|
||||
|
||||
private class BadgeLabel(icon: Icon) : JBLabel(icon)
|
||||
|
||||
private object BadgeIcon : Icon {
|
||||
private val text = KiloBundle.message("model.picker.free")
|
||||
|
||||
override fun getIconWidth(): Int {
|
||||
val font = JBFont.small()
|
||||
val w = font.getStringBounds(text, FontRenderContext(null, true, true)).width.toInt()
|
||||
return w + JBUI.scale(12)
|
||||
}
|
||||
|
||||
override fun getIconHeight(): Int = JBUI.scale(16)
|
||||
|
||||
override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.translate(x, y)
|
||||
g2.color = ModelText.freeBg()
|
||||
g2.fillRoundRect(0, 0, iconWidth, iconHeight, JBUI.scale(4), JBUI.scale(4))
|
||||
g2.color = JBColor.namedColor("Kilo.ModelPicker.freeBadgeForeground", JBColor.WHITE)
|
||||
g2.font = JBFont.small()
|
||||
val fm = g2.fontMetrics
|
||||
val y = (iconHeight + fm.ascent - fm.descent) / 2
|
||||
g2.drawString(text, JBUI.scale(6), y)
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-10
@@ -1,16 +1,9 @@
|
||||
package ai.kilocode.client.session.ui.prompt
|
||||
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import ai.kilocode.client.session.ui.editor.SessionEditorTextField
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.EditorTextField
|
||||
|
||||
internal class PromptEditorTextField(
|
||||
project: Project,
|
||||
private val ctx: SendPromptContext,
|
||||
) : EditorTextField(project, PlainTextFileType.INSTANCE) {
|
||||
override fun uiDataSnapshot(sink: DataSink) {
|
||||
super.uiDataSnapshot(sink)
|
||||
sink.set(PromptDataKeys.SEND, ctx)
|
||||
}
|
||||
}
|
||||
ctx: SendPromptContext,
|
||||
) : SessionEditorTextField(project, ctx)
|
||||
|
||||
+13
-45
@@ -10,6 +10,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.ui.mode.ModePicker
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.RoundedContentPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.iconButton
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
@@ -357,55 +358,22 @@ class PromptPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PromptShell : BorderLayoutPanel() {
|
||||
private val arc = JBValue.UIInteger("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC)
|
||||
private inner class PromptShell : RoundedContentPanel(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
) {
|
||||
private val focus = JBValue.UIInteger("Component.focusWidth", SessionUiStyle.View.Prompt.FOCUS_WIDTH)
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
override fun contentColor() = style.editorScheme.defaultBackground
|
||||
|
||||
override fun outlineColor() = if (UIUtil.isFocusAncestor(editor)) {
|
||||
JBUI.CurrentTheme.Focus.focusColor()
|
||||
} else {
|
||||
SessionUiStyle.View.line()
|
||||
}
|
||||
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
border = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
}
|
||||
override fun outlineWidth() = if (UIUtil.isFocusAncestor(editor)) focus.get() else JBUI.scale(1)
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
try {
|
||||
g2.setRenderingHint(
|
||||
RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON,
|
||||
)
|
||||
g2.color = style.editorScheme.defaultBackground
|
||||
val size = arc.get()
|
||||
g2.fillRoundRect(0, 0, width, height, size, size)
|
||||
val active = UIUtil.isFocusAncestor(editor)
|
||||
g2.color = if (active) {
|
||||
JBUI.CurrentTheme.Focus.focusColor()
|
||||
} else {
|
||||
SessionUiStyle.View.line()
|
||||
}
|
||||
val bw = if (active) focus.get() else JBUI.scale(1)
|
||||
for (idx in 0 until bw) {
|
||||
val inset = idx
|
||||
val w = width - inset * 2 - 1
|
||||
val h = height - inset * 2 - 1
|
||||
if (w > 0 && h > 0) {
|
||||
g2.drawRoundRect(inset, inset, w, h, size, size)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
g2.dispose()
|
||||
}
|
||||
super.paintComponent(g)
|
||||
}
|
||||
override fun cornerArc() = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC))
|
||||
}
|
||||
}
|
||||
|
||||
+20
-12
@@ -1,10 +1,10 @@
|
||||
package ai.kilocode.client.session.ui.style
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.colors.EditorColorsScheme
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Color
|
||||
import java.awt.Font
|
||||
import kotlin.math.roundToInt
|
||||
@@ -14,6 +14,12 @@ import kotlin.math.roundToInt
|
||||
*
|
||||
* Session UI uses this instead of reading editor globals in every component so font and color changes can be applied
|
||||
* consistently through [SessionEditorStyleTarget].
|
||||
*
|
||||
* Editor-specific fields ([transcriptFont], [smallEditorFont], [boldEditorFont], [editorForeground], [editorBackground])
|
||||
* are derived from the active editor color scheme and are used for code/editor-rendered content.
|
||||
*
|
||||
* UI font fields ([headerFont], [hintFont], [regularFont], [boldFont], [smallFont]) come from [UiStyle.Fonts]
|
||||
* and follow standard platform typography — they do not derive from the editor font size.
|
||||
*/
|
||||
data class SessionEditorStyle(
|
||||
val editorScheme: EditorColorsScheme,
|
||||
@@ -24,9 +30,11 @@ data class SessionEditorStyle(
|
||||
val transcriptFont: Font,
|
||||
val smallEditorFont: Font,
|
||||
val boldEditorFont: Font,
|
||||
val uiFont: Font,
|
||||
val smallUiFont: Font,
|
||||
val boldUiFont: Font,
|
||||
val headerFont: Font,
|
||||
val hintFont: Font,
|
||||
val regularFont: Font,
|
||||
val boldFont: Font,
|
||||
val smallFont: Font,
|
||||
) {
|
||||
/** Apply this snapshot to embedded IntelliJ editor components used by session UI. */
|
||||
fun applyToEditor(editor: EditorEx) {
|
||||
@@ -46,9 +54,7 @@ data class SessionEditorStyle(
|
||||
family: String = scheme.editorFontName,
|
||||
size: Int = scheme.editorFontSize,
|
||||
): SessionEditorStyle {
|
||||
val small = scaledSize(size, JBFont.small())
|
||||
val ui = JBUI.Fonts.label().deriveFont(size.toFloat())
|
||||
val smallUi = JBFont.small().deriveFont(small.toFloat())
|
||||
val small = scaledEditorSize(size, JBFont.small())
|
||||
return SessionEditorStyle(
|
||||
editorScheme = scheme,
|
||||
editorFamily = family,
|
||||
@@ -58,14 +64,16 @@ data class SessionEditorStyle(
|
||||
transcriptFont = Font(family, Font.PLAIN, size),
|
||||
smallEditorFont = Font(family, Font.PLAIN, small),
|
||||
boldEditorFont = Font(family, Font.BOLD, size),
|
||||
uiFont = ui,
|
||||
smallUiFont = smallUi,
|
||||
boldUiFont = ui.deriveFont(Font.BOLD),
|
||||
headerFont = UiStyle.Fonts.header(),
|
||||
hintFont = UiStyle.Fonts.hint(),
|
||||
regularFont = UiStyle.Fonts.regular(),
|
||||
boldFont = UiStyle.Fonts.bold(),
|
||||
smallFont = UiStyle.Fonts.small(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun scaledSize(size: Int, font: Font): Int {
|
||||
val base = JBUI.Fonts.label().size.coerceAtLeast(1)
|
||||
private fun scaledEditorSize(size: Int, font: Font): Int {
|
||||
val base = com.intellij.util.ui.JBUI.Fonts.label().size.coerceAtLeast(1)
|
||||
val ratio = font.size.toFloat() / base
|
||||
return (size * ratio).roundToInt().coerceAtLeast(1)
|
||||
}
|
||||
|
||||
+5
@@ -72,6 +72,11 @@ object SessionUiStyle {
|
||||
const val USER_BORDER_HORIZONTAL_PADDING = 12
|
||||
}
|
||||
|
||||
/** Permission card command preview limits. */
|
||||
object Permission {
|
||||
const val COMMAND_LINES = 3
|
||||
}
|
||||
|
||||
/** Tool card preview limits and state colors. */
|
||||
object Tool {
|
||||
const val BODY_LINES = 15
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import ai.kilocode.client.session.model.Compaction
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ui.components.JBLabel
|
||||
@@ -67,8 +68,8 @@ class CompactionView(@Suppress("UNUSED_PARAMETER") compaction: Compaction) : Par
|
||||
override fun update(content: Content) {} // compaction has no mutable state
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
if (text.font == style.smallUiFont) return
|
||||
text.font = style.smallUiFont
|
||||
if (text.font == style.smallFont) return
|
||||
text.font = style.smallFont
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user