mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 05:28:16 +00:00
small improvements
This commit is contained in:
parent
caea293759
commit
c24e3c18e0
11 changed files with 149 additions and 37 deletions
|
@ -3,16 +3,5 @@
|
|||
"gopls": {
|
||||
"command": "gopls"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"coder": {
|
||||
"model": "gpt-4.1"
|
||||
},
|
||||
"task": {
|
||||
"model": "gpt-4.1"
|
||||
},
|
||||
"title": {
|
||||
"model": "gpt-4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
|
|||
q := db.New(conn)
|
||||
sessions := session.NewService(q)
|
||||
messages := message.NewService(q)
|
||||
files := history.NewService(q)
|
||||
files := history.NewService(q, conn)
|
||||
|
||||
app := &App{
|
||||
Sessions: sessions,
|
||||
|
|
|
@ -27,7 +27,8 @@ CREATE TABLE IF NOT EXISTS files (
|
|||
version TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
|
||||
UNIQUE(path, session_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_session_id ON files (session_id);
|
||||
|
|
|
@ -2,9 +2,11 @@ package history
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
|
@ -40,10 +42,11 @@ type Service interface {
|
|||
|
||||
type service struct {
|
||||
*pubsub.Broker[File]
|
||||
q db.Querier
|
||||
db *sql.DB
|
||||
q *db.Queries
|
||||
}
|
||||
|
||||
func NewService(q db.Querier) Service {
|
||||
func NewService(q *db.Queries, db *sql.DB) Service {
|
||||
return &service{
|
||||
Broker: pubsub.NewBroker[File](),
|
||||
q: q,
|
||||
|
@ -91,19 +94,64 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st
|
|||
}
|
||||
|
||||
func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
|
||||
dbFile, err := s.q.CreateFile(ctx, db.CreateFileParams{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
Path: path,
|
||||
Content: content,
|
||||
Version: version,
|
||||
})
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
// Maximum number of retries for transaction conflicts
|
||||
const maxRetries = 3
|
||||
var file File
|
||||
var err error
|
||||
|
||||
// Retry loop for transaction conflicts
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Start a transaction
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
// Create a new queries instance with the transaction
|
||||
qtx := s.q.WithTx(tx)
|
||||
|
||||
// Try to create the file within the transaction
|
||||
dbFile, err := qtx.CreateFile(ctx, db.CreateFileParams{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
Path: path,
|
||||
Content: content,
|
||||
Version: version,
|
||||
})
|
||||
if err != nil {
|
||||
// Rollback the transaction
|
||||
tx.Rollback()
|
||||
|
||||
// Check if this is a uniqueness constraint violation
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
if attempt < maxRetries-1 {
|
||||
// If we have retries left, generate a new version and try again
|
||||
if strings.HasPrefix(version, "v") {
|
||||
versionNum, parseErr := strconv.Atoi(version[1:])
|
||||
if parseErr == nil {
|
||||
version = fmt.Sprintf("v%d", versionNum+1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// If we can't parse the version, use a timestamp-based version
|
||||
version = fmt.Sprintf("v%d", time.Now().Unix())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
return File{}, fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
file = s.fromDBItem(dbFile)
|
||||
s.Publish(pubsub.CreatedEvent, file)
|
||||
return file, nil
|
||||
}
|
||||
file := s.fromDBItem(dbFile)
|
||||
s.Publish(pubsub.CreatedEvent, file)
|
||||
return file, nil
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (s *service) Get(ctx context.Context, id string) (File, error) {
|
||||
|
|
|
@ -118,12 +118,14 @@ func (m *editorCmp) GetSize() (int, int) {
|
|||
}
|
||||
|
||||
func (m *editorCmp) BindingKeys() []key.Binding {
|
||||
bindings := layout.KeyMapToSlice(m.textarea.KeyMap)
|
||||
bindings := []key.Binding{}
|
||||
if m.textarea.Focused() {
|
||||
bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
|
||||
} else {
|
||||
bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
|
||||
}
|
||||
|
||||
bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ func (m *sidebarCmp) modifiedFiles() string {
|
|||
// If no modified files, show a placeholder message
|
||||
if m.modFiles == nil || len(m.modFiles) == 0 {
|
||||
message := "No modified files"
|
||||
remainingWidth := m.width - lipgloss.Width(modifiedFiles)
|
||||
remainingWidth := m.width - lipgloss.Width(message)
|
||||
if remainingWidth > 0 {
|
||||
message += strings.Repeat(" ", remainingWidth)
|
||||
}
|
||||
|
@ -223,6 +223,9 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
|
|||
if initialVersion.ID == "" {
|
||||
continue
|
||||
}
|
||||
if initialVersion.Content == file.Content {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate diff between initial and latest version
|
||||
_, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path)
|
||||
|
|
|
@ -11,6 +11,9 @@ import (
|
|||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
)
|
||||
|
@ -20,6 +23,7 @@ type statusCmp struct {
|
|||
width int
|
||||
messageTTL time.Duration
|
||||
lspClients map[string]*lsp.Client
|
||||
session session.Session
|
||||
}
|
||||
|
||||
// clearMessageCmd is a command that clears status messages after a timeout
|
||||
|
@ -38,6 +42,16 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
return m, nil
|
||||
case chat.SessionSelectedMsg:
|
||||
m.session = msg
|
||||
case chat.SessionClearedMsg:
|
||||
m.session = session.Session{}
|
||||
case pubsub.Event[session.Session]:
|
||||
if msg.Type == pubsub.UpdatedEvent {
|
||||
if m.session.ID == msg.Payload.ID {
|
||||
m.session = msg.Payload
|
||||
}
|
||||
}
|
||||
case util.InfoMsg:
|
||||
m.info = msg
|
||||
ttl := msg.TTL
|
||||
|
@ -53,8 +67,43 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
|
||||
|
||||
func formatTokensAndCost(tokens int64, cost float64) string {
|
||||
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
||||
var formattedTokens string
|
||||
switch {
|
||||
case tokens >= 1_000_000:
|
||||
formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
|
||||
case tokens >= 1_000:
|
||||
formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
|
||||
default:
|
||||
formattedTokens = fmt.Sprintf("%d", tokens)
|
||||
}
|
||||
|
||||
// Remove .0 suffix if present
|
||||
if strings.HasSuffix(formattedTokens, ".0K") {
|
||||
formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
|
||||
}
|
||||
if strings.HasSuffix(formattedTokens, ".0M") {
|
||||
formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
|
||||
}
|
||||
|
||||
// Format cost with $ symbol and 2 decimal places
|
||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||
|
||||
return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
|
||||
}
|
||||
|
||||
func (m statusCmp) View() string {
|
||||
status := helpWidget
|
||||
if m.session.ID != "" {
|
||||
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
|
||||
tokensStyle := styles.Padded.
|
||||
Background(styles.Forground).
|
||||
Foreground(styles.BackgroundDim).
|
||||
Render(tokens)
|
||||
status += tokensStyle
|
||||
}
|
||||
|
||||
diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
|
||||
if m.info.Msg != "" {
|
||||
infoStyle := styles.Padded.
|
||||
|
@ -82,6 +131,7 @@ func (m statusCmp) View() string {
|
|||
Width(m.availableFooterMsgWidth(diagnostics)).
|
||||
Render("")
|
||||
}
|
||||
|
||||
status += diagnostics
|
||||
status += m.model()
|
||||
return status
|
||||
|
@ -136,7 +186,11 @@ func (m *statusCmp) projectDiagnostics() string {
|
|||
}
|
||||
|
||||
func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
|
||||
return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics))
|
||||
tokens := ""
|
||||
if m.session.ID != "" {
|
||||
tokens = formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
|
||||
}
|
||||
return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-lipgloss.Width(tokens))
|
||||
}
|
||||
|
||||
func (m statusCmp) model() string {
|
||||
|
|
|
@ -26,7 +26,7 @@ func (h *helpCmp) SetBindings(k []key.Binding) {
|
|||
func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
h.width = 80
|
||||
h.width = 90
|
||||
h.height = msg.Height
|
||||
}
|
||||
return h, nil
|
||||
|
@ -62,7 +62,7 @@ func (h *helpCmp) render() string {
|
|||
var (
|
||||
pairs []string
|
||||
width int
|
||||
rows = 12 - 2
|
||||
rows = 14 - 2
|
||||
)
|
||||
for i := 0; i < len(bindings); i += rows {
|
||||
var (
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
type Container interface {
|
||||
tea.Model
|
||||
Sizeable
|
||||
Bindings
|
||||
}
|
||||
type container struct {
|
||||
width int
|
||||
|
|
|
@ -15,9 +15,12 @@ import (
|
|||
var ChatPage PageID = "chat"
|
||||
|
||||
type chatPage struct {
|
||||
app *app.App
|
||||
layout layout.SplitPaneLayout
|
||||
session session.Session
|
||||
app *app.App
|
||||
editor layout.Container
|
||||
messages layout.Container
|
||||
layout layout.SplitPaneLayout
|
||||
session session.Session
|
||||
editingMode bool
|
||||
}
|
||||
|
||||
type ChatKeyMap struct {
|
||||
|
@ -59,6 +62,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if cmd != nil {
|
||||
return p, cmd
|
||||
}
|
||||
case chat.EditorFocusMsg:
|
||||
p.editingMode = bool(msg)
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keyMap.NewSession):
|
||||
|
@ -133,7 +138,11 @@ func (p *chatPage) View() string {
|
|||
|
||||
func (p *chatPage) BindingKeys() []key.Binding {
|
||||
bindings := layout.KeyMapToSlice(keyMap)
|
||||
bindings = append(bindings, p.layout.BindingKeys()...)
|
||||
if p.editingMode {
|
||||
bindings = append(bindings, p.editor.BindingKeys()...)
|
||||
} else {
|
||||
bindings = append(bindings, p.messages.BindingKeys()...)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
@ -148,7 +157,10 @@ func NewChatPage(app *app.App) tea.Model {
|
|||
layout.WithBorder(true, false, false, false),
|
||||
)
|
||||
return &chatPage{
|
||||
app: app,
|
||||
app: app,
|
||||
editor: editorContainer,
|
||||
messages: messagesContainer,
|
||||
editingMode: true,
|
||||
layout: layout.NewSplitPane(
|
||||
layout.WithLeftPanel(messagesContainer),
|
||||
layout.WithBottomPanel(editorContainer),
|
||||
|
|
|
@ -215,6 +215,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
a.status, _ = a.status.Update(msg)
|
||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue