Compare commits

...

12 Commits

Author SHA1 Message Date
Dax Raad d869df4fee remove unused permission timeout 2025-06-19 18:00:53 -04:00
Dax Raad b99d4650ec temporarily disable project details in system prompt 2025-06-19 17:37:23 -04:00
Frank 261bb7f110 Infra: fix DO tag 2025-06-19 17:20:13 -04:00
Dax Raad 0515fbb260 fix gopls download spewing into terminal 2025-06-19 17:08:58 -04:00
adamdottv 88211d8c5b fix(tui): upgrade notification 2025-06-19 16:03:45 -05:00
Jay V a812f95b9d docs: share 2025-06-19 16:57:42 -04:00
adamdottv 3728a12bee fix(tui): better help on home 2025-06-19 15:56:28 -05:00
Jay V af07e51213 docs: tweak 2025-06-19 16:40:15 -04:00
Jay V 3113788c92 docs: copy 2025-06-19 16:39:36 -04:00
Jay V efb5fe6d4e docs: styles 2025-06-19 16:38:37 -04:00
Jay V 54dd6c644d docs: adding to config 2025-06-19 16:36:17 -04:00
Dax Raad 39ad8f2667 ignore: do migration 2025-06-19 16:32:32 -04:00
14 changed files with 331 additions and 46 deletions
+3 -2
View File
@@ -23,8 +23,9 @@ export const api = new sst.cloudflare.Worker("Api", {
},
])
args.migrations = {
oldTag: "v1",
newTag: "v1",
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
+3
View File
@@ -70,6 +70,9 @@ export namespace LSPServer {
const proc = Bun.spawn({
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
env: { ...process.env, GOBIN: Global.Path.bin },
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
@@ -64,6 +64,7 @@ export namespace Permission {
title: Info["title"]
metadata: Info["metadata"]
}) {
return
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
+3 -3
View File
@@ -82,9 +82,9 @@ export namespace SystemPrompt {
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
`<project>`,
` ${app.git ? await tree() : ""}`,
`</project>`,
// `<project>`,
// ` ${app.git ? await tree() : ""}`,
// `</project>`,
].join("\n"),
]
}
+2 -2
View File
@@ -23,7 +23,7 @@ type App struct {
Info client.AppInfo
Version string
StatePath string
Configg *client.ConfigInfo
Config *client.ConfigInfo
Client *client.ClientWithResponses
State *config.State
Provider *client.ProviderInfo
@@ -95,7 +95,7 @@ func New(
Info: appInfo,
Version: version,
StatePath: appStatePath,
Configg: configInfo,
Config: configInfo,
State: appState,
Client: httpClient,
Session: &client.SessionInfo{},
+1 -1
View File
@@ -186,7 +186,7 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
},
{
Name: ProjectInitCommand,
Description: "create or update AGENTS.md",
Description: "create/update AGENTS.md",
Keybindings: parseBindings("<leader>i"),
Trigger: "init",
},
@@ -10,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
@@ -37,6 +38,7 @@ type messagesComponent struct {
viewport viewport.Model
spinner spinner.Model
attachments viewport.Model
commands commands.CommandsComponent
cache *MessageCache
rendering bool
showToolDetails bool
@@ -46,7 +48,7 @@ type renderFinishedMsg struct{}
type ToggleToolDetailsMsg struct{}
func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
}
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -93,6 +95,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
spinner, cmd := m.spinner.Update(msg)
m.spinner = spinner
cmds = append(cmds, cmd)
updated, cmd := m.commands.Update(msg)
m.commands = updated.(commands.CommandsComponent)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
@@ -281,7 +288,13 @@ func (m *messagesComponent) View() string {
return m.home()
}
if m.rendering {
return m.viewport.View()
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
"Loading session...",
)
}
t := theme.CurrentTheme()
return lipgloss.JoinVertical(
@@ -319,50 +332,42 @@ func (m *messagesComponent) home() string {
// cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config
commands := [][]string{
{"/help", "show help"},
{"/sessions", "list sessions"},
{"/new", "start a new session"},
{"/model", "switch model"},
{"/theme", "switch theme"},
{"/exit", "exit the app"},
}
versionStyle := lipgloss.NewStyle().
Background(t.Background()).
Foreground(t.TextMuted()).
Width(lipgloss.Width(logo)).
Align(lipgloss.Right)
version := versionStyle.Render(m.app.Version)
commandLines := []string{}
for _, command := range commands {
commandLines = append(commandLines, (base(command[0]+" ") + muted(command[1])))
}
logoAndVersion := lipgloss.JoinVertical(
lipgloss.Right,
logo,
muted(m.app.Version),
logoAndVersion := strings.Join([]string{logo, version}, "\n")
logoAndVersion = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
logoAndVersion,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
)
commands := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.commands.View(),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
)
lines := []string{}
lines = append(lines, "")
lines = append(lines, "")
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config))
// lines = append(lines, "")
lines = append(lines, commandLines...)
lines = append(lines, "")
if m.rendering {
lines = append(lines, base("Loading session..."))
} else {
lines = append(lines, "")
}
lines = append(lines, commands)
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
strings.Join(lines, "\n"),
),
baseStyle.Render(strings.Join(lines, "\n")),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
)
}
@@ -381,6 +386,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
m.attachments.SetWidth(width + 40)
m.attachments.SetHeight(3)
m.commands.SetSize(width, height)
m.renderView()
return nil
}
@@ -444,11 +450,19 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
attachments := viewport.New()
vp.KeyMap = viewport.KeyMap{}
t := theme.CurrentTheme()
commandsView := commands.New(
app,
commands.WithBackground(t.Background()),
commands.WithLimit(6),
)
return &messagesComponent{
app: app,
viewport: vp,
spinner: s,
attachments: attachments,
commands: commandsView,
showToolDetails: true,
cache: NewMessageCache(),
tail: true,
@@ -0,0 +1,196 @@
package commands
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type CommandsComponent interface {
tea.Model
tea.ViewModel
layout.Sizeable
}
type commandsComponent struct {
app *app.App
width, height int
showKeybinds bool
background *compat.AdaptiveColor
limit *int
}
func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
c.width = width
c.height = height
return nil
}
func (c *commandsComponent) GetSize() (int, int) {
return c.width, c.height
}
func (c *commandsComponent) Init() tea.Cmd {
return nil
}
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
c.width = msg.Width
c.height = msg.Height
}
return c, nil
}
func (c *commandsComponent) View() string {
t := theme.CurrentTheme()
triggerStyle := lipgloss.NewStyle().
Foreground(t.Primary()).
Bold(true)
descriptionStyle := lipgloss.NewStyle().
Foreground(t.Text())
keybindStyle := lipgloss.NewStyle().
Foreground(t.TextMuted())
if c.background != nil {
triggerStyle = triggerStyle.Background(*c.background)
descriptionStyle = descriptionStyle.Background(*c.background)
keybindStyle = keybindStyle.Background(*c.background)
}
var commandsWithTriggers []commands.Command
for _, cmd := range c.app.Commands.Sorted() {
if cmd.Trigger != "" {
commandsWithTriggers = append(commandsWithTriggers, cmd)
}
}
if c.limit != nil && len(commandsWithTriggers) > *c.limit {
commandsWithTriggers = commandsWithTriggers[:*c.limit]
}
if len(commandsWithTriggers) == 0 {
return styles.Muted().Render("No commands with triggers available")
}
// Calculate column widths
maxTriggerWidth := 0
maxDescriptionWidth := 0
maxKeybindWidth := 0
// Prepare command data
type commandRow struct {
trigger string
description string
keybinds string
}
rows := make([]commandRow, 0, len(commandsWithTriggers))
for _, cmd := range commandsWithTriggers {
trigger := "/" + cmd.Trigger
description := cmd.Description
// Format keybindings
var keybindStrs []string
if c.showKeybinds {
for _, kb := range cmd.Keybindings {
if kb.RequiresLeader {
keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
} else {
keybindStrs = append(keybindStrs, kb.Key)
}
}
}
keybinds := strings.Join(keybindStrs, ", ")
rows = append(rows, commandRow{
trigger: trigger,
description: description,
keybinds: keybinds,
})
// Update max widths
if len(trigger) > maxTriggerWidth {
maxTriggerWidth = len(trigger)
}
if len(description) > maxDescriptionWidth {
maxDescriptionWidth = len(description)
}
if len(keybinds) > maxKeybindWidth {
maxKeybindWidth = len(keybinds)
}
}
// Add padding between columns
columnPadding := 3
// Build the output
var output strings.Builder
for _, row := range rows {
// Pad each column to align properly
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
// Apply styles and combine
line := triggerStyle.Render(trigger) +
triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
descriptionStyle.Render(description)
if c.showKeybinds && row.keybinds != "" {
line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
keybindStyle.Render(row.keybinds)
}
output.WriteString(line + "\n")
}
// Remove trailing newline
result := strings.TrimSuffix(output.String(), "\n")
return result
}
type Option func(*commandsComponent)
func WithKeybinds(show bool) Option {
return func(c *commandsComponent) {
c.showKeybinds = show
}
}
func WithBackground(background compat.AdaptiveColor) Option {
return func(c *commandsComponent) {
c.background = &background
}
}
func WithLimit(limit int) Option {
return func(c *commandsComponent) {
c.limit = &limit
}
}
func New(app *app.App, opts ...Option) CommandsComponent {
c := &commandsComponent{
app: app,
background: nil,
showKeybinds: true,
}
for _, opt := range opts {
opt(c)
}
return c
}
+4 -4
View File
@@ -214,8 +214,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.completions.SetProvider(a.completionManager.DefaultProvider())
case client.EventInstallationUpdated:
return a, toast.NewSuccessToast(
"New version installed",
toast.WithTitle("opencode updated to "+msg.Properties.Version+", restart to apply."),
"opencode updated to "+msg.Properties.Version+", restart to apply.",
toast.WithTitle("New version installed"),
)
case client.EventSessionUpdated:
if msg.Properties.Info.Id == a.app.Session.Id {
@@ -556,8 +556,8 @@ func NewModel(app *app.App) tea.Model {
messagesContainer := layout.NewContainer(messages)
var leaderBinding *key.Binding
if (*app.Configg.Keybinds).Leader != nil {
binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
if (*app.Config.Keybinds).Leader != nil {
binding := key.NewBinding(key.WithKeys(*app.Config.Keybinds.Leader))
leaderBinding = &binding
}
+1 -1
View File
@@ -802,7 +802,7 @@ export default function Share(props: {
<IconOpencode width={16} height={16} />
</div>
<Show when={store.info?.version} fallback="v0.0.1">
<span>v${store.info?.version}</span>
<span>v{store.info?.version}</span>
</Show>
</li>
</ul>
@@ -21,9 +21,71 @@ In most cases, you'll want to use the global config for things like themes, prov
When opencode starts up, it looks for a config file in the current directory or traverse up to the nearest Git directory.
---
## Schema
The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json).
Your editor should be able to validate and autocomplete based on the schema.
---
### Models
You can configure the providers and models you want to use in your opencode config through the `provider` and `model` options.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": { },
"model": ""
}
```
[Learn more here](/docs/models).
---
### Themes
You can configure the theme you want to use in your opencode config through the `theme` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"theme": ""
}
```
[Learn more here](/docs/themes).
---
### Keybinds
You can customize your keybinds through the `keybinds` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"keybinds": { }
}
```
[Learn more here](/docs/keybinds).
---
### MCP servers
You can configure MCP servers you want to use through the `mcp` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"mcp": { }
}
```
[Learn more here](/docs/mcp-servers).
+5 -1
View File
@@ -13,7 +13,9 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
- Log in with Anthropic to use your Claude Pro or Claude Max account.
- Supports 75+ LLM providers through [Models.dev](https://models.dev), including local models.
## Installation
---
## Install
<Tabs>
<TabItem label="npm">
@@ -58,6 +60,8 @@ brew install sst/tap/opencode
paru -S opencode-bin
```
---
## Providers
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
@@ -75,7 +75,7 @@ When opencode starts up, it checks for the following:
1. The model list in the opencode config.
```json title="opencode.json" {4}
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-20250514"
+4
View File
@@ -3,6 +3,10 @@
--sl-color-divider: var(--sl-color-gray-5);
}
.expressive-code .frame {
box-shadow: none;
}
@media (prefers-color-scheme: dark) {
.shiki,
.shiki span {