Compare commits

..

16 Commits

Author SHA1 Message Date
Dax Raad 1c58023df9 improve anthropic oauth token caching and authentication handling
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 13:23:15 -04:00
Dax Raad 4e0aa58b7e ignore: fix 2025-06-17 13:04:26 -04:00
Dax Raad 23ee34b35f state 2025-06-17 12:29:28 -04:00
Dax Raad 674c9a5220 support disabling providers from automatically being added 2025-06-17 12:23:04 -04:00
Dax Raad 54c86ed43a docs: readme 2025-06-17 12:17:45 -04:00
Dax Raad 676d75ee75 docs: update README 2025-06-17 12:14:38 -04:00
Dax Raad 70dc0a12f2 docs: readme 2025-06-17 12:12:33 -04:00
Dax Raad d579c5e8aa support global config for providers 2025-06-17 12:10:44 -04:00
Dax Raad ee91f31313 fix issue with tool schemas and google 2025-06-17 11:27:07 -04:00
Dax Raad 57b3051024 fix agent getting caught in summary loop 2025-06-17 10:50:03 -04:00
Dax Raad ae5cf3cc23 ci: fix 2025-06-17 10:38:01 -04:00
Dax Raad 68e1b3c46c Fix TypeScript compilation errors and consolidate version handling
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 10:27:49 -04:00
adamdottv 2d68814abc feat: better collapsed tool call visuals 2025-06-17 08:35:18 -05:00
adamdottv a5da5127fa chore: consolidate chat page into tui.go 2025-06-17 07:09:04 -05:00
Dax Raad b5a4439704 Add autoshare configuration and improve run command UI
Enables automatic session sharing via global config or flag, enhances UI with logo display and provider/model info positioning.

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 01:45:32 -04:00
Dax Raad 9c5616521d do not autoupgrade snapshot builds 2025-06-17 01:18:32 -04:00
51 changed files with 663 additions and 636 deletions
+46 -4
View File
@@ -61,9 +61,53 @@ The Models.dev dataset is also used to detect common environment variables like
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
### Global Config
Some basic configuration is available in the global config file.
```toml
# ~/.config/opencode/config
theme = "opencode"
provider = "anthropic"
model = "claude-sonnet-4-20250514"
autoupdate = true
```
You can also extend the models.dev database with your own providers by mirroring the structure found [here](https://github.com/sst/models.dev/tree/dev/providers/anthropic)
Start with a `provider.toml` file in `~/.config/opencode/providers`
```toml
# ~/.config/opencode/providers/openrouter/provider.toml
[provider]
name = "OpenRouter"
env = ["OPENROUTER_API_KEY"]
npm = "@openrouter/ai-sdk-provider"
```
And models in `~/.config/opencode/providers/openrouter/models/[model-id]`
```toml
# ~/.config/opencode/providers/openrouter/models/anthropic/claude-3.5-sonnet.toml
name = "Claude 4 Sonnet"
attachment = true
reasoning = false
temperature = true
[cost]
input = 3.00
output = 15.00
inputCached = 3.75
outputCached = 0.30
[limit]
context = 200_000
output = 50_000
```
### Project Config
Project configuration is optional. You can place an `opencode.json` file in the root of your repo, and it'll be loaded.
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and is meant to be checked in and shared with your team.
```json title="opencode.json"
{
@@ -106,9 +150,7 @@ You can use opencode with any provider listed at [here](https://ai-sdk.dev/provi
"baseURL": "http://localhost:11434/v1"
},
"models": {
"llama2": {
"name": "llama2"
}
"llama2": {}
}
}
}
+6
View File
@@ -45,6 +45,7 @@
"zod-openapi": "4.2.4",
},
"devDependencies": {
"@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
@@ -86,6 +87,9 @@
"sharp",
"esbuild",
],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch",
},
"overrides": {
"zod": "3.24.2",
},
@@ -96,6 +100,8 @@
"zod": "3.24.2",
},
"packages": {
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
+4 -1
View File
@@ -37,5 +37,8 @@
"esbuild",
"protobufjs",
"sharp"
]
],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch"
}
}
+2 -1
View File
@@ -20,7 +20,8 @@
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5"
"zod-to-json-schema": "3.24.5",
"@ai-sdk/anthropic": "1.2.12"
},
"dependencies": {
"@clack/prompts": "0.11.0",
+5 -7
View File
@@ -18,6 +18,7 @@ export namespace App {
data: z.string(),
root: z.string(),
cwd: z.string(),
state: z.string(),
}),
time: z.object({
initialized: z.number().optional(),
@@ -32,7 +33,7 @@ export namespace App {
const APP_JSON = "app.json"
async function create(input: { cwd: string; version: string }) {
async function create(input: { cwd: string }) {
log.info("creating", {
cwd: input.cwd,
})
@@ -49,9 +50,7 @@ export namespace App {
const stateFile = Bun.file(path.join(data, APP_JSON))
const state = (await stateFile.json().catch(() => ({}))) as {
initialized: number
version: string
}
state.version = input.version
await stateFile.write(JSON.stringify(state))
const services = new Map<
@@ -70,13 +69,13 @@ export namespace App {
git: git !== undefined,
path: {
config: Global.Path.config,
state: Global.Path.state,
data,
root: git ?? input.cwd,
cwd: input.cwd,
},
}
const result = {
version: input.version,
services,
info,
}
@@ -108,7 +107,7 @@ export namespace App {
}
export async function provide<T>(
input: { cwd: string; version: string },
input: { cwd: string },
cb: (app: Info) => Promise<T>,
) {
const app = await create(input)
@@ -124,12 +123,11 @@ export namespace App {
}
export async function initialize() {
const { info, version } = ctx.use()
const { info } = ctx.use()
info.time.initialized = Date.now()
await Bun.write(
path.join(info.path.data, APP_JSON),
JSON.stringify({
version,
initialized: Date.now(),
}),
)
+3
View File
@@ -48,6 +48,7 @@ export namespace AuthAnthropic {
await Auth.set("anthropic", {
type: "oauth",
refresh: json.refresh_token as string,
access: json.access_token as string,
expires: Date.now() + json.expires_in * 1000,
})
}
@@ -55,6 +56,7 @@ export namespace AuthAnthropic {
export async function access() {
const info = await Auth.get("anthropic")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access
const response = await fetch(
"https://console.anthropic.com/v1/oauth/token",
{
@@ -74,6 +76,7 @@ export namespace AuthAnthropic {
await Auth.set("anthropic", {
type: "oauth",
refresh: json.refresh_token as string,
access: json.access_token as string,
expires: Date.now() + json.expires_in * 1000,
})
return json.access_token as string
+1
View File
@@ -7,6 +7,7 @@ export namespace Auth {
export const Oauth = z.object({
type: z.literal("oauth"),
refresh: z.string(),
access: z.string(),
expires: z.number(),
})
@@ -2,7 +2,6 @@ import { Server } from "../../server/server"
import fs from "fs/promises"
import path from "path"
import type { CommandModule } from "yargs"
import { Config } from "../../config/config"
export const GenerateCommand = {
command: "generate",
+27 -22
View File
@@ -6,14 +6,9 @@ import { Session } from "../../session"
import { Share } from "../../share/share"
import { Message } from "../../session/message"
import { UI } from "../ui"
import { VERSION } from "../version"
const COLOR = [
UI.Style.TEXT_SUCCESS_BOLD,
UI.Style.TEXT_INFO_BOLD,
UI.Style.TEXT_HIGHLIGHT_BOLD,
UI.Style.TEXT_WARNING_BOLD,
]
import { cmd } from "./cmd"
import { GlobalConfig } from "../../global/config"
import { Flag } from "../../flag/flag"
const TOOL: Record<string, [string, string]> = {
opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -27,7 +22,7 @@ const TOOL: Record<string, [string, string]> = {
opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
}
export const RunCommand = {
export const RunCommand = cmd({
command: "run [message..]",
describe: "Run OpenCode with a message",
builder: (yargs: Argv) => {
@@ -42,17 +37,16 @@ export const RunCommand = {
describe: "Session ID to continue",
type: "string",
})
.option("share", {
type: "boolean",
describe: "Share the session",
})
},
handler: async (args: {
message: string[]
session?: string
printLogs?: boolean
}) => {
handler: async (args) => {
const message = args.message.join(" ")
await App.provide(
{
cwd: process.cwd(),
version: VERSION,
},
async () => {
await Share.init()
@@ -60,14 +54,27 @@ export const RunCommand = {
? await Session.get(args.session)
: await Session.create()
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
UI.empty()
UI.println(UI.logo())
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
UI.empty()
const cfg = await GlobalConfig.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id)
UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://dev.opencode.ai/s/" +
session.id.slice(-8),
)
}
UI.empty()
const { providerID, modelID } = await Provider.defaultModel()
UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://dev.opencode.ai/s/" +
session.id.slice(-8),
UI.Style.TEXT_NORMAL_BOLD + "@ ",
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
)
UI.empty()
@@ -113,8 +120,6 @@ export const RunCommand = {
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
const { providerID, modelID } = await Provider.defaultModel()
await Session.chat({
sessionID: session.id,
providerID,
@@ -130,4 +135,4 @@ export const RunCommand = {
},
)
},
}
})
+7 -5
View File
@@ -1,6 +1,5 @@
import { App } from "../../app/app"
import { LSP } from "../../lsp"
import { VERSION } from "../version"
import { cmd } from "./cmd"
export const ScrapCommand = cmd({
@@ -8,9 +7,12 @@ export const ScrapCommand = cmd({
builder: (yargs) =>
yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) {
await App.provide({ cwd: process.cwd(), version: VERSION }, async (app) => {
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
})
await App.provide(
{ cwd: process.cwd() },
async () => {
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
},
)
},
})
+1 -66
View File
@@ -1,6 +1,5 @@
import type { Argv } from "yargs"
import { UI } from "../ui"
import { VERSION } from "../version"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
@@ -27,7 +26,7 @@ export const UpgradeCommand = {
return
}
const target = args.target ?? (await Installation.latest())
prompts.log.info(`From ${VERSION}${target}`)
prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner()
spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err)
@@ -41,69 +40,5 @@ export const UpgradeCommand = {
}
spinner.stop("Upgrade complete")
prompts.outro("Done")
return
/*
if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
return
}
const release = args.target
? await specific(args.target).catch(() => {})
: await latest().catch(() => {})
if (!release) {
prompts.log.error("Failed to fetch release information")
prompts.outro("Done")
return
}
const target = release.tag_name
if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
prompts.log.success(`Already up to date`)
prompts.outro("Done")
return
}
prompts.log.info(`From ${VERSION} → ${target}`)
const name = asset()
const found = release.assets.find((a) => a.name === name)
if (!found) {
prompts.log.error(`No binary found for platform: ${name}`)
prompts.outro("Done")
return
}
const spinner = prompts.spinner()
spinner.start("Downloading update...")
const downloadPath = await download(found.browser_download_url).catch(
() => {},
)
if (!downloadPath) {
spinner.stop("Download failed")
prompts.log.error("Download failed")
prompts.outro("Done")
return
}
spinner.stop("Download complete")
const renamed = await fs
.rename(downloadPath, process.execPath)
.catch(() => {})
if (renamed === undefined) {
prompts.log.error("Install failed")
await fs.unlink(downloadPath).catch(() => {})
prompts.outro("Done")
return
}
prompts.log.success(`Successfully upgraded to ${target}`)
prompts.outro("Done")
*/
},
}
-6
View File
@@ -1,6 +0,0 @@
declare global {
const OPENCODE_VERSION: string
}
export const VERSION =
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
-1
View File
@@ -1,4 +1,3 @@
import { App } from "../app/app"
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
-10
View File
@@ -1,10 +0,0 @@
export namespace File {
const glob = new Bun.Glob("**/*")
export async function search(path: string, query: string) {
for await (const entry of glob.scan({
cwd: path,
onlyFiles: true,
})) {
}
}
}
+3 -1
View File
@@ -5,9 +5,11 @@ import path from "path"
export namespace GlobalConfig {
export const Info = z.object({
autoupdate: z.boolean().optional(),
provider: z.string().optional(),
model: z.string().optional(),
autoupdate: z.boolean().optional(),
autoshare: z.boolean().optional(),
disabled_providers: z.array(z.string()).optional(),
})
export type Info = z.infer<typeof Info>
+12 -7
View File
@@ -1,5 +1,5 @@
import fs from "fs/promises"
import { xdgData, xdgCache, xdgConfig } from "xdg-basedir"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import path from "path"
const app = "opencode"
@@ -7,18 +7,23 @@ const app = "opencode"
const data = path.join(xdgData!, app)
const cache = path.join(xdgCache!, app)
const config = path.join(xdgConfig!, app)
await Promise.all([
fs.mkdir(data, { recursive: true }),
fs.mkdir(config, { recursive: true }),
fs.mkdir(cache, { recursive: true }),
])
const state = path.join(xdgState!, app)
export namespace Global {
export const Path = {
data,
bin: path.join(data, "bin"),
providers: path.join(config, "providers"),
cache,
config,
state,
} as const
}
await Promise.all([
fs.mkdir(Global.Path.data, { recursive: true }),
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.cache, { recursive: true }),
fs.mkdir(Global.Path.providers, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
])
+58 -54
View File
@@ -9,7 +9,6 @@ import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"
import { GenerateCommand } from "./cli/cmd/generate"
import { VERSION } from "./cli/version"
import { ScrapCommand } from "./cli/cmd/scrap"
import { Log } from "./util/log"
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
@@ -18,19 +17,11 @@ import { Provider } from "./provider/provider"
import { UI } from "./cli/ui"
import { GlobalConfig } from "./global/config"
import { Installation } from "./installation"
;(async () => {
if (Installation.VERSION === "dev") return
const config = await GlobalConfig.get()
if (config.autoupdate === false) return
const latest = await Installation.latest()
if (Installation.VERSION === latest) return
const method = await Installation.method()
await Installation.upgrade(method, latest).catch(() => {})
})()
import { Bus } from "./bus"
const cli = yargs(hideBin(process.argv))
.scriptName("opencode")
.version(VERSION)
.version(Installation.VERSION)
.option("print-logs", {
describe: "Print logs to stderr",
type: "boolean",
@@ -38,7 +29,7 @@ const cli = yargs(hideBin(process.argv))
.middleware(async () => {
await Log.init({ print: process.argv.includes("--print-logs") })
Log.Default.info("opencode", {
version: VERSION,
version: Installation.VERSION,
args: process.argv.slice(2),
})
})
@@ -55,52 +46,65 @@ const cli = yargs(hideBin(process.argv))
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
process.chdir(cwd)
const result = await App.provide(
{ cwd, version: VERSION },
async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
const result = await App.provide({ cwd }, async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
await Share.init()
const server = Server.listen()
let cmd = ["go", "run", "./main.go"]
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
const binary = path.join(Global.Path.cache, "tui", blob.name)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
},
})
await Share.init()
const server = Server.listen()
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await GlobalConfig.get()
if (config.autoupdate === false) return
const latest = await Installation.latest()
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => {})
})()
let cmd = ["go", "run", "./main.go"]
let cwd = new URL("../../tui/cmd/opencode", import.meta.url)
.pathname
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
const binary = path.join(Global.Path.cache, "tui", blob.name)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
},
})
await proc.exited
await server.stop()
await proc.exited
await server.stop()
return "done"
},
)
return "done"
})
if (result === "done") break
if (result === "needs_provider") {
UI.empty()
+19 -1
View File
@@ -2,10 +2,24 @@ import path from "path"
import { $ } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Bus } from "../bus"
declare global {
const OPENCODE_VERSION: string
}
export namespace Installation {
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
Updated: Bus.event(
"installation.updated",
z.object({
version: z.string(),
}),
),
}
export const Info = z
.object({
version: z.string(),
@@ -23,6 +37,10 @@ export namespace Installation {
}
}
export function isSnapshot() {
return VERSION.startsWith("0.0.0")
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
@@ -99,6 +117,6 @@ export namespace Installation {
export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json())
.then((data) => data.tag_name.slice(1))
.then((data) => data.tag_name.slice(1) as string)
}
}
+92 -8
View File
@@ -1,4 +1,5 @@
import z from "zod"
import path from "path"
import { App } from "../app/app"
import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda"
@@ -23,6 +24,8 @@ import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
import { GlobalConfig } from "../global/config"
import { Global } from "../global"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -47,9 +50,18 @@ export namespace Provider {
}
return {
apiKey: "",
headers: {
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
async fetch(input: any, init: any) {
const access = await AuthAnthropic.access()
const headers = {
...init.headers,
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
},
}
},
@@ -102,9 +114,35 @@ export namespace Provider {
provider.source = source
}
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
const configProviders = Object.entries(config.provider ?? {})
for await (const providerPath of new Bun.Glob("*/provider.toml").scan({
cwd: Global.Path.providers,
})) {
const [providerID] = providerPath.split("/")
const toml = await import(
path.join(Global.Path.providers, providerPath),
{
with: {
type: "toml",
},
}
).then((mod) => mod.default)
toml.models = {}
const modelsPath = path.join(Global.Path.providers, providerID, "models")
for await (const modelPath of new Bun.Glob("**/*.toml").scan({
cwd: modelsPath,
})) {
const modelID = modelPath.slice(0, -5)
toml.models[modelID] = await import(path.join(modelsPath, modelPath), {
with: {
type: "toml",
},
})
}
configProviders.unshift([providerID, toml])
}
for (const [providerID, provider] of configProviders) {
const existing = database[providerID]
const parsed: ModelsDev.Provider = {
id: providerID,
@@ -140,8 +178,12 @@ export namespace Provider {
database[providerID] = parsed
}
const disabled = await GlobalConfig.get().then(
(cfg) => new Set(cfg.disabled_providers ?? []),
)
// load env
for (const [providerID, provider] of Object.entries(database)) {
if (disabled.has(providerID)) continue
if (provider.env.some((item) => process.env[item])) {
mergeProvider(providerID, {}, "env")
}
@@ -149,6 +191,7 @@ export namespace Provider {
// load apikeys
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (disabled.has(providerID)) continue
if (provider.type === "api") {
mergeProvider(providerID, { apiKey: provider.key }, "api")
}
@@ -156,6 +199,7 @@ export namespace Provider {
// load custom
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result) mergeProvider(providerID, result, "custom")
}
@@ -257,7 +301,10 @@ export namespace Provider {
}
export async function defaultModel() {
const [provider] = await list().then((val) => Object.values(val))
const cfg = await GlobalConfig.get()
const provider = await list()
.then((val) => Object.values(val))
.then((x) => x.find((p) => !cfg.provider || cfg.provider === p.info.id))
if (!provider) throw new Error("no providers found")
const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found")
@@ -285,11 +332,16 @@ export namespace Provider {
TaskTool,
TodoReadTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
openai: TOOLS,
openai: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS,
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
@@ -301,6 +353,38 @@ export namespace Provider {
return TOOL_MAPPING[providerID] ?? TOOLS
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) =>
optionalToNullable(option),
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
-1
View File
@@ -12,7 +12,6 @@ import { App } from "../app/app"
import { Global } from "../global"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
import { Fzf } from "../external/fzf"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../external/ripgrep"
import { Installation } from "../installation"
+6 -3
View File
@@ -30,6 +30,7 @@ import type { Tool } from "../tool/tool"
import { SystemPrompt } from "./system"
import { Flag } from "../flag/flag"
import type { ModelsDev } from "../provider/models"
import { GlobalConfig } from "../global/config"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -95,7 +96,8 @@ export namespace Session {
log.info("created", result)
state().sessions.set(result.id, result)
await Storage.writeJSON("session/info/" + result.id, result)
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
const cfg = await GlobalConfig.get()
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
share(result.id).then((share) => {
update(result.id, (draft) => {
draft.share = share
@@ -208,8 +210,9 @@ export namespace Session {
previous.metadata.assistant.tokens.cache.write +
previous.metadata.assistant.tokens.output
if (
model.info.limit.context &&
tokens >
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
) {
await summarize({
sessionID: input.sessionID,
@@ -494,7 +497,7 @@ export namespace Session {
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
),
],
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
temperature: model.info.temperature ? 0 : undefined,
tools: {
...tools,
},
-2
View File
@@ -24,8 +24,6 @@ export namespace Storage {
}
})
const locks = new Map<string, Promise<void>>()
export async function readJSON<T>(key: string) {
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
}
+1 -1
View File
@@ -35,7 +35,7 @@ export const BashTool = Tool.define({
.min(0)
.max(MAX_TIMEOUT)
.describe("Optional timeout in milliseconds")
.nullable(),
.optional(),
description: z
.string()
.describe(
+1 -1
View File
@@ -21,7 +21,7 @@ export const EditTool = Tool.define({
),
replaceAll: z
.boolean()
.nullable()
.optional()
.describe("Replace all occurences of old_string (default false)"),
}),
async execute(params, ctx) {
+1 -1
View File
@@ -11,7 +11,7 @@ export const GlobTool = Tool.define({
pattern: z.string().describe("The glob pattern to match files against"),
path: z
.string()
.nullable()
.optional()
.describe(
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
),
+2 -2
View File
@@ -14,13 +14,13 @@ export const GrepTool = Tool.define({
.describe("The regex pattern to search for in file contents"),
path: z
.string()
.nullable()
.optional()
.describe(
"The directory to search in. Defaults to the current working directory.",
),
include: z
.string()
.nullable()
.optional()
.describe(
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
),
+2 -2
View File
@@ -29,11 +29,11 @@ export const ListTool = Tool.define({
.describe(
"The absolute path to the directory to list (must be absolute, not relative)",
)
.nullable(),
.optional(),
ignore: z
.array(z.string())
.describe("List of glob patterns to ignore")
.nullable(),
.optional(),
}),
async execute(params) {
const app = App.info()
-1
View File
@@ -4,7 +4,6 @@ import * as fs from "fs/promises"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import DESCRIPTION from "./patch.txt"
import { App } from "../app/app"
const PatchParams = z.object({
patchText: z
+2 -2
View File
@@ -19,11 +19,11 @@ export const ReadTool = Tool.define({
offset: z
.number()
.describe("The line number to start reading from (0-based)")
.nullable(),
.optional(),
limit: z
.number()
.describe("The number of lines to read (defaults to 2000)")
.nullable(),
.optional(),
}),
async execute(params, ctx) {
let filePath = params.filePath
+1 -1
View File
@@ -22,7 +22,7 @@ export const WebFetchTool = Tool.define({
.min(0)
.max(MAX_TIMEOUT / 1000)
.describe("Optional timeout in seconds (max 120)")
.nullable(),
.optional(),
}),
async execute(params, ctx) {
// Validate URL
+10 -9
View File
@@ -11,15 +11,16 @@ export abstract class NamedError extends Error {
name: Name,
data: Data,
) {
const schema = z
.object({
name: z.literal(name),
data,
})
.openapi({
ref: name,
})
const result = class extends NamedError {
public static readonly Schema = z
.object({
name: z.literal(name),
data: data,
})
.openapi({
ref: name,
})
public static readonly Schema = schema
public readonly name = name as Name
@@ -40,7 +41,7 @@ export abstract class NamedError extends Error {
}
schema() {
return result.Schema
return schema
}
toObject() {
+30 -15
View File
@@ -5,19 +5,33 @@ import { ListTool } from "../../src/tool/ls"
describe("tool.glob", () => {
test("truncate", async () => {
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute(
{ pattern: "./node_modules/**/*" },
{ sessionID: "test" },
{
pattern: "./node_modules/**/*",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
)
expect(result.metadata.truncated).toBe(true)
})
})
test("basic", async () => {
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute(
{ pattern: "*.json" },
{ sessionID: "test" },
{
pattern: "*.json",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
)
expect(result.metadata).toMatchObject({
truncated: false,
@@ -29,15 +43,16 @@ describe("tool.glob", () => {
describe("tool.ls", () => {
test("basic", async () => {
const result = await App.provide(
{ cwd: process.cwd(), version: "test" },
async () => {
return await ListTool.execute(
{ path: "./example" },
{ sessionID: "test" },
)
},
)
const result = await App.provide({ cwd: process.cwd() }, async () => {
return await ListTool.execute(
{ path: "./example", ignore: [".git"] },
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
)
})
expect(result.output).toMatchSnapshot()
})
})
+11 -4
View File
@@ -11,7 +11,6 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client"
@@ -32,6 +31,14 @@ type App struct {
Commands commands.Registry
}
type SessionSelectedMsg = *client.SessionInfo
type ModelSelectedMsg struct {
Provider client.ProviderInfo
Model client.ModelInfo
}
type SessionClearedMsg struct{}
type CompactSessionMsg struct{}
func New(
ctx context.Context,
version string,
@@ -118,7 +125,7 @@ func (a *App) InitializeProvider() tea.Cmd {
}
// TODO: handle no provider or model setup, yet
return state.ModelSelectedMsg{
return ModelSelectedMsg{
Provider: *currentProvider,
Model: *currentModel,
}
@@ -167,7 +174,7 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
}
a.Session = session
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
go func() {
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
@@ -236,7 +243,7 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
return nil
}
a.Session = session
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
}
// TODO: Handle attachments when API supports them
@@ -16,12 +16,17 @@ import (
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/image"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
type EditorComponent interface {
tea.Model
tea.ViewModel
Value() string
}
type editorComponent struct {
width int
height int
@@ -99,7 +104,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
case dialog.ThemeSelectedMsg:
m.textarea = createTextArea(&m.textarea)
m.spinner = createSpinner()
return m, m.spinner.Tick
@@ -434,11 +439,11 @@ func createSpinner() spinner.Model {
)
}
func (m *editorComponent) GetValue() string {
func (m *editorComponent) Value() string {
return m.textarea.Value()
}
func NewEditorComponent(app *app.App) layout.ModelWithView {
func NewEditorComponent(app *app.App) EditorComponent {
s := createSpinner()
ta := createTextArea(nil)
@@ -201,17 +201,20 @@ func renderContentBlock(content string, options ...renderingOption) string {
return content
}
func calculatePadding() int {
if layout.Current.Viewport.Width < 80 {
return 5
} else if layout.Current.Viewport.Width < 120 {
return 15
} else {
return 20
}
}
func renderText(message client.MessageInfo, text string, author string) string {
t := theme.CurrentTheme()
width := layout.Current.Container.Width
padding := 0
if layout.Current.Viewport.Width < 80 {
padding = 5
} else if layout.Current.Viewport.Width < 120 {
padding = 15
} else {
padding = 20
}
padding := calculatePadding()
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
@@ -222,9 +225,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
if message.Role == client.Assistant {
markdownWidth = width - padding - 4
}
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
content = strings.Join([]string{content, info}, "\n")
// content = lipgloss.JoinVertical(align, content, info)
switch message.Role {
case client.User:
@@ -246,6 +251,7 @@ func renderToolInvocation(
result *string,
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
showResult bool,
isLast bool,
) string {
ignoredTools := []string{"opencode_todoread"}
if slices.Contains(ignoredTools, toolCall.ToolName) {
@@ -333,7 +339,7 @@ func renderToolInvocation(
switch toolCall.ToolName {
case "opencode_read":
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed)
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
@@ -341,7 +347,7 @@ func renderToolInvocation(
}
case "opencode_edit":
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed)
if d, ok := metadata.Get("diff"); ok {
patch := d.(string)
var formattedDiff string
@@ -382,14 +388,14 @@ func renderToolInvocation(
}
case "opencode_write":
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed)
if content, ok := toolArgsMap["content"].(string); ok {
body = renderFile(filename, content)
}
}
case "opencode_bash":
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
title = fmt.Sprintf("SHELL %s %s", description, elapsed)
}
if stdout, ok := metadata.Get("stdout"); ok {
command := toolArgsMap["command"].(string)
@@ -400,7 +406,7 @@ func renderToolInvocation(
}
case "opencode_webfetch":
toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed)
if format, ok := toolArgsMap["format"].(string); ok {
body = *result
body = truncateHeight(body, 10)
@@ -410,7 +416,7 @@ func renderToolInvocation(
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "opencode_todowrite":
title = fmt.Sprintf("Planning %s", elapsed)
title = fmt.Sprintf("PLAN %s", elapsed)
if to, ok := metadata.Get("todos"); ok && finished {
todos := to.([]any)
@@ -431,12 +437,27 @@ func renderToolInvocation(
}
default:
toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
title = fmt.Sprintf("%s %s %s", toolName, toolArgs, elapsed)
body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
if !showResult {
padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
paddingBottom := 0
if isLast {
paddingBottom = 1
}
return renderContentBlock(style.Render(title),
WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()),
WithPaddingTop(0),
WithPaddingBottom(paddingBottom),
)
}
if body == "" && error == "" {
body = *result
body = truncateHeight(body, 10)
@@ -464,19 +485,17 @@ func renderToolName(name string) string {
// case agent.AgentToolName:
// return "Task"
case "opencode_ls":
return "List"
return "LIST"
case "opencode_webfetch":
return "Fetch"
case "opencode_todoread":
return "Planning"
return "FETCH"
case "opencode_todowrite":
return "Planning"
return "PLAN"
default:
normalizedName := name
if strings.HasPrefix(name, "opencode_") {
normalizedName = strings.TrimPrefix(name, "opencode_")
}
return cases.Title(language.Und).String(normalizedName)
return cases.Upper(language.Und).String(normalizedName)
}
}
@@ -1,6 +1,7 @@
package chat
import (
"slices"
"strings"
"time"
@@ -12,12 +13,16 @@ import (
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/pkg/client"
)
type MessagesComponent interface {
tea.Model
tea.ViewModel
}
type messagesComponent struct {
app *app.App
width, height int
@@ -69,7 +74,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
m.tail = true
return m, nil
case dialog.ThemeChangedMsg:
case dialog.ThemeSelectedMsg:
m.cache.Clear()
m.renderView()
return m, nil
@@ -77,12 +82,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showToolResults = !m.showToolResults
m.renderView()
return m, nil
case state.SessionSelectedMsg:
case app.SessionSelectedMsg:
m.cache.Clear()
cmd := m.Reload()
m.viewport.GotoBottom()
return m, cmd
case state.SessionClearedMsg:
case app.SessionClearedMsg:
m.cache.Clear()
cmd := m.Reload()
return m, cmd
@@ -101,7 +106,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.tail {
m.viewport.GotoBottom()
}
case state.StateUpdatedMsg:
case client.EventSessionUpdated:
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
case client.EventMessageUpdated:
m.renderView()
if m.tail {
m.viewport.GotoBottom()
@@ -135,6 +145,17 @@ func (m *messagesComponent) renderView() {
for _, message := range m.app.Messages {
var content string
var cached bool
lastToolIndex := 0
lastToolIndices := []int{}
for i, p := range message.Parts {
part, _ := p.ValueByDiscriminator()
switch part.(type) {
case client.MessagePartText:
lastToolIndices = append(lastToolIndices, lastToolIndex)
case client.MessagePartToolInvocation:
lastToolIndex = i
}
}
author := ""
switch message.Role {
@@ -144,7 +165,7 @@ func (m *messagesComponent) renderView() {
author = message.Metadata.Assistant.ModelID
}
for _, p := range message.Parts {
for i, p := range message.Parts {
part, err := p.ValueByDiscriminator()
if err != nil {
continue //TODO: handle error?
@@ -171,6 +192,7 @@ func (m *messagesComponent) renderView() {
previousBlockType = assistantTextBlock
}
case client.MessagePartToolInvocation:
isLastToolInvocation := slices.Contains(lastToolIndices, i)
toolInvocationPart := part.(client.MessagePartToolInvocation)
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{}
@@ -191,15 +213,27 @@ func (m *messagesComponent) renderView() {
)
content, cached = m.cache.Get(key)
if !cached {
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
content = renderToolInvocation(
toolCall,
result,
metadata,
m.showToolResults,
isLastToolInvocation,
)
m.cache.Set(key, content)
}
} else {
// if the tool call isn't finished, never cache
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
// if the tool call isn't finished, don't cache
content = renderToolInvocation(
toolCall,
result,
metadata,
m.showToolResults,
isLastToolInvocation,
)
}
if previousBlockType != toolInvocationBlock {
if previousBlockType != toolInvocationBlock && m.showToolResults {
blocks = append(blocks, "")
}
blocks = append(blocks, content)
@@ -389,7 +423,7 @@ func (m *messagesComponent) Reload() tea.Cmd {
}
}
func NewMessagesComponent(app *app.App) layout.ModelWithView {
func NewMessagesComponent(app *app.App) MessagesComponent {
customSpinner := spinner.Spinner{
Frames: []string{" ", "┃", "┃"},
FPS: time.Second / 3,
@@ -6,7 +6,6 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -76,7 +75,8 @@ type CompletionDialogCompleteItemMsg struct {
type CompletionDialogCloseMsg struct{}
type CompletionDialog interface {
layout.ModelWithView
tea.Model
tea.ViewModel
SetWidth(width int)
IsEmpty() bool
SetProvider(provider CompletionProvider)
@@ -13,7 +13,6 @@ import (
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -115,7 +114,7 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(
state.ModelSelectedMsg{
app.ModelSelectedMsg{
Provider: m.provider,
Model: models[m.selectedIdx],
}),
@@ -6,7 +6,6 @@ import (
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -30,7 +29,8 @@ type PermissionResponseMsg struct {
// PermissionDialogComponent interface for permission dialog component
type PermissionDialogComponent interface {
layout.ModelWithView
tea.Model
tea.ViewModel
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
}
@@ -8,7 +8,6 @@ import (
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -69,7 +68,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.selectedSessionID = selectedSession.Id
return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(state.SessionSelectedMsg(&selectedSession)),
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
)
}
}
@@ -10,8 +10,8 @@ import (
"github.com/sst/opencode/internal/util"
)
// ThemeChangedMsg is sent when the theme is changed
type ThemeChangedMsg struct {
// ThemeSelectedMsg is sent when the theme is changed
type ThemeSelectedMsg struct {
ThemeName string
}
@@ -75,7 +75,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeChangedMsg{ThemeName: selectedTheme}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
}
}
@@ -4,7 +4,6 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
)
type ListItem interface {
@@ -12,7 +11,8 @@ type ListItem interface {
}
type List[T ListItem] interface {
layout.ModelWithView
tea.Model
tea.ViewModel
SetMaxWidth(maxWidth int)
GetSelectedItem() (item T, idx int)
SetItems(items []T)
@@ -1,4 +1,4 @@
package core
package status
import (
"fmt"
@@ -7,13 +7,13 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type StatusComponent interface {
layout.ModelWithView
tea.Model
tea.ViewModel
}
type statusComponent struct {
+29 -27
View File
@@ -6,20 +6,14 @@ import (
"github.com/sst/opencode/internal/theme"
)
type ModelWithView interface {
type Container interface {
tea.Model
tea.ViewModel
}
type Container interface {
ModelWithView
Sizeable
Focus()
Blur()
Focusable
MaxWidth() int
Alignment() lipgloss.Position
GetPosition() (x, y int)
GetContent() ModelWithView
}
type container struct {
@@ -28,7 +22,7 @@ type container struct {
x int
y int
content ModelWithView
content tea.ViewModel
paddingTop int
paddingRight int
@@ -48,13 +42,19 @@ type container struct {
}
func (c *container) Init() tea.Cmd {
return c.content.Init()
if model, ok := c.content.(tea.Model); ok {
return model.Init()
}
return nil
}
func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
u, cmd := c.content.Update(msg)
c.content = u.(ModelWithView)
return c, cmd
if model, ok := c.content.(tea.Model); ok {
u, cmd := model.Update(msg)
c.content = u.(tea.ViewModel)
return c, cmd
}
return c, nil
}
func (c *container) View() string {
@@ -156,21 +156,28 @@ func (c *container) Alignment() lipgloss.Position {
}
// Focus sets the container as focused
func (c *container) Focus() {
func (c *container) Focus() tea.Cmd {
c.focused = true
// Pass focus to content if it supports it
if focusable, ok := c.content.(interface{ Focus() }); ok {
focusable.Focus()
if focusable, ok := c.content.(Focusable); ok {
return focusable.Focus()
}
return nil
}
// Blur removes focus from the container
func (c *container) Blur() {
func (c *container) Blur() tea.Cmd {
c.focused = false
// Remove focus from content if it supports it
if blurable, ok := c.content.(interface{ Blur() }); ok {
blurable.Blur()
if blurable, ok := c.content.(Focusable); ok {
return blurable.Blur()
}
return nil
}
func (c *container) IsFocused() bool {
if blurable, ok := c.content.(Focusable); ok {
return blurable.IsFocused()
}
return c.focused
}
// GetPosition returns the x, y coordinates of the container
@@ -178,14 +185,9 @@ func (c *container) GetPosition() (x, y int) {
return c.x, c.y
}
// GetContent returns the content of the container
func (c *container) GetContent() ModelWithView {
return c.content
}
type ContainerOption func(*container)
func NewContainer(content ModelWithView, options ...ContainerOption) Container {
func NewContainer(content tea.ViewModel, options ...ContainerOption) Container {
c := &container{
content: content,
borderStyle: lipgloss.NormalBorder(),
+2 -1
View File
@@ -25,7 +25,8 @@ func FlexPaneSizeFixed(size int) FlexPaneSize {
}
type FlexLayout interface {
ModelWithView
tea.Model
tea.ViewModel
Sizeable
SetPanes(panes []Container) tea.Cmd
SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
-187
View File
@@ -1,187 +0,0 @@
package page
import (
"context"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/chat"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/util"
)
var ChatPage PageID = "chat"
type chatPage struct {
app *app.App
editor layout.Container
messages layout.Container
layout layout.FlexLayout
completionDialog dialog.CompletionDialog
completionManager *completions.CompletionManager
showCompletionDialog bool
}
type ChatKeyMap struct {
Cancel key.Binding
ToggleTools key.Binding
ShowCompletionDialog key.Binding
}
var keyMap = ChatKeyMap{
Cancel: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
),
ToggleTools: key.NewBinding(
key.WithKeys("ctrl+h"),
key.WithHelp("ctrl+h", "toggle tools"),
),
ShowCompletionDialog: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "Complete"),
),
}
func (p *chatPage) Init() tea.Cmd {
cmds := []tea.Cmd{
p.layout.Init(),
}
cmds = append(cmds, p.completionDialog.Init())
return tea.Batch(cmds...)
}
func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
cmd := p.layout.SetSize(msg.Width, msg.Height)
cmds = append(cmds, cmd)
case chat.SendMsg:
p.showCompletionDialog = false
cmd := p.sendMessage(msg.Text, msg.Attachments)
if cmd != nil {
return p, cmd
}
case dialog.CompletionDialogCloseMsg:
p.showCompletionDialog = false
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
_, cmd := p.editor.Update(msg)
if cmd != nil {
return p, cmd
}
}
switch {
case key.Matches(msg, keyMap.ShowCompletionDialog):
p.showCompletionDialog = true
// Continue sending keys to layout->chat
case key.Matches(msg, keyMap.Cancel):
if p.app.Session.Id != "" {
// Cancel the current session's generation process
// This allows users to interrupt long-running operations
p.app.Cancel(context.Background(), p.app.Session.Id)
return p, nil
}
case key.Matches(msg, keyMap.ToggleTools):
return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
}
}
if p.showCompletionDialog {
// Get the current text from the editor to determine which provider to use
editorModel := p.editor.GetContent().(interface{ GetValue() string })
currentInput := editorModel.GetValue()
provider := p.completionManager.GetProvider(currentInput)
p.completionDialog.SetProvider(provider)
context, contextCmd := p.completionDialog.Update(msg)
p.completionDialog = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
// Doesn't forward event if enter key is pressed
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.String() == "enter" {
return p, tea.Batch(cmds...)
}
}
}
u, cmd := p.layout.Update(msg)
cmds = append(cmds, cmd)
p.layout = u.(layout.FlexLayout)
return p, tea.Batch(cmds...)
}
func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
var cmds []tea.Cmd
cmd := p.app.SendChatMessage(context.Background(), text, attachments)
cmds = append(cmds, cmd)
return tea.Batch(cmds...)
}
func (p *chatPage) SetSize(width, height int) tea.Cmd {
return p.layout.SetSize(width, height)
}
func (p *chatPage) GetSize() (int, int) {
return p.layout.GetSize()
}
func (p *chatPage) View() string {
layoutView := p.layout.View()
if p.showCompletionDialog {
editorWidth, _ := p.editor.GetSize()
editorX, editorY := p.editor.GetPosition()
p.completionDialog.SetWidth(editorWidth)
overlay := p.completionDialog.View()
layoutView = layout.PlaceOverlay(
editorX,
editorY-lipgloss.Height(overlay)+2,
overlay,
layoutView,
)
}
return layoutView
}
func NewChatPage(app *app.App) layout.ModelWithView {
completionManager := completions.NewCompletionManager(app)
initialProvider := completionManager.GetProvider("")
completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
messagesContainer := layout.NewContainer(
chat.NewMessagesComponent(app),
)
editorContainer := layout.NewContainer(
chat.NewEditorComponent(app),
layout.WithMaxWidth(layout.Current.Container.Width),
layout.WithAlignCenter(),
)
return &chatPage{
app: app,
editor: editorContainer,
messages: messagesContainer,
completionDialog: completionDialog,
completionManager: completionManager,
layout: layout.NewFlexLayout(
layout.WithPanes(messagesContainer, editorContainer),
layout.WithDirection(layout.FlexDirectionVertical),
layout.WithPaneSizes(
layout.FlexPaneSizeGrow,
layout.FlexPaneSizeFixed(6),
),
),
}
}
-8
View File
@@ -1,8 +0,0 @@
package page
type PageID string
// PageChangeMsg is used to change the current page
type PageChangeMsg struct {
ID PageID
}
-19
View File
@@ -1,19 +0,0 @@
package state
import (
"github.com/sst/opencode/pkg/client"
)
type SessionSelectedMsg = *client.SessionInfo
type ModelSelectedMsg struct {
Provider client.ProviderInfo
Model client.ModelInfo
}
type SessionClearedMsg struct{}
type CompactSessionMsg struct{}
// TODO: remove
type StateUpdatedMsg struct {
State map[string]any
}
-12
View File
@@ -1,12 +0,0 @@
package styles
const (
OpenCodeIcon string = "◍"
ErrorIcon string = "ⓔ"
WarningIcon string = "ⓦ"
InfoIcon string = "ⓘ"
HintIcon string = "ⓗ"
SpinnerIcon string = "⟳"
DocumentIcon string = "🖼"
)
+157 -89
View File
@@ -12,12 +12,12 @@ import (
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/core"
"github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/chat"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/components/status"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/page"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -25,14 +25,38 @@ import (
)
type appModel struct {
width, height int
currentPage page.PageID
previousPage page.PageID
pages map[page.PageID]layout.ModelWithView
loadedPages map[page.PageID]bool
status core.StatusComponent
app *app.App
modal layout.Modal
width, height int
status status.StatusComponent
app *app.App
modal layout.Modal
editorContainer layout.Container
editor chat.EditorComponent
messagesContainer layout.Container
layout layout.FlexLayout
completionDialog dialog.CompletionDialog
completionManager *completions.CompletionManager
showCompletionDialog bool
}
type ChatKeyMap struct {
Cancel key.Binding
ToggleTools key.Binding
ShowCompletionDialog key.Binding
}
var keyMap = ChatKeyMap{
Cancel: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
),
ToggleTools: key.NewBinding(
key.WithKeys("ctrl+h"),
key.WithHelp("ctrl+h", "toggle tools"),
),
ShowCompletionDialog: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "Complete"),
),
}
func (a appModel) Init() tea.Cmd {
@@ -43,12 +67,9 @@ func (a appModel) Init() tea.Cmd {
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
cmds = append(cmds, tea.RequestBackgroundColor)
cmd := a.pages[a.currentPage].Init()
a.loadedPages[a.currentPage] = true
cmds = append(cmds, cmd)
cmd = a.status.Init()
cmds = append(cmds, cmd)
cmds = append(cmds, a.layout.Init())
cmds = append(cmds, a.completionDialog.Init())
cmds = append(cmds, a.status.Init())
// Check if we should show the init dialog
cmds = append(cmds, func() tea.Msg {
@@ -59,23 +80,6 @@ func (a appModel) Init() tea.Cmd {
return tea.Batch(cmds...)
}
func (a appModel) updateAllPages(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
for id := range a.pages {
updated, cmd := a.pages[id].Update(msg)
a.pages[id] = updated.(layout.ModelWithView)
cmds = append(cmds, cmd)
}
s, cmd := a.status.Update(msg)
cmds = append(cmds, cmd)
a.status = s.(core.StatusComponent)
return a, tea.Batch(cmds...)
}
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
@@ -97,6 +101,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Quit
}
// TODO: do we need this?
// don't send commands to the modal
for _, cmdDef := range a.app.Commands {
if key.Matches(msg, cmdDef.KeyBinding) {
@@ -128,6 +133,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
switch msg := msg.(type) {
case chat.SendMsg:
a.showCompletionDialog = false
cmd := a.sendMessage(msg.Text, msg.Attachments)
if cmd != nil {
return a, cmd
}
case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false
case commands.ExecuteCommandMsg:
switch msg.Name {
case "quit":
@@ -135,7 +148,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "new":
a.app.Session = &client.SessionInfo{}
a.app.Messages = []client.MessageInfo{}
cmds = append(cmds, util.CmdHandler(state.SessionClearedMsg{}))
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
case "sessions":
sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog
@@ -173,28 +186,23 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
BackgroundIsDark: msg.IsDark(),
}
case cursor.BlinkMsg:
return a.updateAllPages(msg)
case spinner.TickMsg:
return a.updateAllPages(msg)
case client.EventSessionUpdated:
if msg.Properties.Info.Id == a.app.Session.Id {
a.app.Session = &msg.Properties.Info
return a.updateAllPages(state.StateUpdatedMsg{State: nil})
}
case client.EventMessageUpdated:
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
exists := false
for i, m := range a.app.Messages {
if m.Id == msg.Properties.Info.Id {
a.app.Messages[i] = msg.Properties.Info
return a.updateAllPages(state.StateUpdatedMsg{State: nil})
exists = true
}
}
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
return a.updateAllPages(state.StateUpdatedMsg{State: nil})
if !exists {
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
}
}
case tea.WindowSizeMsg:
@@ -212,18 +220,20 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
},
}
// Update status
s, cmd := a.status.Update(msg)
a.status = s.(core.StatusComponent)
a.status = s.(status.StatusComponent)
if cmd != nil {
cmds = append(cmds, cmd)
}
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(layout.ModelWithView)
// Update chat layout
cmd = a.layout.SetSize(msg.Width, msg.Height)
if cmd != nil {
cmds = append(cmds, cmd)
}
// Update modal if present
if a.modal != nil {
s, cmd := a.modal.Update(msg)
a.modal = s.(layout.Modal)
@@ -234,35 +244,32 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Batch(cmds...)
case page.PageChangeMsg:
return a, a.moveToPage(msg.ID)
case state.SessionSelectedMsg:
case app.SessionSelectedMsg:
a.app.Session = msg
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
return a.updateAllPages(msg)
case state.ModelSelectedMsg:
case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider
a.app.Model = &msg.Model
a.app.Config.Provider = msg.Provider.Id
a.app.Config.Model = msg.Model.Id
a.app.SaveConfig()
return a.updateAllPages(msg)
case dialog.ThemeChangedMsg:
case dialog.ThemeSelectedMsg:
a.app.Config.Theme = msg.ThemeName
a.app.SaveConfig()
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(layout.ModelWithView)
// Update layout
u, cmd := a.layout.Update(msg)
a.layout = u.(layout.FlexLayout)
if cmd != nil {
cmds = append(cmds, cmd)
}
// Update status
s, cmd := a.status.Update(msg)
cmds = append(cmds, cmd)
a.status = s.(core.StatusComponent)
a.status = s.(status.StatusComponent)
t := theme.CurrentTheme()
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
@@ -272,13 +279,28 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
// give the editor a chance to clear input
case "ctrl+c":
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(layout.ModelWithView)
_, cmd := a.editorContainer.Update(msg)
if cmd != nil {
return a, cmd
}
}
// Handle chat-specific keys
switch {
case key.Matches(msg, keyMap.ShowCompletionDialog):
a.showCompletionDialog = true
// Continue sending keys to layout->chat
case key.Matches(msg, keyMap.Cancel):
if a.app.Session.Id != "" {
// Cancel the current session's generation process
// This allows users to interrupt long-running operations
a.app.Cancel(context.Background(), a.app.Session.Id)
return a, nil
}
case key.Matches(msg, keyMap.ToggleTools):
return a, util.CmdHandler(chat.ToggleToolMessagesMsg{})
}
// First, check for modal triggers from the command registry
if a.modal == nil {
for _, cmdDef := range a.app.Commands {
@@ -291,40 +313,64 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if a.showCompletionDialog {
currentInput := a.editor.Value()
provider := a.completionManager.GetProvider(currentInput)
a.completionDialog.SetProvider(provider)
context, contextCmd := a.completionDialog.Update(msg)
a.completionDialog = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
// Doesn't forward event if enter key is pressed
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.String() == "enter" {
return a, tea.Batch(cmds...)
}
}
}
// update status bar
s, cmd := a.status.Update(msg)
cmds = append(cmds, cmd)
a.status = s.(core.StatusComponent)
a.status = s.(status.StatusComponent)
// update current page
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(layout.ModelWithView)
// update chat layout
u, cmd := a.layout.Update(msg)
a.layout = u.(layout.FlexLayout)
cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
}
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
func (a *appModel) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
var cmds []tea.Cmd
if _, ok := a.loadedPages[pageID]; !ok {
cmd := a.pages[pageID].Init()
cmds = append(cmds, cmd)
a.loadedPages[pageID] = true
}
a.previousPage = a.currentPage
a.currentPage = pageID
if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
cmd := sizable.SetSize(a.width, a.height)
cmds = append(cmds, cmd)
}
cmd := a.app.SendChatMessage(context.Background(), text, attachments)
cmds = append(cmds, cmd)
return tea.Batch(cmds...)
}
func (a appModel) View() string {
components := []string{
a.pages[a.currentPage].View(),
layoutView := a.layout.View()
if a.showCompletionDialog {
editorWidth, _ := a.editorContainer.GetSize()
editorX, editorY := a.editorContainer.GetPosition()
a.completionDialog.SetWidth(editorWidth)
overlay := a.completionDialog.View()
layoutView = layout.PlaceOverlay(
editorX,
editorY-lipgloss.Height(overlay)+2,
overlay,
layoutView,
)
}
components := []string{
layoutView,
a.status.View(),
}
components = append(components, a.status.View())
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
if a.modal != nil {
@@ -335,15 +381,37 @@ func (a appModel) View() string {
}
func NewModel(app *app.App) tea.Model {
startPage := page.ChatPage
completionManager := completions.NewCompletionManager(app)
initialProvider := completionManager.GetProvider("")
completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
messagesContainer := layout.NewContainer(
chat.NewMessagesComponent(app),
)
editor := chat.NewEditorComponent(app)
editorContainer := layout.NewContainer(
editor,
layout.WithMaxWidth(layout.Current.Container.Width),
layout.WithAlignCenter(),
)
model := &appModel{
currentPage: startPage,
loadedPages: make(map[page.PageID]bool),
status: core.NewStatusCmp(app),
app: app,
pages: map[page.PageID]layout.ModelWithView{
page.ChatPage: page.NewChatPage(app),
},
status: status.NewStatusCmp(app),
app: app,
editorContainer: editorContainer,
editor: editor,
messagesContainer: messagesContainer,
completionDialog: completionDialog,
completionManager: completionManager,
showCompletionDialog: false,
layout: layout.NewFlexLayout(
layout.WithPanes(messagesContainer, editorContainer),
layout.WithDirection(layout.FlexDirectionVertical),
layout.WithPaneSizes(
layout.FlexPaneSizeGrow,
layout.FlexPaneSizeFixed(6),
),
),
}
return model
+13
View File
@@ -0,0 +1,13 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 92a80377692488c4ba8801ce33e7736ad7055e43..add6281bbecaa1c03d3b48eb99aead4a7a7336b2 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1593,7 +1593,7 @@ function prepareCallSettings({
return {
maxTokens,
// TODO v5 remove default 0 for temperature
- temperature: temperature != null ? temperature : 0,
+ temperature: temperature,
topP,
topK,
presencePenalty,