Skip to content

Commit 12fc668

Browse files
committed
Use the messages component in the confirmation dialog
Makes the content of the tool call scrollable, which is nice. Signed-off-by: Djordje Lukic <[email protected]>
1 parent 03e37b7 commit 12fc668

File tree

3 files changed

+82
-72
lines changed

3 files changed

+82
-72
lines changed

pkg/tui/components/messages/messages.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ type Model interface {
5656
AddShellOutputMessage(content string) tea.Cmd
5757

5858
ScrollToBottom() tea.Cmd
59-
IsAtBottom() bool
6059
}
6160

6261
// renderedItem represents a cached rendered message with position information
@@ -129,6 +128,17 @@ func New(a *app.App, sessionState *service.SessionState) Model {
129128
}
130129
}
131130

131+
// NewScrollableView creates a simple scrollable view for displaying messages in dialogs
132+
// This is a lightweight version that doesn't require app or session state management
133+
func NewScrollableView(width, height int, sessionState *service.SessionState) Model {
134+
return &model{
135+
width: width,
136+
height: height,
137+
renderedItems: make(map[int]renderedItem),
138+
sessionState: sessionState,
139+
}
140+
}
141+
132142
// Init initializes the component
133143
func (m *model) Init() tea.Cmd {
134144
var cmds []tea.Cmd
@@ -396,7 +406,9 @@ func (m *model) shouldCacheMessage(index int) bool {
396406

397407
switch msg.Type {
398408
case types.MessageTypeToolCall:
399-
return msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError
409+
return msg.ToolStatus == types.ToolStatusCompleted ||
410+
msg.ToolStatus == types.ToolStatusError ||
411+
msg.ToolStatus == types.ToolStatusConfirmation
400412
case types.MessageTypeToolResult:
401413
return true
402414
case types.MessageTypeAssistant, types.MessageTypeAssistantReasoning:
@@ -486,8 +498,8 @@ func (m *model) invalidateAllItems() {
486498
m.totalHeight = 0
487499
}
488500

489-
// IsAtBottom returns true if the viewport is at the bottom
490-
func (m *model) IsAtBottom() bool {
501+
// isAtBottom returns true if the viewport is at the bottom
502+
func (m *model) isAtBottom() bool {
491503
if len(m.messages) == 0 {
492504
return true
493505
}
@@ -497,11 +509,6 @@ func (m *model) IsAtBottom() bool {
497509
return m.scrollOffset >= maxScrollOffset
498510
}
499511

500-
// isAtBottom is kept as a private method for internal use
501-
func (m *model) isAtBottom() bool {
502-
return m.IsAtBottom()
503-
}
504-
505512
// AddUserMessage adds a user message to the chat
506513
func (m *model) AddUserMessage(content string) tea.Cmd {
507514
return m.addMessage(&types.Message{

pkg/tui/dialog/tool_confirmation.go

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import (
88
"charm.land/lipgloss/v2"
99

1010
"github.com/docker/cagent/pkg/runtime"
11-
"github.com/docker/cagent/pkg/tui/components/markdown"
12-
"github.com/docker/cagent/pkg/tui/components/tool"
11+
"github.com/docker/cagent/pkg/tui/components/messages"
1312
"github.com/docker/cagent/pkg/tui/core"
1413
"github.com/docker/cagent/pkg/tui/core/layout"
1514
"github.com/docker/cagent/pkg/tui/service"
@@ -33,12 +32,41 @@ type toolConfirmationDialog struct {
3332
msg *runtime.ToolCallConfirmationEvent
3433
keyMap toolConfirmationKeyMap
3534
sessionState *service.SessionState
35+
scrollView messages.Model
3636
}
3737

3838
// SetSize implements [Dialog].
3939
func (d *toolConfirmationDialog) SetSize(width, height int) tea.Cmd {
4040
d.width = width
4141
d.height = height
42+
43+
// Calculate dialog dimensions
44+
dialogWidth := width * 70 / 100
45+
contentWidth := dialogWidth - 6
46+
maxDialogHeight := (height * 80) / 100
47+
48+
titleStyle := styles.DialogTitleStyle.Width(contentWidth)
49+
title := titleStyle.Render("Tool Confirmation")
50+
titleHeight := lipgloss.Height(title)
51+
52+
separatorWidth := max(contentWidth-10, 20)
53+
separator := styles.DialogSeparatorStyle.
54+
Align(lipgloss.Center).
55+
Width(contentWidth).
56+
Render(strings.Repeat("─", separatorWidth))
57+
separatorHeight := lipgloss.Height(separator)
58+
59+
question := styles.DialogQuestionStyle.Width(contentWidth).Render("Do you want to allow this tool call?")
60+
questionHeight := lipgloss.Height(question)
61+
62+
options := styles.DialogOptionsStyle.Width(contentWidth).Render("[Y]es [N]o [A]ll (approve all tools this session)")
63+
optionsHeight := lipgloss.Height(options)
64+
65+
// Calculate available height for scroll view
66+
// Total = maxDialogHeight - title - separator - 2 empty lines - question - empty line - options - 4 (dialog padding/border)
67+
availableHeight := max(maxDialogHeight-titleHeight-separatorHeight-2-questionHeight-1-optionsHeight-4, 5)
68+
d.scrollView.SetSize(contentWidth, availableHeight)
69+
4270
return nil
4371
}
4472

@@ -69,16 +97,28 @@ func defaultToolConfirmationKeyMap() toolConfirmationKeyMap {
6997

7098
// NewToolConfirmationDialog creates a new tool confirmation dialog
7199
func NewToolConfirmationDialog(msg *runtime.ToolCallConfirmationEvent, sessionState *service.SessionState) Dialog {
100+
// Create scrollable view with initial size (will be updated in SetSize)
101+
scrollView := messages.NewScrollableView(100, 20, sessionState)
102+
103+
// Add the tool call message to the view
104+
scrollView.AddOrUpdateToolCall(
105+
"", // agentName - empty for dialog context
106+
msg.ToolCall,
107+
msg.ToolDefinition,
108+
types.ToolStatusConfirmation,
109+
)
110+
72111
return &toolConfirmationDialog{
73112
msg: msg,
74113
sessionState: sessionState,
75114
keyMap: defaultToolConfirmationKeyMap(),
115+
scrollView: scrollView,
76116
}
77117
}
78118

79119
// Init initializes the tool confirmation dialog
80120
func (d *toolConfirmationDialog) Init() tea.Cmd {
81-
return nil
121+
return d.scrollView.Init()
82122
}
83123

84124
// Update handles messages for the tool confirmation dialog
@@ -87,7 +127,8 @@ func (d *toolConfirmationDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
87127
case tea.WindowSizeMsg:
88128
d.width = msg.Width
89129
d.height = msg.Height
90-
return d, nil
130+
cmd := d.SetSize(msg.Width, msg.Height)
131+
return d, cmd
91132

92133
case tea.KeyPressMsg:
93134
switch {
@@ -102,6 +143,20 @@ func (d *toolConfirmationDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
102143
if msg.String() == "ctrl+c" {
103144
return d, tea.Quit
104145
}
146+
147+
// Forward scrolling keys to the scroll view
148+
switch msg.String() {
149+
case "up", "k", "down", "j", "pgup", "pgdown", "home", "end":
150+
updatedScrollView, cmd := d.scrollView.Update(msg)
151+
d.scrollView = updatedScrollView.(messages.Model)
152+
return d, cmd
153+
}
154+
155+
case tea.MouseWheelMsg:
156+
// Forward mouse wheel events to scroll view
157+
updatedScrollView, cmd := d.scrollView.Update(msg)
158+
d.scrollView = updatedScrollView.(messages.Model)
159+
return d, cmd
105160
}
106161

107162
return d, nil
@@ -114,9 +169,6 @@ func (d *toolConfirmationDialog) View() string {
114169
// Content width (accounting for padding and borders)
115170
contentWidth := dialogWidth - 6
116171

117-
// Calculate max height (80% of screen height)
118-
maxDialogHeight := (d.height * 80) / 100
119-
120172
dialogStyle := styles.DialogStyle.Width(dialogWidth)
121173

122174
// Title
@@ -130,15 +182,8 @@ func (d *toolConfirmationDialog) View() string {
130182
Width(contentWidth).
131183
Render(strings.Repeat("─", separatorWidth))
132184

133-
a := types.Message{
134-
ToolCall: d.msg.ToolCall,
135-
ToolDefinition: d.msg.ToolDefinition,
136-
Type: types.MessageTypeToolCall,
137-
ToolStatus: types.ToolStatusConfirmation,
138-
}
139-
view := tool.New(&a, markdown.NewRenderer(contentWidth), d.sessionState)
140-
view.SetSize(contentWidth, 0)
141-
argumentsSection := view.View()
185+
// Get scrollable tool call view
186+
argumentsSection := d.scrollView.View()
142187

143188
question := styles.DialogQuestionStyle.Width(contentWidth).Render("Do you want to allow this tool call?")
144189
options := styles.DialogOptionsStyle.Width(contentWidth).Render("[Y]es [N]o [A]ll (approve all tools this session)")
@@ -154,45 +199,9 @@ func (d *toolConfirmationDialog) View() string {
154199

155200
content := lipgloss.JoinVertical(lipgloss.Left, parts...)
156201

157-
// Apply max height constraint if needed
158-
contentHeight := lipgloss.Height(content)
159-
if contentHeight > maxDialogHeight-4 { // Account for dialog padding/border
160-
// Limit the arguments section height
161-
availableHeight := maxDialogHeight - 4 - lipgloss.Height(title) - lipgloss.Height(separator) - lipgloss.Height(question) - lipgloss.Height(options) - 4 // spacing
162-
if availableHeight > 0 && argumentsSection != "" {
163-
argumentsSection = d.truncateToHeight(argumentsSection, availableHeight)
164-
165-
// Rebuild content with truncated arguments
166-
parts = []string{title, separator}
167-
if argumentsSection != "" {
168-
parts = append(parts, "", argumentsSection)
169-
}
170-
parts = append(parts, "", question, "", options)
171-
content = lipgloss.JoinVertical(lipgloss.Left, parts...)
172-
}
173-
}
174-
175202
return dialogStyle.Render(content)
176203
}
177204

178-
// truncateToHeight truncates content to fit within the specified height,
179-
// adding an ellipsis indicator at the end
180-
func (d *toolConfirmationDialog) truncateToHeight(content string, maxHeight int) string {
181-
if maxHeight <= 0 {
182-
return ""
183-
}
184-
185-
lines := strings.Split(content, "\n")
186-
if len(lines) <= maxHeight {
187-
return content
188-
}
189-
190-
// Reserve last line for truncation indicator
191-
truncatedLines := lines[:maxHeight-1]
192-
truncatedLines = append(truncatedLines, styles.MutedStyle.Render("... (content truncated)"))
193-
return strings.Join(truncatedLines, "\n")
194-
}
195-
196205
// Position calculates the position to center the dialog
197206
func (d *toolConfirmationDialog) Position() (row, col int) {
198207
dialogWidth := d.width * 70 / 100

pkg/tui/page/chat/chat.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,39 +214,33 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
214214
// Runtime events
215215
case *runtime.ErrorEvent:
216216
cmd := p.messages.AddErrorMessage(msg.Error)
217-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
217+
return p, cmd
218218
case *runtime.ShellOutputEvent:
219219
cmd := p.messages.AddShellOutputMessage(msg.Output)
220-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
220+
return p, cmd
221221
case *runtime.WarningEvent:
222222
cmd := core.CmdHandler(notification.ShowMsg{Text: msg.Message, Type: notification.TypeWarning})
223-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
223+
return p, cmd
224224
case *runtime.UserMessageEvent:
225225
cmd := p.messages.AddUserMessage(msg.Message)
226-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
226+
return p, cmd
227227
case *runtime.StreamStartedEvent:
228228
p.streamCancelled = false
229229
spinnerCmd := p.setWorking(true)
230230
cmd := p.messages.AddAssistantMessage()
231231
p.startProgressBar()
232-
return p, tea.Batch(cmd, p.messages.ScrollToBottom(), spinnerCmd)
232+
return p, tea.Batch(cmd, spinnerCmd)
233233
case *runtime.AgentChoiceEvent:
234234
if p.streamCancelled {
235235
return p, nil
236236
}
237237
cmd := p.messages.AppendToLastMessage(msg.AgentName, types.MessageTypeAssistant, msg.Content)
238-
if p.messages.IsAtBottom() {
239-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
240-
}
241238
return p, cmd
242239
case *runtime.AgentChoiceReasoningEvent:
243240
if p.streamCancelled {
244241
return p, nil
245242
}
246243
cmd := p.messages.AppendToLastMessage(msg.AgentName, types.MessageTypeAssistantReasoning, msg.Content)
247-
if p.messages.IsAtBottom() {
248-
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
249-
}
250244
return p, cmd
251245
case *runtime.TokenUsageEvent:
252246
p.sidebar.SetTokenUsage(msg.Usage)

0 commit comments

Comments
 (0)