summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-07-31 09:34:43 -0500
committeradamdotdevin <[email protected]>2025-07-31 09:59:17 -0500
commit5500698734c5119ccd7b0d9ab504bb3a53e5e39d (patch)
tree6e77b461a2a488aab213355a5fb47c7b304bce78 /packages/tui/internal/components
parente7631763f3e42292d3cecf14c0ebf295d9b9b333 (diff)
downloadopencode-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.go13
-rw-r--r--packages/tui/internal/components/chat/message.go126
-rw-r--r--packages/tui/internal/components/chat/messages.go53
-rw-r--r--packages/tui/internal/components/dialog/session.go2
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{}),