11 "github.com/charmbracelet/crush/internal/db"
12 "github.com/charmbracelet/crush/internal/event"
13 "github.com/charmbracelet/crush/internal/pubsub"
14 "github.com/google/uuid"
15 "github.com/zeebo/xxh3"
21 TodoStatusPending TodoStatus = "pending"
22 TodoStatusInProgress TodoStatus = "in_progress"
23 TodoStatusCompleted TodoStatus = "completed"
26// HashID returns the XXH3 hash of a session ID (UUID) as a hex string.
27func HashID(id string) string {
30 return fmt.Sprintf("%x", h.Sum(nil))
34 Content string `json:"content"`
35 Status TodoStatus `json:"status"`
36 ActiveForm string `json:"active_form"`
39// HasIncompleteTodos returns true if there are any non-completed todos.
40func HasIncompleteTodos(todos []Todo) bool {
41 for _, todo := range todos {
42 if todo.Status != TodoStatusCompleted {
51 ParentSessionID string
55 CompletionTokens int64
56 SummaryMessageID string
63type Service interface {
64 pubsub.Subscriber[Session]
65 Create(ctx context.Context, title string) (Session, error)
66 CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
67 CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
68 Get(ctx context.Context, id string) (Session, error)
69 GetLast(ctx context.Context) (Session, error)
70 List(ctx context.Context) ([]Session, error)
71 Save(ctx context.Context, session Session) (Session, error)
72 UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error
73 Rename(ctx context.Context, id string, title string) error
74 Delete(ctx context.Context, id string) error
76 // Agent tool session management
77 CreateAgentToolSessionID(messageID, toolCallID string) string
78 ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool)
79 IsAgentToolSession(sessionID string) bool
83 *pubsub.Broker[Session]
88func (s *service) Create(ctx context.Context, title string) (Session, error) {
89 dbSession, err := s.q.CreateSession(ctx, db.CreateSessionParams{
90 ID: uuid.New().String(),
96 session := s.fromDBItem(dbSession)
97 s.Publish(pubsub.CreatedEvent, session)
98 event.SessionCreated()
102func (s *service) CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) {
103 dbSession, err := s.q.CreateSession(ctx, db.CreateSessionParams{
105 ParentSessionID: sql.NullString{String: parentSessionID, Valid: true},
109 return Session{}, err
111 session := s.fromDBItem(dbSession)
112 s.Publish(pubsub.CreatedEvent, session)
116func (s *service) CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error) {
117 dbSession, err := s.q.CreateSession(ctx, db.CreateSessionParams{
118 ID: "title-" + parentSessionID,
119 ParentSessionID: sql.NullString{String: parentSessionID, Valid: true},
120 Title: "Generate a title",
123 return Session{}, err
125 session := s.fromDBItem(dbSession)
126 s.Publish(pubsub.CreatedEvent, session)
130func (s *service) Delete(ctx context.Context, id string) error {
131 tx, err := s.db.BeginTx(ctx, nil)
133 return fmt.Errorf("beginning transaction: %w", err)
135 defer tx.Rollback() //nolint:errcheck
137 qtx := s.q.WithTx(tx)
139 dbSession, err := qtx.GetSessionByID(ctx, id)
143 if err = qtx.DeleteSessionMessages(ctx, dbSession.ID); err != nil {
144 return fmt.Errorf("deleting session messages: %w", err)
146 if err = qtx.DeleteSessionFiles(ctx, dbSession.ID); err != nil {
147 return fmt.Errorf("deleting session files: %w", err)
149 if err = qtx.DeleteSession(ctx, dbSession.ID); err != nil {
150 return fmt.Errorf("deleting session: %w", err)
152 if err = tx.Commit(); err != nil {
153 return fmt.Errorf("committing transaction: %w", err)
156 session := s.fromDBItem(dbSession)
157 s.Publish(pubsub.DeletedEvent, session)
158 event.SessionDeleted()
162func (s *service) Get(ctx context.Context, id string) (Session, error) {
163 dbSession, err := s.q.GetSessionByID(ctx, id)
165 return Session{}, err
167 return s.fromDBItem(dbSession), nil
170func (s *service) GetLast(ctx context.Context) (Session, error) {
171 dbSession, err := s.q.GetLastSession(ctx)
173 return Session{}, err
175 return s.fromDBItem(dbSession), nil
178func (s *service) Save(ctx context.Context, session Session) (Session, error) {
179 todosJSON, err := marshalTodos(session.Todos)
181 return Session{}, err
184 dbSession, err := s.q.UpdateSession(ctx, db.UpdateSessionParams{
186 Title: session.Title,
187 PromptTokens: session.PromptTokens,
188 CompletionTokens: session.CompletionTokens,
189 SummaryMessageID: sql.NullString{
190 String: session.SummaryMessageID,
191 Valid: session.SummaryMessageID != "",
194 Todos: sql.NullString{
196 Valid: todosJSON != "",
200 return Session{}, err
202 session = s.fromDBItem(dbSession)
203 s.Publish(pubsub.UpdatedEvent, session)
207// UpdateTitleAndUsage updates only the title and usage fields atomically.
208// This is safer than fetching, modifying, and saving the entire session.
209func (s *service) UpdateTitleAndUsage(ctx context.Context, sessionID, title string, promptTokens, completionTokens int64, cost float64) error {
210 return s.q.UpdateSessionTitleAndUsage(ctx, db.UpdateSessionTitleAndUsageParams{
213 PromptTokens: promptTokens,
214 CompletionTokens: completionTokens,
219// Rename updates only the title of a session without touching updated_at or
221func (s *service) Rename(ctx context.Context, id string, title string) error {
222 return s.q.RenameSession(ctx, db.RenameSessionParams{
228func (s *service) List(ctx context.Context) ([]Session, error) {
229 dbSessions, err := s.q.ListSessions(ctx)
233 sessions := make([]Session, len(dbSessions))
234 for i, dbSession := range dbSessions {
235 sessions[i] = s.fromDBItem(dbSession)
240func (s service) fromDBItem(item db.Session) Session {
241 todos, err := unmarshalTodos(item.Todos.String)
243 slog.Error("Failed to unmarshal todos", "session_id", item.ID, "error", err)
247 ParentSessionID: item.ParentSessionID.String,
249 MessageCount: item.MessageCount,
250 PromptTokens: item.PromptTokens,
251 CompletionTokens: item.CompletionTokens,
252 SummaryMessageID: item.SummaryMessageID.String,
255 CreatedAt: item.CreatedAt,
256 UpdatedAt: item.UpdatedAt,
260func marshalTodos(todos []Todo) (string, error) {
264 data, err := json.Marshal(todos)
268 return string(data), nil
271func unmarshalTodos(data string) ([]Todo, error) {
276 if err := json.Unmarshal([]byte(data), &todos); err != nil {
282func NewService(q *db.Queries, conn *sql.DB) Service {
283 broker := pubsub.NewBroker[Session]()
291// CreateAgentToolSessionID creates a session ID for agent tool sessions using the format "messageID$$toolCallID"
292func (s *service) CreateAgentToolSessionID(messageID, toolCallID string) string {
293 return fmt.Sprintf("%s$$%s", messageID, toolCallID)
296// ParseAgentToolSessionID parses an agent tool session ID into its components
297func (s *service) ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool) {
298 parts := strings.Split(sessionID, "$$")
302 return parts[0], parts[1], true
305// IsAgentToolSession checks if a session ID follows the agent tool session format
306func (s *service) IsAgentToolSession(sessionID string) bool {
307 _, _, ok := s.ParseAgentToolSessionID(sessionID)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list