mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c58023df9 | |||
| 4e0aa58b7e | |||
| 23ee34b35f | |||
| 674c9a5220 | |||
| 54c86ed43a | |||
| 676d75ee75 | |||
| 70dc0a12f2 | |||
| d579c5e8aa | |||
| ee91f31313 | |||
| 57b3051024 | |||
| ae5cf3cc23 | |||
| 68e1b3c46c | |||
| 2d68814abc | |||
| a5da5127fa | |||
| b5a4439704 | |||
| 9c5616521d |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -37,5 +37,8 @@
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp"
|
||||
]
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"ai@4.3.16": "patches/ai@4.3.16.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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")
|
||||
*/
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
declare global {
|
||||
const OPENCODE_VERSION: string
|
||||
}
|
||||
|
||||
export const VERSION =
|
||||
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
import { App } from "../app/app"
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
|
||||
@@ -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,
|
||||
})) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.`,
|
||||
),
|
||||
|
||||
@@ -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}")',
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package page
|
||||
|
||||
type PageID string
|
||||
|
||||
// PageChangeMsg is used to change the current page
|
||||
type PageChangeMsg struct {
|
||||
ID PageID
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package styles
|
||||
|
||||
const (
|
||||
OpenCodeIcon string = "◍"
|
||||
|
||||
ErrorIcon string = "ⓔ"
|
||||
WarningIcon string = "ⓦ"
|
||||
InfoIcon string = "ⓘ"
|
||||
HintIcon string = "ⓗ"
|
||||
SpinnerIcon string = "⟳"
|
||||
DocumentIcon string = "🖼"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user