mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f1d001cc5 | |||
| 65d0b3ed6d | |||
| 22a34d7958 | |||
| cb4401ec92 | |||
| febf467b03 | |||
| d55a2fd56c | |||
| 40f577e5e7 | |||
| 9e49870118 | |||
| fe38e3ab02 | |||
| 0170577743 | |||
| 7de6ea5922 | |||
| 2fe7d13e69 | |||
| 1bc3c98ae7 | |||
| 55787f2caa | |||
| 7df61a74a0 | |||
| 4f23110880 | |||
| 041353f4ff | |||
| c72f8b17c6 | |||
| eb304f4115 | |||
| 5565f14ef5 | |||
| 10a4455c6f | |||
| 5ded6d6ad7 | |||
| 849a38c30c | |||
| 68050ab802 | |||
| 91d01fd4cc | |||
| 9beb0f8512 | |||
| d4cb47eadc | |||
| 261ff416a9 | |||
| d0a70cb217 | |||
| 20fc56d020 |
@@ -19,5 +19,5 @@ For anything in the packages/app use the ignore: prefix.
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
do not do generic messages like "improvied agent experience" be very specific
|
||||
do not do generic messages like "improved agent experience" be very specific
|
||||
about what user facing changes were made
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
||||
|
||||
## Developing OpenCode
|
||||
|
||||
- Requirements: Bun 1.3+, Go 1.24.x.
|
||||
- Requirements: Bun 1.3+
|
||||
- Install dependencies and start the dev server from the repo root:
|
||||
|
||||
```bash
|
||||
@@ -36,11 +36,11 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
||||
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/tui`: The TUI code, written in Go (will be removed soon in favor of [opentui](https://github.com/sst/opentui))
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
> [!NOTE]
|
||||
> After touching `packages/opencode/src/server/server.ts`, the OpenCode team must regenerate the Stainless SDK before any client updates merge.
|
||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
|
||||
@@ -126,3 +126,4 @@
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"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.9",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -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.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -28,8 +28,6 @@ import { Storage } from "@/storage/storage"
|
||||
import { Command } from "@/command"
|
||||
import { Agent as Agents } from "@/agent/agent"
|
||||
import { Permission } from "@/permission"
|
||||
import { Session } from "@/session"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SessionCompaction } from "@/session/compaction"
|
||||
import type { Config } from "@/config/config"
|
||||
import { MCP } from "@/mcp"
|
||||
@@ -89,7 +87,11 @@ export namespace ACP {
|
||||
})
|
||||
if (!res) return
|
||||
if (res.outcome.outcome !== "selected") {
|
||||
Permission.respond({ sessionID: permission.sessionID, permissionID: permission.id, response: "reject" })
|
||||
Permission.respond({
|
||||
sessionID: permission.sessionID,
|
||||
permissionID: permission.id,
|
||||
response: "reject",
|
||||
})
|
||||
return
|
||||
}
|
||||
Permission.respond({
|
||||
@@ -111,9 +113,11 @@ export namespace ACP {
|
||||
const acpSession = this.sessionManager.get(part.sessionID)
|
||||
if (!acpSession) return
|
||||
|
||||
const message = await Storage.read<MessageV2.Info>(["message", part.sessionID, part.messageID]).catch(
|
||||
() => undefined,
|
||||
)
|
||||
const message = await Storage.read<MessageV2.Info>([
|
||||
"message",
|
||||
part.sessionID,
|
||||
part.messageID,
|
||||
]).catch(() => undefined)
|
||||
if (!message || message.role !== "assistant") return
|
||||
|
||||
if (part.type === "tool") {
|
||||
@@ -192,7 +196,9 @@ export namespace ACP {
|
||||
sessionUpdate: "plan",
|
||||
entries: parsedTodos.data.map((todo) => {
|
||||
const status: PlanEntry["status"] =
|
||||
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
||||
todo.status === "cancelled"
|
||||
? "completed"
|
||||
: (todo.status as PlanEntry["status"])
|
||||
return {
|
||||
priority: "medium",
|
||||
status,
|
||||
@@ -375,11 +381,6 @@ export namespace ACP {
|
||||
description: command.description ?? "",
|
||||
}))
|
||||
const names = new Set(availableCommands.map((c) => c.name))
|
||||
if (!names.has("init"))
|
||||
availableCommands.push({
|
||||
name: "init",
|
||||
description: "create/update a AGENTS.md",
|
||||
})
|
||||
if (!names.has("compact"))
|
||||
availableCommands.push({
|
||||
name: "compact",
|
||||
@@ -404,7 +405,8 @@ export namespace ACP {
|
||||
description: agent.description,
|
||||
}))
|
||||
|
||||
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
||||
const currentModeId =
|
||||
availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
||||
|
||||
const mcpServers: Record<string, Config.Mcp> = {}
|
||||
for (const server of params.mcpServers) {
|
||||
@@ -585,14 +587,6 @@ export namespace ACP {
|
||||
}
|
||||
|
||||
switch (cmd.name) {
|
||||
case "init":
|
||||
await Session.initialize({
|
||||
sessionID,
|
||||
messageID: Identifier.ascending("message"),
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
})
|
||||
break
|
||||
case "compact":
|
||||
await SessionCompaction.run({
|
||||
sessionID,
|
||||
@@ -665,7 +659,9 @@ export namespace ACP {
|
||||
|
||||
function parseUri(
|
||||
uri: string,
|
||||
): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
|
||||
):
|
||||
| { type: "file"; url: string; filename: string; mime: string }
|
||||
| { type: "text"; text: string } {
|
||||
try {
|
||||
if (uri.startsWith("file://")) {
|
||||
const path = uri.slice(7)
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Session } from "@tui/routes/session"
|
||||
import { PromptHistoryProvider } from "./component/prompt/history"
|
||||
import { DialogAlert } from "./ui/dialog-alert"
|
||||
import { ToastProvider, useToast } from "./ui/toast"
|
||||
import { ExitProvider } from "./context/exit"
|
||||
import { ExitProvider, useExit } from "./context/exit"
|
||||
import type { SessionRoute } from "./context/route"
|
||||
import { Session as SessionApi } from "@/session"
|
||||
import { TuiEvent } from "./event"
|
||||
@@ -34,6 +34,7 @@ export function tui(input: {
|
||||
sessionID?: string
|
||||
model?: string
|
||||
agent?: string
|
||||
prompt?: string
|
||||
onExit?: () => Promise<void>
|
||||
}) {
|
||||
// promise to prevent immediate exit
|
||||
@@ -65,7 +66,7 @@ export function tui(input: {
|
||||
<SDKProvider url={input.url}>
|
||||
<SyncProvider>
|
||||
<ThemeProvider>
|
||||
<LocalProvider initialModel={input.model} initialAgent={input.agent}>
|
||||
<LocalProvider initialModel={input.model} initialAgent={input.agent} initialPrompt={input.prompt}>
|
||||
<KeybindProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
@@ -109,6 +110,7 @@ function App() {
|
||||
const toast = useToast()
|
||||
const [sessionExists, setSessionExists] = createSignal(false)
|
||||
const { theme } = useTheme()
|
||||
const exit = useExit()
|
||||
|
||||
useKeyboard(async (evt) => {
|
||||
if (evt.meta && evt.name === "t") {
|
||||
@@ -172,6 +174,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",
|
||||
@@ -226,11 +246,17 @@ function App() {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Exit the app",
|
||||
value: "app.exit",
|
||||
onSelect: exit,
|
||||
category: "System",
|
||||
}
|
||||
])
|
||||
|
||||
createEffect(() => {
|
||||
const providerID = local.model.current().providerID
|
||||
if (providerID === "openrouter" && !kv.data.openrouter_warning) {
|
||||
if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
|
||||
untrack(() => {
|
||||
DialogAlert.show(
|
||||
dialog,
|
||||
@@ -278,8 +304,9 @@ function App() {
|
||||
/* @ts-expect-error */
|
||||
renderer.writeOut(finalOsc52)
|
||||
await Clipboard.copy(text)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
renderer.clearSelection()
|
||||
toast.show({ message: "Copied to clipboard", variant: "info" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,9 @@ function init() {
|
||||
return registrations().flatMap((x) => x())
|
||||
})
|
||||
|
||||
let keybinds = true
|
||||
useKeyboard((evt) => {
|
||||
if (!keybinds) return
|
||||
for (const option of options()) {
|
||||
if (option.keybind && keybind.match(option.keybind, evt)) {
|
||||
evt.preventDefault()
|
||||
@@ -39,14 +41,20 @@ function init() {
|
||||
})
|
||||
|
||||
const result = {
|
||||
trigger(name: string) {
|
||||
trigger(name: string, source?: "prompt") {
|
||||
for (const option of options()) {
|
||||
if (option.value === name) {
|
||||
option.onSelect?.(dialog)
|
||||
option.onSelect?.(dialog, source)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
keybinds(enabled: boolean) {
|
||||
keybinds = enabled
|
||||
},
|
||||
show() {
|
||||
dialog.replace(() => <DialogCommand options={options()} />)
|
||||
},
|
||||
register(cb: () => CommandOption[]) {
|
||||
const results = createMemo(cb)
|
||||
setRegistrations((arr) => [results, ...arr])
|
||||
@@ -75,7 +83,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 +98,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,
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function DialogSessionList() {
|
||||
}}
|
||||
keybind={[
|
||||
{
|
||||
keybind: Keybind.parse("delete")[0],
|
||||
keybind: Keybind.parse("ctrl+d")[0],
|
||||
title: "delete",
|
||||
onTrigger: async (option) => {
|
||||
if (toDelete() === option.value) {
|
||||
@@ -76,7 +76,7 @@ export function DialogSessionList() {
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("r")[0],
|
||||
keybind: Keybind.parse("ctrl+r")[0],
|
||||
title: "rename",
|
||||
onTrigger: async (option) => {
|
||||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { For, Match, Switch, Show } from "solid-js"
|
||||
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||
|
||||
export type DialogStatusProps = {}
|
||||
|
||||
@@ -9,13 +9,15 @@ export function DialogStatus() {
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<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)}>
|
||||
@@ -73,6 +75,28 @@ export function DialogStatus() {
|
||||
</For>
|
||||
</box>
|
||||
)}
|
||||
<Show when={enabledFormatters().length > 0} fallback={<text>No Formatters</text>}>
|
||||
<box>
|
||||
<text>{enabledFormatters().length} Formatters</text>
|
||||
<For each={enabledFormatters()}>
|
||||
{(item) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text
|
||||
flexShrink={0}
|
||||
style={{
|
||||
fg: theme.success,
|
||||
}}
|
||||
>
|
||||
•
|
||||
</text>
|
||||
<text wrapMode="word">
|
||||
<b>{item.name}</b>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export type AutocompleteRef = {
|
||||
|
||||
export type AutocompleteOption = {
|
||||
display: string
|
||||
aliases?: string[]
|
||||
disabled?: boolean
|
||||
description?: string
|
||||
onSelect?: () => void
|
||||
@@ -207,6 +208,7 @@ export function Autocomplete(props: {
|
||||
},
|
||||
{
|
||||
display: "/compact",
|
||||
aliases: ["/summarize"],
|
||||
description: "compact the session",
|
||||
onSelect: () => command.trigger("session.compact"),
|
||||
},
|
||||
@@ -232,6 +234,7 @@ export function Autocomplete(props: {
|
||||
results.push(
|
||||
{
|
||||
display: "/new",
|
||||
aliases: ["/clear"],
|
||||
description: "create a new session",
|
||||
onSelect: () => command.trigger("session.new"),
|
||||
},
|
||||
@@ -247,6 +250,7 @@ export function Autocomplete(props: {
|
||||
},
|
||||
{
|
||||
display: "/session",
|
||||
aliases: ["/resume", "/continue"],
|
||||
description: "list sessions",
|
||||
onSelect: () => command.trigger("session.list"),
|
||||
},
|
||||
@@ -263,13 +267,24 @@ export function Autocomplete(props: {
|
||||
{
|
||||
display: "/editor",
|
||||
description: "open editor",
|
||||
onSelect: () => command.trigger("prompt.editor"),
|
||||
onSelect: () => command.trigger("prompt.editor", "prompt"),
|
||||
},
|
||||
{
|
||||
display: "/help",
|
||||
description: "show help",
|
||||
onSelect: () => command.trigger("help.show"),
|
||||
},
|
||||
{
|
||||
display: "/commands",
|
||||
description: "show all commands",
|
||||
onSelect: () => command.show(),
|
||||
},
|
||||
{
|
||||
display: "/exit",
|
||||
aliases: ["/quit", "/q"],
|
||||
description: "exit the app",
|
||||
onSelect: () => command.trigger("app.exit"),
|
||||
},
|
||||
)
|
||||
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
||||
if (!max) return results
|
||||
@@ -288,7 +303,7 @@ export function Autocomplete(props: {
|
||||
const currentFilter = filter()
|
||||
if (!currentFilter) return mixed.slice(0, 10)
|
||||
const result = fuzzysort.go(currentFilter, mixed, {
|
||||
keys: ["display", "description"],
|
||||
keys: ["display", "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||
limit: 10,
|
||||
})
|
||||
return result.map((arr) => arr.obj)
|
||||
@@ -316,6 +331,7 @@ export function Autocomplete(props: {
|
||||
}
|
||||
|
||||
function show(mode: "@" | "/") {
|
||||
command.keybinds(false)
|
||||
setStore({
|
||||
visible: mode,
|
||||
index: props.input().visualCursor.offset,
|
||||
@@ -333,6 +349,7 @@ export function Autocomplete(props: {
|
||||
const cursor = props.input().logicalCursor
|
||||
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||
}
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
dim,
|
||||
fg,
|
||||
} from "@opentui/core"
|
||||
import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { SyntaxTheme, useTheme } from "@tui/context/theme"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
@@ -98,14 +98,9 @@ export function Prompt(props: PromptProps) {
|
||||
category: "Session",
|
||||
keybind: "editor_open",
|
||||
value: "prompt.editor",
|
||||
onSelect: async (dialog) => {
|
||||
onSelect: async (dialog, trigger) => {
|
||||
dialog.clear()
|
||||
const value = input.plainText
|
||||
input.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
const value = trigger === "prompt" ? "" : input.plainText
|
||||
const content = await Editor.open({ value, renderer })
|
||||
if (content) {
|
||||
input.setText(content, { history: false })
|
||||
@@ -194,6 +189,16 @@ export function Prompt(props: PromptProps) {
|
||||
input.focus()
|
||||
})
|
||||
|
||||
local.setInitialPrompt.listen((initialPrompt) => {
|
||||
batch(() => {
|
||||
setStore("prompt", {
|
||||
input: initialPrompt,
|
||||
parts: [],
|
||||
})
|
||||
input.insertText(initialPrompt)
|
||||
})
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
promptPartTypeId = input.extmarks.registerType("prompt-part")
|
||||
})
|
||||
@@ -310,9 +315,9 @@ export function Prompt(props: PromptProps) {
|
||||
const sessionID = props.sessionID
|
||||
? props.sessionID
|
||||
: await (async () => {
|
||||
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
|
||||
return sessionID
|
||||
})()
|
||||
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
|
||||
return sessionID
|
||||
})()
|
||||
const messageID = Identifier.ascending("message")
|
||||
let inputText = store.prompt.input
|
||||
|
||||
@@ -563,10 +568,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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Global } from "@/global"
|
||||
import { createSignal } from "solid-js"
|
||||
import { createSignal, type Setter } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import path from "path"
|
||||
@@ -8,10 +8,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
name: "KV",
|
||||
init: () => {
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [kvStore, setKvStore] = createStore({
|
||||
openrouter_warning: false,
|
||||
theme: "opencode",
|
||||
})
|
||||
const [kvStore, setKvStore] = createStore<Record<string, any>>()
|
||||
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
|
||||
|
||||
file
|
||||
@@ -24,22 +21,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
setReady(true)
|
||||
})
|
||||
|
||||
return {
|
||||
get data() {
|
||||
return kvStore
|
||||
},
|
||||
const result = {
|
||||
get ready() {
|
||||
return ready()
|
||||
},
|
||||
signal<T>(name: string, defaultValue: T) {
|
||||
if (!kvStore[name]) setKvStore(name, defaultValue)
|
||||
return [
|
||||
function () {
|
||||
return result.get(name)
|
||||
},
|
||||
function setter(next: Setter<T>) {
|
||||
result.set(name, next)
|
||||
},
|
||||
] as const
|
||||
},
|
||||
get(key: string, defaultValue?: any) {
|
||||
return kvStore[key] ?? defaultValue
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
setKvStore(key as any, value)
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
[key]: value,
|
||||
}),
|
||||
)
|
||||
setKvStore(key, value)
|
||||
Bun.write(file, JSON.stringify(kvStore, null, 2))
|
||||
},
|
||||
}
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,10 +8,11 @@ import { Global } from "@/global"
|
||||
import { iife } from "@/util/iife"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { createEventBus } from "@solid-primitives/event-bus"
|
||||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
init: (props: { initialModel?: string; initialAgent?: string }) => {
|
||||
init: (props: { initialModel?: string; initialAgent?: string; initialPrompt?: string }) => {
|
||||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -147,15 +148,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 +198,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,21 +223,36 @@ 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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const setInitialPrompt = createEventBus<string>()
|
||||
|
||||
onMount(() => {
|
||||
if (props.initialPrompt)
|
||||
setInitialPrompt.emit(props.initialPrompt)
|
||||
})
|
||||
|
||||
const result = {
|
||||
model,
|
||||
agent,
|
||||
get setInitialPrompt() {
|
||||
return setInitialPrompt
|
||||
},
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
Permission,
|
||||
LspStatus,
|
||||
McpStatus,
|
||||
FormatterStatus,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
@@ -42,6 +43,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
mcp: {
|
||||
[key: string]: McpStatus
|
||||
}
|
||||
formatter: FormatterStatus[]
|
||||
}>({
|
||||
config: {},
|
||||
ready: false,
|
||||
@@ -55,6 +57,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
part: {},
|
||||
lsp: [],
|
||||
mcp: {},
|
||||
formatter: [],
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
@@ -220,6 +223,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||
])
|
||||
|
||||
const result = {
|
||||
|
||||
@@ -72,11 +72,11 @@ type Theme = {
|
||||
|
||||
type HexColor = `#${string}`
|
||||
type RefName = string
|
||||
type ColorModeObj = {
|
||||
type Variant = {
|
||||
dark: HexColor | RefName
|
||||
light: HexColor | RefName
|
||||
}
|
||||
type ColorValue = HexColor | RefName | ColorModeObj
|
||||
type ColorValue = HexColor | RefName | Variant
|
||||
type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
@@ -629,7 +629,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
const sync = useSync()
|
||||
const kv = useKV()
|
||||
|
||||
const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.data.theme)
|
||||
const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.get("theme", "opencode"))
|
||||
|
||||
const values = createMemo(() => {
|
||||
return THEMES[theme()] ?? THEMES.opencode
|
||||
@@ -643,7 +643,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
},
|
||||
}),
|
||||
get selected() {
|
||||
return kv.data.theme
|
||||
return theme()
|
||||
},
|
||||
set(theme: string) {
|
||||
if (!THEMES[theme]) return
|
||||
|
||||
@@ -62,8 +62,10 @@ import { DialogTimeline } from "./dialog-timeline"
|
||||
import { Sidebar } from "./sidebar"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import parsers from "../../../../../../parsers-config.ts"
|
||||
import { Toast } from "../../ui/toast"
|
||||
import { Clipboard } from "../../util/clipboard"
|
||||
import { Toast, useToast } from "../../ui/toast"
|
||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -81,6 +83,7 @@ function use() {
|
||||
export function Session() {
|
||||
const route = useRouteData("session")
|
||||
const sync = useSync()
|
||||
const kv = useKV()
|
||||
const { theme } = useTheme()
|
||||
const session = createMemo(() => sync.session.get(route.sessionID)!)
|
||||
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
||||
@@ -91,7 +94,7 @@ export function Session() {
|
||||
})
|
||||
|
||||
const dimensions = useTerminalDimensions()
|
||||
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">("auto")
|
||||
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
|
||||
const [conceal, setConceal] = createSignal(true)
|
||||
|
||||
const wide = createMemo(() => dimensions().width > 120)
|
||||
@@ -100,6 +103,8 @@ export function Session() {
|
||||
|
||||
createEffect(() => sync.session.sync(route.sessionID))
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const sdk = useSDK()
|
||||
|
||||
let scroll: ScrollBoxRenderable
|
||||
@@ -196,12 +201,20 @@ export function Session() {
|
||||
keybind: "session_share",
|
||||
disabled: !!session()?.share?.url,
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
sdk.client.session.share({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
})
|
||||
onSelect: async (dialog) => {
|
||||
await sdk.client.session
|
||||
.share({
|
||||
path: {
|
||||
id: route.sessionID,
|
||||
},
|
||||
})
|
||||
.then((res) =>
|
||||
Clipboard.copy(res.data!.share!.url).catch(() =>
|
||||
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
|
||||
),
|
||||
)
|
||||
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
|
||||
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
@@ -241,7 +254,9 @@ export function Session() {
|
||||
prompt.set(
|
||||
parts.reduce(
|
||||
(agg, part) => {
|
||||
if (part.type === "text") agg.input += part.text
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
}
|
||||
if (part.type === "file") agg.parts.push(part)
|
||||
return agg
|
||||
},
|
||||
@@ -292,6 +307,8 @@ export function Session() {
|
||||
if (prev === "show") return "hide"
|
||||
return "show"
|
||||
})
|
||||
if (sidebar() === "show") kv.set("sidebar", "auto")
|
||||
if (sidebar() === "hide") kv.set("sidebar", "hide")
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
@@ -33,6 +32,11 @@ export const TuiThreadCommand = cmd({
|
||||
describe: "session id to continue",
|
||||
type: "string",
|
||||
})
|
||||
.option("prompt", {
|
||||
alias: ["p"],
|
||||
type: "string",
|
||||
describe: "prompt to use",
|
||||
})
|
||||
.option("agent", {
|
||||
type: "string",
|
||||
describe: "agent to use",
|
||||
@@ -78,9 +82,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) => {
|
||||
@@ -98,6 +100,7 @@ export const TuiThreadCommand = cmd({
|
||||
sessionID,
|
||||
model: args.model,
|
||||
agent: args.agent,
|
||||
prompt: args.prompt,
|
||||
onExit: async () => {
|
||||
await client.call("shutdown", undefined)
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface DialogSelectOption<T = any> {
|
||||
category?: string
|
||||
disabled?: boolean
|
||||
bg?: RGBA
|
||||
onSelect?: (ctx: DialogContext) => void
|
||||
onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
|
||||
}
|
||||
|
||||
export type DialogSelectRef<T> = {
|
||||
@@ -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") {
|
||||
@@ -242,7 +242,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1}>
|
||||
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1} gap={1}>
|
||||
<For each={props.keybind ?? []}>
|
||||
{(item) => (
|
||||
<text>
|
||||
|
||||
@@ -49,7 +49,7 @@ function init() {
|
||||
|
||||
let timeoutHandle: NodeJS.Timeout | null = null
|
||||
|
||||
return {
|
||||
const toast = {
|
||||
show(options: ToastOptions) {
|
||||
const parsedOptions = TuiEvent.ToastShow.properties.parse(options)
|
||||
const { duration, ...currentToast } = parsedOptions
|
||||
@@ -59,10 +59,22 @@ function init() {
|
||||
setStore("currentToast", null)
|
||||
}, duration).unref()
|
||||
},
|
||||
error: (err: any) => {
|
||||
if (err instanceof Error)
|
||||
return toast.show({
|
||||
variant: "error",
|
||||
message: err.message,
|
||||
})
|
||||
toast.show({
|
||||
variant: "error",
|
||||
message: "An unknown error has occurred",
|
||||
})
|
||||
},
|
||||
get currentToast(): ToastOptions | null {
|
||||
return store.currentToast
|
||||
},
|
||||
}
|
||||
return toast
|
||||
}
|
||||
|
||||
export type ToastContext = ReturnType<typeof init>
|
||||
|
||||
@@ -61,7 +61,7 @@ export namespace Clipboard {
|
||||
const getCopyMethod = lazy(() => {
|
||||
const os = platform()
|
||||
|
||||
if (os === "darwin") {
|
||||
if (os === "darwin" && Bun.which("oascript")) {
|
||||
console.log("clipboard: using osascript")
|
||||
return async (text: string) => {
|
||||
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||
@@ -70,13 +70,13 @@ export namespace Clipboard {
|
||||
}
|
||||
|
||||
if (os === "linux") {
|
||||
if (process.env["WAYLAND_DISPLAY"]) {
|
||||
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
||||
console.log("clipboard: using wl-copy")
|
||||
return async (text: string) => {
|
||||
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
}
|
||||
if (Bun.which("xclip")) {
|
||||
@@ -89,7 +89,7 @@ export namespace Clipboard {
|
||||
})
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
}
|
||||
if (Bun.which("xsel")) {
|
||||
@@ -102,7 +102,7 @@ export namespace Clipboard {
|
||||
})
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export namespace Editor {
|
||||
})
|
||||
await proc.exited
|
||||
const content = await Bun.file(filepath).text()
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
opts.renderer.resume()
|
||||
opts.renderer.requestRender()
|
||||
return content || undefined
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Instance } from "../project/instance"
|
||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||
import { Bus } from "../bus"
|
||||
import { Identifier } from "../id/id"
|
||||
|
||||
export namespace Command {
|
||||
export const Default = {
|
||||
INIT: "init",
|
||||
} as const
|
||||
|
||||
export const Event = {
|
||||
Executed: Bus.event(
|
||||
"command.executed",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
arguments: z.string(),
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
@@ -33,6 +52,14 @@ export namespace Command {
|
||||
}
|
||||
}
|
||||
|
||||
if (result[Default.INIT] === undefined) {
|
||||
result[Default.INIT] = {
|
||||
name: Default.INIT,
|
||||
description: "create/update AGENTS.md",
|
||||
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
+2
@@ -6,3 +6,5 @@ The file you create will be given to agentic coding agents (such as yourself) th
|
||||
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
|
||||
|
||||
If there's already an AGENTS.md, improve it if it's located in ${path}
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -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({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
|
||||
import * as Formatter from "./formatter"
|
||||
import { Config } from "../config/config"
|
||||
@@ -11,6 +12,17 @@ import { Instance } from "../project/instance"
|
||||
export namespace Format {
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
export const Status = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
extensions: z.string().array(),
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
.meta({
|
||||
ref: "FormatterStatus",
|
||||
})
|
||||
export type Status = z.infer<typeof Status>
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
const enabled: Record<string, boolean> = {}
|
||||
const cfg = await Config.get()
|
||||
@@ -62,6 +74,20 @@ export namespace Format {
|
||||
return result
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
const s = await state()
|
||||
const result: Status[] = []
|
||||
for (const formatter of Object.values(s.formatters)) {
|
||||
const enabled = await isEnabled(formatter)
|
||||
result.push({
|
||||
name: formatter.name,
|
||||
extensions: formatter.extensions,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function init() {
|
||||
log.info("init")
|
||||
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||
|
||||
@@ -5,6 +5,10 @@ import { LSP } from "../lsp"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { File } from "../file"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Project } from "./project"
|
||||
import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
|
||||
@@ -14,4 +18,10 @@ export async function InstanceBootstrap() {
|
||||
await LSP.init()
|
||||
FileWatcher.init()
|
||||
File.init()
|
||||
|
||||
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
await Project.setInitialized(Instance.project.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Ripgrep } from "../file/ripgrep"
|
||||
import { Config } from "../config/config"
|
||||
import { File } from "../file"
|
||||
import { LSP } from "../lsp"
|
||||
import { Format } from "../format"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
@@ -1336,6 +1337,26 @@ export namespace Server {
|
||||
return c.json(await LSP.status())
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/formatter",
|
||||
describeRoute({
|
||||
description: "Get formatter status",
|
||||
operationId: "formatter.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Formatter status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Format.Status.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Format.status())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/tui/append-prompt",
|
||||
describeRoute({
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Decimal } from "decimal.js"
|
||||
import z from "zod"
|
||||
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
|
||||
|
||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||
|
||||
import { Bus } from "../bus"
|
||||
import { Config } from "../config/config"
|
||||
import { Flag } from "../flag/flag"
|
||||
@@ -14,11 +12,11 @@ import { Share } from "../share/share"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Project } from "../project/project"
|
||||
import { Instance } from "../project/instance"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Command } from "../command"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -164,7 +162,12 @@ export namespace Session {
|
||||
})
|
||||
})
|
||||
|
||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
||||
export async function createNext(input: {
|
||||
id?: string
|
||||
title?: string
|
||||
parentID?: string
|
||||
directory: string
|
||||
}) {
|
||||
const result: Info = {
|
||||
id: Identifier.descending("session", input.id),
|
||||
version: Installation.VERSION,
|
||||
@@ -402,7 +405,9 @@ export namespace Session {
|
||||
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
|
||||
.add(
|
||||
new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000),
|
||||
)
|
||||
.toNumber(),
|
||||
tokens,
|
||||
}
|
||||
@@ -423,22 +428,13 @@ export namespace Session {
|
||||
messageID: Identifier.schema("message"),
|
||||
}),
|
||||
async (input) => {
|
||||
await SessionPrompt.prompt({
|
||||
await SessionPrompt.command({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model: {
|
||||
providerID: input.providerID,
|
||||
modelID: input.modelID,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
},
|
||||
],
|
||||
model: input.providerID + "/" + input.modelID,
|
||||
command: Command.Default.INIT,
|
||||
arguments: "",
|
||||
})
|
||||
await Project.setInitialized(Instance.project.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1593,6 +1593,7 @@ export namespace SessionPrompt {
|
||||
let index = 0
|
||||
template = template.replace(bashRegex, () => results[index++])
|
||||
}
|
||||
template = template.trim()
|
||||
|
||||
const parts = [
|
||||
{
|
||||
@@ -1657,6 +1658,8 @@ export namespace SessionPrompt {
|
||||
})()
|
||||
|
||||
const agent = await Agent.get(agentName)
|
||||
let result: MessageV2.WithParts
|
||||
|
||||
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
|
||||
using abort = lock(input.sessionID)
|
||||
|
||||
@@ -1732,7 +1735,7 @@ export namespace SessionPrompt {
|
||||
}
|
||||
await Session.updatePart(toolPart)
|
||||
|
||||
const result = await TaskTool.init().then((t) =>
|
||||
const taskResult = await TaskTool.init().then((t) =>
|
||||
t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: abort.signal,
|
||||
@@ -1760,22 +1763,31 @@ export namespace SessionPrompt {
|
||||
},
|
||||
input: toolPart.state.input,
|
||||
title: "",
|
||||
metadata: result.metadata,
|
||||
output: result.output,
|
||||
metadata: taskResult.metadata,
|
||||
output: taskResult.output,
|
||||
}
|
||||
await Session.updatePart(toolPart)
|
||||
}
|
||||
|
||||
return { info: assistantMsg, parts: [toolPart] }
|
||||
result = { info: assistantMsg, parts: [toolPart] }
|
||||
} else {
|
||||
result = await prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model,
|
||||
agent: agentName,
|
||||
parts,
|
||||
})
|
||||
}
|
||||
|
||||
return prompt({
|
||||
Bus.publish(Command.Event.Executed, {
|
||||
name: input.command,
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model,
|
||||
agent: agentName,
|
||||
parts,
|
||||
arguments: input.arguments,
|
||||
messageID: result.info.id,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function ensureTitle(input: {
|
||||
|
||||
@@ -6,10 +6,11 @@ import { generateText, type ModelMessage } from "ai"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Log } from "@/util/log"
|
||||
import path from "path"
|
||||
import { Instance } from "@/project/instance"
|
||||
|
||||
export namespace SessionSummary {
|
||||
const log = Log.create({ service: "session.summary" })
|
||||
@@ -33,10 +34,13 @@ export namespace SessionSummary {
|
||||
input.messages
|
||||
.flatMap((x) => x.parts)
|
||||
.filter((x) => x.type === "patch")
|
||||
.flatMap((x) => x.files),
|
||||
.flatMap((x) => x.files)
|
||||
.map((x) => path.relative(Instance.worktree, x)),
|
||||
)
|
||||
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
|
||||
x.filter((x) => files.has(x.file)),
|
||||
x.filter((x) => {
|
||||
return files.has(x.file)
|
||||
}),
|
||||
)
|
||||
await Session.update(input.sessionID, (draft) => {
|
||||
draft.summary = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -107,6 +107,8 @@ import type {
|
||||
McpStatusResponses,
|
||||
LspStatusData,
|
||||
LspStatusResponses,
|
||||
FormatterStatusData,
|
||||
FormatterStatusResponses,
|
||||
TuiAppendPromptData,
|
||||
TuiAppendPromptResponses,
|
||||
TuiAppendPromptErrors,
|
||||
@@ -773,6 +775,20 @@ class Lsp extends _HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
class Formatter extends _HeyApiClient {
|
||||
/**
|
||||
* Get formatter status
|
||||
*/
|
||||
public status<ThrowOnError extends boolean = false>(
|
||||
options?: Options<FormatterStatusData, ThrowOnError>,
|
||||
) {
|
||||
return (options?.client ?? this._client).get<FormatterStatusResponses, unknown, ThrowOnError>({
|
||||
url: "/formatter",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Control extends _HeyApiClient {
|
||||
/**
|
||||
* Get the next TUI request from the queue
|
||||
@@ -1023,6 +1039,7 @@ export class OpencodeClient extends _HeyApiClient {
|
||||
app = new App({ client: this._client })
|
||||
mcp = new Mcp({ client: this._client })
|
||||
lsp = new Lsp({ client: this._client })
|
||||
formatter = new Formatter({ client: this._client })
|
||||
tui = new Tui({ client: this._client })
|
||||
auth = new Auth({ client: this._client })
|
||||
event = new Event({ client: this._client })
|
||||
|
||||
@@ -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 = {
|
||||
@@ -1054,6 +1070,12 @@ export type LspStatus = {
|
||||
status: "connected" | "error"
|
||||
}
|
||||
|
||||
export type FormatterStatus = {
|
||||
name: string
|
||||
extensions: Array<string>
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
@@ -1232,6 +1254,16 @@ export type EventTodoUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventCommandExecuted = {
|
||||
type: "command.executed"
|
||||
properties: {
|
||||
name: string
|
||||
sessionID: string
|
||||
arguments: string
|
||||
messageID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionIdle = {
|
||||
type: "session.idle"
|
||||
properties: {
|
||||
@@ -1294,6 +1326,7 @@ export type Event =
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
| EventTodoUpdated
|
||||
| EventCommandExecuted
|
||||
| EventSessionIdle
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
@@ -2495,6 +2528,24 @@ export type LspStatusResponses = {
|
||||
|
||||
export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
|
||||
|
||||
export type FormatterStatusData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/formatter"
|
||||
}
|
||||
|
||||
export type FormatterStatusResponses = {
|
||||
/**
|
||||
* Formatter status
|
||||
*/
|
||||
200: Array<FormatterStatus>
|
||||
}
|
||||
|
||||
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
|
||||
|
||||
export type TuiAppendPromptData = {
|
||||
body?: {
|
||||
text: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -61,5 +61,3 @@ We removed some functionality that we weren't sure anyone actually used. If some
|
||||
- thinking_blocks
|
||||
- session_child_cycle
|
||||
- session_child_cycle_reverse
|
||||
- model_cycle_recent
|
||||
- model_cycle_recent_reverse
|
||||
|
||||
@@ -444,6 +444,45 @@ To use Google Vertex AI with OpenCode:
|
||||
|
||||
4. Run the `/models` command to select the one you want.
|
||||
|
||||
---
|
||||
|
||||
### Hugging Face
|
||||
|
||||
[Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers) provides access to open models supported by 17+ providers.
|
||||
|
||||
1. Head over to [Hugging Face settings](https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained) to create a token with permission to make calls to Inference Providers.
|
||||
|
||||
2. Run `opencode auth login` and select **Hugging Face**.
|
||||
|
||||
```bash
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◆ Select provider
|
||||
│ ● Hugging Face
|
||||
│ ...
|
||||
└
|
||||
```
|
||||
|
||||
3. Enter your Hugging Face token.
|
||||
|
||||
```bash
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◇ Select provider
|
||||
│ Hugging Face
|
||||
│
|
||||
◇ Enter your API key
|
||||
│ _
|
||||
└
|
||||
```
|
||||
|
||||
4. Run the `/models` command to select a model like _Kimi-K2-Instruct_ or _GLM-4.6_.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### LM Studio
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user