@@ -10,14 +10,17 @@ import (
10 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/lipgloss"
11 "github.com/opencode-ai/opencode/internal/app"
11 "github.com/opencode-ai/opencode/internal/app"
12 "github.com/opencode-ai/opencode/internal/config"
12 "github.com/opencode-ai/opencode/internal/config"
13+ "github.com/opencode-ai/opencode/internal/llm/agent"
13 "github.com/opencode-ai/opencode/internal/logging"
14 "github.com/opencode-ai/opencode/internal/logging"
14 "github.com/opencode-ai/opencode/internal/permission"
15 "github.com/opencode-ai/opencode/internal/permission"
15 "github.com/opencode-ai/opencode/internal/pubsub"
16 "github.com/opencode-ai/opencode/internal/pubsub"
17+ "github.com/opencode-ai/opencode/internal/session"
16 "github.com/opencode-ai/opencode/internal/tui/components/chat"
18 "github.com/opencode-ai/opencode/internal/tui/components/chat"
17 "github.com/opencode-ai/opencode/internal/tui/components/core"
19 "github.com/opencode-ai/opencode/internal/tui/components/core"
18 "github.com/opencode-ai/opencode/internal/tui/components/dialog"
20 "github.com/opencode-ai/opencode/internal/tui/components/dialog"
19 "github.com/opencode-ai/opencode/internal/tui/layout"
21 "github.com/opencode-ai/opencode/internal/tui/layout"
20 "github.com/opencode-ai/opencode/internal/tui/page"
22 "github.com/opencode-ai/opencode/internal/tui/page"
23+ "github.com/opencode-ai/opencode/internal/tui/theme"
21 "github.com/opencode-ai/opencode/internal/tui/util"
24 "github.com/opencode-ai/opencode/internal/tui/util"
@@ -32,6 +35,8 @@ type keyMap struct {
32 SwitchTheme key.Binding
35 SwitchTheme key.Binding
38+type startCompactSessionMsg struct{}
36 quitKey = "q"
41 quitKey = "q"
@@ -91,13 +96,14 @@ var logsKeyReturnKey = key.NewBinding(
93type appModel struct {
98type appModel struct {
94− width, height int
99+ width, height int
95− currentPage page.PageID
100+ currentPage page.PageID
96− previousPage page.PageID
101+ previousPage page.PageID
97− pages map[page.PageID]tea.Model
102+ pages map[page.PageID]tea.Model
98− loadedPages map[page.PageID]bool
103+ loadedPages map[page.PageID]bool
99− status core.StatusCmp
104+ status core.StatusCmp
100− app *app.App
105+ app *app.App
106+ selectedSession session.Session
102 showPermissions bool
108 showPermissions bool
103 permissions dialog.PermissionDialogCmp
109 permissions dialog.PermissionDialogCmp
@@ -126,9 +132,12 @@ type appModel struct {
127 showThemeDialog bool
133 showThemeDialog bool
128 themeDialog dialog.ThemeDialog
134 themeDialog dialog.ThemeDialog
130 showArgumentsDialog bool
136 showArgumentsDialog bool
131 argumentsDialog dialog.ArgumentsDialogCmp
137 argumentsDialog dialog.ArgumentsDialogCmp
140+ compactingMessage string
134func (a appModel) Init() tea.Cmd {
143func (a appModel) Init() tea.Cmd {
@@ -151,6 +160,7 @@ func (a appModel) Init() tea.Cmd {
151 cmd = a.initDialog.Init()
160 cmd = a.initDialog.Init()
152 cmds = append(cmds, cmd)
161 cmds = append(cmds, cmd)
153 cmd = a.filepicker.Init()
162 cmd = a.filepicker.Init()
163+ cmds = append(cmds, cmd)
154 cmd = a.themeDialog.Init()
164 cmd = a.themeDialog.Init()
155 cmds = append(cmds, cmd)
165 cmds = append(cmds, cmd)
@@ -203,7 +213,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
203 cmds = append(cmds, filepickerCmd)
213 cmds = append(cmds, filepickerCmd)
205 a.initDialog.SetSize(msg.Width, msg.Height)
215 a.initDialog.SetSize(msg.Width, msg.Height)
207 if a.showArgumentsDialog {
217 if a.showArgumentsDialog {
208 a.argumentsDialog.SetSize(msg.Width, msg.Height)
218 a.argumentsDialog.SetSize(msg.Width, msg.Height)
209 args, argsCmd := a.argumentsDialog.Update(msg)
219 args, argsCmd := a.argumentsDialog.Update(msg)
@@ -293,6 +303,70 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
293 a.showCommandDialog = false
303 a.showCommandDialog = false
294 return a, nil
304 return a, nil
306+ case startCompactSessionMsg:
307+ // Start compacting the current session
308+ a.isCompacting = true
309+ a.compactingMessage = "Starting summarization..."
311+ if a.selectedSession.ID == "" {
312+ a.isCompacting = false
313+ return a, util.ReportWarn("No active session to summarize")
316+ // Start the summarization process
317+ return a, func() tea.Msg {
318+ ctx := context.Background()
319+ a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID)
323+ case pubsub.Event[agent.AgentEvent]:
324+ payload := msg.Payload
325+ if payload.Error != nil {
326+ a.isCompacting = false
327+ return a, util.ReportError(payload.Error)
330+ a.compactingMessage = payload.Progress
332+ if payload.Done && payload.Type == agent.AgentEventTypeSummarize {
333+ a.isCompacting = false
335+ if payload.SessionID != "" {
336+ // Switch to the new session
337+ return a, func() tea.Msg {
338+ sessions, err := a.app.Sessions.List(context.Background())
340+ return util.InfoMsg{
341+ Type: util.InfoTypeError,
342+ Msg: "Failed to list sessions: " + err.Error(),
346+ for _, s := range sessions {
347+ if s.ID == payload.SessionID {
348+ return dialog.SessionSelectedMsg{Session: s}
352+ return util.InfoMsg{
353+ Type: util.InfoTypeError,
354+ Msg: "Failed to find new session",
358+ return a, util.ReportInfo("Session summarization complete")
359+ } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" {
360+ model := a.app.CoderAgent.Model()
361+ contextWindow := model.ContextWindow
362+ tokens := a.selectedSession.CompletionTokens + a.selectedSession.PromptTokens
363+ if (tokens >= int64(float64(contextWindow)*0.95)) && config.Get().AutoCompact {
364+ return a, util.CmdHandler(startCompactSessionMsg{})
367+ // Continue listening for events
296 case dialog.CloseThemeDialogMsg:
370 case dialog.CloseThemeDialogMsg:
297 a.showThemeDialog = false
371 a.showThemeDialog = false
298 return a, nil
372 return a, nil
@@ -342,7 +416,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
342 return a, nil
416 return a, nil
344 case chat.SessionSelectedMsg:
418 case chat.SessionSelectedMsg:
419+ a.selectedSession = msg
345 a.sessionDialog.SetSelectedSession(msg.ID)
420 a.sessionDialog.SetSelectedSession(msg.ID)
422+ case pubsub.Event[session.Session]:
423+ if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == a.selectedSession.ID {
424+ a.selectedSession = msg.Payload
346 case dialog.SessionSelectedMsg:
426 case dialog.SessionSelectedMsg:
347 a.showSessionDialog = false
427 a.showSessionDialog = false
348 if a.currentPage == page.ChatPage {
428 if a.currentPage == page.ChatPage {
@@ -357,22 +437,22 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
357 return a, msg.Command.Handler(msg.Command)
437 return a, msg.Command.Handler(msg.Command)
359 return a, util.ReportInfo("Command selected: " + msg.Command.Title)
439 return a, util.ReportInfo("Command selected: " + msg.Command.Title)
361 case dialog.ShowArgumentsDialogMsg:
441 case dialog.ShowArgumentsDialogMsg:
362 // Show arguments dialog
442 // Show arguments dialog
363 a.argumentsDialog = dialog.NewArgumentsDialogCmp(msg.CommandID, msg.Content)
443 a.argumentsDialog = dialog.NewArgumentsDialogCmp(msg.CommandID, msg.Content)
364 a.showArgumentsDialog = true
444 a.showArgumentsDialog = true
365 return a, a.argumentsDialog.Init()
445 return a, a.argumentsDialog.Init()
367 case dialog.CloseArgumentsDialogMsg:
447 case dialog.CloseArgumentsDialogMsg:
368 // Close arguments dialog
448 // Close arguments dialog
369 a.showArgumentsDialog = false
449 a.showArgumentsDialog = false
371 // If submitted, replace $ARGUMENTS and run the command
451 // If submitted, replace $ARGUMENTS and run the command
372 if msg.Submit {
452 if msg.Submit {
373 // Replace $ARGUMENTS with the provided arguments
453 // Replace $ARGUMENTS with the provided arguments
374 content := strings.ReplaceAll(msg.Content, "$ARGUMENTS", msg.Arguments)
454 content := strings.ReplaceAll(msg.Content, "$ARGUMENTS", msg.Arguments)
376 // Execute the command with arguments
456 // Execute the command with arguments
377 return a, util.CmdHandler(dialog.CommandRunCustomMsg{
457 return a, util.CmdHandler(dialog.CommandRunCustomMsg{
378 Content: content,
458 Content: content,
@@ -387,7 +467,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
387 a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
467 a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
388 return a, cmd
468 return a, cmd
393 case key.Matches(msg, keys.Quit):
473 case key.Matches(msg, keys.Quit):
@@ -606,6 +686,15 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) {
606 a.commands = append(a.commands, cmd)
686 a.commands = append(a.commands, cmd)
689+func (a *appModel) findCommand(id string) (dialog.Command, bool) {
690+ for _, cmd := range a.commands {
695+ return dialog.Command{}, false
609func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
698func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
610 if a.app.CoderAgent.IsBusy() {
699 if a.app.CoderAgent.IsBusy() {
611 // For now we don't move to any page if the agent is busy
700 // For now we don't move to any page if the agent is busy
@@ -668,10 +757,29 @@ func (a appModel) View() string {
671− if !a.app.CoderAgent.IsBusy() {
760+ // Show compacting status overlay
672− a.status.SetHelpWidgetMsg("ctrl+? help")
761+ if a.isCompacting {
673− } else {
762+ t := theme.CurrentTheme()
674− a.status.SetHelpWidgetMsg("? help")
763+ style := lipgloss.NewStyle().
764+ Border(lipgloss.RoundedBorder()).
765+ BorderForeground(t.BorderFocused()).
766+ BorderBackground(t.Background()).
768+ Background(t.Background()).
769+ Foreground(t.Text())
771+ overlay := style.Render("Summarizing\n" + a.compactingMessage)
772+ row := lipgloss.Height(appView) / 2
773+ row -= lipgloss.Height(overlay) / 2
774+ col := lipgloss.Width(appView) / 2
775+ col -= lipgloss.Width(overlay) / 2
776+ appView = layout.PlaceOverlay(
677 if a.showHelp {
785 if a.showHelp {
@@ -789,7 +897,7 @@ func (a appModel) View() string {
793 if a.showArgumentsDialog {
901 if a.showArgumentsDialog {
794 overlay := a.argumentsDialog.View()
902 overlay := a.argumentsDialog.View()
795 row := lipgloss.Height(appView) / 2
903 row := lipgloss.Height(appView) / 2
@@ -850,7 +958,17 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
962+ model.RegisterCommand(dialog.Command{
964+ Title: "Compact Session",
965+ Description: "Summarize the current session and create a new one with the summary",
966+ Handler: func(cmd dialog.Command) tea.Cmd {
967+ return func() tea.Msg {
968+ return startCompactSessionMsg{}
854 // Load custom commands
972 // Load custom commands
855 customCommands, err := dialog.LoadCustomCommands()
973 customCommands, err := dialog.LoadCustomCommands()
856 if err != nil {
974 if err != nil {
@@ -860,6 +978,6 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
860 model.RegisterCommand(cmd)
978 model.RegisterCommand(cmd)
864 return model
982 return model