@@ -211,6 +211,13 @@ type UI struct {
211211
212212 // mouse highlighting related state
213213 lastClickTime time.Time
214+
215+ // Prompt history for up/down navigation through previous messages.
216+ promptHistory struct {
217+ messages []string
218+ index int
219+ draft string
220+ }
214221}
215222
216223// New creates a new instance of the [UI] model.
@@ -307,6 +314,8 @@ func (m *UI) Init() tea.Cmd {
307314 }
308315 // load the user commands async
309316 cmds = append(cmds, m.loadCustomCommands())
317+ // load prompt history async
318+ cmds = append(cmds, m.loadPromptHistory())
310319 return tea.Batch(cmds...)
311320}
312321
@@ -390,6 +399,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
390399 }
391400 m.updateLayoutAndSize()
392401 }
402+ // Reload prompt history for the new session.
403+ m.historyReset()
404+ cmds = append(cmds, m.loadPromptHistory())
393405
394406 case sendMessageMsg:
395407 cmds = append(cmds, m.sendMessage(msg.Content, msg.Attachments...))
@@ -417,13 +429,20 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
417429 commands.SetMCPPrompts(m.mcpPrompts)
418430 }
419431
432+ case promptHistoryLoadedMsg:
433+ m.promptHistory.messages = msg.messages
434+ m.promptHistory.index = -1
435+ m.promptHistory.draft = ""
436+
420437 case closeDialogMsg:
421438 m.dialog.CloseFrontDialog()
422439
423440 case pubsub.Event[session.Session]:
424441 if msg.Type == pubsub.DeletedEvent {
425442 if m.session != nil && m.session.ID == msg.Payload.ID {
426− m.newSession()
443+ if cmd := m.newSession(); cmd != nil {
444+ cmds = append(cmds, cmd)
445+ }
427446 }
428447 break
429448 }
@@ -1095,7 +1114,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
10951114 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
10961115 break
10971116 }
1098− m.newSession()
1117+ if cmd := m.newSession(); cmd != nil {
1118+ cmds = append(cmds, cmd)
1119+ }
10991120 m.dialog.CloseDialog(dialog.CommandsID)
11001121 case dialog.ActionSummarize:
11011122 if m.isAgentBusy() {
@@ -1494,8 +1515,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
14941515 }
14951516
14961517 m.randomizePlaceholders()
1518+ m.historyReset()
14971519
1498− return m.sendMessage(value, attachments...)
1520+ return tea.Batch(m.sendMessage(value, attachments...), m.loadPromptHistory())
14991521 case key.Matches(msg, m.keyMap.Chat.NewSession):
15001522 if !m.hasSession() {
15011523 break
@@ -1504,7 +1526,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15041526 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
15051527 break
15061528 }
1507− m.newSession()
1529+ if cmd := m.newSession(); cmd != nil {
1530+ cmds = append(cmds, cmd)
1531+ }
15081532 case key.Matches(msg, m.keyMap.Tab):
15091533 if m.state != uiLanding {
15101534 m.setState(m.state, uiFocusMain)
@@ -1524,6 +1548,21 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15241548 ta, cmd := m.textarea.Update(msg)
15251549 m.textarea = ta
15261550 cmds = append(cmds, cmd)
1551+ case key.Matches(msg, m.keyMap.Editor.HistoryPrev):
1552+ cmd := m.handleHistoryUp(msg)
1553+ if cmd != nil {
1554+ cmds = append(cmds, cmd)
1555+ }
1556+ case key.Matches(msg, m.keyMap.Editor.HistoryNext):
1557+ cmd := m.handleHistoryDown(msg)
1558+ if cmd != nil {
1559+ cmds = append(cmds, cmd)
1560+ }
1561+ case key.Matches(msg, m.keyMap.Editor.Escape):
1562+ cmd := m.handleHistoryEscape(msg)
1563+ if cmd != nil {
1564+ cmds = append(cmds, cmd)
1565+ }
15271566 default:
15281567 if handleGlobalKeys(msg) {
15291568 // Handle global keys first before passing to textarea.
@@ -1557,6 +1596,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15571596 m.textarea = ta
15581597 cmds = append(cmds, cmd)
15591598
1599+ // Any text modification becomes the current draft.
1600+ m.updateHistoryDraft(curValue)
1601+
15601602 // After updating textarea, check if we need to filter completions.
15611603 // Skip filtering on the initial @ keystroke since items are loading async.
15621604 if m.completionsOpen && msg.String() != "@" {
@@ -1596,7 +1638,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15961638 break
15971639 }
15981640 m.focus = uiFocusEditor
1599− m.newSession()
1641+ if cmd := m.newSession(); cmd != nil {
1642+ cmds = append(cmds, cmd)
1643+ }
16001644 case key.Matches(msg, m.keyMap.Chat.Expand):
16011645 m.chat.ToggleExpandedSelectedItem()
16021646 case key.Matches(msg, m.keyMap.Chat.Up):
@@ -2772,9 +2816,10 @@ func (m *UI) handlePermissionNotification(notification permission.PermissionNoti
27722816
27732817// newSession clears the current session state and prepares for a new session.
27742818// The actual session creation happens when the user sends their first message.
2775−func (m *UI) newSession() {
2819+// Returns a command to reload prompt history.
2820+func (m *UI) newSession() tea.Cmd {
27762821 if !m.hasSession() {
2777− return
2822+ return nil
27782823 }
27792824
27802825 m.session = nil
@@ -2786,6 +2831,8 @@ func (m *UI) newSession() {
27862831 m.pillsExpanded = false
27872832 m.promptQueue = 0
27882833 m.pillsView = ""
2834+ m.historyReset()
2835+ return m.loadPromptHistory()
27892836}
27902837
27912838// handlePasteMsg handles a paste message.