on main help sign in
fix(summary): handle cancel and other cases
commitd7619a5c0855f06bed94ac530172b7c0131fa2cf authorkujtimiihoxha <kujtimii.h@gmail.com> dateWed 8 Oct 2025, 14:18 (1 year ago) parentabb654e
21 files changed+200−387
CRUSH.md+1−1 internal/agent/agent.go+87−54 internal/agent/agent_tool.go+1−1 internal/agent/common_test.go+1−1 internal/agent/coordinator.go+1−1 internal/agent/tools/grep.go+9−2 internal/db/db.go+1−1 internal/db/files.sql.go+1−1 internal/db/messages.sql.go+17−11 internal/db/migrations/20250810000000_add_is_summary_message.sql+5 internal/db/models.go+11−10 internal/db/querier.go+1−1 internal/db/sessions.sql.go+1−1 internal/db/sql/messages.sql+2−1 internal/message/content.go+9−8 internal/message/message.go+25−18 internal/tui/components/chat/chat.go+17 internal/tui/components/chat/messages/messages.go+3 internal/tui/components/dialogs/compact/compact.go−168 internal/tui/components/dialogs/compact/keys.go−71 internal/tui/tui.go+7−36
CRUSH.md+1−1
@@ -6,7 +6,7 @@
66- **Test**: `task test` or `go test ./...` (run single test: `go test ./internal/llm/prompt -run TestGetContextFromPaths`)
77- **Update Golden Files**: `go test ./... -update` (regenerates .golden files when test output changes)
88 - Update specific package: `go test ./internal/tui/components/core -update` (in this case, we're updating "core")
9−- **Lint**: `task lint-fix`
9+- **Lint**: `task lint:fix`
1010- **Format**: `task fmt` (gofumpt -w .)
1111- **Dev**: `task dev` (runs with profiling enabled)
1212
internal/agent/agent.go+87−54
@@ -62,36 +62,41 @@ type Model struct {
6262}
6363
6464type sessionAgent struct {
65− largeModel Model
66− smallModel Model
67− systemPrompt string
68− tools []ai.AgentTool
69− sessions session.Service
70− messages message.Service
65+ largeModel Model
66+ smallModel Model
67+ systemPrompt string
68+ tools []ai.AgentTool
69+ sessions session.Service
70+ messages message.Service
71+ disableAutoSummarize bool
7172
7273 messageQueue *csync.Map[string, []SessionAgentCall]
7374 activeRequests *csync.Map[string, context.CancelFunc]
7475}
7576
76−type SessionAgentOption func(*sessionAgent)
77+type SessionAgentOptions struct {
78+ LargeModel Model
79+ SmallModel Model
80+ SystemPrompt string
81+ DisableAutoSummarize bool
82+ Sessions session.Service
83+ Messages message.Service
84+ Tools []ai.AgentTool
85+}
7786
7887func NewSessionAgent(
79− largeModel Model,
80− smallModel Model,
81− systemPrompt string,
82− sessions session.Service,
83− messages message.Service,
84− tools ...ai.AgentTool,
88+ opts SessionAgentOptions,
8589) SessionAgent {
8690 return &sessionAgent{
87− largeModel: largeModel,
88− smallModel: smallModel,
89− systemPrompt: systemPrompt,
90− sessions: sessions,
91− messages: messages,
92− tools: tools,
93− messageQueue: csync.NewMap[string, []SessionAgentCall](),
94− activeRequests: csync.NewMap[string, context.CancelFunc](),
91+ largeModel: opts.LargeModel,
92+ smallModel: opts.SmallModel,
93+ systemPrompt: opts.SystemPrompt,
94+ sessions: opts.Sessions,
95+ messages: opts.Messages,
96+ disableAutoSummarize: opts.DisableAutoSummarize,
97+ tools: opts.Tools,
98+ messageQueue: csync.NewMap[string, []SessionAgentCall](),
99+ activeRequests: csync.NewMap[string, context.CancelFunc](),
95100 }
96101}
97102
@@ -164,6 +169,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
164169 history, files := a.preparePrompt(msgs, call.Attachments...)
165170
166171 var currentAssistant *message.Message
172+ var shouldSummarize bool
167173 result, err := agent.Stream(genCtx, ai.AgentStreamCall{
168174 Prompt: call.Prompt,
169175 Files: files,
@@ -292,16 +298,16 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
292298 IsError: isError,
293299 Metadata: result.ClientMetadata,
294300 }
295− _, err := a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
301+ _, createMsgErr := a.messages.Create(genCtx, currentAssistant.SessionID, message.CreateMessageParams{
296302 Role: message.Tool,
297303 Parts: []message.ContentPart{
298304 toolResult,
299305 },
300306 })
301− if err != nil {
302− return err
307+ if createMsgErr != nil {
308+ return createMsgErr
303309 }
304− return a.messages.Update(genCtx, *currentAssistant)
310+ return nil
305311 },
306312 OnStepFinish: func(stepResult ai.StepResult) error {
307313 finishReason := message.FinishReasonUnknown
@@ -323,6 +329,18 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
323329 }
324330 return a.messages.Update(genCtx, *currentAssistant)
325331 },
332+ StopWhen: []ai.StopCondition{
333+ func(_ []ai.StepResult) bool {
334+ contextWindow := a.largeModel.CatwalkCfg.ContextWindow
335+ tokens := currentSession.CompletionTokens + currentSession.PromptTokens
336+ percentage := (float64(tokens) / float64(contextWindow)) * 100
337+ if (percentage > 80) && !a.disableAutoSummarize {
338+ shouldSummarize = true
339+ return true
340+ }
341+ return false
342+ },
343+ },
326344 })
327345 if err != nil {
328346 isCancelErr := errors.Is(err, context.Canceled)
@@ -358,28 +376,29 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
358376 break
359377 }
360378 }
361− if !found {
362− content := "There was an error while executing the tool"
363− if isCancelErr {
364− content = "Tool execution canceled by user"
365− } else if isPermissionErr {
366− content = "Permission denied"
367− }
368− toolResult := message.ToolResult{
369− ToolCallID: tc.ID,
370− Name: tc.Name,
371− Content: content,
372− IsError: true,
373− }
374− _, createErr = a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
375− Role: message.Tool,
376− Parts: []message.ContentPart{
377− toolResult,
378− },
379− })
380− if createErr != nil {
381− return nil, createErr
382− }
379+ if found {
380+ continue
381+ }
382+ content := "There was an error while executing the tool"
383+ if isCancelErr {
384+ content = "Tool execution canceled by user"
385+ } else if isPermissionErr {
386+ content = "Permission denied"
387+ }
388+ toolResult := message.ToolResult{
389+ ToolCallID: tc.ID,
390+ Name: tc.Name,
391+ Content: content,
392+ IsError: true,
393+ }
394+ _, createErr = a.messages.Create(context.Background(), currentAssistant.SessionID, message.CreateMessageParams{
395+ Role: message.Tool,
396+ Parts: []message.ContentPart{
397+ toolResult,
398+ },
399+ })
400+ if createErr != nil {
401+ return nil, createErr
383402 }
384403 }
385404 if isCancelErr {
@@ -398,6 +417,13 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
398417 }
399418 wg.Wait()
400419
420+ if shouldSummarize {
421+ a.activeRequests.Del(call.SessionID)
422+ if summarizeErr := a.Summarize(genCtx, call.SessionID); summarizeErr != nil {
423+ return nil, summarizeErr
424+ }
425+ }
426+
401427 queuedMessages, ok := a.messageQueue.Get(call.SessionID)
402428 if !ok || len(queuedMessages) == 0 {
403429 return result, err
@@ -437,20 +463,21 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
437463 ai.WithSystemPrompt(string(summaryPrompt)),
438464 )
439465 summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
440− Role: message.Assistant,
441− Model: a.largeModel.Model.Model(),
442− Provider: a.largeModel.Model.Provider(),
466+ Role: message.Assistant,
467+ Model: a.largeModel.Model.Model(),
468+ Provider: a.largeModel.Model.Provider(),
469+ IsSummaryMessage: true,
443470 })
444471 if err != nil {
445472 return err
446473 }
447474
448− resp, err := agent.Stream(ctx, ai.AgentStreamCall{
475+ resp, err := agent.Stream(genCtx, ai.AgentStreamCall{
449476 Prompt: "Provide a detailed summary of our conversation above.",
450477 Messages: aiMsgs,
451478 OnReasoningDelta: func(id string, text string) error {
452479 summaryMessage.AppendReasoningContent(text)
453− return a.messages.Update(ctx, summaryMessage)
480+ return a.messages.Update(genCtx, summaryMessage)
454481 },
455482 OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
456483 // handle anthropic signature
@@ -460,14 +487,20 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
460487 }
461488 }
462489 summaryMessage.FinishThinking()
463− return a.messages.Update(ctx, summaryMessage)
490+ return a.messages.Update(genCtx, summaryMessage)
464491 },
465492 OnTextDelta: func(id, text string) error {
466493 summaryMessage.AppendContent(text)
467− return a.messages.Update(ctx, summaryMessage)
494+ return a.messages.Update(genCtx, summaryMessage)
468495 },
469496 })
470497 if err != nil {
498+ isCancelErr := errors.Is(err, context.Canceled)
499+ if isCancelErr {
500+ // User cancelled summarize we need to remove the summary message
501+ deleteErr := a.messages.Delete(ctx, summaryMessage.ID)
502+ return deleteErr
503+ }
471504 return err
472505 }
473506
internal/agent/agent_tool.go+1−1
@@ -82,7 +82,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
8282 PresencePenalty: model.ModelCfg.PresencePenalty,
8383 })
8484 if err != nil {
85− return ai.ToolResponse{}, fmt.Errorf("error generating agent: %s", err)
85+ return ai.NewTextErrorResponse("error generating response"), nil
8686 }
8787 updatedSession, err := c.sessions.Get(ctx, session.ID)
8888 if err != nil {
internal/agent/common_test.go+1−1
@@ -127,7 +127,7 @@ func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt strin
127127 // todo: add values
128128 },
129129 }
130− agent := NewSessionAgent(largeModel, smallModel, systemPrompt, env.sessions, env.messages, tools...)
130+ agent := NewSessionAgent(SessionAgentOptions{largeModel, smallModel, systemPrompt, false, env.sessions, env.messages, tools})
131131 return agent
132132}
133133
internal/agent/coordinator.go+1−1
@@ -165,7 +165,7 @@ func (c *coordinator) buildAgent(prompt *prompt.Prompt, agent config.Agent) (Ses
165165 if err != nil {
166166 return nil, err
167167 }
168− return NewSessionAgent(large, small, systemPrompt, c.sessions, c.messages, tools...), nil
168+ return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.sessions, c.messages, tools}), nil
169169}
170170
171171func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
internal/agent/tools/grep.go+9−2
@@ -89,7 +89,10 @@ type GrepResponseMetadata struct {
8989 Truncated bool `json:"truncated"`
9090}
9191
92−const GrepToolName = "grep"
92+const (
93+ GrepToolName = "grep"
94+ maxGrepContentWidth = 500
95+)
9396
9497//go:embed grep.md
9598var grepDescription []byte
@@ -135,7 +138,11 @@ func NewGrepTool(workingDir string) ai.AgentTool {
135138 fmt.Fprintf(&output, "%s:\n", match.path)
136139 }
137140 if match.lineNum > 0 {
138− fmt.Fprintf(&output, " Line %d: %s\n", match.lineNum, match.lineText)
141+ lineText := match.lineText
142+ if len(lineText) > maxGrepContentWidth {
143+ lineText = lineText[:maxGrepContentWidth] + "..."
144+ }
145+ fmt.Fprintf(&output, " Line %d: %s\n", match.lineNum, lineText)
139146 } else {
140147 fmt.Fprintf(&output, " %s\n", match.path)
141148 }
internal/db/db.go+1−1
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44
55package db
66
internal/db/files.sql.go+1−1
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44// source: files.sql
55
66package db
internal/db/messages.sql.go+17−11
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44// source: messages.sql
55
66package db
@@ -18,21 +18,23 @@ INSERT INTO messages (
1818 parts,
1919 model,
2020 provider,
21+ is_summary_message,
2122 created_at,
2223 updated_at
2324) VALUES (
24− ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
25+ ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
2526)
26−RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at, provider
27+RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at, provider, is_summary_message
2728`
2829
2930type CreateMessageParams struct {
30− ID string `json:"id"`
31− SessionID string `json:"session_id"`
32− Role string `json:"role"`
33− Parts string `json:"parts"`
34− Model sql.NullString `json:"model"`
35− Provider sql.NullString `json:"provider"`
31+ ID string `json:"id"`
32+ SessionID string `json:"session_id"`
33+ Role string `json:"role"`
34+ Parts string `json:"parts"`
35+ Model sql.NullString `json:"model"`
36+ Provider sql.NullString `json:"provider"`
37+ IsSummaryMessage int64 `json:"is_summary_message"`
3638}
3739
3840func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
@@ -43,6 +45,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
4345 arg.Parts,
4446 arg.Model,
4547 arg.Provider,
48+ arg.IsSummaryMessage,
4649 )
4750 var i Message
4851 err := row.Scan(
@@ -55,6 +58,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
5558 &i.UpdatedAt,
5659 &i.FinishedAt,
5760 &i.Provider,
61+ &i.IsSummaryMessage,
5862 )
5963 return i, err
6064}
@@ -80,7 +84,7 @@ func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) e
8084}
8185
8286const getMessage = `-- name: GetMessage :one
83−SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider
87+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider, is_summary_message
8488FROM messages
8589WHERE id = ? LIMIT 1
8690`
@@ -98,12 +102,13 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
98102 &i.UpdatedAt,
99103 &i.FinishedAt,
100104 &i.Provider,
105+ &i.IsSummaryMessage,
101106 )
102107 return i, err
103108}
104109
105110const listMessagesBySession = `-- name: ListMessagesBySession :many
106−SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider
111+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at, provider, is_summary_message
107112FROM messages
108113WHERE session_id = ?
109114ORDER BY created_at ASC
@@ -128,6 +133,7 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
128133 &i.UpdatedAt,
129134 &i.FinishedAt,
130135 &i.Provider,
136+ &i.IsSummaryMessage,
131137 ); err != nil {
132138 return nil, err
133139 }
internal/db/migrations/20250810000000_add_is_summary_message.sqlnew file+5
@@ -0,0 +1,5 @@
1+-- +goose Up
2+ALTER TABLE messages ADD COLUMN is_summary_message INTEGER DEFAULT 0 NOT NULL;
3+
4+-- +goose Down
5+ALTER TABLE messages DROP COLUMN is_summary_message;
internal/db/models.go+11−10
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44
55package db
66
@@ -19,15 +19,16 @@ type File struct {
1919}
2020
2121type Message struct {
22− ID string `json:"id"`
23− SessionID string `json:"session_id"`
24− Role string `json:"role"`
25− Parts string `json:"parts"`
26− Model sql.NullString `json:"model"`
27− CreatedAt int64 `json:"created_at"`
28− UpdatedAt int64 `json:"updated_at"`
29− FinishedAt sql.NullInt64 `json:"finished_at"`
30− Provider sql.NullString `json:"provider"`
22+ ID string `json:"id"`
23+ SessionID string `json:"session_id"`
24+ Role string `json:"role"`
25+ Parts string `json:"parts"`
26+ Model sql.NullString `json:"model"`
27+ CreatedAt int64 `json:"created_at"`
28+ UpdatedAt int64 `json:"updated_at"`
29+ FinishedAt sql.NullInt64 `json:"finished_at"`
30+ Provider sql.NullString `json:"provider"`
31+ IsSummaryMessage int64 `json:"is_summary_message"`
3132}
3233
3334type Session struct {
internal/db/querier.go+1−1
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44
55package db
66
internal/db/sessions.sql.go+1−1
@@ -1,6 +1,6 @@
11// Code generated by sqlc. DO NOT EDIT.
22// versions:
3−// sqlc v1.29.0
3+// sqlc v1.30.0
44// source: sessions.sql
55
66package db
internal/db/sql/messages.sql+2−1
@@ -17,10 +17,11 @@ INSERT INTO messages (
1717 parts,
1818 model,
1919 provider,
20+ is_summary_message,
2021 created_at,
2122 updated_at
2223) VALUES (
23− ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
24+ ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
2425)
2526RETURNING *;
2627
internal/message/content.go+9−8
@@ -120,14 +120,15 @@ type Finish struct {
120120func (Finish) isPart() {}
121121
122122type Message struct {
123− ID string
124− Role MessageRole
125− SessionID string
126− Parts []ContentPart
127− Model string
128− Provider string
129− CreatedAt int64
130− UpdatedAt int64
123+ ID string
124+ Role MessageRole
125+ SessionID string
126+ Parts []ContentPart
127+ Model string
128+ Provider string
129+ CreatedAt int64
130+ UpdatedAt int64
131+ IsSummaryMessage bool
131132}
132133
133134func (m *Message) Content() TextContent {
internal/message/message.go+25−18
@@ -13,10 +13,11 @@ import (
1313)
1414
1515type CreateMessageParams struct {
16− Role MessageRole
17− Parts []ContentPart
18− Model string
19− Provider string
16+ Role MessageRole
17+ Parts []ContentPart
18+ Model string
19+ Provider string
20+ IsSummaryMessage bool
2021}
2122
2223type Service interface {
@@ -64,13 +65,18 @@ func (s *service) Create(ctx context.Context, sessionID string, params CreateMes
6465 if err != nil {
6566 return Message{}, err
6667 }
68+ isSummary := int64(0)
69+ if params.IsSummaryMessage {
70+ isSummary = 1
71+ }
6772 dbMessage, err := s.q.CreateMessage(ctx, db.CreateMessageParams{
68− ID: uuid.New().String(),
69− SessionID: sessionID,
70− Role: string(params.Role),
71− Parts: string(partsJSON),
72− Model: sql.NullString{String: string(params.Model), Valid: true},
73− Provider: sql.NullString{String: params.Provider, Valid: params.Provider != ""},
73+ ID: uuid.New().String(),
74+ SessionID: sessionID,
75+ Role: string(params.Role),
76+ Parts: string(partsJSON),
77+ Model: sql.NullString{String: string(params.Model), Valid: true},
78+ Provider: sql.NullString{String: params.Provider, Valid: params.Provider != ""},
79+ IsSummaryMessage: isSummary,
7480 })
7581 if err != nil {
7682 return Message{}, err
@@ -151,14 +157,15 @@ func (s *service) fromDBItem(item db.Message) (Message, error) {
151157 return Message{}, err
152158 }
153159 return Message{
154− ID: item.ID,
155− SessionID: item.SessionID,
156− Role: MessageRole(item.Role),
157− Parts: parts,
158− Model: item.Model.String,
159− Provider: item.Provider.String,
160− CreatedAt: item.CreatedAt,
161− UpdatedAt: item.UpdatedAt,
160+ ID: item.ID,
161+ SessionID: item.SessionID,
162+ Role: MessageRole(item.Role),
163+ Parts: parts,
164+ Model: item.Model.String,
165+ Provider: item.Provider.String,
166+ CreatedAt: item.CreatedAt,
167+ UpdatedAt: item.UpdatedAt,
168+ IsSummaryMessage: item.IsSummaryMessage != 0,
162169 }, nil
163170}
164171
internal/tui/components/chat/chat.go+17
@@ -334,6 +334,11 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
334334 return nil
335335 }
336336 return m.handleNewMessage(event.Payload)
337+ case pubsub.DeletedEvent:
338+ if event.Payload.SessionID != m.session.ID {
339+ return nil
340+ }
341+ return m.handleDeleteMessage(event.Payload)
337342 case pubsub.UpdatedEvent:
338343 if event.Payload.SessionID != m.session.ID {
339344 return m.handleChildSession(event)
@@ -360,6 +365,18 @@ func (m *messageListCmp) messageExists(messageID string) bool {
360365 return false
361366}
362367
368+// handleDeleteMessage removes a message from the list.
369+func (m *messageListCmp) handleDeleteMessage(msg message.Message) tea.Cmd {
370+ items := m.listCmp.Items()
371+ for i := len(items) - 1; i >= 0; i-- {
372+ if msgCmp, ok := items[i].(messages.MessageCmp); ok && msgCmp.GetMessage().ID == msg.ID {
373+ m.listCmp.DeleteItem(items[i].ID())
374+ return nil
375+ }
376+ }
377+ return nil
378+}
379+
363380// handleNewMessage routes new messages to appropriate handlers based on role.
364381func (m *messageListCmp) handleNewMessage(msg message.Message) tea.Cmd {
365382 switch msg.Role {
internal/tui/components/chat/messages/messages.go+3
@@ -121,6 +121,9 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
121121// Returns different views for spinning, user, and assistant messages.
122122func (m *messageCmp) View() string {
123123 if m.spinning && m.message.ReasoningContent().Thinking == "" {
124+ if m.message.IsSummaryMessage {
125+ m.anim.SetLabel("Summarizing")
126+ }
124127 return m.style().PaddingLeft(1).Render(m.anim.View())
125128 }
126129 if m.message.ID != "" {
internal/tui/components/dialogs/compact/compact.godeleted−168
@@ -1,168 +0,0 @@
1−package compact
2−
3−import (
4− "context"
5−
6− "github.com/charmbracelet/bubbles/v2/key"
7− tea "github.com/charmbracelet/bubbletea/v2"
8− "github.com/charmbracelet/lipgloss/v2"
9−
10− "github.com/charmbracelet/crush/internal/agent"
11− "github.com/charmbracelet/crush/internal/tui/components/core"
12− "github.com/charmbracelet/crush/internal/tui/components/dialogs"
13− "github.com/charmbracelet/crush/internal/tui/styles"
14− "github.com/charmbracelet/crush/internal/tui/util"
15−)
16−
17−const CompactDialogID dialogs.DialogID = "compact"
18−
19−// CompactDialog interface for the session compact dialog
20−type CompactDialog interface {
21− dialogs.DialogModel
22−}
23−
24−type compactDialogCmp struct {
25− wWidth, wHeight int
26− width, height int
27− selected int
28− keyMap KeyMap
29− sessionID string
30− progress string
31− agent agent.Coordinator
32− noAsk bool // If true, skip confirmation dialog
33−}
34−
35−// NewCompactDialogCmp creates a new session compact dialog
36−func NewCompactDialogCmp(agent agent.Coordinator, sessionID string, noAsk bool) CompactDialog {
37− return &compactDialogCmp{
38− sessionID: sessionID,
39− keyMap: DefaultKeyMap(),
40− selected: 0,
41− agent: agent,
42− noAsk: noAsk,
43− }
44−}
45−
46−func (c *compactDialogCmp) Init() tea.Cmd {
47− if c.noAsk {
48− // If noAsk is true, skip confirmation and start compaction immediately
49− c.agent.Summarize(context.Background(), c.sessionID)
50− return util.CmdHandler(dialogs.CloseDialogMsg{})
51− }
52− return nil
53−}
54−
55−func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
56− switch msg := msg.(type) {
57− case tea.WindowSizeMsg:
58− c.wWidth = msg.Width
59− c.wHeight = msg.Height
60− cmd := c.SetSize()
61− return c, cmd
62− case tea.KeyPressMsg:
63− switch {
64− case key.Matches(msg, c.keyMap.ChangeSelection):
65− c.selected = (c.selected + 1) % 2
66− return c, nil
67− case key.Matches(msg, c.keyMap.Select):
68− if c.selected == 0 {
69− c.agent.Summarize(context.Background(), c.sessionID)
70− return c, util.CmdHandler(dialogs.CloseDialogMsg{})
71− } else {
72− return c, util.CmdHandler(dialogs.CloseDialogMsg{})
73− }
74− case key.Matches(msg, c.keyMap.Y):
75− c.agent.Summarize(context.Background(), c.sessionID)
76− return c, util.CmdHandler(dialogs.CloseDialogMsg{})
77− case key.Matches(msg, c.keyMap.N):
78− return c, util.CmdHandler(dialogs.CloseDialogMsg{})
79− case key.Matches(msg, c.keyMap.Close):
80− return c, util.CmdHandler(dialogs.CloseDialogMsg{})
81− }
82− }
83− return c, nil
84−}
85−
86−func (c *compactDialogCmp) renderButtons() string {
87− t := styles.CurrentTheme()
88− baseStyle := t.S().Base
89−
90− buttons := []core.ButtonOpts{
91− {
92− Text: "Yes",
93− UnderlineIndex: 0, // "Y"
94− Selected: c.selected == 0,
95− },
96− {
97− Text: "No",
98− UnderlineIndex: 0, // "N"
99− Selected: c.selected == 1,
100− },
101− }
102−
103− content := core.SelectableButtons(buttons, " ")
104−
105− return baseStyle.AlignHorizontal(lipgloss.Right).Width(c.width - 4).Render(content)
106−}
107−
108−func (c *compactDialogCmp) render() string {
109− t := styles.CurrentTheme()
110− baseStyle := t.S().Base
111−
112− title := "Compact Session"
113− titleView := core.Title(title, c.width-4)
114− explanation := t.S().Text.
115− Width(c.width - 4).
116− Render("This will summarize the current session and reset the context. The conversation history will be condensed into a summary to free up context space while preserving important information.")
117−
118− question := t.S().Text.
119− Width(c.width - 4).
120− Render("Do you want to continue?")
121−
122− content := baseStyle.Render(lipgloss.JoinVertical(
123− lipgloss.Left,
124− explanation,
125− "",
126− question,
127− ))
128−
129− buttons := c.renderButtons()
130− dialogContent := lipgloss.JoinVertical(
131− lipgloss.Top,
132− titleView,
133− "",
134− content,
135− "",
136− buttons,
137− "",
138− )
139−
140− return baseStyle.
141− Padding(0, 1).
142− Border(lipgloss.RoundedBorder()).
143− BorderForeground(t.BorderFocus).
144− Width(c.width).
145− Render(dialogContent)
146−}
147−
148−func (c *compactDialogCmp) View() string {
149− return c.render()
150−}
151−
152−// SetSize sets the size of the component.
153−func (c *compactDialogCmp) SetSize() tea.Cmd {
154− c.width = min(90, c.wWidth)
155− c.height = min(15, c.wHeight)
156− return nil
157−}
158−
159−func (c *compactDialogCmp) Position() (int, int) {
160− row := (c.wHeight / 2) - (c.height / 2)
161− col := (c.wWidth / 2) - (c.width / 2)
162− return row, col
163−}
164−
165−// ID implements CompactDialog.
166−func (c *compactDialogCmp) ID() dialogs.DialogID {
167− return CompactDialogID
168−}
internal/tui/components/dialogs/compact/keys.godeleted−71
@@ -1,71 +0,0 @@
1−package compact
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5−)
6−
7−// KeyMap defines the key bindings for the compact dialog.
8−type KeyMap struct {
9− ChangeSelection key.Binding
10− Select key.Binding
11− Y key.Binding
12− N key.Binding
13− Close key.Binding
14−}
15−
16−// DefaultKeyMap returns the default key bindings for the compact dialog.
17−func DefaultKeyMap() KeyMap {
18− return KeyMap{
19− ChangeSelection: key.NewBinding(
20− key.WithKeys("tab", "left", "right", "h", "l"),
21− key.WithHelp("tab/←/→", "toggle selection"),
22− ),
23− Select: key.NewBinding(
24− key.WithKeys("enter"),
25− key.WithHelp("enter", "confirm"),
26− ),
27− Y: key.NewBinding(
28− key.WithKeys("y"),
29− key.WithHelp("y", "yes"),
30− ),
31− N: key.NewBinding(
32− key.WithKeys("n"),
33− key.WithHelp("n", "no"),
34− ),
35− Close: key.NewBinding(
36− key.WithKeys("esc", "alt+esc"),
37− key.WithHelp("esc", "cancel"),
38− ),
39− }
40−}
41−
42−// KeyBindings implements layout.KeyMapProvider
43−func (k KeyMap) KeyBindings() []key.Binding {
44− return []key.Binding{
45− k.ChangeSelection,
46− k.Select,
47− k.Y,
48− k.N,
49− k.Close,
50− }
51−}
52−
53−// FullHelp implements help.KeyMap.
54−func (k KeyMap) FullHelp() [][]key.Binding {
55− m := [][]key.Binding{}
56− slice := k.KeyBindings()
57− for i := 0; i < len(slice); i += 4 {
58− end := min(i+4, len(slice))
59− m = append(m, slice[i:end])
60− }
61− return m
62−}
63−
64−// ShortHelp implements help.KeyMap.
65−func (k KeyMap) ShortHelp() []key.Binding {
66− return []key.Binding{
67− k.ChangeSelection,
68− k.Select,
69− k.Close,
70− }
71−}
internal/tui/tui.go+7−36
@@ -21,7 +21,6 @@ import (
2121 "github.com/charmbracelet/crush/internal/tui/components/core/status"
2222 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
2323 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
24− "github.com/charmbracelet/crush/internal/tui/components/dialogs/compact"
2524 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2625 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
2726 "github.com/charmbracelet/crush/internal/tui/components/dialogs/permissions"
@@ -178,9 +177,13 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
178177 )
179178 // Compact
180179 case commands.CompactMsg:
181− return a, util.CmdHandler(dialogs.OpenDialogMsg{
182− Model: compact.NewCompactDialogCmp(a.app.AgentCoordinator, msg.SessionID, true),
183− })
180+ return a, func() tea.Msg {
181+ err := a.app.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
182+ if err != nil {
183+ return util.ReportError(err)()
184+ }
185+ return nil
186+ }
184187 case commands.QuitMsg:
185188 return a, util.CmdHandler(dialogs.OpenDialogMsg{
186189 Model: quit.NewQuitDialog(),
@@ -251,38 +254,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
251254 a.app.Permissions.Deny(msg.Permission)
252255 }
253256 return a, nil
254− // Agent Events
255− // TODO: HANDLE AUTO COMPACT
256− // case pubsub.Event[agent.AgentEvent]:
257− // payload := msg.Payload
258− //
259− // // Forward agent events to dialogs
260− // if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() == compact.CompactDialogID {
261− // u, dialogCmd := a.dialog.Update(payload)
262− // if model, ok := u.(dialogs.DialogCmp); ok {
263− // a.dialog = model
264− // }
265− //
266− // cmds = append(cmds, dialogCmd)
267− // }
268− //
269− // // Handle auto-compact logic
270− // if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSessionID != "" {
271− // // Get current session to check token usage
272− // session, err := a.app.Sessions.Get(context.Background(), a.selectedSessionID)
273− // if err == nil {
274− // model := a.app.AgentCoordinator.Model()
275− // contextWindow := model.CatwalkCfg.ContextWindow
276− // tokens := session.CompletionTokens + session.PromptTokens
277− // if (tokens >= int64(float64(contextWindow)*0.95)) && !config.Get().Options.DisableAutoSummarize { // Show compact confirmation dialog
278− // cmds = append(cmds, util.CmdHandler(dialogs.OpenDialogMsg{
279− // Model: compact.NewCompactDialogCmp(a.app.AgentCoordinator, a.selectedSessionID, false),
280− // }))
281− // }
282− // }
283− // }
284− //
285− // return a, tea.Batch(cmds...)
286257 case splash.OnboardingCompleteMsg:
287258 item, ok := a.pages[a.currentPage]
288259 if !ok {