Compare commits

..

9 Commits

Author SHA1 Message Date
Dax Raad fcaf0e6dbf opencode auth login: validation on provider id and better error messages 2025-06-16 15:09:49 -04:00
Dax Raad 19e259d90d docs: readme 2025-06-16 15:04:32 -04:00
Dax Raad 2c9fd1e776 BREAKING CHANGE: the config structure has changed, custom providers have an npm field now to specify which npm package to load. see examples in README.md 2025-06-16 15:02:25 -04:00
Dax Raad 63996c4189 limit to 4 system prompts cached 2025-06-16 14:51:59 -04:00
adamdottv c7bb7ce4de fix: include cached tokens in tui 2025-06-16 12:59:38 -05:00
adamdottv c8eb1b24c3 feat: believe it or not, even faster tui init 2025-06-16 12:34:34 -05:00
adamdottv b9f894f1e9 feat: even faster tui init 2025-06-16 12:24:18 -05:00
adamdottv 7c0d10a4ce feat: faster tui init 2025-06-16 11:54:55 -05:00
Dax Raad 06af406146 properly track cache token counts 2025-06-16 12:43:22 -04:00
22 changed files with 244 additions and 171 deletions
+5 -4
View File
@@ -78,14 +78,14 @@ Project configuration is optional. You can place an `opencode.json` file in the
#### Providers
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Use the npm package name as the key in your config.
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@ai-sdk/openai-compatible": {
"name": "ollama",
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
@@ -124,7 +124,8 @@ OpenRouter is not yet in the models.dev database, but you can configure it manua
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@openrouter/ai-sdk-provider": {
"openrouter": {
"npm": "@openrouter/ai-sdk-provider",
"name": "OpenRouter",
"options": {
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+2 -2
View File
@@ -1,8 +1,8 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@ai-sdk/openai-compatible": {
"name": "ollama",
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
+3
View File
@@ -21,6 +21,9 @@
"id": {
"type": "string"
},
"npm": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
-1
View File
@@ -1,5 +1,4 @@
import { generatePKCE } from "@openauthjs/openauth/pkce"
import fs from "fs/promises"
import { Auth } from "./index"
export namespace AuthAnthropic {
+1
View File
@@ -43,6 +43,7 @@ export namespace BunProc {
version: z.string(),
}),
)
export async function install(pkg: string, version = "latest") {
const mod = path.join(Global.Path.cache, "node_modules", pkg)
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
+13 -4
View File
@@ -5,7 +5,7 @@ import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sort, sortBy, values } from "remeda"
import { map, pipe, sortBy, values } from "remeda"
export const AuthCommand = cmd({
command: "auth",
@@ -16,7 +16,7 @@ export const AuthCommand = cmd({
.command(AuthLogoutCommand)
.command(AuthListCommand)
.demandCommand(),
async handler(args) {},
async handler() {},
})
export const AuthListCommand = cmd({
@@ -78,9 +78,16 @@ export const AuthLoginCommand = cmd({
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider - must match @ai-sdk/<provider>",
message: "Enter provider id",
validate: (x) =>
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
if (prompts.isCancel(provider)) throw new UI.CancelledError()
prompts.log.warn(
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
)
}
if (provider === "amazon-bedrock") {
@@ -115,7 +122,9 @@ export const AuthLoginCommand = cmd({
try {
await open(url)
} catch (e) {
prompts.log.error("Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:")
prompts.log.error(
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
)
}
prompts.log.info(url)
+2 -1
View File
@@ -46,7 +46,7 @@ const cli = yargs(hideBin(process.argv))
process.chdir(cwd)
const result = await App.provide(
{ cwd, version: VERSION },
async () => {
async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
@@ -78,6 +78,7 @@ const cli = yargs(hideBin(process.argv))
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
+1
View File
@@ -36,6 +36,7 @@ export namespace ModelsDev {
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(Model),
})
.openapi({
+9 -9
View File
@@ -108,6 +108,7 @@ export namespace Provider {
const existing = database[providerID]
const parsed: ModelsDev.Provider = {
id: providerID,
npm: provider.npm ?? existing?.npm,
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
models: existing?.models ?? {},
@@ -181,22 +182,22 @@ export namespace Provider {
return state().then((state) => state.providers)
}
async function getSDK(providerID: string) {
async function getSDK(provider: ModelsDev.Provider) {
return (async () => {
using _ = log.time("getSDK", {
providerID,
providerID: provider.id,
})
const s = await state()
const existing = s.sdk.get(providerID)
const existing = s.sdk.get(provider.id)
if (existing) return existing
const [pkg, version] = await ModelsDev.pkg(providerID)
const [pkg, version] = await ModelsDev.pkg(provider.npm ?? provider.id)
const mod = await import(await BunProc.install(pkg, version))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[providerID]?.options)
s.sdk.set(providerID, loaded)
const loaded = fn(s.providers[provider.id]?.options)
s.sdk.set(provider.id, loaded)
return loaded as SDK
})().catch((e) => {
throw new InitError({ providerID: providerID }, { cause: e })
throw new InitError({ providerID: provider.id }, { cause: e })
})
}
@@ -214,8 +215,7 @@ export namespace Provider {
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
const info = provider.info.models[modelID]
if (!info) throw new ModelNotFoundError({ providerID, modelID })
const sdk = await getSDK(providerID)
const sdk = await getSDK(provider.info)
try {
const language =
+22 -8
View File
@@ -13,7 +13,7 @@ import {
type LanguageModelUsage,
type CoreMessage,
type UIMessage,
type LanguageModelV1Middleware,
type ProviderMetadata,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
@@ -204,6 +204,8 @@ export namespace Session {
if (previous?.metadata.assistant) {
const tokens =
previous.metadata.assistant.tokens.input +
previous.metadata.assistant.tokens.cache.read +
previous.metadata.assistant.tokens.cache.write +
previous.metadata.assistant.tokens.output
if (
tokens >
@@ -262,7 +264,7 @@ export namespace Session {
draft.title = result.text
})
})
.catch((e) => {})
.catch(() => {})
}
const msg: Message.Info = {
role: "user",
@@ -299,6 +301,7 @@ export namespace Session {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
@@ -413,7 +416,7 @@ export namespace Session {
finishReason: step.finishReason,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(step.usage, model.info)
const usage = getUsage(model.info, step.usage, step.providerMetadata)
assistant.cost += usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
@@ -428,7 +431,7 @@ export namespace Session {
},
async onFinish(input) {
const assistant = next.metadata!.assistant!
const usage = getUsage(input.usage, model.info)
const usage = getUsage(model.info, input.usage, input.providerMetadata)
assistant.cost = usage.cost
await updateMessage(next)
},
@@ -472,11 +475,11 @@ export namespace Session {
maxSteps: 1000,
messages: [
...system.map(
(x): CoreMessage => ({
(x, index): CoreMessage => ({
role: "system",
content: x,
providerOptions: {
...(input.providerID === "anthropic"
...(input.providerID === "anthropic" && index < 4
? {
anthropic: {
cacheControl: { type: "ephemeral" },
@@ -675,6 +678,7 @@ export namespace Session {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
},
time: {
@@ -710,7 +714,7 @@ export namespace Session {
text: result.text,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(result.usage, model.info)
const usage = getUsage(model.info, result.usage, result.providerMetadata)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
@@ -731,11 +735,21 @@ export namespace Session {
}
}
function getUsage(usage: LanguageModelUsage, model: ModelsDev.Model) {
function getUsage(
model: ModelsDev.Model,
usage: LanguageModelUsage,
metadata?: ProviderMetadata,
) {
const tokens = {
input: usage.promptTokens ?? 0,
output: usage.completionTokens ?? 0,
reasoning: 0,
cache: {
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
0) as number,
read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ??
0) as number,
},
}
return {
cost: new Decimal(0)
+4
View File
@@ -174,6 +174,10 @@ export namespace Message {
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
})
.optional(),
+31 -23
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"log/slog"
"os"
"path/filepath"
@@ -16,7 +17,16 @@ import (
var Version = "dev"
func main() {
version := Version
if version != "dev" && !strings.HasPrefix(Version, "v") {
version = "v" + Version
}
url := os.Getenv("OPENCODE_SERVER")
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
var appInfo client.AppInfo
json.Unmarshal([]byte(appInfoStr), &appInfo)
httpClient, err := client.NewClientWithResponses(url)
if err != nil {
slog.Error("Failed to create client", "error", err)
@@ -27,11 +37,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
version := Version
if version != "dev" && !strings.HasPrefix(Version, "v") {
version = "v" + Version
}
app_, err := app.New(ctx, version, httpClient)
app_, err := app.New(ctx, version, appInfo, httpClient)
if err != nil {
panic(err)
}
@@ -61,27 +67,29 @@ func main() {
}
}()
paths, err := httpClient.PostPathGetWithResponse(context.Background())
if err != nil {
panic(err)
}
logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
err := os.MkdirAll(filepath.Dir(logfile), 0755)
go func() {
paths, err := httpClient.PostPathGetWithResponse(context.Background())
if err != nil {
slog.Error("Failed to create log directory", "error", err)
panic(err)
}
logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
err := os.MkdirAll(filepath.Dir(logfile), 0755)
if err != nil {
slog.Error("Failed to create log directory", "error", err)
os.Exit(1)
}
}
file, err := os.Create(logfile)
if err != nil {
slog.Error("Failed to create log file", "error", err)
os.Exit(1)
}
}
file, err := os.Create(logfile)
if err != nil {
slog.Error("Failed to create log file", "error", err)
os.Exit(1)
}
defer file.Close()
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)
defer file.Close()
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)
}()
// Run the TUI
result, err := program.Run()
+79 -71
View File
@@ -17,7 +17,11 @@ import (
"github.com/sst/opencode/pkg/client"
)
var RootPath string
type App struct {
Info client.AppInfo
Version string
ConfigPath string
Config *config.Config
Client *client.ClientWithResponses
@@ -28,95 +32,99 @@ type App struct {
Commands commands.Registry
}
type AppInfo struct {
client.AppInfo
Version string
}
func New(
ctx context.Context,
version string,
appInfo client.AppInfo,
httpClient *client.ClientWithResponses,
) (*App, error) {
RootPath = appInfo.Path.Root
var Info AppInfo
func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
appInfo := appInfoResponse.JSON200
Info = AppInfo{
AppInfo: *appInfo,
Version: version,
}
providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
if err != nil {
return nil, err
}
providers := []client.ProviderInfo{}
var defaultProvider *client.ProviderInfo
var defaultModel *client.ModelInfo
var anthropic *client.ProviderInfo
for _, provider := range providersResponse.JSON200.Providers {
if provider.Id == "anthropic" {
anthropic = &provider
}
}
// default to anthropic if available
if anthropic != nil {
defaultProvider = anthropic
defaultModel = getDefaultModel(providersResponse, *anthropic)
}
for _, provider := range providersResponse.JSON200.Providers {
if defaultProvider == nil || defaultModel == nil {
defaultProvider = &provider
defaultModel = getDefaultModel(providersResponse, provider)
}
providers = append(providers, provider)
}
if len(providers) == 0 {
return nil, fmt.Errorf("no providers found")
}
appConfigPath := filepath.Join(Info.Path.Config, "config")
appConfigPath := filepath.Join(appInfo.Path.Config, "config")
appConfig, err := config.LoadConfig(appConfigPath)
if err != nil {
slog.Info("No TUI config found, using default values", "error", err)
appConfig = config.NewConfig("opencode", defaultProvider.Id, defaultModel.Id)
appConfig = config.NewConfig()
config.SaveConfig(appConfigPath, appConfig)
}
var currentProvider *client.ProviderInfo
var currentModel *client.ModelInfo
for _, provider := range providers {
if provider.Id == appConfig.Provider {
currentProvider = &provider
for _, model := range provider.Models {
if model.Id == appConfig.Model {
currentModel = &model
}
}
}
}
if currentProvider == nil || currentModel == nil {
currentProvider = defaultProvider
currentModel = defaultModel
}
theme.SetTheme(appConfig.Theme)
app := &App{
Info: appInfo,
Version: version,
ConfigPath: appConfigPath,
Config: appConfig,
Client: httpClient,
Provider: currentProvider,
Model: currentModel,
Session: &client.SessionInfo{},
Messages: []client.MessageInfo{},
Commands: commands.NewCommandRegistry(),
}
theme.SetTheme(appConfig.Theme)
return app, nil
}
func (a *App) InitializeProvider() tea.Cmd {
return func() tea.Msg {
providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
if err != nil {
slog.Error("Failed to list providers", "error", err)
// TODO: notify user
return nil
}
providers := []client.ProviderInfo{}
var defaultProvider *client.ProviderInfo
var defaultModel *client.ModelInfo
var anthropic *client.ProviderInfo
for _, provider := range providersResponse.JSON200.Providers {
if provider.Id == "anthropic" {
anthropic = &provider
}
}
// default to anthropic if available
if anthropic != nil {
defaultProvider = anthropic
defaultModel = getDefaultModel(providersResponse, *anthropic)
}
for _, provider := range providersResponse.JSON200.Providers {
if defaultProvider == nil || defaultModel == nil {
defaultProvider = &provider
defaultModel = getDefaultModel(providersResponse, provider)
}
providers = append(providers, provider)
}
if len(providers) == 0 {
slog.Error("No providers configured")
return nil
}
var currentProvider *client.ProviderInfo
var currentModel *client.ModelInfo
for _, provider := range providers {
if provider.Id == a.Config.Provider {
currentProvider = &provider
for _, model := range provider.Models {
if model.Id == a.Config.Model {
currentModel = &model
}
}
}
}
if currentProvider == nil || currentModel == nil {
currentProvider = defaultProvider
currentModel = defaultModel
}
// TODO: handle no provider or model setup, yet
return state.ModelSelectedMsg{
Provider: *currentProvider,
Model: *currentModel,
}
}
}
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
if match, ok := response.JSON200.Default[provider.Id]; ok {
model := provider.Models[match]
@@ -23,7 +23,7 @@ import (
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
r := styles.GetMarkdownRenderer(width, backgroundColor)
content = strings.ReplaceAll(content, app.Info.Path.Root+"/", "")
content = strings.ReplaceAll(content, app.RootPath+"/", "")
rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n")
@@ -584,7 +584,7 @@ func truncateHeight(content string, height int) string {
}
func relative(path string) string {
return strings.TrimPrefix(path, app.Info.Path.Root+"/")
return strings.TrimPrefix(path, app.RootPath+"/")
}
func extension(path string) string {
@@ -139,7 +139,7 @@ func (m *messagesComponent) renderView() {
author := ""
switch message.Role {
case client.User:
author = app.Info.User
author = m.app.Info.User
case client.Assistant:
author = message.Metadata.Assistant.ModelID
}
@@ -328,7 +328,7 @@ func (m *messagesComponent) home() string {
logoAndVersion := lipgloss.JoinVertical(
lipgloss.Right,
logo,
muted(app.Info.Version),
muted(m.app.Version),
)
lines := []string{}
@@ -396,8 +396,8 @@ func NewMessagesComponent(app *app.App) layout.ModelWithView {
}
s := spinner.New(spinner.WithSpinner(customSpinner))
vp := viewport.New() //(0, 0)
attachments := viewport.New() //(0, 0)
vp := viewport.New()
attachments := viewport.New()
vp.KeyMap.PageUp = messageKeys.PageUp
vp.KeyMap.PageDown = messageKeys.PageDown
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
@@ -34,14 +34,14 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func logo() string {
func (m statusComponent) logo() string {
t := theme.CurrentTheme()
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
open := base("open")
code := emphasis("code ")
version := base(app.Info.Version)
version := base(m.app.Version)
return styles.Padded().
Background(t.BackgroundElement()).
Render(open + code + version)
@@ -84,12 +84,12 @@ func (m statusComponent) View() string {
Render("")
}
logo := logo()
logo := m.logo()
cwd := styles.Padded().
Foreground(t.TextMuted()).
Background(t.BackgroundSubtle()).
Render(app.Info.Path.Cwd)
Render(m.app.Info.Path.Cwd)
sessionInfo := ""
if m.app.Session.Id != "" {
@@ -102,7 +102,11 @@ func (m statusComponent) View() string {
cost += message.Metadata.Assistant.Cost
usage := message.Metadata.Assistant.Tokens
if usage.Output > 0 {
tokens = (usage.Input + usage.Output + usage.Reasoning)
tokens = (usage.Input +
usage.Cache.Write +
usage.Cache.Read +
usage.Output +
usage.Reasoning)
}
}
}
@@ -141,6 +141,8 @@ func (c *completionDialogComponent) close() tea.Cmd {
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case []CompletionItemI:
c.list.SetItems(msg)
case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() {
if !key.Matches(msg, completionDialogKeys.Complete) {
@@ -155,18 +157,20 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if query != c.query {
items, err := c.completionProvider.GetChildEntries(query)
if err != nil {
// status.Error(err.Error())
}
c.list.SetItems(items)
c.query = query
cmd = func() tea.Msg {
items, err := c.completionProvider.GetChildEntries(query)
if err != nil {
// status.Error(err.Error())
}
// c.list.SetItems(items)
return items
}
cmds = append(cmds, cmd)
}
u, cmd := c.list.Update(msg)
c.list = u.(list.List[CompletionItemI])
cmds = append(cmds, cmd)
}
@@ -186,14 +190,17 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, tea.Batch(cmds...)
} else {
items, err := c.completionProvider.GetChildEntries("")
if err != nil {
// status.Error(err.Error())
cmd := func() tea.Msg {
items, err := c.completionProvider.GetChildEntries("")
if err != nil {
// status.Error(err.Error())
}
return items
}
c.list.SetItems(items)
cmds = append(cmds, cmd)
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
c.pseudoSearchTextArea.SetValue(msg.String())
return c, c.pseudoSearchTextArea.Focus()
return c, tea.Batch(cmds...)
}
case tea.WindowSizeMsg:
c.width = msg.Width
@@ -243,29 +250,27 @@ func (c *completionDialogComponent) IsEmpty() bool {
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
if c.completionProvider.GetId() != provider.GetId() {
c.completionProvider = provider
items, err := provider.GetChildEntries("")
if err != nil {
// status.Error(err.Error())
}
c.list.SetItems(items)
}
}
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
ti := textarea.New()
items, err := completionProvider.GetChildEntries("")
if err != nil {
// status.Error(err.Error())
}
li := list.NewListComponent(
items,
[]CompletionItemI{},
7,
"No matches",
false,
)
go func() {
items, err := completionProvider.GetChildEntries("")
if err != nil {
// status.Error(err.Error())
}
li.SetItems(items)
}()
return &completionDialogComponent{
query: "",
completionProvider: completionProvider,
+2 -8
View File
@@ -17,11 +17,9 @@ type Config struct {
// NewConfig creates a new Config instance with default values.
// This can be useful for initializing a new configuration file.
func NewConfig(theme, provider, model string) *Config {
func NewConfig() *Config {
return &Config{
Theme: theme,
Provider: provider,
Model: model,
Theme: "opencode",
}
}
@@ -35,12 +33,10 @@ func SaveConfig(filePath string, config *Config) error {
defer file.Close()
writer := bufio.NewWriter(file)
encoder := toml.NewEncoder(writer)
if err := encoder.Encode(config); err != nil {
return fmt.Errorf("failed to encode config to TOML file %s: %w", filePath, err)
}
if err := writer.Flush(); err != nil {
return fmt.Errorf("failed to flush writer for config file %s: %w", filePath, err)
}
@@ -53,13 +49,11 @@ func SaveConfig(filePath string, config *Config) error {
// It returns a pointer to the Config struct and an error if any issues occur.
func LoadConfig(filePath string) (*Config, error) {
var config Config
if _, err := toml.DecodeFile(filePath, &config); err != nil {
if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
return nil, fmt.Errorf("config file not found at %s: %w", filePath, statErr)
}
return nil, fmt.Errorf("failed to decode TOML from file %s: %w", filePath, err)
}
return &config, nil
}
+2 -3
View File
@@ -98,7 +98,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 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)
@@ -106,9 +105,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.completionDialog = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
// Doesn't forward event if enter key is pressed and there are completions
// Doesn't forward event if enter key is pressed
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.String() == "enter" { // && !p.completionDialog.IsEmpty() {
if keyMsg.String() == "enter" {
return p, tea.Batch(cmds...)
}
}
+3 -1
View File
@@ -38,6 +38,8 @@ type appModel struct {
func (a appModel) Init() tea.Cmd {
t := theme.CurrentTheme()
var cmds []tea.Cmd
cmds = append(cmds, a.app.InitializeProvider())
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
cmds = append(cmds, tea.RequestBackgroundColor)
@@ -50,7 +52,7 @@ func (a appModel) Init() tea.Cmd {
// Check if we should show the init dialog
cmds = append(cmds, func() tea.Msg {
shouldShow := app.Info.Git && app.Info.Time.Initialized == nil
shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized == nil
return dialog.ShowInitDialogMsg{Show: shouldShow}
})
+17 -1
View File
@@ -782,12 +782,28 @@
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
]
}
},
"required": [
"input",
"output",
"reasoning"
"reasoning",
"cache"
]
}
},
@@ -126,6 +126,10 @@ type MessageInfo struct {
Summary *bool `json:"summary,omitempty"`
System []string `json:"system"`
Tokens struct {
Cache struct {
Read float32 `json:"read"`
Write float32 `json:"write"`
} `json:"cache"`
Input float32 `json:"input"`
Output float32 `json:"output"`
Reasoning float32 `json:"reasoning"`