Merge branch 'main' into fix/local-review-scale-sub-agents

This commit is contained in:
Marian Alexandru Alecu
2026-06-19 16:21:13 +03:00
committed by GitHub
116 changed files with 1566 additions and 306 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@kilocode/cli": minor
"@kilocode/kilo-gateway": minor
"kilo-code": minor
---
Show a BYOK badge for Kilo Gateway models that can use an enabled personal or organization provider key.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Describe custom providers independently of their selected API protocol.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Show docs URL in dialog when no display server is detected on headless Linux systems
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Show the docs URL in an alert dialog when the browser cannot be opened on headless systems instead of silently failing.
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Hide reverted provider errors so Redo controls remain visible after rewinding a session.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep changed identifiers intact when highlighting edits within diff lines.
+7
View File
@@ -0,0 +1,7 @@
---
"@kilocode/cli": patch
"@kilocode/sdk": patch
"kilo-code": patch
---
Limit completion sounds to parent agent sessions.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Avoid failing Agent Manager startup when another extension already registered VS Code panel commands.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep chat output updating after reverting and resubmitting a prompt.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Prevent concurrent subagent updates from blanking the Agent Manager webview.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Speed up Agent Manager worktree hover cards so pull request details appear and dismiss more quickly.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Widen the chat readable lane from 88ch to 98ch so conversations, tools, and diffs can use more editor space.
@@ -40,9 +40,12 @@ jobs:
env:
URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }}
SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }}
VERCEL: ${{ secrets.OPENCODE_WATCH_VERCEL_SECRET }}
run: |
if [ -n "$URL" ] && [ -n "$SECRET" ]; then
curl -sf -X POST "$URL" -H "x-sync-secret: $SECRET" -H "Content-Type: application/json" -d '{}' || true
headers=(-H "x-sync-secret: $SECRET" -H "Content-Type: application/json")
[ -n "$VERCEL" ] && headers+=(-H "x-vercel-protection-bypass: $VERCEL")
curl -sf -X POST "$URL" "${headers[@]}" -d '{}' || true
fi
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
+10 -1
View File
@@ -354,6 +354,7 @@
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"happy-dom": "20.8.9",
"knip": "5.85.0",
"prettier": "3.6.2",
"qrcode": "^1.5.4",
@@ -2204,6 +2205,8 @@
"@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@@ -2486,6 +2489,8 @@
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
@@ -3076,6 +3081,8 @@
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
@@ -4270,7 +4277,7 @@
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
@@ -4630,6 +4637,8 @@
"cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="],
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-ViNoPxLiExkP4OC7F0AfXnE2Ib+oi4qtP0hqnfhV2Ho=",
"aarch64-linux": "sha256-TKp6yzSijtElRhtcInC6b9LOjvUrLunyRUXBwOb/4i8=",
"aarch64-darwin": "sha256-kW9kVAHOMpKF0/d/YOZFIENI5KyHGLwDvijd/JprYHg=",
"x86_64-darwin": "sha256-8m1uCjLAQHoEVoBETPzVVV5ji8Zlllf1f9s+d8HmLpY="
"x86_64-linux": "sha256-6wXsrAKFGbHf3bfRmbKMXsrI4jYOzzOkwcyFnM4JVK4=",
"aarch64-linux": "sha256-Vmkvv4HEf0DQA+B2khHfAvGRjxiawtQjUeN+G+SNbH0=",
"aarch64-darwin": "sha256-u5mIwNkEmf2sJBHH/Lm3o+OXtDbl7rLG38bkLRUH1cY=",
"x86_64-darwin": "sha256-MuA/788i7hYZurDjJJnFUr/BHF9sP/mg0BvxCHQsbCY="
}
}
@@ -265,7 +265,10 @@ export function AgentBuilderRoute() {
onInput={(event) => state.setDesc(event.currentTarget.value)}
/>
</FieldCard>
<FieldCard label="Mode">
<FieldCard
label="Mode"
description="Primary agents can run sessions directly; subagents are delegated to by other agents."
>
<CustomSelect
label="Agent mode"
value={state.mode()}
@@ -323,6 +326,7 @@ export function AgentBuilderRoute() {
<div class="ui-form agent-builder-form">
<FieldCard
label="Model"
description="Leave unset to inherit the default model, or choose one to pin this agent to a specific model."
actions={
<>
<Show when={!state.locked() && state.model()}>
@@ -40,6 +40,7 @@ function chips(model: Model) {
const list: string[] = []
if (model.capabilities.toolcall) list.push("toolcall")
if (model.capabilities.attachment) list.push("attachment")
if (model.capabilities.temperature) list.push("temperature")
if (model.capabilities.input.image) list.push("vision")
if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio")
return list
@@ -87,6 +88,7 @@ function Stat(props: { label: string; value: string; sub?: string; mono?: boolea
const caps = [
{ key: "toolcall", label: "toolcall" },
{ key: "attachment", label: "attachment" },
{ key: "temperature", label: "temperature" },
{ key: "input:image", label: "vision" },
{ key: "input:audio", label: "audio" },
] satisfies { key: Capability; label: string }[]
@@ -476,6 +478,7 @@ export function ModelsAvailableRoute() {
<Stat label="Input" value={money(item.model.cost.input)} sub="/ 1M tok" mono />
<Stat label="Output" value={money(item.model.cost.output)} sub="/ 1M tok" mono />
<Stat label="Reasoning" value={item.model.capabilities.reasoning ? "Yes" : "No"} />
<Stat label="Temperature" value={item.model.capabilities.temperature ? "Yes" : "No"} />
</div>
<Show when={chips(item.model).length}>
<div class="tags model-capabilities">
@@ -272,6 +272,10 @@
flex-wrap: wrap;
}
.kilo-console .models-capabilities-list button {
text-transform: capitalize;
}
.kilo-console .explore-models {
grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));
gap: 0.625rem;
@@ -59,7 +59,8 @@ You'll find these settings in the Kilo Code settings panel (click the {% codicon
- **Provider ID** — A unique identifier (e.g., `my-provider`).
- **Display name** — A human-readable name shown in the UI.
- **Base URL** — The provider's OpenAI-compatible API endpoint (e.g., `https://api.your-provider.com/v1`). Kilo auto-fetches available models when a valid URL is entered. For Azure OpenAI GPT-5, use the native `azure` provider instead.
- **Provider API** — Select **OpenAI Compatible** for an OpenAI Chat Completions-compatible endpoint. Use **OpenAI Responses** for OpenAI and xAI models. Use **Anthropic Messages** for Anthropic and MiniMax models.
- **Base URL** — The provider's API endpoint (e.g., `https://api.your-provider.com/v1`). Kilo auto-fetches available models when a valid URL exposes an OpenAI-compatible models endpoint. For Azure OpenAI GPT-5, use the native `azure` provider instead.
- **API key** — Your API key. Optional — leave empty if authentication is handled via headers.
- **Models** — Add models manually or select from the auto-fetched list (see [Automatic Model Detection](#automatic-model-detection) below).
- **Headers** (optional) — Custom HTTP headers as key-value pairs.
@@ -91,6 +92,7 @@ You must define at least one model. Setting `name` and `limit` (context window a
{
"provider": {
"vllm": {
"npm": "@ai-sdk/openai-compatible",
"models": {
"qwen35": {
"name": "Qwen 3.5",
@@ -119,8 +121,9 @@ Then set your default model using the `provider-id/model-id` format:
**Configuration fields:**
- **`npm`** — The API protocol package. Use `@ai-sdk/openai-compatible` for OpenAI Chat Completions-compatible endpoints (the default when omitted). Other possible values include `@ai-sdk/openai` for OpenAI Responses endpoints and `@ai-sdk/anthropic` for Anthropic Messages endpoints.
- **`models`** — A map of model IDs to model definitions. Each model should include a `name` and `limit` with `context` and `output` token counts. If `limit.context` or `limit.output` is omitted, it defaults to `0`, which limits context management.
- **`options.baseURL`** — The base URL of your OpenAI-compatible API endpoint. For Azure OpenAI GPT-5, configure `provider.azure` instead.
- **`options.baseURL`** — The base URL of your provider's API endpoint. For Azure OpenAI GPT-5, configure `provider.azure` instead.
- **`options.apiKey`** — Your API key. Use any non-empty string (e.g., `"none"`) if the provider doesn't require authentication.
You can also set the API key via an environment variable instead of putting it in the config file. Use the `env` field to specify which variable to read:
+1 -1
View File
@@ -34,7 +34,7 @@ Setting up v0 in Kilo Code is straightforward:
{% /tab %}
{% tab label="VSCode" %}
Open **Settings** (gear icon) and go to the **Providers** tab to add an OpenAI Compatible provider. Set the base URL to `https://api.v0.dev/v1` and enter your v0 API key.
Open **Settings** (gear icon) and go to the **Providers** tab to add a **Custom provider**. Select **OpenAI Compatible** under **Provider API**, set the base URL to `https://api.v0.dev/v1`, and enter your v0 API key.
The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format.
@@ -10,7 +10,7 @@ Kilo Code ships with a curated list of models for each provider, but you can use
- Using a newly released model before it's added to the built-in catalog
- Running a custom or fine-tuned model via LM Studio, Ollama, or another local provider
- Connecting to a self-hosted model behind an OpenAI-compatible API
- Connecting to a self-hosted model through a custom API endpoint
- Configuring model-specific options like token limits, pricing, or reasoning settings
## Defining a Custom Model
@@ -32,7 +32,8 @@ Add custom models under the `provider.<provider_id>.models` key in your config f
- **Provider ID** — A unique identifier using lowercase letters, numbers, hyphens, or underscores (e.g., `myprovider`). This becomes the `provider_id` in the `provider_id/model_id` format.
- **Display name** — A human-readable name shown in the UI (e.g., `My AI Provider`).
- **Base URL** — The OpenAI-compatible API endpoint (e.g., `https://api.myprovider.com/v1`). When a valid URL is entered, Kilo automatically fetches available models from the endpoint.
- **Provider API** — The protocol used by the provider. Use **OpenAI Responses** for OpenAI and xAI models. Use **Anthropic Messages** for Anthropic and MiniMax models. **OpenAI Compatible** is the default for other OpenAI Chat Completions-compatible endpoints.
- **Base URL** — The provider's API endpoint (e.g., `https://api.myprovider.com/v1`). When a valid URL is entered, Kilo automatically fetches available models from the endpoint if it exposes an OpenAI-compatible models endpoint.
- **API key** — Your provider's API key. Optional — leave empty if you manage authentication via headers.
- **Models** — Add models manually by ID and display name, or select from the auto-fetched list that appears after entering a valid base URL.
- **Headers** (optional) — Add custom HTTP headers as key-value pairs if your provider requires them.
@@ -253,7 +254,7 @@ Connect to any provider that exposes an OpenAI-compatible API:
### Configuring model options and variants
Override options or define reasoning variants for a built-in model:
Override options or define reasoning variants for a built-in model. Variant fields are provider-specific and are merged into the request when you select that variant.
```jsonc
{
@@ -286,6 +287,18 @@ Override options or define reasoning variants for a built-in model:
}
```
MiniMax's OpenAI-compatible Chat Completions API supports the optional boolean `reasoning_split` field. Set it on the relevant variant to control how the API returns thinking content:
```jsonc
"variants": {
"thinking": {
"reasoning_split": true,
},
}
```
With `true`, MiniMax returns thinking separately in `reasoning_content` and `reasoning_details`. This setting changes only the response format, not whether the model thinks. Leave it unset for providers that do not support it, including MiniMax using the Anthropic Messages API.
### Using the id field to map model names
If the model key in your config differs from what the provider expects, use the `id` field:
@@ -375,6 +388,7 @@ You can also set options that apply to all models from a provider:
| `apiKey` | `string` | API key (supports `{env:VAR}` syntax) |
| `baseURL` | `string` | Override the provider's base API URL |
| `timeout` | `number \| false` | Request timeout in milliseconds. Defaults to `300000` (5 minutes); set to `false` to disable |
| `chunkTimeout` | `number` | Timeout in milliseconds between streamed response chunks. If no chunk arrives within this window, the request is aborted and retried. This catches silent provider dropouts where the TCP connection stays open but SSE streaming stops. Recommended: `15000``30000` (1530 seconds) for providers with unreliable streaming. |
## Filtering Available Models
@@ -97,5 +97,6 @@ Your IAM user or role must have the following permissions:
## Using BYOK in the Extensions and CLI
- BYOK works with the Kilo Gateway provider. Users should ensure that is set as the active [provider](/docs/ai-providers).
- Select a model from a provider configured for BYOK, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan.
- Kilo Gateway models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. The badge does not apply to models selected through other providers.
- Select a model with the `BYOK` badge, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan.
- (Optional) Validate with the provider that traffic is being served by that key.
@@ -59,6 +59,18 @@ When the Kilo provider is enabled and you are signed in, choose the transcriptio
}
```
### Prompt-Training Model Visibility
Enable **Hide Prompt-Training Models** under **Models** to remove Kilo Gateway models whose providers may use your prompts for training from model lists. Models from other providers and models without explicit prompt-training metadata remain visible. The setting is disabled by default.
You can also enable it in `kilo.jsonc`:
```json
{
"hide_prompt_training_models": true
}
```
### Reasoning Blocks
Reasoning blocks stay expanded by default in the VS Code chat UI. Enable **Auto-Collapse Reasoning** in the Display tab, or set `auto_collapse_reasoning` in `kilo.jsonc`, to collapse them after the agent finishes writing them:
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6ecf78a0bcfaebdb945c3caacb8e32155fe2f6e4017f766e129dd0d17ad8fab
size 51349
oid sha256:270d3ecab98c4462a0e6af3e8fe43b3313da03759c507914d0b1048045474f1e
size 50886
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25f9a2c9499801c7526c6e0dd60a17ecacb873e69e02e7073f33e07b4c96841d
size 28094
oid sha256:4872106b43a1e7e505d0cb1807fa83455a739ec14e86b65a5369c56afc3e7df6
size 26645
+2
View File
@@ -37,6 +37,7 @@ const openRouterModelSchema = z.object({
preferredIndex: z.number().optional(),
isFree: z.boolean().optional(),
mayTrainOnYourPrompts: z.boolean().optional(),
hasUserByokAvailable: z.boolean().optional(),
terminalBench: z
.object({
overallScore: z.number(),
@@ -192,6 +193,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any {
tool_call: supportsTools,
isFree: model.isFree,
mayTrainOnYourPrompts: model.mayTrainOnYourPrompts,
hasUserByokAvailable: model.hasUserByokAvailable,
...(model.terminalBench && { terminalBench: model.terminalBench }),
...(inputPrice !== undefined &&
outputPrice !== undefined && {
@@ -17,6 +17,7 @@ const VALID_RESPONSE = JSON.stringify({
supported_parameters: ["tools", "temperature"],
isFree: false,
mayTrainOnYourPrompts: true,
hasUserByokAvailable: true,
},
],
})
@@ -148,6 +149,7 @@ test("returns models without error on success", async () => {
expect(result.models["test/model-a"]).toMatchObject({
isFree: false,
mayTrainOnYourPrompts: true,
hasUserByokAvailable: true,
})
})
@@ -988,6 +988,7 @@ object KiloCliDataParser {
temperature = model.temperature,
toolCall = model.toolCall,
free = model.free,
byok = model.byok,
status = model.status,
recommendedIndex = model.recommendedIndex,
variants = model.variants,
@@ -1006,6 +1007,7 @@ object KiloCliDataParser {
temperature = cap.bool("temperature"),
toolCall = cap.bool("toolcall"),
free = obj.bool("isFree"),
byok = obj.bool("hasUserByokAvailable"),
status = obj.str("status"),
recommendedIndex = obj.num("recommendedIndex"),
variants = parseVariants(obj),
@@ -74,6 +74,7 @@ internal object KiloWorkspaceDtoMapper {
temperature = m.temperature,
toolCall = m.toolCall,
free = m.free,
byok = m.byok,
status = m.status,
recommendedIndex = m.recommendedIndex,
variants = m.variants,
@@ -54,6 +54,7 @@ data class ModelInfo(
val temperature: Boolean,
val toolCall: Boolean,
val free: Boolean,
val byok: Boolean = false,
val status: String?,
val recommendedIndex: Double?,
val variants: List<String>,
@@ -128,7 +128,8 @@ class GeneratedApiModelSerializationTest {
"options": {},
"headers": {},
"release_date": "2025-01-01",
"isFree": true
"isFree": true,
"hasUserByokAvailable": true
}
}
}],
@@ -139,6 +140,7 @@ class GeneratedApiModelSerializationTest {
val obj = json.decodeFromString<ProviderList200Response>(src)
val model = obj.all[0].models["free-model"]!!
assertEquals(true, model.isFree)
assertEquals(true, model.hasUserByokAvailable)
assertEquals(
ai.kilocode.jetbrains.api.model.Model.Status.BETA,
model.status,
@@ -1254,6 +1254,8 @@ class KiloCliDataParserTest {
},
"limit": {"context": 200000, "input": 100000, "output": 16000},
"status": "active",
"isFree": false,
"hasUserByokAvailable": true,
"recommendedIndex": 2,
"variants": {"high": {}, "low": {}, "medium": {}},
"options": {}, "headers": {}
@@ -1273,6 +1275,8 @@ class KiloCliDataParserTest {
assertTrue(model.temperature)
assertTrue(model.toolCall)
assertEquals("active", model.status)
assertFalse(model.free)
assertTrue(model.byok)
assertEquals(2.0, model.recommendedIndex)
assertEquals(200000L, model.limit?.context)
assertEquals(100000L, model.limit?.input)
@@ -111,7 +111,8 @@ class ProjectModelSerializationTest {
"options": {},
"headers": {},
"release_date": "2025-01-01",
"isFree": true
"isFree": true,
"hasUserByokAvailable": true
}
}
}],
@@ -122,6 +123,7 @@ class ProjectModelSerializationTest {
val obj = json.decodeFromString<ProviderList200Response>(src)
val model = obj.all[0].models["free-model"]!!
assertEquals(true, model.isFree)
assertEquals(true, model.hasUserByokAvailable)
assertEquals(
ai.kilocode.jetbrains.api.model.Model.Status.BETA,
model.status,
@@ -404,6 +404,7 @@ class SessionUi(
it.providerName,
it.recommendedIndex,
it.free,
it.byok,
it.variants,
it.attachment,
)
@@ -663,6 +663,7 @@ class SessionController(
provider.name,
info.recommendedIndex,
info.free,
info.byok,
info.variants,
info.limit?.let { ModelLimitItem(it.context, it.input, it.output) },
info.attachment,
@@ -583,6 +583,7 @@ data class ModelItem(
val providerName: String,
val recommendedIndex: Double?,
val free: Boolean,
val byok: Boolean = false,
val variants: List<String>,
val limit: ModelLimitItem?,
val attachment: Boolean = false,
@@ -56,6 +56,7 @@ class ModelPicker : PickerButton() {
val providerName: String,
val recommendedIndex: Double? = null,
val free: Boolean = false,
val byok: Boolean = false,
val variants: List<String> = emptyList(),
val attachment: Boolean = false,
) {
@@ -74,6 +74,14 @@ internal class ModelPickerRenderer(
private val badgeLabel = BadgeLabel(badge).apply {
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
}
private val byok = FilledBadgeIcon(
"BYOK",
UiStyle.Colors.badgeBg(),
UiStyle.Colors.badgeFg(),
)
private val byokLabel = BadgeLabel(byok).apply {
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
}
private val warn = JBLabel(DATA_COLLECTED).apply {
toolTipText = ModelText.dataCollected()
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
@@ -83,6 +91,7 @@ internal class ModelPickerRenderer(
add(title)
add(warn)
add(badgeLabel)
add(byokLabel)
add(provider)
}
private val star = JBLabel().apply {
@@ -136,6 +145,7 @@ internal class ModelPickerRenderer(
if (item == null) {
title.append(value.emptyText, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
badgeLabel.isVisible = false
byokLabel.isVisible = false
warn.isVisible = false
provider.isVisible = false
star.icon = EmptyIcon.ICON_16
@@ -150,7 +160,8 @@ internal class ModelPickerRenderer(
title.append(name.model, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
warn.isVisible = ModelText.collectsData(item)
badgeLabel.isVisible = item.free
badgeLabel.isVisible = item.free && !item.byok
byokLabel.isVisible = item.byok
provider.isVisible = value.favorite
provider.text = item.providerName
provider.foreground = weak
@@ -174,6 +185,8 @@ internal class ModelPickerRenderer(
internal fun badgeText(): String = badge.text
internal fun byokVisible(): Boolean = byokLabel.isVisible
internal fun warningVisible(): Boolean = warn.isVisible
internal fun warningTooltip(): String? = warn.toolTipText
@@ -174,7 +174,16 @@ internal class ModelsSettingsUi(
.filter { it.id == KILO_PROVIDER || it.id in cfg.connected }
.flatMap { provider ->
provider.models.mapNotNull { (id, model) ->
val item = ModelPicker.Item(id, model.name, provider.id, provider.name, model.recommendedIndex, model.free, model.variants)
val item = ModelPicker.Item(
id,
model.name,
provider.id,
provider.name,
model.recommendedIndex,
model.free,
model.byok,
model.variants,
)
if (!includeSmall && ModelText.small(item)) return@mapNotNull null
item
}
@@ -397,6 +397,22 @@ class ModelPickerTest : BasePlatformTestCase() {
assertEquals("Data may be used for training", renderer.warningTooltip())
}
fun `test renderer shows BYOK instead of free when both are available`() {
val row = ModelPickerRow(
ModelPicker.Item("claude", "Claude", "kilo", "Kilo", free = true, byok = true),
"Kilo",
false,
)
val model = CollectionListModel(listOf(row))
val renderer = ModelPickerRenderer(model, { null }, { emptySet() })
val list = JBList(model)
renderer.getListCellRendererComponent(list, row, 0, false, false)
assertTrue(renderer.byokVisible())
assertFalse(renderer.badgeVisible())
}
fun `test renderer hides data collection warning for non-kilo free model`() {
val row = ModelPickerRow(ModelPicker.Item("free", "Free", "openrouter", "OpenRouter", free = true), "OpenRouter", false)
val model = CollectionListModel(listOf(row))
@@ -420,6 +436,7 @@ class ModelPickerTest : BasePlatformTestCase() {
assertSame(EmptyIcon.ICON_16, renderer.starIcon())
assertFalse(renderer.badgeVisible())
assertFalse(renderer.byokVisible())
}
private fun item(
@@ -11,6 +11,7 @@ data class ModelDto(
val temperature: Boolean = false,
val toolCall: Boolean = false,
val free: Boolean = false,
val byok: Boolean = false,
val status: String? = null,
val recommendedIndex: Double? = null,
val variants: List<String> = emptyList(),
+3 -3
View File
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test"
import { createDefaultOptions } from "./index"
describe("Pierre diff options", () => {
test("highlights changed characters in unified and split diffs", () => {
expect(createDefaultOptions("unified").lineDiffType).toBe("char")
expect(createDefaultOptions("split").lineDiffType).toBe("char")
test("keeps changed identifiers intact in unified and split diffs", () => {
expect(createDefaultOptions("unified").lineDiffType).toBe("word-alt")
expect(createDefaultOptions("split").lineDiffType).toBe("word-alt")
})
})
+3 -1
View File
@@ -10,7 +10,9 @@ import { createDefaultOptions as defaults, styleVariables } from "@opencode-ai/u
export { styleVariables }
export const LINE_DIFF_TYPE = "char" as const
// Character matching fragments inserted identifiers when they share letters with
// existing symbols. Word-alt keeps those logical additions visually intact.
export const LINE_DIFF_TYPE = "word-alt" as const
// Pierre 1.1 treats its changed-line override properties as tint targets. Apply
// Kilo semantic surfaces at the computed row level so host diff colors stay final.
+1
View File
@@ -1100,6 +1100,7 @@
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"happy-dom": "20.8.9",
"knip": "5.85.0",
"prettier": "3.6.2",
"qrcode": "^1.5.4",
+99 -28
View File
@@ -172,6 +172,16 @@ const SESSION_SCOPED_PART_EVENTS = new Set(["message.part.updated", "message.par
const isSessionScopedPartEvent = (type: string) => SESSION_SCOPED_PART_EVENTS.has(type)
type SyncPayload = Extract<GlobalEvent["payload"], { type: "sync" }>
type RawSyncPayload = {
type: "sync"
syncEvent: {
type: SyncPayload["name"]
id: string
seq: number
aggregateID: string
data: unknown
}
}
type LegacySyncEvent =
| {
id: string
@@ -201,6 +211,7 @@ type LegacySyncEvent =
| {
source: "sync"
id: string
seq: number
type: "session.updated"
properties: Extract<SyncPayload, { name: "session.updated.1" }>["data"]
}
@@ -234,24 +245,35 @@ function isFullSessionUpdatedEvent(event: ProviderEvent): event is FullSessionUp
return event.type === "session.updated" && !isLegacySyncEvent(event)
}
function unwrapSyncEvent(event: GlobalEvent["payload"]): ProviderEvent | undefined {
export function unwrapSyncEvent(event: GlobalEvent["payload"] | RawSyncPayload): ProviderEvent | undefined {
if (event.type !== "sync") return event
const payload =
"syncEvent" in event
? ({
type: "sync",
name: event.syncEvent.type,
id: event.syncEvent.id,
seq: event.syncEvent.seq,
aggregateID: event.syncEvent.aggregateID,
data: event.syncEvent.data,
} as SyncPayload)
: event
switch (event.name) {
switch (payload.name) {
case "message.updated.1":
return { id: event.id, type: "message.updated", properties: event.data }
return { id: payload.id, type: "message.updated", properties: payload.data }
case "message.removed.1":
return { id: event.id, type: "message.removed", properties: event.data }
return { id: payload.id, type: "message.removed", properties: payload.data }
case "message.part.updated.1":
return { id: event.id, type: "message.part.updated", properties: event.data }
return { id: payload.id, type: "message.part.updated", properties: payload.data }
case "message.part.removed.1":
return { id: event.id, type: "message.part.removed", properties: event.data }
return { id: payload.id, type: "message.part.removed", properties: payload.data }
case "session.created.1":
return { id: event.id, type: "session.created", properties: event.data }
return { id: payload.id, type: "session.created", properties: payload.data }
case "session.updated.1":
return { source: "sync", id: event.id, type: "session.updated", properties: event.data }
return { source: "sync", id: payload.id, seq: payload.seq, type: "session.updated", properties: payload.data }
case "session.deleted.1":
return { id: event.id, type: "session.deleted", properties: event.data }
return { id: payload.id, type: "session.deleted", properties: payload.data }
default:
return undefined
}
@@ -308,6 +330,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private promptRecovery: Promise<void> | null = null
private trackedSessionIds: Set<string> = new Set()
private syncedChildSessions: Set<string> = new Set()
private readonly checkpoints = new Map<string, Promise<void>>()
private readonly revisions = new Map<string, { id: string; seq: number }>()
private readonly refreshes = new Map<string, number>()
private sessionStatusMap = new Map<string, SessionStatus["type"]>() // Latest status used for destructive config warnings.
private sessionDirectories = new Map<string, string>() // Per-session directory overrides, such as Agent Manager worktrees.
private readonly aborts = new SessionAbort()
@@ -389,10 +414,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
private setCurrentSession(session: Session | null): void {
const ids = new Set([this.currentSession?.id, session?.id])
for (const id of ids) {
if (id) this.refreshes.set(id, (this.refreshes.get(id) ?? 0) + 1)
}
this.currentSession = session
this.opts.tabTitle?.(nativeTitle(session))
}
private checkpoint(sid: string, run: () => Promise<void>): void {
const prior = this.checkpoints.get(sid) ?? Promise.resolve()
const pending = prior.catch(() => undefined).then(run)
const cleanup = () => {
if (this.checkpoints.get(sid) === pending) this.checkpoints.delete(sid)
}
this.checkpoints.set(sid, pending)
void pending.then(cleanup, (error) => {
console.error("[Kilo New] checkpoint mutation failed:", error)
cleanup()
})
}
private stopCurrentSessionProcesses(next?: string): void {
const sid = this.contextSessionID ?? this.currentSession?.id
if (!sid || sid === next) return
@@ -546,6 +588,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
data: profileData,
})
if (this.currentSession) {
this.refreshSessionDetails(this.currentSession.id, this.getWorkspaceDirectory(this.currentSession.id))
}
// Re-send cached worktree stats and git status after webview reload.
if (this.cachedStats) this.postMessage(this.cachedStats)
this.postMessage({ type: "gitStatus", repo: this.cachedGitRepo })
@@ -853,14 +899,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.handleAbort(message.sessionID)
break
case "revertSession":
this.handleRevertSession(message.sessionID, message.messageID, message.partID).catch((e) =>
console.error("[Kilo New] handleRevertSession failed:", e),
this.checkpoint(message.sessionID, () =>
this.handleRevertSession(message.sessionID, message.messageID, message.partID),
)
break
case "unrevertSession":
this.handleUnrevertSession(message.sessionID).catch((e) =>
console.error("[Kilo New] handleUnrevertSession failed:", e),
)
this.checkpoint(message.sessionID, () => this.handleUnrevertSession(message.sessionID))
break
case "permissionResponse":
await handlePermissionResponse(
@@ -1506,13 +1550,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Non-blocking: refresh session metadata + status for the webview after switching. */
private refreshSessionDetails(sessionID: string, dir: string, signal?: AbortSignal): void {
if (!this.client) return
const revision = this.revisions.get(sessionID)
const refresh = (this.refreshes.get(sessionID) ?? 0) + 1
this.refreshes.set(sessionID, refresh)
this.client.session
.get({ sessionID, directory: dir })
.then((r) => {
if (r.data && !signal?.aborted && this.contextSessionID === sessionID) {
this.setCurrentSession(r.data)
this.contextSessionID = r.data.id
if (!r.data || signal?.aborted || this.contextSessionID !== sessionID) return
if (this.refreshes.get(sessionID) !== refresh) {
if (this.revisions.get(sessionID) !== revision) this.refreshSessionDetails(sessionID, dir, signal)
return
}
if (this.revisions.get(sessionID) !== revision) {
this.refreshSessionDetails(sessionID, dir, signal)
return
}
this.setCurrentSession(r.data)
this.contextSessionID = r.data.id
this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(r.data) })
})
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: getSession failed (non-critical):", e))
this.postMessage({ type: "workspaceDirectoryChanged", directory: this.getWorkspaceDirectory(sessionID) })
@@ -1760,6 +1815,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sessionDirectories.delete(sessionID)
this.aborts.delete(sessionID)
this.lastReconciledAt.delete(sessionID)
this.checkpoints.delete(sessionID)
this.revisions.delete(sessionID)
this.refreshes.delete(sessionID)
this.connectionService.pruneSession(sessionID)
if (this.currentSession?.id === sessionID) {
this.contextSessionID = undefined
@@ -2646,6 +2704,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.connectionService.recordMessageSessionId(messageID, sid)
}
await this.checkpoints.get(sid)
await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Message request", () =>
this.withRetry(
() =>
@@ -2724,6 +2783,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const sid = resolved!.sid
const dir = resolved!.dir
await this.checkpoints.get(sid)
await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Command request", () =>
this.withRetry(
() =>
@@ -2772,9 +2832,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (error) {
console.error("[Kilo New] KiloProvider: Failed to revert session:", error)
this.postMessage({ type: "error", message: "Failed to revert session", sessionID })
return
throw error
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
if (!data) throw new Error("Revert returned no session")
this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1)
if (this.currentSession?.id === sessionID) this.setCurrentSession(data)
this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
private async handleUnrevertSession(sessionID: string): Promise<void> {
@@ -2784,9 +2847,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (error) {
console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error)
this.postMessage({ type: "error", message: "Failed to redo session", sessionID })
return
throw error
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
if (!data) throw new Error("Redo returned no session")
this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1)
if (this.currentSession?.id === sessionID) this.setCurrentSession(data)
this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
/**
@@ -3171,6 +3237,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return
}
if (event.type === "session.updated") {
// Full bus snapshots duplicate sync patches with the same event ID but no sequence metadata.
if (isFullSessionUpdatedEvent(event)) return
const sid = event.properties.sessionID
const revision = this.revisions.get(sid)
const versioned = event.seq > 0 || (revision?.seq ?? 0) > 0
if (revision && (versioned ? event.seq <= revision.seq : event.id <= revision.id)) return
this.revisions.set(sid, { id: event.id, seq: event.seq })
}
// Refresh provider and agent lists when the server signals a state disposal
if (event.type === "global.disposed") {
void this.reloadAfterAuthChange()
@@ -3202,10 +3278,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.trackedSessionIds.add(event.properties.info.id)
}
if (event.type === "session.updated" && this.currentSession?.id === event.properties.sessionID) {
const session = isLegacySyncEvent(event)
? applySessionPatch(this.currentSession, event.properties.info)
: event.properties.info
this.setCurrentSession(session)
this.setCurrentSession(applySessionPatch(this.currentSession, event.properties.info))
this.contextSessionID = event.properties.sessionID
}
@@ -3248,9 +3321,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const msg = isLegacySyncEvent(event)
? this.mapSyncEventToWebviewMessage(event)
: isFullSessionUpdatedEvent(event)
? { type: "sessionUpdated" as const, session: this.sessionToWebview(event.properties.info) }
: mapSSEEventToWebviewMessage(event, sessionID)
: mapSSEEventToWebviewMessage(event, sessionID)
if (!msg) return
if (msg.type === "partUpdated") {
this.streams.push({ ...msg, part: this.slimPart(msg.part) })
@@ -203,8 +203,10 @@ export class SessionTerminalManager {
return result
}
const disposable = this.tryRegisterCommand(id, handler)
if (!disposable) return
this.commandHandlers.set(id, handler)
this.commandDisposables.set(id, this.host.registerCommand(id, handler))
this.commandDisposables.set(id, disposable)
}
private async runOriginalCommand(id: string, args: unknown[]): Promise<unknown> {
@@ -218,9 +220,20 @@ export class SessionTerminalManager {
return await this.host.executeCommand(id, ...args)
} finally {
const handler = this.commandHandlers.get(id)
if (!handler) return
const replacement = this.host.registerCommand(id, handler)
this.commandDisposables.set(id, replacement)
if (handler) {
const replacement = this.tryRegisterCommand(id, handler)
if (replacement) this.commandDisposables.set(id, replacement)
}
}
}
private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise<unknown>): Disposable | undefined {
try {
return this.host.registerCommand(id, handler)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
this.log(`panel command registration skipped for ${id}: ${msg}`)
return undefined
}
}
@@ -20,7 +20,6 @@ export class AttentionService implements vscode.Disposable {
private readonly errored = new Set<string>()
private readonly questions = new Set<string>()
private readonly permissions = new Set<string>()
private readonly parents = new Map<string, string | undefined>()
private readonly unsubscribeEvent: () => void
private readonly unsubscribeState: () => void
@@ -35,7 +34,6 @@ export class AttentionService implements vscode.Disposable {
this.unsubscribeEvent()
this.unsubscribeState()
this.reset()
this.parents.clear()
}
private handle(event: SSEPayload) {
@@ -44,26 +42,20 @@ export class AttentionService implements vscode.Disposable {
return this.question(event)
}
if (event.type === "permission.asked" || event.type === "permission.replied") return this.permission(event)
if (event.type === "session.deleted") return this.remove(event.properties.sessionID)
if (event.type === "session.status") return this.status(event)
if (event.type === "session.turn.close") return this.close(event)
if (event.type === "session.error") return this.error(event)
}
private sync(event: Sync) {
if (event.name === "session.created.1") {
this.parents.set(event.data.sessionID, event.data.info.parentID)
return
}
if (event.name === "session.updated.1") {
if (event.data.info.parentID !== undefined) {
this.parents.set(event.data.sessionID, event.data.info.parentID ?? undefined)
}
return
}
if (event.name !== "session.deleted.1") return
this.parents.delete(event.data.sessionID)
this.active.delete(event.data.sessionID)
this.errored.delete(event.data.sessionID)
this.remove(event.data.sessionID)
}
private remove(sessionID: string) {
this.active.delete(sessionID)
this.errored.delete(sessionID)
}
private question(event: Question) {
@@ -98,7 +90,8 @@ export class AttentionService implements vscode.Disposable {
if (!this.active.delete(sessionID)) return
if (this.errored.delete(sessionID)) return
if (event.properties.reason !== "completed") return
this.notify(this.parents.get(sessionID) ? "subagent_done" : "done")
if (event.properties.parentID !== undefined) return
this.notify("done")
}
private error(event: Error) {
@@ -49,6 +49,66 @@ test.describe("webview accessibility ratchet", () => {
})
}
test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => {
await open(page, "agentmanager--sidebar-search-open")
const visibility = await page.locator("#storybook-root").evaluate((root) => {
const layout = document.createElement("div")
layout.className = "am-layout"
root.append(layout)
const names = ["assistant-message", "tool-trigger", "file", "code", "diff"]
const values = names.map((name) => {
const node = document.createElement("div")
node.dataset.component = name
layout.append(node)
return getComputedStyle(node).contentVisibility
})
layout.remove()
return values
})
expect(visibility).toEqual(["visible", "visible", "visible", "visible", "visible"])
})
test("Agent Manager avoids generated separator text in updating tool rows", async ({ page }) => {
await open(page, "agentmanager--sidebar-search-open")
const content = await page.locator("#storybook-root").evaluate((root) => {
const layout = document.createElement("div")
layout.className = "am-layout"
const wrapper = document.createElement("div")
wrapper.dataset.component = "tool-part-wrapper"
wrapper.dataset.partType = "tool"
const collapsible = document.createElement("div")
collapsible.className = "tool-collapsible"
collapsible.dataset.component = "collapsible"
const title = document.createElement("span")
title.dataset.slot = "basic-tool-tool-title"
const subtitle = document.createElement("span")
subtitle.dataset.slot = "basic-tool-tool-subtitle"
collapsible.append(title, subtitle)
wrapper.append(collapsible)
layout.append(wrapper)
root.append(layout)
const value = getComputedStyle(subtitle, "::before").content
layout.remove()
return value
})
expect(content).toBe("none")
})
test("sidebar keeps transcript announcements while Agent Manager bounds them", async ({ page }) => {
await open(page, "chat--chat-view-with-messages")
await expect(page.locator(".message-list")).toHaveAttribute("role", "log")
await expect(page.locator(".message-list")).toHaveAttribute("aria-live", "polite")
await open(page, "chat--chat-view-agent-manager-completed")
await expect(page.locator(".message-list")).not.toHaveAttribute("role")
await expect(page.locator(".message-list")).not.toHaveAttribute("aria-live")
await expect(page.locator('.sr-only[role="status"]')).toHaveAttribute("aria-live", "polite")
})
test("Profile login exposes a keyboard-operable named control", async ({ page }) => {
await open(page, "profile--not-logged-in")
@@ -45,23 +45,20 @@ describe("AttentionService", () => {
test.service.dispose()
})
it("uses the upstream subagent completion sound", () => {
it("plays completion sounds only for parent agents", () => {
const test = setup()
test.event(
event({
type: "sync",
name: "session.created.1",
data: { sessionID: "child", info: { parentID: "parent" } },
}),
)
test.event(
event({ type: "sync", name: "session.updated.1", data: { sessionID: "child", info: { title: "work" } } }),
)
test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "retry" } } }))
test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "idle" } } }))
test.event(event({ type: "session.turn.close", properties: { sessionID: "child", reason: "completed" } }))
test.event(
event({
type: "session.turn.close",
properties: { sessionID: "child", parentID: "parent", reason: "completed" },
}),
)
test.event(event({ type: "session.status", properties: { sessionID: "parent", status: { type: "busy" } } }))
test.event(event({ type: "session.turn.close", properties: { sessionID: "parent", reason: "completed" } }))
expect(test.sounds).toEqual(["subagent_done"])
expect(test.sounds).toEqual(["done"])
test.service.dispose()
})
@@ -123,6 +120,19 @@ describe("AttentionService", () => {
expect(test.sounds).toEqual([])
test.service.dispose()
})
it("clears transitions when a session is deleted", () => {
const test = setup()
test.event(event({ type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } }))
test.event(event({ type: "session.deleted", properties: { sessionID: "s1", info: { id: "s1" } } }))
test.event(event({ type: "session.turn.close", properties: { sessionID: "s1", reason: "completed" } }))
test.event(event({ type: "session.status", properties: { sessionID: "s2", status: { type: "busy" } } }))
test.event(event({ type: "sync", name: "session.deleted.1", data: { sessionID: "s2" } }))
test.event(event({ type: "session.turn.close", properties: { sessionID: "s2", reason: "completed" } }))
expect(test.sounds).toEqual([])
test.service.dispose()
})
})
describe("attention defaults", () => {
@@ -2,7 +2,7 @@ import { describe, it, expect, spyOn } from "bun:test"
import type { PartUpdate } from "../../src/shared/stream-messages"
// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts)
const { KiloProvider } = await import("../../src/KiloProvider")
const { KiloProvider, unwrapSyncEvent } = await import("../../src/KiloProvider")
type State = "connecting" | "connected" | "disconnected" | "error"
@@ -34,6 +34,21 @@ function mkMessage(id: string, role: "user" | "assistant", time = 0) {
}
}
function mkSession(revert?: { messageID: string }) {
return {
id: "s1",
slug: "session",
version: "1",
projectID: "project",
directory: "/repo",
title: "Session",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 1 },
revert,
}
}
function mkResult(items: unknown[]) {
return { data: items, response: { headers: new Headers() } }
}
@@ -42,6 +57,7 @@ function createClient(options?: {
messagesDeferred?: Deferred<{ data: unknown[]; response: { headers: Headers } }>
messagesData?: unknown[]
deleteDeferred?: Deferred<unknown>
revertDeferred?: Deferred<{ data?: unknown; error?: unknown }>
sessionData?: unknown
sessionGet?: (params: { sessionID: string; directory?: string }) => Promise<{ data: unknown }>
abortFailures?: string[]
@@ -49,10 +65,14 @@ function createClient(options?: {
const calls: { before?: string; limit?: number }[] = []
const stopped: { sessionID: string; directory?: string }[] = []
const aborted: { sessionID: string; directory?: string }[] = []
const prompted: Array<Record<string, unknown>> = []
const reverted: Array<Record<string, unknown>> = []
return {
calls,
stopped,
aborted,
prompted,
reverted,
session: {
list: async () => ({ data: [] }),
get: async (params: { sessionID: string; directory?: string }) => {
@@ -60,6 +80,15 @@ function createClient(options?: {
return { data: options?.sessionData ?? null }
},
status: async () => ({ data: {} }),
revert: async (params: Record<string, unknown>) => {
reverted.push(params)
if (options?.revertDeferred) return options.revertDeferred.promise
return { data: mkSession({ messageID: String(params.messageID) }) }
},
promptAsync: async (params: Record<string, unknown>) => {
prompted.push(params)
return { data: undefined }
},
abort: async (params: { sessionID: string; directory?: string }) => {
aborted.push(params)
if (params.directory && options?.abortFailures?.includes(params.directory)) throw new Error("abort failed")
@@ -120,14 +149,21 @@ function createConnection(client: ReturnType<typeof createClient>) {
type ProviderInternals = {
connectionState: State
webview: { postMessage: (message: unknown) => Promise<unknown> } | null
currentSession: { id: string; directory?: string } | null
currentSession: { id: string; directory?: string; revert?: { messageID: string } } | null
contextSessionID: string | undefined
sessionDirectories: Map<string, string>
trackedSessionIds: Set<string>
checkpoints: Map<string, Promise<void>>
revisions: Map<string, { id: string; seq: number }>
streams: { push: (msg: PartUpdate) => void }
checkpoint: (sid: string, run: () => Promise<void>) => void
gatherEditorContext: () => Promise<Record<string, never>>
refreshSessionDetails: (sid: string, dir: string) => void
stopCurrentSessionProcesses: (next?: string) => void
handleEvent: (event: unknown, directory?: string) => void
handleAbort: (sid?: string) => Promise<void>
handleRevertSession: (sid: string, messageID: string) => Promise<void>
handleSendMessage: (text: string, messageID?: string, sessionID?: string) => Promise<void>
handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise<void>
handleDeleteSession: (sid: string) => Promise<void>
}
@@ -210,6 +246,240 @@ describe("KiloProvider.handleAbort", () => {
})
})
describe("KiloProvider revert ordering", () => {
it("unwraps the nested sync payload emitted by the live SSE endpoint", () => {
const event = unwrapSyncEvent({
type: "sync",
syncEvent: {
type: "session.updated.1",
id: "evt_clear",
seq: 0,
aggregateID: "sessionID",
data: { sessionID: "s1", info: { revert: null } },
},
})
expect(event).toEqual({
source: "sync",
id: "evt_clear",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
})
it("waits for an in-flight revert before submitting the replacement prompt", async () => {
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = async () => ({})
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
await Promise.resolve()
expect(client.reverted).toHaveLength(1)
expect(client.prompted).toHaveLength(0)
revert.resolve({ data: mkSession({ messageID: "m1" }) })
await send
expect(client.prompted).toHaveLength(1)
expect(client.prompted[0]?.sessionID).toBe("s1")
})
it("waits for a revert queued while the replacement prompt gathers context", async () => {
const context = defer<Record<string, never>>()
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = () => context.promise
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
context.resolve({})
await Promise.resolve()
await Promise.resolve()
expect(client.prompted).toHaveLength(0)
revert.resolve({ data: mkSession({ messageID: "m1" }) })
await send
expect(client.prompted).toHaveLength(1)
})
it("does not submit the replacement prompt when the revert fails", async () => {
const error = spyOn(console, "error").mockImplementation(() => {})
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = async () => ({})
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
revert.resolve({ error: new Error("revert failed") })
await send
expect(client.prompted).toHaveLength(0)
expect(sent).toContainEqual(expect.objectContaining({ type: "sendMessageFailed", messageID: "m2" }))
error.mockRestore()
})
it("does not restore a stale revert boundary after a newer clear update", () => {
const client = createClient()
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.trackedSessionIds.add("s1")
internal.handleEvent({
source: "sync",
id: "evt_000000000002",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
const count = sent.length
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } },
})
internal.handleEvent({
id: "evt_000000000001",
type: "session.updated",
properties: { sessionID: "s1", info: mkSession({ messageID: "m1" }) },
})
expect(internal.currentSession?.revert).toBeUndefined()
expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000002", seq: 0 })
expect(sent).toHaveLength(count)
expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } })
})
it("uses sequence ordering for workspace-replayed session updates", () => {
const client = createClient()
const { internal } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.trackedSessionIds.add("s1")
internal.handleEvent({
source: "sync",
id: "evt_ffffffffffff",
seq: 1,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } },
})
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 2,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
expect(internal.currentSession?.revert).toBeUndefined()
expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000001", seq: 2 })
})
it("publishes authoritative session state after a missed clear event", async () => {
const client = createClient({ sessionData: mkSession() })
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
await Promise.resolve()
await Promise.resolve()
expect(internal.currentSession?.revert).toBeUndefined()
expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } })
})
it("retries a focused session refresh after a concurrent session update", async () => {
const first = defer<{ data: unknown }>()
const second = defer<{ data: unknown }>()
let calls = 0
const client = createClient({
sessionGet: async () => {
calls += 1
return calls === 1 ? first.promise : second.promise
},
})
const { internal } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.trackedSessionIds.add("s1")
internal.refreshSessionDetails("s1", "/repo")
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { title: "updated" } },
})
first.resolve({ data: mkSession() })
await Bun.sleep(0)
expect(calls).toBe(2)
second.resolve({ data: { ...mkSession(), title: "updated" } })
await Bun.sleep(0)
expect(internal.currentSession?.id).toBe("s1")
expect(internal.currentSession?.revert).toBeUndefined()
})
it("ignores an older session refresh that resolves last", async () => {
const first = defer<{ data: unknown }>()
const second = defer<{ data: unknown }>()
let calls = 0
const client = createClient({
sessionGet: async () => {
calls += 1
return calls === 1 ? first.promise : second.promise
},
})
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
internal.refreshSessionDetails("s1", "/repo")
second.resolve({ data: mkSession() })
await Bun.sleep(0)
first.resolve({ data: mkSession({ messageID: "m1" }) })
await Bun.sleep(0)
expect(internal.currentSession?.revert).toBeUndefined()
expect(sent.filter((msg) => (msg as { type?: string }).type === "sessionUpdated")).toHaveLength(1)
})
it("ignores a session refresh superseded by a revert response", async () => {
const session = defer<{ data: unknown }>()
const client = createClient({ sessionGet: async () => session.promise })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
await internal.handleRevertSession("s1", "m1")
session.resolve({ data: mkSession() })
await Bun.sleep(0)
expect(internal.currentSession?.revert).toEqual({ messageID: "m1" })
})
})
describe("KiloProvider.handleLoadMessages / focus mode freshness", () => {
it("stops background processes for the previous session when switching sessions", async () => {
const client = createClient({
@@ -22,13 +22,32 @@ describe("model preview data collection line", () => {
expect(styles).toContain(".model-preview-data-line")
})
it("renders prompt training independently from the free badge", () => {
expect(selector).toMatch(
/<Show when=\{isFree\(model\)\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\)\}>/,
)
expect(preview).toMatch(
/<Show when=\{model\(\)\.isFree\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\(\)\)\}>/,
)
it("renders prompt training independently from the model badges", () => {
expect(selector).toContain("isDataCollectedModel(model)")
expect(preview).toContain("isDataCollectedModel(model())")
expect(agent).toContain("isDataCollectedModel(model)")
})
it("renders BYOK availability independently from training metadata", () => {
expect(selector).toContain("hasByok(model)")
expect(preview).toContain("hasByok(model())")
expect(agent).toContain("hasByok(model)")
expect(selector).toContain(">BYOK</Tag>")
expect(preview).toContain(">BYOK</span>")
expect(agent).toContain(">BYOK</span>")
})
it("shows BYOK instead of Free when both metadata fields are set", () => {
expect(selector).toContain("isFree(model) && !hasByok(model)")
expect(preview).toContain("model().isFree && !hasByok(model())")
expect(agent).toContain("model.isFree && !hasByok(model)")
})
it("uses neutral colors for the main model picker BYOK badge", () => {
expect(selector).toContain("model-selector-data-badge--byok")
expect(styles).toContain(".model-selector-data-badge--byok")
expect(styles).toContain("background: var(--vscode-badge-background) !important")
expect(styles).toContain("color: var(--vscode-badge-foreground) !important")
})
it("uses the book open check icon for all webview model data disclosures", () => {
@@ -8,6 +8,7 @@ import {
PROVIDER_ORDER,
freeDataLabel,
isDataCollectedModel,
hasByok,
isFree,
} from "../../webview-ui/src/components/shared/model-selector-utils"
@@ -119,6 +120,14 @@ describe("isDataCollectedModel", () => {
})
})
describe("hasByok", () => {
it("uses only explicit user BYOK metadata", () => {
expect(hasByok({ hasUserByokAvailable: true })).toBe(true)
expect(hasByok({ hasUserByokAvailable: false })).toBe(false)
expect(hasByok({})).toBe(false)
})
})
describe("buildTriggerLabel", () => {
it("returns resolved model name for non-kilo provider unchanged", () => {
expect(buildTriggerLabel("GPT-4o", "openai", undefined, null, false, "", true, labels)).toBe("GPT-4o")
@@ -40,16 +40,14 @@ describe("revert session synchronization", () => {
expect(unrevert).toContain('type: "sessionUpdated"')
})
it("distinguishes partial sync patches from full bus snapshots", () => {
it("uses ordered sync patches instead of duplicate bus snapshots", () => {
expect(provider).toMatch(/source: "sync"/)
expect(provider).toMatch(
/if \(event\.type === "session\.updated"\) return "source" in event && event\.source === "sync"/,
)
expect(provider).toMatch(/if \(isFullSessionUpdatedEvent\(event\)\) return/)
expect(provider).toMatch(
/isLegacySyncEvent\(event\)\s*\? applySessionPatch\(this\.currentSession, event\.properties\.info\)\s*:\s*event\.properties\.info/,
)
expect(provider).toMatch(
/isFullSessionUpdatedEvent\(event\)\s*\? \{ type: "sessionUpdated" as const, session: this\.sessionToWebview\(event\.properties\.info\) \}/,
/this\.setCurrentSession\(applySessionPatch\(this\.currentSession, event\.properties\.info\)\)/,
)
})
})
@@ -6,8 +6,10 @@ import {
queuedUserMessageIDs,
stableMessageTurns,
visibleMessages,
visibleParts,
type RevertBoundary,
} from "../../webview-ui/src/context/session-queue"
import type { Message, SessionStatusInfo } from "../../webview-ui/src/types/messages"
import type { Message, Part, SessionStatusInfo } from "../../webview-ui/src/types/messages"
const base = {
sessionID: "session",
@@ -30,10 +32,12 @@ const assistant = (id: string, parentID: string, opts: Partial<Message> = {}): M
...opts,
})
const layout = (messages: Message[], status: SessionStatusInfo, boundary?: string) => {
const part = (id: string, messageID: string): Part => ({ id, messageID, type: "text", text: id })
const layout = (messages: Message[], status: SessionStatusInfo, revert?: RevertBoundary) => {
const active = activeUserMessageID(messages, status)
return partitionTurns(
messageTurns(messages, boundary),
messageTurns(messages, revert),
new Set(active ? [active] : []),
new Set(queuedUserMessageIDs(messages, status)),
)
@@ -292,7 +296,7 @@ describe("partitionTurns", () => {
it("does not render an active turn hidden by a revert boundary", () => {
const messages = [user("message_1"), assistant("message_2", "message_1", { finish: "stop" }), user("message_3")]
const result = layout(messages, { type: "busy" }, "message_3")
const result = layout(messages, { type: "busy" }, { messageID: "message_3" })
expect(result.virtual.map((turn) => turn.user.id)).toEqual(["message_1"])
expect(result.direct).toEqual([])
@@ -394,7 +398,45 @@ describe("messageTurns", () => {
assistant("message_4", "message_3"),
]
expect(messageTurns(messages, "message_3").map((turn) => turn.user.id)).toEqual(["message_1"])
expect(messageTurns(messages, { messageID: "message_3" }).map((turn) => turn.user.id)).toEqual(["message_1"])
})
it("keeps the part-boundary assistant and hides later provider errors", () => {
const messages = [
user("message_1"),
assistant("message_2", "message_1"),
assistant("message_3", "message_1", { error: { name: "ProviderError" } }),
assistant("message_4", "message_1", { error: { name: "ProviderError" } }),
]
const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" })
expect(turns).toHaveLength(1)
expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"])
})
it("applies assistant boundaries by id when messages arrive out of order", () => {
const messages = [
user("message_1"),
assistant("message_4", "message_1", { error: { name: "ProviderError" } }),
assistant("message_2", "message_1"),
]
const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" })
expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"])
})
})
describe("visibleParts", () => {
it("keeps only parts before the active part boundary", () => {
const parts = [part("part_1", "message_2"), part("part_2", "message_2"), part("part_3", "message_2")]
expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_2" })).toEqual([parts[0]])
})
it("fails closed when the boundary part is unavailable", () => {
const parts = [part("part_1", "message_2")]
expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_missing" })).toEqual([])
})
})
@@ -407,7 +449,10 @@ describe("visibleMessages", () => {
assistant("message_4", "message_3"),
]
expect(visibleMessages(messages, "message_3").map((msg) => msg.id)).toEqual(["message_1", "message_2"])
expect(visibleMessages(messages, { messageID: "message_3" }).map((msg) => msg.id)).toEqual([
"message_1",
"message_2",
])
})
it("keeps leading partial assistant output", () => {
@@ -1,18 +1,53 @@
/**
* SessionTerminalManager tests.
*
* The class is tightly coupled to VS Code terminal APIs. We use ts-morph
* static analysis to verify structural invariants that protect against
* real regressions focusing on ordering constraints and cleanup logic
* that are easy to break during refactoring.
* Structural tests use ts-morph to protect ordering and cleanup invariants.
* Command behavior tests exercise the narrow TerminalHost interface directly.
*/
import { describe, it, expect } from "bun:test"
import path from "node:path"
import { Project, SyntaxKind } from "ts-morph"
import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager"
const ROOT = path.resolve(import.meta.dir, "../..")
const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts")
const COMMAND = "workbench.action.togglePanel"
type Handler = (...args: unknown[]) => Promise<unknown>
function runtime(run: () => Promise<unknown>) {
let blocked = false
const handlers = new Map<string, Handler>()
const host: TerminalHost = {
createTerminal() {
throw new Error("not used")
},
activeTerminal: () => undefined,
repoPath: () => undefined,
showWarning() {},
setContext() {},
onTerminalClosed: () => ({ dispose() {} }),
onActiveTerminalChanged: () => ({ dispose() {} }),
registerCommand(id, handler) {
if (blocked && id === COMMAND) throw new Error(`command '${id}' already exists`)
handlers.set(id, handler)
return {
dispose() {
if (handlers.get(id) === handler) handlers.delete(id)
},
}
},
executeCommand() {
blocked = true
return run()
},
}
const manager = new SessionTerminalManager(() => {}, host)
const handler = handlers.get(COMMAND)
if (!handler) throw new Error(`command '${COMMAND}' was not registered`)
return { manager, handler }
}
function getClass() {
const project = new Project({ compilerOptions: { allowJs: true } })
@@ -88,8 +123,35 @@ describe("SessionTerminalManager structure", () => {
expect(text).toContain("this.showExistingLocal()")
})
it("panel command registration is best effort", () => {
const text = body("tryRegisterCommand")
expect(text).toContain("this.host.registerCommand")
expect(text).toContain("catch (err)")
expect(text).toContain("panel command registration skipped")
})
it("exposes active terminal state for terminal context routing", () => {
const text = body("hasActiveTerminal")
expect(text).toContain("this.host.activeTerminal()")
})
})
describe("SessionTerminalManager command restoration", () => {
it("preserves the original command result when re-registration fails", async () => {
const expected = { status: "complete" }
const state = runtime(async () => expected)
expect(await state.handler()).toBe(expected)
state.manager.dispose()
})
it("preserves the original command error when re-registration fails", async () => {
const expected = new Error("panel command failed")
const state = runtime(async () => {
throw expected
})
await expect(state.handler()).rejects.toBe(expected)
state.manager.dispose()
})
})
@@ -0,0 +1,72 @@
import { describe, expect, it } from "bun:test"
import path from "node:path"
const ROOT = path.resolve(import.meta.dir, "../..")
describe("task tool index identity", () => {
it("preserves a rendered child tool row across status updates", () => {
const result = Bun.spawnSync(
[
"bun",
"--conditions=browser",
"-e",
`
import { Window } from "happy-dom"
const window = new Window()
globalThis.window = window
globalThis.document = window.document
globalThis.Node = window.Node
const { Index, createComponent, createRenderEffect } = await import("solid-js")
const { createStore } = await import("solid-js/store")
const { render } = await import("solid-js/web")
const { reconcileSessionToolParts, upsertSessionToolPart } = await import(
"./webview-ui/src/context/session-utils.ts"
)
const running = {
id: "tool-1",
type: "tool",
tool: "read",
state: { status: "running", input: { filePath: "src/app.ts" }, title: "Reading" },
}
const completed = {
id: "tool-1",
type: "tool",
tool: "read",
state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" },
}
const root = document.createElement("div")
const [store, setStore] = createStore({ tools: [running] })
const dispose = render(
() =>
createComponent(Index, {
get each() {
return store.tools
},
children: (item) => {
const row = document.createElement("div")
createRenderEffect(() => {
row.textContent = item().state.status
})
return row
},
}),
root,
)
const row = root.firstElementChild
const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" })
setStore("tools", reconcileSessionToolParts(tools))
if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced")
if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated")
dispose()
`,
],
{ cwd: ROOT, stdout: "pipe", stderr: "pipe" },
)
const output = result.stdout.toString() + result.stderr.toString()
expect(result.exitCode, output).toBe(0)
})
})
@@ -84,6 +84,32 @@ describe("transcriptRows", () => {
expect(rows.at(-1)).toMatchObject({ type: "error", message: a3, error: a3.error })
})
it("hides provider errors at and after an assistant part boundary", () => {
const revert = { messageID: "message_2", partID: "part_2" }
const u1 = user("message_1")
const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } })
const a2 = assistant("message_3", "message_1", { error: { name: "ProviderError" } })
const parts = { message_2: [part("part_1", "message_2"), part("part_2", "message_2")] }
const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert })
expect(rows.map((row) => row.type)).toEqual(["user", "assistant"])
expect(rows.filter((row) => row.type === "assistant").flatMap((row) => row.parts.map((item) => item.id))).toEqual([
"part_1",
])
expect(rows.some((row) => row.message.id === "message_3")).toBe(false)
})
it("keeps provider errors before an assistant part boundary", () => {
const revert = { messageID: "message_3", partID: "part_2" }
const u1 = user("message_1")
const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } })
const a2 = assistant("message_3", "message_1")
const parts = { message_3: [part("part_1", "message_3"), part("part_2", "message_3")] }
const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert })
expect(rows.at(-1)).toMatchObject({ type: "error", message: a1 })
})
it("keeps keys stable when older turns are prepended and parts are appended", () => {
const u1 = user("u1")
const a1 = assistant("a1", "u1")
@@ -131,7 +157,7 @@ describe("transcriptRows", () => {
})
const a2 = assistant("a2", "u1")
const u3 = user("u3")
const turns = messageTurns([u1, a1, u2, a2, u3], "u3")
const turns = messageTurns([u1, a1, u2, a2, u3], { messageID: "u3" })
const rows = transcriptRows(turns, (id) => (id === "u2" ? (u2.parts ?? []) : []))
expect(rows.map((row) => `${row.turn}:${row.message.id}`)).toEqual(["u1:u1", "u1:a1", "u2:u2", "u2:a2"])
@@ -7,6 +7,7 @@ import { useLanguage } from "../src/context/language"
import {
KILO_GATEWAY_ID,
freeDataLabel,
hasByok,
isDataCollectedModel,
providerSortKey,
} from "../src/components/shared/model-selector-utils"
@@ -115,9 +116,14 @@ export const MultiModelSelector: Component<{
}
/>
<span class="am-mm-item-name">{model.name}</span>
<Show when={model.isFree}>
<Show when={model.isFree || hasByok(model) || isDataCollectedModel(model)}>
<span class="am-mm-free-data">
<span class="am-mm-free-badge">{freeLabel()}</span>
<Show when={model.isFree && !hasByok(model)}>
<span class="am-mm-free-badge">{freeLabel()}</span>
</Show>
<Show when={hasByok(model)}>
<span class="am-mm-byok-badge">BYOK</span>
</Show>
<Show when={isDataCollectedModel(model)}>
<Tooltip value={dataLabel()} placement="top">
<span class="am-mm-free-data-icon" aria-label={dataLabel()}>
@@ -151,8 +151,9 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
</Show>
<ContextMenu>
<HoverCard
openDelay={100}
closeDelay={100}
class="am-worktree-hover-card"
openDelay={50}
closeDelay={50}
placement="right-start"
gutter={8}
open={hovered() && !overClose() && !props.pendingDelete}
@@ -7,6 +7,47 @@
overflow: hidden;
}
/* Virtua already removes off-screen transcript rows. Avoid a second layer of
layout skipping, which can leave Chromium accessibility traversing fragments
whose layout items were discarded while Agent Manager scrolls. */
.am-layout
:is(
[data-component="assistant-message"],
[data-component="tool-trigger"],
[data-component="file"],
[data-component="code"],
[data-component="diff"]
) {
content-visibility: visible;
}
/* Generated separators become accessibility text nodes. Keep them out of
concurrently updating Agent Manager tool rows while preserving sidebar UI. */
html[data-theme="kilo-vscode"]
.am-layout
[data-component="tool-part-wrapper"][data-part-type="tool"]
[data-component="collapsible"]
[data-slot="basic-tool-tool-title"]
+ :is(
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"],
[data-slot="message-part-meta-line"],
[data-slot="webfetch-meta"],
[data-component="shell-submessage"]
)::before,
html[data-theme="kilo-vscode"]
.am-layout
[data-component="tool-part-wrapper"][data-part-type="tool"]
[data-component="collapsible"]
[data-slot="message-part-title-text"]
+ :is([data-slot="message-part-meta-line"], [data-slot="basic-tool-tool-subtitle"])::before,
html[data-theme="kilo-vscode"]
.am-layout
[data-component="tool-part-wrapper"][data-part-type="tool"]
[data-slot="context-tool-group-summary"]::before {
content: none;
}
.am-sidebar {
position: relative;
min-width: 200px;
@@ -2891,18 +2932,27 @@ body.am-wt-dragging-active * {
white-space: nowrap;
}
.am-mm-free-badge {
.am-mm-free-badge,
.am-mm-byok-badge {
display: inline-flex;
align-items: center;
padding: 1px 4px;
border-radius: var(--radius-sm);
color: #fff;
background: var(--vscode-testing-iconPassed, #4caf50);
font-size: var(--kilo-font-size-10);
overflow: hidden;
text-overflow: ellipsis;
}
.am-mm-free-badge {
color: #fff;
background: var(--vscode-testing-iconPassed, #4caf50);
}
.am-mm-byok-badge {
color: var(--vscode-badge-foreground);
background: var(--vscode-badge-background);
}
.am-mm-free-data-icon {
display: inline-flex;
align-items: center;
@@ -3076,6 +3126,14 @@ body.am-wt-dragging-active * {
/* HoverCard popover for worktree items */
.am-worktree-hover-card[data-expanded] {
animation-duration: 80ms;
}
.am-worktree-hover-card[data-closed] {
animation-duration: 60ms;
}
.am-hover-card {
padding: 10px 12px;
min-width: 160px;
@@ -332,6 +332,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
suggestions={standaloneSuggestions}
readonly={props.readonly}
emptyState={props.emptyState}
announce={isSidebar()}
/>
</div>
</div>
@@ -63,6 +63,8 @@ interface MessageListProps {
readonly?: boolean
/** Optionally replace the standard welcome content while the conversation is empty. */
emptyState?: () => JSX.Element
/** Announce transcript changes as a live log. Disable for multi-session surfaces with concurrent streams. */
announce?: boolean
}
export const MessageList: Component<MessageListProps> = (props) => {
@@ -73,6 +75,19 @@ export const MessageList: Component<MessageListProps> = (props) => {
const autoScroll = createAutoScroll({
working: () => session.status() !== "idle",
})
const [announcement, setAnnouncement] = createSignal("")
createEffect(
(prev: { sid?: string; working: boolean }) => {
const sid = session.currentSessionID()
const working = session.status() !== "idle"
if (working && (!prev.working || prev.sid !== sid)) setAnnouncement(language.t("session.status.working"))
if (!working && prev.working && prev.sid === sid) {
setAnnouncement(language.t("settings.agentBehaviour.editMode.save"))
}
return { sid, working }
},
{ sid: undefined, working: false },
)
// Explicit output-producing actions resume auto-scroll before appending.
const onResumeAutoScroll = () => autoScroll.resume()
@@ -91,14 +106,14 @@ export const MessageList: Component<MessageListProps> = (props) => {
const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle>()
const [layout, setLayout] = createSignal("")
const boundary = () => session.revert()?.messageID
const revert = () => session.revert() ?? undefined
const turns = createMemo((prev: MessageTurn[] | undefined) =>
stableMessageTurns(
messageTurns(session.messages(), boundary(), (msg) => session.getParts(msg.id)),
messageTurns(session.messages(), revert(), (msg) => session.getParts(msg.id)),
prev,
),
)
const isEmpty = () => turns().length === 0 && !session.loading() && !boundary()
const isEmpty = () => turns().length === 0 && !session.loading() && !revert()
const activeUserID = createMemo(() =>
getActiveUserMessageID(session.messages(), session.statusInfo(), (msg) => session.getParts(msg.id)),
@@ -115,6 +130,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
queued: queuedIDs(),
live: new Set(active ? [active] : []),
hidden: session.isErrorHidden,
revert: revert(),
},
prev,
)
@@ -260,13 +276,25 @@ export const MessageList: Component<MessageListProps> = (props) => {
return (
<div class="message-list-container">
<Show when={props.announce === false}>
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
{announcement()}
</div>
</Show>
<Show when={isEmpty()}>
<div class="welcome-header">
<AccountSwitcher class="account-switcher-welcome" />
<KiloNotifications />
</div>
</Show>
<div ref={setScrollRef} onScroll={handleScroll} class="message-list" role="log" aria-live="polite">
<div
ref={setScrollRef}
onScroll={handleScroll}
class="message-list"
role={props.announce === false ? undefined : "log"}
aria-live={props.announce === false ? undefined : "polite"}
aria-busy={props.announce === false && session.status() !== "idle" ? "true" : undefined}
>
<div ref={autoScroll.contentRef} class={isEmpty() ? "message-list-content-empty" : "message-list-content"}>
<Show when={session.loading()}>
<div class="message-list-loading" role="status">
@@ -326,7 +354,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
</For>
</div>
</Show>
<Show when={boundary()}>
<Show when={revert()}>
<RevertBanner />
</Show>
<For each={partition().queued}>{(row) => <TranscriptRowView row={row} />}</For>
@@ -7,7 +7,7 @@
* Call registerExpandedTaskTool() once at app startup to activate.
*/
import { Component, createEffect, createMemo, createSignal, For, Show, onCleanup } from "solid-js"
import { Component, createEffect, createMemo, createSignal, Index, Show, onCleanup } from "solid-js"
import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part"
import { BasicTool, initialOpen } from "@kilocode/kilo-ui/basic-tool"
import { Icon } from "@kilocode/kilo-ui/icon"
@@ -141,15 +141,13 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
</div>
</Show>
<Show when={result()}>{(text) => <Markdown text={text()} />}</Show>
<For each={childToolParts()}>
<Index each={childToolParts()}>
{(item) => {
const info = createMemo(() => getToolInfo(item.tool, item.state?.input))
const info = createMemo(() => getToolInfo(item().tool, item().state?.input))
const subtitle = createMemo(() => {
if (info().subtitle) return info().subtitle
const state = item.state as { status: string; title?: string }
if (state.status === "completed" || state.status === "running") {
return state.title
}
const state = item().state as { status: string; title?: string }
if (state.status === "completed" || state.status === "running") return state.title
return undefined
})
return (
@@ -162,7 +160,7 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
</div>
)
}}
</For>
</Index>
</div>
</div>
</BasicTool>
@@ -5,7 +5,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { useLanguage } from "../../context/language"
import { freeDataLabel, isDataCollectedModel, sanitizeName } from "./model-selector-utils"
import { freeDataLabel, hasByok, isDataCollectedModel, sanitizeName } from "./model-selector-utils"
import { avgPrice, fmtAttemptCost, fmtCachedPrice, fmtPrice, fmtTerminalBenchScore } from "./model-preview-utils"
interface Props {
@@ -91,11 +91,14 @@ export const ModelPreview: Component<Props> = (props) => {
})()}
</Show>
</div>
<Show when={model().isFree || isDataCollectedModel(model())}>
<Show when={model().isFree || hasByok(model()) || isDataCollectedModel(model())}>
<span class="model-preview-free-data">
<Show when={model().isFree}>
<Show when={model().isFree && !hasByok(model())}>
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
</Show>
<Show when={hasByok(model())}>
<span class="model-preview-badge">BYOK</span>
</Show>
<Show when={isDataCollectedModel(model())}>
<Tooltip value={dataLabel()} placement="top">
<span class="model-preview-free-data-icon" aria-label={dataLabel()}>
@@ -38,6 +38,7 @@ import {
providerSortKey,
isFree,
isDataCollectedModel,
hasByok,
freeDataLabel,
buildTriggerLabel,
sanitizeName,
@@ -936,13 +937,18 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
)
})()}
</span>
<Show when={isFree(model) || isDataCollectedModel(model)}>
<Show when={isFree(model) || hasByok(model) || isDataCollectedModel(model)}>
<span class="model-selector-free-data">
<Show when={isFree(model)}>
<Show when={isFree(model) && !hasByok(model)}>
<span class="model-selector-data-badge">
<Tag data-variant="member">{freeLabel()}</Tag>
</span>
</Show>
<Show when={hasByok(model)}>
<span class="model-selector-data-badge model-selector-data-badge--byok">
<Tag data-variant="member">BYOK</Tag>
</span>
</Show>
<Show when={isDataCollectedModel(model)}>
<Tooltip value={dataLabel()} placement="top">
<span class="model-selector-free-data-icon" aria-label={dataLabel()}>
@@ -26,6 +26,10 @@ export function isDataCollectedModel(model: Pick<EnrichedModel, "mayTrainOnYourP
return model.mayTrainOnYourPrompts === true
}
export function hasByok(model: Pick<EnrichedModel, "hasUserByokAvailable">): boolean {
return model.hasUserByokAvailable === true
}
export function freeDataLabel(_free: string, data: string): string {
return data
}
@@ -1,4 +1,6 @@
import type { Message, SessionStatusInfo } from "../types/messages"
import type { Message, Part, SessionInfo, SessionStatusInfo } from "../types/messages"
export type RevertBoundary = Pick<NonNullable<SessionInfo["revert"]>, "messageID" | "partID">
export interface MessageTurn {
id: string
@@ -55,24 +57,37 @@ function target(messages: Message[], index: number, id: string, parts?: (msg: Me
return id
}
function visibleMessage(id: string, revert?: RevertBoundary) {
if (!revert || id < revert.messageID) return true
return id === revert.messageID && !!revert.partID
}
export function visibleParts(id: string, parts: Part[], revert?: RevertBoundary) {
if (!revert || id < revert.messageID) return parts
if (id !== revert.messageID || !revert.partID) return []
const idx = parts.findIndex((part) => part.id === revert.partID)
return idx < 0 ? [] : parts.slice(0, idx)
}
export function messageTurns(
messages: Message[],
boundary?: string,
revert?: RevertBoundary,
parts?: (msg: Message) => Message["parts"],
): MessageTurn[] {
const result: MessageTurn[] = []
const lead: Message[] = []
const by = new Map<string, { turn: MessageTurn; index: number }>()
const projected = (msg: Message) => visibleParts(msg.id, parts?.(msg) ?? msg.parts ?? [], revert)
let compact: { turn: MessageTurn; index: number } | undefined
for (const msg of messages) {
if (!visibleMessage(msg.id, revert)) continue
if (msg.role === "user") {
if (boundary && msg.id >= boundary) break
const turn = { id: msg.id, user: msg, assistant: [] }
const item = { turn, index: result.length }
result.push(turn)
by.set(msg.id, item)
if (isCompact(msg, parts)) compact = item
if (isCompact(msg, projected)) compact = item
continue
}
@@ -105,10 +120,10 @@ export function messageTurns(
export function visibleMessages(
messages: Message[],
boundary?: string,
revert?: RevertBoundary,
parts?: (msg: Message) => Message["parts"],
): Message[] {
return messageTurns(messages, boundary, parts).flatMap((turn) =>
return messageTurns(messages, revert, parts).flatMap((turn) =>
turn.partial ? turn.assistant : [turn.user, ...turn.assistant],
)
}
@@ -1,3 +1,4 @@
import { reconcile } from "solid-js/store"
import type { Message, Part, ToolPart } from "../types/messages"
export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..."
@@ -85,6 +86,10 @@ export function buildSessionToolParts(
return tools
}
export function reconcileSessionToolParts(tools: ToolPart[]) {
return reconcile(tools, { key: "id" })
}
export function upsertSessionToolPart(
current: ToolPart[],
part: Part,
@@ -57,6 +57,7 @@ import {
buildCostBreakdown,
buildSessionToolParts,
childID,
reconcileSessionToolParts,
removeSessionToolPart,
removeSessionToolPartsForMessage,
upsertSessionToolPart,
@@ -1290,12 +1291,16 @@ export const SessionProvider: ParentComponent = (props) => {
return true
}
function setTools(sessionID: string, tools: ToolPart[]) {
setStore("toolParts", sessionID, reconcileSessionToolParts(tools))
}
function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record<string, Part[]>) {
const tools = buildSessionToolParts(
messages,
(msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts,
)
setStore("toolParts", sessionID, tools)
setTools(sessionID, tools)
}
function messageParts(messages: Message[]): Record<string, Part[]> {
@@ -1310,16 +1315,17 @@ export const SessionProvider: ParentComponent = (props) => {
const sid = sessionID ?? part.sessionID
if (!sid) return
if (part.type !== "tool") return
setStore("toolParts", sid, (tools = []) => upsertSessionToolPart(tools, part, { id: messageID, sessionID: sid }))
const tools = upsertSessionToolPart(store.toolParts[sid] ?? [], part, { id: messageID, sessionID: sid })
setTools(sid, tools)
}
function dropToolPart(sessionID: string | undefined, partID: string) {
if (!sessionID) return
setStore("toolParts", sessionID, (tools = []) => removeSessionToolPart(tools, partID))
setTools(sessionID, removeSessionToolPart(store.toolParts[sessionID] ?? [], partID))
}
function dropMessageTools(sessionID: string, messageID: string) {
setStore("toolParts", sessionID, (tools = []) => removeSessionToolPartsForMessage(tools, messageID))
setTools(sessionID, removeSessionToolPartsForMessage(store.toolParts[sessionID] ?? [], messageID))
}
function handleMessagesLoaded(
@@ -2550,8 +2556,10 @@ export const SessionProvider: ParentComponent = (props) => {
const userMessages = createMemo(() => messages().filter((m) => m.role === "user"))
function visible(sessionID: string) {
return filterVisibleMessages(store.messages[sessionID] ?? [], store.sessions[sessionID]?.revert?.messageID, (msg) =>
getParts(msg.id),
return filterVisibleMessages(
store.messages[sessionID] ?? [],
store.sessions[sessionID]?.revert ?? undefined,
(msg) => getParts(msg.id),
)
}
@@ -1,5 +1,5 @@
import type { Message, Part } from "../types/messages"
import type { MessageTurn } from "./session-queue"
import { visibleParts, type MessageTurn, type RevertBoundary } from "./session-queue"
interface TranscriptMeta {
turn: string
@@ -46,6 +46,7 @@ export interface TranscriptOptions {
queued?: ReadonlySet<string>
live?: ReadonlySet<string>
hidden?: (id: string) => boolean
revert?: RevertBoundary
}
export interface TranscriptPartition {
@@ -127,6 +128,8 @@ export function transcriptRows(
): TranscriptRow[] {
const size = Math.max(1, Math.floor(opts.size ?? 8))
const rows: TranscriptRow[] = []
const parts = (id: string) => visibleParts(id, getParts(id), opts.revert)
const terminal = (msg: Message) => !(opts.revert?.partID && msg.id === opts.revert.messageID)
for (const turn of turns) {
const meta = {
@@ -135,7 +138,7 @@ export function transcriptRows(
queued: opts.queued?.has(turn.id) === true,
live: opts.live?.has(turn.id) === true,
}
const copied = copy(turn.assistant, getParts)
const copied = copy(turn.assistant, parts)
if (!turn.partial) {
rows.push({
@@ -143,27 +146,27 @@ export function transcriptRows(
type: "user",
key: `${turn.id}:user`,
message: turn.user,
parts: getParts(turn.user.id),
interrupted: turn.assistant.some((msg) => msg.error?.name === "MessageAbortedError"),
parts: parts(turn.user.id),
interrupted: turn.assistant.some((msg) => terminal(msg) && msg.error?.name === "MessageAbortedError"),
answered: turn.assistant.length > 0,
})
}
for (const msg of turn.assistant) {
const parts = getParts(msg.id)
if (parts.length === 0) {
const visible = parts(msg.id)
if (visible.length === 0) {
rows.push({
...meta,
type: "assistant",
key: `${turn.id}:assistant:${msg.id}:empty`,
message: msg,
parts,
parts: visible,
copy: copied,
})
continue
}
for (let start = 0; start < parts.length; start += size) {
const chunk = parts.slice(start, start + size)
for (let start = 0; start < visible.length; start += size) {
const chunk = visible.slice(start, start + size)
rows.push({
...meta,
type: "assistant",
@@ -181,7 +184,7 @@ export function transcriptRows(
}
const failed = turn.assistant.find(
(msg) => msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true,
(msg) => terminal(msg) && msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true,
)
if (failed?.error) {
rows.push({ ...meta, type: "error", key: `${turn.id}:error:${failed.id}`, message: failed, error: failed.error })
+3 -3
View File
@@ -811,7 +811,7 @@ export const dict = {
"settings.providers.tag.customProvider": "مزود مخصص",
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
"settings.providers.action.signInChatGPT": "تسجيل الدخول باستخدام ChatGPT",
"settings.providers.custom.description": "أضف مزوداً متوافقاً مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.custom.description": "أضف مزودًا مخصصًا عبر عنوان URL الأساسي.",
"settings.providers.subagentModel.title": "نموذج الوكيل الفرعي",
"settings.providers.subagentModel.description":
"النموذج الافتراضي وجهد التفكير للوكلاء الفرعيين لـ task-tool. اتركه فارغًا ليرث نموذج الوكيل المستدعي.",
@@ -819,11 +819,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"إخفاء نماذج Kilo Gateway التي قد يستخدم مزودوها مطالباتك لأغراض التدريب.",
"settings.providers.modeModels": "نموذج لكل وضع",
"settings.providers.custom.note": "أضف موفرًا متوافقًا مع OpenAI عبر عنوان URL الأساسي.",
"settings.providers.custom.note": "أضف مزودًا مخصصًا عبر عنوان URL الأساسي.",
"settings.providers.modeModels.description":
"تجاوز النموذج الافتراضي لأوضاع محددة. إذا لم يتم التعيين، يتم استخدام النموذج الافتراضي العام.",
"provider.custom.title": "مزود مخصص",
"provider.custom.description.prefix": "قم بتكوين مزود متوافق مع OpenAI. انظر ",
"provider.custom.description.prefix": "قم بتكوين مزود مخصص. انظر ",
"provider.custom.description.link": "وثائق تكوين المزود",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "معرف المزود",
+3 -3
View File
@@ -823,7 +823,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Provedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado a partir das suas variáveis de ambiente",
"settings.providers.action.signInChatGPT": "Entrar com ChatGPT",
"settings.providers.custom.description": "Adicione um provedor compatível com OpenAI pela URL base.",
"settings.providers.custom.description": "Adicione um provedor personalizado pela URL base.",
"settings.providers.subagentModel.title": "Modelo de Subagente",
"settings.providers.subagentModel.description":
"Modelo padrão e esforço de raciocínio para subagentes do task-tool. Deixe em branco para herdar o modelo do agente chamador.",
@@ -831,11 +831,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Oculte os modelos do Kilo Gateway cujos provedores podem usar seus prompts para treinamento.",
"settings.providers.modeModels": "Modelo por Modo",
"settings.providers.custom.note": "Adicione um provedor compatível com OpenAI por URL base.",
"settings.providers.custom.note": "Adicione um provedor personalizado por URL base.",
"settings.providers.modeModels.description":
"Substitua o modelo padrão para modos específicos. Se não definido, o modelo padrão global é usado.",
"provider.custom.title": "Provedor personalizado",
"provider.custom.description.prefix": "Configure um provedor compatível com OpenAI. Veja a ",
"provider.custom.description.prefix": "Configure um provedor personalizado. Veja a ",
"provider.custom.description.link": "documentação de configuração de provedores",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID do provedor",
+3 -3
View File
@@ -864,7 +864,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Prilagođeni provajder",
"settings.providers.connected.environmentDescription": "Povezano iz vaših varijabli okruženja",
"settings.providers.action.signInChatGPT": "Prijavi se putem ChatGPT",
"settings.providers.custom.description": "Dodaj OpenAI-kompatibilan provajder putem osnovnog URL-a.",
"settings.providers.custom.description": "Dodaj prilagođeni provajder putem osnovnog URL-a.",
"settings.providers.subagentModel.title": "Model podagenta",
"settings.providers.subagentModel.description":
"Zadani model i napor zaključivanja za podagente task-tool-a. Ostavite nepodešeno da naslijedi model pozivnog agenta.",
@@ -872,11 +872,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Sakrij Kilo Gateway modele čiji pružaoci mogu koristiti vaše promptove za treniranje.",
"settings.providers.modeModels": "Model po režimu",
"settings.providers.custom.note": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
"settings.providers.custom.note": "Dodajte prilagođenog provajdera putem osnovnog URL-a.",
"settings.providers.modeModels.description":
"Zamijenite podrazumijevani model za određene režime. Ako nije postavljeno, koristi se globalni podrazumijevani model.",
"provider.custom.title": "Prilagođeni provajder",
"provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilan provajder. Pogledaj ",
"provider.custom.description.prefix": "Konfiguriši prilagođeni provajder. Pogledaj ",
"provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID provajdera",
+3 -3
View File
@@ -858,7 +858,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Brugerdefineret udbyder",
"settings.providers.connected.environmentDescription": "Forbundet fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Log ind med ChatGPT",
"settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.custom.description": "Tilføj en brugerdefineret udbyder via basis-URL.",
"settings.providers.subagentModel.title": "Underagentmodel",
"settings.providers.subagentModel.description":
"Standardmodel og ræsonnementsindsats for task-tool-underagenter. Lad den være tom for at nedarve den kaldende agents model.",
@@ -866,11 +866,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Skjul Kilo Gateway-modeller, hvis udbydere muligvis bruger dine prompts til træning.",
"settings.providers.modeModels": "Model pr. tilstand",
"settings.providers.custom.note": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
"settings.providers.custom.note": "Tilføj en brugerdefineret udbyder via basis-URL.",
"settings.providers.modeModels.description":
"Tilsidesæt standardmodellen for bestemte tilstande. Hvis ikke angivet, bruges den globale standardmodel.",
"provider.custom.title": "Brugerdefineret udbyder",
"provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel udbyder. Se ",
"provider.custom.description.prefix": "Konfigurer en brugerdefineret udbyder. Se ",
"provider.custom.description.link": "dokumentation for udbyderkonfiguration",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Udbyder-ID",
+3 -3
View File
@@ -870,7 +870,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Benutzerdefinierter Anbieter",
"settings.providers.connected.environmentDescription": "Verbunden über Ihre Umgebungsvariablen",
"settings.providers.action.signInChatGPT": "Mit ChatGPT anmelden",
"settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter über die Basis-URL hinzu.",
"settings.providers.custom.description": "Fügen Sie einen benutzerdefinierten Anbieter über die Basis-URL hinzu.",
"settings.providers.subagentModel.title": "Subagenten-Modell",
"settings.providers.subagentModel.description":
"Standardmodell und Aufwand für Schlussfolgerungen für task-tool-Subagenten. Leer lassen, um das Modell des aufrufenden Agenten zu übernehmen.",
@@ -878,11 +878,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Blendet Kilo-Gateway-Modelle aus, deren Anbieter Ihre Prompts möglicherweise zum Training verwenden.",
"settings.providers.modeModels": "Modell pro Modus",
"settings.providers.custom.note": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
"settings.providers.custom.note": "Fügen Sie einen benutzerdefinierten Anbieter per Basis-URL hinzu.",
"settings.providers.modeModels.description":
"Überschreiben Sie das Standardmodell für bestimmte Modi. Wenn nicht festgelegt, wird das globale Standardmodell verwendet.",
"provider.custom.title": "Benutzerdefinierter Anbieter",
"provider.custom.description.prefix": "Konfigurieren Sie einen OpenAI-kompatiblen Anbieter. Siehe die ",
"provider.custom.description.prefix": "Konfigurieren Sie einen benutzerdefinierten Anbieter. Siehe die ",
"provider.custom.description.link": "Anbieterkonfigurationsdokumentation",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Anbieter-ID",
@@ -792,10 +792,10 @@ export const dict = {
"settings.providers.tag.other": "Other",
"settings.providers.connected.environmentDescription": "Connected from your environment variables",
"settings.providers.action.signInChatGPT": "Sign in with ChatGPT",
"settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.",
"settings.providers.custom.description": "Add a custom provider by base URL.",
"provider.custom.title": "Custom provider",
"provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ",
"provider.custom.description.prefix": "Configure a custom provider. See the ",
"provider.custom.description.link": "provider config docs",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Provider ID",
@@ -1527,7 +1527,7 @@ export const dict = {
"settings.providers.enabled": "Enabled Providers (Allowlist)",
"settings.providers.enabled.description": "If set, only these providers will be available (exclusive allowlist)",
"settings.providers.notSet": "Not set (use server default)",
"settings.providers.custom.note": "Add an OpenAI-compatible provider by base URL.",
"settings.providers.custom.note": "Add a custom provider by base URL.",
"settings.providers.search.placeholder": "Search providers",
"settings.providers.select.placeholder": "Select provider...",
+3 -3
View File
@@ -866,7 +866,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Proveedor personalizado",
"settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
"settings.providers.action.signInChatGPT": "Iniciar sesión con ChatGPT",
"settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por URL base.",
"settings.providers.custom.description": "Añade un proveedor personalizado por URL base.",
"settings.providers.subagentModel.title": "Modelo de subagente",
"settings.providers.subagentModel.description":
"Modelo predeterminado y esfuerzo de razonamiento para los subagentes de task-tool. Déjelo sin configurar para heredar el modelo del agente invocador.",
@@ -874,11 +874,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Oculta los modelos de Kilo Gateway cuyos proveedores pueden usar tus prompts para entrenamiento.",
"settings.providers.modeModels": "Modelo por modo",
"settings.providers.custom.note": "Agrega un proveedor compatible con OpenAI mediante URL base.",
"settings.providers.custom.note": "Agrega un proveedor personalizado mediante URL base.",
"settings.providers.modeModels.description":
"Anula el modelo predeterminado para modos específicos. Si no se establece, se usa el modelo predeterminado global.",
"provider.custom.title": "Proveedor personalizado",
"provider.custom.description.prefix": "Configura un proveedor compatible con OpenAI. Consulta la ",
"provider.custom.description.prefix": "Configura un proveedor personalizado. Consulta la ",
"provider.custom.description.link": "documentación de configuración de proveedores",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID del proveedor",
+3 -3
View File
@@ -872,7 +872,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Fournisseur personnalisé",
"settings.providers.connected.environmentDescription": "Connecté depuis vos variables d'environnement",
"settings.providers.action.signInChatGPT": "Se connecter avec ChatGPT",
"settings.providers.custom.description": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.custom.description": "Ajoutez un fournisseur personnalisé par URL de base.",
"settings.providers.subagentModel.title": "Modèle de sous-agent",
"settings.providers.subagentModel.description":
"Modèle par défaut et effort de raisonnement pour les sous-agents du task-tool. Laissez vide pour hériter du modèle de l'agent appelant.",
@@ -880,11 +880,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Masquez les modèles Kilo Gateway dont les fournisseurs peuvent utiliser vos prompts à des fins d'entraînement.",
"settings.providers.modeModels": "Modèle par mode",
"settings.providers.custom.note": "Ajoutez un fournisseur compatible OpenAI par URL de base.",
"settings.providers.custom.note": "Ajoutez un fournisseur personnalisé par URL de base.",
"settings.providers.modeModels.description":
"Remplacez le modèle par défaut pour des modes spécifiques. Si non défini, le modèle par défaut global est utilisé.",
"provider.custom.title": "Fournisseur personnalisé",
"provider.custom.description.prefix": "Configurez un fournisseur compatible OpenAI. Voir la ",
"provider.custom.description.prefix": "Configurez un fournisseur personnalisé. Voir la ",
"provider.custom.description.link": "documentation de configuration des fournisseurs",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID du fournisseur",
+3 -3
View File
@@ -689,9 +689,9 @@ export const dict = {
"settings.providers.tag.other": "Altro",
"settings.providers.connected.environmentDescription": "Connesso dalle variabili d'ambiente",
"settings.providers.action.signInChatGPT": "Accedi con ChatGPT",
"settings.providers.custom.description": "Aggiungi un provider compatibile con OpenAI tramite URL base.",
"settings.providers.custom.description": "Aggiungi un provider personalizzato tramite URL base.",
"provider.custom.title": "Provider personalizzato",
"provider.custom.description.prefix": "Configura un provider compatibile con OpenAI. Vedi la ",
"provider.custom.description.prefix": "Configura un provider personalizzato. Vedi la ",
"provider.custom.description.link": "documentazione di configurazione provider",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID provider",
@@ -1371,7 +1371,7 @@ export const dict = {
"settings.providers.enabled.description":
"Se impostato, saranno disponibili solo questi provider (allowlist esclusiva)",
"settings.providers.notSet": "Non impostato (usa default server)",
"settings.providers.custom.note": "Aggiungi un provider compatibile con OpenAI tramite URL base.",
"settings.providers.custom.note": "Aggiungi un provider personalizzato tramite URL base.",
"settings.providers.search.placeholder": "Cerca provider",
"settings.providers.select.placeholder": "Seleziona provider...",
"dialog.model.notSet": "Non impostato",
+3 -3
View File
@@ -854,7 +854,7 @@ export const dict = {
"settings.providers.tag.customProvider": "カスタムプロバイダー",
"settings.providers.connected.environmentDescription": "環境変数から接続されています",
"settings.providers.action.signInChatGPT": "ChatGPT でサインイン",
"settings.providers.custom.description": "ベースURLでOpenAI互換プロバイダーを追加します。",
"settings.providers.custom.description": "ベース URL でカスタムプロバイダーを追加します。",
"settings.providers.subagentModel.title": "サブエージェントモデル",
"settings.providers.subagentModel.description":
"task-tool サブエージェントのデフォルトモデルと推論の労力。呼び出し元のエージェントのモデルを継承する場合は未設定のままにしてください。",
@@ -862,11 +862,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"プロバイダーがプロンプトを学習に使用する可能性のある Kilo Gateway モデルを非表示にします。",
"settings.providers.modeModels": "モードごとのモデル",
"settings.providers.custom.note": "Base URL で OpenAI 互換プロバイダーを追加します。",
"settings.providers.custom.note": "Base URL でカスタムプロバイダーを追加します。",
"settings.providers.modeModels.description":
"特定のモードのデフォルトモデルを上書きします。設定されていない場合、グローバルデフォルトモデルが使用されます。",
"provider.custom.title": "カスタムプロバイダー",
"provider.custom.description.prefix": "OpenAI互換プロバイダーを設定します。",
"provider.custom.description.prefix": "カスタムプロバイダーを設定します。",
"provider.custom.description.link": "プロバイダー設定ドキュメント",
"provider.custom.description.suffix": "を参照してください。",
"provider.custom.field.providerID.label": "プロバイダーID",
+3 -3
View File
@@ -816,7 +816,7 @@ export const dict = {
"settings.providers.tag.customProvider": "사용자 정의 공급자",
"settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
"settings.providers.action.signInChatGPT": "ChatGPT로 로그인",
"settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.custom.description": "기본 URL로 사용자 정의 공급자를 추가합니다.",
"settings.providers.subagentModel.title": "하위 에이전트 모델",
"settings.providers.subagentModel.description":
"task-tool 하위 에이전트의 기본 모델 및 추론 수준입니다. 호출하는 에이전트의 모델을 상속하려면 비워 두세요.",
@@ -824,11 +824,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"제공업체가 사용자의 프롬프트를 학습에 사용할 수 있는 Kilo Gateway 모델을 숨깁니다.",
"settings.providers.modeModels": "모드별 모델",
"settings.providers.custom.note": "Base URL로 OpenAI 호환 공급자를 추가합니다.",
"settings.providers.custom.note": "Base URL로 사용자 정의 공급자를 추가합니다.",
"settings.providers.modeModels.description":
"특정 모드의 기본 모델을 재정의합니다. 설정하지 않으면 전역 기본 모델이 사용됩니다.",
"provider.custom.title": "사용자 정의 공급자",
"provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ",
"provider.custom.description.prefix": "사용자 정의 공급자를 구성합니다. ",
"provider.custom.description.link": "공급자 구성 문서",
"provider.custom.description.suffix": "를 참조하세요.",
"provider.custom.field.providerID.label": "공급자 ID",
+3 -3
View File
@@ -826,10 +826,10 @@ export const dict = {
"settings.providers.tag.other": "Overige",
"settings.providers.connected.environmentDescription": "Gekoppeld via je omgevingsvariabelen",
"settings.providers.action.signInChatGPT": "Inloggen met ChatGPT",
"settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.",
"settings.providers.custom.description": "Voeg een aangepaste provider toe via basis-URL.",
"provider.custom.title": "Aangepaste provider",
"provider.custom.description.prefix": "Configureer een OpenAI-compatibele provider. Zie de ",
"provider.custom.description.prefix": "Configureer een aangepaste provider. Zie de ",
"provider.custom.description.link": "provider configuratie documentatie",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Provider-ID",
@@ -1521,7 +1521,7 @@ export const dict = {
"settings.providers.enabled.description":
"Indien ingesteld, zijn alleen deze providers beschikbaar (exclusieve allowlist)",
"settings.providers.notSet": "Niet ingesteld (gebruik server standaard)",
"settings.providers.custom.note": "Voeg een OpenAI-compatibele provider toe via base URL.",
"settings.providers.custom.note": "Voeg een aangepaste provider toe via base URL.",
"settings.providers.search.placeholder": "Zoek providers",
"settings.providers.select.placeholder": "Selecteer provider...",
+3 -3
View File
@@ -825,7 +825,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Egendefinert leverandør",
"settings.providers.connected.environmentDescription": "Koblet til fra dine miljøvariabler",
"settings.providers.action.signInChatGPT": "Logg inn med ChatGPT",
"settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.custom.description": "Legg til en egendefinert leverandør via basis-URL.",
"settings.providers.subagentModel.title": "Underagentmodell",
"settings.providers.subagentModel.description":
"Standardmodell og resonneringsinnsats for task-tool-underagenter. La stå tom for å arve den kallende agentens modell.",
@@ -833,11 +833,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Skjul Kilo Gateway-modeller der leverandørene kan bruke ledetekstene dine til trening.",
"settings.providers.modeModels": "Modell per modus",
"settings.providers.custom.note": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
"settings.providers.custom.note": "Legg til en egendefinert leverandør via basis-URL.",
"settings.providers.modeModels.description":
"Overstyr standardmodellen for bestemte moduser. Hvis ikke angitt, brukes den globale standardmodellen.",
"provider.custom.title": "Egendefinert leverandør",
"provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ",
"provider.custom.description.prefix": "Konfigurer en egendefinert leverandør. Se ",
"provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Leverandør-ID",
+3 -3
View File
@@ -823,7 +823,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Niestandardowy dostawca",
"settings.providers.connected.environmentDescription": "Połączony z twoich zmiennych środowiskowych",
"settings.providers.action.signInChatGPT": "Zaloguj przez ChatGPT",
"settings.providers.custom.description": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.custom.description": "Dodaj niestandardowego dostawcę przez bazowy URL.",
"settings.providers.subagentModel.title": "Model podagenta",
"settings.providers.subagentModel.description":
"Domyślny model i wysiłek wnioskowania dla podagentów task-tool. Pozostaw puste, aby odziedziczyć model agenta wywołującego.",
@@ -831,11 +831,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Ukryj modele Kilo Gateway, których dostawcy mogą używać Twoich promptów do trenowania.",
"settings.providers.modeModels": "Model na tryb",
"settings.providers.custom.note": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.",
"settings.providers.custom.note": "Dodaj niestandardowego dostawcę przez bazowy URL.",
"settings.providers.modeModels.description":
"Zastąp domyślny model dla określonych trybów. Jeśli nie ustawiono, używany jest globalny domyślny model.",
"provider.custom.title": "Niestandardowy dostawca",
"provider.custom.description.prefix": "Skonfiguruj dostawcę kompatybilnego z OpenAI. Zobacz ",
"provider.custom.description.prefix": "Skonfiguruj niestandardowego dostawcę. Zobacz ",
"provider.custom.description.link": "dokumentację konfiguracji dostawcy",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID dostawcy",
+3 -3
View File
@@ -862,7 +862,7 @@ export const dict = {
"settings.providers.tag.customProvider": "Пользовательский провайдер",
"settings.providers.connected.environmentDescription": "Подключён из ваших переменных окружения",
"settings.providers.action.signInChatGPT": "Войти через ChatGPT",
"settings.providers.custom.description": "Добавьте OpenAI-совместимый провайдер по базовому URL.",
"settings.providers.custom.description": "Добавьте пользовательский провайдер по базовому URL.",
"settings.providers.subagentModel.title": "Модель субагента",
"settings.providers.subagentModel.description":
"Модель по умолчанию и уровень рассуждения для субагентов task-tool. Оставьте пустым, чтобы унаследовать модель вызывающего агента.",
@@ -870,11 +870,11 @@ export const dict = {
"settings.models.hidePromptTraining.description":
"Скрывать модели Kilo Gateway, поставщики которых могут использовать ваши запросы для обучения.",
"settings.providers.modeModels": "Модель для режима",
"settings.providers.custom.note": "Добавьте OpenAI-совместимого провайдера по базовому URL.",
"settings.providers.custom.note": "Добавьте пользовательского провайдера по базовому URL.",
"settings.providers.modeModels.description":
"Переопределите модель по умолчанию для определённых режимов. Если не задано, используется глобальная модель по умолчанию.",
"provider.custom.title": "Пользовательский провайдер",
"provider.custom.description.prefix": "Настройте провайдер, совместимый с OpenAI. См. ",
"provider.custom.description.prefix": "Настройте пользовательский провайдер. См. ",
"provider.custom.description.link": "документацию по настройке провайдера",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "ID провайдера",
+3 -3
View File
@@ -850,18 +850,18 @@ export const dict = {
"settings.providers.tag.customProvider": "ผู้ให้บริการที่กำหนดเอง",
"settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
"settings.providers.action.signInChatGPT": "ลงชื่อเข้าใช้ด้วย ChatGPT",
"settings.providers.custom.description": "เพิ่มผู้ให้บริการที่เข้ากันได้กับ OpenAI ด้วย URL พื้นฐาน",
"settings.providers.custom.description": "เพิ่มผู้ให้บริการแบบกำหนดเองด้วย URL พื้นฐาน",
"settings.providers.subagentModel.title": "โมเดลตัวแทนย่อย",
"settings.providers.subagentModel.description":
"โมเดลเริ่มต้นและระดับการใช้เหตุผลสำหรับตัวแทนย่อยของ task-tool ปล่อยว่างไว้เพื่อรับค่าโมเดลจากตัวแทนที่เรียก",
"settings.models.hidePromptTraining.title": "ซ่อนโมเดลที่ใช้พรอมต์ในการฝึก",
"settings.models.hidePromptTraining.description": "ซ่อนโมเดล Kilo Gateway ที่ผู้ให้บริการอาจใช้พรอมต์ของคุณในการฝึก",
"settings.providers.modeModels": "โมเดลต่อโหมด",
"settings.providers.custom.note": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย Base URL",
"settings.providers.custom.note": "เพิ่มผู้ให้บริการแบบกำหนดเองด้วย Base URL",
"settings.providers.modeModels.description":
"แทนที่โมเดลเริ่มต้นสำหรับโหมดที่กำหนด หากไม่ได้ตั้งค่า จะใช้โมเดลเริ่มต้นทั่วไป",
"provider.custom.title": "ผู้ให้บริการที่กำหนดเอง",
"provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู",
"provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการแบบกำหนดเอง ดู",
"provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ",
"provider.custom.description.suffix": "",
"provider.custom.field.providerID.label": "ID ผู้ให้บริการ",
+3 -3
View File
@@ -824,10 +824,10 @@ export const dict = {
"settings.providers.tag.other": "Diğer",
"settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
"settings.providers.action.signInChatGPT": "ChatGPT ile oturum aç",
"settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
"settings.providers.custom.description": "Temel URL üzerinden özel bir sağlayıcı ekleyin.",
"provider.custom.title": "Özel sağlayıcı",
"provider.custom.description.prefix": "OpenAI uyumlu bir sağlayıcı yapılandırın. ",
"provider.custom.description.prefix": "Özel bir sağlayıcı yapılandırın. ",
"provider.custom.description.link": "Sağlayıcı yapılandırma dökümanları",
"provider.custom.description.suffix": " sayfasına bakın.",
"provider.custom.field.providerID.label": "Sağlayıcı kimlik",
@@ -1510,7 +1510,7 @@ export const dict = {
"settings.providers.enabled.description":
"Ayarlanırsa yalnızca bu sağlayıcılar kullanılabilir olacak (özel izin listesi)",
"settings.providers.notSet": "Ayarlanmadı (sunucu varsayılanını kullan)",
"settings.providers.custom.note": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
"settings.providers.custom.note": "Temel URL üzerinden özel bir sağlayıcı ekleyin.",
"settings.providers.search.placeholder": "Sağlayıcı ara",
"settings.providers.select.placeholder": "Sağlayıcı seç...",
+3 -3
View File
@@ -825,10 +825,10 @@ export const dict = {
"settings.providers.tag.other": "Інші",
"settings.providers.connected.environmentDescription": "Підключено зі змінних середовища",
"settings.providers.action.signInChatGPT": "Увійти через ChatGPT",
"settings.providers.custom.description": "Додати OpenAI-сумісного провайдера через базовий URL.",
"settings.providers.custom.description": "Додати власного провайдера через базовий URL.",
"provider.custom.title": "Власний провайдер",
"provider.custom.description.prefix": "Налаштувати OpenAI-сумісного провайдера. ",
"provider.custom.description.prefix": "Налаштувати власного провайдера. ",
"provider.custom.description.link": "Документація щодо налаштування провайдера",
"provider.custom.description.suffix": ".",
"provider.custom.field.providerID.label": "Ідентифікатор провайдера",
@@ -1507,7 +1507,7 @@ export const dict = {
"settings.providers.enabled": "Увімкнені провайдери (список дозволених)",
"settings.providers.enabled.description": "Якщо встановлено, доступні лише ці провайдери (власний список дозволених)",
"settings.providers.notSet": "Не встановлено (використовувати стандартний сервера)",
"settings.providers.custom.note": "Додати OpenAI-сумісного провайдера через базовий URL.",
"settings.providers.custom.note": "Додати власного провайдера через базовий URL.",
"settings.providers.search.placeholder": "Пошук провайдера",
"settings.providers.select.placeholder": "Вибрати провайдера...",
+3 -3
View File
@@ -838,16 +838,16 @@ export const dict = {
"settings.providers.tag.customProvider": "自定义提供商",
"settings.providers.connected.environmentDescription": "从您的环境变量连接",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登录",
"settings.providers.custom.description": "通过基础 URL 添加 OpenAI 兼容的提供商。",
"settings.providers.custom.description": "通过基础 URL 添加自定义提供商。",
"settings.providers.subagentModel.title": "子代理模型",
"settings.providers.subagentModel.description": "task-tool 子代理的默认模型和推理工作量。留空以继承调用代理的模型。",
"settings.models.hidePromptTraining.title": "隐藏使用提示词训练的模型",
"settings.models.hidePromptTraining.description": "隐藏提供商可能会使用您的提示词进行训练的 Kilo Gateway 模型。",
"settings.providers.modeModels": "按模式选择模型",
"settings.providers.custom.note": "通过 Base URL 添加 OpenAI 兼容提供商。",
"settings.providers.custom.note": "通过 Base URL 添加自定义提供商。",
"settings.providers.modeModels.description": "为特定模式覆盖默认模型。如果未设置,将使用全局默认模型。",
"provider.custom.title": "自定义提供商",
"provider.custom.description.prefix": "配置 OpenAI 兼容的提供商。请参阅",
"provider.custom.description.prefix": "配置自定义提供商。请参阅",
"provider.custom.description.link": "提供商配置文档",
"provider.custom.description.suffix": "。",
"provider.custom.field.providerID.label": "提供商 ID",
+3 -3
View File
@@ -806,16 +806,16 @@ export const dict = {
"settings.providers.tag.customProvider": "自訂提供商",
"settings.providers.connected.environmentDescription": "從您的環境變數連線",
"settings.providers.action.signInChatGPT": "使用 ChatGPT 登入",
"settings.providers.custom.description": "透過基礎 URL 新增 OpenAI 相容的提供商。",
"settings.providers.custom.description": "透過基礎 URL 新增自訂提供商。",
"settings.providers.subagentModel.title": "子代理模型",
"settings.providers.subagentModel.description": "task-tool 子代理的預設模型和推理工作量。留空以繼承呼叫代理的模型。",
"settings.models.hidePromptTraining.title": "隱藏使用提示詞訓練的模型",
"settings.models.hidePromptTraining.description": "隱藏供應商可能會使用您的提示詞進行訓練的 Kilo Gateway 模型。",
"settings.providers.modeModels": "按模式選擇模型",
"settings.providers.custom.note": "透過 Base URL 新增 OpenAI 相容供應商。",
"settings.providers.custom.note": "透過 Base URL 新增自訂供應商。",
"settings.providers.modeModels.description": "為特定模式覆寫預設模型。如果未設定,將使用全域預設模型。",
"provider.custom.title": "自訂提供商",
"provider.custom.description.prefix": "設定 OpenAI 相容的提供商。請參閱",
"provider.custom.description.prefix": "設定自訂提供商。請參閱",
"provider.custom.description.link": "提供商設定文件",
"provider.custom.description.suffix": "。",
"provider.custom.field.providerID.label": "提供商 ID",
@@ -18,7 +18,7 @@
min-height: 0;
overflow: hidden;
container: chat / inline-size;
--chat-readable-width: 88ch;
--chat-readable-width: 98ch;
--chat-gutter: clamp(16px, 4cqi, 48px);
--chat-scrollbar-width: 10px;
}
@@ -437,6 +437,11 @@
opacity: 0.8;
}
.model-selector-data-badge--byok [data-component="tag"] {
background: var(--vscode-badge-background) !important;
color: var(--vscode-badge-foreground) !important;
}
.model-selector-trigger-free-data {
display: inline-flex;
align-items: center;
@@ -19,6 +19,7 @@ export interface ProviderModel {
recommendedIndex?: number
isFree?: boolean
mayTrainOnYourPrompts?: boolean
hasUserByokAvailable?: boolean
terminalBench?: {
overallScore: number
avgAttemptCostUsd: number
+9 -1
View File
@@ -40,6 +40,7 @@ import { DialogMcp } from "@tui/component/dialog-mcp"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
import { DialogHeadlessLink } from "@/kilocode/cli/cmd/tui/component/dialog-headless-link" // kilocode_change
import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
@@ -72,6 +73,7 @@ import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
import { createTuiAttention } from "@/cli/cmd/tui/attention"
import { FormatError, FormatUnknownError } from "@/cli/error"
import { kitty, resetTerminalState } from "@/kilocode/cli/cmd/tui/util/terminal" // kilocode_change
import { hasDisplay } from "@/kilocode/cli/cmd/tui/util/display" // kilocode_change
import { CommandPaletteDialog } from "./component/command-palette"
import {
COMMAND_PALETTE_COMMAND,
@@ -703,8 +705,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
name: "docs.open",
title: "Open docs",
run: () => {
open(KiloApp.DOCS_URL).catch(() => {}) // kilocode_change
// kilocode_change start
if (!hasDisplay()) {
DialogHeadlessLink.show(dialog, KiloApp.DOCS_URL)
return
}
open(KiloApp.DOCS_URL).catch(() => {})
dialog.clear()
// kilocode_change end
},
category: "System",
},
@@ -64,9 +64,12 @@ export function DialogModel(props: { providerID?: string }) {
})
const footer = (providerID: string, model: Model) => {
if (providerID === "kilo" && FreeModelDisclosure.collectsData(model)) return FreeModelDisclosure.label
if (model.cost?.input === 0 && providerID === "opencode") return "Free"
return undefined
const labels = [
providerID === "kilo" && FreeModelDisclosure.hasByok(model) ? FreeModelDisclosure.byok : undefined,
providerID === "kilo" && FreeModelDisclosure.collectsData(model) ? FreeModelDisclosure.label : undefined,
model.cost?.input === 0 && providerID === "opencode" ? "Free" : undefined,
].filter((label) => label !== undefined)
return labels.length > 0 ? labels.join(" · ") : undefined
}
// kilocode_change end
@@ -69,8 +69,8 @@ const tui: TuiPlugin = async (api) => {
if (!active.delete(sessionID)) return
if (errored.delete(sessionID)) return
if (event.properties.reason !== "completed") return
const session = api.state.session.get(sessionID)
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
if (event.properties.parentID !== undefined) return
notify(api, sessionID, "Session done", "done")
})
// kilocode_change end
@@ -0,0 +1,41 @@
// kilocode_change - new file
import { TextAttributes } from "@opentui/core"
import { useTheme } from "@tui/context/theme"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { useBindings } from "@tui/keymap"
export type DialogHeadlessLinkProps = {
url: string
}
export function DialogHeadlessLink(props: DialogHeadlessLinkProps) {
const dialog = useDialog()
const { theme } = useTheme()
useBindings(() => ({
bindings: [
{ key: "return", desc: "Close dialog", group: "Dialog", cmd: () => dialog.clear() },
],
}))
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
No display detected
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1} flexDirection="column" gap={0}>
<text fg={theme.textMuted}>Read the docs at:</text>
<text fg={theme.accent}>{props.url}</text>
</box>
</box>
)
}
DialogHeadlessLink.show = (dialog: DialogContext, url: string) => {
dialog.replace(() => <DialogHeadlessLink url={url} />)
}
@@ -0,0 +1,4 @@
export function hasDisplay() {
if (process.platform !== "linux") return true
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)
}
@@ -1,7 +1,11 @@
export const FreeModelDisclosure = {
label: "May train",
panel: "Data may be used for training",
byok: "BYOK",
collectsData(model: { mayTrainOnYourPrompts?: boolean }): boolean {
return model.mayTrainOnYourPrompts === true
},
hasByok(model: { hasUserByokAvailable?: boolean }): boolean {
return model.hasUserByokAvailable === true
},
} as const
@@ -75,6 +75,11 @@ export function ModelInfoPanel(props: Props) {
</text>
<text fg={theme.textMuted}>{props.provider ?? m().providerID ?? ""}</text>
</box>
<Show when={FreeModelDisclosure.hasByok(m())}>
<box>
<text fg={theme.text}>{FreeModelDisclosure.byok}</text>
</box>
</Show>
<Show when={FreeModelDisclosure.collectsData(m())}>
<box>
<text fg={theme.text}>{FreeModelDisclosure.panel}</text>

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