Skip to content

Commit 8853d25

Browse files
committed
feat: implement command history persistence with database integration
This commit introduces a comprehensive command history persistence system that stores bash commands in the database and enables intelligent history search functionality within the chat editor. Key components: - Database schema for command history with automatic migrations - CommandHistory service for persistence operations - Enhanced chat editor with history navigation (Ctrl+R/Ctrl+P/Ctrl+N) - Full-text search capabilities for command discovery - Background command tracking integration The system maintains a rolling history of commands and provides seamless keyboard navigation for quick command retrieval during conversations. 💘 Generated with Crush Co-Authored-By: Crush <[email protected]>
1 parent a64a4de commit 8853d25

File tree

11 files changed

+749
-38
lines changed

11 files changed

+749
-38
lines changed

internal/app/app.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
tea "github.com/charmbracelet/bubbletea/v2"
13+
"github.com/charmbracelet/crush/internal/commandhistory"
1314
"github.com/charmbracelet/crush/internal/config"
1415
"github.com/charmbracelet/crush/internal/csync"
1516
"github.com/charmbracelet/crush/internal/db"
@@ -26,10 +27,11 @@ import (
2627
)
2728

2829
type App struct {
29-
Sessions session.Service
30-
Messages message.Service
31-
History history.Service
32-
Permissions permission.Service
30+
Sessions session.Service
31+
Messages message.Service
32+
History history.Service
33+
CommandHistory commandhistory.Service
34+
Permissions permission.Service
3335

3436
CoderAgent agent.Service
3537

@@ -53,18 +55,20 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
5355
sessions := session.NewService(q)
5456
messages := message.NewService(q)
5557
files := history.NewService(q, conn)
58+
commandHistory := commandhistory.NewService(q, conn)
5659
skipPermissionsRequests := cfg.Permissions != nil && cfg.Permissions.SkipRequests
5760
allowedTools := []string{}
5861
if cfg.Permissions != nil && cfg.Permissions.AllowedTools != nil {
5962
allowedTools = cfg.Permissions.AllowedTools
6063
}
6164

6265
app := &App{
63-
Sessions: sessions,
64-
Messages: messages,
65-
History: files,
66-
Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
67-
LSPClients: csync.NewMap[string, *lsp.Client](),
66+
Sessions: sessions,
67+
Messages: messages,
68+
History: files,
69+
CommandHistory: commandHistory,
70+
Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
71+
LSPClients: csync.NewMap[string, *lsp.Client](),
6872

6973
globalCtx: ctx,
7074

internal/commandhistory/service.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package commandhistory
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"strings"
7+
8+
"github.com/charmbracelet/crush/internal/db"
9+
"github.com/charmbracelet/crush/internal/pubsub"
10+
"github.com/google/uuid"
11+
)
12+
13+
type CommandHistory struct {
14+
ID string
15+
SessionID string
16+
Command string
17+
CreatedAt int64
18+
UpdatedAt int64
19+
}
20+
21+
type Service interface {
22+
pubsub.Suscriber[CommandHistory]
23+
Add(ctx context.Context, sessionID, command string) (CommandHistory, error)
24+
ListBySession(ctx context.Context, sessionID string, limit int) ([]CommandHistory, error)
25+
DeleteSessionHistory(ctx context.Context, sessionID string) error
26+
}
27+
28+
type service struct {
29+
*pubsub.Broker[CommandHistory]
30+
db *sql.DB
31+
q *db.Queries
32+
}
33+
34+
const MaxHistorySize = 1000
35+
36+
func NewService(q *db.Queries, db *sql.DB) Service {
37+
return &service{
38+
Broker: pubsub.NewBroker[CommandHistory](),
39+
q: q,
40+
db: db,
41+
}
42+
}
43+
44+
func (s *service) Add(ctx context.Context, sessionID, command string) (CommandHistory, error) {
45+
command = strings.TrimSpace(command)
46+
if command == "" {
47+
return CommandHistory{}, nil
48+
}
49+
50+
// Get current count for this session
51+
countRow, err := s.q.GetCommandHistoryCount(ctx, db.GetCommandHistoryCountParams{
52+
SessionID: sessionID,
53+
})
54+
if err != nil {
55+
return CommandHistory{}, err
56+
}
57+
58+
// If we're at the limit, remove oldest entries
59+
if int(countRow.Count) >= MaxHistorySize {
60+
history, err := s.q.ListCommandHistoryBySession(ctx, db.ListCommandHistoryBySessionParams{
61+
SessionID: sessionID,
62+
})
63+
if err != nil {
64+
return CommandHistory{}, err
65+
}
66+
67+
// Remove oldest entries to make room
68+
toRemove := int(countRow.Count) - MaxHistorySize + 1
69+
for i := 0; i < toRemove && i < len(history); i++ {
70+
// Simple deletion - in a more sophisticated implementation,
71+
// we might want to batch delete
72+
s.db.ExecContext(ctx, "DELETE FROM command_history WHERE id = ?", history[i].ID)
73+
}
74+
}
75+
76+
dbHistory, err := s.q.CreateCommandHistory(ctx, db.CreateCommandHistoryParams{
77+
ID: uuid.New().String(),
78+
SessionID: sessionID,
79+
Command: command,
80+
})
81+
if err != nil {
82+
return CommandHistory{}, err
83+
}
84+
85+
history := CommandHistory{
86+
ID: dbHistory.ID,
87+
SessionID: dbHistory.SessionID,
88+
Command: dbHistory.Command,
89+
CreatedAt: dbHistory.CreatedAt,
90+
UpdatedAt: dbHistory.UpdatedAt,
91+
}
92+
93+
s.Publish(pubsub.CreatedEvent, history)
94+
return history, nil
95+
}
96+
97+
func (s *service) ListBySession(ctx context.Context, sessionID string, limit int) ([]CommandHistory, error) {
98+
if limit <= 0 {
99+
limit = MaxHistorySize
100+
}
101+
102+
dbHistory, err := s.q.ListLatestCommandHistoryBySession(ctx, db.ListLatestCommandHistoryBySessionParams{
103+
SessionID: sessionID,
104+
Limit: int64(limit),
105+
})
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
history := make([]CommandHistory, len(dbHistory))
111+
for i, dbItem := range dbHistory {
112+
history[i] = CommandHistory{
113+
ID: dbItem.ID,
114+
SessionID: dbItem.SessionID,
115+
Command: dbItem.Command,
116+
CreatedAt: dbItem.CreatedAt,
117+
UpdatedAt: dbItem.UpdatedAt,
118+
}
119+
}
120+
return history, nil
121+
}
122+
123+
func (s *service) DeleteSessionHistory(ctx context.Context, sessionID string) error {
124+
err := s.q.DeleteSessionCommandHistory(ctx, db.DeleteSessionCommandHistoryParams{
125+
SessionID: sessionID,
126+
})
127+
if err != nil {
128+
return err
129+
}
130+
// Publish deletion event
131+
s.Publish(pubsub.DeletedEvent, CommandHistory{SessionID: sessionID})
132+
return nil
133+
}

internal/db/command_history.sql.go

Lines changed: 136 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)