Merge branch 'main' into mark/kilo-ui-visual-regression

This commit is contained in:
Mark IJbema
2026-03-02 17:21:12 +01:00
committed by GitHub
28 changed files with 578 additions and 332 deletions
+1 -2
View File
@@ -134,8 +134,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
"provider.connect.oauth.auto.visit.suffix":
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
"provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
+2 -4
View File
@@ -389,8 +389,7 @@ export const dict = {
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
"toast.update.title": "Mise à jour disponible",
"toast.update.description":
"Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
"toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
"toast.update.action.installRestart": "Installer et redémarrer",
"toast.update.action.notYet": "Pas encore",
"error.page.title": "Quelque chose s'est mal passé",
@@ -524,8 +523,7 @@ export const dict = {
"sidebar.workspaces.enable": "Activer les espaces de travail",
"sidebar.workspaces.disable": "Désactiver les espaces de travail",
"sidebar.gettingStarted.title": "Commencer",
"sidebar.gettingStarted.line1":
"Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
"sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
"sidebar.gettingStarted.line2":
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessions récentes",
+1 -2
View File
@@ -146,8 +146,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
"provider.connect.oauth.auto.visit.suffix":
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
"provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
+1 -2
View File
@@ -147,8 +147,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "授權碼無效",
"provider.connect.oauth.auto.visit.prefix": "造訪 ",
"provider.connect.oauth.auto.visit.link": "此連結",
"provider.connect.oauth.auto.visit.suffix":
" 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.confirmationCode": "確認碼",
"provider.connect.toast.connected.title": "{{provider}} 已連線",
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
@@ -32,9 +32,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
}
src={props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
+13 -2
View File
@@ -1,8 +1,19 @@
import React from "react"
import React, { useState, useEffect } from "react"
import Link from "next/link"
import Head from "next/head"
const subtitles = [
"The page you requested does not exist or has been moved.",
"That link is dead, and may have ridden off into the sunset on a pink pony 🦄",
]
export default function Custom404() {
const [subtitle, setSubtitle] = useState("")
useEffect(() => {
setSubtitle(subtitles[Math.floor(Math.random() * subtitles.length)])
}, [])
return (
<>
<Head>
@@ -39,7 +50,7 @@ export default function Custom404() {
{/* Message */}
<div className="message-section">
<h1 className="message-title">Page not found</h1>
<p className="message-subtitle">The page you requested does not exist or has been moved.</p>
{subtitle && <p className="message-subtitle">{subtitle}</p>}
</div>
{/* Actions */}
@@ -17,15 +17,7 @@ Kilo's Code Reviews integrate with GitHub via a **GitHub App** to automatically
### Step 1: Install the GitHub App
1. Go to the **Integrations** page:
- **Personal**: [app.kilo.ai/integrations/github](https://app.kilo.ai/integrations/github)
- **Organization**: Your organization → Integrations → GitHub
2. Click **Install GitHub App**
3. Choose which GitHub account or organization to install the app on
4. Select repository access:
- **All repositories** — the app can access all current and future repos
- **Only select repositories** — choose specific repos
5. Click **Install**
Connect your GitHub account via the [Integrations page](/docs/automate/integrations#connecting-github). Once connected, return here to configure the Review Agent.
The GitHub App requests the following permissions:
@@ -21,48 +21,9 @@ Both **GitLab.com** and **self-hosted GitLab instances** are supported.
### Step 1: Connect GitLab
You can connect using **OAuth** or a **Personal Access Token (PAT)**. Choose the method that matches your GitLab setup:
Connect your GitLab account via the [Integrations page](/docs/automate/integrations#connecting-gitlab). You can use **OAuth** (GitLab.com or self-hosted) or a **Personal Access Token (PAT)**.
{% tabs %}
{% tab label="OAuth (GitLab.com)" %}
1. Go to the **Integrations** page:
- **Personal**: [app.kilo.ai/integrations/gitlab](https://app.kilo.ai/integrations/gitlab)
- **Organization**: Your organization → Integrations → GitLab
2. Click **Connect GitLab**
3. Authorize the application on GitLab
4. You'll be redirected back to Kilo with the connection active
{% /tab %}
{% tab label="OAuth (Self-Hosted)" %}
For self-hosted GitLab instances using OAuth, you need to register an OAuth application first:
1. In your GitLab instance, go to **Admin Area → Applications** (or **User Settings → Applications**)
2. Create a new application:
- **Name**: `Kilo Code`
- **Redirect URI**: `https://app.kilo.ai/api/integrations/gitlab/callback`
- **Scopes**: `api`, `read_user`, `read_repository`, `write_repository`
- **Confidential**: Yes
3. Copy the **Application ID** and **Secret**
4. In Kilo, go to the GitLab integration page
5. Enter your **Instance URL**, **Client ID**, and **Client Secret**
6. Click **Connect** and authorize
{% /tab %}
{% tab label="Personal Access Token" %}
1. In GitLab, go to **User Settings → Access Tokens**
2. Create a token with the `api` scope
3. Copy the token
4. In Kilo, go to the GitLab integration page
5. Paste the token (and enter your Instance URL for self-hosted)
6. Click **Connect**
> PAT tokens cannot be refreshed automatically. When your token expires, create a new one in GitLab and reconnect in Kilo.
{% /tab %}
{% /tabs %}
Once connected, return here to configure the Review Agent.
### Step 2: Configure the Review Agent
@@ -26,7 +26,7 @@ Kilo's **Code Reviews** feature automatically analyzes your pull or merge reques
Before enabling Code Reviews:
- **A platform integration must be configured:** Connect your GitHub or GitLab account via the [Integrations page](https://app.kilo.ai/integrations) so that the Review Agent can access your repositories.
- **A platform integration must be configured:** Connect your GitHub or GitLab account via the [Integrations page](https://app.kilo.ai/integrations) so that the Review Agent can access your repositories. See the [Integration setup guide](/docs/automate/integrations) for detailed instructions.
- **Kilo Code credits:** The AI model uses credits when analyzing your code.
## Cost
+115 -18
View File
@@ -5,32 +5,42 @@ description: "Overview of Kilo Code integrations"
# Kilo Code Integrations
Kilo Integrations lets you connect your GitHub account (and soon, GitLab and Bitbucket) to enable advanced features inside Kilo Code. Once connected, Kilo can access your repositories securely through the **KiloConnect** GitHub App, enabling features like **Cloud Agents** and **Kilo Deploy**.
Kilo Integrations lets you connect your GitHub or GitLab account (soon Bitbucket) to enable advanced features inside Kilo Code. Once connected, Kilo can access your repositories securely, enabling features like **Code Reviews**, **Cloud Agents**, and **Kilo Deploy**.
## Supported Platforms
| Platform | Integration Type | Details |
| -------- | ---------------- | --------------------------------- |
| GitHub | GitHub App | [GitHub Setup](#connecting-github)|
| GitLab | OAuth or PAT | [GitLab Setup](#connecting-gitlab)|
## What You Can Do With Integrations
- **Connect GitHub to Kilo Code** in a few clicks
- **Authorize the KiloConnect App** for repo access
- **Enable advanced features** like Cloud Agents and Kilo Deploy
- **Connect GitHub or GitLab to Kilo Code** in a few clicks
- **Enable advanced features** like Cloud Agents, Code Reviews, and Kilo Deploy
- **Authorize repository access** so Kilo can analyze and work with your code
## Prerequisites
Before connecting:
- You must have a **GitHub account**.
- You need permission to install GitHub Apps for the repositories you want Kilo to access.
- (Optional) If you're connecting an organization, you must be an **org admin** or have app installation permissions.
- You must have a **GitHub** or **GitLab** account.
- For GitHub: You need permission to install GitHub Apps for the repositories you want Kilo to access.
- For GitLab: You need **Maintainer** role (or higher) on the projects you want to connect.
- (Optional) If you're connecting an organization, you must be an admin or have app installation permissions.
## Connecting GitHub to Kilo
---
## Connecting GitHub
### 1. Open the Integrations Page
Go to your **Personal** or **Organization Dashboard**, and navigate to the [Integrations](https://app.kilo.ai/integrations) tab
Go to your **Personal** or **Organization Dashboard**, and navigate to the [Integrations](https://app.kilo.ai/integrations) tab.
### 2. Start the Connection Flow
1. Click **Configure** on the GitHub panel.
2. You’ll be redirected to GitHub to authorize the **KiloConnect** App.
2. You'll be redirected to GitHub to authorize the **KiloConnect** App.
3. Select the GitHub account or organization you want to connect.
### 3. Choose Repository Access
@@ -46,13 +56,62 @@ Click **Install & Authorize** to continue.
Once approved:
- You’ll return to the Kilo Integrations page.
- Github will show a **Connected** status.
- You'll return to the Kilo Integrations page.
- GitHub will show a **Connected** status.
- Your Kilo workspace can now access GitHub repositories securely.
---
## Connecting GitLab
You can connect GitLab using **OAuth** or a **Personal Access Token (PAT)**. Both **GitLab.com** and **self-hosted GitLab instances** are supported.
{% tabs %}
{% tab label="OAuth (GitLab.com)" %}
1. Go to the **Integrations** page:
- **Personal**: [app.kilo.ai/integrations/gitlab](https://app.kilo.ai/integrations/gitlab)
- **Organization**: Your organization → Integrations → GitLab
2. Click **Connect GitLab**
3. Authorize the application on GitLab
4. You'll be redirected back to Kilo with the connection active
{% /tab %}
{% tab label="OAuth (Self-Hosted)" %}
For self-hosted GitLab instances using OAuth, you need to register an OAuth application first:
1. In your GitLab instance, go to **Admin Area → Applications** (or **User Settings → Applications**)
2. Create a new application:
- **Name**: `Kilo Code`
- **Redirect URI**: `https://app.kilo.ai/api/integrations/gitlab/callback`
- **Scopes**: `api`, `read_user`, `read_repository`, `write_repository`
- **Confidential**: Yes
3. Copy the **Application ID** and **Secret**
4. In Kilo, go to the GitLab integration page
5. Enter your **Instance URL**, **Client ID**, and **Client Secret**
6. Click **Connect** and authorize
{% /tab %}
{% tab label="Personal Access Token" %}
1. In GitLab, go to **User Settings → Access Tokens**
2. Create a token with the `api` scope
3. Copy the token
4. In Kilo, go to the GitLab integration page
5. Paste the token (and enter your Instance URL for self-hosted)
6. Click **Connect**
> PAT tokens cannot be refreshed automatically. When your token expires, create a new one in GitLab and reconnect in Kilo.
{% /tab %}
{% /tabs %}
---
## What Happens After Connecting
Once GitHub is connected, the following features will be enabled in Kilo:
Once your Git provider is connected, the following features are enabled in Kilo:
### Cloud Agents
@@ -60,6 +119,12 @@ Once GitHub is connected, the following features will be enabled in Kilo:
- Auto-create branches and push work continuously
- Work from anywhere while keeping your repo in sync
### Code Reviews
- Automated AI review on every pull request or merge request
- Consistent feedback based on your team's standards
- See the [Code Reviews guide](/docs/automate/code-reviews/overview) for setup
### Kilo Deploy
- Deploy Next.js 14 & 15 apps directly from Kilo
@@ -68,29 +133,61 @@ Once GitHub is connected, the following features will be enabled in Kilo:
### Upcoming:
- **GitLab Integration**
- **Bitbucket Integration**
---
## Managing or Removing the Integration
From the same **Integrations** page, you can click "Manage on Github" to:
### GitHub
From the **Integrations** page, click "Manage on GitHub" to:
- View the GitHub account you connected
- Update which repositories Kilo has access to
- Disconnect GitHub entirely
- Reauthorize the app if permissions change
### GitLab
From the **Integrations** page:
- Click **Disconnect** to remove the GitLab connection
- Your tokens are cleared, but webhook configuration is preserved so reconnecting restores your setup
> Disconnecting from Kilo does not revoke OAuth tokens on GitLab's side. You can manually revoke them from **GitLab → User Settings → Applications → Authorized Applications**.
---
## Troubleshooting
**“I don’t see my repositories.”**
### GitHub
**"I don't see my repositories."**
Ensure the KiloConnect App is installed for the correct GitHub org and that repo access includes the repositories you need.
**“My organization blocks third-party apps.”**
**"My organization blocks third-party apps."**
You may need an admin to approve installing GitHub Apps.
**“Cloud Agents or Deploy can’t access my repo.”**
**"Cloud Agents or Deploy can't access my repo."**
Revisit the GitHub app settings and confirm the app has the correct repo scope.
### GitLab
**"No projects listed after connecting."**
Click the refresh button to sync projects from GitLab. Ensure your GitLab account has access to the projects you expect.
**"Permission denied" errors.**
You need **Maintainer role** on the GitLab project for webhook and bot token creation.
**"Token expired."**
- **OAuth**: Tokens refresh automatically. If refresh fails, reconnect from the integration page.
- **PAT**: Create a new token in GitLab and reconnect in Kilo.
**"Self-hosted connection issues."**
- Verify your instance URL is accessible from the internet
- Ensure HTTPS is configured
- Check that OAuth application scopes include all required scopes
- Verify the redirect URI matches: `https://app.kilo.ai/api/integrations/gitlab/callback`
@@ -20,7 +20,7 @@ The CLI accepts several config filenames. The recommended file is `kilo.json`:
| **Global** | `~/.config/kilo/kilo.json` | `kilo.jsonc`, `config.json` |
| **Project** | `./kilo.json` or `./.kilo/kilo.json` | `kilo.jsonc` |
Project-level configuration takes precedence over global settings.
Project-level configuration takes precedence over global settings.
## Configuration Format
@@ -18,12 +18,21 @@ Kilo for Slack brings the power of Kilo Code directly into your Slack workspace.
---
## Supported Platforms
| Platform | Integration Type | Details |
| -------- | ---------------- | --------------------------------------------------------------------|
| GitHub | GitHub App | [GitHub Setup Guide](/docs/automate/integrations#connecting-github) |
| GitLab | OAuth or PAT | [GitLab Setup Guide](/docs/automate/integrations#connecting-gitlab) |
---
## Prerequisites
Before using Kilo for Slack:
- You must have a **Kilo Code account** with available credits
- Your **GitHub Integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so Kilo can access your repositories
- Your **Git provider integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so Kilo can access your repositories
To install Kilo for Slack, simply go to the integrations menu in the sidebar on https://app.kilo.ai and set up the Slack integration.
@@ -105,9 +114,9 @@ Can you help me understand what's causing it?
## How It Works
1. **Message Kilo** — Either through DMs or by mentioning it in a channel
2. **Kilo processes your request** — Kilo uses your connected GitHub repositories to understand context
2. **Kilo processes your request** — Kilo uses your connected repositories to understand context
3. **AI generates a response** — Kilo Code's AI analyzes your request and provides helpful responses
4. **Code changes (if requested)** — For implementation requests, Kilo can create pull requests
4. **Code changes (if requested)** — For implementation requests, Kilo can create pull or merge requests
---
@@ -157,10 +166,10 @@ Kilo for Slack supports over 400+ models across different providers.
Ensure Kilo for Slack is installed in your workspace and has been added to the channel you're using.
**"Kilo can't access my repository."**
Verify your GitHub integration is configured correctly in the [Integrations tab](https://app.kilo.ai/integrations).
Verify your Git provider integration is configured correctly in the [Integrations tab](https://app.kilo.ai/integrations).
**"I'm getting incomplete responses."**
Try breaking your request into smaller, more specific questions.
**"Kilo doesn't understand my codebase."**
Make sure the repository you're asking about is connected and accessible through your GitHub integration.
Make sure the repository you're asking about is connected and accessible through your Git provider integration.
@@ -85,10 +85,10 @@ Extend Kilo Auto into four tiers.
**Model options for Auto: Small**:
| Model | Cost | Capability | Notes |
| ----- | ---- | ---------- | ----- |
| Model | Cost | Capability | Notes |
| ----------- | ---------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------- |
| gpt-oss-20b | ~50% cheaper than GPT-5 Nano | Lower — suitable for simple background tasks like titles and summaries | Open-weight, cost-optimized option for high-volume lightweight tasks |
| GPT-5 Nano | Higher than gpt-oss-20b | Higher — better instruction following and output quality | Preferred when credits are available and task quality matters |
| GPT-5 Nano | Higher than gpt-oss-20b | Higher — better instruction following and output quality | Preferred when credits are available and task quality matters |
Auto: Small should prefer **gpt-oss-20b** when minimizing cost (e.g., free users, high-volume background tasks) and **GPT-5 Nano** when credits are available and higher output quality is desired.
@@ -24,7 +24,7 @@ graph LR
gateway["Kilo Gateway"]
cloudagent["Cloud Agent"]
bot["Kilo Bot"]
claw["Kilo Claw"]
claw["KiloClaw"]
review["Code Review"]
triage["Auto Triage"]
appbuilder["App Builder"]
@@ -111,7 +111,7 @@ A Cloudflare Worker within Kilo Cloud that runs the Kilo CLI in isolated sandbox
The GitHub/GitLab bot that responds to issue comments and PR mentions. It dispatches work to the Cloud Agent, enabling users to trigger AI coding tasks directly from their repositories.
### Kilo Claw
### KiloClaw
A multi-tenant compute platform running on Fly.io, orchestrated by a Cloudflare Worker. Each user gets a dedicated persistent machine running an OpenClaw gateway, coordinated via Durable Objects for state management and self-healing reconciliation.
@@ -229,10 +229,10 @@ The project uses:
## Repositories
| Repository | Contents |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode) | CLI engine, VS Code extension, SDK, gateway client, telemetry, docs, UI components |
| Cloud (private) | Web dashboard, Cloud Agent, Kilo Bot, Kilo Claw, code review, auto triage, billing, and supporting Cloudflare Workers |
| Repository | Contents |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode) | CLI engine, VS Code extension, SDK, gateway client, telemetry, docs, UI components |
| Cloud (private) | Web dashboard, Cloud Agent, Kilo Bot, KiloClaw, code review, auto triage, billing, and supporting Cloudflare Workers |
## Further Reading
@@ -18,7 +18,7 @@ If you'd like to migrate your memory bank content to AGENTS.md:
1. Examine the contents in `.kilocode/rules/memory-bank/`
2. Move that content into your project's `AGENTS.md` file (or ask Kilo to do it for you)
{% /callout %}
{% /callout %}
## What is AGENTS.md?
+4 -4
View File
@@ -233,10 +233,10 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
const endpoint = new URL("fim/completions", baseApiUrl)
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }),
[HEADER_FEATURE]: "autocomplete",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }),
[HEADER_FEATURE]: "autocomplete",
}
const response = await fetch(endpoint, {
@@ -1,15 +1,12 @@
/**
* SetupScriptRunner - Executes worktree setup scripts
*
* Runs setup scripts in VS Code integrated terminal before agent starts.
* Uses VS Code shell integration to track execution and exit code.
* Falls back to sendText + onDidCloseTerminal if shell integration is unavailable.
* Cross-platform: Unix uses sh, Windows uses cmd.exe.
* Runs setup scripts as VS Code tasks before the agent starts.
* This relies on VS Code's task execution model instead of manual terminal command strings.
*/
import * as vscode from "vscode"
import { SetupScriptService } from "./SetupScriptService"
import { buildSetupCommand } from "./setup-script-command"
import { SetupScriptService, type SetupScriptInfo } from "./SetupScriptService"
export interface SetupScriptEnvironment {
/** Absolute path to the worktree directory */
@@ -18,6 +15,37 @@ export interface SetupScriptEnvironment {
repoPath: string
}
interface SetupTaskCommand {
command: string
args: string[]
}
const TASK_END_GRACE_MS = 250
const TASK_TIMEOUT_MS = 5 * 60 * 1000
function quoteCmdArg(value: string): string {
return `"${value.replaceAll('"', '""')}"`
}
function buildSetupTaskCommand(script: SetupScriptInfo): SetupTaskCommand {
if (script.kind === "powershell") {
return {
command: "powershell.exe",
args: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.path],
}
}
if (script.kind === "cmd") {
return {
command: "cmd.exe",
args: ["/d", "/s", "/c", quoteCmdArg(script.path)],
}
}
return {
command: "sh",
args: [script.path],
}
}
export class SetupScriptRunner {
constructor(
private readonly output: vscode.OutputChannel,
@@ -31,16 +59,16 @@ export class SetupScriptRunner {
* @returns true if script was executed, false if skipped (no script configured)
*/
async runIfConfigured(env: SetupScriptEnvironment): Promise<boolean> {
if (!this.service.hasScript()) {
const script = this.service.resolveScript()
if (!script) {
this.log("No setup script configured, skipping")
return false
}
const script = this.service.getScriptPath()
this.log(`Running setup script: ${script}`)
this.log(`Running setup script: ${script.path}`)
try {
await this.executeInTerminal(script, env)
await this.executeTask(script, env)
this.log("Setup script completed")
return true
} catch (error) {
@@ -50,99 +78,96 @@ export class SetupScriptRunner {
}
}
/** Execute the setup script in a VS Code terminal and wait for it to finish. */
private async executeInTerminal(script: string, env: SetupScriptEnvironment): Promise<void> {
const terminal = vscode.window.createTerminal({
name: "Worktree Setup",
/** Execute setup script as a VS Code task and wait for completion. */
private async executeTask(script: SetupScriptInfo, env: SetupScriptEnvironment): Promise<void> {
const task = this.createTask(script, env)
const execution = await vscode.tasks.executeTask(task)
await this.waitForTaskEnd(execution)
}
private createTask(script: SetupScriptInfo, env: SetupScriptEnvironment): vscode.Task {
const cmd = buildSetupTaskCommand(script)
const execution = new vscode.ProcessExecution(cmd.command, cmd.args, {
cwd: env.worktreePath,
env: {
WORKTREE_PATH: env.worktreePath,
REPO_PATH: env.repoPath,
},
iconPath: new vscode.ThemeIcon("gear"),
})
terminal.show(true)
// Try shell integration first — gives us proper exit code tracking
const integration = await this.waitForShellIntegration(terminal, 5000)
if (integration) {
this.log("Using shell integration for setup script execution")
await this.runViaShellIntegration(terminal, integration, script, env)
} else {
this.log("Shell integration unavailable, falling back to sendText")
await this.runViaSendText(terminal, script, env)
const task = new vscode.Task(
{
type: "kilo-worktree-setup",
script: script.path,
},
vscode.TaskScope.Workspace,
"Worktree Setup",
"Kilo Code",
execution,
[],
)
task.presentationOptions = {
reveal: vscode.TaskRevealKind.Always,
panel: vscode.TaskPanelKind.Dedicated,
clear: true,
showReuseMessage: false,
}
return task
}
/** Wait for shell integration to become available on a terminal, with timeout. */
private waitForShellIntegration(
terminal: vscode.Terminal,
timeout: number,
): Promise<vscode.TerminalShellIntegration | undefined> {
if (terminal.shellIntegration) return Promise.resolve(terminal.shellIntegration)
return new Promise((resolve) => {
const timer = setTimeout(() => {
listener.dispose()
resolve(undefined)
}, timeout)
const listener = vscode.window.onDidChangeTerminalShellIntegration((e) => {
if (e.terminal !== terminal) return
clearTimeout(timer)
listener.dispose()
resolve(e.shellIntegration)
})
})
}
/** Run script via shell integration — tracks execution and exit code properly. */
private runViaShellIntegration(
terminal: vscode.Terminal,
integration: vscode.TerminalShellIntegration,
script: string,
env: SetupScriptEnvironment,
): Promise<void> {
return new Promise((resolve) => {
const command = buildSetupCommand(script, env)
const execution = integration.executeCommand(command)
const cleanup = () => {
execListener.dispose()
closeListener.dispose()
private waitForTaskEnd(execution: vscode.TaskExecution): Promise<void> {
return new Promise((resolve, reject) => {
const state = {
done: false,
grace: undefined as ReturnType<typeof setTimeout> | undefined,
timeout: undefined as ReturnType<typeof setTimeout> | undefined,
}
// Primary: shell integration reports execution finished with exit code
const execListener = vscode.window.onDidEndTerminalShellExecution((e) => {
if (e.execution !== execution) return
cleanup()
this.log(`Setup script exited with code ${e.exitCode ?? "unknown"}`)
const finish = (error?: Error) => {
if (state.done) return
state.done = true
if (state.grace) {
clearTimeout(state.grace)
}
if (state.timeout) {
clearTimeout(state.timeout)
}
processListener.dispose()
endListener.dispose()
if (error) {
reject(error)
return
}
resolve()
}
const processListener = vscode.tasks.onDidEndTaskProcess((event) => {
if (event.execution !== execution) return
this.log(`Setup script exited with code ${event.exitCode ?? "unknown"}`)
const code = event.exitCode
if (typeof code !== "number") {
finish(new Error("Setup script exited without a valid exit code"))
return
}
if (code !== 0) {
finish(new Error(`Setup script exited with code ${code}`))
return
}
finish()
})
// Fallback: terminal was closed externally (user, VS Code restart, etc.)
const closeListener = vscode.window.onDidCloseTerminal((closed) => {
if (closed !== terminal) return
cleanup()
this.log("Setup script terminal closed before execution event fired")
resolve()
})
})
}
/** Fallback: run via sendText and wait for terminal to close. */
private runViaSendText(terminal: vscode.Terminal, script: string, env: SetupScriptEnvironment): Promise<void> {
return new Promise((resolve) => {
const listener = vscode.window.onDidCloseTerminal((closed) => {
if (closed !== terminal) return
listener.dispose()
resolve()
const endListener = vscode.tasks.onDidEndTask((event) => {
if (event.execution !== execution) return
if (state.done) return
state.grace = setTimeout(() => {
this.log("Setup script finished without process exit event")
finish()
}, TASK_END_GRACE_MS)
})
const command = buildSetupCommand(script, env) + (process.platform === "win32" ? "& exit" : "; exit")
terminal.sendText(command)
this.log("Setup script started in terminal, waiting for completion...")
state.timeout = setTimeout(() => {
this.log("Setup script timed out waiting for task completion")
finish(new Error("Setup script timed out after 5 minutes"))
}, TASK_TIMEOUT_MS)
})
}
@@ -1,66 +1,132 @@
/**
* SetupScriptService - Manages worktree setup scripts
*
* Handles reading, creating, and checking for setup scripts stored in .kilocode/setup-script.
* Handles reading, creating, and checking for setup scripts stored in .kilocode/.
* Setup scripts run before an agent starts in a worktree (new sessions only).
*/
import * as vscode from "vscode"
import * as fs from "node:fs"
import * as path from "node:path"
import { SETUP_SCRIPT_TEMPLATE } from "./setup-script-template"
import { SETUP_SCRIPT_TEMPLATE, SETUP_SCRIPT_TEMPLATE_POWERSHELL } from "./setup-script-template"
const SETUP_SCRIPT_FILENAME = "setup-script"
const SETUP_SCRIPT_SHELL_FILENAME = "setup-script.sh"
const SETUP_SCRIPT_POWERSHELL_FILENAME = "setup-script.ps1"
const SETUP_SCRIPT_CMD_FILENAME = "setup-script.cmd"
const SETUP_SCRIPT_BAT_FILENAME = "setup-script.bat"
const KILOCODE_DIR = ".kilocode"
export type SetupScriptKind = "posix" | "powershell" | "cmd"
type SetupScriptDefaultKind = Exclude<SetupScriptKind, "cmd">
export interface SetupScriptInfo {
path: string
kind: SetupScriptKind
}
interface SetupScriptCandidate {
name: string
kind: SetupScriptKind
}
interface SetupScriptDefaultCandidate {
name: string
kind: SetupScriptDefaultKind
}
export class SetupScriptService {
private readonly root: string
private readonly script: string
private readonly dir: string
constructor(root: string) {
this.root = root
this.script = path.join(root, KILOCODE_DIR, SETUP_SCRIPT_FILENAME)
this.dir = path.join(root, KILOCODE_DIR)
}
/** Get the path to the setup script */
getScriptPath(): string {
return this.script
/** Resolve the setup script path and interpreter type for the current platform. */
resolveScript(platform: NodeJS.Platform = process.platform): SetupScriptInfo | undefined {
for (const candidate of this.candidates(platform)) {
const scriptPath = path.join(this.dir, candidate.name)
if (!fs.existsSync(scriptPath)) continue
return {
path: scriptPath,
kind: candidate.kind,
}
}
return undefined
}
/** Get the script path if configured, otherwise return the default path for this platform. */
getScriptPath(platform: NodeJS.Platform = process.platform): string {
const resolved = this.resolveScript(platform)
if (resolved) return resolved.path
return path.join(this.dir, this.defaultCandidate(platform).name)
}
/** Check if a setup script exists */
hasScript(): boolean {
return fs.existsSync(this.script)
hasScript(platform: NodeJS.Platform = process.platform): boolean {
return this.resolveScript(platform) !== undefined
}
/** Read the setup script content. Returns null if not found or read fails. */
async getScript(): Promise<string | null> {
if (!this.hasScript()) return null
async getScript(platform: NodeJS.Platform = process.platform): Promise<string | null> {
const script = this.resolveScript(platform)
if (!script) return null
try {
return await fs.promises.readFile(this.script, "utf-8")
return await fs.promises.readFile(script.path, "utf-8")
} catch (error) {
this.log(`Failed to read setup script: ${error}`)
return null
}
}
/** Create a default setup script with helpful comments */
async createDefaultScript(): Promise<void> {
const dir = path.join(this.root, KILOCODE_DIR)
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true })
/** Create a default setup script with helpful comments for the current platform. */
async createDefaultScript(platform: NodeJS.Platform = process.platform): Promise<void> {
if (!fs.existsSync(this.dir)) {
await fs.promises.mkdir(this.dir, { recursive: true })
}
await fs.promises.writeFile(this.script, SETUP_SCRIPT_TEMPLATE, "utf-8")
const script = this.defaultCandidate(platform)
const scriptPath = path.join(this.dir, script.name)
const content = this.defaultTemplate(script.kind)
await fs.promises.writeFile(scriptPath, content, "utf-8")
}
/** Open the setup script in VS Code editor. Creates the default script if it doesn't exist. */
async openInEditor(): Promise<void> {
if (!this.hasScript()) {
await this.createDefaultScript()
async openInEditor(platform: NodeJS.Platform = process.platform): Promise<void> {
if (!this.hasScript(platform)) {
await this.createDefaultScript(platform)
}
const document = await vscode.workspace.openTextDocument(this.script)
const resolved = this.resolveScript(platform)
if (!resolved) return
const document = await vscode.workspace.openTextDocument(resolved.path)
await vscode.window.showTextDocument(document)
}
private candidates(platform: NodeJS.Platform): SetupScriptCandidate[] {
if (platform === "win32") {
return [
{ name: SETUP_SCRIPT_POWERSHELL_FILENAME, kind: "powershell" },
{ name: SETUP_SCRIPT_CMD_FILENAME, kind: "cmd" },
{ name: SETUP_SCRIPT_BAT_FILENAME, kind: "cmd" },
]
}
return [
{ name: SETUP_SCRIPT_FILENAME, kind: "posix" },
{ name: SETUP_SCRIPT_SHELL_FILENAME, kind: "posix" },
]
}
private defaultCandidate(platform: NodeJS.Platform): SetupScriptDefaultCandidate {
if (platform === "win32") {
return { name: SETUP_SCRIPT_POWERSHELL_FILENAME, kind: "powershell" }
}
return { name: SETUP_SCRIPT_FILENAME, kind: "posix" }
}
private defaultTemplate(kind: SetupScriptDefaultKind): string {
if (kind === "powershell") return SETUP_SCRIPT_TEMPLATE_POWERSHELL
return SETUP_SCRIPT_TEMPLATE
}
private log(message: string): void {
// Log to console since we don't have an OutputChannel here
console.log(`[SetupScriptService] ${message}`)
@@ -240,6 +240,10 @@ export class WorktreeManager {
await this.addExcludeEntry(excludePath, ".kilocode/worktrees/", "Kilo Code agent worktrees")
await this.addExcludeEntry(excludePath, ".kilocode/agent-manager.json", "Kilo Agent Manager state")
await this.addExcludeEntry(excludePath, ".kilocode/setup-script", "Kilo Code worktree setup script")
await this.addExcludeEntry(excludePath, ".kilocode/setup-script.sh", "Kilo Code worktree setup script")
await this.addExcludeEntry(excludePath, ".kilocode/setup-script.ps1", "Kilo Code worktree setup script")
await this.addExcludeEntry(excludePath, ".kilocode/setup-script.cmd", "Kilo Code worktree setup script")
await this.addExcludeEntry(excludePath, ".kilocode/setup-script.bat", "Kilo Code worktree setup script")
}
private async ensureWorktreeExclude(worktreePath: string): Promise<void> {
@@ -0,0 +1,120 @@
import { describe, it, expect, vi } from "vitest"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
vi.mock("vscode", () => ({
workspace: {
openTextDocument: vi.fn(),
},
window: {
showTextDocument: vi.fn(),
},
}))
import { SetupScriptService } from "../SetupScriptService"
function setupRoot(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "kilo-setup-script-"))
}
function scriptPath(root: string, name: string): string {
return path.join(root, ".kilocode", name)
}
function writeScript(root: string, name: string, content = "echo ok\n"): string {
const dir = path.join(root, ".kilocode")
fs.mkdirSync(dir, { recursive: true })
const script = path.join(dir, name)
fs.writeFileSync(script, content, "utf-8")
return script
}
describe("SetupScriptService", () => {
it("resolves POSIX setup script on macOS", () => {
const root = setupRoot()
try {
const file = writeScript(root, "setup-script")
const service = new SetupScriptService(root)
const resolved = service.resolveScript("darwin")
expect(resolved).toEqual({
path: file,
kind: "posix",
})
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
it("prefers PowerShell script on Windows when multiple variants exist", () => {
const root = setupRoot()
try {
const ps1 = writeScript(root, "setup-script.ps1")
writeScript(root, "setup-script.cmd")
writeScript(root, "setup-script")
const service = new SetupScriptService(root)
const resolved = service.resolveScript("win32")
expect(resolved).toEqual({
path: ps1,
kind: "powershell",
})
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
it("resolves cmd script on Windows when no PowerShell script exists", () => {
const root = setupRoot()
try {
const cmd = writeScript(root, "setup-script.cmd")
const service = new SetupScriptService(root)
const resolved = service.resolveScript("win32")
expect(resolved).toEqual({
path: cmd,
kind: "cmd",
})
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
it("does not resolve POSIX setup scripts on Windows", () => {
const root = setupRoot()
try {
writeScript(root, "setup-script")
writeScript(root, "setup-script.sh")
const service = new SetupScriptService(root)
const resolved = service.resolveScript("win32")
expect(resolved).toBeUndefined()
expect(service.hasScript("win32")).toBe(false)
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
it("creates default PowerShell script on Windows", async () => {
const root = setupRoot()
try {
const service = new SetupScriptService(root)
await service.createDefaultScript("win32")
const file = scriptPath(root, "setup-script.ps1")
expect(fs.existsSync(file)).toBe(true)
expect(service.hasScript("win32")).toBe(true)
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
it("creates default POSIX script on macOS", async () => {
const root = setupRoot()
try {
const service = new SetupScriptService(root)
await service.createDefaultScript("darwin")
const file = scriptPath(root, "setup-script")
expect(fs.existsSync(file)).toBe(true)
expect(service.hasScript("darwin")).toBe(true)
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
})
@@ -1,85 +0,0 @@
import { describe, it, expect } from "vitest"
import { buildSetupCommand } from "../setup-script-command"
const env = {
worktreePath: "/repos/project/.kilocode/worktrees/wt-1",
repoPath: "/repos/project",
}
const script = "/repos/project/.kilocode/setup-script"
describe("buildSetupCommand", () => {
it("builds unix command with inline env vars and sh", () => {
const result = buildSetupCommand(script, env, "darwin")
expect(result).toBe(
`WORKTREE_PATH="/repos/project/.kilocode/worktrees/wt-1" REPO_PATH="/repos/project" sh "/repos/project/.kilocode/setup-script"`,
)
})
it("builds linux command same as darwin", () => {
const result = buildSetupCommand(script, env, "linux")
expect(result).toContain("sh ")
expect(result).not.toContain("set ")
expect(result).not.toContain("call ")
})
it("builds windows command with set and call", () => {
const result = buildSetupCommand(script, env, "win32")
expect(result).toBe(
`set "WORKTREE_PATH=/repos/project/.kilocode/worktrees/wt-1" && set "REPO_PATH=/repos/project" && call "/repos/project/.kilocode/setup-script"`,
)
})
it("includes both env vars in unix command", () => {
const result = buildSetupCommand(script, env, "darwin")
expect(result).toContain(`WORKTREE_PATH="${env.worktreePath}"`)
expect(result).toContain(`REPO_PATH="${env.repoPath}"`)
})
it("includes both env vars in windows command", () => {
const result = buildSetupCommand(script, env, "win32")
expect(result).toContain(`set "WORKTREE_PATH=${env.worktreePath}"`)
expect(result).toContain(`set "REPO_PATH=${env.repoPath}"`)
})
it("handles paths with spaces", () => {
const spaced = {
worktreePath: "/Users/dev/my project/.kilocode/worktrees/wt-1",
repoPath: "/Users/dev/my project",
}
const spacedScript = "/Users/dev/my project/.kilocode/setup-script"
const unix = buildSetupCommand(spacedScript, spaced, "darwin")
expect(unix).toContain(`sh "/Users/dev/my project/.kilocode/setup-script"`)
const win = buildSetupCommand(spacedScript, spaced, "win32")
expect(win).toContain(`call "/Users/dev/my project/.kilocode/setup-script"`)
})
it("escapes double quotes in unix paths", () => {
const dangerous = {
worktreePath: '/repos/proj"ect',
repoPath: "/repos/safe",
}
const result = buildSetupCommand(script, dangerous, "darwin")
expect(result).toContain(`WORKTREE_PATH="/repos/proj\\"ect"`)
})
it("escapes dollar signs in unix paths", () => {
const dangerous = {
worktreePath: "/repos/$HOME/project",
repoPath: "/repos/safe",
}
const result = buildSetupCommand(script, dangerous, "darwin")
expect(result).toContain(`WORKTREE_PATH="/repos/\\$HOME/project"`)
})
it("escapes backticks in unix paths", () => {
const dangerous = {
worktreePath: "/repos/`whoami`/project",
repoPath: "/repos/safe",
}
const result = buildSetupCommand(script, dangerous, "darwin")
expect(result).toContain('WORKTREE_PATH="/repos/\\`whoami\\`/project"')
})
})
@@ -1,21 +0,0 @@
/** Escape characters that are special inside double-quoted shell strings. */
function escapeShell(value: string): string {
return value.replace(/["$`\\]/g, "\\$&")
}
/** Build the platform-appropriate command string for running a setup script. */
export function buildSetupCommand(
script: string,
env: { worktreePath: string; repoPath: string },
platform: NodeJS.Platform = process.platform,
): string {
if (platform === "win32") {
// Windows cmd.exe: double quotes in set values don't need escaping the same way,
// but we escape for the call argument
return `set "WORKTREE_PATH=${env.worktreePath}" && set "REPO_PATH=${env.repoPath}" && call "${script}"`
}
const wt = escapeShell(env.worktreePath)
const repo = escapeShell(env.repoPath)
const path = escapeShell(script)
return `WORKTREE_PATH="${wt}" REPO_PATH="${repo}" sh "${path}"`
}
@@ -1,5 +1,5 @@
/** Default template for worktree setup scripts. */
export const SETUP_SCRIPT_TEMPLATE = `#!/bin/bash
/** Default POSIX template for worktree setup scripts. */
export const SETUP_SCRIPT_TEMPLATE = `#!/bin/sh
# Kilo Code Worktree Setup Script
# This script runs before the agent starts in a worktree (new sessions only).
#
@@ -39,3 +39,44 @@ echo "Setting up worktree: $WORKTREE_PATH"
echo "Setup complete!"
`
/** Default PowerShell template for worktree setup scripts on Windows. */
export const SETUP_SCRIPT_TEMPLATE_POWERSHELL = `# Kilo Code Worktree Setup Script
# This script runs before the agent starts in a worktree (new sessions only).
#
# Available environment variables:
# $env:WORKTREE_PATH - Absolute path to the worktree directory
# $env:REPO_PATH - Absolute path to the main repository
#
# Example tasks:
# - Copy .env files from main repo
# - Install dependencies
# - Run database migrations
# - Set up local configuration
$ErrorActionPreference = "Stop"
Write-Host "Setting up worktree: $env:WORKTREE_PATH"
# Uncomment and modify as needed:
# Copy environment files
# if (Test-Path "$env:REPO_PATH/.env") {
# Copy-Item "$env:REPO_PATH/.env" "$env:WORKTREE_PATH/.env" -Force
# Write-Host "Copied .env"
# }
# Install dependencies (Node.js)
# if (Test-Path "$env:WORKTREE_PATH/package.json") {
# Set-Location "$env:WORKTREE_PATH"
# npm install
# }
# Install dependencies (Python)
# if (Test-Path "$env:WORKTREE_PATH/requirements.txt") {
# Set-Location "$env:WORKTREE_PATH"
# pip install -r requirements.txt
# }
Write-Host "Setup complete!"
`
@@ -395,10 +395,14 @@ class LlamaTokenizer {
function testCase(inputString, expectedTokenIds) {
const actualTokens = tokenizer.encode(inputString, true, true, true)
if (!isEqual(actualTokens, expectedTokenIds)) {
throw new Error(`Test failed. LLaMA Tokenizer Encoder returned unexpected result: expected tokenize(${inputString}) === ${expectedTokenIds}, actual was: ${actualTokens}`)
throw new Error(
`Test failed. LLaMA Tokenizer Encoder returned unexpected result: expected tokenize(${inputString}) === ${expectedTokenIds}, actual was: ${actualTokens}`,
)
}
if (inputString !== tokenizer.decode(actualTokens)) {
throw new Error(`Test failed. LLaMA Tokenizer Decoder returned unexpected result: expected decode(${actualTokens}) === ${inputString}, actual was: ${decode(actualTokens)}`)
throw new Error(
`Test failed. LLaMA Tokenizer Decoder returned unexpected result: expected decode(${actualTokens}) === ${inputString}, actual was: ${decode(actualTokens)}`,
)
}
}
@@ -27,6 +27,7 @@ const TSX_FILES = [
]
const TSX_FILE = TSX_FILES[0]
const PROVIDER_FILE = path.join(ROOT, "src/agent-manager/AgentManagerProvider.ts")
const SETUP_SCRIPT_RUNNER_FILE = path.join(ROOT, "src/agent-manager/SetupScriptRunner.ts")
function readAllTsx(): string {
return TSX_FILES.map((f) => fs.readFileSync(f, "utf-8")).join("\n")
@@ -376,3 +377,25 @@ describe("KiloProvider — pending session refresh on reconnect", () => {
)
})
})
describe("SetupScriptRunner — task execution model", () => {
const runner = fs.readFileSync(SETUP_SCRIPT_RUNNER_FILE, "utf-8")
it("uses VS Code tasks API for setup execution", () => {
expect(runner).toContain("vscode.tasks.executeTask")
expect(runner).toContain("onDidEndTaskProcess")
expect(runner).toContain("onDidEndTask")
})
it("uses process-based task execution with env options", () => {
expect(runner).toContain("new vscode.ProcessExecution")
expect(runner).toContain("WORKTREE_PATH")
expect(runner).toContain("REPO_PATH")
})
it("does not use manual terminal command injection", () => {
expect(runner).not.toContain("createTerminal")
expect(runner).not.toContain("sendText")
expect(runner).not.toContain("buildSetupCommand")
})
})
@@ -96,7 +96,7 @@ function createConnection(client: ReturnType<typeof createClient>) {
}
describe("KiloProvider pending session refresh", () => {
it("flushes deferred refresh in initializeConnection without relying on connected event callback", async () => {
it.skip("flushes deferred refresh in initializeConnection without relying on connected event callback", async () => {
const client = createClient()
const connection = createConnection(client)
const provider = new KiloProvider({} as never, connection as never)
@@ -113,7 +113,7 @@ describe("KiloProvider pending session refresh", () => {
expect(internal.pendingSessionRefresh).toBe(false)
})
it("does not post not-connected errors while still connecting", async () => {
it.skip("does not post not-connected errors while still connecting", async () => {
const client = createClient()
const connection = createConnection(client)
const provider = new KiloProvider({} as never, connection as never)
+5 -1
View File
@@ -1,6 +1,7 @@
import type { Argv } from "yargs"
import type { Session as SDKSession, Message, Part } from "@kilocode/sdk/v2"
import { Session } from "../../session"
import { Bus } from "../../bus"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db"
@@ -131,7 +132,10 @@ export const ImportCommand = cmd({
return
}
Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run())
Database.use((db) => {
db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run()
Database.effect(() => Bus.publish(Session.Event.Created, { info: exportData.info }))
})
for (const msg of exportData.messages) {
Database.use((db) =>
@@ -111,7 +111,11 @@ export namespace PlanFollowup {
export const ANSWER_NEW_SESSION = "Start new session"
export const ANSWER_CONTINUE = "Continue here"
async function resolvePlan(input: { assistant?: MessageV2.WithParts; messages: MessageV2.WithParts[]; sessionID: string }) {
async function resolvePlan(input: {
assistant?: MessageV2.WithParts
messages: MessageV2.WithParts[]
sessionID: string
}) {
// Fast path: check the last assistant message's text first (avoids array scanning)
if (input.assistant) {
const text = toText(input.assistant)
@@ -121,9 +125,7 @@ export namespace PlanFollowup {
// Fallback: scan all assistant messages after the last user message (handles
// cases where plan text is on an earlier assistant and the last one is empty)
const lastUserIdx = input.messages.findLastIndex((m) => m.info.role === "user")
const assistantMessages = input.messages
.slice(lastUserIdx + 1)
.filter((m) => m.info.role === "assistant")
const assistantMessages = input.messages.slice(lastUserIdx + 1).filter((m) => m.info.role === "assistant")
const text = assistantMessages.map(toText).filter(Boolean).join("\n\n").trim()
if (text) return text