mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
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:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep tool-output action buttons visible above streamed output.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Require the todo tool to update multi-step lists after each completed task.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d83b6fdde1c87d7bdbd4fae163dea888c2c50a54f175ceef2405699f8f48443e
|
||||
size 34823
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8dbff32f3c1b73230e1a1c84c1a683235fbf56fff8a418e6d982603f263ead0
|
||||
size 38718
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cef414cf59da1f632a7d5d9a2e44f792e7e2ea6b4041bcc214cb32c052f851d8
|
||||
size 46930
|
||||
oid sha256:24f8e46671ff667d05a5db36fcd45535040a2587202562a40f8735efa351a1e5
|
||||
size 46989
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cd6334875ae3c633396b90ffbcbd53e5567668e430c8ca97a4c1cc8bc30a0585
|
||||
size 17933
|
||||
oid sha256:b7ecf054c5e5ab8daa2ea0e3ef42e2e720a9dc3673d77058ad7df05751990193
|
||||
size 17036
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a093b9658cc15d0789da5afebde7163ec51e743af59975e2a031aed3e9144d56
|
||||
size 22336
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b20e8e50b583cc6a884e08648a9f1efcdfc2a9561b97145811ebc9c24d244fd
|
||||
size 23100
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3b24b0d2694d2eeec0ea4cd827bd3a61bb47150605b838b43e4f422c9b960f0e
|
||||
size 29744
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd27610a2518018c16e4227728221ed9bf67a7d3c8ab207b2835dc60964ec63d
|
||||
size 33323
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8ad46cc736472c6a2a46bb80e6622333f49960e74dcb6782d35aa72fa32378c
|
||||
size 23725
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c0a184f1d7c71f9877c4918e93b7e18f2ebe699f802c46846bd60f29459dbc21
|
||||
size 15864
|
||||
oid sha256:a2c0fe6b4fefd0925b670b8ffc42acd09eb7ca7bd848d100e7e4a7a85d67a9f5
|
||||
size 15807
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7fc23fcb7adf483c0b771ef601b23cb365dc7f40033b00fce703b35910aa4fc
|
||||
size 27159
|
||||
oid sha256:94377b6b66f1b35803963ce7951f473e44f16ca8463dfc8a2c81f3e816296533
|
||||
size 27245
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d11f4004ed3170647d385c14df077d235f5bb9bd6e5dec07557ee2014d553233
|
||||
size 27302
|
||||
oid sha256:102793cbc80c80c7c889db669016566ceafe86eb37d65b7505a7483d1e883cbd
|
||||
size 27527
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8bd56ba87d0c2bbef8a2325ab2e9f0e956c89e2602ff8ca7415d5fb7c9fa1f3
|
||||
size 29709
|
||||
oid sha256:cbe5bc3635a41e9aac342c2fcfd6adb1f38e0ce28ea7cf10b9f7297067103673
|
||||
size 29743
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d5a5fdd318bd14100e0c71f7f9055d830db1e59b6af57007608881f433a177ca
|
||||
size 18703
|
||||
oid sha256:db7fdb7b9699dc4b668259b61731e3a024c1ed20d8232055681f551889fac086
|
||||
size 18942
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5b64674aaceb051ba48bf7db489845f50c8bd7f65a108685dda5243103c735a
|
||||
size 18477
|
||||
oid sha256:21097ecf30d0e605f2117c5e98f047db7e65eb43f921a380e7ccade21fc6fc34
|
||||
size 19594
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ?? []
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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": "ملف كبير (مطوي)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": "فایل بزرگ (جمعشده)",
|
||||
|
||||
@@ -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 à l’agent",
|
||||
"agentManager.pr.comment.sendAllToTerminal": "Envoyer {{count}} non résolus au terminal",
|
||||
"agentManager.pr.comment.send": "Envoyer à l’agent",
|
||||
"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 d’annuler la résolution. {{error}}",
|
||||
"agentManager.review.collapsedOnly": "{{count}} repliés",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux",
|
||||
"agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": "大きなファイル(折りたたみ)",
|
||||
|
||||
@@ -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": "큰 파일(접힘)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": "Большой файл (свернут)",
|
||||
|
||||
@@ -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": "ไฟล์ขนาดใหญ่ (พับอยู่)",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user