diff options
| author | adamdotdevin <[email protected]> | 2025-07-31 09:34:43 -0500 |
|---|---|---|
| committer | adamdotdevin <[email protected]> | 2025-07-31 09:59:17 -0500 |
| commit | 5500698734c5119ccd7b0d9ab504bb3a53e5e39d (patch) | |
| tree | 6e77b461a2a488aab213355a5fb47c7b304bce78 /packages/tui/internal/components | |
| parent | e7631763f3e42292d3cecf14c0ebf295d9b9b333 (diff) | |
| download | opencode-5500698734c5119ccd7b0d9ab504bb3a53e5e39d.tar.gz opencode-5500698734c5119ccd7b0d9ab504bb3a53e5e39d.zip | |
wip: tui permissions
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 13 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/message.go | 126 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 53 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/session.go | 2 |
4 files changed, 159 insertions, 35 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 009a7ab6c..677d903fd 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -344,9 +344,13 @@ func (m *editorComponent) Content() string { hint = base(keyText+" again") + muted(" to exit") } else if m.app.IsBusy() { keyText := m.getInterruptKeyText() - if m.interruptKeyInDebounce { + status := "working" + if m.app.CurrentPermission.ID != "" { + status = "waiting for permission" + } + if m.interruptKeyInDebounce && m.app.CurrentPermission.ID == "" { hint = muted( - "working", + status, ) + m.spinner.View() + muted( " ", ) + base( @@ -355,7 +359,10 @@ func (m *editorComponent) Content() string { " interrupt", ) } else { - hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt") + hint = muted(status) + m.spinner.View() + if m.app.CurrentPermission.ID == "" { + hint += muted(" ") + base(keyText) + muted(" interrupt") + } } } diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 5c92dee5c..10e1b0692 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -3,6 +3,7 @@ package chat import ( "encoding/json" "fmt" + "maps" "slices" "strings" "time" @@ -22,16 +23,17 @@ import ( ) type blockRenderer struct { - textColor compat.AdaptiveColor - border bool - borderColor *compat.AdaptiveColor - borderColorRight bool - paddingTop int - paddingBottom int - paddingLeft int - paddingRight int - marginTop int - marginBottom int + textColor compat.AdaptiveColor + border bool + borderColor *compat.AdaptiveColor + borderLeft bool + borderRight bool + paddingTop int + paddingBottom int + paddingLeft int + paddingRight int + marginTop int + marginBottom int } type renderingOption func(*blockRenderer) @@ -54,10 +56,26 @@ func WithBorderColor(color compat.AdaptiveColor) renderingOption { } } -func WithBorderColorRight(color compat.AdaptiveColor) renderingOption { +func WithBorderLeft() renderingOption { return func(c *blockRenderer) { - c.borderColorRight = true - c.borderColor = &color + c.borderLeft = true + c.borderRight = false + } +} + +func WithBorderRight() renderingOption { + return func(c *blockRenderer) { + c.borderLeft = false + c.borderRight = true + } +} + +func WithBorderBoth(value bool) renderingOption { + return func(c *blockRenderer) { + if value { + c.borderLeft = true + c.borderRight = true + } } } @@ -116,6 +134,8 @@ func renderContentBlock( renderer := &blockRenderer{ textColor: t.TextMuted(), border: true, + borderLeft: true, + borderRight: false, paddingTop: 1, paddingBottom: 1, paddingLeft: 2, @@ -144,19 +164,17 @@ func renderContentBlock( BorderStyle(lipgloss.ThickBorder()). BorderLeft(true). BorderRight(true). - BorderLeftForeground(borderColor). + BorderLeftForeground(t.BackgroundPanel()). BorderLeftBackground(t.Background()). BorderRightForeground(t.BackgroundPanel()). BorderRightBackground(t.Background()) - if renderer.borderColorRight { - style = style. - BorderLeftBackground(t.Background()). - BorderLeftForeground(t.BackgroundPanel()). - BorderRightForeground(borderColor). - BorderRightBackground(t.Background()) + if renderer.borderLeft { + style = style.BorderLeftForeground(borderColor) + } + if renderer.borderRight { + style = style.BorderRightForeground(borderColor) } - } content = style.Render(content) @@ -223,7 +241,7 @@ func renderText( if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 { content = content + "\n\n" for _, toolCall := range toolCalls { - title := renderToolTitle(toolCall, width) + title := renderToolTitle(toolCall, width-2) style := styles.NewStyle() if toolCall.State.Status == opencode.ToolPartStateStatusError { style = style.Foreground(t.Error()) @@ -247,7 +265,8 @@ func renderText( content, width, WithTextColor(t.Text()), - WithBorderColorRight(t.Secondary()), + WithBorderColor(t.Secondary()), + WithBorderRight(), ) case opencode.AssistantMessage: return renderContentBlock( @@ -263,6 +282,7 @@ func renderText( func renderToolDetails( app *app.App, toolCall opencode.ToolPart, + permission opencode.Permission, width int, ) string { measure := util.Measure("chat.renderToolDetails") @@ -301,6 +321,39 @@ func renderToolDetails( borderColor := t.BackgroundPanel() defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render + permissionContent := "" + if permission.ID != "" { + borderColor = t.Warning() + + base := styles.NewStyle().Background(backgroundColor) + text := base.Foreground(t.Text()).Bold(true).Render + muted := base.Foreground(t.TextMuted()).Render + permissionContent = "Permission required to run this tool:\n\n" + permissionContent += text( + "enter ", + ) + muted( + "accept ", + ) + text( + "a", + ) + muted( + " accept always ", + ) + text( + "esc", + ) + muted( + " reject", + ) + + } + + if permission.Metadata != nil { + metadata := toolCall.State.Metadata.(map[string]any) + if metadata == nil { + metadata = map[string]any{} + } + maps.Copy(metadata, permission.Metadata) + toolCall.State.Metadata = metadata + } + if toolCall.State.Metadata != nil { metadata := toolCall.State.Metadata.(map[string]any) switch toolCall.Tool { @@ -351,12 +404,20 @@ func renderToolDetails( title := renderToolTitle(toolCall, width) title = style.Render(title) content := title + "\n" + body + if permissionContent != "" { + permissionContent = styles.NewStyle(). + Background(backgroundColor). + Padding(1, 2). + Render(permissionContent) + content += "\n" + permissionContent + } content = renderContentBlock( app, content, width, WithPadding(0), WithBorderColor(borderColor), + WithBorderBoth(permission.ID != ""), ) return content } @@ -417,7 +478,7 @@ func renderToolDetails( data, _ := json.Marshal(item) var toolCall opencode.ToolPart _ = json.Unmarshal(data, &toolCall) - step := renderToolTitle(toolCall, width) + step := renderToolTitle(toolCall, width-2) step = "∟ " + step steps = append(steps, step) } @@ -460,7 +521,18 @@ func renderToolDetails( title := renderToolTitle(toolCall, width) content := title + "\n\n" + body - return renderContentBlock(app, content, width, WithBorderColor(borderColor)) + + if permissionContent != "" { + content += "\n\n\n" + permissionContent + } + + return renderContentBlock( + app, + content, + width, + WithBorderColor(borderColor), + WithBorderBoth(permission.ID != ""), + ) } func renderToolName(name string) string { @@ -575,6 +647,10 @@ func renderToolTitle( } title = truncate.StringWithTail(title, uint(width-6), "...") + if toolCall.State.Error != "" { + t := theme.CurrentTheme() + title = styles.NewStyle().Foreground(t.Error()).Render(title) + } return title } diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index e675a35d2..96ea8241d 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -100,8 +100,6 @@ func (m *messagesComponent) Init() tea.Cmd { } func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - measure := util.Measure("messages.Update") - defer measure("from", fmt.Sprintf("%T", msg)) var cmds []tea.Cmd switch msg := msg.(type) { case tea.MouseClickMsg: @@ -199,6 +197,9 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cache.Clear() cmds = append(cmds, m.renderView()) } + case opencode.EventListResponseEventPermissionUpdated: + m.tail = true + return m, m.renderView() case renderCompleteMsg: m.partCount = msg.partCount m.lineCount = msg.lineCount @@ -214,6 +215,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.tail = m.viewport.AtBottom() + viewport, cmd := m.viewport.Update(msg) m.viewport = viewport cmds = append(cmds, cmd) @@ -465,7 +467,13 @@ func (m *messagesComponent) renderView() tea.Cmd { revertedToolCount++ continue } - if !m.showToolDetails { + + permission := opencode.Permission{} + if m.app.CurrentPermission.ToolCallID == part.CallID { + permission = m.app.CurrentPermission + } + + if !m.showToolDetails && permission.ID == "" { if !hasTextPart { orphanedToolCalls = append(orphanedToolCalls, part) } @@ -477,12 +485,14 @@ func (m *messagesComponent) renderView() tea.Cmd { part.ID, m.showToolDetails, width, + permission.ID, ) content, cached = m.cache.Get(key) if !cached { content = renderToolDetails( m.app, part, + permission, width, ) content = lipgloss.PlaceHorizontal( @@ -498,6 +508,7 @@ func (m *messagesComponent) renderView() tea.Cmd { content = renderToolDetails( m.app, part, + permission, width, ) content = lipgloss.PlaceHorizontal( @@ -618,6 +629,40 @@ func (m *messagesComponent) renderView() tea.Cmd { blocks = append(blocks, content) } + if m.app.CurrentPermission.ID != "" && + m.app.CurrentPermission.SessionID != m.app.Session.ID { + response, err := m.app.Client.Session.Message( + context.Background(), + m.app.CurrentPermission.SessionID, + m.app.CurrentPermission.MessageID, + ) + if err != nil || response == nil { + slog.Error("Failed to get message from child session", "error", err) + } else { + for _, part := range response.Parts { + if part.CallID == m.app.CurrentPermission.ToolCallID { + content := renderToolDetails( + m.app, + part.AsUnion().(opencode.ToolPart), + m.app.CurrentPermission, + width, + ) + content = lipgloss.PlaceHorizontal( + m.width, + lipgloss.Center, + content, + styles.WhitespaceStyle(t.Background()), + ) + if content != "" { + partCount++ + lineCount += lipgloss.Height(content) + 1 + blocks = append(blocks, content) + } + } + } + } + } + final := []string{} clipboard := []string{} var selection *selection @@ -846,9 +891,7 @@ func (m *messagesComponent) View() string { ) } - measure := util.Measure("messages.View") viewport := m.viewport.View() - measure() return styles.NewStyle(). Background(t.Background()). Render(m.header + "\n" + viewport) diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go index 307897bc5..daf7a142b 100644 --- a/packages/tui/internal/components/dialog/session.go +++ b/packages/tui/internal/components/dialog/session.go @@ -138,8 +138,6 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) } case "n": - s.app.Session = &opencode.Session{} - s.app.Messages = []app.Message{} return s, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(app.SessionClearedMsg{}), |
