Merge remote-tracking branch 'origin/main' into implement-markdown-file-viewer-sidebar

# Conflicts:
#	packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
#	packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx
#	packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx
This commit is contained in:
marius-kilocode
2026-08-20 10:44:26 +02:00
218 changed files with 4419 additions and 4365 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Fix Agent Manager ignoring requests to start new sessions on OpenAI Responses API models, where a start request was answered with a list of existing sessions instead of creating the worktree or session.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Capture `@terminal` from the focused Agent Manager terminal, including embedded Run and Setup terminals, instead of using an unrelated active VS Code terminal.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep tool-output action buttons visible above streamed output.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Require the todo tool to update multi-step lists after each completed task.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Rework PR review comments in the Agent Manager PR panel: resolved threads now collapse into one-line rows in a Resolved group instead of being dimmed, each thread shows its replies, and every card has prominent Send to agent, Resolve, Copy, Open file, and Open on GitHub actions. A single button sends all unresolved comments to the agent, and comments arrive as structured review comments instead of pasted text.
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Remove the experimental agent requirements check and its configuration flag.
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-gateway": major
---
Remove Alibaba and Mistral as native AI SDK providers from Kilo Gateway.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Remove the floating scroll-to-top button from the Agent Manager PR sidebar.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Ensure interactive terminal state is isolated per directory and cleaned up when its scope closes.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep the conversation from shifting when a turn starts or finishes. The working indicator and the session actions now share one row above the composer that keeps its height in both states, instead of resizing two separate areas.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Make Agent Manager shortcuts follow the focused area. In the center, Cmd/Ctrl+T creates a new session tab and Cmd/Ctrl+Shift+T creates a central terminal tab, whether the prompt or a central terminal is focused. In the right sidebar terminal, Cmd/Ctrl+T creates another sidebar terminal tab and Cmd/Ctrl+Shift+T does nothing. Returning from a terminal with Cmd/Ctrl+Shift+M restores the previous session tab before focusing its prompt. Update the shortcut dialog and hints to show the distinct actions.
-10
View File
@@ -283,9 +283,7 @@
"name": "@kilocode/kilo-gateway",
"version": "7.4.22",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.88",
"@ai-sdk/openai-compatible": "2.0.48",
"@clack/prompts": "1.0.0-alpha.1",
@@ -5136,8 +5134,6 @@
"@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@kilocode/kilo-gateway/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="],
"@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=="],
"@kilocode/kilo-indexing/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -5784,10 +5780,6 @@
"@joshwooding/vite-plugin-react-docgen-typescript/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@kilocode/kilo-gateway/@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@kilocode/kilo-gateway/@ai-sdk/mistral/@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=="],
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@npmcli/package-json/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -6186,8 +6178,6 @@
"@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@kilocode/kilo-gateway/@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-2NUgDsd6xqlgBi0cXq00j02ARulVH4D7dOfFwaOwBfE=",
"aarch64-linux": "sha256-Wk/3bmjFCgPjBVxEVYZI5uOUinLy+UYivH8bCrHaT+E=",
"aarch64-darwin": "sha256-/UTl/JFMgWoqmBKO61+y2HPkS52L6sD+f7mS+IzCTic=",
"x86_64-darwin": "sha256-lQi9sFBzWhcFJFwJoXi4cOe2X8Fm6nyOUYXIHPtAX74="
"x86_64-linux": "sha256-9riExKYPZOz7RqMyrtEMiJyxbtBHnCRkwoBrx825t6k=",
"aarch64-linux": "sha256-mCu+gs0rZGv2PRQlojZR+g3TnRXjMdiN0ORrnUwFbRU=",
"aarch64-darwin": "sha256-boQX3rpYs2vgRzIgVwEeyAKJ8b3MzbBbrq8GL5YkKE8=",
"x86_64-darwin": "sha256-flbEqrgSGWDHfuTeVTv3iV8wYCeDHpPeqkj5Mq5WXy0="
}
}
+1 -58
View File
@@ -9,62 +9,6 @@ const Color = Schema.Union([
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
])
// kilocode_change start - agent skill/MCP/VS Code extension requirements schema
const RequirementID = Schema.String.check(
Schema.isMinLength(1),
Schema.isMaxLength(128),
Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
)
const RequirementName = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128), Schema.isPattern(/\S/))
export const VSCodeExtension = Schema.Struct({
name: RequirementName,
id: RequirementID,
})
export type VSCodeExtension = Schema.Schema.Type<typeof VSCodeExtension>
const RequirementGroup = Schema.mutable(Schema.Array(RequirementName)).check(
Schema.isMinLength(1),
Schema.isMaxLength(20),
)
const VSCodeExtensions = Schema.mutable(Schema.Array(VSCodeExtension)).check(
Schema.isMinLength(1),
Schema.isMaxLength(20),
)
export const Requirements = Schema.Struct({
skills: Schema.optional(RequirementGroup),
mcps: Schema.optional(RequirementGroup),
vscode_extensions: Schema.optional(VSCodeExtensions),
}).check(
Schema.makeFilter((input) => {
const issues: Schema.FilterIssue[] = []
if (!input.skills && !input.mcps && !input.vscode_extensions) {
issues.push({ path: [], issue: "At least one requirement group is required" })
}
for (const group of ["skills", "mcps"] as const) {
const seen = new Set<string>()
for (const [index, value] of (input[group] ?? []).entries()) {
if (seen.has(value)) issues.push({ path: [group, index], issue: `Duplicate ${group} requirement` })
seen.add(value)
}
}
const seen = new Set<string>()
for (const [index, extension] of (input.vscode_extensions ?? []).entries()) {
if (seen.has(extension.id)) {
issues.push({ path: ["vscode_extensions", index, "id"], issue: "Duplicate vscode_extensions requirement" })
}
seen.add(extension.id)
}
return issues
}),
)
export type Requirements = Schema.Schema.Type<typeof Requirements>
// kilocode_change end
const AgentSchema = Schema.StructWithRest(
Schema.Struct({
model: Schema.optional(Schema.NullOr(Schema.String)), // kilocode_change - nullable for delete sentinel
@@ -108,7 +52,6 @@ const AgentSchema = Schema.StructWithRest(
// kilocode_change end
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
permission: Schema.optional(ConfigPermissionV1.Info),
requirements: Schema.optional(Requirements), // kilocode_change
}),
[Schema.Record(Schema.String, Schema.Any)],
)
@@ -132,7 +75,7 @@ const KNOWN_KEYS = new Set([
"permission",
"disable",
"tools",
"requirements", // kilocode_change
"requirements", // kilocode_change - ignore declarations from removed agent requirements feature
])
const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
-3
View File
@@ -301,9 +301,6 @@ export const Info = Schema.Struct({
image_generation_model: Schema.optional(Schema.String).annotate({
description: "Model ID to use for image generation (default: openrouter/auto)",
}),
agent_requirements: Schema.optional(Schema.Boolean).annotate({
description: "Require declared agent skills, MCPs, and VS Code extensions before VS Code prompts can run",
}),
native_notebook_tools: Schema.optional(Schema.Boolean).annotate({
description: "Enable native tools for reading, editing, and executing VS Code notebooks",
}),
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d83b6fdde1c87d7bdbd4fae163dea888c2c50a54f175ceef2405699f8f48443e
size 34823
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8dbff32f3c1b73230e1a1c84c1a683235fbf56fff8a418e6d982603f263ead0
size 38718
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cef414cf59da1f632a7d5d9a2e44f792e7e2ea6b4041bcc214cb32c052f851d8
size 46930
oid sha256:24f8e46671ff667d05a5db36fcd45535040a2587202562a40f8735efa351a1e5
size 46989
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cd6334875ae3c633396b90ffbcbd53e5567668e430c8ca97a4c1cc8bc30a0585
size 17933
oid sha256:b7ecf054c5e5ab8daa2ea0e3ef42e2e720a9dc3673d77058ad7df05751990193
size 17036
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a093b9658cc15d0789da5afebde7163ec51e743af59975e2a031aed3e9144d56
size 22336
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b20e8e50b583cc6a884e08648a9f1efcdfc2a9561b97145811ebc9c24d244fd
size 23100
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3b24b0d2694d2eeec0ea4cd827bd3a61bb47150605b838b43e4f422c9b960f0e
size 29744
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd27610a2518018c16e4227728221ed9bf67a7d3c8ab207b2835dc60964ec63d
size 33323
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8ad46cc736472c6a2a46bb80e6622333f49960e74dcb6782d35aa72fa32378c
size 23725
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0a184f1d7c71f9877c4918e93b7e18f2ebe699f802c46846bd60f29459dbc21
size 15864
oid sha256:a2c0fe6b4fefd0925b670b8ffc42acd09eb7ca7bd848d100e7e4a7a85d67a9f5
size 15807
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d7fc23fcb7adf483c0b771ef601b23cb365dc7f40033b00fce703b35910aa4fc
size 27159
oid sha256:94377b6b66f1b35803963ce7951f473e44f16ca8463dfc8a2c81f3e816296533
size 27245
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d11f4004ed3170647d385c14df077d235f5bb9bd6e5dec07557ee2014d553233
size 27302
oid sha256:102793cbc80c80c7c889db669016566ceafe86eb37d65b7505a7483d1e883cbd
size 27527
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8bd56ba87d0c2bbef8a2325ab2e9f0e956c89e2602ff8ca7415d5fb7c9fa1f3
size 29709
oid sha256:cbe5bc3635a41e9aac342c2fcfd6adb1f38e0ce28ea7cf10b9f7297067103673
size 29743
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5a5fdd318bd14100e0c71f7f9055d830db1e59b6af57007608881f433a177ca
size 18703
oid sha256:db7fdb7b9699dc4b668259b61731e3a024c1ed20d8232055681f551889fac086
size 18942
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5b64674aaceb051ba48bf7db489845f50c8bd7f65a108685dda5243103c735a
size 18477
oid sha256:21097ecf30d0e605f2117c5e98f047db7e65eb43f921a380e7ccade21fc6fc34
size 19594
-2
View File
@@ -33,11 +33,9 @@
},
"dependencies": {
"@kilocode/plugin": "workspace:*",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.88",
"@ai-sdk/openai-compatible": "2.0.48",
"@ai-sdk/mistral": "3.0.27",
"@openrouter/ai-sdk-provider": "2.9.0",
"@clack/prompts": "1.0.0-alpha.1",
"ai": "catalog:",
@@ -101,9 +101,7 @@ export const PROMPTS = [
] as const
export const AI_SDK_PROVIDERS = [
"alibaba",
"anthropic",
"mistral",
"openai",
"openai-compatible",
"openrouter",
-10
View File
@@ -1,9 +1,7 @@
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { createAlibaba } from "@ai-sdk/alibaba"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { createMistral } from "@ai-sdk/mistral"
import type { KiloProvider, KiloProviderOptions } from "./types.js"
import { getApiKey } from "./auth/token.js"
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
@@ -78,11 +76,9 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
}
const openrouter = createOpenRouter(sdkOptions)
const alibaba = createAlibaba(sdkOptions)
const anthropic = createAnthropic(sdkOptions)
const openai = createOpenAI(sdkOptions)
const openaiCompatible = createOpenAICompatible({ ...sdkOptions, name: "openaiCompatible" })
const mistral = createMistral(sdkOptions)
return {
languageModel(modelId) {
@@ -97,15 +93,9 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
imageModel(modelId) {
return openrouter.imageModel(modelId)
},
alibaba(modelId) {
return alibaba(modelId)
},
anthropic(modelId) {
return GatewayMetadata.wrap(anthropic(modelId))
},
mistral(modelId) {
return mistral(modelId)
},
openai(modelId) {
return GatewayMetadata.wrap(openai(modelId))
},
-2
View File
@@ -174,9 +174,7 @@ export interface ProviderInfo {
}
export type KiloProvider = Provider & {
alibaba(modelId: string): LanguageModel
anthropic(modelId: string): LanguageModel
mistral(modelId: string): LanguageModel
openai(modelId: string): LanguageModel
openaiCompatible(modelId: string): LanguageModel
}
-1
View File
@@ -113,7 +113,6 @@ plugins {
alias(libs.plugins.kotlin) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.compose.compiler) apply false
}
changelog {
@@ -37,6 +37,5 @@ intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "int
rpc = { id = "rpc", version.ref = "intellij-rpc-plugin" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-jvm-plugin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-jvm-plugin" }
openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" }
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
+11 -4
View File
@@ -73,14 +73,21 @@ function enqueue(job: Job) {
schedule()
}
// When a large batch of diffs becomes near-visible at once, render one diff per
// animation frame. This keeps the UI responsive while preserving expanded state.
const FRAME_BUDGET_MS = 12
// When a batch of diffs becomes near-visible at once, render within a per-frame
// time budget so visible diffs populate smoothly in the same frame while preserving
// 60fps responsiveness for large lists.
function schedule() {
if (frame !== undefined) return
frame = requestAnimationFrame(() => {
frame = undefined
const job = queue.shift()
if (job && !job.cancelled) job.run()
const deadline = performance.now() + FRAME_BUDGET_MS
while (queue.length > 0) {
const job = queue.shift()
if (job && !job.cancelled) job.run()
if (performance.now() >= deadline) break
}
if (queue.length > 0) schedule()
})
}
@@ -293,10 +293,14 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
[data-component="bash-output"] [data-slot="bash-section-actions"] {
position: absolute;
top: 4px;
right: 4px;
right: 20px;
z-index: 1;
display: flex;
align-items: center;
gap: 2px;
padding: 2px;
border-radius: var(--radius-sm);
background: var(--surface-tool-output-base);
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
+3
View File
@@ -4,6 +4,7 @@
### Minor Changes
- [#11219](https://github.com/Kilo-Org/kilocode/pull/11219) [`8013e5f`](https://github.com/Kilo-Org/kilocode/commit/8013e5f50451225bb32b1284579322c137f497e3) Thanks [@sylwester-liljegren](https://github.com/sylwester-liljegren)! - Make file references in agent responses clickable by validating inline code spans against the filesystem. Code spans that match real files in the workspace become clickable links that open the file at the referenced line. Non-existent paths stay as plain code. Also adds fallback workspace search and "File not found" warning when clicking dead links.
- [#13071](https://github.com/Kilo-Org/kilocode/pull/13071) [`3e3dd3d`](https://github.com/Kilo-Org/kilocode/commit/3e3dd3dcb2a711192ec2477ef7c0532b11f67886) Thanks [@cosi-conda](https://github.com/cosi-conda)! - Add Agent Manager PR comment actions: resolve/unresolve review threads, jump to comments section, and scroll-to-top for PR diff view.
@@ -12,6 +13,8 @@
### Patch Changes
- Make Agent Manager shortcuts follow the focused area. In the center, `Cmd+T` / `Ctrl+T` creates a new session tab and `Cmd+Shift+T` / `Ctrl+Shift+T` creates a central terminal tab, whether the prompt or a central terminal is focused. In the right sidebar terminal, `Cmd+T` / `Ctrl+T` creates another sidebar terminal tab and `Cmd+Shift+T` / `Ctrl+Shift+T` does nothing. Returning from a terminal with `Cmd+Shift+M` / `Ctrl+Shift+M` restores the previous session tab before focusing its prompt.
- [#13063](https://github.com/Kilo-Org/kilocode/pull/13063) [`ca9a99f`](https://github.com/Kilo-Org/kilocode/commit/ca9a99ffd8f118b5445e0fc2c890c4c6dd797d66) - Show and select the model's default reasoning variant in chat and Agent Manager.
- [#13100](https://github.com/Kilo-Org/kilocode/pull/13100) [`753d560`](https://github.com/Kilo-Org/kilocode/commit/753d5609859f2b646c404392e71ca048714f61dd) - Support structured AWS access keys and Google Cloud service-account JSON when connecting Bedrock and Vertex AI in VS Code.
+16 -5
View File
@@ -296,8 +296,13 @@
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.newTerminal",
"title": "Agent Manager: New Terminal Tab (Experimental)",
"command": "kilo-code.new.agentManager.newTerminalTab",
"title": "Agent Manager: New Terminal Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.newSideTerminal",
"title": "Agent Manager: New Sidebar Terminal Tab",
"category": "Kilo Code"
},
{
@@ -735,13 +740,19 @@
"command": "kilo-code.new.agentManager.newTab",
"key": "ctrl+t",
"mac": "cmd+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.newTerminal",
"command": "kilo-code.new.agentManager.newTerminalTab",
"key": "ctrl+shift+t",
"mac": "cmd+shift+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.newSideTerminal",
"key": "ctrl+t",
"mac": "cmd+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.closeTab",
+27 -48
View File
@@ -51,7 +51,6 @@ import {
import { GitOps } from "./agent-manager/GitOps"
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
import { createMarketplaceRemover, removeMcp } from "./kilo-provider/remove-config-item"
import { AgentRequirementsController } from "./kilo-provider/agent-requirements-controller"
import type { RemoteStatusService } from "./services/RemoteStatusService"
import { resolveProjectDirectory } from "./project-directory"
import { seedSessionStatuses } from "./session-status"
@@ -319,7 +318,7 @@ type ContextRequestMessage =
| { type: "requestFileSearch"; query: string; requestId: string; sessionID?: string }
| { type: "requestSessionSearch"; requestId: string; sessionID?: string }
| { type: "requestFilePicker"; requestId: string }
| { type: "requestTerminalContext"; requestId: string; sessionID?: string }
| { type: "requestTerminalContext"; requestId: string; sessionID?: string; agentManagerContext?: string }
export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider {
public static readonly viewType = "kilo-code.SidebarProvider"
@@ -468,7 +467,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private diffViewerProvider: import("./diff/DiffViewerProvider").DiffViewerProvider | undefined
private remoteService: RemoteStatusService | null = null
private unsubscribeRemote: (() => void) | null = null
private readonly requirements: AgentRequirementsController
constructor(
private readonly extensionUri: vscode.Uri,
@@ -481,24 +479,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.unsubscribeSandboxPreference = this.connectionService.sandboxPreference?.onChange(() => {
if (this.connectionState === "connected") void this.fetchAndSendSandboxDefault()
})
this.requirements = new AgentRequirementsController({
post: (msg) => this.postMessage(msg),
client: () => this.client,
connected: () => this.connectionState === "connected",
generation: () => this.connectionGeneration,
root: () => this.getRootDirectory(),
folders: () => vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath),
project: () => this.projectDirectory,
sessions: () => this.sessionDirectories,
worktrees: this.opts.worktreeDirectories,
extension: (id) => vscode.extensions.getExtension(id),
subscribe:
typeof vscode.extensions.onDidChange === "function"
? (listener) => vscode.extensions.onDidChange(listener)
: undefined,
error: getErrorMessage,
})
TelemetryProxy.getInstance().setProvider(this)
}
@@ -576,7 +556,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.cachedConfigMessage = null
this.postMessage({ type: "workspaceDirectoryChanged", directory: directory ?? "" })
this.postMessage({ type: "configBindingExpired", reason: "project-changed" })
this.requirements.clear()
}
public setDiffVirtualProvider(provider: import("./DiffVirtualProvider").DiffVirtualProvider): void {
@@ -668,7 +647,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.cachedAgentsMessage = null
this.cachedConfigMessage = null
await Promise.all([this.fetchAndSendAgents(), this.fetchAndSendConfig()])
this.requirements.clear()
},
storage: this.extensionContext?.globalStorageUri,
}
@@ -845,7 +823,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const current = this.sessionDirectories.get(sessionId) ?? this.getRootDirectory()
this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), current)
this.sessionDirectories.set(sessionId, directory)
this.requirements.clear()
if (this.connectionState === "connected") void this.fetchAndSendSandboxStatus(sessionId)
}
@@ -853,7 +830,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const current = this.sessionDirectories.get(sessionId) ?? this.getRootDirectory()
this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), current)
this.sessionDirectories.delete(sessionId)
this.requirements.clear()
if (this.connectionState === "connected") void this.fetchAndSendSandboxStatus(sessionId)
}
@@ -1016,6 +992,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private setupWebviewMessageHandler(webview: vscode.Webview): void {
this.webviewMessageDisposable?.dispose()
this.setFocusTarget("other")
this.autocompleteConfigDisposable?.dispose()
this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg))
this.indexingConfigDisposable?.dispose()
@@ -1280,16 +1257,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "requestSkills":
this.fetchAndSendSkills().catch((e) => console.error("[Kilo New] fetchAndSendSkills failed:", e))
break
case "requestAgentRequirements":
this.requirements
.fetch({
agent: message.agent,
directory: message.directory,
sessionID: message.sessionID,
force: message.force === true,
})
.catch((e) => console.error("[Kilo New] fetchAndSendAgentRequirements failed:", e))
break
case "requestCommands":
this.fetchAndSendCommands().catch((e) => console.error("[Kilo New] fetchAndSendCommands failed:", e))
break
@@ -1569,11 +1536,32 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.webviewMessageDisposable = watchWorkStyleConfig((msg) => this.postMessage(msg), this.webviewMessageDisposable)
}
private handleWebviewFocusMessage(message: TypedWebviewMessage & { focused?: unknown }): void {
if (message.type !== "webviewFocusChanged") return
if (this.opts.focusContext) {
private handleWebviewFocusMessage(message: TypedWebviewMessage & { focused?: unknown; target?: unknown }): void {
if (message.type === "webviewFocusChanged" && this.opts.focusContext) {
void vscode.commands.executeCommand("setContext", this.opts.focusContext, message.focused === true)
}
if (message.type === "webviewFocusChanged" && message.focused === true) {
if (this.opts.focusTargetContext) this.postMessage({ type: "agentManager.focusContextRequested" })
return
}
if (message.type === "webviewFocusChanged" && message.focused !== true) {
this.setFocusTarget("other")
return
}
if (message.type !== "agentManagerFocusChanged") return
const target =
message.target === "prompt" || message.target === "mainTerminal" || message.target === "sideTerminal"
? message.target
: "other"
this.setFocusTarget(target)
}
private setFocusTarget(target: "prompt" | "mainTerminal" | "sideTerminal" | "other"): void {
const contexts = this.opts.focusTargetContext
if (!contexts) return
void vscode.commands.executeCommand("setContext", contexts.prompt, target === "prompt")
void vscode.commands.executeCommand("setContext", contexts.mainTerminal, target === "mainTerminal")
void vscode.commands.executeCommand("setContext", contexts.sideTerminal, target === "sideTerminal")
}
private handleChildSyncMessage(
@@ -1977,7 +1965,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: getSession failed (non-critical):", e))
this.postMessage({ type: "workspaceDirectoryChanged", directory: this.getWorkspaceDirectory(sessionID) })
this.requirements.clear()
this.client.session
.status({ directory: dir })
.then((r) => {
@@ -2671,7 +2658,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.cachedSkillsMessage = null
this.cachedCommandsMessage = null
await Promise.all([this.fetchAndSendSkills(), this.fetchAndSendCommands()])
this.requirements.clear()
return true
}
@@ -2688,7 +2674,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
this.cachedAgentsMessage = null
await this.fetchAndSendAgents()
this.requirements.clear()
}
private async handleRemoveMcp(name: string): Promise<void> {
@@ -2700,7 +2685,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private async refreshMcpStatus(): Promise<void> {
await this.fetchAndSendMcpStatus()
this.requirements.clear()
}
private async fetchAndSendMcpStatus(): Promise<void> {
@@ -3287,7 +3271,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
settings: this.configSettings(),
features: configFeatures(snapshot.effective),
})
this.requirements.clear()
await Promise.all([
refreshProviders ? this.fetchAndSendProviders() : Promise.resolve(),
refreshAgents ? this.fetchAndSendAgents() : Promise.resolve(),
@@ -3750,7 +3733,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
parts.push({ type: "text", text, metadata: review ? reviewMetadata(review) : undefined })
await this.requirements.assertAgentRequirements(agent, dir)
const editorContext = await this.gatherEditorContext(dir)
if (draftID && this.closedDrafts.delete(draftID)) {
for (const [k, v] of this.draftSessions) if (v.sid === sid) this.draftSessions.delete(k)
@@ -3845,7 +3827,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
source: f.source,
}))
await this.requirements.assertAgentRequirements(agent, dir)
await this.checkpoints.get(sid)
await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Command request", () =>
this.withRetry(
@@ -4215,7 +4196,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Re-fetch all server-side state after an auth change. */
private async reloadAfterAuthChange(): Promise<void> {
this.requirements.clear()
await this.fetchAndSendConfig()
await Promise.all([
this.fetchAndSendProviders(),
@@ -4521,7 +4501,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Fetch and push the updated config + refresh agents and providers so the
// Settings panel and mode/model pickers reflect the change.
if (event.type === "global.config.updated") {
this.requirements.clear()
void Promise.all([this.fetchAndSendConfigUpdated(), this.fetchAndSendAgents(), this.fetchAndSendProviders()])
return
}
@@ -5118,6 +5097,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (this.opts.focusContext) {
void vscode.commands.executeCommand("setContext", this.opts.focusContext, false)
}
this.setFocusTarget("other")
this.unsubscribeRemote?.()
this.streams.focus(undefined)
this.connectionService.unregisterVisible(this.instanceId)
@@ -5161,7 +5141,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.anacondaDesktop.dispose()
this.aborts.clear()
this.sessionStatusMap.clear()
this.requirements.dispose()
this.ignoreController?.dispose()
this.chatAutocomplete?.dispose()
disposeGitChangesTarget()
@@ -643,7 +643,6 @@ export class AgentManagerProvider implements Disposable {
})
return null
}
if (
m.type === "requestSandboxDefault" ||
m.type === "setSandboxDefault" ||
@@ -668,9 +667,9 @@ export class AgentManagerProvider implements Disposable {
this.activeSessionId = m.draftID
return msg
}
if (m.type === "requestTerminalContext") {
if (!m.sessionID || this.terminalManager.prepareContext(m.sessionID)) return msg
const ready = this.terminalManager.prepareContext(m.sessionID, m.agentManagerContext)
if (ready) return msg
this.panel?.postMessage({
type: "terminalContextError",
requestId: m.requestId,
@@ -5,7 +5,14 @@ import { execWithShellEnv } from "./shell-env"
import { execGhRead } from "./gh"
import { classifyPRError } from "./git-import"
import type { Semaphore } from "./semaphore"
import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./pr/am-pr-utils"
import {
parsePRResult,
checkStatus,
commentsSig,
formatCheckDuration,
parseComments,
parseReviewers,
} from "./pr/am-pr-utils"
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./pr/am-pr-types"
interface PRStatusPollerOptions {
@@ -273,7 +280,7 @@ export class PRStatusPoller {
}
const reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",")
const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}`
const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}:${commentsSig(comments?.comments)}`
if (this.lastHash.get(worktreeId) === hash) return
this.lastHash.set(worktreeId, hash)
@@ -489,13 +496,15 @@ export class PRStatusPoller {
nodes {
id
isResolved
comments(first: 1) {
isOutdated
comments(first: 10) {
nodes {
id
author { login avatarUrl }
body
path
line
originalLine
url
createdAt
diffHunk
@@ -180,7 +180,7 @@ export class SessionTerminalManager {
/**
* Show the terminal for a session if it already exists (used when switching sessions).
* Returns true if the terminal was shown, false if no terminal exists for the session.
* Pass preserveFocus=true to keep focus on the current editor (default for session switching).
* Pass preserveFocus=true to keep focus on the current editor (the default).
*/
showExisting(sessionId: string, preserveFocus = true): boolean {
return this.showExistingKey(SessionTerminalManager.sessionKey(sessionId), preserveFocus)
@@ -211,10 +211,18 @@ export class SessionTerminalManager {
return undefined
}
prepareContext(sessionId: string): boolean {
if (this.showExisting(sessionId)) return true
prepareContext(sessionId?: string, context?: string): boolean {
const key =
context === undefined
? undefined
: context === "local"
? SessionTerminalManager.LOCAL_KEY
: SessionTerminalManager.worktreeKey(context)
if (sessionId && this.showExisting(sessionId, false)) return true
if (key && this.showExistingKey(key, false)) return true
const active = this.activeKey()
return !active || active === SessionTerminalManager.sessionKey(sessionId)
if (active === undefined) return this.host.activeTerminal() !== undefined
return (sessionId !== undefined && active === SessionTerminalManager.sessionKey(sessionId)) || active === key
}
dispose(): void {
@@ -43,26 +43,47 @@ const GLOBAL_KEYBINDINGS: Record<string, string> = {
"kilo-code.new.cyclePreviousAgentMode": "cyclePreviousAgentMode",
}
function addBinding(bindings: Record<string, string>, name: string, value: string, when?: string): void {
if (name === "newTerminalTab" && when?.includes("!kilo-code.new.agentManagerSideTerminalFocused")) {
bindings.newTerminalCenter = value
return
}
if (name === "newSideTerminal" && when?.includes("agentManagerSideTerminalFocused")) {
bindings.newTerminalTerminal = value
return
}
if (name === "newTerminal" || name === "newTerminalTab" || name === "newSideTerminal") return
bindings[name] = value
}
function addRawBinding(
bindings: Record<string, string>,
kb: { command: string; key?: string; mac?: string; when?: string },
mac: boolean,
): void {
const raw = mac ? (kb.mac ?? kb.key) : kb.key
if (!raw) return
const value = formatKeybinding(raw, mac)
if (kb.command.startsWith(AM_PREFIX)) {
addBinding(bindings, kb.command.slice(AM_PREFIX.length), value, kb.when)
return
}
const name = GLOBAL_KEYBINDINGS[kb.command]
if (name) bindings[name] = value
}
/**
* Build a keybinding map from VS Code's raw `contributes.keybindings` array.
* Returns a record of action name → formatted shortcut string.
*/
export function buildKeybindingMap(
keybindings: Array<{ command: string; key?: string; mac?: string }>,
keybindings: Array<{ command: string; key?: string; mac?: string; when?: string }>,
mac: boolean,
): Record<string, string> {
const bindings: Record<string, string> = {}
for (const kb of keybindings) {
const raw = mac ? (kb.mac ?? kb.key) : kb.key
if (!raw) continue
if (kb.command.startsWith(AM_PREFIX)) {
bindings[kb.command.slice(AM_PREFIX.length)] = formatKeybinding(raw, mac)
continue
}
const name = GLOBAL_KEYBINDINGS[kb.command]
if (name) bindings[name] = formatKeybinding(raw, mac)
addRawBinding(bindings, kb, mac)
}
// Ensure fallback bindings are always present (may be missing from
@@ -74,6 +95,9 @@ export function buildKeybindingMap(
if (!bindings.previousTerminal)
bindings.previousTerminal = formatKeybinding(mac ? "cmd+shift+[" : "ctrl+shift+[", mac)
if (!bindings.nextTerminal) bindings.nextTerminal = formatKeybinding(mac ? "cmd+shift+]" : "ctrl+shift+]", mac)
if (!bindings.newTerminalCenter)
bindings.newTerminalCenter = formatKeybinding(mac ? "cmd+shift+t" : "ctrl+shift+t", mac)
if (!bindings.newTerminalTerminal) bindings.newTerminalTerminal = formatKeybinding(mac ? "cmd+t" : "ctrl+t", mac)
return bindings
}
@@ -167,7 +167,7 @@ export interface Host {
createOutput(name: string): OutputHandle
/** Read extension keybinding metadata. */
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string }>
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string; when?: string }>
/** Copy text to the system clipboard. */
copyToClipboard(text: string): void
@@ -10,6 +10,7 @@ import type { Disposable } from "./host"
import type { Semaphore } from "./semaphore"
import { PRStatusPoller } from "./PRStatusPoller"
import { resolveComment, unresolveComment } from "./pr/PRActions"
import { ghErrorReason } from "./pr/am-pr-utils"
interface PRBridgeHost {
getWorktrees(): Worktree[]
@@ -124,6 +125,7 @@ export class PRStatusBridge {
worktreeId: id,
threadId,
success: false,
error: ghErrorReason(msg),
})
},
)
@@ -12,6 +12,7 @@ export interface GhComment {
body?: string
path?: string
line?: number
originalLine?: number
url?: string
createdAt?: string
diffHunk?: string
@@ -19,6 +20,7 @@ export interface GhComment {
export interface GhThread {
id?: string
isResolved?: boolean
isOutdated?: boolean
comments?: { nodes?: GhComment[] }
}
export interface GhReviewRequest {
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto"
import type { CheckStatus, PRComment, PRReviewer, ReviewerState } from "../types"
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types"
@@ -69,8 +70,10 @@ const REVIEWER_STATE: Record<string, ReviewerState> = {
export function parseComments(threads: GhThread[]): PRComment[] {
const items: PRComment[] = []
for (const thread of threads) {
const first = thread.comments?.nodes?.[0]
const nodes = thread.comments?.nodes ?? []
const first = nodes[0]
if (!first) continue
const replies = nodes.slice(1).map((node) => ({ author: node.author?.login ?? "unknown", body: node.body ?? "" }))
items.push({
id: first.id,
threadId: thread.id ?? first.id,
@@ -78,11 +81,14 @@ export function parseComments(threads: GhThread[]): PRComment[] {
avatar: first.author?.avatarUrl,
body: first.body ?? "",
file: first.path,
line: first.line,
// An outdated thread has no current line, so fall back to the line it was written against.
line: first.line ?? first.originalLine,
url: first.url,
resolved: thread.isResolved ?? false,
outdated: thread.isOutdated ?? false,
createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined,
diffHunk: first.diffHunk,
replies: replies.length > 0 ? replies : undefined,
})
}
return items
@@ -105,3 +111,27 @@ export function parseReviewers(requests: GhReviewRequest[], reviews: GhReview[])
}
return [...map.values()]
}
/**
* Short, user-facing reason from a failed `gh` invocation. The raw message
* repeats the whole command line, which is useless inside a comment card.
*/
export function ghErrorReason(message: string): string {
const lines = message
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("Command failed"))
const last = [...lines].reverse().find((line) => !line.startsWith("query") && !line.startsWith("mutation"))
return (last ?? message.trim()).replace(/^gh:\s*/, "").slice(0, 200)
}
/**
* Signature of the comment threads, for poll deduplication. Thread and
* unresolved counts alone hide edits and new replies, which the panel renders.
*/
export function commentsSig(comments?: PRComment[]): string {
if (!comments?.length) return ""
return createHash("sha256")
.update(JSON.stringify(comments ?? []))
.digest("hex")
}
@@ -59,6 +59,11 @@ export interface PRCheck {
duration?: string
}
export interface PRCommentReply {
author: string
body: string
}
export interface PRComment {
id: string
threadId: string
@@ -69,8 +74,10 @@ export interface PRComment {
line?: number
url?: string
resolved: boolean
outdated: boolean
createdAt?: number
diffHunk?: string
replies?: PRCommentReply[]
}
export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
@@ -914,6 +921,7 @@ interface RequestTerminalContextIn {
type: "requestTerminalContext"
requestId: string
sessionID?: string
agentManagerContext?: string
}
interface ClearSessionIn {
@@ -110,6 +110,11 @@ export class VscodeHost implements Host {
worktreeDirectories: () => opts.worktreeDirectories?.() ?? [],
rootDirectory: opts.workspaceRoot,
disableViewedRegistration: true,
focusTargetContext: {
prompt: "kilo-code.new.agentManagerPromptFocused",
mainTerminal: "kilo-code.new.agentManagerMainTerminalFocused",
sideTerminal: "kilo-code.new.agentManagerSideTerminalFocused",
},
routeService: this.routes,
projectQualifier: () => {
const projectId = opts.projectId?.()
@@ -298,7 +303,7 @@ export class VscodeHost implements Host {
}
}
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string }> {
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string; when?: string }> {
const ext = vscode.extensions.getExtension("kilocode.kilo-code")
return ext?.packageJSON?.contributes?.keybindings ?? []
}
+5 -2
View File
@@ -516,8 +516,11 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.newTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTerminal" })
vscode.commands.registerCommand("kilo-code.new.agentManager.newTerminalTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTerminalTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newSideTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "newSideTerminal" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.closeTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "closeTab" })
@@ -1,198 +0,0 @@
import type { KiloClient } from "@kilocode/sdk/v2/client"
import {
applyVSCodeExtensionRequirements,
requirementDirectory,
requirementKey,
type BackendAgentRequirementResult,
type HostAgentRequirementResult,
} from "./agent-requirements"
type AgentRequirementsClient = {
agentRequirements: (
parameters: { agent: string; directory: string },
options: { throwOnError: true },
) => Promise<{ data: BackendAgentRequirementResult }>
}
type LoadedMessage = {
type: "agentRequirementsLoaded"
result: HostAgentRequirementResult
}
type InvalidatedMessage = {
type: "agentRequirementsInvalidated"
}
type Disposable = {
dispose(): void
}
export type AgentRequirementsRequest = {
agent: string
directory: string
sessionID?: string
force?: boolean
}
export type AgentRequirementsControllerOptions = {
post: (message: LoadedMessage | InvalidatedMessage) => void
client: () => KiloClient | null
connected: () => boolean
generation: () => number
root: () => string
folders: () => readonly string[] | undefined
project: () => string | null | undefined
sessions: () => ReadonlyMap<string, string>
worktrees?: () => readonly string[]
extension: (id: string) => unknown
subscribe?: (listener: () => void) => Disposable
error: (error: unknown) => string
}
export class AgentRequirementsController {
private readonly cache = new Map<string, HostAgentRequirementResult>()
private readonly generations = new Map<string, object>()
private readonly subscription: Disposable | undefined
constructor(private readonly opts: AgentRequirementsControllerOptions) {
this.subscription = opts.subscribe?.(() => this.clear())
}
clear(): void {
const active = this.cache.size > 0 || this.generations.size > 0
this.cache.clear()
this.generations.clear()
if (!active) return
this.opts.post({ type: "agentRequirementsInvalidated" })
}
dispose(): void {
this.subscription?.dispose()
this.cache.clear()
this.generations.clear()
}
fetch(request: AgentRequirementsRequest): Promise<void> {
const dir = this.scope(request.directory, request.sessionID)
if (!dir) {
this.error(request.agent, request.directory, "scope_mismatch", "The agent requirement scope is no longer active")
return Promise.resolve()
}
return this.load(request.agent, dir, request.force === true).then(
(result) => {
if (!this.scope(dir, request.sessionID)) return
this.opts.post({ type: "agentRequirementsLoaded", result })
},
(error) => {
this.error(request.agent, dir, "request_failed", error)
},
)
}
private scope(requested: string, sessionID?: string): string | undefined {
return requirementDirectory({
requested,
sessionID,
workspaceDirectory: this.opts.root(),
workspaceDirectories: this.opts.folders(),
projectDirectory: this.opts.project(),
sessionDirectories: this.opts.sessions(),
worktreeDirectories: this.opts.worktrees,
})
}
private apply(result: BackendAgentRequirementResult, directory: string): HostAgentRequirementResult {
return applyVSCodeExtensionRequirements({ ...result, directory }, (id) => this.opts.extension(id))
}
private error(agent: string, directory: string, code: "scope_mismatch" | "request_failed", error: unknown): void {
this.opts.post({
type: "agentRequirementsLoaded",
result: {
agent,
directory,
enabled: true,
state: "error",
skills: [],
mcps: [],
vscode_extensions: [],
error: {
code,
message: this.opts.error(error) || "Failed to check agent requirements",
},
},
})
}
async assertAgentRequirements(agent: string | undefined, directory: string): Promise<void> {
if (!agent) return
const dir = this.scope(directory)
if (!dir) throw new Error("The agent requirement scope is no longer active")
const result = await this.load(agent, dir)
if (!this.blocked(result)) return
this.opts.post({ type: "agentRequirementsLoaded", result })
throw new Error(this.message(result))
}
private async load(agent: string, directory: string, force = false): Promise<HostAgentRequirementResult> {
const key = requirementKey(agent, directory)
if (!force) {
const cached = this.cache.get(key)
if (cached) return cached
}
const client = this.opts.client()
if (!client || !this.opts.connected()) throw new Error("Not connected to CLI backend")
const connection = this.opts.generation()
const token = {}
this.generations.set(key, token)
const endpoint = client.kilocode as typeof client.kilocode & AgentRequirementsClient
const response = await endpoint.agentRequirements({ agent, directory }, { throwOnError: true }).catch((error) => {
if (this.generations.get(key) === token) this.generations.delete(key)
throw error
})
if (this.opts.generation() !== connection || this.opts.client() !== client) {
if (this.generations.get(key) === token) this.generations.delete(key)
throw new Error("Connection changed while checking agent requirements")
}
if (this.generations.get(key) !== token) throw new Error("Agent requirement check was superseded")
const result = this.apply(response.data, directory)
this.cache.set(key, result)
this.generations.delete(key)
return result
}
private blocked(result: HostAgentRequirementResult): boolean {
if (!result.enabled || result.state === "disabled") return false
if (result.state === "blocked" || result.state === "error") return true
return (
result.skills.some((item) => item.status !== "ready") ||
result.mcps.some((item) => item.status !== "ready") ||
result.vscode_extensions.some((item) => item.status !== "ready")
)
}
private message(result: HostAgentRequirementResult): string {
if (result.error?.message) return result.error.message
const missing = [
...result.skills.filter((item) => item.status !== "ready").map((item) => `skill ${item.name}`),
...result.mcps.filter((item) => item.status !== "ready").map((item) => `MCP ${item.name}`),
...result.vscode_extensions
.filter((item) => item.status !== "ready")
.map((item) => `VS Code extension ${item.name}`),
]
if (missing.length) return `Agent requirements are not met: ${missing.join(", ")}`
return "Agent requirements are not met"
}
}
@@ -1,103 +0,0 @@
import * as path from "path"
import { sameDirectory } from "../kilo-provider-utils"
export type RequirementStatus = "ready" | "missing" | "error"
export type RequirementState = "disabled" | "ready" | "blocked" | "error"
export type RequirementErrorCode =
| "unknown_agent"
| "malformed_declaration"
| "discovery_failed"
| "mcp_status_failed"
| "scope_mismatch"
| "request_failed"
export type RequirementItem = {
name: string
status: RequirementStatus
message?: string
}
export type BackendVSCodeExtensionRequirement = {
name: string
id: string
message?: string
}
export type BackendAgentRequirementResult = {
agent: string
directory: string
enabled: boolean
state: RequirementState
skills: RequirementItem[]
mcps: RequirementItem[]
vscode_extensions: BackendVSCodeExtensionRequirement[]
error?: {
code: RequirementErrorCode
message: string
}
}
export type HostVSCodeExtensionRequirement = BackendVSCodeExtensionRequirement & {
status: RequirementStatus
}
export type HostAgentRequirementResult = Omit<BackendAgentRequirementResult, "vscode_extensions"> & {
vscode_extensions: HostVSCodeExtensionRequirement[]
}
export type RequirementDirectoryInput = {
requested: string
sessionID?: string
workspaceDirectory: string
workspaceDirectories?: readonly string[]
projectDirectory?: string | null
sessionDirectories: ReadonlyMap<string, string>
worktreeDirectories?: () => readonly string[]
}
export type VSCodeExtensionLookup = (id: string) => unknown
function normalize(directory: string): string {
if (!directory) return ""
const resolved = path.resolve(directory)
return process.platform === "win32" ? resolved.toLowerCase() : resolved
}
export function requirementKey(agent: string, directory: string): string {
return `${normalize(directory)}\0${agent}`
}
export function requirementDirectory(input: RequirementDirectoryInput): string | undefined {
if (!input.requested) return undefined
const dirs = [
input.sessionID ? input.sessionDirectories.get(input.sessionID) : undefined,
input.workspaceDirectory,
...(input.workspaceDirectories ?? []),
input.projectDirectory ?? undefined,
...input.sessionDirectories.values(),
...(input.worktreeDirectories?.() ?? []),
].filter((dir): dir is string => !!dir)
return dirs.find((dir) => sameDirectory(dir, input.requested))
}
export function applyVSCodeExtensionRequirements(
result: BackendAgentRequirementResult,
lookup: VSCodeExtensionLookup,
): HostAgentRequirementResult {
const extensions = result.vscode_extensions.map((extension) => {
const status: RequirementStatus = lookup(extension.id) ? "ready" : "missing"
return { ...extension, status }
})
const blocked =
result.enabled && result.state === "ready" && extensions.some((extension) => extension.status !== "ready")
return {
...result,
state: blocked ? "blocked" : result.state,
vscode_extensions: extensions,
}
}
@@ -3,6 +3,12 @@ import type { ProjectRouteService } from "../agent-manager/project/route"
export type KiloProviderOptions = {
/** Context key updated from focus events reported by this provider's webview. */
focusContext?: string
/** Context keys updated by Agent Manager prompt and terminal focus events. */
focusTargetContext?: {
prompt: string
mainTerminal: string
sideTerminal: string
}
projectDirectory?: string | null
platform?: string
snapshotInitialization?: "wait"
@@ -41,11 +41,6 @@ export interface AgentContent {
prompt: string
options?: Record<string, unknown>
permission?: Record<string, unknown>
requirements?: {
skills?: string[]
mcps?: string[]
vscode_extensions?: Array<{ name: string; id: string }>
}
}
export interface AgentMarketplaceItem extends MarketplaceItemBase {
@@ -7,9 +7,33 @@ export interface ReviewCommentData {
selectedText: string
}
export interface PRReviewReply {
author: string
body: string
}
/** A GitHub PR review thread handed to the agent from the Agent Manager PR panel. */
export interface PRReviewCommentData {
id: string
origin: "pr"
author: string
body: string
file?: string
line?: number
diffHunk?: string
outdated?: boolean
replies?: PRReviewReply[]
}
export type ReviewCommentEntry = ReviewCommentData | PRReviewCommentData
export function isPRReviewComment(item: ReviewCommentEntry): item is PRReviewCommentData {
return "origin" in item && item.origin === "pr"
}
export interface ReviewMessageData {
version: 1
comments: ReviewCommentData[]
comments: ReviewCommentEntry[]
}
interface ReviewMessageView {
@@ -21,24 +45,48 @@ const LIMIT = 100
const TOTAL_LIMIT = 1_000_000
const TEXT_LIMIT = 100_000
const SELECTION_LIMIT = 200_000
const AUTHOR_LIMIT = 256
const REPLY_LIMIT = 20
function escapeInline(value: string): string {
return value.replace(/([\\`*_\[\]{}()#+\-!|])/g, "\\$1")
}
export function formatReviewCommentMarkdown(comment: ReviewCommentData): string {
/** Wrap a snippet in a fence long enough to survive backticks inside it. */
function fenced(value: string): string[] {
const matches = value.match(/`+/g) ?? []
const longest = matches.reduce((max, item) => Math.max(max, item.length), 0)
const fence = "`".repeat(Math.max(3, longest + 1))
return [fence, value, fence]
}
function quote(value: string): string {
return value
.split("\n")
.map((line) => (line ? `> ${line}` : ">"))
.join("\n")
}
function formatPR(comment: PRReviewCommentData): string {
const at = comment.file
? `**${escapeInline(comment.file)}**${comment.line ? ` (line ${comment.line})` : ""}, PR comment`
: "PR comment"
const lines = [`${at} by @${comment.author}${comment.outdated ? " (outdated)" : ""}:`]
if (comment.diffHunk) lines.push(...fenced(comment.diffHunk))
lines.push(comment.body)
for (const reply of comment.replies ?? []) lines.push("", quote(`@${reply.author}: ${reply.body}`))
return lines.join("\n")
}
export function formatReviewCommentMarkdown(comment: ReviewCommentEntry): string {
if (isPRReviewComment(comment)) return formatPR(comment)
const lines = [`**${escapeInline(comment.file)}** (line ${comment.line}):`]
if (comment.selectedText) {
const matches = comment.selectedText.match(/`+/g) ?? []
const longest = matches.reduce((max, item) => Math.max(max, item.length), 0)
const fence = "`".repeat(Math.max(3, longest + 1))
lines.push(fence, comment.selectedText, fence)
}
if (comment.selectedText) lines.push(...fenced(comment.selectedText))
lines.push(comment.comment)
return lines.join("\n")
}
export function formatReviewCommentsMarkdown(comments: ReviewCommentData[]): string {
export function formatReviewCommentsMarkdown(comments: ReviewCommentEntry[]): string {
const lines = ["## Review Comments", ""]
for (const item of comments) {
lines.push(formatReviewCommentMarkdown(item), "")
@@ -56,9 +104,80 @@ function text(value: unknown, limit: number): string | undefined {
return value
}
function parseComment(value: unknown): ReviewCommentData | undefined {
function safe(file: string): boolean {
const absolute = file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(file)
const traversal = file.split(/[\\/]/).includes("..")
return !absolute && !traversal && !file.includes("\0")
}
function parseReply(value: unknown): PRReviewReply | undefined {
const item = record(value)
if (!item) return undefined
const author = text(item.author, AUTHOR_LIMIT)
const body = text(item.body, TEXT_LIMIT)
if (!author || body === undefined) return undefined
return { author, body }
}
function parseReplies(value: unknown): PRReviewReply[] | undefined {
if (!Array.isArray(value) || value.length > REPLY_LIMIT) return undefined
const list: PRReviewReply[] = []
for (const item of value) {
const reply = parseReply(item)
if (!reply) return undefined
list.push(reply)
}
return list
}
/** Optional PR field: `undefined` when absent, `false` when present but invalid. */
function optional(value: unknown, limit: number, valid?: (item: string) => boolean): string | false | undefined {
if (value === undefined) return undefined
const item = text(value, limit)
if (item === undefined) return false
if (valid && !valid(item)) return false
return item
}
function optionalLine(value: unknown): number | false | undefined {
if (value === undefined) return undefined
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return false
return value
}
function parsePR(item: Record<string, unknown>): PRReviewCommentData | undefined {
const id = text(item.id, 512)
const author = text(item.author, AUTHOR_LIMIT)
const body = text(item.body, TEXT_LIMIT)
if (!id || !author || body === undefined) return undefined
const file = optional(item.file, 4_096, safe)
const hunk = optional(item.diffHunk, SELECTION_LIMIT)
const line = optionalLine(item.line)
if (file === false || hunk === false || line === false) return undefined
if (item.outdated !== undefined && typeof item.outdated !== "boolean") return undefined
const replies = item.replies === undefined ? undefined : parseReplies(item.replies)
if (item.replies !== undefined && !replies) return undefined
return {
id,
origin: "pr",
author,
body,
file,
line,
diffHunk: hunk,
outdated: item.outdated,
replies,
}
}
function parseComment(value: unknown): ReviewCommentEntry | undefined {
const item = record(value)
if (!item) return undefined
if (item.origin === "pr") return parsePR(item)
if (item.origin !== undefined) return undefined
const id = text(item.id, 512)
const file = text(item.file, 4_096)
@@ -67,30 +186,39 @@ function parseComment(value: unknown): ReviewCommentData | undefined {
const side = item.side
const line = item.line
if (!id || !file || comment === undefined || selectedText === undefined) return undefined
const absolute = file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(file)
const traversal = file.split(/[\\/]/).includes("..")
if (absolute || traversal || file.includes("\0")) return undefined
if (!safe(file)) return undefined
if (side !== "additions" && side !== "deletions") return undefined
if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return undefined
return { id, file, side, line, comment, selectedText }
}
function weight(item: ReviewCommentEntry): number {
if (!isPRReviewComment(item))
return item.id.length + item.file.length + item.comment.length + item.selectedText.length
const replies = (item.replies ?? []).reduce((total, reply) => total + reply.author.length + reply.body.length, 0)
return (
item.id.length +
item.author.length +
item.body.length +
(item.file?.length ?? 0) +
(item.diffHunk?.length ?? 0) +
replies
)
}
function view(value: unknown, content: string): ReviewMessageView | undefined {
const data = record(value)
if (!data || data.version !== 1 || !Array.isArray(data.comments)) return undefined
if (data.comments.length === 0 || data.comments.length > LIMIT) return undefined
const comments: ReviewCommentData[] = []
const comments: ReviewCommentEntry[] = []
for (const value of data.comments) {
const item = parseComment(value)
if (!item) return undefined
comments.push(item)
}
const size = comments.reduce(
(total, item) => total + item.id.length + item.file.length + item.comment.length + item.selectedText.length,
0,
)
const size = comments.reduce((total, item) => total + weight(item), 0)
if (size > TOTAL_LIMIT) return undefined
const prefix = formatReviewCommentsMarkdown(comments)
+80 -16
View File
@@ -29,14 +29,15 @@ Object.assign(globalThis, {
CustomEvent: window.CustomEvent,
Event: window.Event,
MouseEvent: window.MouseEvent,
MessageEvent: window.MessageEvent,
requestAnimationFrame: window.requestAnimationFrame.bind(window),
cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
})
const { render } = await import("solid-js/web")
const { I18nProvider } = await import("@kilocode/kilo-ui/context")
const { MarkedProvider } = await import("@kilocode/kilo-ui/context/marked")
const { VSCodeProvider } = await import("../../webview-ui/src/context/vscode")
const { LanguageProvider } = await import("../../webview-ui/src/context/language")
const { PRComments } = await import("../../webview-ui/agent-manager/pr/PRComments")
const root = document.createElement("div")
@@ -45,56 +46,119 @@ colors.textContent = ":root { --syntax-keyword: rgb(72, 160, 199); --syntax-stri
document.head.append(colors)
document.body.append(root)
const HUNK =
'@@ -1 +1,14 @@\n+import { File as BaseFile, type FileProps } from "@opencode-ai/ui/file"\n+import type { JSX } from "solid-js"\n+import { createDefaultOptions } from "../pierre"\n+\n export * from "@opencode-ai/ui/file"\n+\n+export function File<T>(props: FileProps<T>) {\n+ const View = BaseFile as unknown as (props: FileProps<T>) => JSX.Element\n+ if (props.mode === "text") return <View {...props} />\n+\n+ // Keep inline file diffs on the same Pierre defaults as the dedicated viewer.\n+ const options = { ...createDefaultOptions<T>(props.diffStyle), ...props } as FileProps<T>\n'
const sent: unknown[] = []
window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data?.type === "appendReviewComments") sent.push(ev.data)
})
const dispose = render(
() => (
<VSCodeProvider>
<I18nProvider
value={
{
locale: () => "en",
t: (key: string) => key,
plural: (key: string) => key,
} as never
}
>
<LanguageProvider>
<MarkedProvider>
<PRComments
worktreeId="wt-test"
comments={{
total: 1,
total: 2,
unresolved: 1,
comments: [
{
id: "PRRC_test",
threadId: "PRRT_test",
id: "PRRC_open",
threadId: "PRRT_open",
author: "kilo-code-bot",
body: "comment body survives Pierre rendering",
file: "packages/kilo-ui/src/components/file.tsx",
line: 14,
resolved: false,
diffHunk:
'@@ -1 +1,14 @@\n+import { File as BaseFile, type FileProps } from "@opencode-ai/ui/file"\n+import type { JSX } from "solid-js"\n+import { createDefaultOptions } from "../pierre"\n+\n export * from "@opencode-ai/ui/file"\n+\n+export function File<T>(props: FileProps<T>) {\n+ const View = BaseFile as unknown as (props: FileProps<T>) => JSX.Element\n+ if (props.mode === "text") return <View {...props} />\n+\n+ // Keep inline file diffs on the same Pierre defaults as the dedicated viewer.\n+ const options = { ...createDefaultOptions<T>(props.diffStyle), ...props } as FileProps<T>\n',
outdated: false,
diffHunk: HUNK,
replies: [{ author: "marius", body: "reply body is visible" }],
},
{
id: "PRRC_done",
threadId: "PRRT_done",
author: "reviewer",
body: "settled discussion\n\nsecond paragraph only shows when expanded",
file: "packages/kilo-ui/src/components/other.tsx",
line: 3,
resolved: true,
outdated: false,
},
],
}}
/>
</MarkedProvider>
</I18nProvider>
</LanguageProvider>
</VSCodeProvider>
),
root,
)
await window.happyDOM.waitUntilComplete()
// The unresolved thread renders expanded, with its hunk and its replies.
const host = root.querySelector("diffs-container")
const shadow = host?.shadowRoot
const keyword = shadow?.querySelector('[data-content] span[style*="--syntax-keyword"]')
const string = shadow?.querySelector('[data-content] span[style*="--syntax-string"]')
assert.match(root.textContent ?? "", /comment body survives Pierre rendering/)
assert.match(root.textContent ?? "", /reply body is visible/)
assert.equal(root.querySelectorAll('[data-component="diff"]').length, 1)
assert.ok(keyword)
assert.ok(string)
assert.match(keyword!.getAttribute("style") ?? "", /--syntax-keyword/)
assert.match(string!.getAttribute("style") ?? "", /--syntax-string/)
assert.notEqual(keyword!.getAttribute("style"), string!.getAttribute("style"))
// The resolved thread is hidden behind a collapsed group.
assert.doesNotMatch(root.textContent ?? "", /settled discussion/)
const groups = [...root.querySelectorAll(".am-pr-panel-section-toggle")]
const resolvedGroup = groups.find((node) => /Resolved \(1\)/.test(node.textContent ?? ""))
assert.ok(resolvedGroup, "resolved group heading is present")
;(resolvedGroup as HTMLButtonElement).click()
await window.happyDOM.waitUntilComplete()
// Opening the group reveals a one-line row, not the whole card.
const rows = [...root.querySelectorAll(".am-pr-comment-head")]
const resolvedRow = rows.find((node) => /reviewer/.test(node.textContent ?? ""))
assert.ok(resolvedRow, "resolved row is present")
assert.equal(resolvedRow!.getAttribute("aria-expanded"), "false")
assert.ok(resolvedRow!.querySelector(".am-pr-comment-preview"), "collapsed row shows a preview")
assert.doesNotMatch(root.textContent ?? "", /second paragraph only shows when expanded/)
// The row expands into a full card whose unresolve action is enabled.
;(resolvedRow as HTMLButtonElement).click()
await window.happyDOM.waitUntilComplete()
assert.equal(resolvedRow!.getAttribute("aria-expanded"), "true")
assert.match(root.textContent ?? "", /second paragraph only shows when expanded/)
const card = resolvedRow!.parentElement!
const actions = [...card.querySelectorAll('[data-component="button"]')]
const unresolve = actions.find((node) => /Unresolve/.test(node.textContent ?? ""))
assert.ok(unresolve, "unresolve button is rendered")
assert.equal((unresolve as HTMLButtonElement).disabled, false)
assert.equal(unresolve!.getAttribute("data-disabled"), null)
// Send to agent hands the thread over as a structured review comment.
const send = [...root.querySelectorAll('[data-component="button"]')].find((node) =>
/Send to agent/.test(node.textContent ?? ""),
)
assert.ok(send, "send button is rendered")
;(send as HTMLButtonElement).click()
await window.happyDOM.waitUntilComplete()
assert.equal(sent.length, 1)
const payload = sent[0] as { comments: { id: string; origin: string; author: string; replies?: unknown[] }[] }
assert.equal(payload.comments.length, 1)
assert.equal(payload.comments[0]!.origin, "pr")
assert.equal(payload.comments[0]!.id, "PRRT_open")
assert.equal(payload.comments[0]!.replies?.length, 1)
// Sending the same thread again is a no-op, and the card button is disabled.
;(send as HTMLButtonElement).click()
await window.happyDOM.waitUntilComplete()
assert.equal(sent.length, 1)
assert.equal((send as HTMLButtonElement).disabled, true)
dispose()
@@ -0,0 +1,159 @@
import { expect, test, type Page } from "@playwright/test"
/**
* The row above the composer swaps the working indicator for the session
* actions when a turn finishes. It used to grow by the actions row while the
* in-transcript indicator placeholder shrank, and the leftover difference
* pushed the conversation text up by a few pixels on every turn boundary.
*
* Measure the real geometry across that swap: the dock height, the transcript
* viewport height, and the on-screen position of the last message all have to
* stay put.
*/
const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern"
const STORY_ID = "chat--chat-view-session-dock-stability"
async function openStory(page: Page) {
await page.setViewportSize({ width: 720, height: 640 })
await page.goto(`/iframe.html?id=${STORY_ID}&viewMode=story&globals=${GLOBALS}`, { waitUntil: "load" })
await page.addStyleTag({
content: `*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }`,
})
await page.waitForSelector('[data-component="session-dock"]')
}
async function geometry(page: Page) {
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
return page.evaluate(() => {
const dock = document.querySelector('[data-component="session-dock"]')
const list = document.querySelector(".message-list")
if (!(dock instanceof HTMLElement) || !(list instanceof HTMLElement)) throw new Error("dock or transcript missing")
return {
dock: dock.getBoundingClientRect().height,
viewport: list.getBoundingClientRect().height,
transcriptBottom: list.getBoundingClientRect().bottom,
}
})
}
test("session dock keeps the transcript still across the working swap", async ({ page }) => {
await openStory(page)
const idle = await geometry(page)
expect(idle.dock).toBeGreaterThan(0)
await page.getByTestId("toggle-busy").click()
await expect(page.locator(".working-indicator")).toBeVisible()
const working = await geometry(page)
expect(working.dock).toBe(idle.dock)
expect(working.viewport).toBe(idle.viewport)
expect(working.transcriptBottom).toBe(idle.transcriptBottom)
await page.getByTestId("toggle-busy").click()
await expect(page.locator(".new-task-button-wrapper")).toBeVisible()
const back = await geometry(page)
expect(back.dock).toBe(idle.dock)
expect(back.viewport).toBe(idle.viewport)
expect(back.transcriptBottom).toBe(idle.transcriptBottom)
})
test("only one of the two states is visible in the dock", async ({ page }) => {
await openStory(page)
// Both states stay laid out so the row keeps reserving the taller height;
// only visibility changes.
await expect(page.locator('[data-component="session-dock"] .new-task-button-wrapper')).toBeVisible()
await expect(page.locator('[data-component="session-dock"] .working-indicator')).toBeHidden()
await page.getByTestId("toggle-busy").click()
await expect(page.locator('[data-component="session-dock"] .working-indicator')).toBeVisible()
await expect(page.locator('[data-component="session-dock"] .new-task-button-wrapper')).toBeHidden()
})
test("the indicator stays a centered lane on a wide surface", async ({ page }) => {
await openStory(page)
// Agent Manager width: a full-width indicator put the spinner at the far-left
// edge and pinned the elapsed time to the far-right edge.
await page.setViewportSize({ width: 1400, height: 640 })
await page.getByTestId("toggle-busy").click()
await expect(page.locator('[data-component="session-dock"] .working-indicator')).toBeVisible()
const lane = await page.evaluate(() => {
const dock = document.querySelector('[data-component="session-dock"]')
const indicator = document.querySelector(".working-indicator")
if (!(dock instanceof HTMLElement) || !(indicator instanceof HTMLElement)) throw new Error("dock missing")
const d = dock.getBoundingClientRect()
// Measure the painted cluster (spinner, label, counter), not the box around it.
const parts = [...indicator.children].map((el) => el.getBoundingClientRect())
const left = Math.min(...parts.map((p) => p.left))
const right = Math.max(...parts.map((p) => p.right))
return {
dockWidth: d.width,
clusterWidth: right - left,
leftGap: left - d.left,
rightGap: d.right - right,
spread: right - left,
}
})
// The cluster stays compact instead of reaching for both edges of the surface.
expect(lane.clusterWidth).toBeLessThan(lane.dockWidth / 2)
// and sits on the dock's centre axis, like the actions row it replaces.
expect(lane.leftGap).toBeGreaterThan(0)
expect(Math.abs(lane.leftGap - lane.rightGap)).toBeLessThanOrEqual(2)
})
test("the counter keeps its width as it ticks", async ({ page }) => {
await openStory(page)
await page.getByTestId("toggle-busy").click()
const elapsed = page.locator(".working-elapsed")
await expect(elapsed).toBeVisible()
// A one-character growth (9s to 10s) must not reflow the cluster.
const before = await elapsed.evaluate((el) => el.getBoundingClientRect().width)
const wide = await elapsed.evaluate((el) => {
const original = el.textContent
el.textContent = "10s"
const width = el.getBoundingClientRect().width
el.textContent = original
return width
})
expect(wide).toBe(before)
})
test("a wrapped narrow-sidebar actions row is not clipped", async ({ page }) => {
await openStory(page)
// Narrow enough for the container query to wrap the actions row onto a
// second line, which a hard-coded dock height cut off behind the composer.
await page.setViewportSize({ width: 340, height: 640 })
const wrapped = await page.evaluate(() => {
const dock = document.querySelector('[data-component="session-dock"]')
const row = document.querySelector(".session-actions-row")
if (!(dock instanceof HTMLElement) || !(row instanceof HTMLElement)) throw new Error("dock or actions missing")
return {
dock: dock.getBoundingClientRect().height,
row: row.getBoundingClientRect().height,
overflowBelow: row.getBoundingClientRect().bottom - dock.getBoundingClientRect().bottom,
}
})
expect(wrapped.row).toBeGreaterThan(0)
expect(wrapped.dock).toBeGreaterThanOrEqual(wrapped.row)
expect(wrapped.overflowBelow).toBeLessThanOrEqual(0)
// The swap still leaves the transcript untouched at this width.
const idle = await geometry(page)
await page.getByTestId("toggle-busy").click()
await expect(page.locator('[data-component="session-dock"] .working-indicator')).toBeVisible()
const working = await geometry(page)
expect(working.dock).toBe(idle.dock)
expect(working.viewport).toBe(idle.viewport)
expect(working.transcriptBottom).toBe(idle.transcriptBottom)
})
@@ -399,15 +399,17 @@ describe("Agent Manager Worktree Actions", () => {
expect(manager).toMatchObject({ key: "ctrl+shift+m", mac: "cmd+shift+m" })
})
it("creates side terminals only while a side terminal owns focus", () => {
it("routes prompt and side-terminal shortcut actions separately", () => {
const source = fs.readFileSync(TSX_FILE, "utf-8")
const start = source.indexOf('else if (msg.action === "newTerminal")')
const start = source.indexOf('else if (msg.action === "newTerminalTab")')
const end = source.indexOf('else if (msg.action === "cycleAgentMode"', start)
const action = source.slice(start, end)
expect(action).toContain("if (terms.sideFocusedId()) termHandlers.addSide()")
expect(action).not.toContain("terminalVisible()")
expect(action).toContain("else termHandlers.requestNew()")
expect(action).toContain('msg.action === "newTerminalTab"')
expect(action).toContain("termHandlers.requestNew()")
expect(action).toContain('msg.action === "newSideTerminal"')
expect(action).toContain("termHandlers.addSide()")
expect(action).not.toContain('msg.action === "newMainTerminal"')
})
it("forwards the quick-worktree command to immediate creation", () => {
@@ -529,7 +531,7 @@ describe("Agent Manager Provider — onMessage routing", () => {
it("terminal context reveals the terminal associated with the originating session", () => {
const text = body("onSessionMessage")
const show = text.indexOf("this.terminalManager.prepareContext(m.sessionID)")
const show = text.indexOf("this.terminalManager.prepareContext(m.sessionID, m.agentManagerContext)")
expect(show).toBeGreaterThan(-1)
expect(text).toContain('type: "terminalContextError"')
})
@@ -1115,7 +1117,6 @@ describe("Shared webview provider shell", () => {
"ImageModelsProvider",
"NotificationsProvider",
"SessionProvider",
"AgentRequirementsProvider",
"MemoryProvider",
"FeedbackProvider",
])
@@ -6,6 +6,7 @@ import {
expandableOpenFiles,
initialOpenFiles,
isDiffExpandable,
reconcileOpenFiles,
sanitizeOpenFiles,
shouldVirtualizeDiff,
toggleOpenFiles,
@@ -148,6 +149,20 @@ describe("agent manager diff state", () => {
expect(toggleOpenFiles(diffs, files)).toEqual([])
})
it("opens newly arriving files while preserving a manual collapse", () => {
const current = [diff({ file: "src/app.ts" }), diff({ file: "src/new.ts" })]
expect(reconcileOpenFiles(current, ["src/app.ts"], ["src/app.ts"])).toEqual({
open: ["src/app.ts", "src/new.ts"],
known: ["src/app.ts", "src/new.ts"],
})
})
it("does not initialize a manual empty snapshot until the first state exists", () => {
const current = [diff({ file: "src/app.ts" })]
expect(reconcileOpenFiles(current, undefined, [])).toEqual({ open: undefined, known: ["src/app.ts"] })
expect(reconcileOpenFiles(current, [], ["src/app.ts"])).toEqual({ open: [], known: ["src/app.ts"] })
})
it("opens images while preventing other non-text diffs from entering open state", () => {
const audio = diff({ file: "audio/alert.wav", summarized: false, additions: 0 })
const image = diff({ file: "assets/banner.png", kind: "image", summarized: true, additions: 0 })
@@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test"
import { Window } from "happy-dom"
import { focusQuestionOption, hasQuestionOption, preservesTextFocus } from "../../webview-ui/agent-manager/focus"
import {
agentManagerFocusTarget,
focusQuestionOption,
hasQuestionOption,
preservesTextFocus,
} from "../../webview-ui/agent-manager/focus"
import { isTextControl } from "../../webview-ui/src/utils/focus"
describe("Agent Manager focus", () => {
@@ -71,4 +76,27 @@ describe("Agent Manager focus", () => {
expect(isTextControl(editor)).toBe(true)
expect(isTextControl(button)).toBe(false)
})
it("resolves prompt and terminal focus from the active DOM owner", () => {
const window = new Window()
const prompt = window.document.createElement("textarea")
const main = window.document.createElement("div")
const side = window.document.createElement("div")
const mainHost = window.document.createElement("div")
const sideHost = window.document.createElement("div")
prompt.className = "prompt-input"
main.className = "am-terminal-layer"
side.className = "am-side-terminal-layer"
mainHost.className = "am-terminal-host"
sideHost.className = "am-terminal-host"
main.append(mainHost)
side.append(sideHost)
expect(agentManagerFocusTarget(prompt)).toBe("prompt")
expect(agentManagerFocusTarget(mainHost)).toBe("mainTerminal")
expect(agentManagerFocusTarget(sideHost)).toBe("sideTerminal")
expect(agentManagerFocusTarget(window.document.body)).toBe("other")
expect(agentManagerFocusTarget(mainHost, true)).toBe("prompt")
expect(agentManagerFocusTarget(prompt, true)).toBe("prompt")
})
})
@@ -0,0 +1,77 @@
import { describe, expect, it } from "bun:test"
import { restoreSessionAfterTerminal } from "../../webview-ui/agent-manager/selection-actions"
describe("Agent Manager session restoration", () => {
it("restores the remembered session after a central terminal", () => {
const selected: Array<[string, boolean]> = []
const created: string[] = []
expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: "session:two",
sessions: [{ id: "session:one" }, { id: "session:two" }],
isPending: () => false,
select: (id, pending) => selected.push([id, pending]),
create: () => {
created.push("created")
return "pending"
},
}),
).toBe("ready")
expect(selected).toEqual([["session:two", false]])
expect(created).toEqual([])
})
it("falls back to the first session when the remembered tab is gone", () => {
const selected: Array<[string, boolean]> = []
expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: "session:gone",
sessions: [{ id: "pending:one" }, { id: "session:two" }],
isPending: (id) => id.startsWith("pending:"),
select: (id, pending) => selected.push([id, pending]),
create: () => "pending",
}),
).toBe("ready")
expect(selected).toEqual([["pending:one", true]])
})
it("creates a real session tab only when no session can be restored", () => {
const created: string[] = []
expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: undefined,
sessions: [],
isPending: () => false,
select: () => undefined,
create: () => {
created.push("created")
return "pending"
},
}),
).toBe("pending")
expect(created).toEqual(["created"])
})
it("does nothing when no central terminal is selected", () => {
const selected: string[] = []
expect(
restoreSessionAfterTerminal({
terminal: undefined,
remembered: "session:one",
sessions: [{ id: "session:one" }],
isPending: () => false,
select: (id) => selected.push(id),
create: () => selected.push("created"),
}),
).toBe("none")
expect(selected).toEqual([])
})
})
@@ -0,0 +1,24 @@
import { describe, expect, it } from "bun:test"
import {
readTerminalOutput,
registerTerminalOutput,
unregisterTerminalOutput,
} from "../../webview-ui/agent-manager/terminal/output"
describe("Agent Manager terminal output", () => {
it("reads and unregisters a terminal buffer", () => {
registerTerminalOutput("term-1", () => "embedded output")
expect(readTerminalOutput("term-1")).toBe("embedded output")
unregisterTerminalOutput("term-1")
expect(readTerminalOutput("term-1")).toBeUndefined()
})
it("truncates large terminal buffers", () => {
registerTerminalOutput("term-2", () => Array.from({ length: 501 }, (_, index) => `line-${index}`).join("\n"))
const output = readTerminalOutput("term-2")
expect(output).toContain("[...1 lines omitted...]")
expect(output).toContain("line-0")
expect(output).toContain("line-500")
unregisterTerminalOutput("term-2")
})
})
@@ -1,201 +0,0 @@
import { describe, expect, it } from "bun:test"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import {
applyVSCodeExtensionRequirements,
requirementDirectory,
requirementKey,
type BackendAgentRequirementResult,
type HostAgentRequirementResult,
} from "../../src/kilo-provider/agent-requirements"
import { AgentRequirementsController } from "../../src/kilo-provider/agent-requirements-controller"
import { keepAgentRequirementsResult } from "../../webview-ui/src/context/agent-requirements-state"
const root = "/repo"
function result(input: Partial<BackendAgentRequirementResult> = {}): BackendAgentRequirementResult {
return {
agent: "demo",
directory: root,
enabled: true,
state: "ready",
skills: [],
mcps: [],
vscode_extensions: [],
...input,
}
}
function client(value: BackendAgentRequirementResult | (() => BackendAgentRequirementResult)) {
const calls: Array<{ agent: string; directory: string }> = []
const api = {
kilocode: {
agentRequirements: async (parameters: { agent: string; directory: string }) => {
calls.push(parameters)
return { data: typeof value === "function" ? value() : value }
},
},
} as unknown as KiloClient
return { calls, api }
}
function controller(
input: {
api?: KiloClient | null
posts?: unknown[]
sessions?: ReadonlyMap<string, string>
worktrees?: () => readonly string[]
extensions?: ReadonlySet<string>
subscribe?: (listener: () => void) => { dispose(): void }
} = {},
) {
const posts = input.posts ?? []
return new AgentRequirementsController({
post: (message) => posts.push(message),
client: () => input.api ?? null,
connected: () => true,
generation: () => 1,
root: () => root,
folders: () => [root, "/workspace"],
project: () => root,
sessions: () => input.sessions ?? new Map(),
worktrees: input.worktrees,
extension: (id) => input.extensions?.has(id),
subscribe: input.subscribe,
error: (error) => (error instanceof Error ? error.message : String(error)),
})
}
describe("agent requirement helpers", () => {
it("builds stable cache keys from normalized directories", () => {
expect(requirementKey("demo", "/repo/./subdir")).toBe(requirementKey("demo", "/repo/subdir"))
expect(requirementKey("other", "/repo/subdir")).not.toBe(requirementKey("demo", "/repo/subdir"))
})
it("scopes requests to active workspace, session, and worktree directories", () => {
const sessions = new Map([["session-1", "/repo/.kilo/worktrees/one"]])
const base = {
workspaceDirectory: root,
workspaceDirectories: ["/workspace"],
projectDirectory: root,
sessionDirectories: sessions,
worktreeDirectories: () => ["/repo/.kilo/worktrees/two"],
}
expect(requirementDirectory({ ...base, requested: "/repo" })).toBe(root)
expect(requirementDirectory({ ...base, requested: "/repo/.kilo/worktrees/one", sessionID: "session-1" })).toBe(
"/repo/.kilo/worktrees/one",
)
expect(requirementDirectory({ ...base, requested: "/repo/.kilo/worktrees/two" })).toBe("/repo/.kilo/worktrees/two")
expect(requirementDirectory({ ...base, requested: "/other" })).toBeUndefined()
})
it("adds VS Code extension statuses and blocks ready backend results when extensions are missing", () => {
const checked = applyVSCodeExtensionRequirements(
result({
vscode_extensions: [
{ name: "Ready Extension", id: "publisher.ready" },
{ name: "Missing Extension", id: "publisher.missing" },
],
}),
(id) => id === "publisher.ready",
)
expect(checked.state).toBe("blocked")
expect(checked.vscode_extensions).toEqual([
{ name: "Ready Extension", id: "publisher.ready", status: "ready" },
{ name: "Missing Extension", id: "publisher.missing", status: "missing" },
])
})
})
describe("agent requirement webview state", () => {
const blocked: HostAgentRequirementResult = {
...result({ state: "blocked", skills: [{ name: "skill", status: "missing" }] }),
vscode_extensions: [],
}
it("preserves ready, blocked, and error results only for the same agent and directory", () => {
expect(keepAgentRequirementsResult(blocked, "demo", root)).toBe(true)
expect(keepAgentRequirementsResult({ ...blocked, state: "error" }, "demo", root)).toBe(true)
expect(keepAgentRequirementsResult({ ...blocked, state: "ready" }, "demo", root)).toBe(true)
expect(keepAgentRequirementsResult({ ...blocked, state: "disabled" }, "demo", root)).toBe(false)
expect(keepAgentRequirementsResult(blocked, "other", root)).toBe(false)
expect(keepAgentRequirementsResult(blocked, "demo", "/other")).toBe(false)
})
})
describe("AgentRequirementsController", () => {
it("caches fetched results until invalidated", async () => {
const posts: unknown[] = []
const backend = client(result())
const requirements = controller({ api: backend.api, posts })
await requirements.fetch({ agent: "demo", directory: root })
await requirements.fetch({ agent: "demo", directory: root })
requirements.clear()
await requirements.fetch({ agent: "demo", directory: root })
expect(backend.calls).toEqual([
{ agent: "demo", directory: root },
{ agent: "demo", directory: root },
])
expect(
posts.filter((message) => (message as { type?: string }).type === "agentRequirementsInvalidated"),
).toHaveLength(1)
})
it("posts scope mismatch errors for inactive directories", async () => {
const posts: unknown[] = []
const backend = client(result())
const requirements = controller({ api: backend.api, posts })
await requirements.fetch({ agent: "demo", directory: "/other" })
expect(backend.calls).toEqual([])
expect(posts.at(-1)).toMatchObject({
type: "agentRequirementsLoaded",
result: { state: "error", error: { code: "scope_mismatch" } },
})
})
it("clears requirements when the extension subscription fires", async () => {
const posts: unknown[] = []
let listener: (() => void) | undefined
let disposed = false
const backend = client(result())
const requirements = controller({
api: backend.api,
posts,
subscribe: (fn) => {
listener = fn
return { dispose: () => void (disposed = true) }
},
})
await requirements.fetch({ agent: "demo", directory: root })
posts.length = 0
listener?.()
requirements.dispose()
expect(posts).toEqual([{ type: "agentRequirementsInvalidated" }])
expect(disposed).toBe(true)
})
it("asserts and reports blocked requirements before host sends", async () => {
const posts: unknown[] = []
const backend = client(
result({
vscode_extensions: [{ name: "Missing Extension", id: "publisher.missing" }],
}),
)
const requirements = controller({ api: backend.api, posts })
await expect(requirements.assertAgentRequirements("demo", root)).rejects.toThrow(
"VS Code extension Missing Extension",
)
expect(posts.at(-1)).toMatchObject({
type: "agentRequirementsLoaded",
result: { state: "blocked", vscode_extensions: [{ status: "missing" }] },
})
})
})
@@ -2,11 +2,14 @@ import { describe, expect, it } from "bun:test"
import {
parsePRResult,
checkStatus,
commentsSig,
formatCheckDuration,
ghErrorReason,
parseComments,
parseReviewers,
} from "../../src/agent-manager/pr/am-pr-utils"
import type { GhThread, GhReviewRequest, GhReview } from "../../src/agent-manager/pr/am-pr-types"
import type { PRComment } from "../../src/agent-manager/types"
// --- parsePRResult ---
@@ -248,8 +251,10 @@ describe("parseComments", () => {
line: 10,
url: "https://url",
resolved: true,
outdated: false,
createdAt: new Date("2024-01-01T00:00:00Z").getTime(),
diffHunk: undefined,
replies: undefined,
},
])
})
@@ -278,15 +283,15 @@ describe("parseComments", () => {
expect(parseComments(threads)[0]?.author).toBe("unknown")
})
it("only uses the first comment of each thread", () => {
it("keeps later thread comments as replies of the first one", () => {
const threads: GhThread[] = [
{
id: "PRT_t2",
isResolved: false,
comments: {
nodes: [
{ id: "first", body: "first comment" },
{ id: "second", body: "second comment" },
{ id: "first", body: "first comment", author: { login: "alice" } },
{ id: "second", body: "second comment", author: { login: "bob" } },
],
},
},
@@ -294,6 +299,82 @@ describe("parseComments", () => {
const result = parseComments(threads)
expect(result).toHaveLength(1)
expect(result[0]?.id).toBe("first")
expect(result[0]?.replies).toEqual([{ author: "bob", body: "second comment" }])
})
it("marks an outdated thread", () => {
const threads: GhThread[] = [
{ id: "PRT_t3", isResolved: false, isOutdated: true, comments: { nodes: [{ id: "c4", body: "stale" }] } },
]
expect(parseComments(threads)[0]?.outdated).toBe(true)
})
it("falls back to the original line when the thread has no current line", () => {
const threads: GhThread[] = [
{
id: "PRT_t4",
isResolved: false,
isOutdated: true,
comments: { nodes: [{ id: "c5", body: "moved", path: "src/foo.ts", originalLine: 42 }] },
},
]
expect(parseComments(threads)[0]?.line).toBe(42)
})
})
// --- commentsSig ---
describe("commentsSig", () => {
const thread = (overrides: Partial<PRComment> = {}): PRComment => ({
id: "c1",
threadId: "PRRT_1",
author: "alice",
body: "looks good",
resolved: false,
outdated: false,
...overrides,
})
it("returns an empty signature when there are no comments", () => {
expect(commentsSig()).toBe("")
})
it("changes when a reply is added, which thread counts alone cannot detect", () => {
const before = commentsSig([thread()])
const after = commentsSig([thread({ replies: [{ author: "bob", body: "guard it" }] })])
expect(after).not.toBe(before)
})
it("changes when a body is edited, even when the length stays the same", () => {
expect(commentsSig([thread({ body: "looks fine" })])).not.toBe(commentsSig([thread()]))
expect(commentsSig([thread({ replies: [{ author: "bob", body: "guard it" }] })])).not.toBe(
commentsSig([thread({ replies: [{ author: "bob", body: "guard me" }] })]),
)
})
it("changes when a thread moves line", () => {
expect(commentsSig([thread({ line: 5 })])).not.toBe(commentsSig([thread()]))
})
it("stays stable for unchanged comments", () => {
expect(commentsSig([thread()])).toBe(commentsSig([thread()]))
})
})
// --- ghErrorReason ---
describe("ghErrorReason", () => {
it("keeps the last meaningful line and strips the gh prefix", () => {
const message = "Command failed: gh api graphql -f query=mutation...\ngh: Resource not accessible by integration"
expect(ghErrorReason(message)).toBe("Resource not accessible by integration")
})
it("falls back to the raw message when there is nothing else", () => {
expect(ghErrorReason(" boom ")).toBe("boom")
})
it("truncates very long output", () => {
expect(ghErrorReason("x".repeat(500)).length).toBe(200)
})
})
@@ -117,12 +117,15 @@ describe("Extension — package.json command sync", () => {
})
})
it("keeps Agent Manager terminal shortcuts distinct", () => {
it("keeps Agent Manager session and terminal shortcuts focus-aware", () => {
const terminal = pkg.contributes?.keybindings?.find(
(item: { command: string }) => item.command === "kilo-code.new.agentManager.showTerminal",
)
const create = pkg.contributes?.keybindings?.find(
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTerminal",
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTerminalTab",
)
const sessionCreate = pkg.contributes?.keybindings?.find(
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTab",
)
expect(terminal).toMatchObject({
key: "ctrl+/",
@@ -132,8 +135,29 @@ describe("Extension — package.json command sync", () => {
expect(create).toMatchObject({
key: "ctrl+shift+t",
mac: "cmd+shift+t",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused",
})
expect(sessionCreate).toMatchObject({
key: "ctrl+t",
mac: "cmd+t",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused",
})
const terminalCreate = pkg.contributes?.keybindings?.find(
(item: { command: string; key?: string; mac?: string; when?: string }) =>
item.command === "kilo-code.new.agentManager.newSideTerminal" && item.key === "ctrl+t",
)
expect(terminalCreate).toMatchObject({
key: "ctrl+t",
mac: "cmd+t",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused",
})
expect(
pkg.contributes?.keybindings?.some(
(item: { command: string }) =>
item.command === "kilo-code.new.agentManager.newTerminal" ||
item.command === "kilo-code.new.agentManager.newMainTerminal",
),
).toBe(false)
})
it("declares the Agent Manager terminal destination setting", () => {
@@ -89,4 +89,29 @@ describe("buildKeybindingMap", () => {
expect(buildKeybindingMap([], false).previousTerminal).toBe("Ctrl+Shift+[")
expect(buildKeybindingMap([], false).nextTerminal).toBe("Ctrl+Shift+]")
})
it("keeps prompt and side-terminal shortcuts separate", () => {
const bindings = [
{
command: "kilo-code.new.agentManager.newTerminalTab",
key: "ctrl+shift+t",
mac: "cmd+shift+t",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerPromptFocused",
},
{
command: "kilo-code.new.agentManager.newSideTerminal",
key: "ctrl+t",
mac: "cmd+t",
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused",
},
]
expect(buildKeybindingMap(bindings, true)).toMatchObject({
newTerminalCenter: "⌘⇧T",
newTerminalTerminal: "⌘T",
})
expect(buildKeybindingMap(bindings, false)).toMatchObject({
newTerminalCenter: "Ctrl+Shift+T",
newTerminalTerminal: "Ctrl+T",
})
})
})
@@ -270,17 +270,12 @@ describe("MarketplaceInstaller skills", () => {
})
describe("MarketplaceInstaller agents", () => {
it("preserves requirements in installed agent frontmatter", async () => {
it("preserves agent metadata in installed frontmatter", async () => {
const installer = new MarketplaceInstaller(new TestPaths())
const item = agent({
mode: "all",
description: "Requires local setup",
prompt: "Use the available project tools.",
requirements: {
skills: ["project-skill"],
mcps: ["project-mcp"],
vscode_extensions: [{ name: "Project Helper", id: "publisher.project-helper" }],
},
})
const result = await installer.installAgent(item, "project", tmpDir)
@@ -289,6 +284,6 @@ describe("MarketplaceInstaller agents", () => {
expect(result.filePath).toBeDefined()
if (!result.filePath) throw new Error("agent install did not return a file path")
const data = await frontmatter(result.filePath)
expect(data.requirements).toEqual(item.content.requirements)
expect(data.description).toBe(item.content.description)
})
})
@@ -9,7 +9,7 @@ const WEBVIEW = path.join(ROOT, "webview-ui")
const FIXTURE = path.join(ROOT, "tests/fixtures/pr-comments-render.tsx")
describe("PR comments", () => {
it("keeps a comment mounted while Pierre renders its GitHub hunk", async () => {
it("renders hunks, collapses resolved threads, and sends a thread to the agent", async () => {
const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW))
const aliases: Record<string, string> = {
"solid-js": path.join(solid, "dist/solid.js"),
@@ -358,10 +358,10 @@ describe("PromptInput send origin contract", () => {
it("captures the real or pending tab before asynchronous attachment resolution", () => {
expect(source).toMatch(/const origin = session\.currentSessionID\(\)[\s\S]*const id = origin \?\? pendingId/)
expect(source.indexOf("beginPendingSend(pendingId)")).toBeLessThan(
source.indexOf("await terminal.resolveAttachment"),
expect(source.indexOf("beginPending(pendingId)")).toBeLessThan(
source.indexOf("const terminalFile = await terminal"),
)
expect(source).toMatch(/await terminal\.resolveAttachment\(message, id\)/)
expect(source).toMatch(/resolveAttachment\(message, id, readTerminalContext\(props\.terminalContext\)\)/)
expect(source).toMatch(/await git\.resolveAttachment\(message, id, context\)/)
})
@@ -612,6 +612,29 @@ describe("Cloud import parts cleanup contract", () => {
})
})
describe("Optimistic parts preservation and smooth status contract", () => {
const source = readFile(SESSION_FILE)
it("handleMessageCreated preserves optimistic parts instead of deleting them", () => {
const created = extractFunctionBody(source, "handleMessageCreated")
expect(created).not.toMatch(/delete\s+p\[message\.id\]/)
expect(created).toContain("pendingOptimistic.get(message.sessionID)")
})
it("handlePartUpdated replaces matching optimistic parts in place", () => {
const updated = extractFunctionBody(source, "handlePartUpdated")
expect(updated).toContain("optimisticParts.get(effectiveMessageID)")
expect(updated).toContain("mergeOptimisticPart")
})
it("statusText derives status from the active turn instead of queued follow-ups", () => {
const match = source.match(/const statusText = createMemo<string \| undefined>\(\(\) => \{([\s\S]*?)\n \}\)/)
expect(match).not.toBeNull()
expect(match![1]).toContain("activeUserMessageID(msgs, statusInfo()")
expect(match![1]).toContain('language.t("ui.sessionTurn.status.thinking")')
})
})
describe("KiloConnectionService pruneSession contract", () => {
const source = readFile(CONNECTION_SERVICE_FILE)
@@ -0,0 +1,179 @@
/**
* PR review comment payload
*
* GitHub PR threads travel to the agent through the same review-comment payload
* as local diff comments. The message text must be reproducible from the part
* metadata, otherwise a historical message loses its comment chips.
*/
import { describe, it, expect } from "bun:test"
import {
formatReviewCommentsMarkdown,
isPRReviewComment,
partReview,
parseReview,
reviewMetadata,
type PRReviewCommentData,
type ReviewCommentData,
} from "../../src/shared/review-comments"
import { githubUrl, prMarkdown, prPayload, preview } from "../../webview-ui/agent-manager/pr/pr-comment-payload"
import type { PRComment } from "../../webview-ui/agent-manager/pr/pr-types"
function pr(overrides: Partial<PRReviewCommentData> = {}): PRReviewCommentData {
return {
id: "PRRT_1",
origin: "pr",
author: "alice",
body: "This throws when gh is missing.",
file: "src/gh.ts",
line: 42,
...overrides,
}
}
function local(): ReviewCommentData {
return { id: "c1", file: "src/a.ts", side: "additions", line: 3, comment: "rename", selectedText: "const x = 1" }
}
function thread(overrides: Partial<PRComment> = {}): PRComment {
return {
id: "PRRC_1",
threadId: "PRRT_1",
author: "alice",
body: "This throws when gh is missing.",
file: "src/gh.ts",
line: 42,
resolved: false,
outdated: false,
...overrides,
}
}
describe("PR review comment markdown", () => {
it("names the file, line, and author", () => {
expect(formatReviewCommentsMarkdown([pr()])).toBe(
"## Review Comments\n\n**src/gh.ts** (line 42), PR comment by @alice:\nThis throws when gh is missing.",
)
})
it("drops the location when the thread has none", () => {
const text = formatReviewCommentsMarkdown([pr({ file: undefined, line: undefined })])
expect(text).toContain("PR comment by @alice:")
expect(text).not.toContain("(line")
})
it("marks outdated threads", () => {
expect(formatReviewCommentsMarkdown([pr({ outdated: true })])).toContain("by @alice (outdated):")
})
it("fences the diff hunk and quotes replies", () => {
const text = formatReviewCommentsMarkdown([
pr({
diffHunk: "@@ -1 +1 @@\n-const x = 1\n+const x = 2",
replies: [{ author: "bob", body: "agreed\nguard it" }],
}),
])
expect(text).toContain("```\n@@ -1 +1 @@\n-const x = 1\n+const x = 2\n```")
expect(text).toContain("> @bob: agreed\n> guard it")
})
})
describe("PR review comment metadata", () => {
it("round-trips through the message body", () => {
const data = {
version: 1 as const,
comments: [pr({ diffHunk: "@@ -1 +1 @@", replies: [{ author: "bob", body: "ok" }] })],
}
const text = `${formatReviewCommentsMarkdown(data.comments)}\n\nplease fix these`
const view = partReview(reviewMetadata(data), text)
expect(view?.body).toBe("please fix these")
expect(view?.data.comments[0]).toEqual(data.comments[0])
})
it("round-trips a mixed local and PR payload", () => {
const comments = [local(), pr()]
const text = formatReviewCommentsMarkdown(comments)
const parsed = parseReview({ version: 1, comments }, text)
expect(parsed?.comments).toEqual(comments)
expect(parsed?.comments.filter(isPRReviewComment)).toHaveLength(1)
})
it("keeps parsing a legacy local-only payload", () => {
const comments = [local()]
expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))?.comments).toEqual(comments)
})
it("rejects an unknown origin", () => {
const text = formatReviewCommentsMarkdown([local()])
expect(parseReview({ version: 1, comments: [{ ...local(), origin: "gitlab" }] }, text)).toBeUndefined()
expect(parseReview({ version: 1, comments: [local()] }, text)?.comments).toHaveLength(1)
})
it("rejects a PR entry with a traversal path", () => {
const comments = [pr({ file: "../../etc/passwd" })]
expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))).toBeUndefined()
})
it("rejects a PR entry with a bogus line", () => {
const comments = [pr({ line: 0 })]
expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))).toBeUndefined()
})
})
describe("prPayload", () => {
it("keys the payload by thread so a repeat send replaces the chip", () => {
expect(prPayload(thread()).id).toBe("PRRT_1")
})
it("caps the body", () => {
const payload = prPayload(thread({ body: "x".repeat(5_000) }))
expect(payload.body.length).toBeLessThan(5_000)
expect(payload.body.endsWith("...")).toBe(true)
})
it("keeps the hunk header and the tail of a long hunk", () => {
const hunk = Array.from({ length: 80 }, (_, i) => `line ${i}`).join("\n")
const payload = prPayload(thread({ diffHunk: hunk }))
const lines = payload.diffHunk?.split("\n") ?? []
expect(lines).toHaveLength(42)
expect(lines[0]).toBe("line 0")
expect(lines[1]).toBe("...")
expect(payload.diffHunk?.endsWith("line 79")).toBe(true)
})
it("caps a single-line hunk by characters", () => {
const payload = prPayload(thread({ diffHunk: `@@ -1 +1 @@ ${"x".repeat(20_000)}` }))
expect(payload.diffHunk!.length).toBeLessThan(9_000)
expect(payload.diffHunk?.endsWith("...")).toBe(true)
})
it("caps replies and drops empty reply lists", () => {
const replies = Array.from({ length: 9 }, (_, i) => ({ id: `r${i}`, author: "bob", body: `reply ${i}` }))
expect(prPayload(thread({ replies })).replies).toHaveLength(5)
expect(prPayload(thread({ replies: [] })).replies).toBeUndefined()
})
it("only treats https comment urls as openable", () => {
expect(githubUrl("http://example.com")).toBeUndefined()
expect(githubUrl("javascript:alert(1)")).toBeUndefined()
expect(githubUrl("https://github.com/org/repo/pull/1#discussion_r1")).toBe(
"https://github.com/org/repo/pull/1#discussion_r1",
)
})
it("survives the payload parser", () => {
const comments = [prPayload(thread({ diffHunk: "@@ -1 +1 @@", outdated: true }))]
expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))?.comments).toEqual(comments)
})
it("formats the whole thread for the copy action", () => {
expect(prMarkdown(thread())).toBe("**src/gh.ts** (line 42), PR comment by @alice:\nThis throws when gh is missing.")
})
})
describe("preview", () => {
it("uses the first meaningful line without markdown noise", () => {
expect(preview("\n\n## Heading\nrest")).toBe("Heading")
expect(preview("- nit: rename this")).toBe("nit: rename this")
expect(preview("")).toBe("")
})
})
@@ -0,0 +1,101 @@
/**
* Session dock layout contract.
*
* The row between the transcript and the composer used to be two things at
* once: the working indicator lived inside the scrollable transcript (with a
* 10px placeholder left behind when it disappeared) and the session actions
* were a separate block in the composer column. Both changed height when a turn
* ended, the two offsets did not cancel out, and the conversation text jumped a
* few pixels every time.
*
* The invariant now: one dock, one decision point, one fixed height.
*/
import { describe, expect, it } from "bun:test"
import fs from "node:fs"
import path from "node:path"
import { showsWorking } from "../../webview-ui/src/components/shared/working-indicator-utils"
const ROOT = path.resolve(import.meta.dir, "../..")
function read(file: string): string {
return fs.readFileSync(path.join(ROOT, file), "utf-8")
}
describe("showsWorking", () => {
it("shows the indicator for active backend statuses", () => {
expect(showsWorking("busy", false, false)).toBe(true)
expect(showsWorking("retry", false, false)).toBe(true)
expect(showsWorking("offline", false, false)).toBe(true)
})
it("shows the indicator for a submission before backend status arrives", () => {
expect(showsWorking("idle", true, false)).toBe(true)
})
it("yields the row to the session actions once idle", () => {
expect(showsWorking("idle", false, false)).toBe(false)
})
it("stays hidden while another surface owns the interaction", () => {
expect(showsWorking("busy", false, true)).toBe(false)
expect(showsWorking("idle", true, true)).toBe(false)
})
})
describe("session dock layout", () => {
it("stacks both states in one grid cell so the row measures the taller one", () => {
const css = read("webview-ui/src/styles/chat-layout.css")
const dock = css.match(/\.session-dock \{([\s\S]*?)\}/)
const state = css.match(/\.session-dock-state \{([\s\S]*?)\}/)
expect(dock).not.toBeNull()
expect(state).not.toBeNull()
expect(dock![1]).toContain("display: grid")
expect(state![1]).toContain("grid-area: 1 / 1")
// A hard-coded height clipped the actions row once it wrapped to a second
// line in a narrow sidebar, so the row must size itself.
expect(dock![1]).not.toMatch(/^\s*height:/m)
expect(dock![1]).not.toContain("overflow: hidden")
})
it("hides the inactive state instead of unmounting it", () => {
const css = read("webview-ui/src/styles/chat-layout.css")
const hidden = css.match(/\.session-dock-state:not\(\[data-active\]\) \{([\s\S]*?)\}/)
expect(hidden).not.toBeNull()
// display:none would stop reserving the height and bring the shift back.
expect(hidden![1]).toContain("visibility: hidden")
expect(hidden![1]).toContain("pointer-events: none")
expect(hidden![1]).not.toContain("display: none")
})
it("keeps the reserved actions row independent of the turn lifecycle", () => {
const view = read("webview-ui/src/components/chat/ChatView.tsx")
const fork = view.match(/const canFork = \(hasChat: boolean\) =>([^\n]*)/)
expect(fork).not.toBeNull()
// A button that came and went with the turn would resize the hidden row.
expect(fork![1]).not.toContain('session.status() === "idle"')
expect(view).toContain("session.messages().length > 0 || session.submitting()")
})
it("drops the placeholder that used to offset the session actions", () => {
const css = read("webview-ui/src/styles/chat-layout.css")
expect(css).not.toContain("working-indicator-slot")
expect(read("webview-ui/src/components/shared/WorkingIndicator.tsx")).not.toContain("working-indicator-slot")
})
it("keeps the composer column as the only owner of the row", () => {
// A second copy inside the scrollable transcript would resize the scroll
// content on every turn boundary again.
expect(read("webview-ui/src/components/chat/MessageList.tsx")).not.toContain("WorkingIndicator")
expect(read("webview-ui/src/components/chat/ChatView.tsx")).toContain("<SessionDock")
})
it("routes both states through the dock so neither can claim the row alone", () => {
const dock = read("webview-ui/src/components/chat/SessionDock.tsx")
expect(dock).toContain("showsWorking(session.status(), session.submitting()")
expect(dock).toContain('data-active={working() ? "" : undefined}')
expect(dock).toContain('data-active={actions() ? "" : undefined}')
// The indicator must not re-decide its own visibility.
expect(read("webview-ui/src/components/shared/WorkingIndicator.tsx")).not.toContain("session.submitting() ||")
})
})
@@ -101,11 +101,54 @@ describe("queuedUserMessageIDs", () => {
expect(queuedUserMessageIDs(messages, { type: "busy" })).toEqual(["message_3"])
})
it("returns no queued messages while idle", () => {
it("returns no queued messages while idle without submitting flag", () => {
const messages = [user("message_1"), user("message_2")]
expect(queuedUserMessageIDs(messages, { type: "idle" })).toEqual([])
})
it("queues follow-ups while idle when submitting is true", () => {
const messages = [user("message_1"), user("message_2")]
expect(queuedUserMessageIDs(messages, { type: "idle" }, undefined, true)).toEqual(["message_2"])
})
})
describe("activeUserMessageID", () => {
it("returns undefined while idle and not submitting", () => {
const messages = [user("message_1")]
expect(activeUserMessageID(messages, { type: "idle" })).toBeUndefined()
})
it("returns the pending user message while idle when submitting is true", () => {
const messages = [user("message_1")]
expect(activeUserMessageID(messages, { type: "idle" }, undefined, true)).toBe("message_1")
})
it("returns the next pending user message after finished turn when submitting is true", () => {
const messages = [
user("message_1"),
assistant("message_2", "message_1", { finish: "stop", time: { created: 1, completed: 2 } }),
user("message_3"),
]
expect(activeUserMessageID(messages, { type: "idle" }, undefined, true)).toBe("message_3")
})
})
describe("active queued status boundary", () => {
it("keeps the active assistant status visible when a follow-up is queued", () => {
const messages = [
user("message_1"),
assistant("message_2", "message_1", { finish: "tool-calls" }),
user("message_3"),
]
expect(activeUserMessageID(messages, { type: "busy" })).toBe("message_1")
expect(queuedUserMessageIDs(messages, { type: "busy" })).toEqual(["message_3"])
})
})
describe("partitionTurns", () => {
@@ -139,10 +139,31 @@ describe("SessionTerminalManager structure", () => {
it("rejects context capture from another managed session", () => {
const text = body("prepareContext")
expect(text).toContain("this.showExisting(sessionId)")
expect(text).toContain("this.showExisting(sessionId, false)")
expect(text).toContain("this.activeKey()")
expect(text).toContain("SessionTerminalManager.sessionKey(sessionId)")
})
it("allows a focused legacy Run terminal when no managed terminal exists", () => {
const active = {}
const host: TerminalHost = {
createTerminal() {
throw new Error("not used")
},
activeTerminal: () => active,
repoPath: () => undefined,
showWarning() {},
setContext() {},
onTerminalClosed: () => ({ dispose() {} }),
onActiveTerminalChanged: () => ({ dispose() {} }),
registerCommand: () => ({ dispose() {} }),
executeCommand: () => Promise.resolve(),
}
const manager = new SessionTerminalManager(() => {}, host)
expect(manager.prepareContext("session-1", "local")).toBe(true)
manager.dispose()
})
})
describe("SessionTerminalManager command restoration", () => {
@@ -22,19 +22,25 @@ describe("terminal context architecture", () => {
const util = src("webview-ui/src/hooks/terminal-context-utils.ts")
expect(prompt).toContain("useTerminalContext")
expect(prompt).toContain("resolveAttachment(message, id, readTerminalContext(props.terminalContext))")
expect(prompt).not.toContain("requestTerminalContext")
expect(prompt).not.toContain("data:text/plain")
expect(hook).toContain("requestTerminalContext")
expect(hook).toContain("useVSCode()")
expect(util).toContain("data:text/plain")
})
it("keeps terminal output limits in the shared truncation helper", () => {
const helper = src("src/services/terminal/truncate.ts")
const output = src("webview-ui/agent-manager/terminal/output.ts")
const provider = src("src/KiloProvider.ts")
const prompt = src("webview-ui/src/components/chat/PromptInput.tsx")
expect(helper).toContain("TERMINAL_OUTPUT_LINE_LIMIT = 500")
expect(helper).toContain("TERMINAL_OUTPUT_CHARACTER_LIMIT = 50_000")
expect(output).toContain('from "../../../src/services/terminal/truncate"')
expect(output).not.toContain("LINE_LIMIT = 500")
expect(output).not.toContain("CHAR_LIMIT = 50_000")
expect(provider).not.toContain("TERMINAL_OUTPUT_LINE_LIMIT")
expect(prompt).not.toContain("TERMINAL_OUTPUT_LINE_LIMIT")
})
@@ -77,7 +77,11 @@ async function settle(page: Page) {
// Sandboxing rows can settle at different scroll heights after settings context updates.
// Side terminal tabs mount live xterm instances whose websocket error text
// lands at indeterminate times.
// The session-dock stability story exists to measure geometry across the
// working/idle swap and carries a debug toggle button, so it is not a meaningful
// appearance baseline.
const SKIP = new Set<string>([
"chat--chat-view-session-dock-stability",
"agentmanager--worktree-item-busy",
"agentmanager--full-screen-diff-agent-edit-scroll",
"agentmanager--side-terminal-panel-tabs",
@@ -72,7 +72,6 @@ import { NotificationsProvider } from "../src/context/notifications"
import { FeedbackProvider } from "../src/context/feedback"
import { MemoryProvider } from "../src/context/memory"
import { SessionProvider, useSession } from "../src/context/session"
import { AgentRequirementsProvider } from "../src/context/agent-requirements"
import { WorktreeModeProvider } from "../src/context/worktree-mode"
import { ProviderShell } from "../src/context/provider-shell"
import { ChatView } from "../src/components/chat"
@@ -93,7 +92,7 @@ import { rememberTarget, restoreProjectTarget } from "./project/restore"
import { createProjectStateRouter } from "./project/state"
import { applyRunStatus } from "./project/run-status"
import { clearMultiVersionBusy, markMultiVersionBusy } from "./project/progress"
import { selectLocalAction, selectWorktreeAction } from "./selection-actions"
import { createSessionRestore, selectLocalAction, selectWorktreeAction } from "./selection-actions"
import { DataBridge } from "../src/App"
import { LanguageBridge } from "../src/context/language-bridge"
import { useLanguage } from "../src/context/language"
@@ -142,10 +141,11 @@ import {
resolveRunScriptRequest,
resolveVscodeTerminalRequest,
} from "./terminal"
import { createEmbeddedTerminalReader } from "./terminal/output"
import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
import { useTabScroll } from "./tab-scroll"
import { DiffPanel } from "./DiffPanel"
import { PRPanel } from "./pr/PRPanel"
import { PRPanelHost } from "./pr/PRPanelHost"
import { createRevertFile } from "./revert-file"
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
import { createApplyToLocal } from "./apply-to-local"
@@ -184,7 +184,7 @@ import { DocumentPanelHost } from "./documents/DocumentPanelHost"
import { createDocumentInspector } from "./documents/state"
import { buildShortcutCategories } from "./shortcuts"
import { tracker } from "./telemetry"
import { createChatFocus, createPromptFocus, hasQuestionOption } from "./focus"
import { createChatFocus, createFocusBridge, createPromptFocus, forgetTerminalFocus, hasQuestionOption } from "./focus"
import { usePendingCreate } from "./pending-create"
import { defaultBase as projectDefaultBase } from "./project/default-base"
import "./agent-manager.css"
@@ -357,7 +357,6 @@ const AgentManagerContent: Component = () => {
const setRunScriptConfigured = (v: Parameters<Setter<boolean>>[0]) => registry.active().setRunScriptConfigured(v)
const localStats = () => registry.active().localStats()
const projectLive = createProjectLive({
ensure: (pid) => (pid ? registry.ensure(pid) : registry.active()),
active: isActivePayload,
@@ -372,19 +371,25 @@ const AgentManagerContent: Component = () => {
fontSize: readFontSize(),
})
/** Namespace key so worktree/local ids from different projects never collide. */
const nsKey = (sel: string) => `${currentProjectId() ?? "single"}:${sel}`
const terms = createTerminalState(() => {
const sel = selection()
return sel === null ? null : nsKey(sel)
})
const resolveTerminal = createEmbeddedTerminalReader({
key: (context) => nsKey(context ?? LOCAL),
local: LOCAL,
side: (key) => terms.sidesForContext(key),
tabs: (key) => terms.forSelection(key),
focused: terms.focusedId,
sideActive: terms.sideActiveFor,
active: terms.activeId,
})
const requestChatFocus = createChatFocus({
term: () => terms.activeId(),
history,
review: reviewActive,
})
createEffect(
on(
() => {
@@ -403,25 +408,20 @@ const AgentManagerContent: Component = () => {
type FocusOwner = "prompt" | { terminal: string }
const focusMemory = new Map<string, FocusOwner>()
const prompt = createPromptFocus(terms, requestChatFocus)
const focusKey = () => {
const context = terms.sideKey()
const sessionID = session.currentSessionID() ?? activePendingId() ?? "new"
return `${context}:${sessionID}`
}
const focusKey = () => `${terms.sideKey()}:${session.currentSessionID() ?? activePendingId() ?? "new"}`
const forgetSessionFocus = (sessionID: string) => {
for (const key of focusMemory.keys()) if (key.endsWith(`:${sessionID}`)) focusMemory.delete(key)
}
const forgetContextFocus = (context: string) => {
for (const key of focusMemory.keys()) if (key.startsWith(`${context}:`)) focusMemory.delete(key)
}
const forgetTerminalFocus = (terminalID: string) => {
for (const [key, owner] of focusMemory) {
if (owner !== "prompt" && owner.terminal === terminalID) focusMemory.delete(key)
}
}
const rememberPromptFocus = (focused: boolean) => {
if (focused) focusMemory.set(focusKey(), "prompt")
}
let restoreSession: () => "none" | "ready" | "pending" = () => "none"
const focusCtl = createFocusBridge({
prompt,
post: (target) => vscode.postMessage({ type: "agentManagerFocusChanged", target }),
remember: () => focusMemory.set(focusKey(), "prompt"),
restore: () => restoreSession(),
})
const terminalVisible = () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive()
const focusOnDraftChange = () => {
const key = focusKey()
@@ -1209,13 +1209,12 @@ const AgentManagerContent: Component = () => {
else if (msg.action === "advancedWorktree") showNewWorktreeDialog()
else if (msg.action === "closeWorktree") closeSelectedWorktree()
else if (msg.action === "showShortcuts") handleShowKeyboardShortcuts()
else if (msg.action === "focusInput") prompt.focus()
else if (msg.action === "focusInput") focusCtl.focus()
else if (msg.action === "focusSearch")
focusChatSearch({ history: setHistory, review: setReviewActive, terminal: () => terms.setActiveId(undefined) })
else if (msg.action === "newTerminal") {
if (terms.sideFocusedId()) termHandlers.addSide()
else termHandlers.requestNew()
} else if (msg.action === "cycleAgentMode" && document.hasFocus()) {
else if (msg.action === "newTerminalTab") termHandlers.requestNew()
else if (msg.action === "newSideTerminal") termHandlers.addSide()
else if (msg.action === "cycleAgentMode" && document.hasFocus()) {
if (!mode.dispatch(1)) cycleAgent(1)
} else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) {
if (!mode.dispatch(-1)) cycleAgent(-1)
@@ -1249,6 +1248,8 @@ const AgentManagerContent: Component = () => {
if (["t", "w", "n", "d", "e", "f"].includes(e.key.toLowerCase()) && !e.shiftKey) {
e.preventDefault()
}
// Prevent browser defaults for shift variants (new central terminal,
// close worktree, advanced/new/open worktree, open PR, terminal cycling)
if (["t", "m", "w", "n", "o", "r", "[", "]"].includes(e.key.toLowerCase()) && e.shiftKey) {
e.preventDefault()
}
@@ -1299,6 +1300,7 @@ const AgentManagerContent: Component = () => {
const onWindowFocus = () => {
document.body.style.pointerEvents = ""
document.body.style.overflow = ""
focusCtl.report()
restoreFocus()
}
window.addEventListener("focus", onWindowFocus)
@@ -1359,13 +1361,14 @@ const AgentManagerContent: Component = () => {
state: terms,
activate: termHandlers.activate,
saveTabMemory,
rememberSession: tabs.remember,
setSelection,
showError: (message) =>
showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }),
postMessage: (message) => vscode.postMessage(message as never),
onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId),
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(terminalId),
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(focusMemory, terminalId),
onScriptRunning: (contextKey, terminalId) => {
if (terms.sideKey() !== contextKey) return
// Setup output is informational: reveal without stealing focus, and
@@ -1476,6 +1479,8 @@ const AgentManagerContent: Component = () => {
setKb(ev.bindings)
}
if (msg.type === "agentManager.focusContextRequested") focusCtl.report()
if (msg.type === "agentManager.state") applyState(msg)
// When a multi-version progress update arrives, mark newly created worktrees as loading
@@ -2014,6 +2019,28 @@ const AgentManagerContent: Component = () => {
}
})
}
const handleNewTabForCurrentSelection = () => {
const sel = selection()
if (sel === LOCAL) {
addPendingTab()
return "ready" as const
}
if (sel) vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
return "pending" as const
}
const tabs = createSessionRestore({
terminal: terms.activeId,
selection,
remembered: (sel) => registry.active().sessionRestore.get(sel),
sessions: activeTabs,
current: session.currentSessionID,
pending: activePendingId,
isPending,
select: selectSessionTab,
create: handleNewTabForCurrentSelection,
remember: (sel, id) => registry.active().sessionRestore.set(sel, id),
})
restoreSession = tabs.restore
const termHandlers = createTerminalHandlers({
state: terms,
tabIds: () => tabIds(),
@@ -2205,17 +2232,6 @@ const AgentManagerContent: Component = () => {
handleCloseTab(target.id)
}
// Cmd+T: add a new tab strictly to the current selection (no side effects)
const handleNewTabForCurrentSelection = () => {
const sel = selection()
if (sel === LOCAL) {
addPendingTab()
} else if (sel) {
// Pass the captured worktree ID directly to avoid race conditions
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
}
}
// Close the currently selected worktree with a confirmation dialog
const closeSelectedWorktree = () => {
const sel = selection()
@@ -2505,7 +2521,11 @@ const AgentManagerContent: Component = () => {
>
<div class={`am-main-pane ${terms.activeId() ? "am-main-pane-terminal-active" : ""}`}>
{/* Keep terminal tabs mounted so output streams across worktree switches. */}
{renderTerminalLayer({ state: terms, onFocusPrompt: prompt.focus })}
{renderTerminalLayer({
state: terms,
onFocusPrompt: focusCtl.focus,
onFocusChange: focusCtl.report,
})}
{/* Session-less context (e.g. a worktree mid-provisioning): the
empty state lives in the main pane so the side terminal
panel can render next to it. */}
@@ -2562,10 +2582,12 @@ const AgentManagerContent: Component = () => {
readonly={readOnly()}
continueInWorktree={selection() === LOCAL}
promptBoxId={`agent-manager:${selection() ?? "unassigned"}`}
terminalContext={() => selection() ?? undefined}
deferFocusToQuestion={hasQuestionOption}
pendingSessionID={selection() === LOCAL ? activePendingId() : undefined}
focusOnDraftChange={focusOnDraftChange}
onFocusChange={rememberPromptFocus}
onFocusChange={focusCtl.prompt}
resolveEmbeddedTerminal={resolveTerminal}
/>
<Show when={readOnly()}>
<div class="am-readonly-banner">
@@ -2659,18 +2681,13 @@ const AgentManagerContent: Component = () => {
/>
</Show>
<Show when={sidePanel() === SidePanel.PR && activePR()}>
<PRPanel
<PRPanelHost
pr={activePR()!.pr}
worktree={activePR()!.wt}
worktreeId={activePR()!.selected}
activeTerminalId={terms.activeId()}
sessionId={diffCtx()}
onClose={() => setSidePanel(null)}
onOpenExternal={() =>
vscode.postMessage({
type: "agentManager.openPR",
worktreeId: activePR()!.selected,
url: activePR()!.pr.url,
})
}
/>
</Show>
<Show when={subagents.tabs().length > 0}>
@@ -2699,7 +2716,8 @@ const AgentManagerContent: Component = () => {
visible={() => sidePanel() === SidePanel.Terminal}
nextKeybind={kb().nextTerminal ?? ""}
closeKeybind={kb().closeTab ?? ""}
onFocusPrompt={prompt.focus}
onFocusPrompt={focusCtl.focus}
onFocusChange={focusCtl.report}
onSelect={(id) => termHandlers.selectSide(id)}
onClose={(id) => {
cancelAmbientSetup()
@@ -2762,7 +2780,6 @@ const AgentManagerContent: Component = () => {
</div>
)
}
export const AgentManagerApp: Component = () => {
return (
<ProviderShell.Root>
@@ -51,6 +51,7 @@ import {
initialOpenFiles,
isDiffExpandable,
isLargeDiffFile,
reconcileOpenFiles,
sanitizeOpenFiles,
shouldVirtualizeDiff,
toggleOpenFiles,
@@ -133,7 +134,37 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
})
const localComposer = createReviewComposer()
const composer = () => props.composer ?? localComposer
const [open, setOpen] = createSignal<string[]>([])
const [manualOpen, setManualOpen] = createSignal<Record<string, string[]>>({})
const [knownFiles, setKnownFiles] = createSignal<Record<string, string[]>>({})
const open = createMemo(() => {
const key = props.sessionKey ?? ""
const diffs = props.diffs
if (diffs.length === 0) return []
const manual = manualOpen()[key]
if (manual) return sanitizeOpenFiles(diffs, manual)
return initialOpenFiles(diffs)
})
createEffect(
on(
() => [props.sessionKey, props.diffs] as const,
([key, diffs]) => {
if (diffs.length === 0) return
const id = key ?? ""
const manual = manualOpen()[id]
const result = reconcileOpenFiles(diffs, manual, knownFiles()[id] ?? [])
setKnownFiles((prev) => ({ ...prev, [id]: result.known }))
if (!manual || !result.open) return
if (result.open.length === manual.length && result.open.every((file, index) => file === manual[index])) return
setManualOpen((prev) => ({ ...prev, [id]: result.open! }))
},
),
)
const setOpen = (files: string[] | ((prev: string[]) => string[])) => {
const key = props.sessionKey ?? ""
const current = open()
const next = typeof files === "function" ? files(current) : files
setManualOpen((prev) => ({ ...prev, [key]: sanitizeOpenFiles(props.diffs, next) }))
}
const [draft, setDraft] = createSignal<ReviewDraft | null>(reviewComposerDraft(composer()))
const [editing, setEditing] = createSignal<string | null>(reviewComposerEdit(composer()))
const speechKeys = createMemo(() => {
@@ -152,10 +183,6 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
keys: speechKeys,
})
let nextId = 0
// Initialize each worktree with every file expanded, then preserve manual
// collapse state while adding and removing files from live summaries.
let initializedKey: string | undefined
let known = new Set<string>()
// Reorder diffs to match the file-tree's depth-first visual order so
// scrolling through the accordion matches the tree grouping.
@@ -218,43 +245,6 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
focusRoot()
}
// Unified open-state effect: tracks both sessionKey and diffs in a single effect
// to eliminate the race condition between the old separate sessionKey-reset and
// diffs-watch effects. Uses the session key to decide when initialization is needed
// vs when we just prune stale entries from the open list.
createEffect(
on(
() => [props.sessionKey, props.diffs] as const,
([key, diffs]) => {
// No diffs yet (async fetch in progress) — don't mark as initialized
// so auto-open runs when data arrives.
// Important: do not prune on empty, otherwise transient empty updates
// collapse all files and they stay collapsed for the same key.
if (diffs.length === 0) return
const fileSet = new Set(diffs.map((diff) => diff.file))
// New context: initialize open state from the diff policy.
if (key !== initializedKey) {
initializedKey = key
known = fileSet
setOpen(initialOpenFiles(diffs))
return
}
// Preserve manual collapse state for known files, while keeping newly
// arriving files expanded when a live summary grows.
const added = diffs.filter((diff) => !known.has(diff.file)).map((diff) => diff.file)
known = fileSet
setOpen((prev) => {
const next = sanitizeOpenFiles(diffs, [...prev.filter((file) => fileSet.has(file)), ...added])
if (next.length === prev.length && next.every((file, index) => file === prev[index])) return prev
return next
})
},
),
)
createEffect(
on(
() => props.sessionKey,
@@ -287,7 +287,7 @@ export const TabBar: Component<TabBarProps> = (props) => (
{/* Terminal destination split button: the primary action
follows the user's setting (VS Code integrated terminal
or the embedded side panel), the dropdown picks which.
Cmd+Shift+T creates a terminal in the active terminal container. */}
Cmd+Shift+T creates a central terminal from center focus. Cmd+T creates a session in the center or a terminal in the right sidebar. */}
<TerminalDestinationButton
destination={props.terminalDestination}
active={props.terminalDestinationActive}
@@ -344,11 +344,7 @@ export const DocumentPanel: Component<DocumentPanelProps> = (props) => {
<span class="am-diff-comments-count">
{t("agentManager.documents.comments", { count: props.comments.length })}
</span>
<TooltipKeybind
title={t("agentManager.review.sendAllToChat")}
keybind={sendAllKeybind(t)}
placement="top"
>
<TooltipKeybind title={t("agentManager.review.sendAllToChat")} keybind={sendAllKeybind(t)} placement="top">
<Button variant="primary" size="small" onClick={sendAll}>
{t("agentManager.review.sendAllToChat")}
</Button>
@@ -2,6 +2,48 @@ import { isTextControl } from "../src/utils/focus"
const OPTION = '[data-component="question-dock"] button[data-slot="question-option"]'
export type AgentManagerFocusTarget = "prompt" | "mainTerminal" | "sideTerminal" | "other"
export function forgetTerminalFocus(memory: Map<string, "prompt" | { terminal: string }>, id: string): void {
for (const [key, owner] of memory) if (owner !== "prompt" && owner.terminal === id) memory.delete(key)
}
/** Resolve focus from the current DOM owner, not focus event order. */
export function agentManagerFocusTarget(active: Element | null, promptPending = false): AgentManagerFocusTarget {
if (promptPending || active?.matches("textarea.prompt-input")) return "prompt"
if (active?.closest(".am-side-terminal-layer .am-terminal-host")) return "sideTerminal"
if (active?.closest(".am-terminal-layer .am-terminal-host")) return "mainTerminal"
return "other"
}
export function createFocusBridge(deps: {
prompt: { active: () => boolean; focus: () => void }
post: (target: AgentManagerFocusTarget) => void
remember: () => void
restore: () => "none" | "ready" | "pending"
}) {
const report = () => {
const active = agentManagerFocusTarget(document.activeElement)
deps.post(
active === "mainTerminal" || active === "sideTerminal"
? active
: agentManagerFocusTarget(document.activeElement, deps.prompt.active()),
)
}
return {
report,
prompt: (focused: boolean) => {
if (focused) deps.remember()
report()
},
focus: () => {
if (deps.restore() === "pending") return
deps.post("prompt")
deps.prompt.focus()
},
}
}
/** Keep an active editor, such as the worktree rename input, in control. */
export const preservesTextFocus = (active: Element | null): boolean =>
active !== null && isTextControl(active) && !active.classList.contains("prompt-input")
+17
View File
@@ -61,6 +61,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "محلي & WORKTREES",
"agentManager.terminal.new": "علامة تبويب جديدة للمحطة الطرفية",
"agentManager.terminal.addCentral": "علامة تبويب طرفية مركزية جديدة",
"agentManager.terminal.addTerminal": "علامة تبويب طرفية جديدة في الشريط الجانبي",
"agentManager.terminal.ended": "انتهت المحطة الطرفية — أغلق علامة التبويب للإخفاء",
"agentManager.terminal.endedRestartable": "انتهت المحطة الطرفية - اكتب لبدء صدفة جديدة أو أغلق علامة التبويب",
"agentManager.terminal.setupFailed": "فشل البرنامج النصي للإعداد",
@@ -187,6 +189,21 @@ export const dict = {
"agentManager.review.metaFile": "الملف",
"agentManager.review.metaLine": "السطر",
"agentManager.review.metaComment": "تعليق المستخدم",
"agentManager.review.metaAuthor": "المؤلف",
"agentManager.pr.comment.title": "التعليقات",
"agentManager.pr.comment.unresolvedCount": "{{count}} غير محلولة",
"agentManager.pr.comment.resolvedGroup": "تم الحل ({{count}})",
"agentManager.pr.comment.sendAll": "إرسال {{count}} غير محلولة إلى الوكيل",
"agentManager.pr.comment.sendAllToTerminal": "إرسال {{count}} غير محلولة إلى الطرفية",
"agentManager.pr.comment.send": "إرسال إلى الوكيل",
"agentManager.pr.comment.resolve": "حل",
"agentManager.pr.comment.unresolve": "إلغاء الحل",
"agentManager.pr.comment.outdated": "قديم",
"agentManager.pr.comment.sent": "تم الإرسال",
"agentManager.pr.comment.copy": "نسخ التعليق",
"agentManager.pr.comment.openOnGitHub": "فتح على GitHub",
"agentManager.pr.comment.resolveFailed": "تعذر الحل. {{error}}",
"agentManager.pr.comment.unresolveFailed": "تعذر إلغاء الحل. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} مطوي",
"agentManager.review.collapsedWithLarge": "{{collapsed}} مطوي، {{large}} كبير",
"agentManager.review.largeFileCollapsed": "ملف كبير (مطوي)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
"agentManager.terminal.new": "Nova aba de terminal",
"agentManager.terminal.addCentral": "Nova aba de terminal central",
"agentManager.terminal.addTerminal": "Nova aba de terminal na barra lateral",
"agentManager.terminal.ended": "terminal encerrado — feche a aba para dispensar",
"agentManager.terminal.endedRestartable": "terminal encerrado - digite para iniciar um novo shell ou feche a aba",
"agentManager.terminal.setupFailed": "falha no script de configuração",
@@ -190,6 +192,21 @@ export const dict = {
"agentManager.review.metaFile": "Arquivo",
"agentManager.review.metaLine": "Linha",
"agentManager.review.metaComment": "Comentário do usuário",
"agentManager.review.metaAuthor": "Autor",
"agentManager.pr.comment.title": "Comentários",
"agentManager.pr.comment.unresolvedCount": "{{count}} não resolvidos",
"agentManager.pr.comment.resolvedGroup": "Resolvidos ({{count}})",
"agentManager.pr.comment.sendAll": "Enviar {{count}} não resolvidos para o agente",
"agentManager.pr.comment.sendAllToTerminal": "Enviar {{count}} não resolvidos para o terminal",
"agentManager.pr.comment.send": "Enviar para o agente",
"agentManager.pr.comment.resolve": "Resolver",
"agentManager.pr.comment.unresolve": "Desfazer resolução",
"agentManager.pr.comment.outdated": "Desatualizado",
"agentManager.pr.comment.sent": "Enviado",
"agentManager.pr.comment.copy": "Copiar comentário",
"agentManager.pr.comment.openOnGitHub": "Abrir no GitHub",
"agentManager.pr.comment.resolveFailed": "Não foi possível resolver. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Não foi possível desfazer a resolução. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} recolhidos",
"agentManager.review.collapsedWithLarge": "{{collapsed}} recolhidos, {{large}} grandes",
"agentManager.review.largeFileCollapsed": "Arquivo grande (recolhido)",
+17
View File
@@ -61,6 +61,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKALNO & WORKTREES",
"agentManager.terminal.new": "Nova kartica terminala",
"agentManager.terminal.addCentral": "Nova centralna kartica terminala",
"agentManager.terminal.addTerminal": "Nova kartica terminala u bočnoj traci",
"agentManager.terminal.ended": "terminal je završen — zatvorite karticu da biste odbacili",
"agentManager.terminal.endedRestartable": "terminal je završen - kucajte za novu ljusku ili zatvorite karticu",
"agentManager.terminal.setupFailed": "skripta za postavljanje nije uspjela",
@@ -190,6 +192,21 @@ export const dict = {
"agentManager.review.metaFile": "Datoteka",
"agentManager.review.metaLine": "Linija",
"agentManager.review.metaComment": "Komentar korisnika",
"agentManager.review.metaAuthor": "Autor",
"agentManager.pr.comment.title": "Komentari",
"agentManager.pr.comment.unresolvedCount": "{{count}} neriješenih",
"agentManager.pr.comment.resolvedGroup": "Riješeno ({{count}})",
"agentManager.pr.comment.sendAll": "Pošalji {{count}} neriješenih agentu",
"agentManager.pr.comment.sendAllToTerminal": "Pošalji {{count}} neriješenih terminalu",
"agentManager.pr.comment.send": "Pošalji agentu",
"agentManager.pr.comment.resolve": "Riješi",
"agentManager.pr.comment.unresolve": "Poništi rješenje",
"agentManager.pr.comment.outdated": "Zastarjelo",
"agentManager.pr.comment.sent": "Poslano",
"agentManager.pr.comment.copy": "Kopiraj komentar",
"agentManager.pr.comment.openOnGitHub": "Otvori na GitHub",
"agentManager.pr.comment.resolveFailed": "Nije moguće riješiti. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Nije moguće poništiti rješenje. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} sažeto",
"agentManager.review.collapsedWithLarge": "{{collapsed}} sažeto, {{large}} velikih",
"agentManager.review.largeFileCollapsed": "Velika datoteka (sažeto)",
+17
View File
@@ -63,6 +63,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
"agentManager.terminal.new": "Ny terminalfane",
"agentManager.terminal.addCentral": "Ny central terminalfane",
"agentManager.terminal.addTerminal": "Ny terminalfane i sidepanelet",
"agentManager.terminal.ended": "terminal afsluttet — luk fanen for at fjerne",
"agentManager.terminal.endedRestartable": "terminal afsluttet - skriv for at starte en ny shell, eller luk fanen",
"agentManager.terminal.setupFailed": "opsætningsscript mislykkedes",
@@ -191,6 +193,21 @@ export const dict = {
"agentManager.review.metaFile": "Fil",
"agentManager.review.metaLine": "Linje",
"agentManager.review.metaComment": "Brugerkommentar",
"agentManager.review.metaAuthor": "Forfatter",
"agentManager.pr.comment.title": "Kommentarer",
"agentManager.pr.comment.unresolvedCount": "{{count}} uløste",
"agentManager.pr.comment.resolvedGroup": "Løste ({{count}})",
"agentManager.pr.comment.sendAll": "Send {{count}} uløste til agenten",
"agentManager.pr.comment.sendAllToTerminal": "Send {{count}} uløste til terminalen",
"agentManager.pr.comment.send": "Send til agenten",
"agentManager.pr.comment.resolve": "Løs",
"agentManager.pr.comment.unresolve": "Ophæv løsning",
"agentManager.pr.comment.outdated": "Forældet",
"agentManager.pr.comment.sent": "Sendt",
"agentManager.pr.comment.copy": "Kopiér kommentar",
"agentManager.pr.comment.openOnGitHub": "Åbn på GitHub",
"agentManager.pr.comment.resolveFailed": "Kunne ikke løse. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Kunne ikke ophæve løsning. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} foldet sammen",
"agentManager.review.collapsedWithLarge": "{{collapsed}} foldet sammen, {{large}} store",
"agentManager.review.largeFileCollapsed": "Stor fil (sammenklappet)",
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
"agentManager.terminal.new": "Neuer Terminal-Tab",
"agentManager.terminal.addCentral": "Neuer zentraler Terminal-Tab",
"agentManager.terminal.addTerminal": "Neuer Terminal-Tab in der Seitenleiste",
"agentManager.terminal.ended": "Terminal beendet — Tab schließen zum Verwerfen",
"agentManager.terminal.endedRestartable":
"Terminal beendet - tippen, um eine neue Shell zu starten, oder Tab schließen",
@@ -192,6 +194,21 @@ export const dict = {
"agentManager.review.metaFile": "Datei",
"agentManager.review.metaLine": "Zeile",
"agentManager.review.metaComment": "Benutzerkommentar",
"agentManager.review.metaAuthor": "Autor",
"agentManager.pr.comment.title": "Kommentare",
"agentManager.pr.comment.unresolvedCount": "{{count}} ungelöst",
"agentManager.pr.comment.resolvedGroup": "Gelöst ({{count}})",
"agentManager.pr.comment.sendAll": "{{count}} ungelöste an Agent senden",
"agentManager.pr.comment.sendAllToTerminal": "{{count}} ungelöste an das Terminal senden",
"agentManager.pr.comment.send": "An Agent senden",
"agentManager.pr.comment.resolve": "Auflösen",
"agentManager.pr.comment.unresolve": "Auflösung aufheben",
"agentManager.pr.comment.outdated": "Veraltet",
"agentManager.pr.comment.sent": "Gesendet",
"agentManager.pr.comment.copy": "Kommentar kopieren",
"agentManager.pr.comment.openOnGitHub": "Auf GitHub öffnen",
"agentManager.pr.comment.resolveFailed": "Konnte nicht aufgelöst werden. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Auflösung konnte nicht aufgehoben werden. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} eingeklappt",
"agentManager.review.collapsedWithLarge": "{{collapsed}} eingeklappt, {{large}} groß",
"agentManager.review.largeFileCollapsed": "Große Datei (eingeklappt)",
@@ -66,6 +66,8 @@ export const dict = {
"agentManager.terminal.new": "New Terminal Tab",
"agentManager.terminal.add": "New terminal",
"agentManager.terminal.addCentral": "New central terminal tab",
"agentManager.terminal.addTerminal": "New sidebar terminal tab",
"agentManager.terminal.ended": "terminal ended — close tab to dismiss",
"agentManager.terminal.endedRestartable": "terminal ended - type to start a new shell or close tab to dismiss",
"agentManager.terminal.setupFailed": "setup script failed",
@@ -193,6 +195,21 @@ export const dict = {
"agentManager.review.metaFile": "File",
"agentManager.review.metaLine": "Line",
"agentManager.review.metaComment": "User comment",
"agentManager.review.metaAuthor": "Author",
"agentManager.pr.comment.title": "Comments",
"agentManager.pr.comment.unresolvedCount": "{{count}} unresolved",
"agentManager.pr.comment.resolvedGroup": "Resolved ({{count}})",
"agentManager.pr.comment.sendAll": "Send {{count}} unresolved to agent",
"agentManager.pr.comment.sendAllToTerminal": "Send {{count}} unresolved to terminal",
"agentManager.pr.comment.send": "Send to agent",
"agentManager.pr.comment.resolve": "Resolve",
"agentManager.pr.comment.unresolve": "Unresolve",
"agentManager.pr.comment.outdated": "Outdated",
"agentManager.pr.comment.sent": "Sent",
"agentManager.pr.comment.copy": "Copy comment",
"agentManager.pr.comment.openOnGitHub": "Open on GitHub",
"agentManager.pr.comment.resolveFailed": "Could not resolve. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Could not unresolve. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} collapsed",
"agentManager.review.collapsedWithLarge": "{{collapsed}} collapsed, {{large}} large",
"agentManager.review.largeFileCollapsed": "Large file (collapsed)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
"agentManager.terminal.new": "Nueva pestaña de terminal",
"agentManager.terminal.addCentral": "Nueva pestaña de terminal central",
"agentManager.terminal.addTerminal": "Nueva pestaña de terminal en la barra lateral",
"agentManager.terminal.ended": "terminal finalizado — cierra la pestaña para descartar",
"agentManager.terminal.endedRestartable":
"terminal finalizado - escribe para iniciar un shell nuevo o cierra la pestaña",
@@ -191,6 +193,21 @@ export const dict = {
"agentManager.review.metaFile": "Archivo",
"agentManager.review.metaLine": "Línea",
"agentManager.review.metaComment": "Comentario del usuario",
"agentManager.review.metaAuthor": "Autor",
"agentManager.pr.comment.title": "Comentarios",
"agentManager.pr.comment.unresolvedCount": "{{count}} sin resolver",
"agentManager.pr.comment.resolvedGroup": "Resueltos ({{count}})",
"agentManager.pr.comment.sendAll": "Enviar {{count}} sin resolver al agente",
"agentManager.pr.comment.sendAllToTerminal": "Enviar {{count}} sin resolver al terminal",
"agentManager.pr.comment.send": "Enviar al agente",
"agentManager.pr.comment.resolve": "Resolver",
"agentManager.pr.comment.unresolve": "Deshacer resolución",
"agentManager.pr.comment.outdated": "Obsoleto",
"agentManager.pr.comment.sent": "Enviado",
"agentManager.pr.comment.copy": "Copiar comentario",
"agentManager.pr.comment.openOnGitHub": "Abrir en GitHub",
"agentManager.pr.comment.resolveFailed": "No se pudo resolver. {{error}}",
"agentManager.pr.comment.unresolveFailed": "No se pudo deshacer la resolución. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} contraídos",
"agentManager.review.collapsedWithLarge": "{{collapsed}} contraídos, {{large}} grandes",
"agentManager.review.largeFileCollapsed": "Archivo grande (contraído)",
+17
View File
@@ -65,6 +65,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "محلی و WORKTREE‌ها",
"agentManager.terminal.new": "تب ترمینال جدید",
"agentManager.terminal.addCentral": "زبانه ترمینال مرکزی جدید",
"agentManager.terminal.addTerminal": "زبانه ترمینال جدید در نوار کناری",
"agentManager.terminal.add": "ترمینال جدید",
"agentManager.terminal.ended": "ترمینال پایان یافت — برای بستن، تب را ببندید",
"agentManager.terminal.endedRestartable": "ترمینال پایان یافت - برای شروع پوسته جدید تایپ کنید یا تب را ببندید",
@@ -194,6 +196,21 @@ export const dict = {
"agentManager.review.metaFile": "فایل",
"agentManager.review.metaLine": "خط",
"agentManager.review.metaComment": "نظر کاربر",
"agentManager.review.metaAuthor": "نویسنده",
"agentManager.pr.comment.title": "نظرات",
"agentManager.pr.comment.unresolvedCount": "{{count}} حل‌نشده",
"agentManager.pr.comment.resolvedGroup": "حل‌شده ({{count}})",
"agentManager.pr.comment.sendAll": "ارسال {{count}} مورد حل‌نشده به عامل",
"agentManager.pr.comment.sendAllToTerminal": "ارسال {{count}} مورد حل‌نشده به ترمینال",
"agentManager.pr.comment.send": "ارسال به عامل",
"agentManager.pr.comment.resolve": "حل کردن",
"agentManager.pr.comment.unresolve": "لغو حل",
"agentManager.pr.comment.outdated": "منسوخ",
"agentManager.pr.comment.sent": "ارسال شد",
"agentManager.pr.comment.copy": "کپی نظر",
"agentManager.pr.comment.openOnGitHub": "باز کردن در GitHub",
"agentManager.pr.comment.resolveFailed": "حل کردن ممکن نبود. {{error}}",
"agentManager.pr.comment.unresolveFailed": "لغو حل ممکن نبود. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} جمع‌شده",
"agentManager.review.collapsedWithLarge": "{{collapsed}} جمع‌شده، {{large}} بزرگ",
"agentManager.review.largeFileCollapsed": "فایل بزرگ (جمع‌شده)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOCAL ET WORKTREES",
"agentManager.terminal.new": "Nouvel onglet de terminal",
"agentManager.terminal.addCentral": "Nouvel onglet de terminal central",
"agentManager.terminal.addTerminal": "Nouvel onglet de terminal dans la barre latérale",
"agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer",
"agentManager.terminal.endedRestartable":
"terminal terminé - saisissez du texte pour démarrer un nouveau shell ou fermez l'onglet",
@@ -192,6 +194,21 @@ export const dict = {
"agentManager.review.metaFile": "Fichier",
"agentManager.review.metaLine": "Ligne",
"agentManager.review.metaComment": "Commentaire de l'utilisateur",
"agentManager.review.metaAuthor": "Auteur",
"agentManager.pr.comment.title": "Commentaires",
"agentManager.pr.comment.unresolvedCount": "{{count}} non résolus",
"agentManager.pr.comment.resolvedGroup": "Résolus ({{count}})",
"agentManager.pr.comment.sendAll": "Envoyer {{count}} non résolus à lagent",
"agentManager.pr.comment.sendAllToTerminal": "Envoyer {{count}} non résolus au terminal",
"agentManager.pr.comment.send": "Envoyer à lagent",
"agentManager.pr.comment.resolve": "Résoudre",
"agentManager.pr.comment.unresolve": "Annuler la résolution",
"agentManager.pr.comment.outdated": "Obsolète",
"agentManager.pr.comment.sent": "Envoyé",
"agentManager.pr.comment.copy": "Copier le commentaire",
"agentManager.pr.comment.openOnGitHub": "Ouvrir sur GitHub",
"agentManager.pr.comment.resolveFailed": "Impossible de résoudre. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Impossible dannuler la résolution. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} repliés",
"agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux",
"agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)",
+17
View File
@@ -66,6 +66,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOCALE & WORKTREE",
"agentManager.terminal.new": "Nuova scheda terminale",
"agentManager.terminal.addCentral": "Nuova scheda terminale centrale",
"agentManager.terminal.addTerminal": "Nuova scheda terminale nella barra laterale",
"agentManager.terminal.ended": "terminale terminato - chiudi la scheda per nasconderlo",
"agentManager.terminal.endedRestartable":
"terminale terminato - digita per avviare una nuova shell o chiudi la scheda",
@@ -197,6 +199,21 @@ export const dict = {
"agentManager.review.metaFile": "File",
"agentManager.review.metaLine": "Riga",
"agentManager.review.metaComment": "Commento utente",
"agentManager.review.metaAuthor": "Autore",
"agentManager.pr.comment.title": "Commenti",
"agentManager.pr.comment.unresolvedCount": "{{count}} non risolti",
"agentManager.pr.comment.resolvedGroup": "Risolti ({{count}})",
"agentManager.pr.comment.sendAll": "Invia {{count}} non risolti all'agente",
"agentManager.pr.comment.sendAllToTerminal": "Invia {{count}} non risolti al terminale",
"agentManager.pr.comment.send": "Invia all'agente",
"agentManager.pr.comment.resolve": "Risolvi",
"agentManager.pr.comment.unresolve": "Annulla risoluzione",
"agentManager.pr.comment.outdated": "Obsoleto",
"agentManager.pr.comment.sent": "Inviato",
"agentManager.pr.comment.copy": "Copia commento",
"agentManager.pr.comment.openOnGitHub": "Apri su GitHub",
"agentManager.pr.comment.resolveFailed": "Impossibile risolvere. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Impossibile annullare la risoluzione. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} compressi",
"agentManager.review.collapsedWithLarge": "{{collapsed}} compressi, {{large}} grandi",
"agentManager.review.largeFileCollapsed": "File grande (compresso)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "ローカル & WORKTREES",
"agentManager.terminal.new": "新しいターミナルタブ",
"agentManager.terminal.addCentral": "新しい中央ターミナルタブ",
"agentManager.terminal.addTerminal": "サイドバーに新しいターミナルタブ",
"agentManager.terminal.ended": "ターミナルが終了しました — タブを閉じて破棄",
"agentManager.terminal.endedRestartable":
"ターミナルが終了しました - 入力して新しいシェルを開始するか、タブを閉じてください",
@@ -191,6 +193,21 @@ export const dict = {
"agentManager.review.metaFile": "ファイル",
"agentManager.review.metaLine": "行",
"agentManager.review.metaComment": "ユーザーコメント",
"agentManager.review.metaAuthor": "作成者",
"agentManager.pr.comment.title": "コメント",
"agentManager.pr.comment.unresolvedCount": "{{count}} 件未解決",
"agentManager.pr.comment.resolvedGroup": "解決済み ({{count}})",
"agentManager.pr.comment.sendAll": "{{count}} 件の未解決コメントをエージェントに送信",
"agentManager.pr.comment.sendAllToTerminal": "{{count}} 件の未解決コメントをターミナルに送信",
"agentManager.pr.comment.send": "エージェントに送信",
"agentManager.pr.comment.resolve": "解決",
"agentManager.pr.comment.unresolve": "解決を取り消す",
"agentManager.pr.comment.outdated": "古い",
"agentManager.pr.comment.sent": "送信済み",
"agentManager.pr.comment.copy": "コメントをコピー",
"agentManager.pr.comment.openOnGitHub": "GitHubで開く",
"agentManager.pr.comment.resolveFailed": "解決できませんでした。{{error}}",
"agentManager.pr.comment.unresolveFailed": "解決を取り消せませんでした。{{error}}",
"agentManager.review.collapsedOnly": "{{count}} 件折りたたみ",
"agentManager.review.collapsedWithLarge": "{{collapsed}} 件折りたたみ、{{large}} 件がサイズ大",
"agentManager.review.largeFileCollapsed": "大きなファイル(折りたたみ)",
+17
View File
@@ -61,6 +61,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "로컬 & WORKTREES",
"agentManager.terminal.new": "새 터미널 탭",
"agentManager.terminal.addCentral": "새 중앙 터미널 탭",
"agentManager.terminal.addTerminal": "사이드바에 새 터미널 탭",
"agentManager.terminal.ended": "터미널 종료됨 — 탭을 닫아 해제",
"agentManager.terminal.endedRestartable": "터미널 종료됨 - 입력하여 새 셸을 시작하거나 탭을 닫으세요",
"agentManager.terminal.setupFailed": "설정 스크립트 실패",
@@ -189,6 +191,21 @@ export const dict = {
"agentManager.review.metaFile": "파일",
"agentManager.review.metaLine": "라인",
"agentManager.review.metaComment": "사용자 코멘트",
"agentManager.review.metaAuthor": "작성자",
"agentManager.pr.comment.title": "댓글",
"agentManager.pr.comment.unresolvedCount": "{{count}}개 미해결",
"agentManager.pr.comment.resolvedGroup": "해결됨 ({{count}})",
"agentManager.pr.comment.sendAll": "{{count}}개 미해결 댓글을 에이전트로 보내기",
"agentManager.pr.comment.sendAllToTerminal": "{{count}}개 미해결 댓글을 터미널로 보내기",
"agentManager.pr.comment.send": "에이전트로 보내기",
"agentManager.pr.comment.resolve": "해결",
"agentManager.pr.comment.unresolve": "해결 취소",
"agentManager.pr.comment.outdated": "오래됨",
"agentManager.pr.comment.sent": "전송됨",
"agentManager.pr.comment.copy": "댓글 복사",
"agentManager.pr.comment.openOnGitHub": "GitHub에서 열기",
"agentManager.pr.comment.resolveFailed": "해결할 수 없습니다. {{error}}",
"agentManager.pr.comment.unresolveFailed": "해결을 취소할 수 없습니다. {{error}}",
"agentManager.review.collapsedOnly": "{{count}}개 접힘",
"agentManager.review.collapsedWithLarge": "{{collapsed}}개 접힘, {{large}}개 대용량",
"agentManager.review.largeFileCollapsed": "큰 파일(접힘)",
+17
View File
@@ -65,6 +65,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKAAL & WORKTREES",
"agentManager.terminal.new": "Nieuw terminaltabblad",
"agentManager.terminal.addCentral": "Nieuw centraal terminaltabblad",
"agentManager.terminal.addTerminal": "Nieuw terminaltabblad in de zijbalk",
"agentManager.terminal.ended": "terminal beëindigd — sluit tabblad om te negeren",
"agentManager.terminal.endedRestartable":
"terminal beëindigd - typ om een nieuwe shell te starten of sluit het tabblad",
@@ -196,6 +198,21 @@ export const dict = {
"agentManager.review.metaFile": "Bestand",
"agentManager.review.metaLine": "Regel",
"agentManager.review.metaComment": "Gebruikersopmerking",
"agentManager.review.metaAuthor": "Auteur",
"agentManager.pr.comment.title": "Opmerkingen",
"agentManager.pr.comment.unresolvedCount": "{{count}} onopgelost",
"agentManager.pr.comment.resolvedGroup": "Opgelost ({{count}})",
"agentManager.pr.comment.sendAll": "{{count}} onopgeloste opmerkingen naar agent sturen",
"agentManager.pr.comment.sendAllToTerminal": "{{count}} onopgeloste opmerkingen naar terminal sturen",
"agentManager.pr.comment.send": "Naar agent sturen",
"agentManager.pr.comment.resolve": "Oplossen",
"agentManager.pr.comment.unresolve": "Oplossing ongedaan maken",
"agentManager.pr.comment.outdated": "Verouderd",
"agentManager.pr.comment.sent": "Verzonden",
"agentManager.pr.comment.copy": "Opmerking kopiëren",
"agentManager.pr.comment.openOnGitHub": "Openen op GitHub",
"agentManager.pr.comment.resolveFailed": "Kan niet oplossen. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Kan oplossing niet ongedaan maken. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} ingeklapt",
"agentManager.review.collapsedWithLarge": "{{collapsed}} ingeklapt, {{large}} groot",
"agentManager.review.largeFileCollapsed": "Groot bestand (ingeklapt)",
+17
View File
@@ -61,6 +61,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
"agentManager.terminal.new": "Ny terminalfane",
"agentManager.terminal.addCentral": "Ny sentral terminalfane",
"agentManager.terminal.addTerminal": "Ny terminalfane i sidepanelet",
"agentManager.terminal.ended": "terminal avsluttet — lukk fanen for å avvise",
"agentManager.terminal.endedRestartable": "terminal avsluttet - skriv for å starte et nytt skall, eller lukk fanen",
"agentManager.terminal.setupFailed": "oppsettskript mislyktes",
@@ -188,6 +190,21 @@ export const dict = {
"agentManager.review.metaFile": "Fil",
"agentManager.review.metaLine": "Linje",
"agentManager.review.metaComment": "Brukerkommentar",
"agentManager.review.metaAuthor": "Forfatter",
"agentManager.pr.comment.title": "Kommentarer",
"agentManager.pr.comment.unresolvedCount": "{{count}} uløste",
"agentManager.pr.comment.resolvedGroup": "Løste ({{count}})",
"agentManager.pr.comment.sendAll": "Send {{count}} uløste til agenten",
"agentManager.pr.comment.sendAllToTerminal": "Send {{count}} uløste til terminalen",
"agentManager.pr.comment.send": "Send til agenten",
"agentManager.pr.comment.resolve": "Løs",
"agentManager.pr.comment.unresolve": "Opphev løsning",
"agentManager.pr.comment.outdated": "Utdatert",
"agentManager.pr.comment.sent": "Sendt",
"agentManager.pr.comment.copy": "Kopier kommentar",
"agentManager.pr.comment.openOnGitHub": "Åpne på GitHub",
"agentManager.pr.comment.resolveFailed": "Kunne ikke løse. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Kunne ikke oppheve løsning. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} kollapset",
"agentManager.review.collapsedWithLarge": "{{collapsed}} kollapset, {{large}} store",
"agentManager.review.largeFileCollapsed": "Stor fil (sammenfoldet)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "LOKALNE & WORKTREES",
"agentManager.terminal.new": "Nowa karta terminala",
"agentManager.terminal.addCentral": "Nowa centralna karta terminala",
"agentManager.terminal.addTerminal": "Nowa karta terminala na pasku bocznym",
"agentManager.terminal.ended": "terminal zakończony — zamknij kartę, aby zamknąć",
"agentManager.terminal.endedRestartable":
"terminal zakończony - wpisz tekst, aby uruchomić nową powłokę, lub zamknij kartę",
@@ -190,6 +192,21 @@ export const dict = {
"agentManager.review.metaFile": "Plik",
"agentManager.review.metaLine": "Linia",
"agentManager.review.metaComment": "Komentarz użytkownika",
"agentManager.review.metaAuthor": "Autor",
"agentManager.pr.comment.title": "Komentarze",
"agentManager.pr.comment.unresolvedCount": "{{count}} nierozwiązanych",
"agentManager.pr.comment.resolvedGroup": "Rozwiązane ({{count}})",
"agentManager.pr.comment.sendAll": "Wyślij {{count}} nierozwiązanych do agenta",
"agentManager.pr.comment.sendAllToTerminal": "Wyślij {{count}} nierozwiązanych do terminala",
"agentManager.pr.comment.send": "Wyślij do agenta",
"agentManager.pr.comment.resolve": "Rozwiąż",
"agentManager.pr.comment.unresolve": "Cofnij rozwiązanie",
"agentManager.pr.comment.outdated": "Nieaktualne",
"agentManager.pr.comment.sent": "Wysłano",
"agentManager.pr.comment.copy": "Kopiuj komentarz",
"agentManager.pr.comment.openOnGitHub": "Otwórz na GitHub",
"agentManager.pr.comment.resolveFailed": "Nie można rozwiązać. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Nie można cofnąć rozwiązania. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} zwiniętych",
"agentManager.review.collapsedWithLarge": "{{collapsed}} zwiniętych, {{large}} dużych",
"agentManager.review.largeFileCollapsed": "Duży plik (zwinięty)",
+17
View File
@@ -62,6 +62,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "ЛОКАЛЬНЫЙ & WORKTREES",
"agentManager.terminal.new": "Новая вкладка терминала",
"agentManager.terminal.addCentral": "Новая центральная вкладка терминала",
"agentManager.terminal.addTerminal": "Новая вкладка терминала на боковой панели",
"agentManager.terminal.ended": "терминал завершен — закройте вкладку, чтобы скрыть",
"agentManager.terminal.endedRestartable":
"терминал завершен - введите текст, чтобы запустить новую оболочку, или закройте вкладку",
@@ -191,6 +193,21 @@ export const dict = {
"agentManager.review.metaFile": "Файл",
"agentManager.review.metaLine": "Строка",
"agentManager.review.metaComment": "Комментарий пользователя",
"agentManager.review.metaAuthor": "Автор",
"agentManager.pr.comment.title": "Комментарии",
"agentManager.pr.comment.unresolvedCount": "{{count}} нерешённых",
"agentManager.pr.comment.resolvedGroup": "Решённые ({{count}})",
"agentManager.pr.comment.sendAll": "Отправить {{count}} нерешённых агенту",
"agentManager.pr.comment.sendAllToTerminal": "Отправить {{count}} нерешённых в терминал",
"agentManager.pr.comment.send": "Отправить агенту",
"agentManager.pr.comment.resolve": "Решить",
"agentManager.pr.comment.unresolve": "Отменить решение",
"agentManager.pr.comment.outdated": "Устарело",
"agentManager.pr.comment.sent": "Отправлено",
"agentManager.pr.comment.copy": "Копировать комментарий",
"agentManager.pr.comment.openOnGitHub": "Открыть на GitHub",
"agentManager.pr.comment.resolveFailed": "Не удалось решить. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Не удалось отменить решение. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} свернуто",
"agentManager.review.collapsedWithLarge": "{{collapsed}} свернуто, {{large}} больших",
"agentManager.review.largeFileCollapsed": "Большой файл (свернут)",
+17
View File
@@ -61,6 +61,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "ในเครื่อง & WORKTREES",
"agentManager.terminal.new": "แท็บเทอร์มินัลใหม่",
"agentManager.terminal.addCentral": "แท็บเทอร์มินัลกลางใหม่",
"agentManager.terminal.addTerminal": "แท็บเทอร์มินัลใหม่ในแถบด้านข้าง",
"agentManager.terminal.ended": "เทอร์มินัลสิ้นสุด — ปิดแท็บเพื่อยกเลิก",
"agentManager.terminal.endedRestartable": "เทอร์มินัลสิ้นสุด - พิมพ์เพื่อเริ่มเชลล์ใหม่หรือปิดแท็บ",
"agentManager.terminal.setupFailed": "สคริปต์ติดตั้งล้มเหลว",
@@ -185,6 +187,21 @@ export const dict = {
"agentManager.review.metaFile": "ไฟล์",
"agentManager.review.metaLine": "บรรทัด",
"agentManager.review.metaComment": "ความคิดเห็นของผู้ใช้",
"agentManager.review.metaAuthor": "ผู้เขียน",
"agentManager.pr.comment.title": "ความคิดเห็น",
"agentManager.pr.comment.unresolvedCount": "ยังไม่แก้ไข {{count}} รายการ",
"agentManager.pr.comment.resolvedGroup": "แก้ไขแล้ว ({{count}})",
"agentManager.pr.comment.sendAll": "ส่งความคิดเห็นที่ยังไม่แก้ไข {{count}} รายการไปยังเอเจนต์",
"agentManager.pr.comment.sendAllToTerminal": "ส่งความคิดเห็นที่ยังไม่แก้ไข {{count}} รายการไปยังเทอร์มินัล",
"agentManager.pr.comment.send": "ส่งไปยังเอเจนต์",
"agentManager.pr.comment.resolve": "ทำเครื่องหมายว่าแก้ไขแล้ว",
"agentManager.pr.comment.unresolve": "ยกเลิกการแก้ไข",
"agentManager.pr.comment.outdated": "ล้าสมัย",
"agentManager.pr.comment.sent": "ส่งแล้ว",
"agentManager.pr.comment.copy": "คัดลอกความคิดเห็น",
"agentManager.pr.comment.openOnGitHub": "เปิดบน GitHub",
"agentManager.pr.comment.resolveFailed": "แก้ไขไม่ได้ {{error}}",
"agentManager.pr.comment.unresolveFailed": "ยกเลิกการแก้ไขไม่ได้ {{error}}",
"agentManager.review.collapsedOnly": "ยุบ {{count}} รายการ",
"agentManager.review.collapsedWithLarge": "ยุบ {{collapsed}} รายการ, ขนาดใหญ่ {{large}} รายการ",
"agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (พับอยู่)",
+17
View File
@@ -66,6 +66,8 @@ export const dict = {
"agentManager.sidebarSearch.contexts": "YEREL & WORKTREE'LER",
"agentManager.terminal.new": "Yeni Terminal Sekmesi",
"agentManager.terminal.addCentral": "Yeni merkezi terminal sekmesi",
"agentManager.terminal.addTerminal": "Kenar çubuğunda yeni terminal sekmesi",
"agentManager.terminal.ended": "terminal sona erdi — kapatmak için sekmeyi kapatın",
"agentManager.terminal.endedRestartable":
"terminal sona erdi - yeni bir kabuk başlatmak için yazın veya sekmeyi kapatın",
@@ -198,6 +200,21 @@ export const dict = {
"agentManager.review.metaFile": "Dosya",
"agentManager.review.metaLine": "Satır",
"agentManager.review.metaComment": "Kullanıcı yorumu",
"agentManager.review.metaAuthor": "Yazar",
"agentManager.pr.comment.title": "Yorumlar",
"agentManager.pr.comment.unresolvedCount": "{{count}} çözülmemiş",
"agentManager.pr.comment.resolvedGroup": "Çözüldü ({{count}})",
"agentManager.pr.comment.sendAll": "{{count}} çözülmemiş yorumu ajana gönder",
"agentManager.pr.comment.sendAllToTerminal": "{{count}} çözülmemiş yorumu terminale gönder",
"agentManager.pr.comment.send": "Ajana gönder",
"agentManager.pr.comment.resolve": "Çöz",
"agentManager.pr.comment.unresolve": "Çözümü geri al",
"agentManager.pr.comment.outdated": "Güncel değil",
"agentManager.pr.comment.sent": "Gönderildi",
"agentManager.pr.comment.copy": "Yorumu kopyala",
"agentManager.pr.comment.openOnGitHub": "GitHub'da aç",
"agentManager.pr.comment.resolveFailed": "Çözülemedi. {{error}}",
"agentManager.pr.comment.unresolveFailed": "Çözüm geri alınamadı. {{error}}",
"agentManager.review.collapsedOnly": "{{count}} daraltıldı",
"agentManager.review.collapsedWithLarge": "{{collapsed}} daraltıldı, {{large}} büyük",
"agentManager.review.largeFileCollapsed": "Büyük dosya (daraltıldı)",

Some files were not shown because too many files have changed in this diff Show More