Compare commits

..

6 Commits

Author SHA1 Message Date
opencode 91d01fd4cc release: v1.0.5 2025-10-31 23:51:36 +00:00
Dax Raad 9beb0f8512 tui: improve keyboard navigation and MCP server status display 2025-10-31 19:47:08 -04:00
Dax Raad d4cb47eadc tui: add keyboard shortcuts to cycle through recently used models
Users can now press F2 to cycle forward and Shift+F2 to cycle backward through their recently used models, making it faster to switch between commonly used AI models without opening the model selection dialog.
2025-10-31 19:42:41 -04:00
Dax Raad 261ff416a9 sync 2025-10-31 23:05:11 +00:00
opencode d0a70cb217 release: v1.0.4 2025-10-31 23:05:10 +00:00
Aiden Cline 20fc56d020 Revert "opentui: fix: Make worker.ts path independent from cwd (#3600)"
This reverts commit d473d4ffc8.
2025-10-31 17:57:56 -05:00
25 changed files with 135 additions and 47 deletions
+11 -11
View File
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -111,7 +111,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.3",
"version": "1.0.5",
"bin": {
"opencode": "./bin/opencode",
},
@@ -243,7 +243,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -263,7 +263,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.3",
"version": "1.0.5",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -274,7 +274,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -287,7 +287,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -317,7 +317,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.3"
"version": "1.0.5"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.3",
"version": "1.0.5",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.3",
"version": "1.0.5",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.3",
"version": "1.0.5",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.3",
"version": "1.0.5",
"description": "",
"type": "module",
"scripts": {
+22
View File
@@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
const cycle = (direction: 1 | -1) => {
const recentList = recent()
const current = currentModel()
if (!current) return
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
if (index === -1) return
let next = index + direction
if (next < 0) next = recentList.length - 1
if (next >= recentList.length) next = 0
const val = recentList[next]
if (!val) return
model.set({
providerID: val.provider.id,
modelID: val.id,
})
}
return {
current: currentModel,
recent,
list,
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
setStore("model", agent.current().name, model ?? fallbackModel())
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.3",
"version": "1.0.5",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.3",
"version": "1.0.5",
"name": "opencode",
"type": "module",
"private": true,
+18
View File
@@ -172,6 +172,24 @@ function App() {
dialog.replace(() => <DialogModel />)
},
},
{
title: "Model cycle",
value: "model.cycle_recent",
keybind: "model_cycle_recent",
category: "Agent",
onSelect: () => {
local.model.cycle(1)
},
},
{
title: "Model cycle reverse",
value: "model.cycle_recent_reverse",
keybind: "model_cycle_recent_reverse",
category: "Agent",
onSelect: () => {
local.model.cycle(-1)
},
},
{
title: "Switch agent",
value: "agent.list",
@@ -47,6 +47,9 @@ function init() {
}
}
},
show() {
dialog.replace(() => <DialogCommand options={options()} />)
},
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr])
@@ -75,7 +78,7 @@ export function CommandProvider(props: ParentProps) {
const keybind = useKeybind()
useKeyboard((evt) => {
if (keybind.match("command_list", evt)) {
if (keybind.match("command_list", evt) && dialog.stack.length === 0) {
evt.preventDefault()
dialog.replace(() => <DialogCommand options={value.options} />)
return
@@ -90,7 +93,10 @@ function DialogCommand(props: { options: CommandOption[] }) {
return (
<DialogSelect
title="Commands"
options={props.options.map((x) => ({ ...x, footer: x.keybind ? keybind.print(x.keybind) : undefined }))}
options={props.options.map((x) => ({
...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined,
}))}
/>
)
}
@@ -15,7 +15,7 @@ export function DialogStatus() {
<text attributes={TextAttributes.BOLD}>Status</text>
<text fg={theme.textMuted}>esc</text>
</box>
<Show when={Object.keys(sync.data.mcp).length > 0}>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
<box>
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text>
<For each={Object.entries(sync.data.mcp)}>
@@ -270,6 +270,11 @@ export function Autocomplete(props: {
description: "show help",
onSelect: () => command.trigger("help.show"),
},
{
display: "/commands",
description: "show all commands",
onSelect: () => command.show(),
},
)
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
if (!max) return results
@@ -563,10 +563,11 @@ export function Prompt(props: PromptProps) {
if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) {
if (
(e.name === "up" && input.cursorOffset === 0) ||
(e.name === "down" && input.cursorOffset === input.plainText.length)
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
(keybind.match("history_next", e) &&
input.cursorOffset === input.plainText.length)
) {
const direction = e.name === "up" ? -1 : 1
const direction = keybind.match("history_previous", e) ? -1 : 1
const item = history.move(direction, input.plainText)
if (item) {
@@ -147,15 +147,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setModelStore("ready", true)
})
createEffect(() => {
Bun.write(
file,
JSON.stringify({
recent: modelStore.recent,
}),
)
})
const fallbackModel = createMemo(() => {
if (sync.data.config.model) {
const [providerID, modelID] = sync.data.config.model.split("/")
@@ -206,6 +197,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model: model.name ?? value.modelID,
}
}),
cycle(direction: 1 | -1) {
const current = currentModel()
if (!current) return
const recent = modelStore.recent
const index = recent.findIndex(
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
)
if (index === -1) return
let next = index + direction
if (next < 0) next = recent.length - 1
if (next >= recent.length) next = 0
const val = recent[next]
if (!val) return
setModelStore("model", agent.current().name, { ...val })
},
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
batch(() => {
if (!isModelValid(model)) {
@@ -216,12 +222,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
return
}
setModelStore("model", agent.current().name, model)
if (options?.recent) {
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
setModelStore("recent", uniq)
Bun.write(
file,
JSON.stringify({
recent: modelStore.recent,
}),
)
}
})
},
+1 -4
View File
@@ -6,7 +6,6 @@ import { upgrade } from "@/cli/upgrade"
import { Session } from "@/session"
import { bootstrap } from "@/cli/bootstrap"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
export const TuiThreadCommand = cmd({
@@ -78,9 +77,7 @@ export const TuiThreadCommand = cmd({
return undefined
})()
const worker = new Worker(
path.join(path.dirname(fileURLToPath(import.meta.url)), "worker.ts"),
)
const worker = new Worker("./src/cli/cmd/tui/worker.ts")
worker.onerror = console.error
const client = Rpc.client<typeof rpc>(worker)
process.on("uncaughtException", (e) => {
@@ -123,8 +123,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const keybind = useKeybind()
useKeyboard((evt) => {
if (evt.name === "up") move(-1)
if (evt.name === "down") move(1)
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
if (evt.name === "pageup") move(-10)
if (evt.name === "pagedown") move(10)
if (evt.name === "return") {
+13 -1
View File
@@ -385,7 +385,11 @@ export namespace Config {
.optional()
.default("ctrl+x")
.describe("Leader key for keybind combinations"),
app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
app_exit: z
.string()
.optional()
.default("ctrl+c,ctrl+d,<leader>q")
.describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
@@ -449,6 +453,12 @@ export namespace Config {
.default("<leader>h")
.describe("Toggle code block concealment in messages"),
model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
model_cycle_recent_reverse: z
.string()
.optional()
.default("shift+f2")
.describe("Previous recently used model"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
@@ -462,6 +472,8 @@ export namespace Config {
.optional()
.default("shift+enter,ctrl+j")
.describe("Insert newline in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Previous history item"),
})
.strict()
.meta({
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.3",
"version": "1.0.5",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.3",
"version": "1.0.5",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+16
View File
@@ -114,6 +114,14 @@ export type KeybindsConfig = {
* List available models
*/
model_list?: string
/**
* Next recently used model
*/
model_cycle_recent?: string
/**
* Previous recently used model
*/
model_cycle_recent_reverse?: string
/**
* List available commands
*/
@@ -150,6 +158,14 @@ export type KeybindsConfig = {
* Insert newline in input
*/
input_newline?: string
/**
* Previous history item
*/
history_previous?: string
/**
* Previous history item
*/
history_next?: string
}
export type AgentConfig = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.3",
"version": "1.0.5",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.3",
"version": "1.0.5",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.3",
"version": "1.0.5",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.3",
"version": "1.0.5",
"publisher": "sst-dev",
"repository": {
"type": "git",