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

@@ -2,6 +2,7 @@ import { App } from "../app/app"
import { z } from "zod" import { z } from "zod"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Installation } from "../installation"
export namespace Permission { export namespace Permission {
const log = Log.create({ service: "permission" }) const log = Log.create({ service: "permission" })
@@ -10,6 +11,8 @@ export namespace Permission {
.object({ .object({
id: z.string(), id: z.string(),
sessionID: z.string(), sessionID: z.string(),
messageID: z.string(),
toolCallID: z.string().optional(),
title: z.string(), title: z.string(),
metadata: z.record(z.any()), metadata: z.record(z.any()),
time: z.object({ time: z.object({
@@ -17,7 +20,7 @@ export namespace Permission {
}), }),
}) })
.openapi({ .openapi({
ref: "permission.info", ref: "Permission",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
@@ -52,7 +55,7 @@ export namespace Permission {
async (state) => { async (state) => {
for (const pending of Object.values(state.pending)) { for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) { for (const item of Object.values(pending)) {
item.reject(new RejectedError(item.info.sessionID, item.info.id)) item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.toolCallID))
} }
} }
}, },
@@ -61,25 +64,35 @@ export namespace Permission {
export function ask(input: { export function ask(input: {
id: Info["id"] id: Info["id"]
sessionID: Info["sessionID"] sessionID: Info["sessionID"]
messageID: Info["messageID"]
toolCallID?: Info["toolCallID"]
title: Info["title"] title: Info["title"]
metadata: Info["metadata"] metadata: Info["metadata"]
}) { }) {
return // TODO: dax, remove this when you're happy with permissions
if (!Installation.isDev()) return
const { pending, approved } = state() const { pending, approved } = state()
log.info("asking", { log.info("asking", {
sessionID: input.sessionID, sessionID: input.sessionID,
permissionID: input.id, permissionID: input.id,
messageID: input.messageID,
toolCallID: input.toolCallID,
}) })
if (approved[input.sessionID]?.[input.id]) { if (approved[input.sessionID]?.[input.id]) {
log.info("previously approved", { log.info("previously approved", {
sessionID: input.sessionID, sessionID: input.sessionID,
permissionID: input.id, permissionID: input.id,
messageID: input.messageID,
toolCallID: input.toolCallID,
}) })
return return
} }
const info: Info = { const info: Info = {
id: input.id, id: input.id,
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: input.messageID,
toolCallID: input.toolCallID,
title: input.title, title: input.title,
metadata: input.metadata, metadata: input.metadata,
time: { time: {
@@ -93,29 +106,28 @@ export namespace Permission {
resolve, resolve,
reject, reject,
} }
setTimeout(() => { // setTimeout(() => {
respond({ // respond({
sessionID: input.sessionID, // sessionID: input.sessionID,
permissionID: input.id, // permissionID: input.id,
response: "always", // response: "always",
}) // })
}, 1000) // }, 1000)
Bus.publish(Event.Updated, info) Bus.publish(Event.Updated, info)
}) })
} }
export function respond(input: { export const Response = z.enum(["once", "always", "reject"])
sessionID: Info["sessionID"] export type Response = z.infer<typeof Response>
permissionID: Info["id"]
response: "once" | "always" | "reject" export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
}) {
log.info("response", input) log.info("response", input)
const { pending, approved } = state() const { pending, approved } = state()
const match = pending[input.sessionID]?.[input.permissionID] const match = pending[input.sessionID]?.[input.permissionID]
if (!match) return if (!match) return
delete pending[input.sessionID][input.permissionID] delete pending[input.sessionID][input.permissionID]
if (input.response === "reject") { if (input.response === "reject") {
match.reject(new RejectedError(input.sessionID, input.permissionID)) match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.toolCallID))
return return
} }
match.resolve() match.resolve()
@@ -129,6 +141,7 @@ export namespace Permission {
constructor( constructor(
public readonly sessionID: string, public readonly sessionID: string,
public readonly permissionID: string, public readonly permissionID: string,
public readonly toolCallID?: string,
) { ) {
super(`The user rejected permission to use this functionality`) super(`The user rejected permission to use this functionality`)
} }

View File

@@ -18,6 +18,7 @@ import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2" import { MessageV2 } from "../session/message-v2"
import { Mode } from "../session/mode" import { Mode } from "../session/mode"
import { callTui, TuiRoute } from "./tui" import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission"
const ERRORS = { const ERRORS = {
400: { 400: {
@@ -457,6 +458,39 @@ export namespace Server {
return c.json(messages) return c.json(messages)
}, },
) )
.get(
"/session/:id/message/:messageID",
describeRoute({
description: "Get a message from a session",
responses: {
200: {
description: "Message",
content: {
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Info,
parts: MessageV2.Part.array(),
}),
),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
messageID: z.string().openapi({ description: "Message ID" }),
}),
),
async (c) => {
const params = c.req.valid("param")
const message = await Session.getMessage(params.id, params.messageID)
return c.json(message)
},
)
.post( .post(
"/session/:id/message", "/session/:id/message",
describeRoute({ describeRoute({
@@ -545,6 +579,37 @@ export namespace Server {
return c.json(session) return c.json(session)
}, },
) )
.post(
"/session/:id/permissions/:permissionID",
describeRoute({
description: "Respond to a permission request",
responses: {
200: {
description: "Permission processed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
permissionID: z.string(),
}),
),
zValidator("json", z.object({ response: Permission.Response })),
async (c) => {
const params = c.req.valid("param")
const id = params.id
const permissionID = params.permissionID
Permission.respond({ sessionID: id, permissionID, response: c.req.valid("json").response })
return c.json(true)
},
)
.get( .get(
"/config/providers", "/config/providers",
describeRoute({ describeRoute({

View File

@@ -256,7 +256,10 @@ export namespace Session {
} }
export async function getMessage(sessionID: string, messageID: string) { export async function getMessage(sessionID: string, messageID: string) {
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID) return {
info: await Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID),
parts: await getParts(sessionID, messageID),
}
} }
export async function getParts(sessionID: string, messageID: string) { export async function getParts(sessionID: string, messageID: string) {
@@ -714,6 +717,7 @@ export namespace Session {
sessionID: input.sessionID, sessionID: input.sessionID,
abort: abort.signal, abort: abort.signal,
messageID: assistantMsg.id, messageID: assistantMsg.id,
toolCallID: options.toolCallId,
metadata: async (val) => { metadata: async (val) => {
const match = processor.partFromToolCall(options.toolCallId) const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") { if (match && match.state.status === "running") {

View File

@@ -2,6 +2,8 @@ import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt" import DESCRIPTION from "./bash.txt"
import { App } from "../app/app" import { App } from "../app/app"
import { Permission } from "../permission"
import { Config } from "../config/config"
// import Parser from "tree-sitter" // import Parser from "tree-sitter"
// import Bash from "tree-sitter-bash" // import Bash from "tree-sitter-bash"
@@ -93,6 +95,8 @@ export const BashTool = Tool.define("bash", {
await Permission.ask({ await Permission.ask({
id: "bash", id: "bash",
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: ctx.messageID,
toolCallID: ctx.toolCallID,
title: params.command, title: params.command,
metadata: { metadata: {
command: params.command, command: params.command,
@@ -101,6 +105,21 @@ export const BashTool = Tool.define("bash", {
} }
*/ */
const cfg = await Config.get()
if (cfg.permission?.bash === "ask")
await Permission.ask({
id: "bash",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
toolCallID: ctx.toolCallID,
title: "Run this command: " + params.command,
metadata: {
command: params.command,
description: params.description,
timeout: params.timeout,
},
})
const process = Bun.spawn({ const process = Bun.spawn({
cmd: ["bash", "-c", params.command], cmd: ["bash", "-c", params.command],
cwd: app.path.cwd, cwd: app.path.cwd,

View File

@@ -35,61 +35,77 @@ export const EditTool = Tool.define("edit", {
} }
const app = App.info() const app = App.info()
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
if (!Filesystem.contains(app.path.cwd, filepath)) { if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filepath} is not in the current working directory`) throw new Error(`File ${filePath} is not in the current working directory`)
} }
const cfg = await Config.get() const cfg = await Config.get()
if (cfg.permission?.edit === "ask") let diff = ""
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
title: "Edit this file: " + filepath,
metadata: {
filePath: filepath,
oldString: params.oldString,
newString: params.newString,
},
})
let contentOld = "" let contentOld = ""
let contentNew = "" let contentNew = ""
await (async () => { await (async () => {
if (params.oldString === "") { if (params.oldString === "") {
contentNew = params.newString contentNew = params.newString
await Bun.write(filepath, params.newString) diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
toolCallID: ctx.toolCallID,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, { await Bus.publish(File.Event.Edited, {
file: filepath, file: filePath,
}) })
return return
} }
const file = Bun.file(filepath) const file = Bun.file(filePath)
const stats = await file.stat().catch(() => {}) const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filepath} not found`) if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`) if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filepath) await FileTime.assert(ctx.sessionID, filePath)
contentOld = await file.text() contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
toolCallID: ctx.toolCallID,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await file.write(contentNew) await file.write(contentNew)
await Bus.publish(File.Event.Edited, { await Bus.publish(File.Event.Edited, {
file: filepath, file: filePath,
}) })
contentNew = await file.text() contentNew = await file.text()
})() })()
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew)) FileTime.read(ctx.sessionID, filePath)
FileTime.read(ctx.sessionID, filepath)
let output = "" let output = ""
await LSP.touchFile(filepath, true) await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics() const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) { for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue if (issues.length === 0) continue
if (file === filepath) { if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n` output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue continue
} }
@@ -104,7 +120,7 @@ export const EditTool = Tool.define("edit", {
diagnostics, diagnostics,
diff, diff,
}, },
title: `${path.relative(app.path.root, filepath)}`, title: `${path.relative(app.path.root, filePath)}`,
output, output,
} }
}, },

View File

@@ -20,7 +20,7 @@ export const TaskTool = Tool.define("task", async () => {
async execute(params, ctx) { async execute(params, ctx) {
const session = await Session.create(ctx.sessionID) const session = await Session.create(ctx.sessionID)
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID) const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
if (msg.role !== "assistant") throw new Error("Not an assistant message") if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
const agent = await Agent.get(params.subagent_type) const agent = await Agent.get(params.subagent_type)
const messageID = Identifier.ascending("message") const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {} const parts: Record<string, MessageV2.ToolPart> = {}
@@ -38,8 +38,8 @@ export const TaskTool = Tool.define("task", async () => {
}) })
const model = agent.model ?? { const model = agent.model ?? {
modelID: msg.modelID, modelID: msg.info.modelID,
providerID: msg.providerID, providerID: msg.info.providerID,
} }
ctx.abort.addEventListener("abort", () => { ctx.abort.addEventListener("abort", () => {
@@ -50,7 +50,7 @@ export const TaskTool = Tool.define("task", async () => {
sessionID: session.id, sessionID: session.id,
modelID: model.modelID, modelID: model.modelID,
providerID: model.providerID, providerID: model.providerID,
mode: msg.mode, mode: msg.info.mode,
system: agent.prompt, system: agent.prompt,
tools: { tools: {
...agent.tools, ...agent.tools,

View File

@@ -7,6 +7,7 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = { export type Context<M extends Metadata = Metadata> = {
sessionID: string sessionID: string
messageID: string messageID: string
toolCallID: string
abort: AbortSignal abort: AbortSignal
metadata(input: { title?: string; metadata?: M }): void metadata(input: { title?: string; metadata?: M }): void
} }

View File

@@ -33,6 +33,8 @@ export const WriteTool = Tool.define("write", {
await Permission.ask({ await Permission.ask({
id: "write", id: "write",
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: ctx.messageID,
toolCallID: ctx.toolCallID,
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath, title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
metadata: { metadata: {
filePath: filepath, filePath: filepath,

View File

@@ -0,0 +1,44 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
export {
Permissions,
type Permission,
type PermissionRespondResponse,
type PermissionRespondParams,
} from './permissions';
export {
SessionResource,
type AssistantMessage,
type FilePart,
type FilePartInput,
type FilePartSource,
type FilePartSourceText,
type FileSource,
type Message,
type Part,
type Session,
type SnapshotPart,
type StepFinishPart,
type StepStartPart,
type SymbolSource,
type TextPart,
type TextPartInput,
type ToolPart,
type ToolStateCompleted,
type ToolStateError,
type ToolStatePending,
type ToolStateRunning,
type UserMessage,
type SessionListResponse,
type SessionDeleteResponse,
type SessionAbortResponse,
type SessionInitResponse,
type SessionMessageResponse,
type SessionMessagesResponse,
type SessionSummarizeResponse,
type SessionChatParams,
type SessionInitParams,
type SessionMessageParams,
type SessionRevertParams,
type SessionSummarizeParams,
} from './session';

View File

@@ -0,0 +1,64 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import { APIPromise } from '../../core/api-promise';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class Permissions extends APIResource {
/**
* Respond to a permission request
*/
respond(
permissionID: string,
params: PermissionRespondParams,
options?: RequestOptions,
): APIPromise<PermissionRespondResponse> {
const { id, ...body } = params;
return this._client.post(path`/session/${id}/permissions/${permissionID}`, { body, ...options });
}
}
export interface Permission {
id: string;
messageID: string;
metadata: { [key: string]: unknown };
sessionID: string;
time: Permission.Time;
title: string;
toolCallID?: string;
}
export namespace Permission {
export interface Time {
created: number;
}
}
export type PermissionRespondResponse = boolean;
export interface PermissionRespondParams {
/**
* Path param:
*/
id: string;
/**
* Body param:
*/
response: 'once' | 'always' | 'reject';
}
export declare namespace Permissions {
export {
type Permission as Permission,
type PermissionRespondResponse as PermissionRespondResponse,
type PermissionRespondParams as PermissionRespondParams,
};
}

View File

@@ -0,0 +1,645 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import * as SessionAPI from './session';
import * as Shared from '../shared';
import * as PermissionsAPI from './permissions';
import { Permission, PermissionRespondParams, PermissionRespondResponse, Permissions } from './permissions';
import { APIPromise } from '../../core/api-promise';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class SessionResource extends APIResource {
permissions: PermissionsAPI.Permissions = new PermissionsAPI.Permissions(this._client);
/**
* Create a new session
*/
create(options?: RequestOptions): APIPromise<Session> {
return this._client.post('/session', options);
}
/**
* List all sessions
*/
list(options?: RequestOptions): APIPromise<SessionListResponse> {
return this._client.get('/session', options);
}
/**
* Delete a session and all its data
*/
delete(id: string, options?: RequestOptions): APIPromise<SessionDeleteResponse> {
return this._client.delete(path`/session/${id}`, options);
}
/**
* Abort a session
*/
abort(id: string, options?: RequestOptions): APIPromise<SessionAbortResponse> {
return this._client.post(path`/session/${id}/abort`, options);
}
/**
* Create and send a new message to a session
*/
chat(id: string, body: SessionChatParams, options?: RequestOptions): APIPromise<AssistantMessage> {
return this._client.post(path`/session/${id}/message`, { body, ...options });
}
/**
* Analyze the app and create an AGENTS.md file
*/
init(id: string, body: SessionInitParams, options?: RequestOptions): APIPromise<SessionInitResponse> {
return this._client.post(path`/session/${id}/init`, { body, ...options });
}
/**
* Get a message from a session
*/
message(
messageID: string,
params: SessionMessageParams,
options?: RequestOptions,
): APIPromise<SessionMessageResponse> {
const { id } = params;
return this._client.get(path`/session/${id}/message/${messageID}`, options);
}
/**
* List messages for a session
*/
messages(id: string, options?: RequestOptions): APIPromise<SessionMessagesResponse> {
return this._client.get(path`/session/${id}/message`, options);
}
/**
* Revert a message
*/
revert(id: string, body: SessionRevertParams, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/revert`, { body, ...options });
}
/**
* Share a session
*/
share(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/share`, options);
}
/**
* Summarize the session
*/
summarize(
id: string,
body: SessionSummarizeParams,
options?: RequestOptions,
): APIPromise<SessionSummarizeResponse> {
return this._client.post(path`/session/${id}/summarize`, { body, ...options });
}
/**
* Restore all reverted messages
*/
unrevert(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/unrevert`, options);
}
/**
* Unshare the session
*/
unshare(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.delete(path`/session/${id}/share`, options);
}
}
export interface AssistantMessage {
id: string;
cost: number;
mode: string;
modelID: string;
path: AssistantMessage.Path;
providerID: string;
role: 'assistant';
sessionID: string;
system: Array<string>;
time: AssistantMessage.Time;
tokens: AssistantMessage.Tokens;
error?:
| Shared.ProviderAuthError
| Shared.UnknownError
| AssistantMessage.MessageOutputLengthError
| Shared.MessageAbortedError;
summary?: boolean;
}
export namespace AssistantMessage {
export interface Path {
cwd: string;
root: string;
}
export interface Time {
created: number;
completed?: number;
}
export interface Tokens {
cache: Tokens.Cache;
input: number;
output: number;
reasoning: number;
}
export namespace Tokens {
export interface Cache {
read: number;
write: number;
}
}
export interface MessageOutputLengthError {
data: unknown;
name: 'MessageOutputLengthError';
}
}
export interface FilePart {
id: string;
messageID: string;
mime: string;
sessionID: string;
type: 'file';
url: string;
filename?: string;
source?: FilePartSource;
}
export interface FilePartInput {
mime: string;
type: 'file';
url: string;
id?: string;
filename?: string;
source?: FilePartSource;
}
export type FilePartSource = FileSource | SymbolSource;
export interface FilePartSourceText {
end: number;
start: number;
value: string;
}
export interface FileSource {
path: string;
text: FilePartSourceText;
type: 'file';
}
export type Message = UserMessage | AssistantMessage;
export type Part =
| TextPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| Part.PatchPart;
export namespace Part {
export interface PatchPart {
id: string;
files: Array<string>;
hash: string;
messageID: string;
sessionID: string;
type: 'patch';
}
}
export interface Session {
id: string;
time: Session.Time;
title: string;
version: string;
parentID?: string;
revert?: Session.Revert;
share?: Session.Share;
}
export namespace Session {
export interface Time {
created: number;
updated: number;
}
export interface Revert {
messageID: string;
diff?: string;
partID?: string;
snapshot?: string;
}
export interface Share {
url: string;
}
}
export interface SnapshotPart {
id: string;
messageID: string;
sessionID: string;
snapshot: string;
type: 'snapshot';
}
export interface StepFinishPart {
id: string;
cost: number;
messageID: string;
sessionID: string;
tokens: StepFinishPart.Tokens;
type: 'step-finish';
}
export namespace StepFinishPart {
export interface Tokens {
cache: Tokens.Cache;
input: number;
output: number;
reasoning: number;
}
export namespace Tokens {
export interface Cache {
read: number;
write: number;
}
}
}
export interface StepStartPart {
id: string;
messageID: string;
sessionID: string;
type: 'step-start';
}
export interface SymbolSource {
kind: number;
name: string;
path: string;
range: SymbolSource.Range;
text: FilePartSourceText;
type: 'symbol';
}
export namespace SymbolSource {
export interface Range {
end: Range.End;
start: Range.Start;
}
export namespace Range {
export interface End {
character: number;
line: number;
}
export interface Start {
character: number;
line: number;
}
}
}
export interface TextPart {
id: string;
messageID: string;
sessionID: string;
text: string;
type: 'text';
synthetic?: boolean;
time?: TextPart.Time;
}
export namespace TextPart {
export interface Time {
start: number;
end?: number;
}
}
export interface TextPartInput {
text: string;
type: 'text';
id?: string;
synthetic?: boolean;
time?: TextPartInput.Time;
}
export namespace TextPartInput {
export interface Time {
start: number;
end?: number;
}
}
export interface ToolPart {
id: string;
callID: string;
messageID: string;
sessionID: string;
state: ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError;
tool: string;
type: 'tool';
}
export interface ToolStateCompleted {
input: { [key: string]: unknown };
metadata: { [key: string]: unknown };
output: string;
status: 'completed';
time: ToolStateCompleted.Time;
title: string;
}
export namespace ToolStateCompleted {
export interface Time {
end: number;
start: number;
}
}
export interface ToolStateError {
error: string;
input: { [key: string]: unknown };
status: 'error';
time: ToolStateError.Time;
}
export namespace ToolStateError {
export interface Time {
end: number;
start: number;
}
}
export interface ToolStatePending {
status: 'pending';
}
export interface ToolStateRunning {
status: 'running';
time: ToolStateRunning.Time;
input?: unknown;
metadata?: { [key: string]: unknown };
title?: string;
}
export namespace ToolStateRunning {
export interface Time {
start: number;
}
}
export interface UserMessage {
id: string;
role: 'user';
sessionID: string;
time: UserMessage.Time;
}
export namespace UserMessage {
export interface Time {
created: number;
}
}
export type SessionListResponse = Array<Session>;
export type SessionDeleteResponse = boolean;
export type SessionAbortResponse = boolean;
export type SessionInitResponse = boolean;
export interface SessionMessageResponse {
info: Message;
parts: Array<Part>;
}
export type SessionMessagesResponse = Array<SessionMessagesResponse.SessionMessagesResponseItem>;
export namespace SessionMessagesResponse {
export interface SessionMessagesResponseItem {
info: SessionAPI.Message;
parts: Array<SessionAPI.Part>;
}
}
export type SessionSummarizeResponse = boolean;
export interface SessionChatParams {
modelID: string;
parts: Array<TextPartInput | FilePartInput>;
providerID: string;
messageID?: string;
mode?: string;
system?: string;
tools?: { [key: string]: boolean };
}
export interface SessionInitParams {
messageID: string;
modelID: string;
providerID: string;
}
export interface SessionMessageParams {
/**
* Session ID
*/
id: string;
}
export interface SessionRevertParams {
messageID: string;
partID?: string;
}
export interface SessionSummarizeParams {
modelID: string;
providerID: string;
}
SessionResource.Permissions = Permissions;
export declare namespace SessionResource {
export {
type AssistantMessage as AssistantMessage,
type FilePart as FilePart,
type FilePartInput as FilePartInput,
type FilePartSource as FilePartSource,
type FilePartSourceText as FilePartSourceText,
type FileSource as FileSource,
type Message as Message,
type Part as Part,
type Session as Session,
type SnapshotPart as SnapshotPart,
type StepFinishPart as StepFinishPart,
type StepStartPart as StepStartPart,
type SymbolSource as SymbolSource,
type TextPart as TextPart,
type TextPartInput as TextPartInput,
type ToolPart as ToolPart,
type ToolStateCompleted as ToolStateCompleted,
type ToolStateError as ToolStateError,
type ToolStatePending as ToolStatePending,
type ToolStateRunning as ToolStateRunning,
type UserMessage as UserMessage,
type SessionListResponse as SessionListResponse,
type SessionDeleteResponse as SessionDeleteResponse,
type SessionAbortResponse as SessionAbortResponse,
type SessionInitResponse as SessionInitResponse,
type SessionMessageResponse as SessionMessageResponse,
type SessionMessagesResponse as SessionMessagesResponse,
type SessionSummarizeResponse as SessionSummarizeResponse,
type SessionChatParams as SessionChatParams,
type SessionInitParams as SessionInitParams,
type SessionMessageParams as SessionMessageParams,
type SessionRevertParams as SessionRevertParams,
type SessionSummarizeParams as SessionSummarizeParams,
};
export {
Permissions as Permissions,
type Permission as Permission,
type PermissionRespondResponse as PermissionRespondResponse,
type PermissionRespondParams as PermissionRespondParams,
};
}

View File

@@ -118,11 +118,19 @@ resources:
share: post /session/{id}/share share: post /session/{id}/share
unshare: delete /session/{id}/share unshare: delete /session/{id}/share
summarize: post /session/{id}/summarize summarize: post /session/{id}/summarize
message: get /session/{id}/message/{messageID}
messages: get /session/{id}/message messages: get /session/{id}/message
chat: post /session/{id}/message chat: post /session/{id}/message
revert: post /session/{id}/revert revert: post /session/{id}/revert
unrevert: post /session/{id}/unrevert unrevert: post /session/{id}/unrevert
subresources:
permissions:
models:
permission: Permission
methods:
respond: post /session/{id}/permissions/{permissionID}
tui: tui:
methods: methods:
appendPrompt: post /tui/append-prompt appendPrompt: post /tui/append-prompt

View File

@@ -0,0 +1,27 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
describe('resource permissions', () => {
// skipped: tests are disabled for the time being
test.skip('respond: only required params', async () => {
const responsePromise = client.session.permissions.respond('permissionID', {
id: 'id',
response: 'once',
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
// skipped: tests are disabled for the time being
test.skip('respond: required and optional params', async () => {
const response = await client.session.permissions.respond('permissionID', { id: 'id', response: 'once' });
});
});

View File

@@ -40,6 +40,8 @@ type App struct {
Model *opencode.Model Model *opencode.Model
Session *opencode.Session Session *opencode.Session
Messages []Message Messages []Message
Permissions []opencode.Permission
CurrentPermission opencode.Permission
Commands commands.CommandRegistry Commands commands.CommandRegistry
InitialModel *string InitialModel *string
InitialPrompt *string InitialPrompt *string

View File

@@ -344,9 +344,13 @@ func (m *editorComponent) Content() string {
hint = base(keyText+" again") + muted(" to exit") hint = base(keyText+" again") + muted(" to exit")
} else if m.app.IsBusy() { } else if m.app.IsBusy() {
keyText := m.getInterruptKeyText() 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( hint = muted(
"working", status,
) + m.spinner.View() + muted( ) + m.spinner.View() + muted(
" ", " ",
) + base( ) + base(
@@ -355,7 +359,10 @@ func (m *editorComponent) Content() string {
" interrupt", " interrupt",
) )
} else { } 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 ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"slices" "slices"
"strings" "strings"
"time" "time"
@@ -25,7 +26,8 @@ type blockRenderer struct {
textColor compat.AdaptiveColor textColor compat.AdaptiveColor
border bool border bool
borderColor *compat.AdaptiveColor borderColor *compat.AdaptiveColor
borderColorRight bool borderLeft bool
borderRight bool
paddingTop int paddingTop int
paddingBottom int paddingBottom int
paddingLeft int paddingLeft int
@@ -54,10 +56,26 @@ func WithBorderColor(color compat.AdaptiveColor) renderingOption {
} }
} }
func WithBorderColorRight(color compat.AdaptiveColor) renderingOption { func WithBorderLeft() renderingOption {
return func(c *blockRenderer) { return func(c *blockRenderer) {
c.borderColorRight = true c.borderLeft = true
c.borderColor = &color 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{ renderer := &blockRenderer{
textColor: t.TextMuted(), textColor: t.TextMuted(),
border: true, border: true,
borderLeft: true,
borderRight: false,
paddingTop: 1, paddingTop: 1,
paddingBottom: 1, paddingBottom: 1,
paddingLeft: 2, paddingLeft: 2,
@@ -144,19 +164,17 @@ func renderContentBlock(
BorderStyle(lipgloss.ThickBorder()). BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true). BorderLeft(true).
BorderRight(true). BorderRight(true).
BorderLeftForeground(borderColor). BorderLeftForeground(t.BackgroundPanel()).
BorderLeftBackground(t.Background()). BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundPanel()). BorderRightForeground(t.BackgroundPanel()).
BorderRightBackground(t.Background()) BorderRightBackground(t.Background())
if renderer.borderColorRight { if renderer.borderLeft {
style = style. style = style.BorderLeftForeground(borderColor)
BorderLeftBackground(t.Background()). }
BorderLeftForeground(t.BackgroundPanel()). if renderer.borderRight {
BorderRightForeground(borderColor). style = style.BorderRightForeground(borderColor)
BorderRightBackground(t.Background())
} }
} }
content = style.Render(content) content = style.Render(content)
@@ -223,7 +241,7 @@ func renderText(
if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 { if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
content = content + "\n\n" content = content + "\n\n"
for _, toolCall := range toolCalls { for _, toolCall := range toolCalls {
title := renderToolTitle(toolCall, width) title := renderToolTitle(toolCall, width-2)
style := styles.NewStyle() style := styles.NewStyle()
if toolCall.State.Status == opencode.ToolPartStateStatusError { if toolCall.State.Status == opencode.ToolPartStateStatusError {
style = style.Foreground(t.Error()) style = style.Foreground(t.Error())
@@ -247,7 +265,8 @@ func renderText(
content, content,
width, width,
WithTextColor(t.Text()), WithTextColor(t.Text()),
WithBorderColorRight(t.Secondary()), WithBorderColor(t.Secondary()),
WithBorderRight(),
) )
case opencode.AssistantMessage: case opencode.AssistantMessage:
return renderContentBlock( return renderContentBlock(
@@ -263,6 +282,7 @@ func renderText(
func renderToolDetails( func renderToolDetails(
app *app.App, app *app.App,
toolCall opencode.ToolPart, toolCall opencode.ToolPart,
permission opencode.Permission,
width int, width int,
) string { ) string {
measure := util.Measure("chat.renderToolDetails") measure := util.Measure("chat.renderToolDetails")
@@ -301,6 +321,39 @@ func renderToolDetails(
borderColor := t.BackgroundPanel() borderColor := t.BackgroundPanel()
defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render 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 { if toolCall.State.Metadata != nil {
metadata := toolCall.State.Metadata.(map[string]any) metadata := toolCall.State.Metadata.(map[string]any)
switch toolCall.Tool { switch toolCall.Tool {
@@ -351,12 +404,20 @@ func renderToolDetails(
title := renderToolTitle(toolCall, width) title := renderToolTitle(toolCall, width)
title = style.Render(title) title = style.Render(title)
content := title + "\n" + body content := title + "\n" + body
if permissionContent != "" {
permissionContent = styles.NewStyle().
Background(backgroundColor).
Padding(1, 2).
Render(permissionContent)
content += "\n" + permissionContent
}
content = renderContentBlock( content = renderContentBlock(
app, app,
content, content,
width, width,
WithPadding(0), WithPadding(0),
WithBorderColor(borderColor), WithBorderColor(borderColor),
WithBorderBoth(permission.ID != ""),
) )
return content return content
} }
@@ -417,7 +478,7 @@ func renderToolDetails(
data, _ := json.Marshal(item) data, _ := json.Marshal(item)
var toolCall opencode.ToolPart var toolCall opencode.ToolPart
_ = json.Unmarshal(data, &toolCall) _ = json.Unmarshal(data, &toolCall)
step := renderToolTitle(toolCall, width) step := renderToolTitle(toolCall, width-2)
step = "∟ " + step step = "∟ " + step
steps = append(steps, step) steps = append(steps, step)
} }
@@ -460,7 +521,18 @@ func renderToolDetails(
title := renderToolTitle(toolCall, width) title := renderToolTitle(toolCall, width)
content := title + "\n\n" + body 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 { func renderToolName(name string) string {
@@ -575,6 +647,10 @@ func renderToolTitle(
} }
title = truncate.StringWithTail(title, uint(width-6), "...") title = truncate.StringWithTail(title, uint(width-6), "...")
if toolCall.State.Error != "" {
t := theme.CurrentTheme()
title = styles.NewStyle().Foreground(t.Error()).Render(title)
}
return 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) { 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 var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.MouseClickMsg: case tea.MouseClickMsg:
@@ -199,6 +197,9 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cache.Clear() m.cache.Clear()
cmds = append(cmds, m.renderView()) cmds = append(cmds, m.renderView())
} }
case opencode.EventListResponseEventPermissionUpdated:
m.tail = true
return m, m.renderView()
case renderCompleteMsg: case renderCompleteMsg:
m.partCount = msg.partCount m.partCount = msg.partCount
m.lineCount = msg.lineCount m.lineCount = msg.lineCount
@@ -214,6 +215,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
m.tail = m.viewport.AtBottom() m.tail = m.viewport.AtBottom()
viewport, cmd := m.viewport.Update(msg) viewport, cmd := m.viewport.Update(msg)
m.viewport = viewport m.viewport = viewport
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@@ -465,7 +467,13 @@ func (m *messagesComponent) renderView() tea.Cmd {
revertedToolCount++ revertedToolCount++
continue 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 { if !hasTextPart {
orphanedToolCalls = append(orphanedToolCalls, part) orphanedToolCalls = append(orphanedToolCalls, part)
} }
@@ -477,12 +485,14 @@ func (m *messagesComponent) renderView() tea.Cmd {
part.ID, part.ID,
m.showToolDetails, m.showToolDetails,
width, width,
permission.ID,
) )
content, cached = m.cache.Get(key) content, cached = m.cache.Get(key)
if !cached { if !cached {
content = renderToolDetails( content = renderToolDetails(
m.app, m.app,
part, part,
permission,
width, width,
) )
content = lipgloss.PlaceHorizontal( content = lipgloss.PlaceHorizontal(
@@ -498,6 +508,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
content = renderToolDetails( content = renderToolDetails(
m.app, m.app,
part, part,
permission,
width, width,
) )
content = lipgloss.PlaceHorizontal( content = lipgloss.PlaceHorizontal(
@@ -618,6 +629,40 @@ func (m *messagesComponent) renderView() tea.Cmd {
blocks = append(blocks, content) 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{} final := []string{}
clipboard := []string{} clipboard := []string{}
var selection *selection var selection *selection
@@ -846,9 +891,7 @@ func (m *messagesComponent) View() string {
) )
} }
measure := util.Measure("messages.View")
viewport := m.viewport.View() viewport := m.viewport.View()
measure()
return styles.NewStyle(). return styles.NewStyle().
Background(t.Background()). Background(t.Background()).
Render(m.header + "\n" + viewport) Render(m.header + "\n" + viewport)

View File

@@ -138,8 +138,6 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
) )
} }
case "n": case "n":
s.app.Session = &opencode.Session{}
s.app.Messages = []app.Message{}
return s, tea.Sequence( return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(app.SessionClearedMsg{}), 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) { 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 cmd tea.Cmd
var cmds []tea.Cmd var cmds []tea.Cmd
@@ -113,6 +110,45 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyPressMsg: case tea.KeyPressMsg:
keyString := msg.String() 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 // 1. Handle active modal
if a.modal != nil { if a.modal != nil {
switch keyString { switch keyString {
@@ -341,6 +377,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
updated, cmd := a.editor.Focus() updated, cmd := a.editor.Focus()
a.editor = updated.(chat.EditorComponent) a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case app.SessionClearedMsg:
a.app.Session = &opencode.Session{}
a.app.Messages = []app.Message{}
case dialog.CompletionDialogCloseMsg: case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false a.showCompletionDialog = false
case opencode.EventListResponseEventInstallationUpdated: case opencode.EventListResponseEventInstallationUpdated:
@@ -364,7 +403,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app.Session = &msg.Properties.Info a.app.Session = &msg.Properties.Info
} }
case opencode.EventListResponseEventMessagePartUpdated: 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 { if msg.Properties.Part.SessionID == a.app.Session.ID {
messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
switch casted := m.Info.(type) { switch casted := m.Info.(type) {
@@ -402,7 +441,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
case opencode.EventListResponseEventMessagePartRemoved: 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 { if msg.Properties.SessionID == a.app.Session.ID {
messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
switch casted := m.Info.(type) { switch casted := m.Info.(type) {
@@ -438,7 +477,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
case opencode.EventListResponseEventMessageRemoved: 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 { if msg.Properties.SessionID == a.app.Session.ID {
messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool { messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
switch casted := m.Info.(type) { 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: case opencode.EventListResponseEventSessionError:
switch err := msg.Properties.Error.AsUnion().(type) { switch err := msg.Properties.Error.AsUnion().(type) {
case nil: case nil:
@@ -613,8 +658,6 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (a Model) View() string { func (a Model) View() string {
measure := util.Measure("app.View")
defer measure()
t := theme.CurrentTheme() t := theme.CurrentTheme()
var mainLayout string var mainLayout string
@@ -674,8 +717,6 @@ func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
} }
func (a Model) home() string { func (a Model) home() string {
measure := util.Measure("home.View")
defer measure()
t := theme.CurrentTheme() t := theme.CurrentTheme()
effectiveWidth := a.width - 4 effectiveWidth := a.width - 4
baseStyle := styles.NewStyle().Background(t.Background()) baseStyle := styles.NewStyle().Background(t.Background())
@@ -796,8 +837,6 @@ func (a Model) home() string {
} }
func (a Model) chat() string { func (a Model) chat() string {
measure := util.Measure("chat.View")
defer measure()
effectiveWidth := a.width - 4 effectiveWidth := a.width - 4
t := theme.CurrentTheme() t := theme.CurrentTheme()
editorView := a.editor.View() editorView := a.editor.View()
@@ -911,9 +950,8 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
if a.app.Session.ID == "" { if a.app.Session.ID == "" {
return a, nil return a, nil
} }
a.app.Session = &opencode.Session{}
a.app.Messages = []app.Message{}
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{})) cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
case commands.SessionListCommand: case commands.SessionListCommand:
sessionDialog := dialog.NewSessionDialog(a.app) sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog a.modal = sessionDialog

View File

@@ -1,4 +1,4 @@
configured_endpoints: 26 configured_endpoints: 28
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-90f0ff2a2f214a34b74f49a5909e95c31f617bd9bb881da24ab3fe664424c79d.yml
openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532 openapi_spec_hash: 5ef69219c1869f78455b0c5374f638f8
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3 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#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#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#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> - <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: 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}/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}/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="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="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}/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> - <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="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> - <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 # Tui
Methods: Methods:

View File

@@ -54,8 +54,7 @@ type EventListResponse struct {
// [EventListResponseEventMessageRemovedProperties], // [EventListResponseEventMessageRemovedProperties],
// [EventListResponseEventMessagePartUpdatedProperties], // [EventListResponseEventMessagePartUpdatedProperties],
// [EventListResponseEventMessagePartRemovedProperties], // [EventListResponseEventMessagePartRemovedProperties],
// [EventListResponseEventStorageWriteProperties], // [EventListResponseEventStorageWriteProperties], [Permission],
// [EventListResponseEventPermissionUpdatedProperties],
// [EventListResponseEventFileEditedProperties], // [EventListResponseEventFileEditedProperties],
// [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionDeletedProperties],
@@ -643,7 +642,7 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool {
} }
type EventListResponseEventPermissionUpdated struct { type EventListResponseEventPermissionUpdated struct {
Properties EventListResponseEventPermissionUpdatedProperties `json:"properties,required"` Properties Permission `json:"properties,required"`
Type EventListResponseEventPermissionUpdatedType `json:"type,required"` Type EventListResponseEventPermissionUpdatedType `json:"type,required"`
JSON eventListResponseEventPermissionUpdatedJSON `json:"-"` JSON eventListResponseEventPermissionUpdatedJSON `json:"-"`
} }
@@ -667,56 +666,6 @@ func (r eventListResponseEventPermissionUpdatedJSON) RawJSON() string {
func (r EventListResponseEventPermissionUpdated) implementsEventListResponse() {} 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 type EventListResponseEventPermissionUpdatedType string
const ( const (

View File

@@ -25,6 +25,7 @@ import (
// the [NewSessionService] method instead. // the [NewSessionService] method instead.
type SessionService struct { type SessionService struct {
Options []option.RequestOption Options []option.RequestOption
Permissions *SessionPermissionService
} }
// NewSessionService generates a new service that applies the given options to each // 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) { func NewSessionService(opts ...option.RequestOption) (r *SessionService) {
r = &SessionService{} r = &SessionService{}
r.Options = opts r.Options = opts
r.Permissions = NewSessionPermissionService(opts...)
return return
} }
@@ -100,6 +102,22 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa
return 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 // List messages for a session
func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) { func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) {
opts = append(r.Options[:], opts...) opts = append(r.Options[:], opts...)
@@ -2012,6 +2030,29 @@ func (r userMessageTimeJSON) RawJSON() string {
return r.raw 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 { type SessionMessagesResponse struct {
Info Message `json:"info,required"` Info Message `json:"info,required"`
Parts []Part `json:"parts,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) { func TestSessionMessages(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being") t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010" 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())
}
}