on main help sign in
refactor: new compact dialog
commitb744b0f444aa94454f1c8bee6c4f3a03255b86b9 authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 9 Jun 2025, 11:49 (1 year ago) parent6e526f2
7 files changed+443−62
internal/llm/agent/agent.go+15−11 internal/llm/provider/anthropic.go+2−1 internal/tui/components/dialogs/commands/commands.go+25−20 internal/tui/components/dialogs/compact/compact.go+266 internal/tui/components/dialogs/compact/keys.go+61 internal/tui/components/dialogs/init/init.go+38−4 internal/tui/tui.go+36−26
internal/llm/agent/agent.go+15−11
@@ -586,22 +586,26 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
586586 a.Publish(pubsub.CreatedEvent, event)
587587
588588 // Send the messages to the summarize provider
589− response, err := a.summarizeProvider.SendMessages(
589+ response := a.summarizeProvider.StreamResponse(
590590 summarizeCtx,
591591 msgsWithPrompt,
592592 make([]tools.BaseTool, 0),
593593 )
594− if err != nil {
595− event = AgentEvent{
596− Type: AgentEventTypeError,
597− Error: fmt.Errorf("failed to summarize: %w", err),
598− Done: true,
594+ var finalResponse *provider.ProviderResponse
595+ for r := range response {
596+ if r.Error != nil {
597+ event = AgentEvent{
598+ Type: AgentEventTypeError,
599+ Error: fmt.Errorf("failed to summarize: %w", err),
600+ Done: true,
601+ }
602+ a.Publish(pubsub.CreatedEvent, event)
603+ return
599604 }
600− a.Publish(pubsub.CreatedEvent, event)
601− return
605+ finalResponse = r.Response
602606 }
603607
604− summary := strings.TrimSpace(response.Content)
608+ summary := strings.TrimSpace(finalResponse.Content)
605609 if summary == "" {
606610 event = AgentEvent{
607611 Type: AgentEventTypeError,
@@ -651,10 +655,10 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
651655 return
652656 }
653657 oldSession.SummaryMessageID = msg.ID
654− oldSession.CompletionTokens = response.Usage.OutputTokens
658+ oldSession.CompletionTokens = finalResponse.Usage.OutputTokens
655659 oldSession.PromptTokens = 0
656660 model := a.summarizeProvider.Model()
657− usage := response.Usage
661+ usage := finalResponse.Usage
658662 cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
659663 model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
660664 model.CostPer1MIn/1e6*float64(usage.InputTokens) +
internal/llm/provider/anthropic.go+2−1
@@ -195,7 +195,7 @@ func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, to
195195 }
196196}
197197
198−func (a *anthropicClient) send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (resposne *ProviderResponse, err error) {
198+func (a *anthropicClient) send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (response *ProviderResponse, err error) {
199199 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
200200 cfg := config.Get()
201201 if cfg.Debug {
@@ -339,6 +339,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
339339 Usage: a.usage(accumulatedMessage),
340340 FinishReason: a.finishReason(string(accumulatedMessage.StopReason)),
341341 },
342+ Content: content,
342343 }
343344 }
344345 }
internal/tui/components/dialogs/commands/commands.go+25−20
@@ -50,14 +50,18 @@ type commandDialogCmp struct {
5050 help help.Model
5151 commandType int // SystemCommands or UserCommands
5252 userCommands []Command // User-defined commands
53+ sessionID string // Current session ID
5354}
5455
5556type (
5657 SwitchSessionsMsg struct{}
5758 SwitchModelMsg struct{}
59+ CompactMsg struct {
60+ SessionID string
61+ }
5862)
5963
60−func NewCommandDialog() CommandsDialog {
64+func NewCommandDialog(sessionID string) CommandsDialog {
6165 listKeyMap := list.DefaultKeyMap()
6266 keyMap := DefaultCommandsDialogKeyMap()
6367
@@ -87,6 +91,7 @@ func NewCommandDialog() CommandsDialog {
8791 keyMap: DefaultCommandsDialogKeyMap(),
8892 help: help,
8993 commandType: SystemCommands,
94+ sessionID: sessionID,
9095 }
9196}
9297
@@ -222,7 +227,7 @@ func (c *commandDialogCmp) Position() (int, int) {
222227}
223228
224229func (c *commandDialogCmp) defaultCommands() []Command {
225− return []Command{
230+ commands := []Command{
226231 {
227232 ID: "init",
228233 Title: "Initialize Project",
@@ -235,33 +240,35 @@ func (c *commandDialogCmp) defaultCommands() []Command {
235240 The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
236241 If there's already a crush.md, improve it.
237242 If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
238− return tea.Batch(
239− util.CmdHandler(chat.SendMsg{
240− Text: prompt,
241− }),
242− )
243+ return util.CmdHandler(chat.SendMsg{
244+ Text: prompt,
245+ })
243246 },
244247 },
245− {
248+ }
249+
250+ // Only show compact command if there's an active session
251+ if c.sessionID != "" {
252+ commands = append(commands, Command{
246253 ID: "compact",
247254 Title: "Compact Session",
248255 Description: "Summarize the current session and create a new one with the summary",
249256 Handler: func(cmd Command) tea.Cmd {
250− return func() tea.Msg {
251− // TODO: implement compact message
252− return ""
253− }
257+ return util.CmdHandler(CompactMsg{
258+ SessionID: c.sessionID,
259+ })
254260 },
255− },
261+ })
262+ }
263+
264+ return append(commands, []Command{
256265 {
257266 ID: "switch_session",
258267 Title: "Switch Session",
259268 Description: "Switch to a different session",
260269 Shortcut: "ctrl+s",
261270 Handler: func(cmd Command) tea.Cmd {
262− return func() tea.Msg {
263− return SwitchSessionsMsg{}
264− }
271+ return util.CmdHandler(SwitchSessionsMsg{})
265272 },
266273 },
267274 {
@@ -269,12 +276,10 @@ func (c *commandDialogCmp) defaultCommands() []Command {
269276 Title: "Switch Model",
270277 Description: "Switch to a different model",
271278 Handler: func(cmd Command) tea.Cmd {
272− return func() tea.Msg {
273− return SwitchModelMsg{}
274− }
279+ return util.CmdHandler(SwitchModelMsg{})
275280 },
276281 },
277− }
282+ }...)
278283}
279284
280285func (c *commandDialogCmp) ID() dialogs.DialogID {
internal/tui/components/dialogs/compact/compact.gonew file+266
@@ -0,0 +1,266 @@
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/llm/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+ state compactState
31+ progress string
32+ agent agent.Service
33+ noAsk bool // If true, skip confirmation dialog
34+}
35+
36+type compactState int
37+
38+const (
39+ stateConfirm compactState = iota
40+ stateCompacting
41+ stateError
42+)
43+
44+// NewCompactDialogCmp creates a new session compact dialog
45+func NewCompactDialogCmp(agent agent.Service, sessionID string, noAsk bool) CompactDialog {
46+ return &compactDialogCmp{
47+ sessionID: sessionID,
48+ keyMap: DefaultKeyMap(),
49+ state: stateConfirm,
50+ selected: 0,
51+ agent: agent,
52+ noAsk: noAsk,
53+ }
54+}
55+
56+func (c *compactDialogCmp) Init() tea.Cmd {
57+ if c.noAsk {
58+ // If noAsk is true, skip confirmation and start compaction immediately
59+ return c.startCompaction()
60+ }
61+ return nil
62+}
63+
64+func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65+ switch msg := msg.(type) {
66+ case tea.WindowSizeMsg:
67+ c.wWidth = msg.Width
68+ c.wHeight = msg.Height
69+ cmd := c.SetSize()
70+ return c, cmd
71+
72+ case tea.KeyPressMsg:
73+ switch c.state {
74+ case stateConfirm:
75+ switch {
76+ case key.Matches(msg, c.keyMap.ChangeSelection):
77+ c.selected = (c.selected + 1) % 2
78+ return c, nil
79+ case key.Matches(msg, c.keyMap.Select):
80+ if c.selected == 0 {
81+ return c, c.startCompaction()
82+ } else {
83+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
84+ }
85+ case key.Matches(msg, c.keyMap.Y):
86+ return c, c.startCompaction()
87+ case key.Matches(msg, c.keyMap.N):
88+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
89+ case key.Matches(msg, c.keyMap.Close):
90+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
91+ }
92+ case stateCompacting:
93+ switch {
94+ case key.Matches(msg, c.keyMap.Close):
95+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
96+ }
97+ case stateError:
98+ switch {
99+ case key.Matches(msg, c.keyMap.Select):
100+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
101+ case key.Matches(msg, c.keyMap.Close):
102+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
103+ }
104+ }
105+
106+ case agent.AgentEvent:
107+ if msg.Type == agent.AgentEventTypeSummarize {
108+ if msg.Error != nil {
109+ c.state = stateError
110+ c.progress = "Error: " + msg.Error.Error()
111+ } else if msg.Done {
112+ return c, util.CmdHandler(
113+ dialogs.CloseDialogMsg{},
114+ )
115+ } else {
116+ c.progress = msg.Progress
117+ }
118+ }
119+ return c, nil
120+ }
121+
122+ return c, nil
123+}
124+
125+func (c *compactDialogCmp) startCompaction() tea.Cmd {
126+ c.state = stateCompacting
127+ c.progress = "Starting summarization..."
128+ return func() tea.Msg {
129+ err := c.agent.Summarize(context.Background(), c.sessionID)
130+ if err != nil {
131+ c.state = stateError
132+ c.progress = "Error: " + err.Error()
133+ }
134+ return nil
135+ }
136+}
137+
138+func (c *compactDialogCmp) renderButtons() string {
139+ t := styles.CurrentTheme()
140+ baseStyle := t.S().Base
141+
142+ buttons := []core.ButtonOpts{
143+ {
144+ Text: "Yes",
145+ UnderlineIndex: 0, // "Y"
146+ Selected: c.selected == 0,
147+ },
148+ {
149+ Text: "No",
150+ UnderlineIndex: 0, // "N"
151+ Selected: c.selected == 1,
152+ },
153+ }
154+
155+ content := core.SelectableButtons(buttons, " ")
156+
157+ return baseStyle.AlignHorizontal(lipgloss.Right).Width(c.width - 4).Render(content)
158+}
159+
160+func (c *compactDialogCmp) renderContent() string {
161+ t := styles.CurrentTheme()
162+ baseStyle := t.S().Base
163+
164+ switch c.state {
165+ case stateConfirm:
166+ explanation := t.S().Text.
167+ Width(c.width - 4).
168+ 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.")
169+
170+ question := t.S().Text.
171+ Width(c.width - 4).
172+ Render("Do you want to continue?")
173+
174+ return baseStyle.Render(lipgloss.JoinVertical(
175+ lipgloss.Left,
176+ explanation,
177+ "",
178+ question,
179+ ))
180+ case stateCompacting:
181+ return baseStyle.Render(lipgloss.JoinVertical(
182+ lipgloss.Left,
183+ c.progress,
184+ "",
185+ "Please wait...",
186+ ))
187+ case stateError:
188+ return baseStyle.Render(lipgloss.JoinVertical(
189+ lipgloss.Left,
190+ c.progress,
191+ "",
192+ "Press Enter to close",
193+ ))
194+ }
195+ return ""
196+}
197+
198+func (c *compactDialogCmp) render() string {
199+ t := styles.CurrentTheme()
200+ baseStyle := t.S().Base
201+
202+ var title string
203+ switch c.state {
204+ case stateConfirm:
205+ title = "Compact Session"
206+ case stateCompacting:
207+ title = "Compacting Session"
208+ case stateError:
209+ title = "Compact Failed"
210+ }
211+
212+ titleView := core.Title(title, c.width-4)
213+ content := c.renderContent()
214+
215+ var dialogContent string
216+ if c.state == stateConfirm {
217+ buttons := c.renderButtons()
218+ dialogContent = lipgloss.JoinVertical(
219+ lipgloss.Top,
220+ titleView,
221+ "",
222+ content,
223+ "",
224+ buttons,
225+ "",
226+ )
227+ } else {
228+ dialogContent = lipgloss.JoinVertical(
229+ lipgloss.Top,
230+ titleView,
231+ "",
232+ content,
233+ "",
234+ )
235+ }
236+
237+ return baseStyle.
238+ Padding(0, 1).
239+ Border(lipgloss.RoundedBorder()).
240+ BorderForeground(t.BorderFocus).
241+ Width(c.width).
242+ Render(dialogContent)
243+}
244+
245+func (c *compactDialogCmp) View() tea.View {
246+ return tea.NewView(c.render())
247+}
248+
249+// SetSize sets the size of the component.
250+func (c *compactDialogCmp) SetSize() tea.Cmd {
251+ c.width = min(90, c.wWidth)
252+ c.height = min(15, c.wHeight)
253+ return nil
254+}
255+
256+func (c *compactDialogCmp) Position() (int, int) {
257+ row := (c.wHeight / 2) - (c.height / 2)
258+ col := (c.wWidth / 2) - (c.width / 2)
259+ return row, col
260+}
261+
262+// ID implements CompactDialog.
263+func (c *compactDialogCmp) ID() dialogs.DialogID {
264+ return CompactDialogID
265+}
266+
internal/tui/components/dialogs/compact/keys.gonew file+61
@@ -0,0 +1,61 @@
1+package compact
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/key"
5+ "github.com/charmbracelet/crush/internal/tui/layout"
6+)
7+
8+// KeyMap defines the key bindings for the compact dialog.
9+type KeyMap struct {
10+ ChangeSelection key.Binding
11+ Select key.Binding
12+ Y key.Binding
13+ N key.Binding
14+ Close key.Binding
15+}
16+
17+// DefaultKeyMap returns the default key bindings for the compact dialog.
18+func DefaultKeyMap() KeyMap {
19+ return KeyMap{
20+ ChangeSelection: key.NewBinding(
21+ key.WithKeys("tab", "left", "right", "h", "l"),
22+ key.WithHelp("tab/←/→", "toggle selection"),
23+ ),
24+ Select: key.NewBinding(
25+ key.WithKeys("enter"),
26+ key.WithHelp("enter", "confirm"),
27+ ),
28+ Y: key.NewBinding(
29+ key.WithKeys("y"),
30+ key.WithHelp("y", "yes"),
31+ ),
32+ N: key.NewBinding(
33+ key.WithKeys("n"),
34+ key.WithHelp("n", "no"),
35+ ),
36+ Close: key.NewBinding(
37+ key.WithKeys("esc"),
38+ key.WithHelp("esc", "cancel"),
39+ ),
40+ }
41+}
42+
43+// FullHelp implements help.KeyMap.
44+func (k KeyMap) FullHelp() [][]key.Binding {
45+ m := [][]key.Binding{}
46+ slice := layout.KeyMapToSlice(k)
47+ for i := 0; i < len(slice); i += 4 {
48+ end := min(i+4, len(slice))
49+ m = append(m, slice[i:end])
50+ }
51+ return m
52+}
53+
54+// ShortHelp implements help.KeyMap.
55+func (k KeyMap) ShortHelp() []key.Binding {
56+ return []key.Binding{
57+ k.ChangeSelection,
58+ k.Select,
59+ k.Close,
60+ }
61+}
internal/tui/components/dialogs/init/init.go+38−4
@@ -5,6 +5,8 @@ import (
55 tea "github.com/charmbracelet/bubbletea/v2"
66 "github.com/charmbracelet/lipgloss/v2"
77
8+ "github.com/charmbracelet/crush/internal/config"
9+ cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
810 "github.com/charmbracelet/crush/internal/tui/components/core"
911 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
1012 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -51,7 +53,7 @@ func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5153 case key.Matches(msg, m.keyMap.Close):
5254 return m, tea.Batch(
5355 util.CmdHandler(dialogs.CloseDialogMsg{}),
54− util.CmdHandler(CloseInitDialogMsg{Initialize: false}),
56+ m.handleInitialization(false),
5557 )
5658 case key.Matches(msg, m.keyMap.ChangeSelection):
5759 m.selected = (m.selected + 1) % 2
@@ -59,17 +61,17 @@ func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5961 case key.Matches(msg, m.keyMap.Select):
6062 return m, tea.Batch(
6163 util.CmdHandler(dialogs.CloseDialogMsg{}),
62− util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0}),
64+ m.handleInitialization(m.selected == 0),
6365 )
6466 case key.Matches(msg, m.keyMap.Y):
6567 return m, tea.Batch(
6668 util.CmdHandler(dialogs.CloseDialogMsg{}),
67− util.CmdHandler(CloseInitDialogMsg{Initialize: true}),
69+ m.handleInitialization(true),
6870 )
6971 case key.Matches(msg, m.keyMap.N):
7072 return m, tea.Batch(
7173 util.CmdHandler(dialogs.CloseDialogMsg{}),
72− util.CmdHandler(CloseInitDialogMsg{Initialize: false}),
74+ m.handleInitialization(false),
7375 )
7476 }
7577 }
@@ -168,6 +170,38 @@ func (m *initDialogCmp) Position() (int, int) {
168170 return row, col
169171}
170172
173+// handleInitialization handles the initialization logic when the dialog is closed.
174+func (m *initDialogCmp) handleInitialization(initialize bool) tea.Cmd {
175+ if initialize {
176+ // Run the initialization command
177+ prompt := `Please analyze this codebase and create a Crush.md file containing:
178+1. Build/lint/test commands - especially for running a single test
179+2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
180+
181+The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
182+If there's already a crush.md, improve it.
183+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
184+
185+ // Mark the project as initialized
186+ if err := config.MarkProjectInitialized(); err != nil {
187+ return util.ReportError(err)
188+ }
189+
190+ return tea.Sequence(
191+ util.CmdHandler(cmpChat.SessionClearedMsg{}),
192+ util.CmdHandler(cmpChat.SendMsg{
193+ Text: prompt,
194+ }),
195+ )
196+ } else {
197+ // Mark the project as initialized without running the command
198+ if err := config.MarkProjectInitialized(); err != nil {
199+ return util.ReportError(err)
200+ }
201+ }
202+ return nil
203+}
204+
171205// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
172206type CloseInitDialogMsg struct {
173207 Initialize bool
internal/tui/tui.go+36−26
@@ -7,6 +7,7 @@ import (
77 tea "github.com/charmbracelet/bubbletea/v2"
88 "github.com/charmbracelet/crush/internal/app"
99 "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/llm/agent"
1011 "github.com/charmbracelet/crush/internal/llm/tools"
1112 "github.com/charmbracelet/crush/internal/logging"
1213 "github.com/charmbracelet/crush/internal/permission"
@@ -16,6 +17,7 @@ import (
1617 "github.com/charmbracelet/crush/internal/tui/components/core/status"
1718 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
1819 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
20+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/compact"
1921 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2022 initDialog "github.com/charmbracelet/crush/internal/tui/components/dialogs/init"
2123 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
@@ -157,6 +159,12 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157159 Model: models.NewModelDialogCmp(),
158160 },
159161 )
162+ // Compact
163+ case commands.CompactMsg:
164+ return a, util.CmdHandler(dialogs.OpenDialogMsg{
165+ Model: compact.NewCompactDialogCmp(a.app.CoderAgent, msg.SessionID, true),
166+ })
167+
160168 // File Picker
161169 case chat.OpenFilePickerMsg:
162170 if a.dialog.ActiveDialogId() == filepicker.FilePickerID {
@@ -181,33 +189,35 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
181189 a.app.Permissions.Deny(msg.Permission)
182190 }
183191 return a, nil
184− // Init Dialog
185− case initDialog.CloseInitDialogMsg:
186− if msg.Initialize {
187− // Run the initialization command
188− prompt := `Please analyze this codebase and create a Crush.md file containing:
189−1. Build/lint/test commands - especially for running a single test
190−2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
191−
192−The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
193−If there's already a crush.md, improve it.
194−If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
195−
196− // Mark the project as initialized
197− if err := config.MarkProjectInitialized(); err != nil {
198− return a, util.ReportError(err)
199− }
200−
201− return a, util.CmdHandler(cmpChat.SendMsg{
202− Text: prompt,
203− })
204− } else {
205− // Mark the project as initialized without running the command
206− if err := config.MarkProjectInitialized(); err != nil {
207− return a, util.ReportError(err)
192+ // Agent Events
193+ case pubsub.Event[agent.AgentEvent]:
194+ payload := msg.Payload
195+
196+ // Forward agent events to dialogs
197+ if a.dialog.HasDialogs() && a.dialog.ActiveDialogId() == compact.CompactDialogID {
198+ u, dialogCmd := a.dialog.Update(payload)
199+ a.dialog = u.(dialogs.DialogCmp)
200+ cmds = append(cmds, dialogCmd)
201+ }
202+
203+ // Handle auto-compact logic
204+ if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSessionID != "" {
205+ // Get current session to check token usage
206+ session, err := a.app.Sessions.Get(context.Background(), a.selectedSessionID)
207+ if err == nil {
208+ model := a.app.CoderAgent.Model()
209+ contextWindow := model.ContextWindow
210+ tokens := session.CompletionTokens + session.PromptTokens
211+ if (tokens >= int64(float64(contextWindow)*0.95)) && config.Get().AutoCompact {
212+ // Show compact confirmation dialog
213+ cmds = append(cmds, util.CmdHandler(dialogs.OpenDialogMsg{
214+ Model: compact.NewCompactDialogCmp(a.app.CoderAgent, a.selectedSessionID, false),
215+ }))
216+ }
208217 }
209218 }
210− return a, nil
219+
220+ return a, tea.Batch(cmds...)
211221 // Key Press Messages
212222 case tea.KeyPressMsg:
213223 if msg.String() == "ctrl+t" {
@@ -296,7 +306,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
296306 return util.CmdHandler(dialogs.CloseDialogMsg{})
297307 }
298308 return util.CmdHandler(dialogs.OpenDialogMsg{
299− Model: commands.NewCommandDialog(),
309+ Model: commands.NewCommandDialog(a.selectedSessionID),
300310 })
301311 case key.Matches(msg, a.keyMap.Sessions):
302312 if a.dialog.ActiveDialogId() == sessions.SessionsDialogID {