diff --git a/pkg/tui/commands/commands.go b/pkg/tui/commands/commands.go index 77d0d9d73..267605c5b 100644 --- a/pkg/tui/commands/commands.go +++ b/pkg/tui/commands/commands.go @@ -36,6 +36,16 @@ type Item struct { func builtInSessionCommands() []Item { cmds := []Item{ + { + ID: "session.clear", + Label: "Clear", + SlashCommand: "/clear", + Description: "Clear the current tab and start a new session", + Category: "Session", + Execute: func(string) tea.Cmd { + return core.CmdHandler(messages.ClearSessionMsg{}) + }, + }, { ID: "session.attach", Label: "Attach", diff --git a/pkg/tui/commands/commands_test.go b/pkg/tui/commands/commands_test.go index 945b1367b..3113b8e4a 100644 --- a/pkg/tui/commands/commands_test.go +++ b/pkg/tui/commands/commands_test.go @@ -71,6 +71,15 @@ func TestParseSlashCommand_OtherCommands(t *testing.T) { assert.True(t, ok) }) + t.Run("clear command", func(t *testing.T) { + t.Parallel() + cmd := ParseSlashCommand("/clear") + require.NotNil(t, cmd) + msg := cmd() + _, ok := msg.(messages.ClearSessionMsg) + assert.True(t, ok) + }) + t.Run("star command", func(t *testing.T) { t.Parallel() cmd := ParseSlashCommand("/star") diff --git a/pkg/tui/messages/session.go b/pkg/tui/messages/session.go index fbce752ce..8b3ce855e 100644 --- a/pkg/tui/messages/session.go +++ b/pkg/tui/messages/session.go @@ -25,6 +25,10 @@ type ( // NewSessionMsg requests creation of a new session. NewSessionMsg struct{} + // ClearSessionMsg resets the current tab and starts a new session + // in the same working directory. + ClearSessionMsg struct{} + // ExitSessionMsg requests exiting the current session. ExitSessionMsg struct{} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 09f7522a1..0d4d782e8 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -702,6 +702,10 @@ func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // /new spawns a new tab when a session spawner is configured. return m.handleSpawnSession("") + case messages.ClearSessionMsg: + // /clear resets the current tab with a fresh session in the same working dir. + return m.handleClearSession() + // --- Exit --- case messages.ExitSessionMsg: @@ -1116,6 +1120,46 @@ func (m *appModel) replaceActiveSession(ctx context.Context, sess *session.Sessi return m, cmd } +// handleClearSession resets the current tab by creating a fresh session +// in the same working directory. +func (m *appModel) handleClearSession() (tea.Model, tea.Cmd) { + activeID := m.supervisor.ActiveID() + + // Cleanup old editor for the active session. + if ed, ok := m.editors[activeID]; ok { + ed.Cleanup() + } + + // Create a fresh session in the same app, preserving the working dir. + m.application.NewSession() + newSess := m.application.Session() + + // Rebuild all per-session UI components. + m.initSessionComponents(activeID, m.application, newSess) + m.dialogMgr = dialog.New() + m.supervisor.SetRunnerTitle(activeID, "") + m.sessionState.SetSessionTitle("") + m.sessionState.SetPreviousMessage(nil) + + // Update persisted tab to point to the new session. + if m.tuiStore != nil { + ctx := context.Background() + oldPersistedID := m.persistedSessionID(activeID) + if err := m.tuiStore.UpdateTabSessionID(ctx, oldPersistedID, newSess.ID); err != nil { + slog.Warn("Failed to update tab session ID after clear", "error", err) + } + } + m.persistActiveTab(newSess.ID) + + m.reapplyKeyboardEnhancements() + + return m, tea.Sequence( + m.chatPage.Init(), + m.resizeAll(), + m.editor.Focus(), + ) +} + // handleSpawnSession spawns a new session. func (m *appModel) handleSpawnSession(workingDir string) (tea.Model, tea.Cmd) { // If no working dir specified, open the picker