Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit c24e3c1

Browse files
committed
small improvements
1 parent caea293 commit c24e3c1

File tree

11 files changed

+149
-37
lines changed

11 files changed

+149
-37
lines changed

.opencode.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,5 @@
33
"gopls": {
44
"command": "gopls"
55
}
6-
},
7-
"agents": {
8-
"coder": {
9-
"model": "gpt-4.1"
10-
},
11-
"task": {
12-
"model": "gpt-4.1"
13-
},
14-
"title": {
15-
"model": "gpt-4.1"
16-
}
176
}
187
}

internal/app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
3939
q := db.New(conn)
4040
sessions := session.NewService(q)
4141
messages := message.NewService(q)
42-
files := history.NewService(q)
42+
files := history.NewService(q, conn)
4343

4444
app := &App{
4545
Sessions: sessions,

internal/db/migrations/000001_initial.up.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ CREATE TABLE IF NOT EXISTS files (
2727
version TEXT NOT NULL,
2828
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
2929
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
30-
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
30+
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
31+
UNIQUE(path, session_id, version)
3132
);
3233

3334
CREATE INDEX IF NOT EXISTS idx_files_session_id ON files (session_id);

internal/history/file.go

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package history
22

33
import (
44
"context"
5+
"database/sql"
56
"fmt"
67
"strconv"
78
"strings"
9+
"time"
810

911
"github.com/google/uuid"
1012
"github.com/kujtimiihoxha/opencode/internal/db"
@@ -40,10 +42,11 @@ type Service interface {
4042

4143
type service struct {
4244
*pubsub.Broker[File]
43-
q db.Querier
45+
db *sql.DB
46+
q *db.Queries
4447
}
4548

46-
func NewService(q db.Querier) Service {
49+
func NewService(q *db.Queries, db *sql.DB) Service {
4750
return &service{
4851
Broker: pubsub.NewBroker[File](),
4952
q: q,
@@ -91,19 +94,64 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st
9194
}
9295

9396
func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
94-
dbFile, err := s.q.CreateFile(ctx, db.CreateFileParams{
95-
ID: uuid.New().String(),
96-
SessionID: sessionID,
97-
Path: path,
98-
Content: content,
99-
Version: version,
100-
})
101-
if err != nil {
102-
return File{}, err
97+
// Maximum number of retries for transaction conflicts
98+
const maxRetries = 3
99+
var file File
100+
var err error
101+
102+
// Retry loop for transaction conflicts
103+
for attempt := 0; attempt < maxRetries; attempt++ {
104+
// Start a transaction
105+
tx, err := s.db.BeginTx(ctx, nil)
106+
if err != nil {
107+
return File{}, fmt.Errorf("failed to begin transaction: %w", err)
108+
}
109+
110+
// Create a new queries instance with the transaction
111+
qtx := s.q.WithTx(tx)
112+
113+
// Try to create the file within the transaction
114+
dbFile, err := qtx.CreateFile(ctx, db.CreateFileParams{
115+
ID: uuid.New().String(),
116+
SessionID: sessionID,
117+
Path: path,
118+
Content: content,
119+
Version: version,
120+
})
121+
if err != nil {
122+
// Rollback the transaction
123+
tx.Rollback()
124+
125+
// Check if this is a uniqueness constraint violation
126+
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
127+
if attempt < maxRetries-1 {
128+
// If we have retries left, generate a new version and try again
129+
if strings.HasPrefix(version, "v") {
130+
versionNum, parseErr := strconv.Atoi(version[1:])
131+
if parseErr == nil {
132+
version = fmt.Sprintf("v%d", versionNum+1)
133+
continue
134+
}
135+
}
136+
// If we can't parse the version, use a timestamp-based version
137+
version = fmt.Sprintf("v%d", time.Now().Unix())
138+
continue
139+
}
140+
}
141+
return File{}, err
142+
}
143+
144+
// Commit the transaction
145+
if err = tx.Commit(); err != nil {
146+
return File{}, fmt.Errorf("failed to commit transaction: %w", err)
147+
}
148+
149+
file = s.fromDBItem(dbFile)
150+
s.Publish(pubsub.CreatedEvent, file)
151+
return file, nil
103152
}
104-
file := s.fromDBItem(dbFile)
105-
s.Publish(pubsub.CreatedEvent, file)
106-
return file, nil
153+
154+
return file, err
107155
}
108156

109157
func (s *service) Get(ctx context.Context, id string) (File, error) {

internal/tui/components/chat/editor.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ func (m *editorCmp) GetSize() (int, int) {
118118
}
119119

120120
func (m *editorCmp) BindingKeys() []key.Binding {
121-
bindings := layout.KeyMapToSlice(m.textarea.KeyMap)
121+
bindings := []key.Binding{}
122122
if m.textarea.Focused() {
123123
bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
124124
} else {
125125
bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
126126
}
127+
128+
bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
127129
return bindings
128130
}
129131

internal/tui/components/chat/sidebar.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (m *sidebarCmp) modifiedFiles() string {
127127
// If no modified files, show a placeholder message
128128
if m.modFiles == nil || len(m.modFiles) == 0 {
129129
message := "No modified files"
130-
remainingWidth := m.width - lipgloss.Width(modifiedFiles)
130+
remainingWidth := m.width - lipgloss.Width(message)
131131
if remainingWidth > 0 {
132132
message += strings.Repeat(" ", remainingWidth)
133133
}
@@ -223,6 +223,9 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
223223
if initialVersion.ID == "" {
224224
continue
225225
}
226+
if initialVersion.Content == file.Content {
227+
continue
228+
}
226229

227230
// Calculate diff between initial and latest version
228231
_, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path)

internal/tui/components/core/status.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"github.com/kujtimiihoxha/opencode/internal/llm/models"
1212
"github.com/kujtimiihoxha/opencode/internal/lsp"
1313
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
14+
"github.com/kujtimiihoxha/opencode/internal/pubsub"
15+
"github.com/kujtimiihoxha/opencode/internal/session"
16+
"github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
1417
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
1518
"github.com/kujtimiihoxha/opencode/internal/tui/util"
1619
)
@@ -20,6 +23,7 @@ type statusCmp struct {
2023
width int
2124
messageTTL time.Duration
2225
lspClients map[string]*lsp.Client
26+
session session.Session
2327
}
2428

2529
// 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) {
3842
case tea.WindowSizeMsg:
3943
m.width = msg.Width
4044
return m, nil
45+
case chat.SessionSelectedMsg:
46+
m.session = msg
47+
case chat.SessionClearedMsg:
48+
m.session = session.Session{}
49+
case pubsub.Event[session.Session]:
50+
if msg.Type == pubsub.UpdatedEvent {
51+
if m.session.ID == msg.Payload.ID {
52+
m.session = msg.Payload
53+
}
54+
}
4155
case util.InfoMsg:
4256
m.info = msg
4357
ttl := msg.TTL
@@ -53,8 +67,43 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5367

5468
var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
5569

70+
func formatTokensAndCost(tokens int64, cost float64) string {
71+
// Format tokens in human-readable format (e.g., 110K, 1.2M)
72+
var formattedTokens string
73+
switch {
74+
case tokens >= 1_000_000:
75+
formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
76+
case tokens >= 1_000:
77+
formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
78+
default:
79+
formattedTokens = fmt.Sprintf("%d", tokens)
80+
}
81+
82+
// Remove .0 suffix if present
83+
if strings.HasSuffix(formattedTokens, ".0K") {
84+
formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
85+
}
86+
if strings.HasSuffix(formattedTokens, ".0M") {
87+
formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
88+
}
89+
90+
// Format cost with $ symbol and 2 decimal places
91+
formattedCost := fmt.Sprintf("$%.2f", cost)
92+
93+
return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
94+
}
95+
5696
func (m statusCmp) View() string {
5797
status := helpWidget
98+
if m.session.ID != "" {
99+
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
100+
tokensStyle := styles.Padded.
101+
Background(styles.Forground).
102+
Foreground(styles.BackgroundDim).
103+
Render(tokens)
104+
status += tokensStyle
105+
}
106+
58107
diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
59108
if m.info.Msg != "" {
60109
infoStyle := styles.Padded.
@@ -82,6 +131,7 @@ func (m statusCmp) View() string {
82131
Width(m.availableFooterMsgWidth(diagnostics)).
83132
Render("")
84133
}
134+
85135
status += diagnostics
86136
status += m.model()
87137
return status
@@ -136,7 +186,11 @@ func (m *statusCmp) projectDiagnostics() string {
136186
}
137187

138188
func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
139-
return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics))
189+
tokens := ""
190+
if m.session.ID != "" {
191+
tokens = formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
192+
}
193+
return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-lipgloss.Width(tokens))
140194
}
141195

142196
func (m statusCmp) model() string {

internal/tui/components/dialog/help.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (h *helpCmp) SetBindings(k []key.Binding) {
2626
func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
2727
switch msg := msg.(type) {
2828
case tea.WindowSizeMsg:
29-
h.width = 80
29+
h.width = 90
3030
h.height = msg.Height
3131
}
3232
return h, nil
@@ -62,7 +62,7 @@ func (h *helpCmp) render() string {
6262
var (
6363
pairs []string
6464
width int
65-
rows = 12 - 2
65+
rows = 14 - 2
6666
)
6767
for i := 0; i < len(bindings); i += rows {
6868
var (

internal/tui/layout/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
type Container interface {
1111
tea.Model
1212
Sizeable
13+
Bindings
1314
}
1415
type container struct {
1516
width int

internal/tui/page/chat.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import (
1515
var ChatPage PageID = "chat"
1616

1717
type chatPage struct {
18-
app *app.App
19-
layout layout.SplitPaneLayout
20-
session session.Session
18+
app *app.App
19+
editor layout.Container
20+
messages layout.Container
21+
layout layout.SplitPaneLayout
22+
session session.Session
23+
editingMode bool
2124
}
2225

2326
type ChatKeyMap struct {
@@ -59,6 +62,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5962
if cmd != nil {
6063
return p, cmd
6164
}
65+
case chat.EditorFocusMsg:
66+
p.editingMode = bool(msg)
6267
case tea.KeyMsg:
6368
switch {
6469
case key.Matches(msg, keyMap.NewSession):
@@ -133,7 +138,11 @@ func (p *chatPage) View() string {
133138

134139
func (p *chatPage) BindingKeys() []key.Binding {
135140
bindings := layout.KeyMapToSlice(keyMap)
136-
bindings = append(bindings, p.layout.BindingKeys()...)
141+
if p.editingMode {
142+
bindings = append(bindings, p.editor.BindingKeys()...)
143+
} else {
144+
bindings = append(bindings, p.messages.BindingKeys()...)
145+
}
137146
return bindings
138147
}
139148

@@ -148,7 +157,10 @@ func NewChatPage(app *app.App) tea.Model {
148157
layout.WithBorder(true, false, false, false),
149158
)
150159
return &chatPage{
151-
app: app,
160+
app: app,
161+
editor: editorContainer,
162+
messages: messagesContainer,
163+
editingMode: true,
152164
layout: layout.NewSplitPane(
153165
layout.WithLeftPanel(messagesContainer),
154166
layout.WithBottomPanel(editorContainer),

0 commit comments

Comments
 (0)