wip: tui permissions

This commit is contained in:
adamdotdevin
2025-07-31 09:34:43 -05:00
parent e7631763f3
commit 5500698734
26 changed files with 1448 additions and 179 deletions

View File

@@ -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 {

View File

@@ -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")
}
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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{}),

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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 (

View File

@@ -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"`

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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())
}
}