diff --git a/.changeset/agent-manager-worktree-sandbox.md b/.changeset/agent-manager-worktree-sandbox.md new file mode 100644 index 0000000000..75c4a0cab1 --- /dev/null +++ b/.changeset/agent-manager-worktree-sandbox.md @@ -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 diff --git a/.changeset/clear-marketplace-installs.md b/.changeset/clear-marketplace-installs.md new file mode 100644 index 0000000000..717a39f491 --- /dev/null +++ b/.changeset/clear-marketplace-installs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Explain Marketplace item types, installation scopes, destination files, and MCP security before installation. diff --git a/.changeset/jetbrains-session-error-logs.md b/.changeset/jetbrains-session-error-logs.md new file mode 100644 index 0000000000..0b93c49b02 --- /dev/null +++ b/.changeset/jetbrains-session-error-logs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve JetBrains session error logs so nested CLI error messages are visible. diff --git a/.changeset/kilo-pass-profile-contract.md b/.changeset/kilo-pass-profile-contract.md new file mode 100644 index 0000000000..c8b975f015 --- /dev/null +++ b/.changeset/kilo-pass-profile-contract.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch +"@kilocode/sdk": patch +--- + +Expose Kilo Pass state on the Kilo profile API contract. diff --git a/.changeset/marketplace-match-notification.md b/.changeset/marketplace-match-notification.md new file mode 100644 index 0000000000..1ea0f8b87d --- /dev/null +++ b/.changeset/marketplace-match-notification.md @@ -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. diff --git a/.changeset/reset-read-notifications.md b/.changeset/reset-read-notifications.md new file mode 100644 index 0000000000..bbf0cf929b --- /dev/null +++ b/.changeset/reset-read-notifications.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Support resetting read notifications from the VS Code extension settings. diff --git a/packages/kilo-docs/lib/nav/getting-started.ts b/packages/kilo-docs/lib/nav/getting-started.ts index 7f85afc0cb..780a85880a 100644 --- a/packages/kilo-docs/lib/nav/getting-started.ts +++ b/packages/kilo-docs/lib/nav/getting-started.ts @@ -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" }, ], }, { diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index fe1be68c3a..b4f1c000e7 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -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', diff --git a/packages/kilo-docs/pages/getting-started/cost-controls-and-usage-safeguards.md b/packages/kilo-docs/pages/getting-started/cost-controls-and-usage-safeguards.md new file mode 100644 index 0000000000..ec281b1cae --- /dev/null +++ b/packages/kilo-docs/pages/getting-started/cost-controls-and-usage-safeguards.md @@ -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..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 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png index fe7821d7e8..fdc3892c14 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3545994fd7a126ea860b31e2ac07f9cbeaa03e872f805e3fd2ec7012556c6ea8 -size 12665 +oid sha256:36101257010ff15372b8e2b7ace1b5128ecc820ce5ca4111ff1ba7905473b0b4 +size 19407 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/install-mcp-modal-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/install-mcp-modal-chromium-linux.png new file mode 100644 index 0000000000..67085a1338 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/install-mcp-modal-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45d2e8b0e2a5dd7a40ec5c0b5ab4acee6035ede90135980038628e98e3478cd9 +size 682 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png index 0e841eadf3..b9a99af353 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10adc41427bfb41fb25fb25e14c771443c1b3cf23a546b05665370974fbc2209 -size 54839 +oid sha256:cca305368e62ec5b45c8b20bc5d4921e0ca8df1c54eb110efffe188837400b95 +size 57533 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/relevant-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/relevant-items-chromium-linux.png index b842681355..f292e63509 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/relevant-items-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/relevant-items-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:572b5a2bfd530b743a8ebaaed2ddacc9611aaa31f450df0f2904f314d8cfff6d -size 52660 +oid sha256:9a681b7c8866aa3aa6f1e8673afcb5efb79d3a251ba46b3c7201e22dcc77b9c2 +size 55898 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index ce7301c277..e7995b9b78 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -123,10 +123,15 @@ - +- + - - +- + + - - diff --git a/packages/kilo-gateway/src/api/kilo-pass.ts b/packages/kilo-gateway/src/api/kilo-pass.ts new file mode 100644 index 0000000000..fd75f512cd --- /dev/null +++ b/packages/kilo-gateway/src/api/kilo-pass.ts @@ -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) : 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 { + 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 + } +} diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index bc8d21df72..642e13efc6 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -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 diff --git a/packages/kilo-gateway/src/server/handlers.ts b/packages/kilo-gateway/src/server/handlers.ts index ff8e7f2114..722eef71f8 100644 --- a/packages/kilo-gateway/src/server/handlers.ts +++ b/packages/kilo-gateway/src/server/handlers.ts @@ -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 { 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) { diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index c90c4f1253..40fb57937c 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -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(), }) diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index ab214d3274..801464966b 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -33,6 +33,13 @@ export interface KilocodeBalance { balance: number } +export interface KiloPassState { + currentPeriodBaseCreditsUsd: number + currentPeriodUsageUsd: number + currentPeriodBonusCreditsUsd: number + nextBillingAt?: string | null +} + export interface PollOptions { interval: number maxAttempts: number diff --git a/packages/kilo-gateway/test/api/kilo-pass.test.ts b/packages/kilo-gateway/test/api/kilo-pass.test.ts new file mode 100644 index 0000000000..a179eeecbb --- /dev/null +++ b/packages/kilo-gateway/test/api/kilo-pass.test.ts @@ -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() + }) +}) diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index d9e3bcfa0b..4c267f2672 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -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": "إلغاء", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 6f388cba3a..1f816170e5 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -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", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index 174956f59e..619faf6357 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -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", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 1c76d9c2df..457b3f97d2 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -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", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 0a127f11d1..08808907e2 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -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", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index 312c76d545..6c79b7dc8d 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -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", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index f777cd9b4a..bcbf615f04 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -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", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index 715ff06bfc..895b9cff15 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -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", diff --git a/packages/kilo-i18n/src/it.ts b/packages/kilo-i18n/src/it.ts index 757c4a5caa..8224cf6910 100644 --- a/packages/kilo-i18n/src/it.ts +++ b/packages/kilo-i18n/src/it.ts @@ -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", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 6b6ecc44ed..0092971f30 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -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": "キャンセル", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index e905fbdfbb..09fbfa7cf7 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -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": "취소", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 4ad67883c9..9e5fc2210b 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -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", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index d6d2d33d46..9600db9243 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -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", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 6a9a617cbe..07845f6f11 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -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", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 04e3fc2fb5..6a3e9c14d5 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -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": "Отмена", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 130ec6ba84..13a1dbd0d7 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -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": "ยกเลิก", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index fc87ffcc3d..9b0724277a 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -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", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index 05c759a9b4..d3a885bf03 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -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": "Скасувати", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 25fb70a41a..96587a5374 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -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": "取消", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index dd4ebefaef..547fb96436 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -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": "取消", diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 4534ec376c..22d2fbc625 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -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() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt index 19a399cdca..c9d29933a2 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt @@ -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) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt index 69c5dc1824..6f63a3acde 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt @@ -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() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index f6140172ab..7a7d7d53c8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -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"), ) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 0f079d2c09..b01aa990ed 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -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? = 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 = - 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) } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt index 29b0ec0350..3692bb7dd0 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt @@ -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(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")) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ChatLogSummaryTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ChatLogSummaryTest.kt index 29deb0cce4..23afe33909 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ChatLogSummaryTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ChatLogSummaryTest.kt @@ -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)) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 3d96d25710..414daa625a 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -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(""" diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt new file mode 100644 index 0000000000..c5f1f98158 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt @@ -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 { + 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")) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index 41a054592c..89c0f47c10 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -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 = 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 { 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). */ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index bce1c8e2eb..3b4acdefc4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -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.ALL.map(::bind) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 53dfcde950..b4793d377e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -93,6 +93,7 @@ class SessionController( private val openProfileAction: () -> Unit = {}, private val telemetry: (String, Map) -> 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" diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt index b8115a662e..715f107946 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt @@ -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 { + 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", diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt index 21b9735083..715c2c9567 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt @@ -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`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index 07dcfb934f..7d94f601a8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -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() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt index 2cc6d6fadd..1c7e27d3b4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionSubscriptionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionSubscriptionTest.kt new file mode 100644 index 0000000000..47ae2b4ea3 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionSubscriptionTest.kt @@ -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") }) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt index 78a62bac11..b5debea3ee 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt @@ -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() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt index 4d6c6aecf9..6ff0af1a68 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt @@ -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>() val searchQueries = mutableListOf() - val opened = mutableListOf() + val opened = CopyOnWriteArrayList() val localConfigs = mutableListOf() var globalConfigs = 0 var localConfigPathCalls = 0 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestLog.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestLog.kt new file mode 100644 index 0000000000..e398eddb6a --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestLog.kt @@ -0,0 +1,47 @@ +package ai.kilocode.client.testing + +import ai.kilocode.log.KiloLog + +class TestLog : KiloLog { + private val items = mutableListOf() + private val lock = Object() + val messages: List + 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() + } + } +} diff --git a/packages/kilo-jetbrains/gradle/libs.versions.toml b/packages/kilo-jetbrains/gradle/libs.versions.toml index b255dc98c7..dca5158089 100644 --- a/packages/kilo-jetbrains/gradle/libs.versions.toml +++ b/packages/kilo-jetbrains/gradle/libs.versions.toml @@ -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" diff --git a/packages/kilo-jetbrains/settings.gradle.kts b/packages/kilo-jetbrains/settings.gradle.kts index c9b17f77c2..d18a500665 100644 --- a/packages/kilo-jetbrains/settings.gradle.kts +++ b/packages/kilo-jetbrains/settings.gradle.kts @@ -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/") diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt index 5283789459..0dace99738 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt @@ -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}", diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index 623915fc52..cfc0ef4777 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -41,6 +41,8 @@ data class MessageErrorDto( val message: String? = null, val statusCode: Int? = null, val responseBody: String? = null, + val dataKeys: List = emptyList(), + val ref: String? = null, ) @Serializable diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index fd35221493..e9211d3326 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -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 { - 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("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("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 { + 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 { - if (!this.extensionContext) return - const existing = this.extensionContext.globalState.get("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()) diff --git a/packages/kilo-vscode/src/MarketplacePanelProvider.ts b/packages/kilo-vscode/src/MarketplacePanelProvider.ts index bd36dfd845..0c704ac580 100644 --- a/packages/kilo-vscode/src/MarketplacePanelProvider.ts +++ b/packages/kilo-vscode/src/MarketplacePanelProvider.ts @@ -36,6 +36,7 @@ export class MarketplacePanelProvider implements vscode.Disposable { private generation = 0 private refresh: ReturnType | undefined private statuses = new Map() + 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) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index d2c07c39db..1696870cba 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -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 { + 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) diff --git a/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts new file mode 100644 index 0000000000..13e81e6ebc --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts @@ -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 { + 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) +} diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 7a5d686dab..3fda920167 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -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 { diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 1a22505abd..27472ec9c5 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -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) diff --git a/packages/kilo-vscode/src/kilo-provider/notifications.ts b/packages/kilo-vscode/src/kilo-provider/notifications.ts new file mode 100644 index 0000000000..0e29770fff --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/notifications.ts @@ -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 { + if (!ctx.client) { + const cached = ctx.cached() + if (cached) { + const persisted = ctx.context?.globalState.get(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(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 { + if (!ctx.context) return + const existing = ctx.context.globalState.get(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 { + await ctx.context?.globalState.update(KEY, undefined) + await fetchAndSendNotifications(ctx) + vscode.window.showInformationMessage("Read notifications have been reset.") +} diff --git a/packages/kilo-vscode/src/services/marketplace/notifier.ts b/packages/kilo-vscode/src/services/marketplace/notifier.ts new file mode 100644 index 0000000000..55f6b06f18 --- /dev/null +++ b/packages/kilo-vscode/src/services/marketplace/notifier.ts @@ -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 | 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() + + 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(DISMISSED_KEY, []) ?? [] + } + + private async dismiss(slug: string): Promise { + 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 { + 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) + } +} diff --git a/packages/kilo-vscode/src/services/marketplace/notify.ts b/packages/kilo-vscode/src/services/marketplace/notify.ts new file mode 100644 index 0000000000..0243bbb4a0 --- /dev/null +++ b/packages/kilo-vscode/src/services/marketplace/notify.ts @@ -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): 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, +): 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 { + 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 +} diff --git a/packages/kilo-vscode/tests/unit/marketplace-notify.test.ts b/packages/kilo-vscode/tests/unit/marketplace-notify.test.ts new file mode 100644 index 0000000000..7fa082fa4d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/marketplace-notify.test.ts @@ -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]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts index 1e1689cd82..9bcd471faa 100644 --- a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts @@ -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("") 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(" { expect(src).toContain( "const sandboxNetworkEnabled = () => config().experimental?.sandbox_restrict_network !== false", ) - expect(src).toContain('') expect(src).toContain("") - expect(src).toContain('') - expect(src).toContain('') - 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('') + expect(button).toContain('') + expect(button).toContain('') + 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: {") }) }) diff --git a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts new file mode 100644 index 0000000000..136dc3da29 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts @@ -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("") + }) + + test("places the sandbox toggle with prompt actions instead of model selectors", () => { + const selectors = dialog.indexOf('
') + const actions = dialog.indexOf('
', selectors) + const sandbox = dialog.indexOf(" 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) => 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(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
+ + + } + tooltipClass="prompt-sandbox-tooltip-content" + onToggle={click( + "sandbox_toggle", + "configure_worktree_dialog", + () => setSandbox(!sandbox()), + () => ({ enabled: !sandbox() }), + )} + /> + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index c68e070acd..946afb7ae9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -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 ( -
-
- {language.t(props.enabled ? "prompt.action.sandbox.status.enabled" : "prompt.action.sandbox.status.disabled")} -
-
- - {language.t("prompt.action.sandbox.filesystem")} - - {language.t( - props.enabled ? "prompt.action.sandbox.filesystem.restricted" : "prompt.action.sandbox.unrestricted", - )} - -
-
- - {language.t("prompt.action.sandbox.network")} - - {language.t( - props.enabled && props.network - ? "prompt.action.sandbox.network.blocked" - : props.enabled - ? "prompt.action.sandbox.network.allowed" - : "prompt.action.sandbox.unrestricted", - )} - -
-
- {language.t( - props.enabled - ? "prompt.action.sandbox.description.enabled" - : props.network - ? "prompt.action.sandbox.description.disabled" - : "prompt.action.sandbox.description.disabledNetworkAllowed", - )} -
-
- ) -} - export const PromptInput: Component = (props) => { const session = useSession() const server = useServer() @@ -1308,33 +1266,15 @@ export const PromptInput: Component = (props) => { - - ) - } - contentClass="prompt-sandbox-tooltip-content" - placement="top" - > - - + } + tooltipClass="prompt-sandbox-tooltip-content" + onToggle={toggleSandbox} + /> + + + +
+
+
{t("marketplace.install.scope")} { label={(x: ScopeOption) => x.label} onSelect={(v: ScopeOption | undefined) => v && setScope(v)} /> +

{scopeDescription()}

+
+ {t("marketplace.install.destination")} + {destination()} +
+ +
+ +

{t("marketplace.install.mcp.warning")}

+
+ +

{t("marketplace.install.project.warning")}

+
+
+
+ 1}>
{t("marketplace.install.method")} @@ -215,6 +283,7 @@ export const InstallModal = (props: Props) => { } >

{t("marketplace.install.success")}

+

{t("marketplace.install.installedAt", { path: r().path })}

diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx index 4735d7c6b1..2dddafc494 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -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({ value: "all", label: t("marketplace.filter.all") }) const [types, setTypes] = createSignal([]) @@ -109,6 +111,18 @@ export const MarketplaceListView = (props: Props) => { return (
+
+ {t("marketplace.intro")} + +
diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index fa6e8b9b03..ee98aa08f4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -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() } }} diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css index a22b1f5116..a695688f17 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css @@ -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 { diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx index ea15459c9b..7e74c6aefd 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx @@ -357,21 +357,18 @@ const AboutKiloCodeTab: Component = (props) => { > {language.t("settings.aboutKiloCode.resetSettings.description")}

- +
+ + +
) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx new file mode 100644 index 0000000000..a8de4cfed2 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx @@ -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 ( +
+
+ {language.t(props.enabled ? "prompt.action.sandbox.status.enabled" : "prompt.action.sandbox.status.disabled")} +
+
+ + {language.t("prompt.action.sandbox.filesystem")} + + {language.t( + props.enabled ? "prompt.action.sandbox.filesystem.restricted" : "prompt.action.sandbox.unrestricted", + )} + +
+
+ + {language.t("prompt.action.sandbox.network")} + + {language.t( + props.enabled && props.network + ? "prompt.action.sandbox.network.blocked" + : props.enabled + ? "prompt.action.sandbox.network.allowed" + : "prompt.action.sandbox.unrestricted", + )} + +
+
+ {language.t( + props.enabled + ? "prompt.action.sandbox.description.enabled" + : props.network + ? "prompt.action.sandbox.description.disabled" + : "prompt.action.sandbox.description.disabledNetworkAllowed", + )} +
+
+ ) +} + +export const SandboxButtonBase: Component = (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 ( + + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/src/context/language.tsx b/packages/kilo-vscode/webview-ui/src/context/language.tsx index 444518a0f8..7dad79270d 100644 --- a/packages/kilo-vscode/webview-ui/src/context/language.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/language.tsx @@ -196,7 +196,7 @@ export const LanguageProvider: ParentComponent = (props) }) const t = (key: UiI18nKey, params?: UiI18nParams) => { - const text = (dict() as Record)[key] ?? String(key) + const text = (dict() as Record)[key] ?? (dicts.en as Record)[key] ?? String(key) return resolveTemplate(text, params) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index ebea1d7c8e..fd2c265e91 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -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": "تصدير", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 8b36f169d3..d0378e8c7b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index c8283620a7..de1e41dbf8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index f360d7680a..dee2237b79 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 3489d9193a..fb6b30e846 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 95a2c88cea..7cc617f8ea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index f09a59a4cc..2a383c61f9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 79052b1d5f..78d031b278 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 005494f6bd..190ee91011 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 283c0ec787..c813407f60 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -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 インスタンス間で設定を転送するには、エクスポートまたはインポートしてください。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 7109078ac1..454b473130 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -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 인스턴스 간에 설정을 전송하려면 내보내기 또는 가져오기하세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 20b6adf528..827c863f5a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index fdf4011a5b..e988e09c01 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 024c85fa02..1e55d7debe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index c1dbc2fc26..156d818f3d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -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.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 9e3dfbb212..a1e6079680 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1234,6 +1234,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", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 0f61e74c65..b2c3b68958 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1238,6 +1238,7 @@ export const dict = { "settings.aboutKiloCode.resetSettings.description": "Bu, yalnızca VS Code uzantısına özgü ayarları varsayılan değerlerine sıfırlar. Modlar ve otomatik onay kuralları gibi CLI ile paylaşılan ayarlar, CLI yapılandırmasında depolanır ve sıfırlanmaz.", "settings.aboutKiloCode.resetSettings.button": "Tüm Ayarları Sıfırla", + "settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications", "settings.aboutKiloCode.settingsTransfer.title": "Ayar Aktarımı", "settings.aboutKiloCode.settingsTransfer.description": "Ayarlarınızı VS Code örnekleri arasında aktarmak için dışa veya içe aktarın.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index ac62609fd1..28c61a41b1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1234,6 +1234,7 @@ export const dict = { "settings.aboutKiloCode.resetSettings.description": "Це скине лише налаштування, специфічні для розширення VS Code, до стандартних значень. Налаштування, що зберігаються в конфігурації CLI (такі як режими та правила автоматичного схвалення), не будуть скинуті.", "settings.aboutKiloCode.resetSettings.button": "Скинути всі налаштування", + "settings.aboutKiloCode.resetSettings.notificationsButton": "Reset Read Notifications", "settings.aboutKiloCode.settingsTransfer.title": "Перенесення налаштувань", "settings.aboutKiloCode.settingsTransfer.description": "Експортуйте або імпортуйте налаштування для перенесення між екземплярами VS Code.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 1d6c8147cf..7767c601c7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1216,6 +1216,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": "导出", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 4603eb794e..fba8e44d91 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1180,6 +1180,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": "匯出", diff --git a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx index 2c2a4b5588..ae61f252b9 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx @@ -10,6 +10,8 @@ import type { Meta, StoryObj } from "storybook-solidjs-vite" import { StoryProviders } from "./StoryProviders" import { MarketplaceListView } from "../components/marketplace/MarketplaceListView" import { ItemCard } from "../components/marketplace/ItemCard" +import { InstallModal } from "../components/marketplace/InstallModal" +import { MarketplaceSessionProvider } from "../context/marketplace-session" import type { SkillMarketplaceItem, McpMarketplaceItem, @@ -444,6 +446,19 @@ export const InstalledMcpCard: Story = { ), } +export const InstallMcpModal: Story = { + name: "InstallModal — MCP explanation and destination", + render: () => ( + + +
+ +
+
+
+ ), +} + // --------------------------------------------------------------------------- // Mode Stories // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx index 438157fa6c..cdded50649 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/prompt-input.stories.tsx @@ -15,7 +15,8 @@ import type { Meta, StoryObj } from "storybook-solidjs-vite" import { type ParentComponent } from "solid-js" import { StoryProviders, mockSessionValue } from "./StoryProviders" import { SessionContext } from "../context/session" -import { PromptInput, SandboxTooltipContent } from "../components/chat/PromptInput" +import { PromptInput } from "../components/chat/PromptInput" +import { SandboxTooltipContent } from "../components/shared/SandboxButton" import { Button } from "@kilocode/kilo-ui/button" import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index f65fb10ab1..7c032c7e3a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -931,6 +931,11 @@ export interface MarketplaceInstallResultMessage { error?: string } +export interface OpenInstallModalMessage { + type: "openInstallModal" + mpItem: MarketplaceItem +} + export interface MarketplaceRemoveResultMessage { type: "marketplaceRemoveResult" success: boolean @@ -1132,6 +1137,7 @@ export type ExtensionMessage = | MarketplaceDataMessage | MarketplaceInstallResultMessage | MarketplaceRemoveResultMessage + | OpenInstallModalMessage | ProviderOAuthReadyMessage | ProviderConnectedMessage | ProviderDisconnectedMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 363286bd2d..2b4b43cde2 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -466,6 +466,10 @@ export interface ResetAllSettingsRequest { type: "resetAllSettings" } +export interface ResetReadNotificationsRequest { + type: "resetReadNotifications" +} + export interface SettingsTabChangedMessage { type: "settingsTabChanged" tab: string @@ -681,6 +685,9 @@ export interface CreateMultiVersionRequest { // Overrides `versions`, `providerID`, and `modelID`. variant?: string modelAllocations?: ModelAllocation[] + // When set, start each created worktree session with the sandbox override + // reconciled to this state. Only sent when sandbox controls are available. + sandbox?: boolean } // Persist tab order for a context (worktree ID or "local") @@ -1202,6 +1209,7 @@ export type WebviewMessage = | RequestNotificationSettingsMessage | TestNotificationMessage | ResetAllSettingsRequest + | ResetReadNotificationsRequest | SettingsTabChangedMessage | SyncSessionRequest | CreateWorktreeSessionRequest diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts index fc1bc59ee9..f807b480bd 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts @@ -27,9 +27,17 @@ export const Balance = Schema.Struct({ balance: Schema.Finite, }) +export const KiloPassState = Schema.Struct({ + currentPeriodBaseCreditsUsd: Schema.Finite, + currentPeriodUsageUsd: Schema.Finite, + currentPeriodBonusCreditsUsd: Schema.Finite, + nextBillingAt: Schema.optional(Schema.NullOr(Schema.String)), +}) + export const ProfileWithBalance = Schema.Struct({ profile: Profile, balance: Schema.NullOr(Balance), + kiloPass: Schema.NullOr(KiloPassState), currentOrgId: Schema.NullOr(Schema.String), }) diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts index f6fa2aa97c..e8cc1200c9 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts @@ -16,6 +16,7 @@ import { clearModesCache, fetchBalance, fetchKilocodeNotifications, + fetchKiloPassState, fetchOrganizationModes, fetchProfile, } from "@kilocode/kilo-gateway" @@ -66,11 +67,16 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", if (!info || info.type !== "oauth") return yield* Effect.fail(new HttpApiError.Unauthorized({})) const currentOrgId = info.accountId ?? null - const [profile, balance] = yield* Effect.tryPromise({ - try: () => Promise.all([fetchProfile(info.access), fetchBalance(info.access, currentOrgId ?? undefined)]), + const [profile, balance, kiloPass] = yield* Effect.tryPromise({ + try: () => + Promise.all([ + fetchProfile(info.access), + fetchBalance(info.access, currentOrgId ?? undefined), + fetchKiloPassState(info.access), + ]), catch: () => new HttpApiError.BadRequest({}), }) - return { profile, balance, currentOrgId } + return { profile, balance, kiloPass, currentOrgId } }) const authStatus = Effect.fn("KiloGatewayHttpApi.authStatus")(function* () { diff --git a/packages/opencode/src/kilocode/server/httpapi/public.ts b/packages/opencode/src/kilocode/server/httpapi/public.ts index 78deb6d62b..ecd0d0a1d2 100644 --- a/packages/opencode/src/kilocode/server/httpapi/public.ts +++ b/packages/opencode/src/kilocode/server/httpapi/public.ts @@ -52,6 +52,7 @@ export function matchLegacyKiloOpenApi(input: Record) { const json = (path: string) => spec.paths?.[path]?.get?.responses?.["200"]?.content?.["application/json"] const profile = json("/kilo/profile")?.schema?.properties if (profile?.balance) profile.balance = nullable(profile.balance) + if (profile?.kiloPass) profile.kiloPass = nullable(profile.kiloPass) if (profile?.currentOrgId) profile.currentOrgId = nullable(profile.currentOrgId) const sessions = json("/kilo/cloud-sessions")?.schema?.properties diff --git a/packages/opencode/test/kilocode/server/httpapi-public.test.ts b/packages/opencode/test/kilocode/server/httpapi-public.test.ts index 4f2ddde798..5389b540a3 100644 --- a/packages/opencode/test/kilocode/server/httpapi-public.test.ts +++ b/packages/opencode/test/kilocode/server/httpapi-public.test.ts @@ -164,6 +164,7 @@ describe("Kilo PublicApi OpenAPI contract", () => { const profile = response(KiloGatewayPaths.profile)?.properties expect(profile?.balance).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] }) + expect(profile?.kiloPass).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] }) expect(profile?.currentOrgId).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) const auth = response(KiloGatewayPaths.authStatus)?.properties diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0045862b2e..79922a5a09 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -10114,6 +10114,12 @@ export type KiloProfileResponses = { balance: { balance: number } | null + kiloPass: { + currentPeriodBaseCreditsUsd: number + currentPeriodUsageUsd: number + currentPeriodBonusCreditsUsd: number + nextBillingAt?: string | null + } | null currentOrgId: string | null } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b97594d167..d571b75314 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -13064,6 +13064,43 @@ } ] }, + "kiloPass": { + "anyOf": [ + { + "type": "object", + "properties": { + "currentPeriodBaseCreditsUsd": { + "type": "number" + }, + "currentPeriodUsageUsd": { + "type": "number" + }, + "currentPeriodBonusCreditsUsd": { + "type": "number" + }, + "nextBillingAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "currentPeriodBaseCreditsUsd", + "currentPeriodUsageUsd", + "currentPeriodBonusCreditsUsd" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, "currentOrgId": { "anyOf": [ { @@ -13075,7 +13112,7 @@ ] } }, - "required": ["profile", "balance", "currentOrgId"], + "required": ["profile", "balance", "kiloPass", "currentOrgId"], "additionalProperties": false, "description": "Profile data" }