Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f51bd28
ci: increase ARM runner to 8 vCPUs for faster Tauri builds
thdxr Jan 30, 2026
4a56491
fix(provider): exclude chat models from textVerbosity setting (#11363)
R44VC0RP Jan 30, 2026
77fa8dd
refactor(app): refactored tests + added project tests (#11349)
neriousy Jan 30, 2026
2c36cbb
refactor(provider): remove google-vertex-anthropic special case in ge…
MichaelYochpaz Jan 30, 2026
e7ff714
fix: handle redirected_statement treesitter node in bash permissions …
pschiel Jan 30, 2026
9d3f320
test: add llm.test.ts (#11375)
rekram1-node Jan 30, 2026
e834a2e
docs: update agents options section to include color and top_p option…
IdrisGit Jan 30, 2026
1a6461e
fix: ensure ask question tool isn't included when using acp (#11379)
rekram1-node Jan 30, 2026
aef0e58
chore(deps): bump ai-sdk packages (#11383)
rekram1-node Jan 30, 2026
0c32afb
fix(provider): use snake_case for thinking param with OpenAI-compatib…
Chesars Jan 31, 2026
252b2c4
chore: generate
opencode-agent[bot] Jan 31, 2026
8512655
feat: make skills invokable as slash commands in the TUI
thdxr Jan 30, 2026
f1caf84
feat(build): respect OPENCODE_MODELS_URL env var (#11384)
bbartels Jan 31, 2026
3542f3e
Revert "feat: make skills invokable as slash commands in the TUI"
thdxr Jan 31, 2026
2f4374c
Merge remote dev and apply revert
thdxr Jan 31, 2026
d9f18e4
feat(opencode): add copilot specific provider to properly handle copi…
SteffenDE Jan 31, 2026
644f0d4
chore: generate
opencode-agent[bot] Jan 31, 2026
571f5b3
ci: schedule beta workflow hourly to automate sync runs
thdxr Jan 31, 2026
73c4d36
ci: allow manual beta workflow trigger so users can release on demand…
thdxr Jan 31, 2026
d713026
ci: remove workflow restrictions to allow all PR triggers for broader…
thdxr Jan 31, 2026
b6bbb95
ci: remove pull-request write permissions from beta workflow
thdxr Jan 31, 2026
95bf01a
fix: ensure the mistral ordering fixes also apply to devstral (#11412)
rekram1-node Jan 31, 2026
90f39bf
core: prevent parallel test runs from contaminating environment varia…
thdxr Jan 31, 2026
507f13a
ci: run tests automatically when code is pushed to dev branch
thdxr Jan 31, 2026
c0e71c4
fix: don't --follow by default for grep and other things using ripgre…
rekram1-node Jan 31, 2026
81ac41e
feat: make skills invokable as slash commands in the TUI (#11390)
thdxr Jan 31, 2026
46122d9
chore: generate
opencode-agent[bot] Jan 31, 2026
d005e70
core: ensure models configuration is not empty before loading
thdxr Jan 31, 2026
8e5db30
ci: copy models fixture for e2e test consistency
thdxr Jan 31, 2026
6ecd011
tui: allow specifying custom models file path via OPENCODE_MODELS_PATH
thdxr Jan 31, 2026
6b97232
sync
thdxr Jan 31, 2026
65c21f8
chore: generate
opencode-agent[bot] Jan 31, 2026
f834915
test: fix flaky test (#11427)
rekram1-node Jan 31, 2026
511c7ab
test(app): session actions (#11386)
neriousy Jan 31, 2026
a552652
Revert "feat: Transitions, spacing, scroll fade, prompt area update (…
adamdotdevin Jan 31, 2026
597ae57
release: v1.1.48
Jan 31, 2026
b0b442d
refactor: kilo compat for v1.1.48
catrielmuller Feb 4, 2026
1a0b007
feat: merge opencode v1.1.48
catrielmuller Feb 4, 2026
e49b1ec
refactor: remove merge report
catrielmuller Feb 4, 2026
1b9e8cf
refactor: use kilo gateway on go to session actions
catrielmuller Feb 4, 2026
e92365e
Merge branch 'dev' into catrielmuller/kilo-opencode-v1.1.48
catrielmuller Feb 4, 2026
8dce77d
refactor: fix e2e test
catrielmuller Feb 4, 2026
17e39c5
refactor: disable share e2e test
catrielmuller Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/beta.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: beta

on:
push:
branches: [dev]
pull_request:
types: [opened, synchronize, labeled, unlabeled]
workflow_dispatch:
schedule:
- cron: "0 * * * *"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Hourly schedule will still create workflow runs even though the job is disabled

on.schedule triggers a workflow run every hour (0 * * * *) regardless of jobs.sync.if: false. Even with the job skipped, this can clutter Actions history and may count toward workflow-run limits. Consider removing the schedule while the job is disabled, or making it less frequent until the sync is re-enabled.


jobs:
sync:
Expand All @@ -18,7 +17,6 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: test

on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
Expand Down Expand Up @@ -65,6 +68,7 @@ jobs:
env:
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
OPENCODE_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SESSION_INGEST: "true" # kilocode_change
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
Expand All @@ -87,6 +91,7 @@ jobs:
env:
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
OPENCODE_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SESSION_INGEST: "true" # kilocode_change
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
Expand Down Expand Up @@ -115,6 +120,7 @@ jobs:
CI: true
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
OPENCODE_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SHARE: "true" # kilocode_change
KILO_DISABLE_SESSION_INGEST: "true" # kilocode_change
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
Expand Down
54 changes: 29 additions & 25 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"ai": "5.0.119",
"ai": "5.0.124",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
Expand Down
271 changes: 271 additions & 0 deletions packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { expect, type Locator, type Page } from "@playwright/test"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { execSync } from "node:child_process"
import { modKey, serverUrl } from "./utils"
import {
sessionItemSelector,
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
titlebarRightSelector,
popoverBodySelector,
listItemSelector,
listItemKeySelector,
listItemKeyStartsWithSelector,
} from "./selectors"
import type { createSdk } from "./utils"

export async function defocus(page: Page) {
await page.mouse.click(5, 5)
}

export async function openPalette(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+P`)

const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
return dialog
}

export async function closeDialog(page: Page, dialog: Locator) {
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)

if (closed) return

await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)

if (closedSecond) return

await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
}

export async function isSidebarClosed(page: Page) {
const main = page.locator("main")
const classes = (await main.getAttribute("class")) ?? ""
return classes.includes("xl:border-l")
}

export async function toggleSidebar(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+B`)
}

export async function openSidebar(page: Page) {
if (!(await isSidebarClosed(page))) return
await toggleSidebar(page)
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
}

export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return
await toggleSidebar(page)
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
}

export async function openSettings(page: Page) {
await defocus(page)

const dialog = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)

const opened = await dialog
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)

if (opened) return dialog

await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
return dialog
}

export async function seedProjects(page: Page, input: { directory: string; extra?: string[] }) {
await page.addInitScript(
(args: { directory: string; serverUrl: string; extra: string[] }) => {
const key = "opencode.global.dat:server"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return undefined
try {
return JSON.parse(raw) as unknown
} catch {
return undefined
}
})()

const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
const list = Array.isArray(store.list) ? store.list : []
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
const nextProjects = { ...(projects as Record<string, unknown>) }

const add = (origin: string, directory: string) => {
const current = nextProjects[origin]
const items = Array.isArray(current) ? current : []
const existing = items.filter(
(p): p is { worktree: string; expanded?: boolean } =>
!!p &&
typeof p === "object" &&
"worktree" in p &&
typeof (p as { worktree?: unknown }).worktree === "string",
)

if (existing.some((p) => p.worktree === directory)) return
nextProjects[origin] = [{ worktree: directory, expanded: true }, ...existing]
}

const directories = [args.directory, ...args.extra]
for (const directory of directories) {
add("local", directory)
add(args.serverUrl, directory)
}

localStorage.setItem(
key,
JSON.stringify({
list,
projects: nextProjects,
lastProject,
}),
)
},
{ directory: input.directory, serverUrl, extra: input.extra ?? [] },
)
}

export async function createTestProject() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))

await fs.writeFile(path.join(root, "README.md"), "# e2e\n")

execSync("git init", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
cwd: root,
stdio: "ignore",
})

return root
}

export async function cleanupTestProject(directory: string) {
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
}

export function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
}

export async function hoverSessionItem(page: Page, sessionID: string) {
const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
await expect(sessionEl).toBeVisible()
await sessionEl.hover()
return sessionEl
}

export async function openSessionMoreMenu(page: Page, sessionID: string) {
const sessionEl = await hoverSessionItem(page, sessionID)

const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()

const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible()
return menu
}

export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
await expect(item).toBeVisible()
await item.click({ force: options?.force })
}

export async function confirmDialog(page: Page, buttonName: string | RegExp) {
const dialog = page.getByRole("dialog").first()
await expect(dialog).toBeVisible()

const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
await expect(button).toBeVisible()
await button.click()
}

export async function openSharePopover(page: Page) {
const rightSection = page.locator(titlebarRightSelector)
const shareButton = rightSection.getByRole("button", { name: "Share" }).first()
await expect(shareButton).toBeVisible()

const popoverBody = page
.locator(popoverBodySelector)
.filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
.first()

const opened = await popoverBody
.isVisible()
.then((x) => x)
.catch(() => false)

if (!opened) {
await shareButton.click()
await expect(popoverBody).toBeVisible()
}
return { rightSection, popoverBody }
}

export async function clickPopoverButton(page: Page, buttonName: string | RegExp) {
const button = page.getByRole("button").filter({ hasText: buttonName }).first()
await expect(button).toBeVisible()
await button.click()
}

export async function clickListItem(
container: Locator | Page,
filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
): Promise<Locator> {
let item: Locator

if (typeof filter === "string" || filter instanceof RegExp) {
item = container.locator(listItemSelector).filter({ hasText: filter }).first()
} else if (filter.keyStartsWith) {
item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
} else if (filter.key) {
item = container.locator(listItemKeySelector(filter.key)).first()
} else if (filter.text) {
item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
} else {
throw new Error("Invalid filter provided to clickListItem")
}

await expect(item).toBeVisible()
await item.click()
return item
}

export async function withSession<T>(
sdk: ReturnType<typeof createSdk>,
title: string,
callback: (session: { id: string; title: string }) => Promise<T>,
): Promise<T> {
const session = await sdk.session.create({ title }).then((r) => r.data)
if (!session?.id) throw new Error("Session create did not return an id")

try {
return await callback(session)
} finally {
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
}
}
3 changes: 2 additions & 1 deletion packages/app/e2e/app/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect } from "../fixtures"
import { dirPath, promptSelector } from "../utils"
import { promptSelector } from "../selectors"
import { dirPath } from "../utils"

test("project route redirects to /session", async ({ page, directory, slug }) => {
await page.goto(dirPath(directory))
Expand Down
8 changes: 2 additions & 6 deletions packages/app/e2e/app/palette.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
import { openPalette } from "../actions"

test("search palette opens and closes", async ({ page, gotoSession }) => {
await gotoSession()

await page.keyboard.press(`${modKey}+P`)

const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
const dialog = await openPalette(page)

await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
Expand Down
Loading