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 | |
| parent | e7631763f3e42292d3cecf14c0ebf295d9b9b333 (diff) | |
| download | opencode-5500698734c5119ccd7b0d9ab504bb3a53e5e39d.tar.gz opencode-5500698734c5119ccd7b0d9ab504bb3a53e5e39d.zip | |
wip: tui permissions
Diffstat (limited to 'packages/tui')
| -rw-r--r-- | packages/tui/internal/app/app.go | 42 | ||||
| -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 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 66 | ||||
| -rw-r--r-- | packages/tui/sdk/.stats.yml | 8 | ||||
| -rw-r--r-- | packages/tui/sdk/api.md | 12 | ||||
| -rw-r--r-- | packages/tui/sdk/event.go | 59 | ||||
| -rw-r--r-- | packages/tui/sdk/session.go | 43 | ||||
| -rw-r--r-- | packages/tui/sdk/session_test.go | 26 | ||||
| -rw-r--r-- | packages/tui/sdk/sessionpermission.go | 126 | ||||
| -rw-r--r-- | packages/tui/sdk/sessionpermission_test.go | 43 |
13 files changed, 490 insertions, 129 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index df4e209cf..672bc0ee6 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -26,26 +26,28 @@ type Message struct { } type App struct { - Info opencode.App - Modes []opencode.Mode - Providers []opencode.Provider - Version string - StatePath string - Config *opencode.Config - Client *opencode.Client - State *State - ModeIndex int - Mode *opencode.Mode - Provider *opencode.Provider - Model *opencode.Model - Session *opencode.Session - Messages []Message - Commands commands.CommandRegistry - InitialModel *string - InitialPrompt *string - IntitialMode *string - compactCancel context.CancelFunc - IsLeaderSequence bool + Info opencode.App + Modes []opencode.Mode + Providers []opencode.Provider + Version string + StatePath string + Config *opencode.Config + Client *opencode.Client + State *State + ModeIndex int + Mode *opencode.Mode + Provider *opencode.Provider + Model *opencode.Model + Session *opencode.Session + Messages []Message + Permissions []opencode.Permission + CurrentPermission opencode.Permission + Commands commands.CommandRegistry + InitialModel *string + InitialPrompt *string + IntitialMode *string + compactCancel context.CancelFunc + IsLeaderSequence bool } type SessionCreatedMsg = struct { 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{}), diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 76b96a8e4..9b6ec7ea6 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -103,9 +103,6 @@ func (a Model) Init() tea.Cmd { } func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - measure := util.Measure("app.Update") - defer measure("from", fmt.Sprintf("%T", msg)) - var cmd tea.Cmd var cmds []tea.Cmd @@ -113,6 +110,45 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: keyString := msg.String() + if a.app.CurrentPermission.ID != "" { + if keyString == "enter" || keyString == "esc" || keyString == "a" { + sessionID := a.app.CurrentPermission.SessionID + permissionID := a.app.CurrentPermission.ID + a.editor.Focus() + a.app.Permissions = a.app.Permissions[1:] + if len(a.app.Permissions) > 0 { + a.app.CurrentPermission = a.app.Permissions[0] + } else { + a.app.CurrentPermission = opencode.Permission{} + } + + response := opencode.SessionPermissionRespondParamsResponseOnce + switch keyString { + case "enter": + response = opencode.SessionPermissionRespondParamsResponseOnce + case "a": + response = opencode.SessionPermissionRespondParamsResponseAlways + case "esc": + response = opencode.SessionPermissionRespondParamsResponseReject + } + + return a, func() tea.Msg { + resp, err := a.app.Client.Session.Permissions.Respond( + context.Background(), + sessionID, + permissionID, + opencode.SessionPermissionRespondParams{Response: opencode.F(response)}, + ) + if err != nil { + slog.Error("Failed to respond to permission request", "error", err) + return toast.NewErrorToast("Failed to respond to permission request") + } + slog.Debug("Responded to permission request", "response", resp) + return nil + } + } + } + // 1. Handle active modal if a.modal != nil { switch keyString { @@ -341,6 +377,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { updated, cmd := a.editor.Focus() a.editor = updated.(chat.EditorComponent) cmds = append(cmds, cmd) + case app.SessionClearedMsg: + a.app.Session = &opencode.Session{} + a.app.Messages = []app.Message{} case dialog.CompletionDialogCloseMsg: a.showCompletionDialog = false case opencode.EventListResponseEventInstallationUpdated: @@ -364,7 +403,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.app.Session = &msg.Properties.Info } case opencode.EventListResponseEventMessagePartUpdated: - slog.Info("message part updated", "message", msg.Properties.Part.MessageID, "part", msg.Properties.Part.ID) + slog.Debug("message part updated", "message", msg.Properties.Part.MessageID, "part", msg.Properties.Part.ID) if msg.Properties.Part.SessionID == a.app.Session.ID { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { switch casted := m.Info.(type) { @@ -402,7 +441,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case opencode.EventListResponseEventMessagePartRemoved: - slog.Info("message part removed", "session", msg.Properties.SessionID, "message", msg.Properties.MessageID, "part", msg.Properties.PartID) + slog.Debug("message part removed", "session", msg.Properties.SessionID, "message", msg.Properties.MessageID, "part", msg.Properties.PartID) if msg.Properties.SessionID == a.app.Session.ID { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { switch casted := m.Info.(type) { @@ -438,7 +477,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case opencode.EventListResponseEventMessageRemoved: - slog.Info("message removed", "session", msg.Properties.SessionID, "message", msg.Properties.MessageID) + slog.Debug("message removed", "session", msg.Properties.SessionID, "message", msg.Properties.MessageID) if msg.Properties.SessionID == a.app.Session.ID { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { switch casted := m.Info.(type) { @@ -480,6 +519,12 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) } } + case opencode.EventListResponseEventPermissionUpdated: + slog.Debug("permission updated", "session", msg.Properties.SessionID, "permission", msg.Properties.ID) + a.app.Permissions = append(a.app.Permissions, msg.Properties) + a.app.CurrentPermission = a.app.Permissions[0] + cmds = append(cmds, toast.NewInfoToast(msg.Properties.Title, toast.WithTitle("Permission requested"))) + a.editor.Blur() case opencode.EventListResponseEventSessionError: switch err := msg.Properties.Error.AsUnion().(type) { case nil: @@ -613,8 +658,6 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (a Model) View() string { - measure := util.Measure("app.View") - defer measure() t := theme.CurrentTheme() var mainLayout string @@ -674,8 +717,6 @@ func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) { } func (a Model) home() string { - measure := util.Measure("home.View") - defer measure() t := theme.CurrentTheme() effectiveWidth := a.width - 4 baseStyle := styles.NewStyle().Background(t.Background()) @@ -796,8 +837,6 @@ func (a Model) home() string { } func (a Model) chat() string { - measure := util.Measure("chat.View") - defer measure() effectiveWidth := a.width - 4 t := theme.CurrentTheme() editorView := a.editor.View() @@ -911,9 +950,8 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { if a.app.Session.ID == "" { return a, nil } - a.app.Session = &opencode.Session{} - a.app.Messages = []app.Message{} cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{})) + case commands.SessionListCommand: sessionDialog := dialog.NewSessionDialog(a.app) a.modal = sessionDialog diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml index 3f719fabf..d69918800 100644 --- a/packages/tui/sdk/.stats.yml +++ b/packages/tui/sdk/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml -openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532 -config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3 +configured_endpoints: 28 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-90f0ff2a2f214a34b74f49a5909e95c31f617bd9bb881da24ab3fe664424c79d.yml +openapi_spec_hash: 5ef69219c1869f78455b0c5374f638f8 +config_hash: 7707d73ebbd7ad7042ab70466b39348d diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md index fb3db9c53..0291c776e 100644 --- a/packages/tui/sdk/api.md +++ b/packages/tui/sdk/api.md @@ -103,6 +103,7 @@ Response Types: - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a> +- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a> Methods: @@ -113,6 +114,7 @@ Methods: - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> +- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> @@ -120,6 +122,16 @@ Methods: - <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> +## Permissions + +Response Types: + +- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Permission">Permission</a> + +Methods: + +- <code title="post /session/{id}/permissions/{permissionID}">client.Session.Permissions.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionService.Respond">Respond</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, permissionID <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionRespondParams">SessionPermissionRespondParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> + # Tui Methods: diff --git a/packages/tui/sdk/event.go b/packages/tui/sdk/event.go index 5203ab23a..3c08b327e 100644 --- a/packages/tui/sdk/event.go +++ b/packages/tui/sdk/event.go @@ -54,8 +54,7 @@ type EventListResponse struct { // [EventListResponseEventMessageRemovedProperties], // [EventListResponseEventMessagePartUpdatedProperties], // [EventListResponseEventMessagePartRemovedProperties], - // [EventListResponseEventStorageWriteProperties], - // [EventListResponseEventPermissionUpdatedProperties], + // [EventListResponseEventStorageWriteProperties], [Permission], // [EventListResponseEventFileEditedProperties], // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], @@ -643,9 +642,9 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool { } type EventListResponseEventPermissionUpdated struct { - Properties EventListResponseEventPermissionUpdatedProperties `json:"properties,required"` - Type EventListResponseEventPermissionUpdatedType `json:"type,required"` - JSON eventListResponseEventPermissionUpdatedJSON `json:"-"` + Properties Permission `json:"properties,required"` + Type EventListResponseEventPermissionUpdatedType `json:"type,required"` + JSON eventListResponseEventPermissionUpdatedJSON `json:"-"` } // eventListResponseEventPermissionUpdatedJSON contains the JSON metadata for the @@ -667,56 +666,6 @@ func (r eventListResponseEventPermissionUpdatedJSON) RawJSON() string { func (r EventListResponseEventPermissionUpdated) implementsEventListResponse() {} -type EventListResponseEventPermissionUpdatedProperties struct { - ID string `json:"id,required"` - Metadata map[string]interface{} `json:"metadata,required"` - SessionID string `json:"sessionID,required"` - Time EventListResponseEventPermissionUpdatedPropertiesTime `json:"time,required"` - Title string `json:"title,required"` - JSON eventListResponseEventPermissionUpdatedPropertiesJSON `json:"-"` -} - -// eventListResponseEventPermissionUpdatedPropertiesJSON contains the JSON metadata -// for the struct [EventListResponseEventPermissionUpdatedProperties] -type eventListResponseEventPermissionUpdatedPropertiesJSON struct { - ID apijson.Field - Metadata apijson.Field - SessionID apijson.Field - Time apijson.Field - Title apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventPermissionUpdatedProperties) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventPermissionUpdatedPropertiesJSON) RawJSON() string { - return r.raw -} - -type EventListResponseEventPermissionUpdatedPropertiesTime struct { - Created float64 `json:"created,required"` - JSON eventListResponseEventPermissionUpdatedPropertiesTimeJSON `json:"-"` -} - -// eventListResponseEventPermissionUpdatedPropertiesTimeJSON contains the JSON -// metadata for the struct [EventListResponseEventPermissionUpdatedPropertiesTime] -type eventListResponseEventPermissionUpdatedPropertiesTimeJSON struct { - Created apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventPermissionUpdatedPropertiesTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventPermissionUpdatedPropertiesTimeJSON) RawJSON() string { - return r.raw -} - type EventListResponseEventPermissionUpdatedType string const ( diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go index 2598d51c6..d38c37e0e 100644 --- a/packages/tui/sdk/session.go +++ b/packages/tui/sdk/session.go @@ -24,7 +24,8 @@ import ( // automatically. You should not instantiate this service directly, and instead use // the [NewSessionService] method instead. type SessionService struct { - Options []option.RequestOption + Options []option.RequestOption + Permissions *SessionPermissionService } // NewSessionService generates a new service that applies the given options to each @@ -33,6 +34,7 @@ type SessionService struct { func NewSessionService(opts ...option.RequestOption) (r *SessionService) { r = &SessionService{} r.Options = opts + r.Permissions = NewSessionPermissionService(opts...) return } @@ -100,6 +102,22 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa return } +// Get a message from a session +func (r *SessionService) Message(ctx context.Context, id string, messageID string, opts ...option.RequestOption) (res *SessionMessageResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + if messageID == "" { + err = errors.New("missing required messageID parameter") + return + } + path := fmt.Sprintf("session/%s/message/%s", id, messageID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + // List messages for a session func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) { opts = append(r.Options[:], opts...) @@ -2012,6 +2030,29 @@ func (r userMessageTimeJSON) RawJSON() string { return r.raw } +type SessionMessageResponse struct { + Info Message `json:"info,required"` + Parts []Part `json:"parts,required"` + JSON sessionMessageResponseJSON `json:"-"` +} + +// sessionMessageResponseJSON contains the JSON metadata for the struct +// [SessionMessageResponse] +type sessionMessageResponseJSON struct { + Info apijson.Field + Parts apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionMessageResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionMessageResponseJSON) RawJSON() string { + return r.raw +} + type SessionMessagesResponse struct { Info Message `json:"info,required"` Parts []Part `json:"parts,required"` diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go index 295e9e7ce..ab9fbcf7b 100644 --- a/packages/tui/sdk/session_test.go +++ b/packages/tui/sdk/session_test.go @@ -176,6 +176,32 @@ func TestSessionInit(t *testing.T) { } } +func TestSessionMessage(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Message( + context.TODO(), + "id", + "messageID", + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestSessionMessages(t *testing.T) { t.Skip("skipped: tests are disabled for the time being") baseURL := "http://localhost:4010" diff --git a/packages/tui/sdk/sessionpermission.go b/packages/tui/sdk/sessionpermission.go new file mode 100644 index 000000000..90a2134d8 --- /dev/null +++ b/packages/tui/sdk/sessionpermission.go @@ -0,0 +1,126 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// SessionPermissionService contains methods and other services that help with +// interacting with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewSessionPermissionService] method instead. +type SessionPermissionService struct { + Options []option.RequestOption +} + +// NewSessionPermissionService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewSessionPermissionService(opts ...option.RequestOption) (r *SessionPermissionService) { + r = &SessionPermissionService{} + r.Options = opts + return +} + +// Respond to a permission request +func (r *SessionPermissionService) Respond(ctx context.Context, id string, permissionID string, body SessionPermissionRespondParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + if permissionID == "" { + err = errors.New("missing required permissionID parameter") + return + } + path := fmt.Sprintf("session/%s/permissions/%s", id, permissionID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +type Permission struct { + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + Metadata map[string]interface{} `json:"metadata,required"` + SessionID string `json:"sessionID,required"` + Time PermissionTime `json:"time,required"` + Title string `json:"title,required"` + ToolCallID string `json:"toolCallID"` + JSON permissionJSON `json:"-"` +} + +// permissionJSON contains the JSON metadata for the struct [Permission] +type permissionJSON struct { + ID apijson.Field + MessageID apijson.Field + Metadata apijson.Field + SessionID apijson.Field + Time apijson.Field + Title apijson.Field + ToolCallID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Permission) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r permissionJSON) RawJSON() string { + return r.raw +} + +type PermissionTime struct { + Created float64 `json:"created,required"` + JSON permissionTimeJSON `json:"-"` +} + +// permissionTimeJSON contains the JSON metadata for the struct [PermissionTime] +type permissionTimeJSON struct { + Created apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *PermissionTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r permissionTimeJSON) RawJSON() string { + return r.raw +} + +type SessionPermissionRespondParams struct { + Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"` +} + +func (r SessionPermissionRespondParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +type SessionPermissionRespondParamsResponse string + +const ( + SessionPermissionRespondParamsResponseOnce SessionPermissionRespondParamsResponse = "once" + SessionPermissionRespondParamsResponseAlways SessionPermissionRespondParamsResponse = "always" + SessionPermissionRespondParamsResponseReject SessionPermissionRespondParamsResponse = "reject" +) + +func (r SessionPermissionRespondParamsResponse) IsKnown() bool { + switch r { + case SessionPermissionRespondParamsResponseOnce, SessionPermissionRespondParamsResponseAlways, SessionPermissionRespondParamsResponseReject: + return true + } + return false +} diff --git a/packages/tui/sdk/sessionpermission_test.go b/packages/tui/sdk/sessionpermission_test.go new file mode 100644 index 000000000..728976be4 --- /dev/null +++ b/packages/tui/sdk/sessionpermission_test.go @@ -0,0 +1,43 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestSessionPermissionRespond(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Permissions.Respond( + context.TODO(), + "id", + "permissionID", + opencode.SessionPermissionRespondParams{ + Response: opencode.F(opencode.SessionPermissionRespondParamsResponseOnce), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} |
