66 "github.com/charmbracelet/bubbles/v2/textarea"
77 tea "github.com/charmbracelet/bubbletea/v2"
88
9+ "github.com/docker/cagent/pkg/history"
910 "github.com/docker/cagent/pkg/tui/core"
1011 "github.com/docker/cagent/pkg/tui/core/layout"
1112 "github.com/docker/cagent/pkg/tui/styles"
@@ -16,12 +17,21 @@ type SendMsg struct {
1617 Content string
1718}
1819
20+ // historyNavigation describes which direction we want to pull from history.
21+ type historyNavigation int
22+
23+ const (
24+ NAVIGATEPREVIOUS historyNavigation = iota
25+ NAVIGATENEXT
26+ )
27+
1928// Editor represents an input editor component
2029type Editor interface {
2130 layout.Model
2231 layout.Sizeable
2332 layout.Focusable
2433 layout.Help
34+ SetHistory (hist * history.History )
2535 SetWorking (working bool ) tea.Cmd
2636}
2737
@@ -31,6 +41,13 @@ type editor struct {
3141 width int
3242 height int
3343 working bool
44+
45+ // history is the shared command store backing up/down navigation.
46+ hist * history.History
47+ // draftInput holds the user's unsent text while they browse history.
48+ draftInput string
49+ // historyBrowsing marks that we're currently showing history entries.
50+ historyBrowsing bool
3451}
3552
3653// New creates a new editor component
@@ -70,14 +87,29 @@ func (e *editor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7087 }
7188 value := e .textarea .Value ()
7289 if value != "" && ! e .working {
90+ // Treat enter as send: clear input and exit history browse state.
7391 e .textarea .Reset ()
74- return e , core .CmdHandler (SendMsg {
75- Content : value ,
76- })
92+ e .endHistoryBrowse ()
93+ return e , core .CmdHandler (SendMsg {Content : value })
7794 }
7895 return e , nil
7996 case "ctrl+c" :
8097 return e , tea .Quit
98+ case "up" :
99+ // Consume the key when we replace the buffer with an older command.
100+ if e .navigateHistory (NAVIGATEPREVIOUS ) {
101+ return e , nil
102+ }
103+ case "down" :
104+ // Consume the key when we replace the buffer with a newer command.
105+ if e .navigateHistory (NAVIGATENEXT ) {
106+ return e , nil
107+ }
108+ default :
109+ // Any other key exits history browsing so input becomes fresh text.
110+ if e .historyBrowsing {
111+ e .endHistoryBrowse ()
112+ }
81113 }
82114 }
83115
@@ -141,3 +173,90 @@ func (e *editor) SetWorking(working bool) tea.Cmd {
141173 e .working = working
142174 return nil
143175}
176+
177+ func (e * editor ) SetHistory (hist * history.History ) {
178+ e .hist = hist
179+ }
180+
181+ func (e * editor ) navigateHistory (direction historyNavigation ) bool {
182+ // Returning true tells Update to stop Bubble Tea's default cursor handling,
183+ // because we've already replaced the textarea content for this key press.
184+ if ! e .canBrowseHistory () {
185+ return false
186+ }
187+
188+ if ! e .historyBrowsing {
189+ e .beginHistoryBrowse ()
190+ }
191+
192+ var entry string
193+ switch direction {
194+ case NAVIGATEPREVIOUS :
195+ // Up arrow walks toward older commands.
196+ entry = e .hist .Previous ()
197+ case NAVIGATENEXT :
198+ // Down arrow walks toward newer commands.
199+ entry = e .hist .Next ()
200+ if entry == "" {
201+ // Restore the draft when we step past the newest entry.
202+ e .restoreDraftFromHistory ()
203+ return true
204+ }
205+ default :
206+ return false
207+ }
208+
209+ if entry == "" {
210+ return true
211+ }
212+
213+ // Replace the input with the selected history entry.
214+ e .textarea .SetValue (entry )
215+ // Place the cursor at the end so the user can immediately append or send.
216+ e .textarea .MoveToEnd ()
217+ return true
218+ }
219+
220+ func (e * editor ) canBrowseHistory () bool {
221+ // We only take over arrow keys when there's at least one history entry and
222+ // the textarea is a single line (multi-line inputs retain normal movement).
223+ return e .hist != nil &&
224+ len (e .hist .Messages ) > 0 &&
225+ e .textarea .LineCount () == 1
226+ }
227+
228+ func (e * editor ) beginHistoryBrowse () {
229+ if e .hist == nil {
230+ return
231+ }
232+ // Capture the in-progress text so we can restore it after browsing.
233+ e .draftInput = e .textarea .Value ()
234+ e .historyBrowsing = true
235+ // Start from the newest entry so the first "up" pulls the latest command.
236+ e .moveHistoryCursorToLatest ()
237+ }
238+
239+ func (e * editor ) restoreDraftFromHistory () {
240+ e .textarea .SetValue (e .draftInput )
241+ e .textarea .MoveToEnd ()
242+ e .endHistoryBrowse ()
243+ }
244+
245+ func (e * editor ) endHistoryBrowse () {
246+ e .historyBrowsing = false
247+ e .draftInput = ""
248+ if e .hist == nil {
249+ return
250+ }
251+ e .moveHistoryCursorToLatest ()
252+ }
253+
254+ func (e * editor ) moveHistoryCursorToLatest () {
255+ if e .hist == nil {
256+ return
257+ }
258+ // Advance until Next returns empty, which positions the cursor just after
259+ // the most recent saved command.
260+ for e .hist .Next () != "" {
261+ }
262+ }
0 commit comments