Merge branch 'main' into hardly-wish

# Conflicts:
#	packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts
#	packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx
This commit is contained in:
marius-kilocode
2026-06-26 13:53:57 +02:00
115 changed files with 2528 additions and 263 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Add a sandbox toggle to the Agent Manager New Worktree modal so each worktree session can start sandboxed
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Explain Marketplace item types, installation scopes, destination files, and MCP security before installation.
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve JetBrains session error logs so nested CLI error messages are visible.
+7
View File
@@ -0,0 +1,7 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
"@kilocode/sdk": patch
---
Expose Kilo Pass state on the Kilo profile API contract.
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Notify when a marketplace item matches your workspace, with a one-click install button and a "Don't show again" option per suggestion.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Support resetting read notifications from the VS Code extension settings.
@@ -40,6 +40,7 @@ export const GettingStartedNav: NavSection[] = [
},
{ href: "/getting-started/adding-credits", children: "Adding Credits" },
{ href: "/getting-started/rate-limits-and-costs", children: "Cost Efficiency & Model Selection" },
{ href: "/getting-started/cost-controls-and-usage-safeguards", children: "Cost Controls and Usage Safeguards" },
],
},
{
+2
View File
@@ -50,6 +50,8 @@ exclude = [
'^https?://api\.apertis\.ai/v1/?$',
# Redirects to authenticated Google Cloud console
'^https?://console\.cloud\.google\.com',
# Linear intermittently returns 503 to CI link checks.
'^https?://linear\.app/?$',
# Consistently times out in CI
'^https?://opncd\.ai',
'^https?://zod\.dev/v4/changelog',
@@ -0,0 +1,400 @@
---
title: "Cost Controls and Usage Safeguards"
description: "How to prevent runaway agent usage, reduce token consumption, choose the right model, and govern spend at the individual and organization level"
---
# Cost Controls and Usage Safeguards
## Overview
How much you spend with Kilo Code is shaped by several factors working together:
- **Model selection** — frontier models cost significantly more per token than efficient or free tiers
- **Prompt and context size** — every token in your system prompt, conversation history, file attachments, and tool definitions is billed as input
- **Number of agent steps and tool calls** — each step the agent takes (read a file, run a command, write code) generates its own request
- **Repeated retries or loops** — a stuck agent that keeps retrying the same failing step multiplies your cost
- **Background and automated tasks** — long-running or unattended tasks accumulate cost without immediate visibility
- **Session length** — long sessions carry more conversation history into every new request
No single control eliminates cost on its own. The most effective approach combines model selection, context management, task scope, and account-level monitoring together.
This page covers the controls currently available in Kilo Code. For a direct overview of Auto Model tiers and token optimization tips, see [Cost Efficiency & Model Selection](/docs/getting-started/rate-limits-and-costs).
---
## Preventing Loops and Runaway Usage
### Doom loop protection
When the agent enters a repeated failure cycle — attempting the same action multiple times without making progress — Kilo pauses and asks for permission before continuing. This is controlled by the `doom_loop` permission, which defaults to `ask`.
**Where to configure:** Settings → Auto Approve (VS Code) or the `permission.doom_loop` key in `kilo.jsonc` (CLI).
**When to use:** Leave this at `ask` (the default) unless you are running fully unattended automation in a controlled environment. Setting it to `deny` blocks recovery entirely; `allow` lets loops continue without interruption.
### Per-tool approval controls
Every action Kilo takes — reading files, editing code, running shell commands, launching sub-agents — is governed by the permission system. Each tool can be set to `allow`, `ask`, or `deny`. When set to `ask`, Kilo pauses before executing and you can approve or reject that specific action.
**Where to configure:** Settings → Auto Approve (VS Code) or the `permission` section in `kilo.jsonc` (CLI). See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) for the full list of available permissions.
**When to use:** Keep `bash` set to `ask` by default for unfamiliar tasks. You can allow specific safe command prefixes (e.g. `git *`, `npm *`) while keeping everything else at `ask`. This prevents the agent from running expensive or destructive commands in a loop without oversight.
### Runtime auto-approve toggle (VS Code)
A shield button in the prompt controls lets you toggle auto-approve on and off at runtime without opening Settings. When enabled, pending permission prompts are approved automatically. The state stays synced across the sidebar and open Kilo tabs.
**When to use:** Turn it on when working on a well-understood, low-risk task that does not need step-by-step review. Turn it off as soon as you want to pause and review the agent's next actions.
### Spending limits
Individual accounts stop spending when their balance reaches zero — further requests to paid models return an error and prompt you to add credits. This acts as a hard ceiling on total spend.
Organization accounts can additionally configure **per-user daily spending limits**. When a member reaches their daily cap, subsequent requests are blocked until midnight UTC, when the limit resets.
**Where to configure:** Organization spending limits are managed in the organization dashboard at [app.kilo.ai](https://app.kilo.ai). Individual credit top-up is at Settings → Adding Credits.
### Free model rate limits
Requests to free models (`kilo-auto/free` and other free-tier models) are rate-limited to **200 requests per hour**. If you exceed this, requests return HTTP 429 and you must wait before continuing.
### Practical recommendations
- **Define a narrow scope before starting.** "Fix the null pointer in `processData`" generates far fewer steps than "keep fixing everything that looks wrong." Specificity reduces both steps and cost.
- **Ask the agent to stop and report after a stage.** Phrases like "analyze the problem and summarize your findings before making any changes" let you review the plan and cost before committing to implementation.
- **Review plans before allowing broad execution.** Use Architect mode (which cannot modify code) to get a plan first, then switch to Code mode to apply it incrementally.
- **Monitor long-running or unattended tasks.** Check the per-request cost estimates in the chat history as the session progresses. If cost is climbing unexpectedly, pause and review what the agent is doing.
- **Avoid open-ended prompts.** Prompts like "keep trying until it works" or "explore the whole codebase and clean it up" give the agent unlimited scope to continue generating steps.
---
## Reducing Context and Token Usage
### Start a new session when the topic changes
Conversation history accumulates with every turn. When you finish a task and move on to something unrelated, starting a new session resets the context to just your system prompt and instructions — significantly cheaper than carrying the full prior conversation.
### Keep prompts focused
Concise, specific prompts cost fewer tokens. Instead of pasting large blocks of background, describe what you need in plain terms and let the agent ask for files if it needs them. Repeating context you already provided earlier in the session is rarely necessary.
### Use `@file` and `@folder` mentions selectively
Attaching an entire folder sends every file in it as input tokens, even if only one or two files are relevant. Use `@file` with specific paths rather than broad directory mentions. When reviewing a bug, include only the file containing the bug and its closest dependencies.
### Exclude generated, build, and vendor directories
Kilo automatically skips a set of directories including `node_modules`, `dist`, `build`, `.git`, `__pycache__`, `.cache`, and `vendor`. You can add additional paths using **permission deny rules** in `kilo.jsonc` or using a `.kilocodeignore` file at your workspace root.
```jsonc
{
"permission": {
"read": {
"coverage/**": "deny",
".next/**": "deny",
"*": "allow"
}
}
}
```
**Where to configure:** `kilo.jsonc` (VS Code / CLI). See [.kilocodeignore](/docs/customize/context/kilocodeignore) for full details.
### Compact long conversations
When a conversation grows long, use `/compact` in the chat (also searchable as `smol` or `condense`) to summarize the history and free up context space. Kilo replaces older conversation turns with an anchored summary that captures your goal, constraints, progress, and next steps.
Auto-compaction is **enabled by default** — Kilo automatically compacts when approaching the context window limit so you do not need to intervene manually.
**Where to configure:** Toggle auto-compaction in **Settings → Context** (VS Code) or set `compaction.auto` in `kilo.jsonc`. Configure the trigger threshold with `compaction.threshold_percent` (e.g. `80` to compact at 80% of the model's context window).
You can also configure a cheaper model specifically for compaction, so summarization does not consume frontier model tokens:
```jsonc
{
"agent": {
"compaction": {
"model": "anthropic/claude-haiku-4-5"
}
}
}
```
See [Context Condensing](/docs/customize/context/context-condensing) for full configuration options.
### Keep max output tokens conservative
Every token you allocate to model output reduces how much conversation history can remain in the context window. For routine coding tasks, keep Code mode at **16k max output tokens or below**. Raise the limit only in Architect or Debug modes where extended reasoning is useful.
**Where to configure:** Model settings in the Kilo Code UI, or the `limit.output` key in custom model configuration.
### Use project instructions efficiently
Encode recurring guidance — coding standards, project conventions, preferred libraries — in your `AGENTS.md` or custom instructions once. This avoids repeating the same context in every prompt, and prompt caching means stable instructions are served from cache at a discounted rate on supported providers.
### Disable unused MCP servers
MCP tool definitions are included in the system prompt sent with every request. If you are not using MCP features, disable MCP servers in **Settings → Agent Behaviour → MCP Servers**. This can meaningfully reduce per-request system prompt size.
See [MCP Overview](/docs/automate/mcp/overview) for details.
### Prompt caching
Kilo automatically applies prompt caching on supported providers. Repeated context — your system prompt, stable file contents, and tool definitions — is reused from cache at a discounted rate. No configuration is required to benefit from this.
---
## Choosing Models for Specific Tasks
Different tasks benefit from different model characteristics. Routing work to the right model reduces cost without sacrificing quality.
Kilo has auto-models that can help you control costs; more information is available in [Auto Model](/docs/code-with-ai/agents/auto-model).
### Practical examples by task type
| Task type | Suggested approach |
|---|---|
| Quick questions, syntax lookups, simple formatting | `kilo-auto/efficient` or a lightweight model |
| Routine edits, test generation, straightforward refactors | `kilo-auto/efficient` or a mid-tier model |
| Complex debugging, tracing unexpected behavior | `kilo-auto/frontier` or a strong reasoning model; Debug mode |
| Architecture planning, design decisions | `kilo-auto/frontier`; Architect mode |
| Repository-wide analysis or search | A model with a large context window (256K+); Architect mode |
| Code review and summarization | `kilo-auto/efficient` or a cost-effective model |
| Automated background tasks (CI, scripting) | `kilo-auto/efficient` or `kilo-auto/free` |
### Manually selecting a model
Use the **model selector dropdown** in the Kilo Code chat interface to switch models for the current session. In the CLI, pass the `--model` flag to `kilo run` or use the model picker in the TUI (`Ctrl+X m` or `/models`).
### Configuring a model per agent or mode
You can set a default model for each agent (Code, Architect, Debug, Plan, or a custom subagent) independently:
- **VS Code:** Settings → Models → Model per Mode, or edit `kilo.jsonc` directly.
- **CLI:** Set `agent.<name>.model` in `kilo.jsonc`.
```jsonc
{
"agent": {
"code": {
"model": "kilo-auto/efficient"
},
"architect": {
"model": "kilo-auto/frontier"
}
}
}
```
This lets you run cost-effective models for implementation while automatically routing planning tasks to a more capable model.
### Organization-level model restrictions
Enterprise organizations can restrict which models team members may use. See [Enterprise Cost Controls](#enterprise-cost-controls) below.
For full guidance on model selection, see the [Model Selection Guide](/docs/code-with-ai/agents/model-selection).
---
## Enterprise Cost Controls
The following controls are available to organizations and, where noted, are exclusive to Enterprise plans.
### Model access controls (Enterprise only)
Enterprise organization owners can block specific models or entire providers for all team members using the **Providers & Models** page in the dashboard. The system uses a blocklist approach — everything is allowed by default, and admins explicitly block what should not be accessible.
Blocking a provider blocks all current and future models from that provider. Filters are available for:
- Data policy (trains on prompts, retains prompts)
- Provider location / datacenter region
- Specific model ID
Only **Owners** can modify model access controls. Individual members cannot override organization-level restrictions.
**Where to configure:** Dashboard → Providers and Models (Enterprise only). See [Model Access Controls](/docs/collaborate/enterprise/model-access-controls).
**How it helps:** Prevents accidental use of high-cost frontier models; enforces data residency or compliance requirements; limits cost surface by allowing only approved models.
### Shared credit pool and auto top-up
All organization members draw from a single shared credit balance. Administrators can configure:
- **Auto top-up:** Automatically replenish credits when the balance drops below a threshold (minimum $50 balance, minimum $100 purchase)
- **Minimum balance alerts:** Email notifications when the balance drops below a configured amount
**Where to configure:** Dashboard → Billing.
### Usage analytics
The **Usage** tab of the organization dashboard provides:
- Total spend, request count, average cost per request, total tokens, and active users for any selected time period (past week, month, year, or all time)
- Usage broken down by day, by model and day, or by project
- Per-user attribution — individual usage statistics visible to Owners and Admins
This gives administrators visibility into which team members, models, and projects are driving the majority of spend.
**Where to access:** [app.kilo.ai](https://app.kilo.ai) → Usage tab.
### Administrative permissions
Dashboard administrative actions (model restrictions, spending limits, billing management) are gated by role. Only **Owners** can modify model access controls and organization-level settings. **Owners and Admins** can view per-user usage data.
---
## Recommended Configurations
### Cost-conscious individual developer
- Use `kilo-auto/efficient` as the default model
- Switch to `kilo-auto/free` for low-stakes questions and exploration
- Enable auto-compaction (on by default); set `compaction.threshold_percent: 80` to compact earlier
- Set Code agent max output tokens to 16k or below
- Keep `doom_loop` permission at `ask`
- Start a new session whenever you switch to an unrelated task
- Use `@file` mentions with specific paths instead of `@folder` for whole directories
- Disable any MCP servers you are not actively using
### Developer working on a large repository
- Use Architect mode for initial codebase exploration — it cannot modify code, keeping exploration cost lower
- Use `@file` mentions with specific paths instead of attaching whole directories
- Add generated and build directories to permission deny rules (`coverage/**`, `.next/**`, etc.)
- Configure a cheap model for compaction (`anthropic/claude-haiku-4-5` or equivalent)
- Consider using a model with a large context window (256K+) for cross-file analysis tasks
- Break large tasks into focused sub-tasks rather than asking for a single comprehensive change
### Team using multiple models
- Assign `kilo-auto/efficient` to Code and Debug agents for everyday work
- Assign `kilo-auto/frontier` to Architect (or Plan) agent for planning tasks
- Set `kilo-auto/efficient` as the compaction model for all agents
- If on an Enterprise plan, use Providers & Models to block high-cost models that are not needed for your team's typical work
```jsonc
{
"agent": {
"code": { "model": "kilo-auto/efficient" },
"debug": { "model": "kilo-auto/efficient" },
"architect": { "model": "kilo-auto/frontier" },
"compaction": { "model": "anthropic/claude-haiku-4-5" }
}
}
```
### Maximum-constraint starter configuration
The snippet below is a ready-to-copy `kilo.jsonc` that turns on every available cost-control knob at its most restrictive setting. Drop it into your project root (or your global `~/.config/kilo/kilo.jsonc`) and adjust individual values upward as you get comfortable with how each one behaves.
{% callout type="tip" %}
This configuration uses only `kilo-auto/efficient`.
{% /callout %}
```jsonc
{
"$schema": "https://app.kilo.ai/config.json",
// ── Model selection ──────────────────────────────────────────────────────
// Route all requests through the two lowest-cost Kilo Auto tiers.
// kilo-auto/efficient: lowest-cost paid tier (classifies each request by
// difficulty and routes to the cheapest benchmark-proven model).
"model": "kilo-auto/efficient",
"subagent_model": "kilo-auto/efficient", // default model for Task-tool subagents
// ── Per-agent model and step limits ─────────────────────────────────────
// Assign the cheapest suitable tier to each agent and cap how many
// agentic iterations it may take before it must produce a text-only reply.
// Raise `steps` for agents that need more room; lower it to tighten cost.
"agent": {
"code": {
"model": "kilo-auto/efficient",
"steps": 20 // hard cap on agentic iterations per turn
},
"plan": {
"model": "kilo-auto/efficient",
"steps": 10
},
"debug": {
"model": "kilo-auto/efficient",
"steps": 20
},
"ask": {
"model": "kilo-auto/efficient",
"steps": 5
},
"orchestrator": {
"model": "kilo-auto/efficient",
"steps": 10
},
"explore": {
"model": "kilo-auto/free",
"steps": 15
},
"general": {
"model": "kilo-auto/efficient",
"steps": 15
},
// Dedicated agents for background summarization
"compaction": { "model": "kilo-auto/free" },
"title": { "model": "kilo-auto/free" },
"summary": { "model": "kilo-auto/free" }
},
// ── Compaction (context management) ─────────────────────────────────────
// Auto-compact aggressively to keep conversation history short and cheap.
"compaction": {
"auto": true, // enable automatic compaction (default: true)
"threshold_percent": 50, // compact when context reaches 50% full (default: ~80%)
"prune": true, // prune old tool outputs to recover context space
"tail_turns": 1, // keep only 1 recent user-turn verbatim after compaction
"preserve_recent_tokens": 2000, // cap on tokens preserved verbatim from recent turns
"reserved": 8000 // token buffer reserved so compaction itself doesn't overflow
},
// ── Tool output truncation ───────────────────────────────────────────────
// Clip large tool responses early so they don't bloat the context window.
// Increase these if the agent needs more output (e.g. long test logs).
"tool_output": {
"max_lines": 500, // default: 2000 lines
"max_bytes": 10240 // default: 51200 bytes (~50 KB)
},
// ── Permission safeguards ────────────────────────────────────────────────
// "ask" means Kilo pauses and requires your approval before executing.
// This prevents runaway loops from autonomously consuming tokens or making
// irreversible changes. Flip individual entries to "allow" once you trust them.
"permission": {
"bash": "ask", // shell commands (highest risk of runaway cost)
"edit": "ask", // file writes and edits
"task": "ask", // launching sub-agents (each sub-agent = extra LLM requests)
"webfetch": "ask", // outbound HTTP fetches
"websearch": "ask", // web search calls
"doom_loop": "ask" // repeated-failure loop detection — always keep at "ask" or "deny"
},
}
```
Every field in this block is documented in the sections above. Use it as a starting point, then relax individual settings (for example, setting `permission.edit` to `"allow"` for a trusted project, or raising `compaction.threshold_percent` to `70` if compaction feels too aggressive) as you build confidence in how the agent behaves.
---
## Troubleshooting Unexpected Usage
If your spend is higher than expected:
- **Check your usage dashboard** at [app.kilo.ai/usage](https://app.kilo.ai/usage) for a breakdown by day, model, and project
- **Review the model in use** — an accidental switch to a frontier model for routine tasks can significantly raise costs
- **Look for long sessions** — sessions that were never compacted carry their full history as input tokens on every request; use `/compact` to reset them
- **Check MCP server configuration** — unused MCP servers add tool definitions to every system prompt
- **Review permission settings** — auto-approving all actions with no `doom_loop` guard removes the friction that normally slows down runaway loops
For further reading: [4 Levers to Take Control of Your AI Spend](https://blog.kilo.ai/p/4-spend-levers)
## Related
- [Cost Efficiency & Model Selection](/docs/getting-started/rate-limits-and-costs) — Auto Model tier comparison, rate limits, and per-request cost calculation
- [Auto Model](/docs/code-with-ai/agents/auto-model) — Full details on each Auto Model tier and routing strategy
- [Context Condensing](/docs/customize/context/context-condensing) — How compaction works and all configuration options
- [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) — Permission system reference for VS Code and CLI
- [Model Access Controls](/docs/collaborate/enterprise/model-access-controls) — Enterprise model and provider blocklist configuration
- [Usage & Billing](/docs/gateway/usage-and-billing) — Gateway billing mechanics and organization controls
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3545994fd7a126ea860b31e2ac07f9cbeaa03e872f805e3fd2ec7012556c6ea8
size 12665
oid sha256:36101257010ff15372b8e2b7ace1b5128ecc820ce5ca4111ff1ba7905473b0b4
size 19407
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45d2e8b0e2a5dd7a40ec5c0b5ab4acee6035ede90135980038628e98e3478cd9
size 682
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:10adc41427bfb41fb25fb25e14c771443c1b3cf23a546b05665370974fbc2209
size 54839
oid sha256:cca305368e62ec5b45c8b20bc5d4921e0ca8df1c54eb110efffe188837400b95
size 57533
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:572b5a2bfd530b743a8ebaaed2ddacc9611aaa31f450df0f2904f314d8cfff6d
size 52660
oid sha256:9a681b7c8866aa3aa6f1e8673afcb5efb79d3a251ba46b3c7201e22dcc77b9c2
size 55898
+5
View File
@@ -123,10 +123,15 @@
<!-- packages/opencode/src/cli/cmd/providers.ts -->
- <https://kilo.ai/docs/ai-providers#custom-provider>
<!-- packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx -->
- <https://kilo.ai/docs/automate/mcp/what-is-mcp>
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx -->
- <https://kilo.ai/docs/code-with-ai/platforms/vscode/whats-new>
<!-- packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx -->
- <https://kilo.ai/docs/customize/custom-subagents>
<!-- packages/opencode/src/config/config.ts -->
- <https://kilo.ai/docs/customize/marketplace>
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx -->
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx -->
- <https://kilo.ai/docs/customize/workflows>
<!-- packages/opencode/src/config/config.ts -->
- <https://kilo.ai/gateway>
@@ -0,0 +1,44 @@
import { buildKiloHeaders } from "../headers.js"
import type { KiloPassState } from "../types.js"
import { KILO_API_BASE } from "./constants.js"
function record(value: unknown) {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : undefined
}
function num(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0
}
export function parseKiloPassState(value: unknown): KiloPassState | null {
const item = Array.isArray(value) ? value[0] : value
const data = record(record(record(item)?.result)?.data)
const root = record(data?.json) ?? data ?? record(value)
const sub = record(root?.subscription)
if (!sub || (sub.currentPeriodBaseCreditsUsd == null && sub.currentPeriodUsageUsd == null)) return null
const next = sub.nextBillingAt ?? sub.nextRenewalAt
return {
currentPeriodBaseCreditsUsd: num(sub.currentPeriodBaseCreditsUsd),
currentPeriodUsageUsd: num(sub.currentPeriodUsageUsd),
currentPeriodBonusCreditsUsd: num(sub.currentPeriodBonusCreditsUsd),
nextBillingAt: typeof next === "string" ? next : null,
}
}
export async function fetchKiloPassState(token: string): Promise<KiloPassState | null> {
try {
const params = new URLSearchParams({ batch: "1", input: JSON.stringify({ "0": null }) })
const response = await fetch(`${KILO_API_BASE}/api/trpc/kiloPass.getState?${params}`, {
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...buildKiloHeaders() },
})
if (!response.ok) {
console.warn(`Failed to fetch Kilo Pass: ${response.status}`)
return null
}
return parseKiloPassState(await response.json())
} catch (err) {
console.warn("Error fetching Kilo Pass:", err)
return null
}
}
+2
View File
@@ -33,6 +33,7 @@ export {
getKiloDefaultModel,
promptOrganizationSelection,
} from "./api/profile.js"
export { fetchKiloPassState } from "./api/kilo-pass.js"
export { fetchKiloModels, type KiloModelsResult } from "./api/models.js"
export {
EMPTY_KILO_EMBEDDING_MODEL_CATALOG,
@@ -94,6 +95,7 @@ export type {
Organization,
KilocodeProfile,
KilocodeBalance,
KiloPassState,
PollOptions,
PollResult,
// Provider types
+6 -3
View File
@@ -1,8 +1,9 @@
import { fetchBalance, fetchProfile } from "../api/profile.js"
import { fetchKiloPassState } from "../api/kilo-pass.js"
import { fetchKilocodeNotifications } from "../api/notifications.js"
import { clearModesCache } from "../api/modes.js"
import { HEADER_ORGANIZATIONID, KILO_API_BASE, KILO_CHAT_URL, KILO_EVENT_SERVICE_URL } from "../api/constants.js"
import type { KilocodeBalance, KilocodeProfile } from "../types.js"
import type { KilocodeBalance, KilocodeProfile, KiloPassState } from "../types.js"
import { buildKiloHeaders } from "../headers.js"
export type KiloAuth =
@@ -13,6 +14,7 @@ export type KiloAuth =
export interface KiloProfileResult {
profile: KilocodeProfile
balance: KilocodeBalance | null
kiloPass: KiloPassState | null
currentOrgId: string | null
}
@@ -67,11 +69,12 @@ export async function getProfile(auth: AuthStore): Promise<KiloProfileResult> {
if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway")
const currentOrgId = info.accountId ?? null
const [profile, balance] = await Promise.all([
const [profile, balance, kiloPass] = await Promise.all([
fetchProfile(info.access),
fetchBalance(info.access, currentOrgId ?? undefined),
fetchKiloPassState(info.access),
])
return { profile, balance, currentOrgId }
return { profile, balance, kiloPass, currentOrgId }
}
export async function getNotifications(auth: AuthStore) {
@@ -107,9 +107,17 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
balance: z.number(),
})
const KiloPassState = z.object({
currentPeriodBaseCreditsUsd: z.number(),
currentPeriodUsageUsd: z.number(),
currentPeriodBonusCreditsUsd: z.number(),
nextBillingAt: z.string().nullable().optional(),
})
const ProfileWithBalance = z.object({
profile: Profile,
balance: Balance.nullable(),
kiloPass: KiloPassState.nullable(),
currentOrgId: z.string().nullable(),
})
+7
View File
@@ -33,6 +33,13 @@ export interface KilocodeBalance {
balance: number
}
export interface KiloPassState {
currentPeriodBaseCreditsUsd: number
currentPeriodUsageUsd: number
currentPeriodBonusCreditsUsd: number
nextBillingAt?: string | null
}
export interface PollOptions<T> {
interval: number
maxAttempts: number
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test"
import { parseKiloPassState } from "../../src/api/kilo-pass"
describe("parseKiloPassState", () => {
test("parses batched tRPC subscription data", () => {
const state = parseKiloPassState([
{
result: {
data: {
json: {
subscription: {
tier: "tier_199",
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 73.27,
currentPeriodBonusCreditsUsd: 99.5,
nextBillingAt: "2026-07-01T00:00:00.000Z",
},
},
},
},
},
])
expect(state).toEqual({
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 73.27,
currentPeriodBonusCreditsUsd: 99.5,
nextBillingAt: "2026-07-01T00:00:00.000Z",
})
})
test("parses plain subscription payload", () => {
const state = parseKiloPassState([
{
result: {
data: {
subscription: {
tier: "tier_199",
status: "active",
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 0.01,
currentPeriodBonusCreditsUsd: 29.85,
isBonusUnlocked: false,
nextBillingAt: "2026-07-20T09:30:20.806Z",
},
isEligibleForFirstMonthPromo: false,
},
},
},
])
expect(state).toEqual({
currentPeriodBaseCreditsUsd: 199,
currentPeriodUsageUsd: 0.01,
currentPeriodBonusCreditsUsd: 29.85,
nextBillingAt: "2026-07-20T09:30:20.806Z",
})
})
test("returns null without period amounts", () => {
expect(parseKiloPassState({ status: "none" })).toBeNull()
})
})
+18
View File
@@ -42,6 +42,24 @@ export const dict = {
"marketplace.install.scope": "النطاق",
"marketplace.install.scope.project": "المشروع",
"marketplace.install.scope.global": "عالمي",
"marketplace.install.scope.project.description":
"هذا المشروع فقط. يمكن إضافة الملفات المثبتة إلى نظام التحكم في الإصدارات ومشاركتها مع فريقك.",
"marketplace.install.scope.global.description":
"جميع المشاريع على هذا الجهاز. تُحفظ في إعدادات المستخدم الخاصة بك.",
"marketplace.install.destination": "وجهة التثبيت",
"marketplace.install.about.mcp":
"يمنح خادم MCP منصة Kilo أدوات إضافية للعمل مع الخدمات الخارجية أو البرامج المحلية.",
"marketplace.install.about.agent": "يضيف الوكيل دورًا قابلاً لإعادة الاستخدام بتعليماته وأذوناته الخاصة.",
"marketplace.install.about.skill": "تضيف المهارة تعليمات وموارد خاصة بمهام معينة يمكن لـ Kilo تحميلها عند الحاجة.",
"marketplace.install.mcp.warning":
"يمكن لخوادم MCP تشغيل أوامر محلية أو الاتصال بخدمات خارجية. ستطلب Kilo الإذن قبل استخدام أدواتها ما لم تسمح أذوناتك بذلك تلقائيًا.",
"marketplace.install.project.warning":
"قد تُضاف ملفات المشروع إلى نظام التحكم في الإصدارات. لا تخزّن الأسرار هنا إلا إذا كان الإعداد يشير إلى متغير بيئة.",
"marketplace.install.learnMore": "تعرّف على كيفية عمل عمليات التثبيت من Marketplace",
"marketplace.install.learnMcp": "معرفة المزيد عن MCP",
"marketplace.install.installedAt": "تم التثبيت في {{path}}",
"marketplace.intro": "ثبّت وكلاء ومهارات وأدوات MCP قابلة لإعادة الاستخدام لمشروع واحد أو لجميع المشاريع.",
"marketplace.intro.learnMore": "حول Marketplace",
"marketplace.install.prerequisites": "المتطلبات الأساسية",
"marketplace.install.installing": "جاري التثبيت...",
"marketplace.install.cancel": "إلغاء",
+19
View File
@@ -42,6 +42,25 @@ export const dict = {
"marketplace.install.scope": "Escopo",
"marketplace.install.scope.project": "Projeto",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Somente este projeto. Os arquivos instalados podem ser adicionados ao controle de versão e compartilhados com sua equipe.",
"marketplace.install.scope.global.description":
"Todos os projetos nesta máquina. Armazenado na sua configuração de usuário.",
"marketplace.install.destination": "Destino da instalação",
"marketplace.install.about.mcp":
"Um servidor MCP fornece ao Kilo ferramentas adicionais para trabalhar com serviços externos ou programas locais.",
"marketplace.install.about.agent": "Um agente adiciona uma função reutilizável com instruções e permissões próprias.",
"marketplace.install.about.skill":
"Uma habilidade adiciona instruções e recursos específicos para tarefas que o Kilo pode carregar quando necessário.",
"marketplace.install.mcp.warning":
"Servidores MCP podem executar comandos locais ou se conectar a serviços externos. O Kilo solicitará permissão antes de usar suas ferramentas, a menos que suas permissões permitam isso automaticamente.",
"marketplace.install.project.warning":
"Os arquivos do projeto podem ser adicionados ao controle de versão. Não armazene segredos aqui, a menos que a configuração faça referência a uma variável de ambiente.",
"marketplace.install.learnMore": "Saiba como funcionam as instalações do Marketplace",
"marketplace.install.learnMcp": "Saiba mais sobre MCP",
"marketplace.install.installedAt": "Instalado em {{path}}",
"marketplace.intro": "Instale agentes, habilidades e ferramentas MCP reutilizáveis em um projeto ou em todos os projetos.",
"marketplace.intro.learnMore": "Sobre o Marketplace",
"marketplace.install.prerequisites": "Pré-requisitos",
"marketplace.install.installing": "Instalando...",
"marketplace.install.cancel": "Cancelar",
+19
View File
@@ -47,6 +47,25 @@ export const dict = {
"marketplace.install.scope": "Opseg",
"marketplace.install.scope.project": "Projekat",
"marketplace.install.scope.global": "Globalno",
"marketplace.install.scope.project.description":
"Samo ovaj projekat. Instalirane datoteke mogu se dodati u kontrolu verzija i dijeliti s vašim timom.",
"marketplace.install.scope.global.description":
"Svi projekti na ovom računaru. Čuva se u vašoj korisničkoj konfiguraciji.",
"marketplace.install.destination": "Odredište instalacije",
"marketplace.install.about.mcp":
"MCP server pruža Kilu dodatne alate za rad s vanjskim servisima ili lokalnim programima.",
"marketplace.install.about.agent": "Agent dodaje višekratnu ulogu s vlastitim uputama i dozvolama.",
"marketplace.install.about.skill":
"Vještina dodaje upute i resurse za određene zadatke koje Kilo može učitati kada su potrebni.",
"marketplace.install.mcp.warning":
"MCP serveri mogu pokretati lokalne naredbe ili se povezivati s vanjskim servisima. Kilo će zatražiti dozvolu prije korištenja njihovih alata, osim ako vaše dozvole to automatski dopuštaju.",
"marketplace.install.project.warning":
"Datoteke projekta mogu se dodati u kontrolu verzija. Ne čuvajte tajne ovdje osim ako konfiguracija upućuje na varijablu okruženja.",
"marketplace.install.learnMore": "Saznajte kako funkcionišu instalacije s Marketplacea",
"marketplace.install.learnMcp": "Saznajte više o MCP-u",
"marketplace.install.installedAt": "Instalirano u {{path}}",
"marketplace.intro": "Instalirajte višekratne agente, vještine i MCP alate za jedan ili sve projekte.",
"marketplace.intro.learnMore": "O Marketplaceu",
"marketplace.install.prerequisites": "Preduslovi",
"marketplace.install.installing": "Instalacija...",
"marketplace.install.cancel": "Otkaži",
+19
View File
@@ -42,6 +42,25 @@ export const dict = {
"marketplace.install.scope": "Omfang",
"marketplace.install.scope.project": "Projekt",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Kun dette projekt. De installerede filer kan føjes til versionsstyring og deles med dit team.",
"marketplace.install.scope.global.description":
"Alle projekter på denne maskine. Gemmes i din brugerkonfiguration.",
"marketplace.install.destination": "Installationsplacering",
"marketplace.install.about.mcp":
"En MCP-server giver Kilo yderligere værktøjer til at arbejde med eksterne tjenester eller lokale programmer.",
"marketplace.install.about.agent": "En agent tilføjer en genanvendelig rolle med egne instruktioner og tilladelser.",
"marketplace.install.about.skill":
"En færdighed tilføjer opgavespecifikke instruktioner og ressourcer, som Kilo kan indlæse efter behov.",
"marketplace.install.mcp.warning":
"MCP-servere kan køre lokale kommandoer eller oprette forbindelse til eksterne tjenester. Kilo beder om tilladelse, før deres værktøjer bruges, medmindre dine tilladelser automatisk tillader det.",
"marketplace.install.project.warning":
"Projektfiler kan føjes til versionsstyring. Gem ikke hemmeligheder her, medmindre konfigurationen henviser til en miljøvariabel.",
"marketplace.install.learnMore": "Se, hvordan installationer fra Marketplace fungerer",
"marketplace.install.learnMcp": "Få mere at vide om MCP",
"marketplace.install.installedAt": "Installeret i {{path}}",
"marketplace.intro": "Installer genanvendelige agenter, færdigheder og MCP-værktøjer til ét eller alle projekter.",
"marketplace.intro.learnMore": "Om Marketplace",
"marketplace.install.prerequisites": "Forudsætninger",
"marketplace.install.installing": "Installerer...",
"marketplace.install.cancel": "Annuller",
+21
View File
@@ -42,6 +42,27 @@ export const dict = {
"marketplace.install.scope": "Bereich",
"marketplace.install.scope.project": "Projekt",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Nur dieses Projekt. Die installierten Dateien können versioniert und mit Ihrem Team geteilt werden.",
"marketplace.install.scope.global.description":
"Alle Projekte auf diesem Computer. Wird in Ihrer Benutzerkonfiguration gespeichert.",
"marketplace.install.destination": "Installationsziel",
"marketplace.install.about.mcp":
"Ein MCP-Server stellt Kilo zusätzliche Werkzeuge für die Arbeit mit externen Diensten oder lokalen Programmen bereit.",
"marketplace.install.about.agent":
"Ein Agent fügt eine wiederverwendbare Rolle mit eigenen Anweisungen und Berechtigungen hinzu.",
"marketplace.install.about.skill":
"Ein Skill fügt aufgabenspezifische Anweisungen und Ressourcen hinzu, die Kilo bei Bedarf laden kann.",
"marketplace.install.mcp.warning":
"MCP-Server können lokale Befehle ausführen oder eine Verbindung zu externen Diensten herstellen. Kilo fragt vor der Verwendung ihrer Werkzeuge um Erlaubnis, sofern Ihre Berechtigungen dies nicht automatisch erlauben.",
"marketplace.install.project.warning":
"Projektdateien können in die Versionsverwaltung aufgenommen werden. Speichern Sie hier keine Geheimnisse, es sei denn, die Konfiguration verweist auf eine Umgebungsvariable.",
"marketplace.install.learnMore": "Erfahren Sie, wie Installationen aus dem Marketplace funktionieren",
"marketplace.install.learnMcp": "Mehr über MCP erfahren",
"marketplace.install.installedAt": "Installiert unter {{path}}",
"marketplace.intro":
"Installieren Sie wiederverwendbare Agenten, Skills und MCP-Werkzeuge für ein Projekt oder für alle Projekte.",
"marketplace.intro.learnMore": "Über den Marketplace",
"marketplace.install.prerequisites": "Voraussetzungen",
"marketplace.install.installing": "Wird installiert...",
"marketplace.install.cancel": "Abbrechen",
+18 -1
View File
@@ -41,9 +41,26 @@ export const dict = {
"marketplace.card.showMore": "Show more",
"marketplace.card.showLess": "Show less",
"marketplace.install.title": "Install {{name}}",
"marketplace.install.scope": "Scope",
"marketplace.install.scope": "Where should this be available?",
"marketplace.install.scope.project": "Project",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Only this project. The installed files can be committed and shared with your team.",
"marketplace.install.scope.global.description": "All projects on this machine. Stored in your user configuration.",
"marketplace.install.destination": "Installation destination",
"marketplace.install.about.mcp":
"An MCP server gives Kilo additional tools for working with external services or local programs.",
"marketplace.install.about.agent": "An agent adds a reusable role with its own instructions and permissions.",
"marketplace.install.about.skill": "A skill adds task-specific instructions and resources that Kilo can load when needed.",
"marketplace.install.mcp.warning":
"MCP servers can run local commands or connect to external services. Kilo will ask for permission before using their tools unless your permissions allow them automatically.",
"marketplace.install.project.warning":
"Project files may be committed to version control. Do not store secrets here unless the configuration references an environment variable.",
"marketplace.install.learnMore": "Learn how Marketplace installs work",
"marketplace.install.learnMcp": "Learn more about MCP",
"marketplace.install.installedAt": "Installed to {{path}}",
"marketplace.intro": "Install reusable agents, skills, and MCP tools for one project or every project.",
"marketplace.intro.learnMore": "About Marketplace",
"marketplace.install.prerequisites": "Prerequisites",
"marketplace.install.installing": "Installing...",
"marketplace.install.cancel": "Cancel",
+20
View File
@@ -42,6 +42,26 @@ export const dict = {
"marketplace.install.scope": "Alcance",
"marketplace.install.scope.project": "Proyecto",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Solo este proyecto. Los archivos instalados se pueden añadir al control de versiones y compartir con tu equipo.",
"marketplace.install.scope.global.description":
"Todos los proyectos de este equipo. Se almacena en tu configuración de usuario.",
"marketplace.install.destination": "Destino de la instalación",
"marketplace.install.about.mcp":
"Un servidor MCP proporciona a Kilo herramientas adicionales para trabajar con servicios externos o programas locales.",
"marketplace.install.about.agent":
"Un agente añade un rol reutilizable con sus propias instrucciones y permisos.",
"marketplace.install.about.skill":
"Una habilidad añade instrucciones y recursos específicos para tareas que Kilo puede cargar cuando sea necesario.",
"marketplace.install.mcp.warning":
"Los servidores MCP pueden ejecutar comandos locales o conectarse a servicios externos. Kilo pedirá permiso antes de usar sus herramientas, a menos que tus permisos lo permitan automáticamente.",
"marketplace.install.project.warning":
"Los archivos del proyecto pueden añadirse al control de versiones. No guardes secretos aquí, a menos que la configuración haga referencia a una variable de entorno.",
"marketplace.install.learnMore": "Descubre cómo funcionan las instalaciones de Marketplace",
"marketplace.install.learnMcp": "Más información sobre MCP",
"marketplace.install.installedAt": "Instalado en {{path}}",
"marketplace.intro": "Instala agentes, habilidades y herramientas MCP reutilizables en uno o todos los proyectos.",
"marketplace.intro.learnMore": "Acerca de Marketplace",
"marketplace.install.prerequisites": "Requisitos previos",
"marketplace.install.installing": "Instalando...",
"marketplace.install.cancel": "Cancelar",
+21
View File
@@ -42,6 +42,27 @@ export const dict = {
"marketplace.install.scope": "Portée",
"marketplace.install.scope.project": "Projet",
"marketplace.install.scope.global": "Global",
"marketplace.install.scope.project.description":
"Uniquement ce projet. Les fichiers installés peuvent être ajoutés au contrôle de version et partagés avec votre équipe.",
"marketplace.install.scope.global.description":
"Tous les projets sur cette machine. Enregistré dans votre configuration utilisateur.",
"marketplace.install.destination": "Destination de l'installation",
"marketplace.install.about.mcp":
"Un serveur MCP fournit à Kilo des outils supplémentaires pour interagir avec des services externes ou des programmes locaux.",
"marketplace.install.about.agent":
"Un agent ajoute un rôle réutilisable avec ses propres instructions et autorisations.",
"marketplace.install.about.skill":
"Une compétence ajoute des instructions et des ressources propres à une tâche que Kilo peut charger en cas de besoin.",
"marketplace.install.mcp.warning":
"Les serveurs MCP peuvent exécuter des commandes locales ou se connecter à des services externes. Kilo demandera votre autorisation avant d'utiliser leurs outils, sauf si vos autorisations le permettent automatiquement.",
"marketplace.install.project.warning":
"Les fichiers du projet peuvent être ajoutés au contrôle de version. N'y stockez pas de secrets, sauf si la configuration fait référence à une variable d'environnement.",
"marketplace.install.learnMore": "Découvrir le fonctionnement des installations depuis le Marketplace",
"marketplace.install.learnMcp": "En savoir plus sur MCP",
"marketplace.install.installedAt": "Installé dans {{path}}",
"marketplace.intro":
"Installez des agents, des compétences et des outils MCP réutilisables pour un projet ou pour tous vos projets.",
"marketplace.intro.learnMore": "À propos du Marketplace",
"marketplace.install.prerequisites": "Prérequis",
"marketplace.install.installing": "Installation en cours...",
"marketplace.install.cancel": "Annuler",
+20
View File
@@ -45,6 +45,26 @@ export const dict = {
"marketplace.install.scope": "Ambito",
"marketplace.install.scope.project": "Progetto",
"marketplace.install.scope.global": "Globale",
"marketplace.install.scope.project.description":
"Solo questo progetto. I file installati possono essere aggiunti al controllo versione e condivisi con il team.",
"marketplace.install.scope.global.description":
"Tutti i progetti su questo computer. Viene salvato nella configurazione utente.",
"marketplace.install.destination": "Destinazione dell'installazione",
"marketplace.install.about.mcp":
"Un server MCP fornisce a Kilo strumenti aggiuntivi per interagire con servizi esterni o programmi locali.",
"marketplace.install.about.agent":
"Un agente aggiunge un ruolo riutilizzabile con istruzioni e autorizzazioni proprie.",
"marketplace.install.about.skill":
"Una skill aggiunge istruzioni e risorse specifiche per un'attività che Kilo può caricare quando necessario.",
"marketplace.install.mcp.warning":
"I server MCP possono eseguire comandi locali o connettersi a servizi esterni. Kilo chiederà l'autorizzazione prima di usare i loro strumenti, a meno che le tue autorizzazioni non lo consentano automaticamente.",
"marketplace.install.project.warning":
"I file del progetto possono essere aggiunti al controllo versione. Non salvare segreti qui, a meno che la configurazione non faccia riferimento a una variabile di ambiente.",
"marketplace.install.learnMore": "Scopri come funzionano le installazioni dal Marketplace",
"marketplace.install.learnMcp": "Scopri di più su MCP",
"marketplace.install.installedAt": "Installato in {{path}}",
"marketplace.intro": "Installa agenti, skill e strumenti MCP riutilizzabili per uno o tutti i progetti.",
"marketplace.intro.learnMore": "Informazioni sul Marketplace",
"marketplace.install.prerequisites": "Prerequisiti",
"marketplace.install.installing": "Installazione...",
"marketplace.install.cancel": "Annulla",
+20
View File
@@ -41,6 +41,26 @@ export const dict = {
"marketplace.install.scope": "スコープ",
"marketplace.install.scope.project": "プロジェクト",
"marketplace.install.scope.global": "グローバル",
"marketplace.install.scope.project.description":
"このプロジェクトのみ。インストールしたファイルはバージョン管理に追加し、チームと共有できます。",
"marketplace.install.scope.global.description":
"このマシン上のすべてのプロジェクト。ユーザー設定に保存されます。",
"marketplace.install.destination": "インストール先",
"marketplace.install.about.mcp":
"MCPサーバーは、外部サービスやローカルプログラムを操作するための追加ツールをKiloに提供します。",
"marketplace.install.about.agent": "エージェントは、独自の指示と権限を持つ再利用可能な役割を追加します。",
"marketplace.install.about.skill":
"スキルは、必要に応じてKiloが読み込めるタスク固有の指示とリソースを追加します。",
"marketplace.install.mcp.warning":
"MCPサーバーはローカルコマンドを実行したり、外部サービスに接続したりできます。権限で自動的に許可されていない限り、Kiloはツールを使用する前に許可を求めます。",
"marketplace.install.project.warning":
"プロジェクトファイルはバージョン管理に追加される場合があります。設定で環境変数を参照している場合を除き、ここにシークレットを保存しないでください。",
"marketplace.install.learnMore": "Marketplaceからのインストールの仕組みを見る",
"marketplace.install.learnMcp": "MCPについて詳しく見る",
"marketplace.install.installedAt": "{{path}} にインストール済み",
"marketplace.intro":
"再利用可能なエージェント、スキル、MCPツールを1つのプロジェクトまたはすべてのプロジェクトにインストールできます。",
"marketplace.intro.learnMore": "Marketplaceについて",
"marketplace.install.prerequisites": "前提条件",
"marketplace.install.installing": "インストール中...",
"marketplace.install.cancel": "キャンセル",
+20
View File
@@ -41,6 +41,26 @@ export const dict = {
"marketplace.install.scope": "범위",
"marketplace.install.scope.project": "프로젝트",
"marketplace.install.scope.global": "글로벌",
"marketplace.install.scope.project.description":
"이 프로젝트에만 적용됩니다. 설치된 파일을 버전 관리에 추가하고 팀과 공유할 수 있습니다.",
"marketplace.install.scope.global.description":
"이 컴퓨터의 모든 프로젝트에 적용됩니다. 사용자 구성에 저장됩니다.",
"marketplace.install.destination": "설치 위치",
"marketplace.install.about.mcp":
"MCP 서버는 외부 서비스나 로컬 프로그램과 작업할 수 있는 추가 도구를 Kilo에 제공합니다.",
"marketplace.install.about.agent": "에이전트는 자체 지침과 권한을 가진 재사용 가능한 역할을 추가합니다.",
"marketplace.install.about.skill":
"스킬은 필요할 때 Kilo가 불러올 수 있는 작업별 지침과 리소스를 추가합니다.",
"marketplace.install.mcp.warning":
"MCP 서버는 로컬 명령을 실행하거나 외부 서비스에 연결할 수 있습니다. 권한 설정에서 자동으로 허용하지 않는 한 Kilo는 도구를 사용하기 전에 권한을 요청합니다.",
"marketplace.install.project.warning":
"프로젝트 파일이 버전 관리에 추가될 수 있습니다. 구성에서 환경 변수를 참조하는 경우가 아니면 여기에 비밀 정보를 저장하지 마세요.",
"marketplace.install.learnMore": "Marketplace 설치 방식 알아보기",
"marketplace.install.learnMcp": "MCP 자세히 알아보기",
"marketplace.install.installedAt": "{{path}}에 설치됨",
"marketplace.intro":
"재사용 가능한 에이전트, 스킬 및 MCP 도구를 하나의 프로젝트 또는 모든 프로젝트에 설치하세요.",
"marketplace.intro.learnMore": "Marketplace 정보",
"marketplace.install.prerequisites": "사전 요구 사항",
"marketplace.install.installing": "설치 중...",
"marketplace.install.cancel": "취소",
+20
View File
@@ -44,6 +44,26 @@ export const dict = {
"marketplace.install.scope": "Scope",
"marketplace.install.scope.project": "Project",
"marketplace.install.scope.global": "Globaal",
"marketplace.install.scope.project.description":
"Alleen dit project. De geïnstalleerde bestanden kunnen aan versiebeheer worden toegevoegd en met je team worden gedeeld.",
"marketplace.install.scope.global.description":
"Alle projecten op deze computer. Wordt opgeslagen in je gebruikersconfiguratie.",
"marketplace.install.destination": "Installatielocatie",
"marketplace.install.about.mcp":
"Een MCP-server geeft Kilo extra hulpmiddelen om met externe diensten of lokale programma's te werken.",
"marketplace.install.about.agent": "Een agent voegt een herbruikbare rol toe met eigen instructies en machtigingen.",
"marketplace.install.about.skill":
"Een vaardigheid voegt taakspecifieke instructies en bronnen toe die Kilo indien nodig kan laden.",
"marketplace.install.mcp.warning":
"MCP-servers kunnen lokale opdrachten uitvoeren of verbinding maken met externe diensten. Kilo vraagt toestemming voordat hun hulpmiddelen worden gebruikt, tenzij je machtigingen dit automatisch toestaan.",
"marketplace.install.project.warning":
"Projectbestanden kunnen aan versiebeheer worden toegevoegd. Sla hier geen geheimen op, tenzij de configuratie naar een omgevingsvariabele verwijst.",
"marketplace.install.learnMore": "Lees hoe installaties vanuit Marketplace werken",
"marketplace.install.learnMcp": "Meer informatie over MCP",
"marketplace.install.installedAt": "Geïnstalleerd in {{path}}",
"marketplace.intro":
"Installeer herbruikbare agenten, vaardigheden en MCP-hulpmiddelen voor één project of voor alle projecten.",
"marketplace.intro.learnMore": "Over Marketplace",
"marketplace.install.prerequisites": "Vereisten",
"marketplace.install.installing": "Installeren...",
"marketplace.install.cancel": "Annuleren",
+19
View File
@@ -42,6 +42,25 @@ export const dict = {
"marketplace.install.scope": "Omfang",
"marketplace.install.scope.project": "Prosjekt",
"marketplace.install.scope.global": "Globalt",
"marketplace.install.scope.project.description":
"Bare dette prosjektet. De installerte filene kan legges til i versjonskontroll og deles med teamet ditt.",
"marketplace.install.scope.global.description":
"Alle prosjekter på denne maskinen. Lagres i brukerkonfigurasjonen din.",
"marketplace.install.destination": "Installasjonssted",
"marketplace.install.about.mcp":
"En MCP-server gir Kilo flere verktøy for å arbeide med eksterne tjenester eller lokale programmer.",
"marketplace.install.about.agent": "En agent legger til en gjenbrukbar rolle med egne instruksjoner og tillatelser.",
"marketplace.install.about.skill":
"En ferdighet legger til oppgavespesifikke instruksjoner og ressurser som Kilo kan laste inn ved behov.",
"marketplace.install.mcp.warning":
"MCP-servere kan kjøre lokale kommandoer eller koble til eksterne tjenester. Kilo ber om tillatelse før verktøyene brukes, med mindre tillatelsene dine automatisk tillater det.",
"marketplace.install.project.warning":
"Prosjektfiler kan legges til i versjonskontroll. Ikke lagre hemmeligheter her med mindre konfigurasjonen viser til en miljøvariabel.",
"marketplace.install.learnMore": "Finn ut hvordan installasjoner fra Marketplace fungerer",
"marketplace.install.learnMcp": "Finn ut mer om MCP",
"marketplace.install.installedAt": "Installert i {{path}}",
"marketplace.intro": "Installer gjenbrukbare agenter, ferdigheter og MCP-verktøy for ett eller alle prosjekter.",
"marketplace.intro.learnMore": "Om Marketplace",
"marketplace.install.prerequisites": "Forutsetninger",
"marketplace.install.installing": "Installerer...",
"marketplace.install.cancel": "Avbryt",
+20
View File
@@ -42,6 +42,26 @@ export const dict = {
"marketplace.install.scope": "Zakres",
"marketplace.install.scope.project": "Projekt",
"marketplace.install.scope.global": "Globalny",
"marketplace.install.scope.project.description":
"Tylko ten projekt. Zainstalowane pliki można dodać do systemu kontroli wersji i udostępnić zespołowi.",
"marketplace.install.scope.global.description":
"Wszystkie projekty na tym komputerze. Zapisywane w konfiguracji użytkownika.",
"marketplace.install.destination": "Miejsce instalacji",
"marketplace.install.about.mcp":
"Serwer MCP zapewnia Kilo dodatkowe narzędzia do pracy z usługami zewnętrznymi lub programami lokalnymi.",
"marketplace.install.about.agent": "Agent dodaje rolę wielokrotnego użytku z własnymi instrukcjami i uprawnieniami.",
"marketplace.install.about.skill":
"Umiejętność dodaje instrukcje i zasoby dotyczące określonych zadań, które Kilo może wczytać w razie potrzeby.",
"marketplace.install.mcp.warning":
"Serwery MCP mogą uruchamiać lokalne polecenia lub łączyć się z usługami zewnętrznymi. Kilo poprosi o pozwolenie przed użyciem ich narzędzi, chyba że uprawnienia zezwalają na to automatycznie.",
"marketplace.install.project.warning":
"Pliki projektu mogą zostać dodane do systemu kontroli wersji. Nie przechowuj tutaj sekretów, chyba że konfiguracja odwołuje się do zmiennej środowiskowej.",
"marketplace.install.learnMore": "Dowiedz się, jak działają instalacje z Marketplace",
"marketplace.install.learnMcp": "Dowiedz się więcej o MCP",
"marketplace.install.installedAt": "Zainstalowano w {{path}}",
"marketplace.intro":
"Instaluj agentów, umiejętności i narzędzia MCP wielokrotnego użytku w jednym lub we wszystkich projektach.",
"marketplace.intro.learnMore": "O Marketplace",
"marketplace.install.prerequisites": "Wymagania wstępne",
"marketplace.install.installing": "Instalowanie...",
"marketplace.install.cancel": "Anuluj",
+21
View File
@@ -42,6 +42,27 @@ export const dict = {
"marketplace.install.scope": "Область",
"marketplace.install.scope.project": "Проект",
"marketplace.install.scope.global": "Глобально",
"marketplace.install.scope.project.description":
"Только этот проект. Установленные файлы можно добавить в систему контроля версий и предоставить команде.",
"marketplace.install.scope.global.description":
"Все проекты на этом компьютере. Сохраняется в вашей пользовательской конфигурации.",
"marketplace.install.destination": "Место установки",
"marketplace.install.about.mcp":
"MCP-сервер предоставляет Kilo дополнительные инструменты для работы с внешними сервисами или локальными программами.",
"marketplace.install.about.agent":
"Агент добавляет многократно используемую роль с собственными инструкциями и разрешениями.",
"marketplace.install.about.skill":
"Навык добавляет инструкции и ресурсы для определённых задач, которые Kilo может загрузить при необходимости.",
"marketplace.install.mcp.warning":
"MCP-серверы могут выполнять локальные команды или подключаться к внешним сервисам. Kilo запросит разрешение перед использованием их инструментов, если только ваши разрешения не допускают это автоматически.",
"marketplace.install.project.warning":
"Файлы проекта могут быть добавлены в систему контроля версий. Не храните здесь секреты, если только конфигурация не ссылается на переменную окружения.",
"marketplace.install.learnMore": "Узнайте, как работает установка из Marketplace",
"marketplace.install.learnMcp": "Подробнее о MCP",
"marketplace.install.installedAt": "Установлено в {{path}}",
"marketplace.intro":
"Устанавливайте многократно используемых агентов, навыки и инструменты MCP для одного или всех проектов.",
"marketplace.intro.learnMore": "О Marketplace",
"marketplace.install.prerequisites": "Предварительные требования",
"marketplace.install.installing": "Установка...",
"marketplace.install.cancel": "Отмена",
+19
View File
@@ -42,6 +42,25 @@ export const dict = {
"marketplace.install.scope": "ขอบเขต",
"marketplace.install.scope.project": "โปรเจกต์",
"marketplace.install.scope.global": "โกลบอล",
"marketplace.install.scope.project.description":
"เฉพาะโปรเจกต์นี้ ไฟล์ที่ติดตั้งสามารถเพิ่มลงในระบบควบคุมเวอร์ชันและแชร์กับทีมของคุณได้",
"marketplace.install.scope.global.description":
"ทุกโปรเจกต์ในเครื่องนี้ จัดเก็บไว้ในการกำหนดค่าผู้ใช้ของคุณ",
"marketplace.install.destination": "ปลายทางการติดตั้ง",
"marketplace.install.about.mcp":
"เซิร์ฟเวอร์ MCP เพิ่มเครื่องมือให้ Kilo สำหรับทำงานกับบริการภายนอกหรือโปรแกรมในเครื่อง",
"marketplace.install.about.agent": "เอเจนต์เพิ่มบทบาทที่นำกลับมาใช้ใหม่ได้พร้อมคำสั่งและสิทธิ์ของตนเอง",
"marketplace.install.about.skill":
"ทักษะเพิ่มคำสั่งและทรัพยากรเฉพาะงานที่ Kilo สามารถโหลดได้เมื่อจำเป็น",
"marketplace.install.mcp.warning":
"เซิร์ฟเวอร์ MCP สามารถเรียกใช้คำสั่งในเครื่องหรือเชื่อมต่อกับบริการภายนอกได้ Kilo จะขออนุญาตก่อนใช้เครื่องมือ เว้นแต่สิทธิ์ของคุณจะอนุญาตโดยอัตโนมัติ",
"marketplace.install.project.warning":
"ไฟล์โปรเจกต์อาจถูกเพิ่มลงในระบบควบคุมเวอร์ชัน อย่าเก็บข้อมูลลับไว้ที่นี่ เว้นแต่การกำหนดค่าจะอ้างอิงตัวแปรสภาพแวดล้อม",
"marketplace.install.learnMore": "เรียนรู้วิธีการติดตั้งจาก Marketplace",
"marketplace.install.learnMcp": "เรียนรู้เพิ่มเติมเกี่ยวกับ MCP",
"marketplace.install.installedAt": "ติดตั้งไปยัง {{path}} แล้ว",
"marketplace.intro": "ติดตั้งเอเจนต์ ทักษะ และเครื่องมือ MCP ที่นำกลับมาใช้ใหม่ได้สำหรับหนึ่งโปรเจกต์หรือทุกโปรเจกต์",
"marketplace.intro.learnMore": "เกี่ยวกับ Marketplace",
"marketplace.install.prerequisites": "ข้อกำหนดเบื้องต้น",
"marketplace.install.installing": "กำลังติดตั้ง...",
"marketplace.install.cancel": "ยกเลิก",
+20
View File
@@ -42,6 +42,26 @@ export const dict = {
"marketplace.install.scope": "Kapsam",
"marketplace.install.scope.project": "Proje",
"marketplace.install.scope.global": "Genel",
"marketplace.install.scope.project.description":
"Yalnızca bu proje. Yüklenen dosyalar sürüm kontrolüne eklenebilir ve ekibinizle paylaşılabilir.",
"marketplace.install.scope.global.description":
"Bu makinedeki tüm projeler. Kullanıcı yapılandırmanızda saklanır.",
"marketplace.install.destination": "Yükleme hedefi",
"marketplace.install.about.mcp":
"Bir MCP sunucusu, harici hizmetler veya yerel programlarla çalışmak için Kilo'ya ek araçlar sağlar.",
"marketplace.install.about.agent": "Bir ajan, kendi talimatları ve izinleri olan yeniden kullanılabilir bir rol ekler.",
"marketplace.install.about.skill":
"Bir yetenek, Kilo'nun gerektiğinde yükleyebileceği göreve özel talimatlar ve kaynaklar ekler.",
"marketplace.install.mcp.warning":
"MCP sunucuları yerel komutları çalıştırabilir veya harici hizmetlere bağlanabilir. İzinleriniz otomatik olarak izin vermediği sürece Kilo, araçlarını kullanmadan önce izin ister.",
"marketplace.install.project.warning":
"Proje dosyaları sürüm kontrolüne eklenebilir. Yapılandırma bir ortam değişkenine başvurmuyorsa gizli bilgileri burada saklamayın.",
"marketplace.install.learnMore": "Marketplace yüklemelerinin nasıl çalıştığını öğrenin",
"marketplace.install.learnMcp": "MCP hakkında daha fazla bilgi edinin",
"marketplace.install.installedAt": "{{path}} konumuna yüklendi",
"marketplace.intro":
"Yeniden kullanılabilir ajanları, yetenekleri ve MCP araçlarını bir proje veya tüm projeler için yükleyin.",
"marketplace.intro.learnMore": "Marketplace hakkında",
"marketplace.install.prerequisites": "Ön koşullar",
"marketplace.install.installing": "Yükleniyor...",
"marketplace.install.cancel": "İptal",
+21
View File
@@ -42,6 +42,27 @@ export const dict = {
"marketplace.install.scope": "Область",
"marketplace.install.scope.project": "Проєкт",
"marketplace.install.scope.global": "Глобально",
"marketplace.install.scope.project.description":
"Лише цей проєкт. Встановлені файли можна додати до системи контролю версій і надати команді.",
"marketplace.install.scope.global.description":
"Усі проєкти на цьому комп'ютері. Зберігається у вашій користувацькій конфігурації.",
"marketplace.install.destination": "Місце встановлення",
"marketplace.install.about.mcp":
"MCP-сервер надає Kilo додаткові інструменти для роботи із зовнішніми сервісами або локальними програмами.",
"marketplace.install.about.agent":
"Агент додає багаторазову роль із власними інструкціями та дозволами.",
"marketplace.install.about.skill":
"Навичка додає інструкції та ресурси для певних завдань, які Kilo може завантажити за потреби.",
"marketplace.install.mcp.warning":
"MCP-сервери можуть виконувати локальні команди або підключатися до зовнішніх сервісів. Kilo запитає дозвіл перед використанням їхніх інструментів, якщо ваші дозволи не дають змоги робити це автоматично.",
"marketplace.install.project.warning":
"Файли проєкту можуть бути додані до системи контролю версій. Не зберігайте тут секрети, якщо конфігурація не посилається на змінну середовища.",
"marketplace.install.learnMore": "Дізнайтеся, як працює встановлення з Marketplace",
"marketplace.install.learnMcp": "Докладніше про MCP",
"marketplace.install.installedAt": "Встановлено в {{path}}",
"marketplace.intro":
"Встановлюйте багаторазових агентів, навички та інструменти MCP для одного або всіх проєктів.",
"marketplace.intro.learnMore": "Про Marketplace",
"marketplace.install.prerequisites": "Передумови",
"marketplace.install.installing": "Встановлення...",
"marketplace.install.cancel": "Скасувати",
+17
View File
@@ -40,6 +40,23 @@ export const dict = {
"marketplace.install.scope": "作用域",
"marketplace.install.scope.project": "项目",
"marketplace.install.scope.global": "全局",
"marketplace.install.scope.project.description":
"仅限此项目。安装的文件可以提交到版本控制并与你的团队共享。",
"marketplace.install.scope.global.description": "此计算机上的所有项目。存储在你的用户配置中。",
"marketplace.install.destination": "安装位置",
"marketplace.install.about.mcp":
"MCP 服务器为 Kilo 提供用于处理外部服务或本地程序的额外工具。",
"marketplace.install.about.agent": "智能体会添加一个具有专属指令和权限的可复用角色。",
"marketplace.install.about.skill": "技能会添加特定任务的指令和资源,Kilo 可在需要时加载它们。",
"marketplace.install.mcp.warning":
"MCP 服务器可以运行本地命令或连接外部服务。除非你的权限允许自动使用,否则 Kilo 会在使用其工具前请求许可。",
"marketplace.install.project.warning":
"项目文件可能会提交到版本控制。除非配置引用了环境变量,否则不要在此处存储密钥。",
"marketplace.install.learnMore": "了解 Marketplace 安装的工作方式",
"marketplace.install.learnMcp": "详细了解 MCP",
"marketplace.install.installedAt": "已安装到 {{path}}",
"marketplace.intro": "为一个项目或所有项目安装可复用的智能体、技能和 MCP 工具。",
"marketplace.intro.learnMore": "关于 Marketplace",
"marketplace.install.prerequisites": "先决条件",
"marketplace.install.installing": "安装中...",
"marketplace.install.cancel": "取消",
+17
View File
@@ -40,6 +40,23 @@ export const dict = {
"marketplace.install.scope": "作用域",
"marketplace.install.scope.project": "專案",
"marketplace.install.scope.global": "全域",
"marketplace.install.scope.project.description":
"僅限此專案。安裝的檔案可以提交至版本控制並與你的團隊分享。",
"marketplace.install.scope.global.description": "此電腦上的所有專案。儲存在你的使用者設定中。",
"marketplace.install.destination": "安裝位置",
"marketplace.install.about.mcp":
"MCP 伺服器為 Kilo 提供用於處理外部服務或本機程式的額外工具。",
"marketplace.install.about.agent": "智能體會新增一個具有專屬指示和權限的可重複使用角色。",
"marketplace.install.about.skill": "技能會新增特定任務的指示和資源,Kilo 可在需要時載入它們。",
"marketplace.install.mcp.warning":
"MCP 伺服器可以執行本機命令或連線至外部服務。除非你的權限允許自動使用,否則 Kilo 會在使用其工具前請求許可。",
"marketplace.install.project.warning":
"專案檔案可能會提交至版本控制。除非設定引用了環境變數,否則請勿在此儲存密鑰。",
"marketplace.install.learnMore": "瞭解 Marketplace 安裝的運作方式",
"marketplace.install.learnMcp": "深入瞭解 MCP",
"marketplace.install.installedAt": "已安裝至 {{path}}",
"marketplace.intro": "為一個專案或所有專案安裝可重複使用的智能體、技能和 MCP 工具。",
"marketplace.intro.learnMore": "關於 Marketplace",
"marketplace.install.prerequisites": "先決條件",
"marketplace.install.installing": "安裝中...",
"marketplace.install.cancel": "取消",
@@ -419,6 +419,10 @@ class KiloBackendAppService private constructor(
warnings = warns,
)
)
log.info(
"Application snapshot: profile=${if (prof != null) "loaded" else "not_logged_in"} " +
"warnings=${warns.size} notifications=${notifs.size} ${configSummary(cfg)}",
)
log.info("Application started — config, profile, notifications loaded")
} catch (e: TimeoutCancellationException) {
val err = LoadError(
@@ -639,6 +643,11 @@ class KiloBackendAppService private constructor(
return "${warn.path}: ${warn.message}$detail"
}
private fun configSummary(cfg: Config): String {
val text = cfg.toString()
return "configChars=${text.length} configHash=${text.hashCode().toUInt().toString(16)}"
}
private suspend fun restartConnection(reason: String) {
clear()
connection.restart()
@@ -15,6 +15,7 @@ import ai.kilocode.rpc.dto.PromptDto
import ai.kilocode.rpc.dto.QuestionReplyDto
import ai.kilocode.rpc.dto.QuestionRequestDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -89,10 +90,26 @@ class KiloBackendChatManager(
watcher = cs.launch {
sse.collect { event ->
if (event.type in CHAT_EVENTS) {
val events = normalizer.parse(event.type, event.data)
val events = try {
normalizer.parse(event.type, event.data)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log.warn(
"route=chat-events parse=false type=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}",
e,
)
return@collect
}
if (events != null) {
for (parsed in events) {
log.debug { ChatLogSummary.event(parsed) }
ChatLogSummary.error(parsed)?.let { error ->
log.warn(
"route=chat-events emit=true raw=${event.type} bytes=${event.data.length} " +
"subscribers=${_events.subscriptionCount.value} $error",
)
}
if (parsed is ChatEventDto.SessionStatusChanged && parsed.status.type != "busy") {
log.info(
"${ChatLogSummary.sid(parsed.sessionID)} kind=status route=chat-events emit=true " +
@@ -102,7 +119,7 @@ class KiloBackendChatManager(
_events.emit(parsed)
}
} else {
log.warn("SSE parse returned null for type=${event.type} bytes=${event.data.length}")
log.warn("route=chat-events parse=null type=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}")
}
}
}
@@ -171,6 +188,7 @@ class KiloBackendChatManager(
val detail = raw?.takeIf { it.isNotBlank() }?.let { ": ${ChatLogSummary.body(it)}" }.orEmpty()
throw RuntimeException("prompt_async failed: HTTP $code$detail")
}
log.info("${ChatLogSummary.sid(id)} kind=prompt op=prompt_async accepted=true code=$code ${ChatLogSummary.prompt(prompt)}")
log.debug { "${ChatLogSummary.sid(id)} kind=prompt op=prompt_async ok=true code=$code" }
}
} catch (e: RuntimeException) {
@@ -267,15 +267,13 @@ class KiloConnectionService(
override fun onFailure(src: EventSource, t: Throwable?, response: Response?) {
if (source.get() !== src) return
val detail = when {
t != null -> t.stackTraceToString()
response != null -> response.body?.string()
else -> null
}?.trim()?.ifEmpty { null }
val raw = response?.body?.string()?.trim()?.ifEmpty { null }
val body = raw?.let { ChatLogSummary.body(it) }
val detail = t?.stackTraceToString() ?: body
if (t != null) {
log.warn("SSE: failure (${t.message}) — scheduling reconnect")
log.warn("SSE: failure (${t.message}) code=${response?.code} body=${body ?: "none"} — scheduling reconnect", t)
} else {
log.warn("SSE: failure (HTTP ${response?.code}) — scheduling reconnect")
log.warn("SSE: failure (HTTP ${response?.code}) body=${body ?: "none"} — scheduling reconnect")
}
setState(ConnectionState.Error(t?.message ?: "SSE connection failed (HTTP ${response?.code})", detail))
scheduleReconnect()
@@ -907,6 +907,8 @@ object KiloCliDataParser {
msg,
statusCode = data?.long("statusCode")?.safeInt(),
responseBody = data?.str("responseBody"),
dataKeys = data?.keys?.sorted().orEmpty(),
ref = data?.str("ref"),
)
}
@@ -25,8 +25,11 @@ import ai.kilocode.rpc.dto.SessionListDto
import ai.kilocode.rpc.dto.SessionStatusDto
import com.intellij.openapi.components.service
import ai.kilocode.log.KiloLog
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
/**
* Backend implementation of [KiloSessionRpcApi].
@@ -37,7 +40,11 @@ import kotlinx.coroutines.flow.filter
* [KiloBackendSessionManager]. Chat operations delegate to
* [KiloBackendChatManager].
*/
class KiloSessionRpcApiImpl : KiloSessionRpcApi {
class KiloSessionRpcApiImpl internal constructor(
private val appOverride: KiloBackendAppService? = null,
private val log: KiloLog = LOG,
private val source: Flow<ChatEventDto>? = null,
) : KiloSessionRpcApi {
companion object {
private val LOG = KiloLog.create(KiloSessionRpcApiImpl::class.java)
}
@@ -52,7 +59,7 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
get() = app.chat
private val app: KiloBackendAppService
get() = service()
get() = appOverride ?: service()
override suspend fun list(directory: String): SessionListDto =
ready { workspaces.get(directory).sessions() }
@@ -62,7 +69,7 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
override suspend fun create(directory: String): SessionDto {
app.requireReady()
LOG.info("create session: directory=$directory")
log.info("create session: directory=$directory")
return workspaces.get(directory).createSession()
}
@@ -106,13 +113,13 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
override suspend fun prompt(id: String, directory: String, prompt: PromptDto) {
app.requireReady()
LOG.info("prompt RPC: session=$id, dir=$directory, parts=${prompt.parts.size}")
log.info("prompt RPC: session=$id, dir=$directory, parts=${prompt.parts.size}")
chat.prompt(id, directory, prompt)
}
override suspend fun command(id: String, directory: String, command: String, arguments: String, prompt: PromptDto) {
app.requireReady()
LOG.info("command RPC: session=$id, dir=$directory, command=$command, parts=${prompt.parts.size}")
log.info("command RPC: session=$id, dir=$directory, command=$command, parts=${prompt.parts.size}")
chat.command(id, directory, command, arguments, prompt)
}
@@ -129,40 +136,31 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
ready { chat.attachmentPart(id, directory, messageId, partId, attachmentKey) }
override suspend fun events(id: String, directory: String): Flow<ChatEventDto> =
chat.events.filter { event ->
val sid = when (event) {
is ChatEventDto.MessageUpdated -> event.sessionID
is ChatEventDto.PartUpdated -> event.sessionID
is ChatEventDto.PartDelta -> event.sessionID
is ChatEventDto.PartRemoved -> event.sessionID
is ChatEventDto.TurnOpen -> event.sessionID
is ChatEventDto.TurnClose -> event.sessionID
is ChatEventDto.SessionCreated -> event.sessionID
is ChatEventDto.Error -> event.sessionID
is ChatEventDto.MessageRemoved -> event.sessionID
is ChatEventDto.PermissionAsked -> event.sessionID
is ChatEventDto.PermissionReplied -> event.sessionID
is ChatEventDto.QuestionAsked -> event.sessionID
is ChatEventDto.QuestionReplied -> event.sessionID
is ChatEventDto.QuestionRejected -> event.sessionID
is ChatEventDto.SessionStatusChanged -> event.sessionID
is ChatEventDto.SessionUpdated -> event.sessionID
is ChatEventDto.SessionIdle -> event.sessionID
is ChatEventDto.SessionCompacted -> event.sessionID
is ChatEventDto.SessionDiffChanged -> event.sessionID
is ChatEventDto.TodoUpdated -> event.sessionID
(source ?: chat.events)
.onStart { log.info("${ChatLogSummary.sid(id)} kind=subscription route=rpc-events start=true dir=${ChatLogSummary.dir(directory)}") }
.filter { event ->
val sid = ChatLogSummary.sid(event)
val passes = event is ChatEventDto.SessionCreated || sid == null || sid == id
if (passes) log.debug { "${ChatLogSummary.sid(id)} pass=true ${ChatLogSummary.eventBody(event)}" }
else log.debug { "${ChatLogSummary.sid(id)} pass=false srcSid=$sid ${ChatLogSummary.eventBody(event)}" }
if (passes) {
ChatLogSummary.error(event)?.let { log.warn("${ChatLogSummary.sid(id)} route=rpc-events pass=true $it") }
}
if (passes && event is ChatEventDto.SessionStatusChanged && event.status.type != "busy") {
log.info(
"${ChatLogSummary.sid(id)} kind=status route=rpc-events pass=true " +
ChatLogSummary.status(event.status),
)
}
passes
}
val passes = event is ChatEventDto.SessionCreated || sid == null || sid == id
if (passes) LOG.debug { "${ChatLogSummary.sid(id)} pass=true ${ChatLogSummary.eventBody(event)}" }
else LOG.debug { "${ChatLogSummary.sid(id)} pass=false srcSid=$sid ${ChatLogSummary.eventBody(event)}" }
if (passes && event is ChatEventDto.SessionStatusChanged && event.status.type != "busy") {
LOG.info(
"${ChatLogSummary.sid(id)} kind=status route=rpc-events pass=true " +
ChatLogSummary.status(event.status),
)
.onCompletion { cause ->
if (cause == null || cause is CancellationException) {
log.info("${ChatLogSummary.sid(id)} kind=subscription route=rpc-events stop=true cancelled=${cause is CancellationException}")
return@onCompletion
}
log.warn("${ChatLogSummary.sid(id)} kind=subscription route=rpc-events stop=true failed message=${cause.message}", cause)
}
passes
}
override suspend fun updateConfig(directory: String, config: ConfigUpdateDto) =
ready { chat.updateConfig(directory, config) }
@@ -171,25 +169,25 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi {
override suspend fun replyPermission(requestId: String, directory: String, reply: PermissionReplyDto) {
app.requireReady()
LOG.info("replyPermission: requestId=$requestId, reply=${reply.reply}")
log.info("replyPermission: requestId=$requestId, reply=${reply.reply}")
chat.replyPermission(requestId, directory, reply)
}
override suspend fun savePermissionRules(requestId: String, directory: String, rules: PermissionAlwaysRulesDto) {
app.requireReady()
LOG.info("savePermissionRules: requestId=$requestId")
log.info("savePermissionRules: requestId=$requestId")
chat.savePermissionRules(requestId, directory, rules)
}
override suspend fun replyQuestion(requestId: String, directory: String, answers: QuestionReplyDto) {
app.requireReady()
LOG.info("replyQuestion: requestId=$requestId, answers=${answers.answers.size}")
log.info("replyQuestion: requestId=$requestId, answers=${answers.answers.size}")
chat.replyQuestion(requestId, directory, answers)
}
override suspend fun rejectQuestion(requestId: String, directory: String) {
app.requireReady()
LOG.info("rejectQuestion: requestId=$requestId")
log.info("rejectQuestion: requestId=$requestId")
chat.rejectQuestion(requestId, directory)
}
@@ -2,6 +2,7 @@ package ai.kilocode.backend.app
import ai.kilocode.backend.testing.MockCliServer
import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.ModelSelectionDto
import ai.kilocode.rpc.dto.PromptDto
import ai.kilocode.rpc.dto.PromptPartDto
@@ -11,8 +12,10 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import okhttp3.OkHttpClient
import java.util.concurrent.CountDownLatch
import kotlin.test.AfterTest
@@ -108,4 +111,22 @@ class KiloBackendChatManagerTest {
assertTrue(request.isCancelled)
}
@Test
fun `malformed session error logs warning and keeps collecting`() = runBlocking {
val port = mock.start()
val log = TestLog()
val sse = MutableSharedFlow<SseEvent>(replay = 8)
val chat = KiloBackendChatManager(scope, log)
chat.start(OkHttpClient(), port, sse)
val received = async { withTimeout(5_000) { chat.events.first() } }
sse.emit(SseEvent("session.error", """{"payload":{"properties":{"sessionID":"ses_abc","error":42}}}"""))
sse.emit(SseEvent("session.turn.open", """{"payload":{"properties":{"sessionID":"ses_abc"}}}"""))
val event = received.await()
assertTrue(event is ChatEventDto.TurnOpen)
assertEquals("ses_abc", event.sessionID)
assertTrue(log.messages.any { it.contains("route=chat-events parse=false type=session.error") }, log.messages.joinToString("\n"))
}
}
@@ -3,6 +3,7 @@ package ai.kilocode.backend.cli
import ai.kilocode.log.ChatLogSummary
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.MessageDto
import ai.kilocode.rpc.dto.MessageErrorDto
import ai.kilocode.rpc.dto.MessageTimeDto
import ai.kilocode.rpc.dto.PartDto
import ai.kilocode.rpc.dto.PermissionRequestDto
@@ -17,6 +18,7 @@ import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ChatLogSummaryTest {
@@ -196,4 +198,63 @@ class ChatLogSummaryTest {
assertTrue(out.contains("agent=code"), out)
assertTrue(out.contains("model=kilo/gpt-5"), out)
}
@Test
fun `message updated with message error is error-bearing`() {
val event = ChatEventDto.MessageUpdated(
sessionID = "ses_1",
info = MessageDto(
id = "msg_1",
sessionID = "ses_1",
role = "assistant",
time = MessageTimeDto(created = 0.0),
error = MessageErrorDto(
type = "APIError",
message = "Bad Request",
statusCode = 400,
responseBody = "secret provider payload",
),
),
)
val out = ChatLogSummary.error(event)
assertTrue(ChatLogSummary.hasError(event))
assertTrue(out!!.contains("sid=ses_1"), out)
assertTrue(out.contains("evt=message.updated"), out)
assertTrue(out.contains("mid=msg_1"), out)
assertTrue(out.contains("err=APIError"), out)
assertTrue(out.contains("code=400"), out)
assertFalse(out.contains("secret"), out)
}
@Test
fun `session error summary includes nested named error details by default`() {
val event = ChatEventDto.Error(
sessionID = "ses_1",
error = MessageErrorDto(
type = "UnknownError",
message = "Cannot find module '@kilocode/plugin' from '/workspace/.opencode/tool/github-triage.ts'",
dataKeys = listOf("message", "ref"),
ref = "err_123",
),
)
val out = ChatLogSummary.error(event)!!
assertTrue(out.contains("sid=ses_1"), out)
assertTrue(out.contains("evt=session.error"), out)
assertTrue(out.contains("err=UnknownError"), out)
assertTrue(out.contains("Cannot find module '@kilocode/plugin'"), out)
assertTrue(out.contains("dataKeys=message,ref"), out)
assertTrue(out.contains("ref=err_123"), out)
}
@Test
fun `non-error event has no error summary`() {
val event = ChatEventDto.TurnOpen("ses_1")
assertFalse(ChatLogSummary.hasError(event))
assertNull(ChatLogSummary.error(event))
}
}
@@ -564,6 +564,32 @@ class KiloCliDataParserTest {
assertEquals("""{"error":{"code":"PAID_MODEL_AUTH_REQUIRED"}}""", result.error?.responseBody)
}
@Test
fun `parseChatEvent - session error preserves nested named error details`() {
val data = globalEvent("""
"type": "session.error",
"properties": {
"sessionID": "ses_1",
"error": {
"name": "UnknownError",
"data": {
"message": "Cannot find module '@kilocode/plugin' from '/workspace/.opencode/tool/github-triage.ts'",
"ref": "err_123"
}
}
}
""")
val result = KiloCliDataParser.parseChatEvent("session.error", data)
assertNotNull(result)
assertTrue(result is ChatEventDto.Error)
assertEquals("ses_1", result.sessionID)
assertEquals("UnknownError", result.error?.type)
assertEquals("Cannot find module '@kilocode/plugin' from '/workspace/.opencode/tool/github-triage.ts'", result.error?.message)
assertEquals(listOf("message", "ref"), result.error?.dataKeys)
assertEquals("err_123", result.error?.ref)
}
@Test
fun `parseChatEvent - message removed`() {
val data = globalEvent("""
@@ -0,0 +1,52 @@
package ai.kilocode.backend.rpc
import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.ChatEventDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class KiloSessionRpcApiImplTest {
@Test
fun `events logs normal completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
val api = KiloSessionRpcApiImpl(log = log, source = flowOf(ChatEventDto.TurnOpen("ses_test")))
api.events("ses_test", "/test").toList()
assertTrue(log.messages.any { it.contains("route=rpc-events start=true") }, log.messages.joinToString("\n"))
assertTrue(log.messages.any { it.contains("route=rpc-events stop=true cancelled=false") }, log.messages.joinToString("\n"))
}
@Test
fun `events logs cancelled completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
val api = KiloSessionRpcApiImpl(log = log, source = flow { kotlinx.coroutines.awaitCancellation() })
val job = launch { api.events("ses_test", "/test").collect {} }
assertTrue(log.awaitMessage { it.contains("route=rpc-events start=true") })
job.cancelAndJoin()
assertTrue(log.messages.any { it.contains("route=rpc-events stop=true cancelled=true") }, log.messages.joinToString("\n"))
}
@Test
fun `events logs failed completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
val api = KiloSessionRpcApiImpl(log = log, source = flow { throw IllegalStateException("stream failed") })
assertFailsWith<IllegalStateException> {
api.events("ses_test", "/test").toList()
}
assertTrue(log.messages.any { it.contains("route=rpc-events stop=true failed message=stream failed") }, log.messages.joinToString("\n"))
}
}
@@ -24,6 +24,7 @@ import com.intellij.openapi.components.Service
import ai.kilocode.log.KiloLog
import com.intellij.openapi.project.Project
import fleet.rpc.client.durable
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,6 +32,8 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -46,6 +49,7 @@ class KiloSessionService internal constructor(
private val project: Project,
private val cs: CoroutineScope,
private val rpc: KiloSessionRpcApi?,
private val log: KiloLog = LOG,
) {
/** Platform constructor — resolves RPC from the service container. */
constructor(project: Project, cs: CoroutineScope) : this(project, cs, null)
@@ -82,7 +86,7 @@ class KiloSessionService internal constructor(
try {
list(dir)
} catch (e: Exception) {
LOG.warn("kind=session-list dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
log.warn("kind=session-list dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
}
}
}
@@ -108,9 +112,9 @@ class KiloSessionService internal constructor(
/** Create a new session. Caller awaits the result. */
suspend fun create(dir: String): SessionDto {
LOG.info("create: dir=$dir")
log.info("create: dir=$dir")
val session = call { create(dir) }
LOG.info("create: id=${session.id}")
log.info("create: id=${session.id}")
refresh(dir)
return session
}
@@ -121,7 +125,7 @@ class KiloSessionService internal constructor(
try {
deleteSession(id, dir)
} catch (e: Exception) {
LOG.warn("${ChatLogSummary.sid(id)} kind=session delete=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
log.warn("${ChatLogSummary.sid(id)} kind=session delete=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
}
}
}
@@ -149,7 +153,7 @@ class KiloSessionService internal constructor(
try {
call { setDirectory(id, dir) }
} catch (e: Exception) {
LOG.warn("${ChatLogSummary.sid(id)} kind=session setDirectory=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
log.warn("${ChatLogSummary.sid(id)} kind=session setDirectory=true dir=${ChatLogSummary.dir(dir)} failed message=${e.message}", e)
}
}
}
@@ -161,20 +165,20 @@ class KiloSessionService internal constructor(
/** Send a prompt to a session. */
suspend fun prompt(id: String, dir: String, dto: PromptDto) {
val meta = if (LOG.isDebugEnabled) {
val meta = if (log.isDebugEnabled) {
"${ChatLogSummary.dir(dir)} ${ChatLogSummary.prompt(dto)}"
} else {
"kind=prompt parts=${dto.parts.size}"
}
LOG.info("${ChatLogSummary.sid(id)} $meta")
log.info("${ChatLogSummary.sid(id)} $meta")
call { prompt(id, dir, dto) }
LOG.info("${ChatLogSummary.sid(id)} kind=prompt ok=true")
log.info("${ChatLogSummary.sid(id)} kind=prompt ok=true")
}
suspend fun command(id: String, dir: String, command: String, args: String, dto: PromptDto) {
LOG.info("${ChatLogSummary.sid(id)} kind=command command=$command parts=${dto.parts.size}")
log.info("${ChatLogSummary.sid(id)} kind=command command=$command parts=${dto.parts.size}")
call { command(id, dir, command, args, dto) }
LOG.info("${ChatLogSummary.sid(id)} kind=command ok=true")
log.info("${ChatLogSummary.sid(id)} kind=command ok=true")
}
/** Abort ongoing processing for a session. */
@@ -190,7 +194,7 @@ class KiloSessionService internal constructor(
/** Load message history for a session. */
suspend fun messages(id: String, dir: String): List<MessageWithPartsDto> =
call { messages(id, dir) }
.also { LOG.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(it)} ${ChatLogSummary.dir(dir)}" } }
.also { log.debug { "${ChatLogSummary.sid(id)} ${ChatLogSummary.history(it)} ${ChatLogSummary.dir(dir)}" } }
suspend fun attachmentPart(id: String, dir: String, message: String, part: String, key: String?): PartDto? =
call { attachmentPart(id, dir, message, part, key) }
@@ -198,19 +202,30 @@ class KiloSessionService internal constructor(
/** Subscribe to streaming chat events for a session. */
fun events(id: String, dir: String): Flow<ChatEventDto> {
val api = rpc
return if (api != null) flow {
val events = if (api != null) flow {
api.events(id, dir).collect {
LOG.debug { ChatLogSummary.event(it) }
log.debug { ChatLogSummary.event(it) }
ChatLogSummary.error(it)?.let { msg -> log.warn("${ChatLogSummary.sid(id)} route=client-events $msg") }
emit(it)
}
} else flow {
durable {
KiloSessionRpcApi.getInstance().events(id, dir).collect {
LOG.debug { ChatLogSummary.event(it) }
log.debug { ChatLogSummary.event(it) }
ChatLogSummary.error(it)?.let { msg -> log.warn("${ChatLogSummary.sid(id)} route=client-events $msg") }
emit(it)
}
}
}
return events
.onStart { log.info("${ChatLogSummary.sid(id)} kind=subscription route=client-events start=true dir=${ChatLogSummary.dir(dir)}") }
.onCompletion { cause ->
if (cause == null || cause is CancellationException) {
log.info("${ChatLogSummary.sid(id)} kind=subscription route=client-events stop=true cancelled=${cause is CancellationException}")
return@onCompletion
}
log.warn("${ChatLogSummary.sid(id)} kind=subscription route=client-events stop=true failed message=${cause.message}", cause)
}
}
/** Update config (model, agent/mode, temperature). */
@@ -642,7 +642,10 @@ class SessionUi(
spec.name,
KiloBundle.message(spec.descriptionKey),
spec.hints,
action,
{
Telemetry.send("Slash Command Used", mapOf("slashCommandType" to "client", "command" to spec.name))
action()
},
)
private fun mentionActions(): List<MentionAction> = MentionAction.ALL.map(::bind)
@@ -93,6 +93,7 @@ class SessionController(
private val openProfileAction: () -> Unit = {},
private val telemetry: (String, Map<String, String>) -> Unit = { event, props -> Telemetry.send(event, props) },
private val timers: UiTimerSource = UiTimers,
private val log: KiloLog = LOG,
) : Disposable {
private data class OrganizationTarget(val org: String?)
@@ -268,18 +269,19 @@ class SessionController(
private fun dispatch(data: Dispatch, send: suspend (String) -> Unit) {
assertEdt()
val props = data.props + if (data.kind == "command") slashProps() else emptyMap()
capture("Conversation Send Clicked", sessionProps(sid ?: ref?.key) + mapOf(
"source" to data.source,
"hasExistingSession" to data.exists.toString(),
"textLength" to bucket(data.text),
) + data.props)
) + props)
showSession()
val pending = sid?.let { CompletableDeferred(it) } ?: session()
cs.launch {
try {
val id = pending.await() ?: return@launch
send(id)
capture("Conversation Message", sessionProps(id) + mapOf("source" to data.source, "hasExistingSession" to data.exists.toString()) + data.props)
capture("Conversation Message", sessionProps(id) + mapOf("source" to data.source, "hasExistingSession" to data.exists.toString()) + props)
LOG.debug { "${ChatLogSummary.sid(id)} kind=${data.kind} dispatched=true" }
} catch (e: Exception) {
capture("Session Error", sessionProps(sid ?: ref?.key ?: data.start) + mapOf("context" to data.kind, "errorClass" to e::class.java.name))
@@ -863,6 +865,10 @@ class SessionController(
LOG.debug { "${ChatLogSummary.sid(id)} pass=true ${ChatLogSummary.eventBody(event)}" }
updates.enqueue(event)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log.warn("${ChatLogSummary.sid(id)} kind=subscription route=controller-events failed message=${e.message}", e)
} finally {
LOG.debug { "${ChatLogSummary.sid(id)} kind=subscription subscribe=false" }
}
@@ -881,6 +887,10 @@ class SessionController(
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-event child=$child ${ChatLogSummary.eventBody(event)}" }
updates.enqueue(event)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log.warn("${ChatLogSummary.sid(sid ?: "pending")} kind=child-subscription child=$child failed message=${e.message}", e)
} finally {
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-subscription child=$child subscribe=false" }
}
@@ -1531,8 +1541,18 @@ class SessionController(
put("attachmentCount", files.size.toString())
put("mediaAttachmentCount", files.count { it.mime?.startsWith("image/") == true || it.mime == "application/pdf" }.toString())
}
val mentions = files.filter { it.source?.text?.value?.startsWith("@") == true }
if (mentions.isNotEmpty()) {
val resources = mentions.count { it.source?.path == "git-changes" }
put("hasMentions", "true")
put("mentionCount", mentions.size.toString())
put("fileMentionCount", (mentions.size - resources).toString())
put("resourceMentionCount", resources.toString())
}
}
private fun slashProps() = mapOf("hasSlashCommand" to "true", "slashCommandType" to "server")
private fun bucket(text: String): String = when (text.length) {
0 -> "empty"
in 1..80 -> "short"
@@ -1,6 +1,8 @@
package ai.kilocode.client.app
import ai.kilocode.client.testing.FakeSessionRpcApi
import ai.kilocode.client.testing.TestLog
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.SessionDto
import ai.kilocode.rpc.dto.SessionTimeDto
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@@ -8,8 +10,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.test.assertFailsWith
@Suppress("UnstableApiUsage")
class KiloSessionServiceTest : BasePlatformTestCase() {
@@ -98,6 +105,40 @@ class KiloSessionServiceTest : BasePlatformTestCase() {
assertEquals(listOf("/workspace" to "make a plan"), rpc.enhancements)
}
fun `test events logs normal completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
service = KiloSessionService(project, scope, rpc, log)
rpc.eventFlow = { _, _ -> flowOf(ChatEventDto.TurnOpen("ses_test")) }
service.events("ses_test", "/test").toList()
assertTrue(log.messages.joinToString("\n"), log.messages.any { it.contains("route=client-events start=true") })
assertTrue(log.messages.joinToString("\n"), log.messages.any { it.contains("route=client-events stop=true cancelled=false") })
}
fun `test events logs cancelled completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
service = KiloSessionService(project, scope, rpc, log)
val job = launch { service.events("ses_test", "/test").collect {} }
assertTrue(log.awaitMessage { it.contains("route=client-events start=true") })
job.cancelAndJoin()
assertTrue(log.messages.joinToString("\n"), log.messages.any { it.contains("route=client-events stop=true cancelled=true") })
}
fun `test events logs failed completion`() = runBlocking(Dispatchers.Default) {
val log = TestLog()
service = KiloSessionService(project, scope, rpc, log)
rpc.eventFlow = { _, _ -> flow { throw IllegalStateException("stream failed") } }
assertFailsWith<IllegalStateException> {
service.events("ses_test", "/test").toList()
}
assertTrue(log.messages.joinToString("\n"), log.messages.any { it.contains("route=client-events stop=true failed message=stream failed") })
}
private fun session(id: String, title: String) = SessionDto(
id = id,
projectID = "prj",
@@ -48,8 +48,12 @@ class CommandLifecycleTest : SessionControllerTestBase() {
val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" }
assertEquals("command", sent.properties["source"])
assertEquals("true", sent.properties["hasSlashCommand"])
assertEquals("server", sent.properties["slashCommandType"])
val message = appRpc.telemetry.single { it.event == "Conversation Message" }
assertEquals("command", message.properties["source"])
assertEquals("true", message.properties["hasSlashCommand"])
assertEquals("server", message.properties["slashCommandType"])
}
fun `test command errors set state and telemetry`() {
@@ -9,11 +9,14 @@ import ai.kilocode.rpc.dto.AgentDto
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.ModelDto
import ai.kilocode.rpc.dto.PartDto
import ai.kilocode.rpc.dto.PartSourceDto
import ai.kilocode.rpc.dto.PartSourceTextDto
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
import ai.kilocode.rpc.dto.PermissionFileDiffDto
import ai.kilocode.rpc.dto.PermissionReplyDto
import ai.kilocode.rpc.dto.PermissionRequestDto
import ai.kilocode.rpc.dto.ProviderDto
import ai.kilocode.rpc.dto.PromptPartDto
import ai.kilocode.rpc.dto.QuestionInfoDto
import ai.kilocode.rpc.dto.QuestionOptionDto
import ai.kilocode.rpc.dto.QuestionReplyDto
@@ -45,6 +48,51 @@ class PromptLifecycleTest : SessionControllerTestBase() {
assertEquals("short", event.properties["textLength"])
}
fun `test prompt records aggregate mention telemetry`() {
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"))
projectRpc.state.value = workspaceReady()
val m = controller()
val files = listOf(
PromptPartDto(
type = "file",
mime = "text/plain",
url = "file:///repo/src/A.kt",
source = PartSourceDto("file", PartSourceTextDto("@src/A.kt", 0.0, 9.0), path = "src/A.kt"),
),
PromptPartDto(
type = "file",
mime = "text/plain",
url = "file:///repo/src/B.kt",
source = PartSourceDto("file", PartSourceTextDto("@src/B.kt", 10.0, 19.0), path = "/repo/src/B.kt"),
),
PromptPartDto(
type = "text",
text = "diff",
source = PartSourceDto("resource", PartSourceTextDto("@git-changes", 20.0, 32.0), path = "git-changes"),
),
)
flush()
edt { m.prompt("review", files) }
flush()
val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" }
assertEquals("true", sent.properties["hasMentions"])
assertEquals("3", sent.properties["mentionCount"])
assertEquals("2", sent.properties["fileMentionCount"])
assertEquals("1", sent.properties["resourceMentionCount"])
assertFalse(sent.properties.containsValue("@src/A.kt"))
assertFalse(sent.properties.containsValue("src/A.kt"))
assertFalse(sent.properties.containsValue("/repo/src/B.kt"))
val message = appRpc.telemetry.single { it.event == "Conversation Message" }
assertEquals("true", message.properties["hasMentions"])
assertEquals("3", message.properties["mentionCount"])
assertEquals("2", message.properties["fileMentionCount"])
assertEquals("1", message.properties["resourceMentionCount"])
assertFalse(message.properties.containsValue("@git-changes"))
assertFalse(message.properties.containsValue("git-changes"))
}
fun `test PermissionAsked moves state to AwaitingPermission`() {
val (m, _, _) = prompted()
@@ -13,6 +13,7 @@ import ai.kilocode.client.testing.TestUiTimers
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.app.Workspace
import ai.kilocode.client.session.SessionRef
import ai.kilocode.log.KiloLog
import ai.kilocode.rpc.dto.AgentDto
import ai.kilocode.rpc.dto.AgentsDto
import ai.kilocode.rpc.dto.ChatEventDto
@@ -134,8 +135,9 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
flushMs: Long = Long.MAX_VALUE,
displayMs: Long = Long.MAX_VALUE,
open: (SessionRef) -> Unit = {},
log: KiloLog? = null,
): SessionController {
return controller(id, flushMs, true, displayMs = displayMs, open = open)
return controller(id, flushMs, true, displayMs = displayMs, open = open, log = log)
}
protected fun controller(
@@ -155,6 +157,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
beforeUpdate: () -> Boolean = { false },
afterUpdate: (Boolean) -> Unit = {},
open: (SessionRef) -> Unit = {},
log: KiloLog? = null,
ref: SessionRef? = if (session != null) SessionRef.Local(session) else SessionRef.from(id),
): SessionController {
val root = Root()
@@ -174,6 +177,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
afterUpdate = afterUpdate,
telemetry = { event, props -> appRpc.telemetry.add(TelemetryCaptureDto(event, props)) },
timers = timers,
log = log ?: KiloLog.create(SessionController::class.java),
)
controllers.add(m)
roots[m] = root
@@ -0,0 +1,38 @@
package ai.kilocode.client.session.controller
import ai.kilocode.client.testing.TestLog
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.flow
class SessionSubscriptionTest : SessionControllerTestBase() {
override fun setUp() {
super.setUp()
rpc.session = rpc.session.copy(id = "ses_test")
appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5"))
projectRpc.state.value = workspaceReady()
}
fun `test controller event subscription logs failures`() {
val log = TestLog()
rpc.eventFlow = { _, _ -> flow { throw IllegalStateException("stream failed") } }
controller("ses_test", log = log)
flush()
assertTrue(log.messages.joinToString("\n"), log.messages.any { it.contains("kind=subscription route=controller-events failed message=stream failed") })
}
fun `test controller event subscription rethrows cancellation without failure log`() {
val log = TestLog()
rpc.eventFlow = { _, _ -> flow { throw CancellationException("stop") } }
controller("ses_test", log = log)
flush()
assertFalse(log.messages.joinToString("\n"), log.messages.any { it.contains("route=controller-events failed") })
}
}
@@ -17,6 +17,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.awt.event.InputEvent
import java.awt.event.MouseEvent
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.fail
@Suppress("UnstableApiUsage")
@@ -47,10 +49,11 @@ class MentionNavigatorTest : BasePlatformTestCase() {
editor = factory.createEditor(factory.createDocument(text), project) as EditorEx
navigator = MentionNavigator(editor, provider)
navigator.install()
provider.validate(text, -1) {}
waitFor {
provider.mentionAt(text, 6)?.resolved == true && provider.mentionAt(text, 20)?.resolved == false
}
val resolved = CountDownLatch(2)
provider.validate(text, -1) { resolved.countDown() }
assertTrue("mention validation did not complete", resolved.await(5, TimeUnit.SECONDS))
assertTrue(provider.mentionAt(text, 6)?.resolved == true)
assertTrue(provider.mentionAt(text, 20)?.resolved == false)
}
override fun tearDown() {
@@ -10,6 +10,7 @@ import ai.kilocode.rpc.dto.WorkspaceFileDto
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.concurrent.CopyOnWriteArrayList
/**
* Fake [KiloWorkspaceRpcApi] for testing.
@@ -41,7 +42,7 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
var globalConfigExists = true
val fileCalls = mutableListOf<Pair<String, String>>()
val searchQueries = mutableListOf<String>()
val opened = mutableListOf<String>()
val opened = CopyOnWriteArrayList<String>()
val localConfigs = mutableListOf<String>()
var globalConfigs = 0
var localConfigPathCalls = 0
@@ -0,0 +1,47 @@
package ai.kilocode.client.testing
import ai.kilocode.log.KiloLog
class TestLog : KiloLog {
private val items = mutableListOf<String>()
private val lock = Object()
val messages: List<String>
get() = synchronized(lock) { items.toList() }
override var isDebugEnabled: Boolean = true
fun awaitMessage(timeout: Long = 5_000, predicate: (String) -> Boolean): Boolean {
val end = System.currentTimeMillis() + timeout
synchronized(lock) {
while (items.none(predicate)) {
val wait = end - System.currentTimeMillis()
if (wait <= 0) return false
lock.wait(wait)
}
return true
}
}
override fun debug(block: () -> String) {
if (!isDebugEnabled) return
add("DEBUG: ${block()}")
}
override fun info(msg: String) {
add("INFO: $msg")
}
override fun warn(msg: String, t: Throwable?) {
add("WARN: $msg")
}
override fun error(msg: String, t: Throwable?) {
add("ERROR: $msg")
}
private fun add(msg: String) {
synchronized(lock) {
items.add(msg)
lock.notifyAll()
}
}
}
@@ -1,6 +1,6 @@
[versions]
intellij-platform = "2026.1"
intellij-gradle-plugin = "2.16.0"
intellij-gradle-plugin = "2.16.1-20260623.221413-11"
intellij-rpc-plugin = "2.3.20-RC2-0.1"
kotlin-jvm-plugin = "2.3.20"
kotlin-serialization-plugin = "2.3.20"
@@ -6,7 +6,19 @@ include("backend")
pluginManagement {
includeBuild("build-tasks")
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.jetbrains.intellij.platform") {
useModule("org.jetbrains.intellij.platform:intellij-platform-gradle-plugin:${requested.version}")
}
}
}
repositories {
maven("https://central.sonatype.com/repository/maven-snapshots/") {
content {
includeGroup("org.jetbrains.intellij.platform")
}
}
mavenCentral()
gradlePluginPortal()
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/")
@@ -2,6 +2,7 @@ package ai.kilocode.log
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.DiffFileDto
import ai.kilocode.rpc.dto.MessageErrorDto
import ai.kilocode.rpc.dto.MessageDto
import ai.kilocode.rpc.dto.MessageWithPartsDto
import ai.kilocode.rpc.dto.PartDto
@@ -233,6 +234,20 @@ object ChatLogSummary {
join("evt=$evt", rest)
}
fun hasError(event: ChatEventDto): Boolean = when (event) {
is ChatEventDto.Error -> true
is ChatEventDto.MessageUpdated -> event.info.error != null
else -> false
}
fun error(event: ChatEventDto): String? = when (event) {
is ChatEventDto.Error -> error(event.error, sid(event.sessionID), "evt=session.error")
is ChatEventDto.MessageUpdated -> event.info.error?.let { err ->
error(err, sid(event.sessionID), "evt=message.updated", "mid=${event.info.id}")
}
else -> null
}
private fun message(dto: MessageDto): String = join(
"mid=${dto.id}",
"role=${dto.role}",
@@ -241,6 +256,16 @@ object ChatLogSummary {
dto.error?.type?.let { "err=$it" },
)
private fun error(err: MessageErrorDto?, vararg parts: String): String = join(
*parts,
err?.type?.let { "err=$it" },
err?.statusCode?.let { "code=$it" },
err?.message?.let { msg -> statusPreview(msg)?.let { "message=\"$it\"" } },
err?.responseBody?.let { body(it) },
err?.dataKeys?.takeIf { it.isNotEmpty() }?.let { "dataKeys=${it.joinToString(",")}" },
err?.ref?.takeIf { it.isNotBlank() }?.let { "ref=$it" },
)
private fun part(dto: PartDto): String = join(
"mid=${dto.messageID}",
"pid=${dto.id}",
@@ -41,6 +41,8 @@ data class MessageErrorDto(
val message: String? = null,
val statusCode: Int? = null,
val responseBody: String? = null,
val dataKeys: List<String> = emptyList(),
val ref: String? = null,
)
@Serializable
+26 -67
View File
@@ -70,6 +70,13 @@ import { interceptMessage } from "./kilo-provider/git-changes-request"
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
import { clearCommandsCache, loadCommands } from "./kilo-provider/commands"
import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-page"
import {
dismissNotification,
fetchAndSendNotifications as fetchNotifications,
resetReadNotifications,
type NotificationsContext,
type NotificationsMessage,
} from "./kilo-provider/notifications"
import { childID } from "./kilo-provider/task-session"
import { VisibleTaskStreams } from "./kilo-provider/visible-task-streams"
import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network"
@@ -343,7 +350,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private pending = 0
private configWarningsShown = false
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: unknown = null
private cachedNotificationsMessage: NotificationsMessage | null = null
private pendingKiloModel: { modelID?: string; agent?: string } | null = null
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
private readyResolvers: (() => void)[] = []
@@ -1255,6 +1262,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "resetAllSettings":
await this.handleResetAllSettings()
break
case "resetReadNotifications":
await resetReadNotifications(this.notificationsContext())
break
case "telemetry":
TelemetryProxy.capture(message.event, message.properties)
break
@@ -2395,79 +2405,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
/**
* Fetch Kilo news/notifications and send to webview.
* Uses the cached message pattern so the webview gets data immediately on refresh.
*/
private async fetchAndSendNotifications(): Promise<void> {
if (!this.client) {
if (this.cachedNotificationsMessage) {
// Merge the latest dismissed IDs from globalState into the cached
// message so that dismissals persisted while offline are honoured.
const persisted = this.extensionContext?.globalState.get<string[]>("kilo.dismissedNotificationIds", []) ?? []
if (persisted.length > 0) {
const cached = this.cachedNotificationsMessage as {
type: string
notifications: unknown[]
dismissedIds: string[]
}
const merged = Array.from(new Set([...cached.dismissedIds, ...persisted]))
this.cachedNotificationsMessage = { ...cached, dismissedIds: merged }
}
this.postMessage(this.cachedNotificationsMessage)
}
return
private notificationsContext(): NotificationsContext {
return {
context: this.extensionContext,
client: this.client,
cached: () => this.cachedNotificationsMessage,
set: (message) => {
this.cachedNotificationsMessage = message
},
post: (message) => this.postMessage(message),
notify: (id) => this.connectionService.notifyNotificationDismissed(id),
}
}
try {
const { data: all } = await retry(() => this.client!.kilo.notifications(undefined, { throwOnError: true }))
const notifications = all.filter((n) => !n.showIn || n.showIn.includes("extension"))
const existing = this.extensionContext?.globalState.get<string[]>("kilo.dismissedNotificationIds", []) ?? []
const active = new Set(notifications.map((n) => n.id))
// Only prune stale dismissed IDs when we have a non-empty notification
// list. An empty list may mean the API returned nothing due to being
// unauthenticated (e.g. right after logout), not that all notifications
// are gone — pruning in that case would wipe the persisted dismissals.
const dismissedIds = notifications.length > 0 ? existing.filter((id) => active.has(id)) : existing
if (dismissedIds.length !== existing.length) {
await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", dismissedIds)
}
const message = { type: "notificationsLoaded", notifications, dismissedIds }
this.cachedNotificationsMessage = message
this.postMessage(message)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch notifications:", error)
}
private async fetchAndSendNotifications(): Promise<void> {
await fetchNotifications(this.notificationsContext())
}
// Cloud session methods extracted to kilo-provider/handlers/cloud-session.ts
/**
* Persist a dismissed notification ID in globalState and push updated lists to webview.
*/
private async handleDismissNotification(notificationId: string): Promise<void> {
if (!this.extensionContext) return
const existing = this.extensionContext.globalState.get<string[]>("kilo.dismissedNotificationIds", [])
if (!existing.includes(notificationId)) {
await this.extensionContext.globalState.update("kilo.dismissedNotificationIds", [...existing, notificationId])
}
// Update the cached message so the dismiss persists even if
// fetchAndSendNotifications() fails (e.g. no client / API error).
if (this.cachedNotificationsMessage) {
const cached = this.cachedNotificationsMessage as {
type: string
notifications: unknown[]
dismissedIds: string[]
}
if (!cached.dismissedIds.includes(notificationId)) {
this.cachedNotificationsMessage = {
...cached,
dismissedIds: [...cached.dismissedIds, notificationId],
}
}
}
await this.fetchAndSendNotifications()
this.connectionService.notifyNotificationDismissed(notificationId)
await dismissNotification(this.notificationsContext(), notificationId)
}
/** Read attention settings from VS Code config and push to webview. */
@@ -3334,6 +3292,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.extensionContext?.globalState.update("recentModels", undefined)
await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", undefined)
await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", undefined)
await this.extensionContext?.globalState.update("kilo.marketplace.dismissedSuggestions", undefined)
// Re-send all settings to the webview so the UI reflects the reset
this.postMessage(buildAutocompleteSettingsMessage())
@@ -36,6 +36,7 @@ export class MarketplacePanelProvider implements vscode.Disposable {
private generation = 0
private refresh: ReturnType<typeof setTimeout> | undefined
private statuses = new Map<string, SessionStatus["type"]>()
private pendingInstall: MarketplaceItem | undefined
private disposables: vscode.Disposable[] = []
private subscriptions: Array<() => void> = []
private readonly marketplace = new MarketplaceService()
@@ -83,6 +84,13 @@ export class MarketplacePanelProvider implements vscode.Disposable {
this.attach(panel, this.resolveProject())
}
/** Open the panel and surface the install dialog for a specific item, project scope preselected. */
openInstall(item: MarketplaceItem): void {
this.openPanel()
this.pendingInstall = item
this.flushPendingInstall()
}
dispose(): void {
this.panel?.dispose()
this.cleanup()
@@ -183,6 +191,7 @@ export class MarketplacePanelProvider implements vscode.Disposable {
if (this.connection.getConnectionState() === "connected") await this.sync(true)
else await this.connect()
await this.fetchData()
this.flushPendingInstall()
return
case "retryConnection":
await this.connect()
@@ -208,6 +217,14 @@ export class MarketplacePanelProvider implements vscode.Disposable {
}
}
/** Ask the webview to open the install dialog for a queued suggestion, once it can receive it. */
private flushPendingInstall(): void {
if (!this.pendingInstall || !this.ready) return
const item = this.pendingInstall
this.pendingInstall = undefined
this.post({ type: "openInstallModal", mpItem: item })
}
private scheduleRefresh(): void {
if (!this.ready) return
if (this.refresh) clearTimeout(this.refresh)
@@ -40,6 +40,7 @@ import { startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
import { buildKeybindingMap } from "./format-keybinding"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
import { ensureSandbox } from "./sandbox-bootstrap"
import { Semaphore } from "./semaphore"
import { PLATFORM } from "./constants"
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
@@ -856,6 +857,28 @@ export class AgentManagerProvider implements Disposable {
}
}
/** Remove a worktree whose session could not be safely initialized. */
private async discardWorktree(id: string, dir: string, branch: string, sessionId?: string): Promise<void> {
this.getStateManager()?.removeWorktree(id)
this.pushState()
if (sessionId) {
try {
await this.connectionService
.getClient()
.session.delete({ sessionID: sessionId, directory: dir }, { throwOnError: true })
} catch (err) {
this.log(`Failed to delete session ${sessionId} after worktree setup failed:`, err)
}
}
try {
await this.getWorktreeManager()?.removeWorktree(dir, branch)
} catch (err) {
this.log(`Failed to remove worktree ${id} after setup failed:`, err)
}
}
/** Send worktreeSetup.ready + sessionMeta + pushState after worktree creation. */
private notifyWorktreeReady(sessionId: string, result: CreateWorktreeResult, worktreeId?: string): void {
this.pushState()
@@ -1295,6 +1318,31 @@ export class AgentManagerProvider implements Disposable {
const state = this.getStateManager()!
state.addSession(session.id, wt.worktree.id)
// Sandbox must match the user's choice before this session is exposed or
// receives its initial prompt. A failed reconciliation aborts this version.
if (msg.sandbox !== undefined) {
try {
await ensureSandbox(this.connectionService.getClient(), session.id, wt.result.path, msg.sandbox)
} catch (error) {
const err = getErrorMessage(error)
this.log(`Failed to configure sandbox for ${session.id}: ${err}`)
this.postToWebview({
type: "agentManager.worktreeSetup",
status: "error",
message: `Failed to configure sandbox: ${err}`,
worktreeId: wt.worktree.id,
})
this.host.capture("Agent Manager Session Error", {
source: PLATFORM,
error: err,
context: "configureSandbox",
})
await this.discardWorktree(wt.worktree.id, wt.result.path, wt.result.branch, session.id)
continue
}
}
this.registerWorktreeSession(session.id, wt.result.path)
this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id)
@@ -0,0 +1,39 @@
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { sameDirectory } from "../kilo-provider-utils"
type State = {
directory: string
enabled: boolean
available: boolean
reason?: string
version: number
}
function unavailable(state: State) {
return new Error(state.reason ?? "Sandbox backend is unavailable")
}
function routed(state: State, dir: string) {
if (!sameDirectory(state.directory, dir)) throw new Error("Sandbox status resolved a different directory")
}
function confirm(state: State, dir: string, desired: boolean) {
routed(state, dir)
if (desired && !state.available) throw unavailable(state)
if (state.enabled !== desired) {
throw new Error(`Sandbox remained ${state.enabled ? "enabled" : "disabled"} after reconciliation`)
}
return state
}
/** Ensure a new session uses the selected sandbox state before its first prompt. */
export async function ensureSandbox(client: KiloClient, sid: string, dir: string, desired: boolean): Promise<State> {
const sandbox = client.sandbox
const { data: current } = await sandbox.status({ sessionID: sid, directory: dir }, { throwOnError: true })
routed(current, dir)
if (current.enabled === desired) return confirm(current, dir, desired)
if (!current.available) throw unavailable(current)
const { data: next } = await sandbox.toggle({ sessionID: sid, directory: dir }, { throwOnError: true })
return confirm(next, dir, desired)
}
@@ -439,6 +439,8 @@ interface CreateMultiVersionIn {
baseBranch?: string
branchName?: string
modelAllocations?: Array<{ providerID: string; modelID: string; count: number }>
/** When set, reconcile each created session's sandbox override to this state. */
sandbox?: boolean
}
interface RenameWorktreeIn {
+8
View File
@@ -8,6 +8,7 @@ import { DiffSourceCatalog } from "./diff/sources/catalog"
import { DiffVirtualProvider } from "./DiffVirtualProvider"
import { SettingsEditorProvider } from "./SettingsEditorProvider"
import { MarketplacePanelProvider } from "./MarketplacePanelProvider"
import { MarketplaceNotifier } from "./services/marketplace/notifier"
import { SubAgentViewerProvider } from "./SubAgentViewerProvider"
import { EXTENSION_DISPLAY_NAME } from "./constants"
import { KiloConnectionService } from "./services/cli-backend"
@@ -271,6 +272,13 @@ export function activate(context: vscode.ExtensionContext) {
const marketplacePanelProvider = new MarketplacePanelProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(settingsEditorProvider, marketplacePanelProvider)
// Surface a discardable notification when a marketplace item matches the workspace.
const marketplaceNotifier = new MarketplaceNotifier(connectionService, context, (item) =>
marketplacePanelProvider.openInstall(item),
)
context.subscriptions.push(marketplaceNotifier)
marketplaceNotifier.start()
// Create sub-agent viewer provider (read-only editor panel for sub-agent sessions)
const subAgentViewerProvider = new SubAgentViewerProvider(context.extensionUri, connectionService, context)
context.subscriptions.push(subAgentViewerProvider)
@@ -0,0 +1,86 @@
import * as vscode from "vscode"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { retry } from "../services/cli-backend/retry"
const KEY = "kilo.dismissedNotificationIds"
interface NotificationAction {
actionText: string
actionURL: string
}
interface NotificationItem {
id: string
title: string
message: string
action?: NotificationAction
showIn?: string[]
suggestModelId?: string
}
export interface NotificationsMessage {
type: "notificationsLoaded"
notifications: NotificationItem[]
dismissedIds: string[]
}
export interface NotificationsContext {
context: vscode.ExtensionContext | undefined
client: KiloClient | null
cached: () => NotificationsMessage | null
set: (message: NotificationsMessage) => void
post: (message: NotificationsMessage) => void
notify: (id: string) => void
}
export async function fetchAndSendNotifications(ctx: NotificationsContext): Promise<void> {
if (!ctx.client) {
const cached = ctx.cached()
if (cached) {
const persisted = ctx.context?.globalState.get<string[]>(KEY, []) ?? []
const dismissedIds =
persisted.length > 0 ? Array.from(new Set([...cached.dismissedIds, ...persisted])) : cached.dismissedIds
const message = { ...cached, dismissedIds }
if (message !== cached) ctx.set(message)
ctx.post(message)
}
return
}
try {
const { data: all } = await retry(() => ctx.client!.kilo.notifications(undefined, { throwOnError: true }))
const notifications = all.filter((n) => !n.showIn || n.showIn.includes("extension"))
const existing = ctx.context?.globalState.get<string[]>(KEY, []) ?? []
const active = new Set(notifications.map((n) => n.id))
const dismissedIds = notifications.length > 0 ? existing.filter((id) => active.has(id)) : existing
if (dismissedIds.length !== existing.length) await ctx.context?.globalState.update(KEY, dismissedIds)
const message = { type: "notificationsLoaded" as const, notifications, dismissedIds }
ctx.set(message)
ctx.post(message)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch notifications:", error)
}
}
export async function dismissNotification(ctx: NotificationsContext, id: string): Promise<void> {
if (!ctx.context) return
const existing = ctx.context.globalState.get<string[]>(KEY, [])
if (!existing.includes(id)) await ctx.context.globalState.update(KEY, [...existing, id])
const cached = ctx.cached()
if (cached && !cached.dismissedIds.includes(id)) {
ctx.set({
...cached,
dismissedIds: [...cached.dismissedIds, id],
})
}
await fetchAndSendNotifications(ctx)
ctx.notify(id)
}
export async function resetReadNotifications(ctx: NotificationsContext): Promise<void> {
await ctx.context?.globalState.update(KEY, undefined)
await fetchAndSendNotifications(ctx)
vscode.window.showInformationMessage("Read notifications have been reset.")
}
@@ -0,0 +1,123 @@
import * as os from "os"
import * as vscode from "vscode"
import { MarketplaceService } from "."
import { fetchMarketplaceData, type MarketplaceActionContext } from "./actions"
import { selectSuggestions, showSuggestionNotification, suggestionSlug } from "./notify"
import type { KiloConnectionService } from "../cli-backend"
import type { MarketplaceItem } from "./types"
const DISMISSED_KEY = "kilo.marketplace.dismissedSuggestions"
const DEBOUNCE = 1500
/** Opens the marketplace install flow for a suggested item. */
export type InstallHandler = (item: MarketplaceItem) => void
/**
* Scans the workspace for marketplace items annotated with relevant `suggest_for`
* metadata and surfaces a discardable VS Code notification offering a one-click
* install. Runs in the background, independent of the marketplace panel.
*/
export class MarketplaceNotifier implements vscode.Disposable {
private readonly marketplace = new MarketplaceService()
private disposables: vscode.Disposable[] = []
private timer: ReturnType<typeof setTimeout> | undefined
private generation = 0
private disposed = false
/** Slugs already shown this session so a single scan burst doesn't re-toast. */
private shown = new Set<string>()
constructor(
private readonly connection: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
private readonly install: InstallHandler,
) {
this.disposables.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => this.schedule()),
vscode.extensions.onDidChange(() => this.schedule()),
vscode.workspace.onDidCreateFiles(() => this.schedule()),
)
}
/** Begin the first background scan. Safe to call once after activation. */
start(): void {
this.schedule()
}
dispose(): void {
this.disposed = true
if (this.timer) clearTimeout(this.timer)
this.timer = undefined
this.generation++
for (const disposable of this.disposables) disposable.dispose()
this.disposables = []
this.marketplace.dispose()
}
private schedule(): void {
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.timer = undefined
void this.scan()
}, DEBOUNCE)
}
private dismissed(): string[] {
return this.context.globalState.get<string[]>(DISMISSED_KEY, []) ?? []
}
private async dismiss(slug: string): Promise<void> {
const existing = this.dismissed()
if (existing.includes(slug)) return
await this.context.globalState.update(DISMISSED_KEY, [...existing, slug])
}
private project(): string | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
}
private directory(): string {
return this.project() ?? os.homedir()
}
private roots(): vscode.Uri[] {
return vscode.workspace.workspaceFolders?.map((folder) => folder.uri) ?? []
}
private get ctx(): MarketplaceActionContext {
return { connection: this.connection, marketplace: this.marketplace, storage: this.context.globalStorageUri }
}
private async scan(): Promise<void> {
const generation = ++this.generation
const data = await fetchMarketplaceData(this.ctx, this.project(), this.directory(), this.roots()).catch(
(err: unknown) => {
console.warn("[Kilo New] Marketplace suggestion scan failed:", err)
return undefined
},
)
if (!data || generation !== this.generation) return
const installed = new Set([
...Object.keys(data.marketplaceInstalledMetadata.project),
...Object.keys(data.marketplaceInstalledMetadata.global),
])
const suggestions = selectSuggestions(data.marketplaceItems, data.marketplaceRelevance, [
...this.dismissed(),
...this.shown,
...installed,
])
if (suggestions.length === 0) return
// Surface one suggestion at a time to avoid stacking toasts.
const item = suggestions[0]
const slug = suggestionSlug(item)
this.shown.add(slug)
// A later rescan must never void the user's explicit choice, so only a
// disposed notifier short-circuits here — not a bumped generation.
const choice = await showSuggestionNotification(item)
if (this.disposed) return
if (choice?.action === "install") this.install(item)
if (choice?.action === "dismiss") await this.dismiss(slug)
}
}
@@ -0,0 +1,52 @@
import * as vscode from "vscode"
import type { MarketplaceItem, MarketplaceRelevanceMetadata } from "./types"
/** Stable, discardable identifier for a suggestion. Matches the relevance map key. */
export function suggestionSlug(item: Pick<MarketplaceItem, "id" | "type">): string {
return `${item.type}:${item.id}`
}
/**
* Pick the items worth surfacing as a notification: relevant to the workspace and
* not previously dismissed. Pure so it can be unit tested without VS Code.
*/
export function selectSuggestions(
items: MarketplaceItem[],
relevance: MarketplaceRelevanceMetadata,
dismissed: Iterable<string>,
): MarketplaceItem[] {
const skip = new Set(dismissed)
return items.filter((item) => {
const slug = suggestionSlug(item)
return Boolean(relevance[slug]) && !skip.has(slug)
})
}
export interface SuggestionChoice {
action: "install" | "dismiss"
item: MarketplaceItem
}
function describe(item: MarketplaceItem): string {
if (item.type === "agent") return `the ${item.name} agent`
if (item.type === "skill") return `the ${item.name} skill`
return `the ${item.name} MCP server`
}
/**
* Show a native VS Code notification for a matched item, offering a direct install
* and a persistent "Don't show again" dismissal. Resolves with the user's choice,
* or `undefined` if the toast was closed without picking an action.
*/
export async function showSuggestionNotification(item: MarketplaceItem): Promise<SuggestionChoice | undefined> {
const install = "Install"
const dismiss = "Don't show again"
const picked = await vscode.window.showInformationMessage(
`Kilo found ${describe(item)} that matches this workspace. Install it?`,
install,
dismiss,
)
if (picked === install) return { action: "install", item }
if (picked === dismiss) return { action: "dismiss", item }
return undefined
}
@@ -0,0 +1,49 @@
import { describe, expect, it } from "bun:test"
import { selectSuggestions, suggestionSlug } from "../../src/services/marketplace/notify"
import type { MarketplaceItem, MarketplaceRelevanceMetadata } from "../../src/services/marketplace/types"
const agent: MarketplaceItem = {
type: "agent",
id: "angular",
name: "Angular",
description: "Angular specialist",
category: "development",
content: { mode: "all", description: "Angular specialist", prompt: "Help with Angular" },
suggest_for: { filename: ["*.component.ts"] },
}
const mcp: MarketplaceItem = {
type: "mcp",
id: "jupyter",
name: "Jupyter",
description: "Jupyter notebooks",
category: "data",
url: "https://example.com",
content: "{}",
suggest_for: { vscode_extension: ["ms-toolsai.jupyter"] },
}
const items = [agent, mcp]
describe("Marketplace suggestion notification", () => {
it("derives a stable discardable slug from type and id", () => {
expect(suggestionSlug(agent)).toBe("agent:angular")
expect(suggestionSlug(mcp)).toBe("mcp:jupyter")
})
it("selects only relevant, non-dismissed items", () => {
const relevance: MarketplaceRelevanceMetadata = {
"agent:angular": { filename: ["*.component.ts"] },
"mcp:jupyter": { vscodeExtension: ["ms-toolsai.jupyter"] },
}
expect(selectSuggestions(items, relevance, [])).toEqual([agent, mcp])
expect(selectSuggestions(items, relevance, ["agent:angular"])).toEqual([mcp])
expect(selectSuggestions(items, relevance, ["agent:angular", "mcp:jupyter"])).toEqual([])
})
it("ignores items without a relevance match", () => {
const relevance: MarketplaceRelevanceMetadata = { "mcp:jupyter": { vscodeExtension: ["ms-toolsai.jupyter"] } }
expect(selectSuggestions(items, relevance, [])).toEqual([mcp])
})
})
@@ -3,8 +3,10 @@ import { readFileSync } from "node:fs"
import { join } from "node:path"
const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "PromptInput.tsx")
const src = readFileSync(path, "utf8")
const buttonPath = join(__dirname, "..", "..", "webview-ui", "src", "components", "shared", "SandboxButton.tsx")
const iconPath = join(__dirname, "..", "..", "..", "kilo-ui", "src", "components", "icon.tsx")
const src = readFileSync(path, "utf8")
const button = readFileSync(buttonPath, "utf8")
const icons = readFileSync(iconPath, "utf8")
describe("PromptInput connection guard", () => {
@@ -66,7 +68,7 @@ describe("PromptInput sandbox toggle", () => {
expect(src).toContain("<Show when={sandboxVisible()}>")
expect(src).toContain("{ action: toggleSandbox, enabled: () => sandboxVisible() && !sandboxDisabled() }")
expect(src).toContain('if (!sandboxVisible()) hidden.add("sandbox")')
expect(src).toContain("onClick={toggleSandbox}")
expect(src).toContain("onToggle={toggleSandbox}")
expect(src).toContain('message.type === "sandboxStatus"')
expect(src).toContain("message.sessionID !== sandboxID() && !matching")
expect(src).toContain("setSandboxState(state)")
@@ -75,8 +77,12 @@ describe("PromptInput sandbox toggle", () => {
expect(src).toContain("if (target !== undefined && target !== sessionID) clearSandboxRequest()")
expect(src).toContain("sandboxID() ? sandbox()?.enabled : sandboxDefault()?.enabled")
expect(src).toContain('type: "requestSandboxDefault", agentManagerContext: ctx()')
expect(src).toContain("aria-pressed={sandboxEnabled()}")
expect(src).toContain("<SandboxButtonBase")
expect(src).toContain("enabled={sandboxEnabled()}")
expect(src).toContain("available={sandboxReady() ? sandboxAvailable() : undefined}")
expect(src).toContain("!sandboxReady()")
expect(button).toContain("aria-pressed={props.enabled}")
expect(button).toContain('class={`prompt-status-button ${props.enabled ? "prompt-status-button--active" : ""}`}')
expect(src).toContain("if (sandboxRequest() && target === null) return")
expect(src).not.toContain("if (state === current) return true")
})
@@ -96,13 +102,14 @@ describe("PromptInput sandbox toggle", () => {
expect(src).toContain(
"const sandboxNetworkEnabled = () => config().experimental?.sandbox_restrict_network !== false",
)
expect(src).toContain('<Icon name="lock" size="small" />')
expect(src).toContain("<SandboxTooltipContent enabled={sandboxEnabled()} network={sandboxNetworkEnabled()} />")
expect(src).toContain('<Icon name="folder" size="small" />')
expect(src).toContain('<Icon name="globe" size="small" />')
expect(src).toContain("props.enabled && props.network")
expect(src).not.toContain('class="prompt-sandbox-network"')
expect(src).not.toContain('class="prompt-sandbox-icon"')
expect(src).toContain('tooltipClass="prompt-sandbox-tooltip-content"')
expect(button).toContain('<Icon name="lock" size="small" />')
expect(button).toContain('<Icon name="folder" size="small" />')
expect(button).toContain('<Icon name="globe" size="small" />')
expect(button).toContain("props.enabled && props.network")
expect(button).not.toContain('class="prompt-sandbox-network"')
expect(button).not.toContain('class="prompt-sandbox-icon"')
expect(icons).toContain("globe: {")
})
})
@@ -0,0 +1,144 @@
import { describe, expect, test } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { createKiloClient } from "@kilocode/sdk/v2/client"
import { ensureSandbox } from "../../src/agent-manager/sandbox-bootstrap"
type State = {
directory: string
enabled: boolean
available: boolean
reason?: string
version: number
}
function setup(states: State[]) {
const calls: string[] = []
const fetch = Object.assign(
async (input: RequestInfo | URL, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init)
calls.push(`${request.method} ${new URL(request.url).pathname}`)
const state = states.shift()
if (!state) return Response.json({ message: "Unexpected request" }, { status: 500 })
return Response.json(state)
},
{ preconnect: globalThis.fetch.preconnect },
) satisfies typeof globalThis.fetch
return {
calls,
client: createKiloClient({ baseUrl: "http://localhost", fetch }),
}
}
function state(enabled: boolean, available = true, directory = "/repo"): State {
return { directory, enabled, available, version: 1 }
}
describe("ensureSandbox", () => {
test("does not toggle when the effective state already matches", async () => {
const ctx = setup([state(true)])
const result = await ensureSandbox(ctx.client, "session-1", "/repo", true)
expect(result.enabled).toBe(true)
expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"])
})
test("toggles and verifies the selected state", async () => {
const ctx = setup([state(false), state(true)])
const result = await ensureSandbox(ctx.client, "session-1", "/repo", true)
expect(result.enabled).toBe(true)
expect(ctx.calls).toEqual(["GET /session/session-1/sandbox", "POST /session/session-1/sandbox/toggle"])
})
test("rejects unavailable sandboxing when sandbox was requested", async () => {
const unavailable = { ...state(false, false), reason: "Sandbox backend unavailable" }
const ctx = setup([unavailable])
expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow("Sandbox backend unavailable")
expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"])
})
test("allows an effectively disabled sandbox when the backend is unavailable", async () => {
const ctx = setup([state(false, false)])
const result = await ensureSandbox(ctx.client, "session-1", "/repo", false)
expect(result.enabled).toBe(false)
expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"])
})
test("rejects a toggle that does not reach the selected state", async () => {
const ctx = setup([state(false), state(false)])
expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow(
"Sandbox remained disabled after reconciliation",
)
})
test("rejects status returned for a different directory without toggling", async () => {
const ctx = setup([state(false, true, "/other")])
expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow(
"Sandbox status resolved a different directory",
)
expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"])
})
})
describe("Agent Manager sandbox startup", () => {
const provider = readFileSync(join(__dirname, "..", "..", "src", "agent-manager", "AgentManagerProvider.ts"), "utf8")
const dialog = readFileSync(
join(__dirname, "..", "..", "webview-ui", "agent-manager", "NewWorktreeDialog.tsx"),
"utf8",
)
test("reconciles before exposing or prompting the session", () => {
const start = provider.indexOf("private async onCreateMultiVersion")
const end = provider.indexOf("\n private ", start + 1)
const body = provider.slice(start, end)
const ensure = body.indexOf("await ensureSandbox")
const discard = body.indexOf("await this.discardWorktree", ensure)
const skip = body.indexOf("continue", discard)
const register = body.indexOf("this.registerWorktreeSession", ensure)
const ready = body.indexOf("this.notifyWorktreeReady", register)
const created = body.indexOf("created.push", ready)
const prompt = body.indexOf('type: "agentManager.sendInitialMessage"', created)
expect(ensure).toBeGreaterThan(-1)
expect(discard).toBeGreaterThan(ensure)
expect(skip).toBeGreaterThan(discard)
expect(register).toBeGreaterThan(skip)
expect(ready).toBeGreaterThan(register)
expect(created).toBeGreaterThan(ready)
expect(prompt).toBeGreaterThan(created)
})
test("deletes the fresh branch when sandbox setup rolls back", () => {
expect(provider).toContain("private async discardWorktree(id: string, dir: string, branch: string")
expect(provider).toContain("removeWorktree(dir, branch)")
expect(provider).toContain("wt.result.path, wt.result.branch, session.id")
})
test("uses the experiment-aware visibility condition for UI and payload", () => {
expect(dialog).toContain("const sandboxVisible = () => isSandboxVisible(features(), config())")
expect(dialog).toContain("sandbox: sandboxVisible() ? sandbox() : undefined")
expect(dialog).toContain("<Show when={sandboxVisible()}>")
})
test("places the sandbox toggle with prompt actions instead of model selectors", () => {
const selectors = dialog.indexOf('<div class="prompt-input-hint-selectors">')
const actions = dialog.indexOf('<div class="prompt-input-hint-actions">', selectors)
const sandbox = dialog.indexOf("<SandboxButtonBase", actions)
const speech = dialog.indexOf("<SpeechToTextButton", actions)
expect(selectors).toBeGreaterThan(-1)
expect(actions).toBeGreaterThan(selectors)
expect(dialog.slice(selectors, actions)).not.toContain("<SandboxButtonBase")
expect(sandbox).toBeGreaterThan(actions)
expect(speech).toBeGreaterThan(sandbox)
})
})
@@ -20,7 +20,9 @@ import { ModelSelectorBase } from "../src/components/shared/ModelSelector"
import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher"
import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { visible as isSandboxVisible } from "../src/components/settings/sandboxing"
import { ThinkingSelectorBase } from "../src/components/shared/ThinkingSelector"
import { SandboxButtonBase, SandboxTooltipContent } from "../src/components/shared/SandboxButton"
import {
MultiModelSelector,
type ModelAllocations,
@@ -71,7 +73,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
const server = useServer()
const session = useSession()
const provider = useProvider()
const { config } = useConfig()
const { config, features } = useConfig()
const metrics = tracker(vscode)
const track = (button: string, properties?: Record<string, string | number | boolean | undefined>) =>
metrics.track(button, "configure_worktree_dialog", properties)
@@ -102,6 +104,8 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
const [compareOpen, setCompareOpen] = createSignal(false)
const [highlightedIndex, setHighlightedIndex] = createSignal(0)
const [variant, setVariant] = createSignal<string | undefined>(session.currentVariant())
const [sandbox, setSandbox] = createSignal(config().experimental?.sandbox === true)
const sandboxVisible = () => isSandboxVisible(features(), config())
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates())
const speechModel = () => selectedSpeechToTextModel(config())
@@ -246,6 +250,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
baseBranch: advanced ? (baseBranch() ?? undefined) : undefined,
branchName: customBranch,
modelAllocations: allocations,
sandbox: sandboxVisible() ? sandbox() : undefined,
files: imgFiles,
})
@@ -460,6 +465,24 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
</Show>
</div>
<div class="prompt-input-hint-actions">
<Show when={sandboxVisible()}>
<SandboxButtonBase
enabled={sandbox()}
tooltip={
<SandboxTooltipContent
enabled={sandbox()}
network={config().experimental?.sandbox_restrict_network !== false}
/>
}
tooltipClass="prompt-sandbox-tooltip-content"
onToggle={click(
"sandbox_toggle",
"configure_worktree_dialog",
() => setSandbox(!sandbox()),
() => ({ enabled: !sandbox() }),
)}
/>
</Show>
<Show when={canUseSpeech()}>
<SpeechToTextButton speech={speech} disabled={starting()} start={startSpeech} label={t} />
</Show>
@@ -20,6 +20,7 @@ import { useConfig } from "../../context/config"
import { useProvider } from "../../context/provider"
import { ModelSelector } from "../shared/ModelSelector"
import { ModeSwitcher } from "../shared/ModeSwitcher"
import { SandboxButtonBase, SandboxTooltipContent } from "../shared/SandboxButton"
import { SpeechToTextButton } from "../speech-to-text/SpeechToTextButton"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../speech-to-text/availability"
import { ThinkingSelector } from "../shared/ThinkingSelector"
@@ -84,49 +85,6 @@ interface PromptInputProps {
pendingSessionID?: string
}
export const SandboxTooltipContent: Component<{ enabled: boolean; network: boolean }> = (props) => {
const language = useLanguage()
return (
<div class="prompt-sandbox-tooltip">
<div class="prompt-sandbox-tooltip-title">
{language.t(props.enabled ? "prompt.action.sandbox.status.enabled" : "prompt.action.sandbox.status.disabled")}
</div>
<div class="prompt-sandbox-tooltip-row">
<Icon name="folder" size="small" />
<span>{language.t("prompt.action.sandbox.filesystem")}</span>
<span class="prompt-sandbox-tooltip-state">
{language.t(
props.enabled ? "prompt.action.sandbox.filesystem.restricted" : "prompt.action.sandbox.unrestricted",
)}
</span>
</div>
<div class="prompt-sandbox-tooltip-row">
<Icon name="globe" size="small" />
<span>{language.t("prompt.action.sandbox.network")}</span>
<span class="prompt-sandbox-tooltip-state">
{language.t(
props.enabled && props.network
? "prompt.action.sandbox.network.blocked"
: props.enabled
? "prompt.action.sandbox.network.allowed"
: "prompt.action.sandbox.unrestricted",
)}
</span>
</div>
<div class="prompt-sandbox-tooltip-description">
{language.t(
props.enabled
? "prompt.action.sandbox.description.enabled"
: props.network
? "prompt.action.sandbox.description.disabled"
: "prompt.action.sandbox.description.disabledNetworkAllowed",
)}
</div>
</div>
)
}
export const PromptInput: Component<PromptInputProps> = (props) => {
const session = useSession()
const server = useServer()
@@ -1308,33 +1266,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Button>
</Tooltip>
<Show when={sandboxVisible()}>
<Tooltip
value={
!sandboxAvailable() && sandboxReady() ? (
(sandboxReason() ?? language.t("common.requestFailed"))
) : (
<SandboxTooltipContent enabled={sandboxEnabled()} network={sandboxNetworkEnabled()} />
)
}
contentClass="prompt-sandbox-tooltip-content"
placement="top"
>
<Button
variant="ghost"
size="small"
onClick={toggleSandbox}
disabled={sandboxDisabled()}
aria-label={
sandboxEnabled()
? language.t("prompt.action.sandbox.disable")
: language.t("prompt.action.sandbox.enable")
}
aria-pressed={sandboxEnabled()}
class={`prompt-status-button ${sandboxEnabled() ? "prompt-status-button--active" : ""}`}
>
<Icon name="lock" size="small" />
</Button>
</Tooltip>
<SandboxButtonBase
enabled={sandboxEnabled()}
available={sandboxReady() ? sandboxAvailable() : undefined}
reason={sandboxReason()}
disabled={sandboxDisabled()}
tooltip={<SandboxTooltipContent enabled={sandboxEnabled()} network={sandboxNetworkEnabled()} />}
tooltipClass="prompt-sandbox-tooltip-content"
onToggle={toggleSandbox}
/>
</Show>
<Tooltip value={language.t("prompt.action.enhance")} placement="top">
<Button
@@ -17,6 +17,9 @@ interface ScopeOption {
label: string
}
const MARKETPLACE_DOCS = "https://kilo.ai/docs/customize/marketplace"
const MCP_DOCS = "https://kilo.ai/docs/automate/mcp/what-is-mcp"
interface Props {
item: MarketplaceItem
onClose: () => void
@@ -44,8 +47,32 @@ export const InstallModal = (props: Props) => {
const initial = workspace() ? options()[0] : options()[0]
const [scope, setScope] = createSignal<ScopeOption>(initial)
const [installing, setInstalling] = createSignal(false)
const [result, setResult] = createSignal<{ success: boolean; error?: string } | null>(null)
const [result, setResult] = createSignal<{
success: boolean
error?: string
scope: "project" | "global"
path: string
hasParameters: boolean
method?: string
} | null>(null)
const [params, setParams] = createSignal<Record<string, string>>({})
const [pending, setPending] = createSignal<{
scope: "project" | "global"
path: string
hasParameters: boolean
method?: string
} | null>(null)
const destination = (target = scope().value) => {
const base = target === "project" ? ".kilo" : "~/.config/kilo"
if (props.item.type === "mcp") return `${base}/kilo.json`
if (props.item.type === "agent") return `${base}/agents/${props.item.id}.md`
if (target === "project") return `.kilo/skills/${props.item.id}/`
return `~/.kilo/skills/${props.item.id}/`
}
const about = () => t(`marketplace.install.about.${props.item.type}`)
const scopeDescription = () => t(`marketplace.install.scope.${scope().value}.description`)
const openDocs = (url: string) => vscode.postMessage({ type: "openExternal", url })
// MCP installation methods
const methods = (): McpInstallationMethod[] => {
@@ -82,12 +109,15 @@ export const InstallModal = (props: Props) => {
createEffect(() => {
const unsub = vscode.onMessage((msg) => {
if (msg.type === "marketplaceInstallResult" && msg.slug === props.item.id) {
const request = pending()
setInstalling(false)
setResult({ success: msg.success, error: msg.error })
props.onInstallResult(msg.success, scope().value, {
hasParameters: Object.keys(params()).length > 0,
installationMethodName: method()?.name,
if (!request) return
setResult({ success: msg.success, error: msg.error, ...request })
props.onInstallResult(msg.success, request.scope, {
hasParameters: request.hasParameters,
installationMethodName: request.method,
})
setPending(null)
}
})
onCleanup(unsub)
@@ -96,14 +126,22 @@ export const InstallModal = (props: Props) => {
const doInstall = () => {
setInstalling(true)
const paramValues: Record<string, unknown> = { ...params() }
if (method()) {
paramValues.__method = method()!.name
const current = method()
if (current) {
paramValues.__method = current.name
}
const target = scope().value
setPending({
scope: target,
path: destination(target),
hasParameters: Object.keys(params()).length > 0,
method: current?.name,
})
vscode.postMessage({
type: "installMarketplaceItem",
mpItem: props.item,
mpInstallOptions: {
target: scope().value,
target,
parameters: Object.keys(paramValues).length > 0 ? paramValues : undefined,
},
})
@@ -131,6 +169,20 @@ export const InstallModal = (props: Props) => {
<Dialog title={t("marketplace.install.title", { name: props.item.name })} fit>
<Show when={!result()}>
<div class="install-modal-body">
<div class="install-modal-about">
<p>{about()}</p>
<div class="install-modal-links">
<button type="button" class="link" onClick={() => openDocs(MARKETPLACE_DOCS)}>
{t("marketplace.install.learnMore")}
</button>
<Show when={props.item.type === "mcp"}>
<button type="button" class="link" onClick={() => openDocs(MCP_DOCS)}>
{t("marketplace.install.learnMcp")}
</button>
</Show>
</div>
</div>
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.scope")}</span>
<RadioGroup
@@ -140,8 +192,24 @@ export const InstallModal = (props: Props) => {
label={(x: ScopeOption) => x.label}
onSelect={(v: ScopeOption | undefined) => v && setScope(v)}
/>
<p class="install-modal-help">{scopeDescription()}</p>
<div class="install-modal-destination">
<span>{t("marketplace.install.destination")}</span>
<code>{destination()}</code>
</div>
</div>
<Show when={props.item.type === "mcp" || scope().value === "project"}>
<div class="install-modal-warning">
<Show when={props.item.type === "mcp"}>
<p>{t("marketplace.install.mcp.warning")}</p>
</Show>
<Show when={scope().value === "project"}>
<p>{t("marketplace.install.project.warning")}</p>
</Show>
</div>
</Show>
<Show when={methods().length > 1}>
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.method")}</span>
@@ -215,6 +283,7 @@ export const InstallModal = (props: Props) => {
}
>
<p class="install-modal-success">{t("marketplace.install.success")}</p>
<p class="install-modal-result-path">{t("marketplace.install.installedAt", { path: r().path })}</p>
<div class="install-modal-footer">
<Button onClick={props.onClose}>{t("marketplace.install.done")}</Button>
</div>
@@ -12,6 +12,7 @@ import type {
MarketplaceRelevanceMetadata,
} from "../../types/marketplace"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import { filterItems, hasRelevantItems, retain } from "./utils"
import { ItemCard } from "./ItemCard"
import { MarketplaceContribute } from "./MarketplaceContribute"
@@ -36,6 +37,7 @@ interface Props {
export const MarketplaceListView = (props: Props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const [search, setSearch] = createSignal("")
const [status, setStatus] = createSignal<StatusOption>({ value: "all", label: t("marketplace.filter.all") })
const [types, setTypes] = createSignal<MarketplaceItem["type"][]>([])
@@ -109,6 +111,18 @@ export const MarketplaceListView = (props: Props) => {
return (
<div class="marketplace-list">
<div class="marketplace-intro">
<span>{t("marketplace.intro")}</span>
<button
type="button"
class="link"
onClick={() =>
vscode.postMessage({ type: "openExternal", url: "https://kilo.ai/docs/customize/marketplace" })
}
>
{t("marketplace.intro.learnMore")}
</button>
</div>
<div class="marketplace-filters">
<div class="marketplace-search-field">
<TextField placeholder={props.searchPlaceholder} value={search()} onChange={setSearch} />
@@ -49,6 +49,10 @@ export const MarketplaceView = () => {
setFetching(false)
setShowMigrationBanner(msg.showAgentMigrationBanner ?? false)
}
if (msg.type === "openInstallModal") {
const match = items().find((i) => i.type === msg.mpItem.type && i.id === msg.mpItem.id)
handleInstall(match ?? msg.mpItem)
}
if (msg.type === "marketplaceRemoveResult") {
const removed = pending()
setPending(null)
@@ -104,7 +108,6 @@ export const MarketplaceView = () => {
...(extra?.hasParameters && { hasParameters: true }),
...(extra?.installationMethodName && { installationMethodName: extra.installationMethodName }),
})
dialog.close()
fetchData()
}
}}
@@ -17,6 +17,32 @@
gap: 12px;
}
.marketplace-intro {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
color: var(--text-weak);
font-size: var(--kilo-font-size-12);
}
.marketplace-intro .link,
.install-modal-links .link {
all: unset;
color: var(--text-interactive-base);
cursor: pointer;
}
.marketplace-intro .link:hover,
.install-modal-links .link:hover {
text-decoration: underline;
}
.marketplace-intro .link:focus-visible,
.install-modal-links .link:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.marketplace-filters {
display: flex;
gap: 8px;
@@ -277,6 +303,57 @@
gap: 6px;
}
.install-modal-about,
.install-modal-warning,
.install-modal-destination {
padding: 10px;
border: 1px solid var(--border-base, var(--vscode-widget-border));
border-radius: 4px;
background: var(--surface-raised-base, var(--vscode-editorWidget-background));
}
.install-modal-about p,
.install-modal-warning p,
.install-modal-help,
.install-modal-result-path {
margin: 0;
color: var(--text-weak);
font-size: var(--kilo-font-size-12);
}
.install-modal-links {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-top: 6px;
font-size: var(--kilo-font-size-12);
}
.install-modal-warning {
display: flex;
flex-direction: column;
gap: 6px;
border-color: var(--border-warning-base);
}
.install-modal-destination {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 2px;
}
.install-modal-destination span {
color: var(--text-weak);
font-size: var(--kilo-font-size-11);
}
.install-modal-destination code {
color: var(--text-base);
font-size: var(--kilo-font-size-12);
overflow-wrap: anywhere;
}
.install-modal-label {
font-weight: 600;
font-size: var(--kilo-font-size-13);
@@ -309,7 +386,12 @@
color: var(--text-on-success-base);
font-size: var(--kilo-font-size-14);
font-weight: 600;
margin: 0 0 16px;
margin: 0 0 6px;
}
.install-modal-result-path {
margin-bottom: 16px;
overflow-wrap: anywhere;
}
.install-modal-error-msg {
@@ -357,21 +357,18 @@ const AboutKiloCodeTab: Component<AboutKiloCodeTabProps> = (props) => {
>
{language.t("settings.aboutKiloCode.resetSettings.description")}
</p>
<button
type="button"
onClick={() => vscode.postMessage({ type: "resetAllSettings" })}
style={{
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
padding: "6px 14px",
"border-radius": "2px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-12)",
}}
>
{language.t("settings.aboutKiloCode.resetSettings.button")}
</button>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<Button variant="primary" size="small" onClick={() => vscode.postMessage({ type: "resetAllSettings" })}>
{language.t("settings.aboutKiloCode.resetSettings.button")}
</Button>
<Button
variant="secondary"
size="small"
onClick={() => vscode.postMessage({ type: "resetReadNotifications" })}
>
{language.t("settings.aboutKiloCode.resetSettings.notificationsButton")}
</Button>
</div>
</div>
</div>
)
@@ -0,0 +1,88 @@
/** Shared sandbox lock control used by the chat prompt and Agent Manager. */
import { type Component, type JSX } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { Icon } from "@kilocode/kilo-ui/icon"
import { useLanguage } from "../../context/language"
export interface SandboxButtonBaseProps {
enabled: boolean
available?: boolean
reason?: string
disabled?: boolean
tooltip?: JSX.Element
tooltipClass?: string
onToggle: () => void
}
export const SandboxTooltipContent: Component<{ enabled: boolean; network: boolean }> = (props) => {
const language = useLanguage()
return (
<div class="prompt-sandbox-tooltip">
<div class="prompt-sandbox-tooltip-title">
{language.t(props.enabled ? "prompt.action.sandbox.status.enabled" : "prompt.action.sandbox.status.disabled")}
</div>
<div class="prompt-sandbox-tooltip-row">
<Icon name="folder" size="small" />
<span>{language.t("prompt.action.sandbox.filesystem")}</span>
<span class="prompt-sandbox-tooltip-state">
{language.t(
props.enabled ? "prompt.action.sandbox.filesystem.restricted" : "prompt.action.sandbox.unrestricted",
)}
</span>
</div>
<div class="prompt-sandbox-tooltip-row">
<Icon name="globe" size="small" />
<span>{language.t("prompt.action.sandbox.network")}</span>
<span class="prompt-sandbox-tooltip-state">
{language.t(
props.enabled && props.network
? "prompt.action.sandbox.network.blocked"
: props.enabled
? "prompt.action.sandbox.network.allowed"
: "prompt.action.sandbox.unrestricted",
)}
</span>
</div>
<div class="prompt-sandbox-tooltip-description">
{language.t(
props.enabled
? "prompt.action.sandbox.description.enabled"
: props.network
? "prompt.action.sandbox.description.disabled"
: "prompt.action.sandbox.description.disabledNetworkAllowed",
)}
</div>
</div>
)
}
export const SandboxButtonBase: Component<SandboxButtonBaseProps> = (props) => {
const language = useLanguage()
const unavailable = () => props.available === false
const tooltip = () =>
unavailable()
? (props.reason ?? language.t("common.requestFailed"))
: (props.tooltip ??
language.t(props.enabled ? "prompt.action.sandbox.enabled" : "prompt.action.sandbox.disabled"))
return (
<Tooltip value={tooltip()} contentClass={unavailable() ? undefined : props.tooltipClass} placement="top">
<Button
variant="ghost"
size="small"
onClick={props.onToggle}
disabled={props.disabled || unavailable()}
aria-label={
props.enabled ? language.t("prompt.action.sandbox.disable") : language.t("prompt.action.sandbox.enable")
}
aria-pressed={props.enabled}
class={`prompt-status-button ${props.enabled ? "prompt-status-button--active" : ""}`}
>
<Icon name="lock" size="small" />
</Button>
</Tooltip>
)
}
@@ -196,7 +196,7 @@ export const LanguageProvider: ParentComponent<LanguageProviderProps> = (props)
})
const t = (key: UiI18nKey, params?: UiI18nParams) => {
const text = (dict() as Record<string, string>)[key] ?? String(key)
const text = (dict() as Record<string, string>)[key] ?? (dicts.en as Record<string, string>)[key] ?? String(key)
return resolveTemplate(text, params)
}
+1
View File
@@ -1227,6 +1227,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"يؤدي هذا إلى إعادة تعيين الإعدادات الخاصة بامتداد VS Code فقط إلى قيمها الافتراضية. الإعدادات المشتركة مع CLI، مثل الأوضاع وقواعد الموافقة التلقائية، مخزّنة في تكوين CLI ولن تتأثر.",
"settings.aboutKiloCode.resetSettings.button": "إعادة تعيين جميع الإعدادات",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "نقل الإعدادات",
"settings.aboutKiloCode.settingsTransfer.description": "تصدير أو استيراد إعداداتك لنقلها بين نُسخ VS Code.",
"settings.aboutKiloCode.exportSettings": "تصدير",
+1
View File
@@ -1251,6 +1251,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Isso redefine apenas as configurações específicas da extensão VS Code para seus valores padrão. As configurações compartilhadas com o CLI, como modos e regras de aprovação automática, são armazenadas na configuração do CLI e não serão redefinidas.",
"settings.aboutKiloCode.resetSettings.button": "Redefinir Todas as Configurações",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Transferência de configurações",
"settings.aboutKiloCode.settingsTransfer.description":
"Exporte ou importe suas configurações para transferi-las entre instâncias do VS Code.",
+1
View File
@@ -1253,6 +1253,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Ovo resetuje samo postavke specifične za VS Code ekstenziju na njihove zadane vrijednosti. Postavke koje se dijele s CLI-jem, kao što su načini rada i pravila automatskog odobravanja, pohranjene su u CLI konfiguraciji i neće biti resetovane.",
"settings.aboutKiloCode.resetSettings.button": "Resetuj sve postavke",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Prijenos postavki",
"settings.aboutKiloCode.settingsTransfer.description":
"Izvezite ili uvezite postavke za prijenos između VS Code instanci.",
+1
View File
@@ -1247,6 +1247,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Dette nulstiller kun VS Code-udvidelsesspecifikke indstillinger til deres standardværdier. Indstillinger der deles med CLI, såsom tilstande og regler for automatisk godkendelse, er gemt i CLI-konfigurationen og vil ikke blive nulstillet.",
"settings.aboutKiloCode.resetSettings.button": "Nulstil alle indstillinger",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Overførsel af indstillinger",
"settings.aboutKiloCode.settingsTransfer.description":
"Eksportér eller importér dine indstillinger for at overføre dem mellem VS Code-instanser.",
+1
View File
@@ -1266,6 +1266,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Dies setzt nur VS Code-erweiterungsspezifische Einstellungen auf ihre Standardwerte zurück. Einstellungen, die mit der CLI geteilt werden, wie Modi und Regeln für die automatische Genehmigung, werden in der CLI-Konfiguration gespeichert und nicht zurückgesetzt.",
"settings.aboutKiloCode.resetSettings.button": "Alle Einstellungen zurücksetzen",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Einstellungen übertragen",
"settings.aboutKiloCode.settingsTransfer.description":
"Exportieren oder importieren Sie Ihre Einstellungen, um sie zwischen VS Code-Instanzen zu übertragen.",
@@ -1227,6 +1227,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"This resets only VS Code extension-specific settings to their default values. Settings shared with the CLI, such as modes and auto-approve rules, are stored in the CLI configuration and will not be reset.",
"settings.aboutKiloCode.resetSettings.button": "Reset All Settings",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Settings Transfer",
"settings.aboutKiloCode.settingsTransfer.description":
"Export or import your settings to transfer them between VS Code instances.",
+1
View File
@@ -1259,6 +1259,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Esto restablece únicamente las configuraciones específicas de la extensión VS Code a sus valores predeterminados. Las configuraciones compartidas con el CLI, como los modos y las reglas de aprobación automática, se almacenan en la configuración del CLI y no serán restablecidas.",
"settings.aboutKiloCode.resetSettings.button": "Restablecer toda la configuración",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Transferencia de ajustes",
"settings.aboutKiloCode.settingsTransfer.description":
"Exporta o importa tus ajustes para transferirlos entre instancias de VS Code.",
+1
View File
@@ -1272,6 +1272,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Ceci réinitialise uniquement les paramètres spécifiques à l'extension VS Code à leurs valeurs par défaut. Les paramètres partagés avec le CLI, tels que les modes et les règles d'approbation automatique, sont stockés dans la configuration du CLI et ne seront pas réinitialisés.",
"settings.aboutKiloCode.resetSettings.button": "Réinitialiser tous les paramètres",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Transfert des paramètres",
"settings.aboutKiloCode.settingsTransfer.description":
"Exportez ou importez vos paramètres pour les transférer entre instances VS Code.",
+1
View File
@@ -1076,6 +1076,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Ripristina solo le impostazioni specifiche dell'estensione VS Code ai valori predefiniti. Le impostazioni condivise con la CLI, come modalità e regole di approvazione automatica, sono salvate nella configurazione CLI e non verranno ripristinate.",
"settings.aboutKiloCode.resetSettings.button": "Ripristina tutte le impostazioni",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Trasferimento impostazioni",
"settings.aboutKiloCode.settingsTransfer.description":
"Esporta o importa le impostazioni per trasferirle tra istanze VS Code.",
+1
View File
@@ -1242,6 +1242,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"これはVS Code拡張機能固有の設定のみをデフォルト値にリセットします。モードや自動承認ルールなど、CLIと共有される設定はCLI設定ファイルに保存されており、リセットされません。",
"settings.aboutKiloCode.resetSettings.button": "すべての設定をリセット",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "設定の移行",
"settings.aboutKiloCode.settingsTransfer.description":
"VS Code インスタンス間で設定を転送するには、エクスポートまたはインポートしてください。",
+1
View File
@@ -1237,6 +1237,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"이 기능은 VS Code 확장 프로그램 전용 설정만 기본값으로 초기화합니다. 모드 및 자동 승인 규칙과 같이 CLI와 공유되는 설정은 CLI 구성에 저장되며 초기화되지 않습니다.",
"settings.aboutKiloCode.resetSettings.button": "모든 설정 초기화",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "설정 이전",
"settings.aboutKiloCode.settingsTransfer.description":
"VS Code 인스턴스 간에 설정을 전송하려면 내보내기 또는 가져오기하세요.",
+1
View File
@@ -1241,6 +1241,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Dit reset alleen VS Code-extensiespecifieke instellingen naar hun standaardwaarden. Instellingen die gedeeld worden met de CLI, zoals modi en regels voor automatisch goedkeuren, worden opgeslagen in de CLI-configuratie en worden niet gereset.",
"settings.aboutKiloCode.resetSettings.button": "Alle instellingen resetten",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Instellingen overdragen",
"settings.aboutKiloCode.settingsTransfer.description":
"Exporteer of importeer uw instellingen om ze tussen VS Code-instanties over te dragen.",
+1
View File
@@ -1210,6 +1210,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Dette tilbakestiller kun VS Code-utvidelsesspecifikke innstillinger til standardverdiene. Innstillinger som deles med CLI, som modi og regler for automatisk godkjenning, lagres i CLI-konfigurasjonen og vil ikke tilbakestilles.",
"settings.aboutKiloCode.resetSettings.button": "Tilbakestill alle innstillinger",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Overføring av innstillinger",
"settings.aboutKiloCode.settingsTransfer.description":
"Eksporter eller importer innstillingene dine for å overføre dem mellom VS Code-instanser.",
+1
View File
@@ -1210,6 +1210,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"To resetuje tylko ustawienia specyficzne dla rozszerzenia VS Code do ich domyślnych wartości. Ustawienia współdzielone z CLI, takie jak tryby i reguły automatycznego zatwierdzania, są przechowywane w konfiguracji CLI i nie zostaną zresetowane.",
"settings.aboutKiloCode.resetSettings.button": "Resetuj wszystkie ustawienia",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Przenoszenie ustawień",
"settings.aboutKiloCode.settingsTransfer.description":
"Eksportuj lub importuj ustawienia, aby przenosić je między instancjami VS Code.",
+1
View File
@@ -1252,6 +1252,7 @@ export const dict = {
"settings.aboutKiloCode.resetSettings.description":
"Это сбрасывает только настройки, специфичные для расширения VS Code, до значений по умолчанию. Настройки, общие с CLI, такие как режимы и правила автоматического утверждения, хранятся в конфигурации CLI и не будут сброшены.",
"settings.aboutKiloCode.resetSettings.button": "Сбросить все настройки",
"settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications",
"settings.aboutKiloCode.settingsTransfer.title": "Перенос настроек",
"settings.aboutKiloCode.settingsTransfer.description":
"Экспортируйте или импортируйте настройки для переноса между экземплярами VS Code.",

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