diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml
new file mode 100644
index 0000000000..04b6ae7ac8
--- /dev/null
+++ b/.github/workflows/close-issues.yml
@@ -0,0 +1,24 @@
+name: close-issues
+
+on:
+ schedule:
+ - cron: "0 2 * * *" # Daily at 2:00 AM
+ workflow_dispatch:
+
+jobs:
+ close:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Close stale issues
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: bun script/github/close-issues.ts
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
deleted file mode 100644
index 51c649d823..0000000000
--- a/.github/workflows/stale-issues.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: stale-issues
-
-on:
- schedule:
- - cron: "30 1 * * *" # Daily at 1:30 AM
- workflow_dispatch:
-
-env:
- DAYS_BEFORE_ISSUE_CLOSE: 90
- DAYS_BEFORE_PR_CLOSE: 30
-
-jobs:
- stale:
- if: github.repository == 'Kilo-Org/kilocode'
- runs-on: blacksmith-4vcpu-ubuntu-2404
- permissions:
- issues: write
- pull-requests: write
- steps:
- - uses: actions/stale@v10
- with:
- days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
- days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_CLOSE }}
- days-before-issue-close: 0
- days-before-pr-close: 0
- close-issue-message: |
- Closing this one out since it's been inactive for a while — not because it's a bad issue, just that older items tend to lose context over time and we'd rather start fresh if this is still a problem.
-
- Please feel free to reopen (or open a new issue) if you're still running into this. We're happy to take another look! 🙏
- close-pr-message: |
- Closing this one out since it's been inactive for a while — not because it's a bad PR, just that older items tend to lose context over time and we'd rather start fresh if this is still relevant.
-
- Please feel free to reopen (or open a new PR) if you're still working on this. We're happy to take another look! 🙏
- operations-per-run: 200
diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts
index d804195c40..c8e2dbb5d0 100644
--- a/packages/app/src/context/command-keybind.test.ts
+++ b/packages/app/src/context/command-keybind.test.ts
@@ -32,6 +32,25 @@ describe("command keybind helpers", () => {
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
})
+ test("matchKeybind supports bracket keys", () => {
+ const keybinds = parseKeybind("mod+alt+[, mod+alt+]")
+ const prev = keybinds[0]
+ const next = keybinds[1]
+
+ expect(
+ matchKeybind(
+ keybinds,
+ new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }),
+ ),
+ ).toBe(true)
+ expect(
+ matchKeybind(
+ keybinds,
+ new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }),
+ ),
+ ).toBe(true)
+ })
+
test("formatKeybind returns human readable output", () => {
const display = formatKeybind("ctrl+alt+arrowup")
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index cba3d5adbc..aa8953a20c 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -15,7 +15,7 @@ import { retry } from "@opencode-ai/util/retry"
import { batch } from "solid-js"
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types"
-import { cmp, normalizeProviderList } from "./utils"
+import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
type GlobalStore = {
@@ -174,7 +174,7 @@ export async function bootstrapDirectory(input: {
seededProject
? Promise.resolve()
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
- () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
+ () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() =>
retry(() =>
diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts
new file mode 100644
index 0000000000..24f1547c27
--- /dev/null
+++ b/packages/app/src/context/global-sync/utils.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, test } from "bun:test"
+import type { Agent } from "@kilocode/sdk/v2/client"
+import { normalizeAgentList } from "./utils"
+
+const agent = (name = "build") =>
+ ({
+ name,
+ mode: "primary",
+ permission: {},
+ options: {},
+ }) as Agent
+
+describe("normalizeAgentList", () => {
+ test("keeps array payloads", () => {
+ expect(normalizeAgentList([agent("build"), agent("docs")])).toEqual([agent("build"), agent("docs")])
+ })
+
+ test("wraps a single agent payload", () => {
+ expect(normalizeAgentList(agent("docs"))).toEqual([agent("docs")])
+ })
+
+ test("extracts agents from keyed objects", () => {
+ expect(
+ normalizeAgentList({
+ build: agent("build"),
+ docs: agent("docs"),
+ }),
+ ).toEqual([agent("build"), agent("docs")])
+ })
+
+ test("drops invalid payloads", () => {
+ expect(normalizeAgentList({ name: "AbortError" })).toEqual([])
+ expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
+ })
+})
diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts
index 6ba7524164..e6fd6f64ba 100644
--- a/packages/app/src/context/global-sync/utils.ts
+++ b/packages/app/src/context/global-sync/utils.ts
@@ -1,7 +1,21 @@
-import type { Project, ProviderListResponse } from "@kilocode/sdk/v2/client"
+import type { Agent, Project, ProviderListResponse } from "@kilocode/sdk/v2/client"
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
+function isAgent(input: unknown): input is Agent {
+ if (!input || typeof input !== "object") return false
+ const item = input as { name?: unknown; mode?: unknown }
+ if (typeof item.name !== "string") return false
+ return item.mode === "subagent" || item.mode === "primary" || item.mode === "all"
+}
+
+export function normalizeAgentList(input: unknown): Agent[] {
+ if (Array.isArray(input)) return input.filter(isAgent)
+ if (isAgent(input)) return [input]
+ if (!input || typeof input !== "object") return []
+ return Object.values(input).filter(isAgent)
+}
+
export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse {
return {
...input,
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index da22c55523..b5cbed6e75 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -97,15 +97,10 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
throw new Error(getRootNotFoundError())
}
-const localUrl = () =>
- `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
-
-const isLocalHost = () => ["localhost", "127.0.0.1", "0.0.0.0"].includes(location.hostname)
-
const getCurrentUrl = () => {
- if (location.hostname.includes("opencode.ai")) return localUrl()
- if (import.meta.env.DEV) return localUrl()
- if (isLocalHost()) return localUrl()
+ if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
+ if (import.meta.env.DEV)
+ return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin
}
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index 064d63d09e..2f3c359df5 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -137,7 +137,8 @@ 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}} متاحة الآن للاستخدام.",
@@ -721,8 +722,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
- "settings.permissions.tool.todoread.title": "قراءة المهام",
- "settings.permissions.tool.todoread.description": "قراءة قائمة المهام",
"settings.permissions.tool.todowrite.title": "كتابة المهام",
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
"settings.permissions.tool.webfetch.title": "جلب الويب",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 67f745f662..986485f804 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -732,8 +732,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
- "settings.permissions.tool.todoread.title": "Ler Tarefas",
- "settings.permissions.tool.todoread.description": "Ler a lista de tarefas",
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
"settings.permissions.tool.webfetch.title": "Buscar Web",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index 7dce2aa49c..f306242230 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -806,8 +806,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
- "settings.permissions.tool.todoread.title": "Čitanje liste zadataka",
- "settings.permissions.tool.todoread.description": "Čitanje liste zadataka",
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
"settings.permissions.tool.webfetch.title": "Web preuzimanje",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index eb4e5a013e..32047b71f2 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -800,8 +800,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler",
- "settings.permissions.tool.todoread.title": "Læs To-do",
- "settings.permissions.tool.todoread.description": "Læs to-do listen",
"settings.permissions.tool.todowrite.title": "Skriv To-do",
"settings.permissions.tool.todowrite.description": "Opdater to-do listen",
"settings.permissions.tool.webfetch.title": "Webhentning",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index 940e8f5e28..79422106dc 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -743,8 +743,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen",
- "settings.permissions.tool.todoread.title": "Todo lesen",
- "settings.permissions.tool.todoread.description": "Die Todo-Liste lesen",
"settings.permissions.tool.todowrite.title": "Todo schreiben",
"settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren",
"settings.permissions.tool.webfetch.title": "Web-Abruf",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 69ad646738..4e1a9c1f76 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -900,8 +900,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Load a skill by name",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Run language server queries",
- "settings.permissions.tool.todoread.title": "Todo Read",
- "settings.permissions.tool.todoread.description": "Read the todo list",
"settings.permissions.tool.todowrite.title": "Todo Write",
"settings.permissions.tool.todowrite.description": "Update the todo list",
"settings.permissions.tool.webfetch.title": "Web Fetch",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index a6410b7a23..be284d5c53 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -813,8 +813,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Cargar una habilidad por nombre",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje",
- "settings.permissions.tool.todoread.title": "Leer Todo",
- "settings.permissions.tool.todoread.description": "Leer la lista de tareas",
"settings.permissions.tool.todowrite.title": "Escribir Todo",
"settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas",
"settings.permissions.tool.webfetch.title": "Web Fetch",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 5324d76c2f..4e9cb212ac 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -741,8 +741,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Charger une compétence par son nom",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage",
- "settings.permissions.tool.todoread.title": "Lire Todo",
- "settings.permissions.tool.todoread.description": "Lire la liste de tâches",
"settings.permissions.tool.todowrite.title": "Écrire Todo",
"settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches",
"settings.permissions.tool.webfetch.title": "Récupération Web",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index 13efedfe10..8c000dd0f3 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -727,8 +727,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "名前によるスキルの読み込み",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "言語サーバークエリの実行",
- "settings.permissions.tool.todoread.title": "Todo読み込み",
- "settings.permissions.tool.todoread.description": "Todoリストの読み込み",
"settings.permissions.tool.todowrite.title": "Todo書き込み",
"settings.permissions.tool.todowrite.description": "Todoリストの更新",
"settings.permissions.tool.webfetch.title": "Web取得",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index a2cc1499d2..90fdf9ba98 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -726,8 +726,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "이름으로 기술 로드",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행",
- "settings.permissions.tool.todoread.title": "할 일 읽기",
- "settings.permissions.tool.todoread.description": "할 일 목록 읽기",
"settings.permissions.tool.todowrite.title": "할 일 쓰기",
"settings.permissions.tool.todowrite.description": "할 일 목록 업데이트",
"settings.permissions.tool.webfetch.title": "웹 가져오기",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index 26f7e778ee..cd52fb43fc 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -807,8 +807,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
- "settings.permissions.tool.todoread.title": "Les gjøremål",
- "settings.permissions.tool.todoread.description": "Les gjøremålslisten",
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
"settings.permissions.tool.webfetch.title": "Webhenting",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index 4b9f4d7ecc..6677e122e1 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -729,8 +729,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego",
- "settings.permissions.tool.todoread.title": "Odczyt Todo",
- "settings.permissions.tool.todoread.description": "Odczyt listy zadań",
"settings.permissions.tool.todowrite.title": "Zapis Todo",
"settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań",
"settings.permissions.tool.webfetch.title": "Pobieranie z sieci",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index 1790e371c8..12d003a7ac 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -808,8 +808,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Загрузка навыка по имени",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Запросы к языковому серверу",
- "settings.permissions.tool.todoread.title": "Todo Read",
- "settings.permissions.tool.todoread.description": "Чтение списка задач",
"settings.permissions.tool.todowrite.title": "Todo Write",
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
"settings.permissions.tool.webfetch.title": "Web Fetch",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 7b67c3bc45..abcbb5dc61 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -796,8 +796,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
- "settings.permissions.tool.todoread.title": "อ่านรายการงาน",
- "settings.permissions.tool.todoread.description": "อ่านรายการงาน",
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts
index fc2591be1e..cbc183fcfd 100644
--- a/packages/app/src/i18n/tr.ts
+++ b/packages/app/src/i18n/tr.ts
@@ -816,8 +816,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Ada göre bir beceri yükle",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır",
- "settings.permissions.tool.todoread.title": "Görev Oku",
- "settings.permissions.tool.todoread.description": "Görev listesini oku",
"settings.permissions.tool.todowrite.title": "Görev Yaz",
"settings.permissions.tool.todowrite.description": "Görev listesini güncelle",
"settings.permissions.tool.webfetch.title": "Web Getir",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index b525571ef6..8485dfb523 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -795,8 +795,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "按名称加载技能",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "运行语言服务器查询",
- "settings.permissions.tool.todoread.title": "读取待办",
- "settings.permissions.tool.todoread.description": "读取待办列表",
"settings.permissions.tool.todowrite.title": "更新待办",
"settings.permissions.tool.todowrite.description": "更新待办列表",
"settings.permissions.tool.webfetch.title": "网页获取",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 7d2687e68a..c35834fcff 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -790,8 +790,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "按名稱載入技能",
"settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "執行語言伺服器查詢",
- "settings.permissions.tool.todoread.title": "讀取待辦",
- "settings.permissions.tool.todoread.description": "讀取待辦清單",
"settings.permissions.tool.todowrite.title": "更新待辦",
"settings.permissions.tool.todowrite.description": "更新待辦清單",
"settings.permissions.tool.webfetch.title": "Web Fetch",
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 465e9a083d..a3a5c73fa3 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -965,6 +965,8 @@ export default function Layout(props: ParentProps) {
: projects[(index + offset + projects.length) % projects.length]
if (!target) return
+ // warm up child store to prevent flicker
+ globalSync.child(target.worktree)
openProject(target.worktree)
}
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx
index e6c3d0c1d4..b2120e8470 100644
--- a/packages/app/src/pages/session/message-timeline.tsx
+++ b/packages/app/src/pages/session/message-timeline.tsx
@@ -896,7 +896,8 @@ export function MessageTimeline(props: {
{
id: "message.previous",
title: language.t("command.message.previous"),
description: language.t("command.message.previous.description"),
- keybind: "mod+arrowup",
+ keybind: "mod+alt+[",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(-1),
}),
@@ -341,7 +341,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
id: "message.next",
title: language.t("command.message.next"),
description: language.t("command.message.next.description"),
- keybind: "mod+arrowdown",
+ keybind: "mod+alt+]",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(1),
}),
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts
index 543f857a5e..d2cfc25241 100644
--- a/packages/desktop-electron/src/main/ipc.ts
+++ b/packages/desktop-electron/src/main/ipc.ts
@@ -88,7 +88,7 @@ export function registerIpcHandlers(deps: Deps) {
"open-directory-picker",
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
const result = await dialog.showOpenDialog({
- properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : [])],
+ properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
title: opts?.title ?? "Choose a folder",
defaultPath: opts?.defaultPath,
})
diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts
index fa1c5682e2..cf2d25b110 100644
--- a/packages/desktop-electron/src/main/store.ts
+++ b/packages/desktop-electron/src/main/store.ts
@@ -7,7 +7,7 @@ const cache = new Map
()
export function getStore(name = SETTINGS_STORE) {
const cached = cache.get(name)
if (cached) return cached
- const next = new Store({ name })
+ const next = new Store({ name, fileExtension: "" })
cache.set(name, next)
return next
}
diff --git a/packages/desktop/src/i18n/de.ts b/packages/desktop/src/i18n/de.ts
index 224e0b62d2..6ef1167a36 100644
--- a/packages/desktop/src/i18n/de.ts
+++ b/packages/desktop/src/i18n/de.ts
@@ -45,7 +45,8 @@ export const dict = {
"desktop.menu.help.shareFeedback": "Feedback teilen",
"desktop.menu.help.reportBug": "Einen Fehler melden",
"desktop.cli.error.unsupportedPlatform": "Die CLI-Installation wird nur unter macOS und Linux unterstützt.",
- "desktop.cli.error.sidecarMissing": "Das Kilo CLI-Binary fehlt. Versuchen Sie, die Desktop-App neu zu installieren.",
+ "desktop.cli.error.sidecarMissing":
+ "Das Kilo CLI-Binary fehlt. Versuchen Sie, die Desktop-App neu zu installieren.",
"desktop.cli.error.scriptWriteFailed": "Das CLI-Installationsskript konnte nicht vorbereitet werden.",
"desktop.cli.error.scriptPermissionFailed": "Das CLI-Installationsskript konnte nicht ausführbar gemacht werden.",
"desktop.cli.error.scriptRunFailed": "Das CLI-Installationsskript konnte nicht ausgeführt werden.",
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index d3969b8591..f6679d2eca 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
-version = "1.3.2"
+version = "1.3.3"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.2/opencode-darwin-arm64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.3/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.2/opencode-darwin-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.3/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.2/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.3/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.2/opencode-linux-x64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.3/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.2/opencode-windows-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.3/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md
index a71c634635..430da2cc63 100644
--- a/packages/kilo-docs/source-links.md
+++ b/packages/kilo-docs/source-links.md
@@ -1,13 +1,125 @@
# Source Code Links
-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
@@ -23,18 +135,132 @@
-
+-
+
-
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
@@ -68,13 +294,38 @@
-
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
+
-
-
@@ -98,10 +349,54 @@
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
@@ -114,22 +409,74 @@
-
+-
+
-
-
+-
+
+-
+
+-
+
-
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
-
+-
+
+-
+
+-
+
-
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
+-
+
-
-
@@ -162,11 +509,29 @@
-
+-
+
+-
+
+-
+
-
+-
+
-
+-
+
+-
+
-
+-
+
+-
+
+-
+
-
diff --git a/packages/opencode/migration/20260323234822_events/migration.sql b/packages/opencode/migration/20260323234822_events/migration.sql
new file mode 100644
index 0000000000..b0fe7e4e6b
--- /dev/null
+++ b/packages/opencode/migration/20260323234822_events/migration.sql
@@ -0,0 +1,13 @@
+CREATE TABLE `event_sequence` (
+ `aggregate_id` text PRIMARY KEY,
+ `seq` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `event` (
+ `id` text PRIMARY KEY,
+ `aggregate_id` text NOT NULL,
+ `seq` integer NOT NULL,
+ `type` text NOT NULL,
+ `data` text NOT NULL,
+ CONSTRAINT `fk_event_aggregate_id_event_sequence_aggregate_id_fk` FOREIGN KEY (`aggregate_id`) REFERENCES `event_sequence`(`aggregate_id`) ON DELETE CASCADE
+);
diff --git a/packages/opencode/migration/20260323234822_events/snapshot.json b/packages/opencode/migration/20260323234822_events/snapshot.json
new file mode 100644
index 0000000000..07519aab71
--- /dev/null
+++ b/packages/opencode/migration/20260323234822_events/snapshot.json
@@ -0,0 +1,1271 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed",
+ "prevIds": ["37e1554d-af4c-43f2-aa7c-307fb49a315e"],
+ "ddl": [
+ {
+ "name": "account_state",
+ "entityType": "tables"
+ },
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "control_account",
+ "entityType": "tables"
+ },
+ {
+ "name": "workspace",
+ "entityType": "tables"
+ },
+ {
+ "name": "project",
+ "entityType": "tables"
+ },
+ {
+ "name": "message",
+ "entityType": "tables"
+ },
+ {
+ "name": "part",
+ "entityType": "tables"
+ },
+ {
+ "name": "permission",
+ "entityType": "tables"
+ },
+ {
+ "name": "session",
+ "entityType": "tables"
+ },
+ {
+ "name": "todo",
+ "entityType": "tables"
+ },
+ {
+ "name": "session_share",
+ "entityType": "tables"
+ },
+ {
+ "name": "event_sequence",
+ "entityType": "tables"
+ },
+ {
+ "name": "event",
+ "entityType": "tables"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_account_id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_org_id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token_expiry",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token_expiry",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "branch",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "directory",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "extra",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "worktree",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "vcs",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "icon_url",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "icon_color",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_initialized",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "sandboxes",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "commands",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "message_id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "workspace_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "parent_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "slug",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "directory",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "title",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "version",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "share_url",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_additions",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_deletions",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_files",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_diffs",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "revert",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "permission",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_compacting",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_archived",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "content",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "priority",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "position",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "secret",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "aggregate_id",
+ "entityType": "columns",
+ "table": "event_sequence"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seq",
+ "entityType": "columns",
+ "table": "event_sequence"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "aggregate_id",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seq",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "columns": ["active_account_id"],
+ "tableTo": "account",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "nameExplicit": false,
+ "name": "fk_account_state_active_account_id_account_id_fk",
+ "entityType": "fks",
+ "table": "account_state"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_workspace_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "workspace"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_message_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "message"
+ },
+ {
+ "columns": ["message_id"],
+ "tableTo": "message",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_part_message_id_message_id_fk",
+ "entityType": "fks",
+ "table": "part"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_permission_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "permission"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "session"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_todo_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "todo"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_share_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "session_share"
+ },
+ {
+ "columns": ["aggregate_id"],
+ "tableTo": "event_sequence",
+ "columnsTo": ["aggregate_id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
+ "entityType": "fks",
+ "table": "event"
+ },
+ {
+ "columns": ["email", "url"],
+ "nameExplicit": false,
+ "name": "control_account_pk",
+ "entityType": "pks",
+ "table": "control_account"
+ },
+ {
+ "columns": ["session_id", "position"],
+ "nameExplicit": false,
+ "name": "todo_pk",
+ "entityType": "pks",
+ "table": "todo"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_state_pk",
+ "table": "account_state",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "workspace_pk",
+ "table": "workspace",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "project_pk",
+ "table": "project",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "message_pk",
+ "table": "message",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "part_pk",
+ "table": "part",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["project_id"],
+ "nameExplicit": false,
+ "name": "permission_pk",
+ "table": "permission",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "session_pk",
+ "table": "session",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["session_id"],
+ "nameExplicit": false,
+ "name": "session_share_pk",
+ "table": "session_share",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["aggregate_id"],
+ "nameExplicit": false,
+ "name": "event_sequence_pk",
+ "table": "event_sequence",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "event_pk",
+ "table": "event",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ },
+ {
+ "value": "time_created",
+ "isExpression": false
+ },
+ {
+ "value": "id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "message_session_time_created_id_idx",
+ "entityType": "indexes",
+ "table": "message"
+ },
+ {
+ "columns": [
+ {
+ "value": "message_id",
+ "isExpression": false
+ },
+ {
+ "value": "id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "part_message_id_id_idx",
+ "entityType": "indexes",
+ "table": "part"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "part_session_idx",
+ "entityType": "indexes",
+ "table": "part"
+ },
+ {
+ "columns": [
+ {
+ "value": "project_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_project_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "workspace_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_workspace_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "parent_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_parent_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "todo_session_idx",
+ "entityType": "indexes",
+ "table": "todo"
+ }
+ ],
+ "renames": []
+}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 124674913c..04a9dc2f0a 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -91,15 +91,20 @@
"@aws-sdk/credential-providers": "3.1025.0",
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
+ "@gitlab/gitlab-ai-provider": "3.6.0",
+ "@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
+ "@kilocode/kilo-gateway": "workspace:*",
+ "@kilocode/kilo-telemetry": "workspace:*",
+ "@kilocode/plugin": "workspace:*",
+ "@kilocode/sdk": "workspace:*",
"@modelcontextprotocol/sdk": "1.29.0",
+ "@morphllm/morphsdk": "0.2.148",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
- "@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
- "@kilocode/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.90",
@@ -134,12 +139,15 @@
"minimatch": "10.2.5",
"open": "10.1.2",
"opencode-gitlab-auth": "2.0.0",
+ "opencode-poe-auth": "0.0.1",
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
- "opencode-poe-auth": "0.0.1",
"remeda": "catalog:",
+ "rotating-file-stream": "3.2.9",
"semver": "^7.6.3",
+ "simple-git": "3.35.2",
"solid-js": "catalog:",
+ "stream-chat": "9.38.0",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
"turndown": "7.2.0",
@@ -150,15 +158,7 @@
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
- "zod-to-json-schema": "3.24.5",
- "@gitlab/gitlab-ai-provider": "3.6.0",
- "@gitlab/opencode-gitlab-auth": "1.3.3",
- "@kilocode/kilo-gateway": "workspace:*",
- "@kilocode/kilo-telemetry": "workspace:*",
- "@morphllm/morphsdk": "0.2.148",
- "rotating-file-stream": "3.2.9",
- "simple-git": "3.35.2",
- "stream-chat": "9.38.0"
+ "zod-to-json-schema": "3.24.5"
},
"overrides": {
"drizzle-orm": "catalog:"
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index 4e45f5cdba..8cbf31ca6b 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -21,10 +21,14 @@ const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
await Bun.write(
- path.join(dir, "src/provider/models-snapshot.ts"),
- `// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData} as const\n`,
+ path.join(dir, "src/provider/models-snapshot.js"),
+ `// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
)
-console.log("Generated models-snapshot.ts")
+await Bun.write(
+ path.join(dir, "src/provider/models-snapshot.d.ts"),
+ `// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record\n`,
+)
+console.log("Generated models-snapshot.js")
// Load migrations from migration directories
const migrationDirs = (
@@ -59,6 +63,26 @@ console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
+const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
+
+
+const createEmbeddedWebUIBundle = async()=>{
+ console.log(`Building Web UI to embed in the binary`);
+ const appDir = path.join(import.meta.dirname, "../../app")
+ await $`bun run --cwd ${appDir} build`;
+ const allFiles = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: path.join(appDir, "dist")}));
+ const fileMap = `
+ // Import all files as file_$i with type: "file"
+ ${allFiles.map((filePath, i) => `import file_${i} from "${path.join(appDir, "dist", filePath).replaceAll("\\", "/")}" with { type: "file" };`).join("\n")}
+ // Export with original mappings
+ export default {
+ ${allFiles.map((filePath, i)=>`"${filePath.replaceAll("\\", "/")}": file_${i},`).join("\n")}
+ }
+ `.trim()
+ return fileMap;
+}
+
+const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle();
const allTargets: {
os: string
@@ -196,7 +220,10 @@ for (const item of targets) {
execArgv: [`--user-agent=kilo/${Script.version}`, "--use-system-ca", "--"], // kilocode_change
windows: {},
},
- entrypoints: ["./src/index.ts", parserWorker, workerPath],
+ files: {
+ ...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
+ },
+ entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
define: {
KILO_VERSION: `'${Script.version}'`,
KILO_MIGRATIONS: JSON.stringify(migrations),
diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md
index cf217871da..43b3194858 100644
--- a/packages/opencode/specs/effect-migration.md
+++ b/packages/opencode/specs/effect-migration.md
@@ -6,7 +6,7 @@ Practical reference for new and migrated Effect code in `packages/opencode`.
Use `InstanceState` (from `src/effect/instance-state.ts`) for services that need per-directory state, per-instance cleanup, or project-bound background work. InstanceState uses a `ScopedCache` keyed by directory, so each open project gets its own copy of the state that is automatically cleaned up on disposal.
-Use `makeRunPromise` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`.
+Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`.
- Global services (no per-directory state): Account, Auth, Installation, Truncate
- Instance-scoped (per-directory state via InstanceState): File, FileTime, FileWatcher, Format, Permission, Question, Skill, Snapshot, Vcs, ProviderAuth
@@ -46,7 +46,7 @@ export namespace Foo {
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
// Per-service runtime (inside the namespace)
- const runPromise = makeRunPromise(Service, defaultLayer)
+ const { runPromise } = makeRuntime(Service, defaultLayer)
// Async facade functions
export async function get(id: FooID) {
@@ -79,22 +79,24 @@ See `Auth.ZodInfo` for the canonical example.
The `InstanceState.make` init callback receives a `Scope`, so you can use `Effect.acquireRelease`, `Effect.addFinalizer`, and `Effect.forkScoped` inside it. Resources acquired this way are automatically cleaned up when the instance is disposed or invalidated by `ScopedCache`. This makes it the right place for:
-- **Subscriptions**: Use `Effect.acquireRelease` to subscribe and auto-unsubscribe:
+- **Subscriptions**: Yield `Bus.Service` at the layer level, then use `Stream` + `forkScoped` inside the init closure. The fiber is automatically interrupted when the instance scope closes:
```ts
+const bus = yield * Bus.Service
+
const cache =
yield *
InstanceState.make(
Effect.fn("Foo.state")(function* (ctx) {
// ... load state ...
- yield* Effect.acquireRelease(
- Effect.sync(() =>
- Bus.subscribeAll((event) => {
+ yield* bus.subscribeAll().pipe(
+ Stream.runForEach((event) =>
+ Effect.sync(() => {
/* handle */
}),
),
- (unsub) => Effect.sync(unsub),
+ Effect.forkScoped,
)
return {
@@ -104,6 +106,16 @@ const cache =
)
```
+- **Resource cleanup**: Use `Effect.acquireRelease` or `Effect.addFinalizer` for resources that need teardown (native watchers, process handles, etc.):
+
+```ts
+yield *
+ Effect.acquireRelease(
+ Effect.sync(() => nativeAddon.watch(dir)),
+ (watcher) => Effect.sync(() => watcher.close()),
+ )
+```
+
- **Background fibers**: Use `Effect.forkScoped` — the fiber is interrupted on disposal.
- **Side effects at init**: Config notification, event wiring, etc. all belong in the init closure. Callers just do `InstanceState.get(cache)` to trigger everything, and `ScopedCache` deduplicates automatically.
@@ -164,8 +176,8 @@ Still open and likely worth migrating:
- [x] `Plugin`
- [x] `ToolRegistry`
- [ ] `Pty`
-- [ ] `Worktree`
-- [ ] `Bus`
+- [x] `Worktree`
+- [x] `Bus`
- [x] `Command`
- [ ] `Config`
- [ ] `Session`
@@ -175,4 +187,4 @@ Still open and likely worth migrating:
- [ ] `Provider`
- [x] `Project`
- [ ] `LSP`
-- [ ] `MCP`
+- [x] `MCP`
diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts
index 0a8d3687a3..82b166ef2a 100644
--- a/packages/opencode/src/account/index.ts
+++ b/packages/opencode/src/account/index.ts
@@ -1,7 +1,7 @@
import { Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
-import { makeRunPromise } from "@/effect/run-service"
+import { makeRuntime } from "@/effect/run-service"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { AccountRepo, type AccountRow } from "./repo"
import {
@@ -379,7 +379,7 @@ export namespace Account {
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
- export const runPromise = makeRunPromise(Service, defaultLayer)
+ export const { runPromise } = makeRuntime(Service, defaultLayer)
export async function active(): Promise {
return Option.getOrUndefined(await runPromise((service) => service.active()))
diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts
index 96f980cdad..d02cf1b637 100644
--- a/packages/opencode/src/account/repo.ts
+++ b/packages/opencode/src/account/repo.ts
@@ -8,6 +8,7 @@ import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } f
export type AccountRow = (typeof AccountTable)["$inferSelect"]
type DbClient = Parameters[0] extends (db: infer T) => unknown ? T : never
+type DbTransactionCallback = Parameters>[0]
const ACCOUNT_STATE_ID = 1
@@ -42,13 +43,13 @@ export class AccountRepo extends ServiceMap.Service(f: (db: DbClient) => A) =>
+ const query = (f: DbTransactionCallback) =>
Effect.try({
try: () => Database.use(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
})
- const tx = (f: (db: DbClient) => A) =>
+ const tx = (f: DbTransactionCallback) =>
Effect.try({
try: () => Database.transaction(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 7950f091cb..300f0ab0d5 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -21,7 +21,7 @@ import { Plugin } from "@/plugin"
import { Skill } from "../skill"
import { Effect, ServiceMap, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
-import { makeRunPromise } from "@/effect/run-service"
+import { makeRuntime } from "@/effect/run-service"
import * as KiloAgent from "@/kilocode/agent" // kilocode_change
export namespace Agent {
@@ -157,7 +157,6 @@ export namespace Agent {
permission: Permission.merge(
defaults,
Permission.fromConfig({
- todoread: "deny",
todowrite: "deny",
}),
user,
@@ -409,7 +408,7 @@ export namespace Agent {
export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
- const runPromise = makeRunPromise(Service, defaultLayer)
+ const { runPromise } = makeRuntime(Service, defaultLayer)
export async function get(agent: string) {
return runPromise((svc) => svc.get(agent))
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index d446d5f52f..7ca6e1bf02 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/packages/opencode/src/auth/index.ts
@@ -1,6 +1,6 @@
import path from "path"
import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
-import { makeRunPromise } from "@/effect/run-service"
+import { makeRuntime } from "@/effect/run-service"
import { zod } from "@/util/effect-zod"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
@@ -96,7 +96,7 @@ export namespace Auth {
}),
)
- const runPromise = makeRunPromise(Service, layer)
+ const { runPromise } = makeRuntime(Service, layer)
export async function get(providerID: string) {
return runPromise((service) => service.get(providerID))
diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts
index 7fe13833c8..d97922290e 100644
--- a/packages/opencode/src/bus/bus-event.ts
+++ b/packages/opencode/src/bus/bus-event.ts
@@ -1,10 +1,7 @@
import z from "zod"
import type { ZodType } from "zod"
-import { Log } from "../util/log"
export namespace BusEvent {
- const log = Log.create({ service: "event" })
-
export type Definition = ReturnType
const registry = new Map()
diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts
index 625f296622..db6327c82e 100644
--- a/packages/opencode/src/bus/index.ts
+++ b/packages/opencode/src/bus/index.ts
@@ -1,12 +1,14 @@
import z from "zod"
+import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
+import { InstanceState } from "@/effect/instance-state"
+import { makeRuntime } from "@/effect/run-service"
export namespace Bus {
const log = Log.create({ service: "bus" })
- type Subscription = (event: any) => void
export const InstanceDisposed = BusEvent.define(
"server.instance.disposed",
@@ -15,91 +17,168 @@ export namespace Bus {
}),
)
- const state = Instance.state(
- () => {
- const subscriptions = new Map()
+ type Payload = {
+ type: D["type"]
+ properties: z.infer
+ }
- return {
- subscriptions,
+ type State = {
+ wildcard: PubSub.PubSub
+ typed: Map>
+ }
+
+ export interface Interface {
+ readonly publish: (
+ def: D,
+ properties: z.output,
+ ) => Effect.Effect
+ readonly subscribe: (def: D) => Stream.Stream>
+ readonly subscribeAll: () => Stream.Stream
+ readonly subscribeCallback: (
+ def: D,
+ callback: (event: Payload) => unknown,
+ ) => Effect.Effect<() => void>
+ readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
+ }
+
+ export class Service extends ServiceMap.Service()("@opencode/Bus") {}
+
+ export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const cache = yield* InstanceState.make(
+ Effect.fn("Bus.state")(function* (ctx) {
+ const wildcard = yield* PubSub.unbounded()
+ const typed = new Map>()
+
+ yield* Effect.addFinalizer(() =>
+ Effect.gen(function* () {
+ // Publish InstanceDisposed before shutting down so subscribers see it
+ yield* PubSub.publish(wildcard, {
+ type: InstanceDisposed.type,
+ properties: { directory: ctx.directory },
+ })
+ yield* PubSub.shutdown(wildcard)
+ for (const ps of typed.values()) {
+ yield* PubSub.shutdown(ps)
+ }
+ }),
+ )
+
+ return { wildcard, typed }
+ }),
+ )
+
+ function getOrCreate(state: State, def: D) {
+ return Effect.gen(function* () {
+ let ps = state.typed.get(def.type)
+ if (!ps) {
+ ps = yield* PubSub.unbounded()
+ state.typed.set(def.type, ps)
+ }
+ return ps as unknown as PubSub.PubSub>
+ })
}
- },
- async (entry) => {
- const wildcard = entry.subscriptions.get("*")
- if (!wildcard) return
- const event = {
- type: InstanceDisposed.type,
- properties: {
- directory: Instance.directory,
- },
+
+ function publish(def: D, properties: z.output) {
+ return Effect.gen(function* () {
+ const state = yield* InstanceState.get(cache)
+ const payload: Payload = { type: def.type, properties }
+ log.info("publishing", { type: def.type })
+
+ const ps = state.typed.get(def.type)
+ if (ps) yield* PubSub.publish(ps, payload)
+ yield* PubSub.publish(state.wildcard, payload)
+
+ GlobalBus.emit("event", {
+ directory: Instance.directory,
+ payload,
+ })
+ })
}
- for (const sub of [...wildcard]) {
- sub(event)
+
+ function subscribe(def: D): Stream.Stream> {
+ log.info("subscribing", { type: def.type })
+ return Stream.unwrap(
+ Effect.gen(function* () {
+ const state = yield* InstanceState.get(cache)
+ const ps = yield* getOrCreate(state, def)
+ return Stream.fromPubSub(ps)
+ }),
+ ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: def.type }))))
}
- },
+
+ function subscribeAll(): Stream.Stream {
+ log.info("subscribing", { type: "*" })
+ return Stream.unwrap(
+ Effect.gen(function* () {
+ const state = yield* InstanceState.get(cache)
+ return Stream.fromPubSub(state.wildcard)
+ }),
+ ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: "*" }))))
+ }
+
+ function on(pubsub: PubSub.PubSub, type: string, callback: (event: T) => unknown) {
+ return Effect.gen(function* () {
+ log.info("subscribing", { type })
+ const scope = yield* Scope.make()
+ const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub))
+
+ yield* Scope.provide(scope)(
+ Stream.fromSubscription(subscription).pipe(
+ Stream.runForEach((msg) =>
+ Effect.tryPromise({
+ try: () => Promise.resolve().then(() => callback(msg)),
+ catch: (cause) => {
+ log.error("subscriber failed", { type, cause })
+ },
+ }).pipe(Effect.ignore),
+ ),
+ Effect.forkScoped,
+ ),
+ )
+
+ return () => {
+ log.info("unsubscribing", { type })
+ Effect.runFork(Scope.close(scope, Exit.void))
+ }
+ })
+ }
+
+ const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* (
+ def: D,
+ callback: (event: Payload) => unknown,
+ ) {
+ const state = yield* InstanceState.get(cache)
+ const ps = yield* getOrCreate(state, def)
+ return yield* on(ps, def.type, callback)
+ })
+
+ const subscribeAllCallback = Effect.fn("Bus.subscribeAllCallback")(function* (callback: (event: any) => unknown) {
+ const state = yield* InstanceState.get(cache)
+ return yield* on(state.wildcard, "*", callback)
+ })
+
+ return Service.of({ publish, subscribe, subscribeAll, subscribeCallback, subscribeAllCallback })
+ }),
)
- export async function publish(
- def: Definition,
- properties: z.output,
+ const { runPromise, runSync } = makeRuntime(Service, layer)
+
+ // runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
+ // Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
+ export async function publish(def: D, properties: z.output) {
+ return runPromise((svc) => svc.publish(def, properties))
+ }
+
+ export function subscribe(
+ def: D,
+ callback: (event: { type: D["type"]; properties: z.infer }) => unknown,
) {
- const payload = {
- type: def.type,
- properties,
- }
- log.info("publishing", {
- type: def.type,
- })
- const pending = []
- for (const key of [def.type, "*"]) {
- const match = [...(state().subscriptions.get(key) ?? [])]
- for (const sub of match) {
- pending.push(sub(payload))
- }
- }
- GlobalBus.emit("event", {
- directory: Instance.directory,
- payload,
- })
- return Promise.all(pending)
+ return runSync((svc) => svc.subscribeCallback(def, callback))
}
- export function subscribe(
- def: Definition,
- callback: (event: { type: Definition["type"]; properties: z.infer }) => void,
- ) {
- return raw(def.type, callback)
- }
-
- export function once(
- def: Definition,
- callback: (event: {
- type: Definition["type"]
- properties: z.infer
- }) => "done" | undefined,
- ) {
- const unsub = subscribe(def, (event) => {
- if (callback(event)) unsub()
- })
- }
-
- export function subscribeAll(callback: (event: any) => void) {
- return raw("*", callback)
- }
-
- function raw(type: string, callback: (event: any) => void) {
- log.info("subscribing", { type })
- const subscriptions = state().subscriptions
- let match = subscriptions.get(type) ?? []
- match.push(callback)
- subscriptions.set(type, match)
-
- return () => {
- log.info("unsubscribing", { type })
- const match = subscriptions.get(type)
- if (!match) return
- const index = match.indexOf(callback)
- if (index === -1) return
- match.splice(index, 1)
- }
+ export function subscribeAll(callback: (event: any) => unknown) {
+ return runSync((svc) => svc.subscribeAllCallback(callback))
}
}
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index 29596e9a5b..22371e6acb 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -14,19 +14,7 @@ import type { Argv } from "yargs"
type AgentMode = "all" | "primary" | "subagent"
-const AVAILABLE_TOOLS = [
- "bash",
- "read",
- "write",
- "edit",
- "list",
- "glob",
- "grep",
- "webfetch",
- "task",
- "todowrite",
- "todoread",
-]
+const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task", "todowrite"]
const AgentCreateCommand = cmd({
command: "create",
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index 186f35705a..1b575bb93e 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -876,7 +876,6 @@ export const GithubRunCommand = cmd({
function subscribeSessionEvents() {
const TOOL: Record = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
- todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
@@ -897,7 +896,7 @@ export const GithubRunCommand = cmd({
}
let text = ""
- Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
+ Bus.subscribe(MessageV2.Event.PartUpdated, (evt) => {
if (evt.properties.part.sessionID !== session.id) return
//if (evt.properties.part.messageID === messageID) return
const part = evt.properties.part
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 20209db9c1..cad0ae39cf 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -202,7 +202,7 @@ export function tui(input: {
targetFps: 60,
gatherStats: false,
exitOnCtrlC: false,
- useKittyKeyboard: {},
+ useKittyKeyboard: { events: process.platform === "win32" },
autoFocus: false,
openConsoleOnError: false,
consoleOptions: {
@@ -797,7 +797,7 @@ function App(props: { onSnapshot?: () => Promise }) {
})
})
- sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
+ sdk.event.on("session.deleted", (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
route.navigate({ type: "home" })
toast.show({
@@ -807,7 +807,7 @@ function App(props: { onSnapshot?: () => Promise }) {
}
})
- sdk.event.on(SessionApi.Event.Error.type, (evt) => {
+ sdk.event.on("session.error", (evt) => {
const error = evt.properties.error
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
// kilocode_change start - Show warning toast for Kilo errors instead of generic error toast
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 78122d6d21..cb3b42b8e4 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -18,7 +18,7 @@ import { usePromptStash } from "./stash"
import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
-import { useRenderer } from "@opentui/solid"
+import { useKeyboard, useRenderer } from "@opentui/solid"
import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
@@ -357,6 +357,20 @@ export function Prompt(props: PromptProps) {
]
})
+ // Windows Terminal 1.25+ handles Ctrl+V on keydown when kitty events are
+ // enabled, but still reports the kitty key-release event. Probe on release.
+ if (process.platform === "win32") {
+ useKeyboard(
+ (evt) => {
+ if (!input.focused) return
+ if (evt.name === "v" && evt.ctrl && evt.eventType === "release") {
+ command.trigger("prompt.paste")
+ }
+ },
+ { release: true },
+ )
+ }
+
const ref: PromptRef = {
get focused() {
return input.focused
@@ -857,10 +871,9 @@ export function Prompt(props: PromptProps) {
e.preventDefault()
return
}
- // Handle clipboard paste (Ctrl+V) - check for images first on Windows
- // This is needed because Windows terminal doesn't properly send image data
- // through bracketed paste, so we need to intercept the keypress and
- // directly read from clipboard before the terminal handles it
+ // Check clipboard for images before terminal-handled paste runs.
+ // This helps terminals that forward Ctrl+V to the app; Windows
+ // Terminal 1.25+ usually handles Ctrl+V before this path.
if (keybind.match("input_paste", e)) {
const content = await Clipboard.read()
if (content?.mime.startsWith("image/")) {
@@ -958,6 +971,9 @@ export function Prompt(props: PromptProps) {
// Replace CRLF first, then any remaining CR
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const pastedContent = normalizedText.trim()
+
+ // Windows Terminal <1.25 can surface image-only clipboard as an
+ // empty bracketed paste. Windows Terminal 1.25+ does not.
if (!pastedContent) {
command.trigger("prompt.paste")
return
diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
index 85e13d3133..87c0a63abc 100644
--- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
@@ -28,6 +28,14 @@ export namespace Clipboard {
mime: string
}
+ // Checks clipboard for images first, then falls back to text.
+ //
+ // On Windows prompt/ can call this from multiple paste signals because
+ // terminals surface image paste differently:
+ // 1. A forwarded Ctrl+V keypress
+ // 2. An empty bracketed-paste hint for image-only clipboard in Windows
+ // Terminal <1.25
+ // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
export async function read(): Promise