wip: tui permissions
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{}),
|
||||
|
||||
Reference in New Issue
Block a user