mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
Merge remote-tracking branch 'origin/main' into gigantic-fighter
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Allow agents to stop and remove a targeted Agent Manager session.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Respect the Terminal Command Blocks display setting for background process tool cards.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
Show the actual routed model and upstream cost for Kilo Gateway Anthropic Messages and OpenAI Responses requests, including requests served through OpenRouter.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Support bidirectional text in chat messages so right-to-left and mixed-language conversations render in the correct direction.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-ui": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Use distinct icons for context, project memory, and code indexing, and group context controls to preserve task titles in narrow VS Code sidebars.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Open the Agent Manager new-worktree dialog from primary actions and keyboard shortcuts, with an explicit quick-create action in its dropdown.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Speed up local conversation recall searches on large histories.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep sessions usable after deprecated local review commands complete without a session status transition.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Restore directory `@`-mentions by listing their entries without inlining child file contents. Untrusted external directory attachments remain denied.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent sessions from freezing after submitting or dismissing a question.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show newly submitted messages immediately after reverting a conversation.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Keep chat auto-scroll detached after a small upward wheel scroll near the latest message.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix Grok 4.5 reasoning variants not showing up in the model picker.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/kilo-indexing": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Configure a custom file extension allowlist for codebase indexing to limit scans to relevant project files and support additional text formats.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support editing custom OpenAI-compatible providers from JetBrains settings and replace their Disconnect action with Edit and Delete. Added or edited providers stay selected, and the custom provider dialog now closes after a successful save.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix adding a Custom OpenAI-Compatible Provider silently failing. The dialog now requires at least one model and reports save errors inline so you can correct your input and retry without re-entering the form.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Match the model picker Close button styling to JetBrains dialog primary buttons.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Use a trash icon for provider delete and show provider edit/delete actions on selection, matching the other settings lists.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Improve CLI project-memory controls, status, activity indicators, and optional recall details.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-memory": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Improve project-memory activity in VS Code with a task-header menu, quick actions, and optional verbose details.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/sdk": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support verbose project-memory settings and show recalled memory snippets in conversation markers when enabled.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-indexing": patch
|
||||
---
|
||||
|
||||
Allow remote embedder validation timeouts to retry before indexing reports a connection failure.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Remote CLI: expose slash command discovery, execution, and `/new` session creation over the relay. `list_commands` and `send_command` (including the built-in `compact` flow) are scoped to the current session's directory. `create_session` creates a root session in that directory, attaches it to the relay heartbeat set, and returns the new session id only after the heartbeat completes so the mobile client can navigate immediately. Failures are sanitized; the command is not auto-retried and the user may retry manually after a transient relay failure.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Retry incomplete model responses that end without final output or tool activity while preserving partial answers and completed tools.
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -139,7 +139,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -151,7 +151,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -164,8 +164,9 @@
|
||||
},
|
||||
"packages/kilo-console": {
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@lottiefiles/dotlottie-web": "0.74.0",
|
||||
@@ -186,7 +187,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -216,7 +217,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -252,7 +253,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -262,7 +263,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -298,7 +299,7 @@
|
||||
},
|
||||
"packages/kilo-memory": {
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -312,7 +313,7 @@
|
||||
},
|
||||
"packages/kilo-sandbox": {
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -327,7 +328,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -341,7 +342,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -378,7 +379,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -447,7 +448,7 @@
|
||||
},
|
||||
"packages/kilo-web-ui": {
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -464,7 +465,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -482,7 +483,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -646,7 +647,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -674,7 +675,7 @@
|
||||
},
|
||||
"packages/plugin-atomic-chat": {
|
||||
"name": "@kilocode/plugin-atomic-chat",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
},
|
||||
@@ -688,7 +689,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -699,7 +700,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -714,7 +715,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -728,7 +729,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -751,7 +752,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -793,6 +794,7 @@
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/luxon": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"happy-dom": "20.8.9",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-yNHrsBXfCWeuX13frVTjIL7U4k71wSimDtdTjoBndoY=",
|
||||
"aarch64-linux": "sha256-zbRBPcPduV1a2EH4BiZWPJ+LyZMLCEPkQ3s+U7tDxtk=",
|
||||
"aarch64-darwin": "sha256-NLLlE2t3vs1E5fDwyiFGoneJAdTb6uXrFuVa+LwBtlY=",
|
||||
"x86_64-darwin": "sha256-BH31+FPgkt1xXJixkj+DgXIveJ49fWk4AIc3Hx+4XzM="
|
||||
"x86_64-linux": "sha256-SiTkoVUEnR1ugnP39xrw+khD3+TN+3wQ733gw8go76Y=",
|
||||
"aarch64-linux": "sha256-9czWyKgE0xqJiUl6Aprampob49cHe8awMQOmxpX1FB8=",
|
||||
"aarch64-darwin": "sha256-yRs6Mincbja/s2EJ7Ln+WjO/f/uwg6xD4d/SkmqM+OY=",
|
||||
"x86_64-darwin": "sha256-4DbNm21/+pNGVLfgdNLtotqqcwzr70sbYWCqxftcbAo="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -164,6 +164,6 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.4.9"
|
||||
version = "7.4.11"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.9/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@lottiefiles/dotlottie-web": "0.74.0",
|
||||
|
||||
@@ -6,7 +6,16 @@ import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { loadEmbeddingModels } from "../../client"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { clean, clone, merge, providerPatch, removed, shouldSync, validate } from "./state/indexing"
|
||||
import {
|
||||
clean,
|
||||
clone,
|
||||
merge,
|
||||
parseFileExtensions,
|
||||
providerPatch,
|
||||
removed,
|
||||
shouldSync,
|
||||
validate,
|
||||
} from "./state/indexing"
|
||||
|
||||
type Provider = NonNullable<IndexingConfig["provider"]>
|
||||
type ProviderValue = Provider | ""
|
||||
@@ -111,6 +120,7 @@ export function IndexingRoute() {
|
||||
const [draft, setDraft] = createSignal<IndexingConfig>({})
|
||||
const [source, setSource] = createSignal("")
|
||||
const [dirty, setDirty] = createSignal(false)
|
||||
const [extensionText, setExtensionText] = createSignal("")
|
||||
const scope = () => ctx.query()?.scope ?? "global"
|
||||
const [selected, setSelected] = createSignal(scope())
|
||||
const project = () => scope() === "project"
|
||||
@@ -149,6 +159,8 @@ export function IndexingRoute() {
|
||||
setSelected(current)
|
||||
setSource(key)
|
||||
setDraft(clone(next))
|
||||
const extensions = project() ? merge(global(), next).fileExtensions : next.fileExtensions
|
||||
setExtensionText(extensions?.join(", ") ?? "")
|
||||
setDirty(false)
|
||||
})
|
||||
|
||||
@@ -478,6 +490,28 @@ export function IndexingRoute() {
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="File extensions"
|
||||
description="Comma-separated allowlist. Leave empty to use the built-in defaults."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("fileExtensions")?.source}
|
||||
inherited={field("fileExtensions")?.inherited}
|
||||
overridden={field("fileExtensions")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={extensionText()}
|
||||
placeholder=".php, .js, .css"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => {
|
||||
const value = event.currentTarget.value
|
||||
setExtensionText(value)
|
||||
update({ fileExtensions: parseFileExtensions(value) })
|
||||
}}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard
|
||||
label="Minimum search score"
|
||||
description="Similarity threshold from 0 to 1. Default is model-specific or 0.4."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { clean, merge, providerPatch, removed, shouldSync, validate } from "./indexing"
|
||||
import { clean, merge, parseFileExtensions, providerPatch, removed, shouldSync, validate } from "./indexing"
|
||||
|
||||
describe("indexing config state", () => {
|
||||
test("merges project settings over nested global settings", () => {
|
||||
@@ -59,4 +59,13 @@ describe("indexing config state", () => {
|
||||
"Embedding batch size must be a positive integer.",
|
||||
])
|
||||
})
|
||||
|
||||
test("parses and cleans file extension allowlists", () => {
|
||||
expect(parseFileExtensions(" PHP, , .JS, js, C++, c++ ")).toEqual([".c++", ".js", ".php"])
|
||||
expect(parseFileExtensions(" , , ")).toBeUndefined()
|
||||
expect(clean({ fileExtensions: [] })).toEqual({})
|
||||
expect(validate({ fileExtensions: ["*.js", ".d.ts", "src/php"] })).toEqual([
|
||||
"File extensions must contain only a name with an optional leading dot.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { IndexingConfig } from "@kilocode/sdk/v2/client"
|
||||
import { isFileExtension } from "@kilocode/kilo-indexing/file-extensions"
|
||||
|
||||
export { parseFileExtensions } from "@kilocode/kilo-indexing/file-extensions"
|
||||
|
||||
function record(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
@@ -26,6 +29,7 @@ export function merge(base: IndexingConfig | undefined, patch: IndexingConfig |
|
||||
|
||||
function prune(input: unknown): unknown {
|
||||
if (typeof input === "string") return input.trim() || undefined
|
||||
if (Array.isArray(input)) return input.length > 0 ? input : undefined
|
||||
if (!record(input)) return input ?? undefined
|
||||
const entries = Object.entries(input).flatMap(([key, value]) => {
|
||||
const next = prune(value)
|
||||
@@ -64,6 +68,9 @@ export function removed(before: IndexingConfig, after: IndexingConfig): string[]
|
||||
|
||||
export function validate(input: IndexingConfig): string[] {
|
||||
const errors: string[] = []
|
||||
if (input.fileExtensions?.some((item) => !isFileExtension(item))) {
|
||||
errors.push("File extensions must contain only a name with an optional leading dot.")
|
||||
}
|
||||
if (input.dimension !== undefined && input.dimension !== null) {
|
||||
if (!Number.isInteger(input.dimension) || input.dimension <= 0)
|
||||
errors.push("Vector dimension must be a positive integer.")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -35,7 +35,7 @@ Parallel work pays off when sessions are **independent** — neither one's outpu
|
||||
|
||||
Every productive worktree session follows the same rhythm:
|
||||
|
||||
1. **Create a worktree** (`Cmd+N` / `Ctrl+N`) and describe the task.
|
||||
1. **Open a new worktree dialog** (`Cmd+N` / `Ctrl+N`), then describe the task.
|
||||
2. **Let the agent run.** Switch to another worktree, another session, or step away.
|
||||
3. **Verify manually.** Before you trust "all tests pass", run the app with the run script (`Cmd+E` / `Ctrl+E`) or open the worktree's terminal (`Cmd+/` / `Ctrl+/`) and run the tests yourself.
|
||||
4. **Review the diff** (`Cmd+D` / `Ctrl+D`). Drop inline comments, then **Send to chat** to feed them back to the agent.
|
||||
@@ -48,7 +48,7 @@ The single biggest lever on this loop is **keeping each worktree's scope small**
|
||||
|
||||
### 1. Side quest
|
||||
|
||||
Something unrelated came up while you are mid-task. Create a new worktree for it (`Cmd+N`), let the agent work, review when it is done. Your main work is unaffected.
|
||||
Something unrelated came up while you are mid-task. Open a new worktree dialog for it (`Cmd+N`), let the agent work, review when it is done. Your main work is unaffected.
|
||||
|
||||
### 2. Build a skeleton, then split the work
|
||||
|
||||
@@ -64,7 +64,7 @@ This mirrors how a human team works: agree the API contract first, then split ba
|
||||
|
||||
For genuinely hard tasks where you do not know which approach will work:
|
||||
|
||||
1. Open the advanced new-worktree dialog (`Cmd+Shift+N` / `Ctrl+Shift+N`) and pick 2–4 versions.
|
||||
1. Open the new-worktree dialog (`Cmd+N` / `Ctrl+N`) and pick 2–4 versions.
|
||||
2. Optionally assign a different model to each.
|
||||
3. Review the diffs side by side, pick the winner, apply it, discard the rest.
|
||||
|
||||
@@ -76,7 +76,7 @@ A related pattern: use the sidebar as an investigation surface. Start two or thr
|
||||
|
||||
### 5. A worktree per bug
|
||||
|
||||
For a day of small fixes: one worktree per bug (`Cmd+N`), one branch per fix, merge each quickly so none drift. Close the worktree when the fix lands.
|
||||
For a day of small fixes: one worktree per bug. Use `Cmd+N` to configure each worktree or `Cmd+Shift+N` to create one immediately from the default branch, merge each quickly so none drift. Close the worktree when the fix lands.
|
||||
|
||||
### 6. Multiple sessions on one branch
|
||||
|
||||
@@ -225,8 +225,9 @@ Merge the most foundational one first. Then, in each remaining worktree, ask the
|
||||
| Situation | Where |
|
||||
|---|---|
|
||||
| Small, interactive task | Sidebar |
|
||||
| Long task, want to do something else meanwhile | New worktree (`Cmd+N`) |
|
||||
| Two or three approaches, pick the winner | Multi-version (`Cmd+Shift+N`) |
|
||||
| Long task, want to do something else meanwhile | Configure a new worktree (`Cmd+N`) |
|
||||
| Quick task on the default base branch | Create a new worktree (`Cmd+Shift+N`) |
|
||||
| Two or three approaches, pick the winner | Configure multi-version worktrees (`Cmd+N`) |
|
||||
| Sidebar task outgrew the sidebar | Continue in Worktree |
|
||||
| Separate conversation on the same branch | New tab (`Cmd+T`) |
|
||||
| Long conversation, want a fresh context on same branch | New tab, summarize |
|
||||
|
||||
@@ -69,7 +69,7 @@ The extension uses `gh` to automatically discover PRs for each worktree branch.
|
||||
2. **Branch name** — `gh pr view <branch>` matches same-repo branches pushed to origin
|
||||
3. **HEAD commit SHA** — `gh pr list --search "<sha>"` as a last resort, matching PRs whose head ref points to the exact same commit
|
||||
|
||||
You can also import a PR directly from the advanced new worktree dialog: open the **New Worktree** dropdown and select **Advanced**, or press `Cmd+Shift+N` (macOS) / `Ctrl+Shift+N` (Windows/Linux), switch to the **Import** tab, then paste the GitHub PR URL. The branch is checked out and the badge appears automatically.
|
||||
You can also import a PR directly from the new worktree dialog: click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux), switch to the **Import** tab, then paste the GitHub PR URL. The branch is checked out and the badge appears automatically.
|
||||
|
||||
#### Badge colors
|
||||
|
||||
@@ -106,12 +106,14 @@ PR badges update automatically in the background. The active worktree refreshes
|
||||
|
||||
### Creating a New Worktree Session
|
||||
|
||||
1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to create a new worktree
|
||||
1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to open the new worktree dialog
|
||||
2. Enter a branch name (or let Kilo generate one)
|
||||
3. Type your first message to start the agent
|
||||
3. Type your first message, then create the worktree
|
||||
|
||||
A new git worktree is created from your current branch. The agent works in isolation — your main branch is unaffected.
|
||||
|
||||
To create a worktree immediately from the default base branch, press `Cmd+Shift+N` (macOS) / `Ctrl+Shift+N` (Windows/Linux).
|
||||
|
||||
### Multi-Version Mode
|
||||
|
||||
You can run up to 4 parallel implementations of the same prompt across separate worktrees:
|
||||
@@ -352,8 +354,8 @@ Closing a managed worktree removes it from Agent Manager, deletes its `.kilo/wor
|
||||
| Shortcut (macOS) | Shortcut (Windows/Linux) | Action |
|
||||
|---|---|---|
|
||||
| `Cmd+Shift+M` | `Ctrl+Shift+M` | Open / focus Agent Manager (works from anywhere) |
|
||||
| `Cmd+N` | `Ctrl+N` | New worktree |
|
||||
| `Cmd+Shift+N` | `Ctrl+Shift+N` | New worktree (advanced options) |
|
||||
| `Cmd+N` | `Ctrl+N` | Configure a new worktree |
|
||||
| `Cmd+Shift+N` | `Ctrl+Shift+N` | Create a new worktree immediately |
|
||||
| `Cmd+Shift+O` | `Ctrl+Shift+O` | Import/open worktree |
|
||||
| `Cmd+Shift+W` | `Ctrl+Shift+W` | Close current worktree |
|
||||
| `Cmd+T` | `Ctrl+T` | New tab (session) in worktree |
|
||||
|
||||
@@ -51,6 +51,7 @@ You can also edit the `indexing` section in `kilo.jsonc` directly:
|
||||
"enabled": true,
|
||||
"provider": "openai",
|
||||
"model": "text-embedding-3-small",
|
||||
"fileExtensions": [".php", ".js", ".css"],
|
||||
"vectorStore": "lancedb",
|
||||
"openai": { "apiKey": "sk-..." },
|
||||
"lancedb": {}
|
||||
@@ -129,6 +130,7 @@ You can also edit the `indexing` section directly. This is the full shape of the
|
||||
"apiKey": "pa-..."
|
||||
},
|
||||
"lancedb": {},
|
||||
"fileExtensions": [".php", ".js", ".css"],
|
||||
"searchMinScore": 0.4,
|
||||
"searchMaxResults": 50,
|
||||
"embeddingBatchSize": 60,
|
||||
@@ -227,6 +229,18 @@ The interface shows real-time status:
|
||||
|
||||
### Automatic File Filtering
|
||||
|
||||
Set `indexing.fileExtensions` to a non-empty array to index only the listed file extensions. Values are case-insensitive and may be written with or without a leading dot. When this setting is omitted, Kilo uses its built-in language list. Configured text formats without a Tree-sitter parser use line-based fallback chunking.
|
||||
|
||||
```json
|
||||
{
|
||||
"indexing": {
|
||||
"fileExtensions": [".php", ".js", ".css"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The configured list replaces the built-in defaults rather than adding to them. Clear the field in the settings UI to restore the defaults, or to inherit the global list from project scope.
|
||||
|
||||
The indexer automatically excludes:
|
||||
|
||||
- Binary files and images
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -54,7 +54,7 @@ type Data = z.infer<typeof dataSchema>
|
||||
function routed(meta: Gateway) {
|
||||
const route = meta.routing
|
||||
if (!route) return
|
||||
const hit = route.modelAttempts?.findLast((item) => item.success === true)
|
||||
const hit = route.modelAttempts?.slice().reverse().find((item) => item.success === true)
|
||||
const id = hit?.canonicalSlug ?? route.canonicalSlug
|
||||
if (!id) return
|
||||
const value = id.trim()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
@@ -19,6 +19,7 @@
|
||||
"./detect": "./src/detect.ts",
|
||||
"./embedding-models": "./src/kilo-embedding-models.ts",
|
||||
"./engine": "./src/indexing/index.ts",
|
||||
"./file-extensions": "./src/file-extensions.ts",
|
||||
"./server": "./src/server/routes.ts",
|
||||
"./status": "./src/status.ts"
|
||||
},
|
||||
|
||||
@@ -3,8 +3,10 @@ import z from "zod"
|
||||
import type { IndexingConfigInput } from "./indexing/config-manager"
|
||||
import { DEFAULT_VECTOR_STORE } from "./indexing/constants"
|
||||
import type { EmbedderProvider } from "./indexing/interfaces/manager"
|
||||
import { FILE_EXTENSION_PATTERN, normalizeFileExtensions } from "./file-extensions"
|
||||
|
||||
export { DEFAULT_VECTOR_STORE } from "./indexing/constants"
|
||||
export { isFileExtension, normalizeFileExtensions, parseFileExtensions } from "./file-extensions"
|
||||
|
||||
const providers = [
|
||||
"kilo",
|
||||
@@ -128,6 +130,11 @@ export const IndexingConfig = z
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum retry attempts for failed embedding batches (default: 3)"),
|
||||
fileExtensions: z
|
||||
.array(z.string().trim().regex(FILE_EXTENSION_PATTERN))
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("File extension allowlist for codebase indexing (uses built-in defaults if omitted)"),
|
||||
})
|
||||
.strict()
|
||||
.meta({ ref: "IndexingConfig" })
|
||||
@@ -229,6 +236,15 @@ export const IndexingSchema = Schema.Struct({
|
||||
scannerMaxBatchRetries: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum retry attempts for failed embedding batches (default: 3)",
|
||||
}),
|
||||
fileExtensions: Schema.optional(
|
||||
Schema.mutable(
|
||||
Schema.Array(Schema.String.check(Schema.isPattern(/^\s*\.?[A-Za-z0-9][A-Za-z0-9_+-]*\s*$/))).check(
|
||||
Schema.isMinLength(1),
|
||||
),
|
||||
),
|
||||
).annotate({
|
||||
description: "File extension allowlist for codebase indexing (uses built-in defaults if omitted)",
|
||||
}),
|
||||
}).annotate({
|
||||
identifier: "IndexingConfig",
|
||||
description: "Codebase indexing configuration",
|
||||
@@ -250,6 +266,7 @@ export function toIndexingConfigInput(cfg: IndexingConfig | undefined): Indexing
|
||||
searchMaxResults: cfg?.searchMaxResults,
|
||||
embeddingBatchSize: cfg?.embeddingBatchSize,
|
||||
scannerMaxBatchRetries: cfg?.scannerMaxBatchRetries,
|
||||
fileExtensions: normalizeFileExtensions(cfg?.fileExtensions),
|
||||
kiloApiKey: cfg?.kilo?.apiKey,
|
||||
kiloBaseUrl: cfg?.kilo?.baseUrl,
|
||||
kiloOrganizationId: cfg?.kilo?.organizationId,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const FILE_EXTENSION_PATTERN = /^\.?[A-Za-z0-9][A-Za-z0-9_+-]*$/
|
||||
|
||||
export function isFileExtension(input: string): boolean {
|
||||
return FILE_EXTENSION_PATTERN.test(input.trim())
|
||||
}
|
||||
|
||||
export function normalizeFileExtensions(input: readonly string[] | undefined): string[] | undefined {
|
||||
if (!input) return undefined
|
||||
const values = new Set<string>()
|
||||
for (const raw of input) {
|
||||
const item = raw.trim().toLowerCase()
|
||||
if (!item) continue
|
||||
values.add(item.startsWith(".") ? item : `.${item}`)
|
||||
}
|
||||
return values.size > 0 ? [...values].sort() : undefined
|
||||
}
|
||||
|
||||
export function parseFileExtensions(input: string): string[] | undefined {
|
||||
const values = input
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
return values.length > 0 ? normalizeFileExtensions(values) : undefined
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/confi
|
||||
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_VECTOR_STORE } from "./constants"
|
||||
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "./model-registry"
|
||||
import { isEmbeddingProfileEqual, resolveEmbeddingProfile } from "./embedding-profile"
|
||||
import { resolveFileExtensions } from "./shared/supported-extensions"
|
||||
|
||||
/**
|
||||
* Raw input fed to CodeIndexConfigManager from the host environment.
|
||||
@@ -22,6 +23,7 @@ export interface IndexingConfigInput {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions?: string[]
|
||||
kiloApiKey?: string
|
||||
kiloBaseUrl?: string
|
||||
kiloOrganizationId?: string
|
||||
@@ -70,6 +72,7 @@ export class CodeIndexConfigManager {
|
||||
private searchMaxResults?: number
|
||||
private embeddingBatchSize?: number
|
||||
private scannerMaxBatchRetries?: number
|
||||
private fileExtensions: string[] = resolveFileExtensions(undefined)
|
||||
|
||||
constructor(input: IndexingConfigInput) {
|
||||
this.applyInput(input)
|
||||
@@ -96,6 +99,7 @@ export class CodeIndexConfigManager {
|
||||
this.searchMaxResults = input.searchMaxResults
|
||||
this.embeddingBatchSize = input.embeddingBatchSize
|
||||
this.scannerMaxBatchRetries = input.scannerMaxBatchRetries
|
||||
this.fileExtensions = resolveFileExtensions(input.fileExtensions)
|
||||
this.modelId = input.modelId
|
||||
|
||||
// Validate and set model dimension
|
||||
@@ -153,6 +157,7 @@ export class CodeIndexConfigManager {
|
||||
voyageApiKey: this.voyageOptions?.apiKey ?? "",
|
||||
qdrantUrl: this.qdrantUrl ?? "",
|
||||
qdrantApiKey: this.qdrantApiKey ?? "",
|
||||
fileExtensions: [...this.fileExtensions],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +235,8 @@ export class CodeIndexConfigManager {
|
||||
if ((prev.qdrantUrl ?? "") !== (this.qdrantUrl ?? "") || (prev.qdrantApiKey ?? "") !== (this.qdrantApiKey ?? ""))
|
||||
return true
|
||||
|
||||
if (prev.fileExtensions.join("\0") !== this.fileExtensions.join("\0")) return true
|
||||
|
||||
if (this.hasEmbeddingProfileChanged(prevProvider, prev.modelId, prev.modelDimension)) return true
|
||||
|
||||
return false
|
||||
@@ -276,6 +283,7 @@ export class CodeIndexConfigManager {
|
||||
searchMaxResults: this.currentSearchMaxResults,
|
||||
embeddingBatchSize: this.currentEmbeddingBatchSize,
|
||||
scannerMaxBatchRetries: this.currentScannerMaxBatchRetries,
|
||||
fileExtensions: [...this.fileExtensions],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface CodeIndexConfig {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions: string[]
|
||||
}
|
||||
|
||||
export type PreviousConfigSnapshot = {
|
||||
@@ -57,4 +58,5 @@ export type PreviousConfigSnapshot = {
|
||||
voyageApiKey?: string
|
||||
qdrantUrl?: string
|
||||
qdrantApiKey?: string
|
||||
fileExtensions: string[]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { scannerExtensions } from "../shared/supported-extensions"
|
||||
import {
|
||||
type IFileWatcher,
|
||||
type ICodeParser,
|
||||
type FileProcessingResult,
|
||||
type IEmbedder,
|
||||
type IVectorStore,
|
||||
@@ -33,6 +34,7 @@ import { Log } from "../../util/log"
|
||||
import type { WorktreeOverlay } from "../worktree-overlay"
|
||||
import { sanitizeErrorMessage } from "../shared/validation-helpers"
|
||||
import type { IgnoreMatcher } from "../shared/load-ignore"
|
||||
import { isBinary } from "../shared/is-binary"
|
||||
|
||||
const log = Log.create({ service: "file-watcher" })
|
||||
|
||||
@@ -56,6 +58,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
private drainTask?: Promise<void>
|
||||
private ready?: Promise<void>
|
||||
private overlay?: WorktreeOverlay
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
public readonly onDidStartBatchProcessing = new Emitter<string[]>()
|
||||
public readonly onBatchProgressUpdate = new Emitter<{
|
||||
@@ -75,12 +78,15 @@ export class FileWatcher implements IFileWatcher {
|
||||
maxBatchRetries?: number,
|
||||
private readonly onTelemetry?: IndexingTelemetryReporter,
|
||||
private readonly telemetryMeta?: IndexingTelemetryMeta,
|
||||
extensions: readonly string[] = scannerExtensions,
|
||||
private readonly parser: ICodeParser = codeParser,
|
||||
) {
|
||||
if (ignoreInstance) {
|
||||
this.ignoreInstance = ignoreInstance
|
||||
}
|
||||
this.batchSegmentThreshold = batchSegmentThreshold ?? BATCH_SEGMENT_THRESHOLD
|
||||
this.maxBatchRetries = maxBatchRetries ?? MAX_BATCH_RETRIES
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
|
||||
private emitRetry(attempt: number, batchSize: number, err: unknown): void {
|
||||
@@ -271,7 +277,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (FileIgnore.match(relativeFilePath)) return false
|
||||
if (this.ignoreInstance?.ignores(relativeFilePath)) return false
|
||||
return scannerExtensions.includes(ext) || !path.extname(filePath)
|
||||
return this.extensions.has(ext)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,6 +663,14 @@ export class FileWatcher implements IFileWatcher {
|
||||
*/
|
||||
async processFile(filePath: string): Promise<FileProcessingResult> {
|
||||
try {
|
||||
if (!this.extensions.has(path.extname(filePath).toLowerCase())) {
|
||||
return {
|
||||
path: filePath,
|
||||
status: "skipped" as const,
|
||||
reason: "File extension is not configured for indexing",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file is in an ignored directory
|
||||
const relativeFilePath = generateRelativeIgnorePath(filePath, this.workspacePath)
|
||||
if (!relativeFilePath) {
|
||||
@@ -695,7 +709,16 @@ export class FileWatcher implements IFileWatcher {
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const bytes = await readFile(filePath)
|
||||
if (isBinary(bytes)) {
|
||||
this.cacheManager.deleteHash(filePath)
|
||||
return {
|
||||
path: filePath,
|
||||
status: "skipped" as const,
|
||||
reason: "File is binary",
|
||||
}
|
||||
}
|
||||
const content = bytes.toString("utf-8")
|
||||
|
||||
// Calculate hash
|
||||
const newHash = createHash("sha256").update(content).digest("hex")
|
||||
@@ -710,7 +733,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
}
|
||||
|
||||
// Parse file
|
||||
const blocks = await codeParser.parseFile(filePath, { content, fileHash: newHash })
|
||||
const blocks = await this.parser.parseFile(filePath, { content, fileHash: newHash })
|
||||
|
||||
// Prepare points for batch processing
|
||||
let pointsToUpsert: PointStruct[] = []
|
||||
|
||||
@@ -20,6 +20,11 @@ export class CodeParser implements ICodeParser {
|
||||
private pendingLoads: Map<string, Promise<LanguageParser>> = new Map()
|
||||
private failedParsers: Set<string> = new Set()
|
||||
private parserFallbackNotified: Set<string> = new Set()
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
constructor(extensions: readonly string[] = scannerExtensions) {
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
// Markdown files are now supported using the custom markdown parser
|
||||
// which extracts headers and sections for semantic indexing
|
||||
|
||||
@@ -93,7 +98,7 @@ export class CodeParser implements ICodeParser {
|
||||
* @returns Boolean indicating if the language is supported
|
||||
*/
|
||||
private isSupportedLanguage(extension: string): boolean {
|
||||
return scannerExtensions.includes(extension)
|
||||
return this.extensions.has(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Log } from "../../util/log"
|
||||
import { sanitizeErrorMessage } from "../shared/validation-helpers"
|
||||
import type { IndexingTelemetryMeta, IndexingTelemetryMode, IndexingTelemetryReporter } from "../interfaces/telemetry"
|
||||
import type { IgnoreMatcher } from "../shared/load-ignore"
|
||||
import { isBinary } from "../shared/is-binary"
|
||||
|
||||
const log = Log.create({ service: "indexing-scanner" })
|
||||
|
||||
@@ -35,6 +36,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
private _cancelled = false
|
||||
private batchSegmentThreshold: number
|
||||
private maxBatchRetries: number
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
constructor(
|
||||
private readonly embedder: IEmbedder,
|
||||
@@ -46,9 +48,11 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
maxBatchRetries?: number,
|
||||
private readonly onTelemetry?: IndexingTelemetryReporter,
|
||||
private readonly telemetryMeta?: IndexingTelemetryMeta,
|
||||
extensions: readonly string[] = scannerExtensions,
|
||||
) {
|
||||
this.batchSegmentThreshold = batchSegmentThreshold ?? BATCH_SEGMENT_THRESHOLD
|
||||
this.maxBatchRetries = maxBatchRetries ?? MAX_BATCH_RETRIES
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
|
||||
private emitFileCount(mode: IndexingTelemetryMode, discovered: number, candidate: number): void {
|
||||
@@ -166,7 +170,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
return false
|
||||
}
|
||||
|
||||
return scannerExtensions.includes(ext) && !this.ignoreInstance.ignores(relativeFilePath)
|
||||
return this.extensions.has(ext) && !this.ignoreInstance.ignores(relativeFilePath)
|
||||
})
|
||||
log.info("discovered candidate files for indexing", {
|
||||
workspacePath: scanWorkspace,
|
||||
@@ -265,7 +269,12 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
}
|
||||
|
||||
// Read file content using fs/promises
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const bytes = await readFile(filePath)
|
||||
if (isBinary(bytes)) {
|
||||
skippedCount++
|
||||
return
|
||||
}
|
||||
const content = bytes.toString("utf-8")
|
||||
|
||||
if (this._cancelled) {
|
||||
return
|
||||
|
||||
@@ -23,6 +23,12 @@ export class CodeIndexSearchService {
|
||||
private readonly baseline?: BaselineSearch,
|
||||
) {}
|
||||
|
||||
private allowed(result: VectorStoreSearchResult, extensions: ReadonlySet<string>): boolean {
|
||||
const file = result.payload?.filePath
|
||||
if (typeof file !== "string") return false
|
||||
return extensions.has(path.extname(file).toLowerCase())
|
||||
}
|
||||
|
||||
public async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
|
||||
if (!this.configManager.isFeatureEnabled || !this.configManager.isFeatureConfigured) {
|
||||
throw new Error("Code index feature is disabled or not configured.")
|
||||
@@ -44,7 +50,11 @@ export class CodeIndexSearchService {
|
||||
}
|
||||
|
||||
const normalizedPrefix = directoryPrefix ? path.normalize(directoryPrefix) : undefined
|
||||
if (!this.baseline) return await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults)
|
||||
const extensions = new Set(this.configManager.getConfig().fileExtensions)
|
||||
if (!this.baseline) {
|
||||
const results = await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults)
|
||||
return results.filter((result) => this.allowed(result, extensions))
|
||||
}
|
||||
if (!this.baseline.overlay.ready) throw new Error("Worktree index reconciliation is not complete.")
|
||||
|
||||
const ceiling = Math.max(maxResults, Math.min(maxResults * 16, 1000))
|
||||
@@ -77,9 +87,12 @@ export class CodeIndexSearchService {
|
||||
"\0",
|
||||
)
|
||||
|
||||
for (const result of baseline) merged.set(key(result), result)
|
||||
for (const result of baseline) {
|
||||
if (this.allowed(result, extensions)) merged.set(key(result), result)
|
||||
}
|
||||
for (const result of current) {
|
||||
if (this.baseline.overlay.deltaResult(result)) merged.set(key(result), result)
|
||||
if (this.allowed(result, extensions) && this.baseline.overlay.deltaResult(result))
|
||||
merged.set(key(result), result)
|
||||
}
|
||||
return [...merged.values()].sort((left, right) => right.score - left.score).slice(0, maxResults)
|
||||
} catch (err) {
|
||||
|
||||
@@ -15,8 +15,8 @@ import { OpenRouterEmbedder } from "./embedders/openrouter"
|
||||
import { VoyageEmbedder } from "./embedders/voyage"
|
||||
import { QdrantVectorStore } from "./vector-store/qdrant-client"
|
||||
import { LanceDBVectorStore } from "./vector-store/lancedb-vector-store"
|
||||
import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
|
||||
import type { ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
|
||||
import { CodeParser, DirectoryScanner, FileWatcher } from "./processors"
|
||||
import type { AvailableEmbedders, ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
|
||||
import type { CodeIndexConfigManager } from "./config-manager"
|
||||
import type { CacheManager } from "./cache-manager"
|
||||
import type { IndexingTelemetryMeta, IndexingTelemetryReporter } from "./interfaces/telemetry"
|
||||
@@ -31,10 +31,28 @@ import type { IgnoreMatcher } from "./shared/load-ignore"
|
||||
|
||||
const log = Log.create({ service: "indexing-factory" })
|
||||
|
||||
function timeout(provider: string): number {
|
||||
if (provider === "ollama") return OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS
|
||||
return REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS
|
||||
}
|
||||
// RATIONALE: The OpenAI SDK applies the per-attempt timeout and retries internally.
|
||||
const policy = {
|
||||
openai: undefined,
|
||||
openrouter: undefined,
|
||||
"openai-compatible": undefined,
|
||||
kilo: undefined,
|
||||
gemini: undefined,
|
||||
mistral: undefined,
|
||||
"vercel-ai-gateway": undefined,
|
||||
ollama: {
|
||||
timeout: OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
|
||||
error: "Connection to embedding service failed (timeout)",
|
||||
},
|
||||
voyage: {
|
||||
timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
|
||||
error: "Connection failed. Please check the endpoint URL and network connectivity.",
|
||||
},
|
||||
bedrock: {
|
||||
timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
|
||||
error: "Connection failed. Please check the endpoint URL and network connectivity.",
|
||||
},
|
||||
} satisfies Record<AvailableEmbedders, { timeout: number; error: string } | undefined>
|
||||
|
||||
/**
|
||||
* Factory class responsible for creating and configuring code indexing service dependencies.
|
||||
@@ -128,26 +146,26 @@ export class CodeIndexServiceFactory {
|
||||
}
|
||||
|
||||
public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> {
|
||||
const ms = timeout(embedder.embedderInfo.name)
|
||||
const deadline = policy[embedder.embedderInfo.name]
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
const wait = embedder.validateConfiguration()
|
||||
const fail = new Promise<{ valid: boolean; error?: string }>((resolve) => {
|
||||
timer = setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
valid: false,
|
||||
error:
|
||||
embedder.embedderInfo.name === "ollama"
|
||||
? "Connection to embedding service failed (timeout)"
|
||||
: "Connection failed. Please check the endpoint URL and network connectivity.",
|
||||
}),
|
||||
ms,
|
||||
)
|
||||
})
|
||||
const fail =
|
||||
deadline === undefined
|
||||
? undefined
|
||||
: new Promise<{ valid: boolean; error?: string }>((resolve) => {
|
||||
timer = setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
valid: false,
|
||||
error: deadline.error,
|
||||
}),
|
||||
deadline.timeout,
|
||||
)
|
||||
})
|
||||
|
||||
try {
|
||||
log.info("validating embedder", { provider: embedder.embedderInfo.name })
|
||||
const result = await Promise.race([wait, fail])
|
||||
const result = fail ? await Promise.race([wait, fail]) : await wait
|
||||
if (result.valid) {
|
||||
log.info("embedder validation succeeded", { provider: embedder.embedderInfo.name })
|
||||
}
|
||||
@@ -222,6 +240,7 @@ export class CodeIndexServiceFactory {
|
||||
config.scannerMaxBatchRetries,
|
||||
this.onTelemetry,
|
||||
meta,
|
||||
config.fileExtensions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,6 +249,7 @@ export class CodeIndexServiceFactory {
|
||||
vectorStore: IVectorStore,
|
||||
cacheManager: CacheManager,
|
||||
ignoreInstance: IgnoreMatcher,
|
||||
parser: ICodeParser,
|
||||
): IFileWatcher {
|
||||
const config = this.configManager.getConfig()
|
||||
const meta = this.getTelemetryMeta()
|
||||
@@ -243,6 +263,8 @@ export class CodeIndexServiceFactory {
|
||||
config.scannerMaxBatchRetries,
|
||||
this.onTelemetry,
|
||||
meta,
|
||||
config.fileExtensions,
|
||||
parser,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -271,9 +293,9 @@ export class CodeIndexServiceFactory {
|
||||
|
||||
const embedder = this.createEmbedder()
|
||||
const vectorStore = this.createVectorStore()
|
||||
const parser = codeParser
|
||||
const parser = new CodeParser(config.fileExtensions)
|
||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(embedder, vectorStore, cacheManager, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(embedder, vectorStore, cacheManager, ignoreInstance, parser)
|
||||
|
||||
log.info("indexing services created", {
|
||||
workspacePath: this.workspacePath,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const SAMPLE_BYTES = 4096
|
||||
|
||||
export function isBinary(input: Uint8Array): boolean {
|
||||
const length = Math.min(input.length, SAMPLE_BYTES)
|
||||
if (length === 0) return false
|
||||
|
||||
let control = 0
|
||||
for (let index = 0; index < length; index++) {
|
||||
const byte = input[index]
|
||||
if (byte === 0) return true
|
||||
if (byte < 9 || (byte > 13 && byte < 32)) control++
|
||||
}
|
||||
return control / length > 0.3
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { extensions as allExtensions } from "../../tree-sitter"
|
||||
import { normalizeFileExtensions } from "../../file-extensions"
|
||||
|
||||
// Include all extensions including markdown for the scanner
|
||||
export const scannerExtensions = allExtensions
|
||||
|
||||
export function resolveFileExtensions(input: readonly string[] | undefined): string[] {
|
||||
return normalizeFileExtensions(input) ?? [...scannerExtensions]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extensions that should always use fallback chunking instead of tree-sitter parsing.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { toIndexingConfigInput } from "../../../src/config"
|
||||
import {
|
||||
IndexingConfig,
|
||||
normalizeFileExtensions,
|
||||
parseFileExtensions,
|
||||
toIndexingConfigInput,
|
||||
} from "../../../src/config"
|
||||
import { CodeIndexConfigManager, type IndexingConfigInput } from "../../../src/indexing/config-manager"
|
||||
|
||||
function createInput(input: Partial<IndexingConfigInput> = {}): IndexingConfigInput {
|
||||
@@ -72,6 +77,25 @@ describe("CodeIndexConfigManager", () => {
|
||||
expect(cfg.getConfig().vectorStoreProvider).toBe("qdrant")
|
||||
})
|
||||
|
||||
test("normalizes configured file extensions", () => {
|
||||
expect(normalizeFileExtensions([" PHP ", ".JS", "js", "css"])).toEqual([".css", ".js", ".php"])
|
||||
expect(parseFileExtensions(" PHP, .JS, js, css ")).toEqual([".css", ".js", ".php"])
|
||||
expect(parseFileExtensions(" ")).toBeUndefined()
|
||||
expect(toIndexingConfigInput({ fileExtensions: ["PHP", ".JS"] }).fileExtensions).toEqual([".js", ".php"])
|
||||
expect(normalizeFileExtensions(["", " "])).toBeUndefined()
|
||||
expect(
|
||||
normalizeFileExtensions(Array.from({ length: 10_000 }, (_, index) => (index % 2 ? " PHP " : ".JS"))),
|
||||
).toEqual([".js", ".php"])
|
||||
})
|
||||
|
||||
test("validates file extension tokens", () => {
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["php", " .JS "] }).success).toBe(true)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: [] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["*.js"] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["src/php"] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: [".d.ts"] }).success).toBe(false)
|
||||
})
|
||||
|
||||
test("configures Kilo with hosted auth options and explicit model metadata", () => {
|
||||
const cfg = new CodeIndexConfigManager(
|
||||
createInput({
|
||||
@@ -206,5 +230,13 @@ describe("CodeIndexConfigManager", () => {
|
||||
|
||||
expect(result.requiresRestart).toBe(true)
|
||||
})
|
||||
|
||||
test("restarts only when the normalized file extension allowlist changes", () => {
|
||||
const cfg = new CodeIndexConfigManager(createInput({ fileExtensions: ["php", ".JS"] }))
|
||||
|
||||
expect(cfg.getConfig().fileExtensions).toEqual([".js", ".php"])
|
||||
expect(cfg.loadConfiguration(createInput({ fileExtensions: [".js", ".PHP", "php"] })).requiresRestart).toBe(false)
|
||||
expect(cfg.loadConfiguration(createInput({ fileExtensions: [".css"] })).requiresRestart).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
VectorStoreSearchResult,
|
||||
} from "../../../../src/indexing/interfaces"
|
||||
import { FileWatcher } from "../../../../src/indexing/processors/file-watcher"
|
||||
import { CodeParser } from "../../../../src/indexing/processors/parser"
|
||||
import { loadIgnore } from "../../../../src/indexing/shared/load-ignore"
|
||||
import { WorktreeOverlay } from "../../../../src/indexing/worktree-overlay"
|
||||
|
||||
@@ -318,6 +319,46 @@ describe("FileWatcher", () => {
|
||||
expect(result.reason).toBe("File is ignored by .gitignore or .kilocodeignore")
|
||||
})
|
||||
|
||||
test("processFile uses the configured extension allowlist", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-"))
|
||||
const cacheDir = path.join(root, ".cache")
|
||||
const custom = path.join(root, "source.custom")
|
||||
const excluded = path.join(root, "source.ts")
|
||||
const content = "custom source content ".repeat(20)
|
||||
await mkdir(cacheDir, { recursive: true })
|
||||
await writeFile(custom, content)
|
||||
await writeFile(excluded, content)
|
||||
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
const watcher = new FileWatcher(
|
||||
root,
|
||||
cache,
|
||||
createEmbedder(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
new CodeParser([".custom"]),
|
||||
)
|
||||
|
||||
const first = await watcher.processFile(custom)
|
||||
expect(first.status).toBe("processed_for_batching")
|
||||
if (first.status === "processed_for_batching" && first.newHash) cache.updateHash(custom, first.newHash)
|
||||
expect(await watcher.processFile(excluded)).toMatchObject({
|
||||
status: "skipped",
|
||||
reason: "File extension is not configured for indexing",
|
||||
})
|
||||
await writeFile(custom, new Uint8Array([0, 1, 2, 3]))
|
||||
expect(await watcher.processFile(custom)).toMatchObject({ status: "skipped", reason: "File is binary" })
|
||||
expect(cache.getHash(custom)).toBeUndefined()
|
||||
await writeFile(custom, content)
|
||||
expect((await watcher.processFile(custom)).status).toBe("processed_for_batching")
|
||||
})
|
||||
|
||||
test("processFile skips files matched by nested .gitignore during incremental updates", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-"))
|
||||
try {
|
||||
|
||||
@@ -71,6 +71,21 @@ describe("CodeParser", () => {
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test("uses fallback chunking for configured custom extensions", async () => {
|
||||
const custom = new CodeParser([".custom"])
|
||||
const result = await custom.parseFile("test.custom", { content: "custom source line ".repeat(20) })
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result[0]?.type).toBe("fallback_chunk")
|
||||
})
|
||||
|
||||
test("excludes built-in extensions outside a configured allowlist", async () => {
|
||||
const custom = new CodeParser([".php"])
|
||||
const result = await custom.parseFile("test.js", { content: "const value = 1;".repeat(20) })
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test("should use provided content instead of reading file when options.content is provided", async () => {
|
||||
const content = `/* This is a long test content string that exceeds 100 characters to properly test the parser's behavior with large inputs.
|
||||
It includes multiple lines and various JavaScript constructs to simulate real-world code.
|
||||
|
||||
@@ -309,6 +309,63 @@ describe("DirectoryScanner", () => {
|
||||
expect(cache.getHash(open)).toBeDefined()
|
||||
})
|
||||
|
||||
test("uses a configured extension allowlist and removes excluded cached files", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
const custom = join(root, "main.custom")
|
||||
const excluded = join(root, "main.ts")
|
||||
await Bun.write(custom, "custom source content\n")
|
||||
await Bun.write(excluded, "export const excluded = 1\n")
|
||||
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
cache.updateHash(excluded, "old-hash")
|
||||
const scan = new DirectoryScanner(
|
||||
new Emb(),
|
||||
new Store(),
|
||||
new Parser(),
|
||||
cache,
|
||||
ignore(),
|
||||
1,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
)
|
||||
|
||||
const result = await scan.scanDirectory(root)
|
||||
|
||||
expect(result.stats.processed).toBe(1)
|
||||
expect(cache.getHash(custom)).toBeDefined()
|
||||
expect(cache.getHash(excluded)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips binary files admitted by a custom extension", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
const file = join(root, "data.custom")
|
||||
await Bun.write(file, new Uint8Array([0, 1, 2, 3]))
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
const scan = new DirectoryScanner(
|
||||
new Emb(),
|
||||
new Store(),
|
||||
new Parser(),
|
||||
cache,
|
||||
ignore(),
|
||||
1,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
)
|
||||
|
||||
const result = await scan.scanDirectory(root)
|
||||
|
||||
expect(result.stats).toEqual({ processed: 0, skipped: 1 })
|
||||
expect(cache.getHash(file)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips files matched by nested .kilocodeignore during full scans", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
|
||||
@@ -36,16 +36,34 @@ const embedder = (calls: string[][]): IEmbedder => ({
|
||||
},
|
||||
})
|
||||
|
||||
const config = () =>
|
||||
const config = (fileExtensions?: string[]) =>
|
||||
new CodeIndexConfigManager({
|
||||
enabled: true,
|
||||
embedderProvider: "openai",
|
||||
openAiKey: "test",
|
||||
vectorStoreProvider: "lancedb",
|
||||
searchMaxResults: 2,
|
||||
fileExtensions,
|
||||
})
|
||||
|
||||
describe("CodeIndexSearchService worktree search", () => {
|
||||
test("filters stale results using one vector query", async () => {
|
||||
const limits: number[] = []
|
||||
const state = new CodeIndexStateManager()
|
||||
state.setSystemState("Indexed")
|
||||
const service = new CodeIndexSearchService(
|
||||
config([".php"]),
|
||||
state,
|
||||
embedder([]),
|
||||
store([result("src/old.ts", 0.99), result("src/first.php", 0.9), result("src/second.php", 0.8)], limits),
|
||||
)
|
||||
|
||||
const results = await service.searchIndex("query")
|
||||
|
||||
expect(limits).toEqual([2])
|
||||
expect(results.map((item) => item.payload?.filePath)).toEqual(["src/first.php"])
|
||||
})
|
||||
|
||||
test("embeds once, hides baseline paths, and merges the current delta", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "search-worktree-"))
|
||||
const main = await mkdtemp(path.join(tmpdir(), "search-main-"))
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, test, mock, beforeEach } from "bun:test"
|
||||
import path from "path"
|
||||
import { mockEmbeddingsCreate, openAIMockFactory, setOpenAIConstructorHook } from "./embedders/__helpers__/openai-mock"
|
||||
import {
|
||||
OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
|
||||
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
|
||||
} from "../../../src/indexing/constants"
|
||||
import type { AvailableEmbedders, IEmbedder } from "../../../src/indexing/interfaces/embedder"
|
||||
|
||||
mock.module("openai", openAIMockFactory)
|
||||
import { CodeIndexServiceFactory } from "../../../src/indexing/service-factory"
|
||||
@@ -23,6 +28,20 @@ function createFactory(input?: Partial<ConstructorParameters<typeof CodeIndexCon
|
||||
return new CodeIndexServiceFactory(cfg, workspacePath, cache, cacheDirectory)
|
||||
}
|
||||
|
||||
function createEmbedder(name: AvailableEmbedders): IEmbedder {
|
||||
return {
|
||||
async createEmbeddings() {
|
||||
return { embeddings: [] }
|
||||
},
|
||||
async validateConfiguration() {
|
||||
return { valid: true }
|
||||
},
|
||||
get embedderInfo() {
|
||||
return { name }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("CodeIndexServiceFactory", () => {
|
||||
beforeEach(() => {
|
||||
mockEmbeddingsCreate.mockReset()
|
||||
@@ -39,6 +58,55 @@ describe("CodeIndexServiceFactory", () => {
|
||||
expect(factory.createEmbedder().embedderInfo).toEqual({ name: "openai-compatible" })
|
||||
})
|
||||
|
||||
test("lets SDK-backed embedders own validation timeouts", async () => {
|
||||
const original = globalThis.setTimeout
|
||||
const timer = mock((...args: Parameters<typeof setTimeout>) => original(...args))
|
||||
globalThis.setTimeout = timer
|
||||
|
||||
try {
|
||||
const factory = createFactory()
|
||||
const providers = [
|
||||
"openai",
|
||||
"openrouter",
|
||||
"openai-compatible",
|
||||
"kilo",
|
||||
"gemini",
|
||||
"mistral",
|
||||
"vercel-ai-gateway",
|
||||
] satisfies AvailableEmbedders[]
|
||||
|
||||
for (const provider of providers) {
|
||||
await expect(factory.validateEmbedder(createEmbedder(provider))).resolves.toEqual({ valid: true })
|
||||
}
|
||||
|
||||
expect(timer).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
globalThis.setTimeout = original
|
||||
}
|
||||
})
|
||||
|
||||
test("retains factory deadlines for non-SDK embedders", async () => {
|
||||
const original = globalThis.setTimeout
|
||||
const timer = mock((...args: Parameters<typeof setTimeout>) => original(...args))
|
||||
globalThis.setTimeout = timer
|
||||
|
||||
try {
|
||||
const factory = createFactory()
|
||||
|
||||
await factory.validateEmbedder(createEmbedder("voyage"))
|
||||
await factory.validateEmbedder(createEmbedder("bedrock"))
|
||||
await factory.validateEmbedder(createEmbedder("ollama"))
|
||||
|
||||
expect(timer.mock.calls.map((call) => call[1])).toEqual([
|
||||
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
|
||||
REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
|
||||
OLLAMA_EMBEDDER_REQUEST_TIMEOUT_MS,
|
||||
])
|
||||
} finally {
|
||||
globalThis.setTimeout = original
|
||||
}
|
||||
})
|
||||
|
||||
test("uses default LanceDB directory when config is unset", () => {
|
||||
const factory = createFactory({ vectorStoreProvider: "lancedb", lancedbVectorStoreDirectory: undefined })
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 7.4.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`d6b36a0`](https://github.com/Kilo-Org/kilocode/commit/d6b36a028cc0a4b7bfd158d75e287c110e2838f7) - Support editing custom OpenAI-compatible providers from JetBrains settings and replace their Disconnect action with Edit and Delete. Added or edited providers stay selected, and the custom provider dialog now closes after a successful save.
|
||||
|
||||
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`6077c1c`](https://github.com/Kilo-Org/kilocode/commit/6077c1c3b36d4c5cd68f206fc146ca472d841c5e) - Fix adding a Custom OpenAI-Compatible Provider silently failing. The dialog now requires at least one model and reports save errors inline so you can correct your input and retry without re-entering the form.
|
||||
|
||||
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`cae3270`](https://github.com/Kilo-Org/kilocode/commit/cae3270c9dacc4097a681539cf3e07cfadceaca7) - Match the model picker Close button styling to JetBrains dialog primary buttons.
|
||||
|
||||
- [#12217](https://github.com/Kilo-Org/kilocode/pull/12217) [`cae3270`](https://github.com/Kilo-Org/kilocode/commit/cae3270c9dacc4097a681539cf3e07cfadceaca7) - Use a trash icon for provider delete and show provider edit/delete actions on selection, matching the other settings lists.
|
||||
|
||||
## 7.4.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"test": "./gradlew test",
|
||||
"test:ci": "bun script/test-ci.ts"
|
||||
},
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Project memory storage, indexing, recall, and command helpers for Kilo Code",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -392,6 +392,9 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
}
|
||||
|
||||
[data-slot="user-message-text"] {
|
||||
unicode-bidi: plaintext; /* Match the prompt input */
|
||||
background-color: color-mix(in srgb, var(--text-base) 7%, transparent);
|
||||
border: 1px solid transparent;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&[data-queued] {
|
||||
@@ -408,6 +411,11 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] {
|
||||
}
|
||||
}
|
||||
|
||||
body.vscode-high-contrast [data-component="user-message"] [data-slot="user-message-text"],
|
||||
body.vscode-high-contrast-light [data-component="user-message"] [data-slot="user-message-text"] {
|
||||
border-color: var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-component="tool-output"] {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
@@ -857,7 +857,7 @@ export function UserMessageDisplay(props: {
|
||||
<div data-slot="user-message-body">
|
||||
{props.header}
|
||||
<Show when={text()}>
|
||||
<div data-slot="user-message-text" data-queued={props.queued ? "" : undefined}>
|
||||
<div data-slot="user-message-text" dir="auto" data-queued={props.queued ? "" : undefined}>
|
||||
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# kilo-code
|
||||
|
||||
## 7.4.11
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#12254](https://github.com/Kilo-Org/kilocode/pull/12254) [`30e7ec4`](https://github.com/Kilo-Org/kilocode/commit/30e7ec4ab45fac724b41ec0b4342e272e7f584d2) - Improve project-memory activity in VS Code with a task-header menu, quick actions, and optional verbose details.
|
||||
|
||||
- [#12250](https://github.com/Kilo-Org/kilocode/pull/12250) [`bd69158`](https://github.com/Kilo-Org/kilocode/commit/bd69158131aafdcc2f44aede22b573c2b0432f21) - Support verbose project-memory settings and show recalled memory snippets in conversation markers when enabled.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#12287](https://github.com/Kilo-Org/kilocode/pull/12287) [`bb75928`](https://github.com/Kilo-Org/kilocode/commit/bb759287495fabc1f3fa7e0fbecff13ab4d41cb2) - Use distinct icons for context, project memory, and code indexing, and group context controls to preserve task titles in narrow VS Code sidebars.
|
||||
|
||||
- [#12285](https://github.com/Kilo-Org/kilocode/pull/12285) [`9fa2e18`](https://github.com/Kilo-Org/kilocode/commit/9fa2e187047a4d2b6e76066af3c139c21851e854) - Prevent sessions from freezing after submitting or dismissing a question.
|
||||
|
||||
- [#12274](https://github.com/Kilo-Org/kilocode/pull/12274) [`5180c10`](https://github.com/Kilo-Org/kilocode/commit/5180c10c4f69500ce303437646371500a71dba46) - Show newly submitted messages immediately after reverting a conversation.
|
||||
|
||||
- Updated dependencies [[`319f159`](https://github.com/Kilo-Org/kilocode/commit/319f159ac333d18855a72ddb1fa61ed471ebf2d9), [`bb75928`](https://github.com/Kilo-Org/kilocode/commit/bb759287495fabc1f3fa7e0fbecff13ab4d41cb2), [`9fa8a13`](https://github.com/Kilo-Org/kilocode/commit/9fa8a13e26db552373149e1275597eb07861de91), [`30e7ec4`](https://github.com/Kilo-Org/kilocode/commit/30e7ec4ab45fac724b41ec0b4342e272e7f584d2), [`bd69158`](https://github.com/Kilo-Org/kilocode/commit/bd69158131aafdcc2f44aede22b573c2b0432f21)]:
|
||||
- @kilocode/kilo-gateway@7.4.10
|
||||
- @kilocode/kilo-ui@7.4.10
|
||||
- @kilocode/kilo-memory@7.5.0
|
||||
- @kilocode/sdk@7.5.0
|
||||
- @opencode-ai/core@7.4.10
|
||||
- @kilocode/kilo-indexing@7.4.10
|
||||
- @kilocode/plugin@7.4.10
|
||||
- @opencode-ai/ui@7.4.10
|
||||
|
||||
## 7.4.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
@@ -291,6 +291,11 @@
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newWorktree",
|
||||
"title": "Agent Manager: Configure New Worktree",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.quickWorktree",
|
||||
"title": "Agent Manager: New Worktree",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
@@ -311,7 +316,7 @@
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.advancedWorktree",
|
||||
"title": "Agent Manager: Advanced New Worktree",
|
||||
"title": "Agent Manager: Configure New Worktree",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
@@ -472,6 +477,10 @@
|
||||
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.advancedWorktree",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
|
||||
"when": "false"
|
||||
@@ -713,6 +722,12 @@
|
||||
"mac": "cmd+n",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.quickWorktree",
|
||||
"key": "ctrl+shift+n",
|
||||
"mac": "cmd+shift+n",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.openWorktree",
|
||||
"key": "ctrl+shift+o",
|
||||
@@ -731,12 +746,6 @@
|
||||
"mac": "cmd+shift+w",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.advancedWorktree",
|
||||
"key": "ctrl+shift+n",
|
||||
"mac": "cmd+shift+n",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.jumpTo1",
|
||||
"key": "ctrl+1",
|
||||
@@ -1159,8 +1168,7 @@
|
||||
"pretest": "bun run compile-tests && bun run compile && bun run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
"check-types:webview": "bun script/typecheck.ts --project webview-ui/tsconfig.json",
|
||||
"typecheck": "bun run check-types:extension && bun run check-types:webview",
|
||||
"check-types:extension": "bun script/typecheck.ts",
|
||||
"typecheck": "bun run check-types && bun run check-types:webview",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"knip": "knip",
|
||||
|
||||
@@ -139,6 +139,7 @@ import {
|
||||
import { fetchAndSendPendingSuggestions } from "./kilo-provider/handlers/suggestion"
|
||||
import { nativeTitle } from "./kilo-provider/native-tab-title"
|
||||
import { parseReview, reviewMetadata, type ReviewMessageData } from "./shared/review-comments"
|
||||
import { completesWithoutStatus } from "./kilo-provider/command-completion"
|
||||
import { KiloProviderMemory } from "./kilo-provider/memory"
|
||||
|
||||
import {
|
||||
@@ -3413,6 +3414,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
messageID,
|
||||
),
|
||||
)
|
||||
if (messageID && completesWithoutStatus(command)) {
|
||||
this.postMessage({ type: "sessionCommandCompleted", messageID })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to send command:", error)
|
||||
this.postMessage({
|
||||
|
||||
@@ -194,6 +194,11 @@ export class AgentManagerProvider implements Disposable {
|
||||
},
|
||||
stats: (refresh) => this.statsPoller.snapshot(refresh),
|
||||
prs: () => this.prBridge.snapshot(),
|
||||
managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id),
|
||||
close: async (id) => {
|
||||
await this.onCloseSession(id)
|
||||
this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id })
|
||||
},
|
||||
log: (...args) => this.log(...args),
|
||||
})
|
||||
this.unsubTool = this.connectionService.onEventFiltered(
|
||||
|
||||
@@ -25,10 +25,12 @@ interface RequestBase {
|
||||
type Request =
|
||||
| (RequestBase & { operation: "overview"; filter?: OverviewFilter })
|
||||
| (RequestBase & { operation: "prompt"; targetSessionID: string; prompt: string })
|
||||
| (RequestBase & { operation: "stop"; targetSessionID: string })
|
||||
|
||||
type Result =
|
||||
| { operation: "overview"; overview: Overview }
|
||||
| { operation: "prompt"; sessionID: string; delivered: true }
|
||||
| { operation: "stop"; sessionID: string; stopped: true }
|
||||
|
||||
interface Failure {
|
||||
code: FailureCode | "cancelled" | "disconnected" | "timeout"
|
||||
@@ -41,6 +43,8 @@ interface Options {
|
||||
state(): WorktreeStateManager | undefined
|
||||
stats(refresh?: boolean): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
|
||||
prs(): Map<string, PRStatus>
|
||||
managed(sessionID: string): boolean
|
||||
close(sessionID: string): Promise<void>
|
||||
log(...args: unknown[]): void
|
||||
}
|
||||
|
||||
@@ -262,17 +266,25 @@ export class AgentManagerOrchestrationBridge {
|
||||
})
|
||||
return { result: { operation: "overview", overview: result } }
|
||||
}
|
||||
await prompt({
|
||||
client,
|
||||
root,
|
||||
state,
|
||||
sessionID: request.targetSessionID,
|
||||
text: request.prompt,
|
||||
messageID: request.id,
|
||||
signal: active.controller.signal,
|
||||
})
|
||||
if (request.operation === "prompt") {
|
||||
await prompt({
|
||||
client,
|
||||
root,
|
||||
state,
|
||||
sessionID: request.targetSessionID,
|
||||
text: request.prompt,
|
||||
messageID: request.id,
|
||||
signal: active.controller.signal,
|
||||
})
|
||||
if (this.disposed || active.cancelled) return
|
||||
return { result: { operation: "prompt", sessionID: request.targetSessionID, delivered: true } }
|
||||
}
|
||||
if (!this.options.managed(request.targetSessionID)) {
|
||||
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
|
||||
}
|
||||
await this.options.close(request.targetSessionID)
|
||||
if (this.disposed || active.cancelled) return
|
||||
return { result: { operation: "prompt", sessionID: request.targetSessionID, delivered: true } }
|
||||
return { result: { operation: "stop", sessionID: request.targetSessionID, stopped: true } }
|
||||
} catch (error) {
|
||||
if (this.disposed || active.cancelled) return
|
||||
return { error: failure(error) }
|
||||
|
||||
@@ -190,6 +190,11 @@ interface SessionForkedMessage {
|
||||
worktreeId?: string
|
||||
}
|
||||
|
||||
interface SessionClosedMessage {
|
||||
type: "agentManager.sessionClosed"
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
interface MultiVersionProgressMessage {
|
||||
type: "agentManager.multiVersionProgress"
|
||||
status: "creating" | "done"
|
||||
@@ -307,6 +312,7 @@ export type AgentManagerOutMessage =
|
||||
| ErrorOutMessage
|
||||
| SessionAddedMessage
|
||||
| SessionForkedMessage
|
||||
| SessionClosedMessage
|
||||
| MultiVersionProgressMessage
|
||||
| SetSessionModelMessage
|
||||
| SendInitialMessage
|
||||
|
||||
@@ -498,6 +498,9 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.newWorktree", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "newWorktree" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.quickWorktree", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "quickWorktree" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.openWorktree", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "openWorktree" })
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function completesWithoutStatus(command: string): boolean {
|
||||
return command === "local-review" || command === "local-review-uncommitted"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type { CloudSessionData, EditorContext } from "../../services/cli-backend
|
||||
import { getErrorMessage, sessionToWebview, mapCloudSessionMessageToWebviewMessage } from "../../kilo-provider-utils"
|
||||
import type { MessageFile } from "../message-files"
|
||||
import { reviewMetadata, type ReviewMessageData } from "../../shared/review-comments"
|
||||
import { completesWithoutStatus } from "../command-completion"
|
||||
|
||||
const TIMEOUT = 30_000
|
||||
|
||||
@@ -232,6 +233,9 @@ export async function handleImportAndSend(
|
||||
{ throwOnError: true },
|
||||
)
|
||||
})
|
||||
if (messageID && command && completesWithoutStatus(command)) {
|
||||
ctx.postMessage({ type: "sessionCommandCompleted", messageID })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Kilo New] Failed to send message after cloud import:", err)
|
||||
ctx.postMessage({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "7.4.9",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -20,6 +20,7 @@ const CSS_FILES = [
|
||||
]
|
||||
const TSX_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/NewWorktreeDialog.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"),
|
||||
@@ -269,6 +270,89 @@ describe("Agent Manager Model Picker", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Agent Manager Worktree Actions", () => {
|
||||
it("opens the configuration dialog from the primary plus action", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"), "utf-8")
|
||||
const start = source.indexOf('<div class="am-split-button">')
|
||||
const end = source.indexOf("</div>", start)
|
||||
const actions = source.slice(start, end)
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
expect(actions).toContain("onClick={props.onNew}")
|
||||
expect(actions).toContain('keybind={props.bindings.newWorktree ?? ""}')
|
||||
expect(actions).toContain("<DropdownMenu.Item onSelect={props.onCreate}>")
|
||||
expect(actions).toContain('props.t("sidebar.session.newWorktree.from")')
|
||||
expect(actions).toContain('props.bindings.quickWorktree ?? ""')
|
||||
expect(actions).not.toContain("onSelect={props.onNew}")
|
||||
})
|
||||
|
||||
it("opens the configuration dialog from the new-worktree shortcut", () => {
|
||||
const source = fs.readFileSync(TSX_FILE, "utf-8")
|
||||
const start = source.indexOf('else if (msg.action === "newWorktree")')
|
||||
const end = source.indexOf('else if (msg.action === "quickWorktree")', start)
|
||||
const action = source.slice(start, end)
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
expect(action).toContain("showNewWorktreeDialog()")
|
||||
})
|
||||
|
||||
it("creates a worktree from the quick-worktree shortcut", () => {
|
||||
const source = fs.readFileSync(TSX_FILE, "utf-8")
|
||||
const start = source.indexOf('else if (msg.action === "quickWorktree")')
|
||||
const end = source.indexOf('else if (msg.action === "openWorktree")', start)
|
||||
const action = source.slice(start, end)
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
expect(action).toContain("handleCreateWorktree()")
|
||||
})
|
||||
|
||||
it("registers distinct dialog and quick-create worktree shortcuts", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8")) as {
|
||||
contributes: { keybindings: { command: string; key?: string; mac?: string }[] }
|
||||
}
|
||||
const source = fs.readFileSync(TSX_FILE, "utf-8")
|
||||
const dialog = manifest.contributes.keybindings.find(
|
||||
(item) => item.command === "kilo-code.new.agentManager.newWorktree",
|
||||
)
|
||||
const quick = manifest.contributes.keybindings.find(
|
||||
(item) => item.command === "kilo-code.new.agentManager.quickWorktree",
|
||||
)
|
||||
|
||||
expect(dialog).toMatchObject({ key: "ctrl+n", mac: "cmd+n" })
|
||||
expect(quick).toMatchObject({ key: "ctrl+shift+n", mac: "cmd+shift+n" })
|
||||
expect(source).toContain('newWorktree: isMac ? "⌘N" : "Ctrl+N"')
|
||||
expect(source).toContain('quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N"')
|
||||
})
|
||||
|
||||
it("forwards the quick-worktree command to immediate creation", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "src/extension.ts"), "utf-8")
|
||||
const start = source.indexOf('vscode.commands.registerCommand("kilo-code.new.agentManager.quickWorktree"')
|
||||
const end = source.indexOf('vscode.commands.registerCommand("kilo-code.new.agentManager.openWorktree"', start)
|
||||
const command = source.slice(start, end)
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
expect(end).toBeGreaterThan(start)
|
||||
expect(command).toContain('agentManagerProvider.postMessage({ type: "action", action: "quickWorktree" })')
|
||||
})
|
||||
|
||||
it("shows the same mapping in the keyboard shortcuts dialog", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/shortcuts.ts"), "utf-8")
|
||||
|
||||
expect(source).toContain('t("agentManager.shortcuts.advancedWorktree"), binding: bindings.newWorktree')
|
||||
expect(source).toContain('t("agentManager.shortcuts.newWorktree"), binding: bindings.quickWorktree')
|
||||
})
|
||||
|
||||
it("does not attribute the new-worktree shortcut to session promotion", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/UnassignedSessionsSection.tsx"), "utf-8")
|
||||
|
||||
expect(source).toContain('<Tooltip value={t("agentManager.session.openInWorktree")}')
|
||||
expect(source).not.toContain("TooltipKeybind")
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider message routing — static-analysis regression tests
|
||||
//
|
||||
|
||||
@@ -5,6 +5,7 @@ const { AgentManagerProvider } = await import("../../src/agent-manager/AgentMana
|
||||
type Manager = {
|
||||
connectionService: { getClient: () => unknown }
|
||||
panel: {
|
||||
postMessage: (message: unknown) => void
|
||||
sessions: {
|
||||
getSessionDirectories: () => ReadonlyMap<string, string>
|
||||
clearSessionDirectory: (id: string) => void
|
||||
@@ -24,6 +25,7 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
|
||||
const aborted: string[][] = []
|
||||
const cleared: string[] = []
|
||||
const removed: string[] = []
|
||||
const messages: unknown[] = []
|
||||
const events: string[] = []
|
||||
const client = {
|
||||
backgroundProcess: {
|
||||
@@ -44,6 +46,7 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
|
||||
const manager = Object.create(AgentManagerProvider.prototype) as Manager
|
||||
manager.connectionService = { getClient: () => client }
|
||||
manager.panel = {
|
||||
postMessage: (message) => messages.push(message),
|
||||
sessions: {
|
||||
getSessionDirectories: () => new Map(options?.panelDir ? [["s1", options.panelDir]] : []),
|
||||
clearSessionDirectory: (id) => cleared.push(id),
|
||||
@@ -59,12 +62,12 @@ function createManager(options?: { dir?: string; panelDir?: string; state?: bool
|
||||
manager.pushState = mock(() => undefined)
|
||||
manager.log = mock(() => undefined)
|
||||
|
||||
return { manager, stopped, aborted, cleared, removed, events }
|
||||
return { manager, stopped, aborted, cleared, removed, messages, events }
|
||||
}
|
||||
|
||||
describe("AgentManagerProvider closeSession", () => {
|
||||
it("aborts the agent before stopping processes and removing its tab", async () => {
|
||||
const { manager, stopped, aborted, cleared, removed, events } = createManager({ dir: "/repo/worktree" })
|
||||
const { manager, stopped, aborted, cleared, removed, messages, events } = createManager({ dir: "/repo/worktree" })
|
||||
|
||||
await manager.onCloseSession("s1")
|
||||
|
||||
@@ -73,6 +76,7 @@ describe("AgentManagerProvider closeSession", () => {
|
||||
expect(events).toEqual(["abort", "processes", "remove"])
|
||||
expect(removed).toEqual(["s1"])
|
||||
expect(cleared).toEqual(["s1"])
|
||||
expect(messages).toEqual([])
|
||||
expect(manager.panelSessions.has("s1")).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ describe("AgentManagerOrchestrationBridge", () => {
|
||||
state?: (state: "connecting" | "connected" | "disconnected" | "error") => void
|
||||
} = {}
|
||||
const status = { failList: "", failReply: false }
|
||||
const managed = new Set(["ses_target"])
|
||||
const promptAsync = mock(async () => ({ data: undefined }))
|
||||
const close = mock(async () => undefined)
|
||||
const client = {
|
||||
session: {
|
||||
get: mock(async () => ({
|
||||
@@ -95,6 +97,8 @@ describe("AgentManagerOrchestrationBridge", () => {
|
||||
state: () => state,
|
||||
stats: async () => ({ worktrees: [] }),
|
||||
prs: () => new Map(),
|
||||
managed: (id) => managed.has(id),
|
||||
close,
|
||||
log: () => undefined,
|
||||
})
|
||||
const request = (value: AgentManagerRequest, directory = root) =>
|
||||
@@ -102,7 +106,7 @@ describe("AgentManagerOrchestrationBridge", () => {
|
||||
{ id: `event-${value.id}`, type: "kilocode.agent_manager.requested", properties: value } as SSEPayload,
|
||||
directory,
|
||||
)
|
||||
return { bridge, client, handlers, lists, promptAsync, rejections, replies, request, status }
|
||||
return { bridge, client, close, handlers, lists, managed, promptAsync, rejections, replies, request, status }
|
||||
}
|
||||
|
||||
const request: AgentManagerRequest = {
|
||||
@@ -148,6 +152,85 @@ describe("AgentManagerOrchestrationBridge", () => {
|
||||
test.bridge.dispose()
|
||||
})
|
||||
|
||||
it("stops a managed session through the same close operation as the UI", async () => {
|
||||
const test = harness()
|
||||
test.status.failReply = true
|
||||
const stop: AgentManagerRequest = {
|
||||
id: "amr_stop",
|
||||
sessionID: "ses_caller",
|
||||
operation: "stop",
|
||||
targetSessionID: "ses_target",
|
||||
}
|
||||
|
||||
test.request(stop)
|
||||
await waitFor(() => test.replies.length === 1)
|
||||
test.status.failReply = false
|
||||
test.request(stop)
|
||||
await waitFor(() => test.replies.length === 2)
|
||||
|
||||
expect(test.close).toHaveBeenCalledTimes(1)
|
||||
expect(test.close).toHaveBeenCalledWith("ses_target")
|
||||
expect(test.replies).toEqual([
|
||||
{
|
||||
requestID: "amr_stop",
|
||||
directory: root,
|
||||
result: { operation: "stop", sessionID: "ses_target", stopped: true },
|
||||
},
|
||||
{
|
||||
requestID: "amr_stop",
|
||||
directory: root,
|
||||
result: { operation: "stop", sessionID: "ses_target", stopped: true },
|
||||
},
|
||||
])
|
||||
test.bridge.dispose()
|
||||
})
|
||||
|
||||
it("stops a live panel session before it is persisted", async () => {
|
||||
const test = harness()
|
||||
test.managed.add("ses_live")
|
||||
|
||||
test.request({
|
||||
id: "amr_stop_live",
|
||||
sessionID: "ses_caller",
|
||||
operation: "stop",
|
||||
targetSessionID: "ses_live",
|
||||
})
|
||||
await waitFor(() => test.replies.length === 1)
|
||||
|
||||
expect(state.getSession("ses_live")).toBeUndefined()
|
||||
expect(test.close).toHaveBeenCalledWith("ses_live")
|
||||
expect(test.replies[0]).toEqual({
|
||||
requestID: "amr_stop_live",
|
||||
directory: root,
|
||||
result: { operation: "stop", sessionID: "ses_live", stopped: true },
|
||||
})
|
||||
test.bridge.dispose()
|
||||
})
|
||||
|
||||
it("rejects stopping a session not managed by the current workspace", async () => {
|
||||
const test = harness()
|
||||
test.request({
|
||||
id: "amr_stop_unknown",
|
||||
sessionID: "ses_caller",
|
||||
operation: "stop",
|
||||
targetSessionID: "ses_unknown",
|
||||
})
|
||||
await waitFor(() => test.rejections.length === 1)
|
||||
|
||||
expect(test.close).not.toHaveBeenCalled()
|
||||
expect(test.rejections).toEqual([
|
||||
{
|
||||
requestID: "amr_stop_unknown",
|
||||
directory: root,
|
||||
error: {
|
||||
code: "unknown_session",
|
||||
message: "The session is not managed by this Agent Manager workspace",
|
||||
},
|
||||
},
|
||||
])
|
||||
test.bridge.dispose()
|
||||
})
|
||||
|
||||
it("rejects request origins outside the current Agent Manager workspace", async () => {
|
||||
const test = harness()
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { completesWithoutStatus } from "../../src/kilo-provider/command-completion"
|
||||
|
||||
describe("completesWithoutStatus", () => {
|
||||
it("matches only deprecated static review aliases", () => {
|
||||
expect(completesWithoutStatus("local-review")).toBe(true)
|
||||
expect(completesWithoutStatus("local-review-uncommitted")).toBe(true)
|
||||
expect(completesWithoutStatus("review")).toBe(false)
|
||||
expect(completesWithoutStatus("init")).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -126,4 +126,13 @@ describe("indexing tab scope state", () => {
|
||||
qdrant: { url: "http://project", apiKey: "global-secret" },
|
||||
})
|
||||
})
|
||||
|
||||
it("replaces inherited file extensions with a project allowlist", () => {
|
||||
const global = { fileExtensions: [".ts", ".tsx"] }
|
||||
|
||||
expect(indexingConfig("project", global, {}).fileExtensions).toEqual([".ts", ".tsx"])
|
||||
expect(indexingConfig("project", global, { fileExtensions: [".php"] }).fileExtensions).toEqual([".php"])
|
||||
expect(indexingInheritance("project", global, {}, [["fileExtensions"]])).toBe("inherited")
|
||||
expect(indexingInheritance("project", global, { fileExtensions: [".php"] }, [["fileExtensions"]])).toBe("none")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const CHATVIEW_FILE = path.join(ROOT, "webview-ui/src/components/chat/ChatView.t
|
||||
const PROMPT_UTILS_FILE = path.join(ROOT, "webview-ui/src/components/chat/prompt-input-utils.ts")
|
||||
const PROMPT_FILE = path.join(ROOT, "webview-ui/src/components/chat/PromptInput.tsx")
|
||||
const KILOPROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
|
||||
const CLOUD_SESSION_FILE = path.join(ROOT, "src/kilo-provider/handlers/cloud-session.ts")
|
||||
const CONNECTION_SERVICE_FILE = path.join(ROOT, "src/services/cli-backend/connection-service.ts")
|
||||
|
||||
function readFile(filePath: string): string {
|
||||
@@ -79,6 +80,32 @@ describe("sendCommand dismisses pending tool requests", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("static command completion contract", () => {
|
||||
const source = readFile(SESSION_FILE)
|
||||
|
||||
it("finishes only the acknowledged command submission", () => {
|
||||
const body = extractFunctionBody(source, "handleCommandCompletion")
|
||||
expect(body).toMatch(/message\.type === "sessionCommandCompleted"\) finishSubmission\(message\.messageID\)/)
|
||||
})
|
||||
|
||||
it("acknowledges aliases after direct and cloud command confirmation", () => {
|
||||
const provider = readFile(KILOPROVIDER_FILE)
|
||||
const cloud = readFile(CLOUD_SESSION_FILE)
|
||||
expect(provider).toMatch(
|
||||
/await runWithMessageConfirmation[\s\S]*?if \(messageID && completesWithoutStatus\(command\)\)[\s\S]*?sessionCommandCompleted/,
|
||||
)
|
||||
expect(cloud).toMatch(
|
||||
/await run\(messageID, "Cloud import send"[\s\S]*?if \(messageID && command && completesWithoutStatus\(command\)\)[\s\S]*?sessionCommandCompleted/,
|
||||
)
|
||||
})
|
||||
|
||||
it("does not clear every session submission or active abort", () => {
|
||||
const body = extractFunctionBody(source, "handleCommandCompletion")
|
||||
expect(body).not.toContain("confirmSubmissions")
|
||||
expect(body).not.toContain("aborts.clear")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ChatView prompt-block contract", () => {
|
||||
const source = readFile(CHATVIEW_FILE)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import type { Part, ToolPart } from "@kilocode/sdk/v2"
|
||||
import { toolDefaultOpen } from "../../webview-ui/src/components/chat/tool-default-open"
|
||||
|
||||
function tool(name: string) {
|
||||
return { type: "tool", tool: name } as ToolPart
|
||||
}
|
||||
|
||||
describe("toolDefaultOpen", () => {
|
||||
it.each(["bash", "background_process"])("uses the terminal preference for %s", (name) => {
|
||||
expect(toolDefaultOpen(tool(name), false, true)).toBe(false)
|
||||
expect(toolDefaultOpen(tool(name), true, false)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(["edit", "write", "apply_patch"])("uses the code edit preference for %s", (name) => {
|
||||
expect(toolDefaultOpen(tool(name), true, false)).toBe(false)
|
||||
expect(toolDefaultOpen(tool(name), false, true)).toBe(true)
|
||||
})
|
||||
|
||||
it("leaves unrelated parts unchanged", () => {
|
||||
expect(toolDefaultOpen(tool("read"), true, true)).toBeUndefined()
|
||||
expect(toolDefaultOpen({ type: "text" } as Part, true, true)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -56,7 +56,6 @@ import { ThemeProvider } from "@kilocode/kilo-ui/theme"
|
||||
import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { Dialog } from "@kilocode/kilo-ui/dialog"
|
||||
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
|
||||
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
|
||||
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
|
||||
@@ -92,7 +91,6 @@ import { NewWorktreeDialog } from "./NewWorktreeDialog"
|
||||
import { DataBridge, MermaidDownloadBridge } from "../src/App"
|
||||
import { LanguageBridge } from "../src/context/language-bridge"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
import { createTabFocus } from "../src/utils/tab-navigation"
|
||||
import {
|
||||
canOpenRootSession,
|
||||
@@ -137,6 +135,7 @@ import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search"
|
||||
import { WorktreeSectionActions } from "./WorktreeSectionActions"
|
||||
import { BranchSelect } from "../src/components/shared/BranchSelect"
|
||||
import { WorktreeItem } from "./WorktreeItem"
|
||||
import { UnassignedSessionsSection } from "./UnassignedSessionsSection"
|
||||
import SectionHeader from "./SectionHeader"
|
||||
import { randomColor } from "./section-colors"
|
||||
import { createNewTaskDrafts } from "./new-task-drafts"
|
||||
@@ -204,7 +203,7 @@ const defaultBindings: Record<string, string> = {
|
||||
newTab: isMac ? "⌘T" : "Ctrl+T",
|
||||
closeTab: isMac ? "⌘W" : "Ctrl+W",
|
||||
newWorktree: isMac ? "⌘N" : "Ctrl+N",
|
||||
advancedWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N",
|
||||
quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N",
|
||||
closeWorktree: isMac ? "⌘⇧W" : "Ctrl+Shift+W",
|
||||
openWorktree: isMac ? "⌘⇧O" : "Ctrl+Shift+O",
|
||||
openPR: isMac ? "⌘⇧R" : "Ctrl+Shift+R",
|
||||
@@ -256,6 +255,11 @@ const AgentManagerContent: Component = () => {
|
||||
setLocalSessionIDs((prev) => (prev.includes(sid) ? prev.filter((id) => id !== sid) : prev))
|
||||
const [sidebarWidth, setSidebarWidth] = createSignal(persisted?.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH)
|
||||
const [sessionsCollapsed, setSessionsCollapsed] = createSignal(true)
|
||||
const toggleSessions = () => {
|
||||
const collapsed = !sessionsCollapsed()
|
||||
setSessionsCollapsed(collapsed)
|
||||
vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed })
|
||||
}
|
||||
const sidebar = createSidebarCollapse(vscode)
|
||||
const sidebarCollapsed = sidebar.collapsed
|
||||
const expandSidebar = sidebar.expand
|
||||
@@ -925,15 +929,17 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const scrollIntoView = (el: HTMLElement) => el.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||
|
||||
const selectUnassigned = (id: string) => {
|
||||
saveTabMemory()
|
||||
setSelection(null)
|
||||
setReviewActive(false)
|
||||
session.selectSession(id)
|
||||
}
|
||||
|
||||
const focusSidebarItem = (item: { type: string; id: string }) => {
|
||||
if (item.type === "local") selectLocal()
|
||||
else if (item.type === "wt") selectWorktree(item.id)
|
||||
else {
|
||||
saveTabMemory()
|
||||
setSelection(null)
|
||||
setReviewActive(false)
|
||||
session.selectSession(item.id)
|
||||
}
|
||||
else selectUnassigned(item.id)
|
||||
const el = document.querySelector(`[data-sidebar-id="${item.id}"]`)
|
||||
if (el instanceof HTMLElement) scrollIntoView(el)
|
||||
}
|
||||
@@ -1100,11 +1106,12 @@ const AgentManagerContent: Component = () => {
|
||||
} else setSidePanel((prev) => (prev === "diff" ? null : "diff"))
|
||||
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
|
||||
else if (msg.action === "closeTab") closeActiveTab()
|
||||
else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
|
||||
else if (msg.action === "newWorktree") showNewWorktreeDialog()
|
||||
else if (msg.action === "quickWorktree") handleCreateWorktree()
|
||||
else if (msg.action === "openWorktree") openWorktreeDirectory()
|
||||
else if (msg.action === "openPR") openSelectedPR()
|
||||
else if (msg.action === "runScript") runSelected()
|
||||
else if (msg.action === "advancedWorktree") showAdvancedWorktreeDialog()
|
||||
else if (msg.action === "advancedWorktree") showNewWorktreeDialog()
|
||||
else if (msg.action === "closeWorktree") closeSelectedWorktree()
|
||||
else if (msg.action === "showShortcuts") handleShowKeyboardShortcuts()
|
||||
else if (msg.action === "focusInput") window.dispatchEvent(new Event("focusPrompt"))
|
||||
@@ -1215,8 +1222,8 @@ const AgentManagerContent: Component = () => {
|
||||
// Mark sessions loaded as soon as the session context receives data (even if empty)
|
||||
const unsubSessions = vscode.onMessage((msg) => {
|
||||
if (msg.type === "sessionsLoaded" && !sessionsLoaded()) setSessionsLoaded(true)
|
||||
if (msg.type === "agentManager.sessionClosed") handleCloseTab(msg.sessionId, false)
|
||||
})
|
||||
|
||||
const unsubRun = vscode.onMessage((msg) => {
|
||||
if (msg.type !== "agentManager.runStatus") return
|
||||
const ev = msg as RunStatus
|
||||
@@ -1844,8 +1851,7 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
const createWorktree = metrics.click("new_worktree", "worktrees", handleCreateWorktree)
|
||||
|
||||
// Advanced worktree dialog — opens a full dialog with prompt, versions, model, mode
|
||||
const showAdvancedWorktreeDialog = () => {
|
||||
const showNewWorktreeDialog = () => {
|
||||
if (!loaded()) return
|
||||
expandSidebar()
|
||||
dialog.show(() => <NewWorktreeDialog onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />)
|
||||
@@ -1934,8 +1940,7 @@ const AgentManagerContent: Component = () => {
|
||||
confirmDeleteWorktree(worktreeId)
|
||||
}
|
||||
|
||||
const handlePromote = (sessionId: string, e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
const promoteSession = (sessionId: string) => {
|
||||
if (!loaded()) return
|
||||
metrics.track("promote_session", "unassigned_session")
|
||||
vscode.postMessage({ type: "agentManager.promoteSession", sessionId })
|
||||
@@ -1953,6 +1958,11 @@ const AgentManagerContent: Component = () => {
|
||||
vscode.postMessage({ type: "agentManager.openLocally", sessionId: sid })
|
||||
}
|
||||
|
||||
const openUnassigned = (id: string) => {
|
||||
metrics.track("open_session_locally", "unassigned_session_menu")
|
||||
openLocally(id)
|
||||
}
|
||||
|
||||
const handleAddSession = () => {
|
||||
const sel = selection()
|
||||
expandSidebar()
|
||||
@@ -1969,7 +1979,7 @@ const AgentManagerContent: Component = () => {
|
||||
if (!sel || sel === LOCAL) return vscode.postMessage(msg)
|
||||
vscode.postMessage({ ...msg, worktreeId: sel })
|
||||
}
|
||||
const handleCloseTab = (sessionId: string) => {
|
||||
const handleCloseTab = (sessionId: string, notify = true) => {
|
||||
freezeTabs()
|
||||
const pending = isPending(sessionId)
|
||||
const isActive = pending ? sessionId === activePendingId() : session.currentSessionID() === sessionId
|
||||
@@ -1999,7 +2009,7 @@ const AgentManagerContent: Component = () => {
|
||||
if (session.isSubmitting(sessionId) || isPendingSend(sessionId)) discardPendingDraft(sessionId)
|
||||
queueMicrotask(() => deletePendingDraft(sessionId))
|
||||
}
|
||||
vscode.postMessage({ type: "agentManager.closeSession", sessionId })
|
||||
if (notify) vscode.postMessage({ type: "agentManager.closeSession", sessionId })
|
||||
tabFocus.restore()
|
||||
}
|
||||
|
||||
@@ -2174,18 +2184,6 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Cmd+N: if an unassigned session is selected, promote it; otherwise create a new worktree
|
||||
const handleNewWorktreeOrPromote = () => {
|
||||
if (!loaded()) return
|
||||
const sel = selection()
|
||||
const sid = session.currentSessionID()
|
||||
if (sel === null && sid && !worktreeSessionIds().has(sid)) {
|
||||
vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
|
||||
return
|
||||
}
|
||||
handleCreateWorktree()
|
||||
}
|
||||
|
||||
// Close the currently selected worktree with a confirmation dialog
|
||||
const closeSelectedWorktree = () => {
|
||||
const sel = selection()
|
||||
@@ -2312,7 +2310,7 @@ const AgentManagerContent: Component = () => {
|
||||
onRef={(value) => (sidebarSearchMenu = value)}
|
||||
onSelect={focusSidebarSearchItem}
|
||||
onCreate={createWorktree}
|
||||
onAdvanced={showAdvancedWorktreeDialog}
|
||||
onNew={showNewWorktreeDialog}
|
||||
onSection={newSection}
|
||||
onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)}
|
||||
onSetup={setupScript}
|
||||
@@ -2549,7 +2547,7 @@ const AgentManagerContent: Component = () => {
|
||||
)
|
||||
})()}
|
||||
<Show when={worktrees().length === 0}>
|
||||
<button class="am-worktree-create" onClick={createWorktree}>
|
||||
<button class="am-worktree-create" onClick={showNewWorktreeDialog}>
|
||||
<Icon name="plus" size="small" />
|
||||
<span>{t("agentManager.worktree.new")}</span>
|
||||
</button>
|
||||
@@ -2559,102 +2557,16 @@ const AgentManagerContent: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SESSIONS section (unassigned) — collapsible */}
|
||||
<div class={`am-section ${sessionsCollapsed() ? "" : "am-section-grow"}`}>
|
||||
<button
|
||||
class="am-section-header am-section-toggle"
|
||||
onClick={() => {
|
||||
const next = !sessionsCollapsed()
|
||||
setSessionsCollapsed(next)
|
||||
vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed: next })
|
||||
}}
|
||||
>
|
||||
<span class="am-section-label">
|
||||
<Icon
|
||||
name={sessionsCollapsed() ? "chevron-right" : "chevron-down"}
|
||||
size="small"
|
||||
class="am-section-chevron"
|
||||
/>
|
||||
{t("agentManager.section.sessions")}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={!sessionsCollapsed()}>
|
||||
<div class="am-list">
|
||||
<Show
|
||||
when={sessionsLoaded()}
|
||||
fallback={
|
||||
<div class="am-skeleton-list">
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "70%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "55%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "65%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={unassignedSessions()}>
|
||||
{(s) => (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger as="div" style={{ display: "contents" }}>
|
||||
<button
|
||||
class={`am-item ${s.id === session.currentSessionID() && selection() === null ? "am-item-active" : ""}`}
|
||||
data-sidebar-id={s.id}
|
||||
onClick={() => {
|
||||
saveTabMemory()
|
||||
setSelection(null)
|
||||
setReviewActive(false)
|
||||
session.selectSession(s.id)
|
||||
}}
|
||||
>
|
||||
<span class="am-item-title">{s.title || t("agentManager.session.untitled")}</span>
|
||||
<span class="am-item-time">{formatRelativeDate(s.updatedAt)}</span>
|
||||
<div class="am-item-promote">
|
||||
<TooltipKeybind
|
||||
title={t("agentManager.session.openInWorktree")}
|
||||
keybind={kb().newWorktree ?? ""}
|
||||
placement="right"
|
||||
>
|
||||
<IconButton
|
||||
icon="branch"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.session.openInWorktree")}
|
||||
onClick={(e: MouseEvent) => handlePromote(s.id, e)}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content class="am-ctx-menu">
|
||||
<ContextMenu.Item onSelect={() => handlePromote(s.id, new MouseEvent("click"))}>
|
||||
<Icon name="branch" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
onSelect={metrics.click("open_session_locally", "unassigned_session_menu", () =>
|
||||
openLocally(s.id),
|
||||
)}
|
||||
>
|
||||
<Icon name="folder" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<UnassignedSessionsSection
|
||||
sessions={unassignedSessions}
|
||||
loaded={sessionsLoaded}
|
||||
collapsed={sessionsCollapsed}
|
||||
active={() => (selection() === null ? session.currentSessionID() : undefined)}
|
||||
onToggle={toggleSessions}
|
||||
onSelect={selectUnassigned}
|
||||
onPromote={promoteSession}
|
||||
onOpen={openUnassigned}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="am-detail">
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { For, Show, type Accessor, type Component } from "solid-js"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { SessionInfo } from "../src/types/messages"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
|
||||
interface Props {
|
||||
sessions: Accessor<SessionInfo[]>
|
||||
loaded: Accessor<boolean>
|
||||
collapsed: Accessor<boolean>
|
||||
active: Accessor<string | undefined>
|
||||
onToggle: () => void
|
||||
onSelect: (id: string) => void
|
||||
onPromote: (id: string) => void
|
||||
onOpen: (id: string) => void
|
||||
}
|
||||
|
||||
export const UnassignedSessionsSection: Component<Props> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
|
||||
const promote = (id: string, event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
props.onPromote(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={`am-section ${props.collapsed() ? "" : "am-section-grow"}`}>
|
||||
<button class="am-section-header am-section-toggle" onClick={props.onToggle}>
|
||||
<span class="am-section-label">
|
||||
<Icon name={props.collapsed() ? "chevron-right" : "chevron-down"} size="small" class="am-section-chevron" />
|
||||
{t("agentManager.section.sessions")}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={!props.collapsed()}>
|
||||
<div class="am-list">
|
||||
<Show
|
||||
when={props.loaded()}
|
||||
fallback={
|
||||
<div class="am-skeleton-list">
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "70%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "55%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
<div class="am-skeleton-session">
|
||||
<div class="am-skeleton-session-title" style={{ width: "65%" }} />
|
||||
<div class="am-skeleton-session-time" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={props.sessions()}>
|
||||
{(session) => (
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger as="div" style={{ display: "contents" }}>
|
||||
<button
|
||||
class={`am-item ${session.id === props.active() ? "am-item-active" : ""}`}
|
||||
data-sidebar-id={session.id}
|
||||
onClick={() => props.onSelect(session.id)}
|
||||
>
|
||||
<span class="am-item-title" dir="auto">
|
||||
{session.title || t("agentManager.session.untitled")}
|
||||
</span>
|
||||
<span class="am-item-time">{formatRelativeDate(session.updatedAt)}</span>
|
||||
<div class="am-item-promote">
|
||||
<Tooltip value={t("agentManager.session.openInWorktree")} placement="right">
|
||||
<IconButton
|
||||
icon="branch"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.session.openInWorktree")}
|
||||
onClick={(event: MouseEvent) => promote(session.id, event)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content class="am-ctx-menu">
|
||||
<ContextMenu.Item onSelect={() => props.onPromote(session.id)}>
|
||||
<Icon name="branch" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item onSelect={() => props.onOpen(session.id)}>
|
||||
<Icon name="folder" size="small" />
|
||||
<ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ interface WorktreeSectionActionsProps {
|
||||
onRef: (ref: SidebarSearchMenuRef) => void
|
||||
onSelect: (item: SidebarSearchItem) => void
|
||||
onCreate: () => void
|
||||
onAdvanced: () => void
|
||||
onNew: () => void
|
||||
onSection: () => void
|
||||
onShortcuts: () => void
|
||||
onSetup: () => void
|
||||
@@ -48,14 +48,19 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
|
||||
/>
|
||||
<Show when={props.git}>
|
||||
<div class="am-split-button">
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.worktree.new")}
|
||||
onClick={props.onCreate}
|
||||
disabled={!props.loaded}
|
||||
/>
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.shortcuts.advancedWorktree")}
|
||||
keybind={props.bindings.newWorktree ?? ""}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.t("agentManager.worktree.new")}
|
||||
onClick={props.onNew}
|
||||
disabled={!props.loaded}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
class="am-split-arrow"
|
||||
@@ -76,16 +81,7 @@ export const WorktreeSectionActions: Component<WorktreeSectionActionsProps> = (p
|
||||
</span>
|
||||
</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(props.bindings.newWorktree ?? "").map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={props.onAdvanced}>
|
||||
<Icon name="settings-gear" size="small" />
|
||||
<DropdownMenu.ItemLabel>{props.t("agentManager.dialog.configureWorktree")}</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(props.bindings.advancedWorktree ?? "").map((token) => (
|
||||
{parseBindingTokens(props.bindings.quickWorktree ?? "").map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -78,9 +78,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "العنصر السابق",
|
||||
"agentManager.shortcuts.nextItem": "العنصر التالي",
|
||||
"agentManager.shortcuts.newWorktree": "Worktree جديد",
|
||||
"agentManager.shortcuts.advancedWorktree": "تكوين worktree جديد",
|
||||
"agentManager.shortcuts.openWorktree": "فتح Worktree",
|
||||
"agentManager.shortcuts.openPR": "فتح طلب السحب",
|
||||
"agentManager.shortcuts.advancedWorktree": "تكوين worktree جديد",
|
||||
"agentManager.shortcuts.deleteWorktree": "حذف Worktree",
|
||||
"agentManager.shortcuts.previousTab": "علامة التبويب السابقة",
|
||||
"agentManager.shortcuts.nextTab": "علامة التبويب التالية",
|
||||
|
||||
+1
-1
@@ -80,9 +80,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Item anterior",
|
||||
"agentManager.shortcuts.nextItem": "Próximo item",
|
||||
"agentManager.shortcuts.newWorktree": "Novo Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurar novo worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Abrir Worktree",
|
||||
"agentManager.shortcuts.openPR": "Abrir PR",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurar novo worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Excluir Worktree",
|
||||
"agentManager.shortcuts.previousTab": "Aba anterior",
|
||||
"agentManager.shortcuts.nextTab": "Próxima aba",
|
||||
|
||||
+1
-1
@@ -80,9 +80,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Prethodni element",
|
||||
"agentManager.shortcuts.nextItem": "Sljedeći element",
|
||||
"agentManager.shortcuts.newWorktree": "Novi Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Konfiguriši novi worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Otvori worktree",
|
||||
"agentManager.shortcuts.openPR": "Otvori PR",
|
||||
"agentManager.shortcuts.advancedWorktree": "Konfiguriši novi worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Obriši Worktree",
|
||||
"agentManager.shortcuts.previousTab": "Prethodna kartica",
|
||||
"agentManager.shortcuts.nextTab": "Sljedeća kartica",
|
||||
|
||||
+1
-1
@@ -81,9 +81,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Forrige element",
|
||||
"agentManager.shortcuts.nextItem": "Næste element",
|
||||
"agentManager.shortcuts.newWorktree": "Nyt Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Konfigurer nyt worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Åbn Worktree",
|
||||
"agentManager.shortcuts.openPR": "Åbn PR",
|
||||
"agentManager.shortcuts.advancedWorktree": "Konfigurer nyt worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Slet Worktree",
|
||||
"agentManager.shortcuts.previousTab": "Forrige fane",
|
||||
"agentManager.shortcuts.nextTab": "Næste fane",
|
||||
|
||||
@@ -81,9 +81,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Vorheriges Element",
|
||||
"agentManager.shortcuts.nextItem": "Nächstes Element",
|
||||
"agentManager.shortcuts.newWorktree": "Neuer Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Neuen Worktree konfigurieren",
|
||||
"agentManager.shortcuts.openWorktree": "Worktree öffnen",
|
||||
"agentManager.shortcuts.openPR": "Pull Request öffnen",
|
||||
"agentManager.shortcuts.advancedWorktree": "Neuen Worktree konfigurieren",
|
||||
"agentManager.shortcuts.deleteWorktree": "Worktree löschen",
|
||||
"agentManager.shortcuts.previousTab": "Vorheriger Tab",
|
||||
"agentManager.shortcuts.nextTab": "Nächster Tab",
|
||||
|
||||
@@ -84,9 +84,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Previous item",
|
||||
"agentManager.shortcuts.nextItem": "Next item",
|
||||
"agentManager.shortcuts.newWorktree": "New worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configure new worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Open worktree",
|
||||
"agentManager.shortcuts.openPR": "Open pull request",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configure new worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Delete worktree",
|
||||
"agentManager.shortcuts.previousTab": "Previous tab",
|
||||
"agentManager.shortcuts.nextTab": "Next tab",
|
||||
|
||||
+1
-1
@@ -80,9 +80,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Elemento anterior",
|
||||
"agentManager.shortcuts.nextItem": "Siguiente elemento",
|
||||
"agentManager.shortcuts.newWorktree": "Nuevo Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurar nuevo worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Abrir Worktree",
|
||||
"agentManager.shortcuts.openPR": "Abrir pull request",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurar nuevo worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Eliminar Worktree",
|
||||
"agentManager.shortcuts.previousTab": "Pestaña anterior",
|
||||
"agentManager.shortcuts.nextTab": "Siguiente pestaña",
|
||||
|
||||
+1
-1
@@ -80,9 +80,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Élément précédent",
|
||||
"agentManager.shortcuts.nextItem": "Élément suivant",
|
||||
"agentManager.shortcuts.newWorktree": "Nouveau Worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurer un nouveau worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Ouvrir le worktree",
|
||||
"agentManager.shortcuts.openPR": "Ouvrir la pull request",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configurer un nouveau worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Supprimer le Worktree",
|
||||
"agentManager.shortcuts.previousTab": "Onglet précédent",
|
||||
"agentManager.shortcuts.nextTab": "Onglet suivant",
|
||||
|
||||
+1
-1
@@ -85,9 +85,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "Elemento precedente",
|
||||
"agentManager.shortcuts.nextItem": "Elemento successivo",
|
||||
"agentManager.shortcuts.newWorktree": "Nuovo worktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configura nuovo worktree",
|
||||
"agentManager.shortcuts.openWorktree": "Apri worktree",
|
||||
"agentManager.shortcuts.openPR": "Apri pull request",
|
||||
"agentManager.shortcuts.advancedWorktree": "Configura nuovo worktree",
|
||||
"agentManager.shortcuts.deleteWorktree": "Elimina worktree",
|
||||
"agentManager.shortcuts.previousTab": "Scheda precedente",
|
||||
"agentManager.shortcuts.nextTab": "Scheda successiva",
|
||||
|
||||
+1
-1
@@ -80,9 +80,9 @@ export const dict = {
|
||||
"agentManager.shortcuts.previousItem": "前の項目",
|
||||
"agentManager.shortcuts.nextItem": "次の項目",
|
||||
"agentManager.shortcuts.newWorktree": "新しいWorktree",
|
||||
"agentManager.shortcuts.advancedWorktree": "新規 worktree の構成",
|
||||
"agentManager.shortcuts.openWorktree": "Worktreeを開く",
|
||||
"agentManager.shortcuts.openPR": "PRを開く",
|
||||
"agentManager.shortcuts.advancedWorktree": "新規 worktree の構成",
|
||||
"agentManager.shortcuts.deleteWorktree": "Worktreeを削除",
|
||||
"agentManager.shortcuts.previousTab": "前のタブ",
|
||||
"agentManager.shortcuts.nextTab": "次のタブ",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user