mini: add reconnect, forms, and shared targets (#37811)

Centralize session target resolution for mini and noninteractive
run paths. Recover from transport drops, replace questions with
forms, and keep tool/catalog state location-scoped with live
progress and theme discovery.
This commit is contained in:
Simon Klee 2026-07-19 22:45:10 +02:00 committed by GitHub
commit 925c2423de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 5631 additions and 4292 deletions

View file

@ -13,8 +13,7 @@
//
// permissionInfo() extracts display info (icon, title, lines, diff) from
// the request, delegating to tool.ts for tool-specific formatting.
import type { PermissionV2Request } from "@opencode-ai/client/promise"
import type { PermissionReply } from "./types"
import type { MiniPermissionRequest, PermissionReply } from "./types"
import { toolPath, toolPermissionInfo } from "./tool"
type Dict = Record<string, unknown>
@ -24,6 +23,7 @@ export type PermissionOption = "once" | "always" | "reject" | "confirm" | "cance
export type PermissionBodyState = {
requestID: string
sessionID: string
stage: PermissionStage
selected: PermissionOption
message: string
@ -35,6 +35,7 @@ export type PermissionInfo = {
title: string
lines: string[]
diff?: string
patch?: string
file?: string
}
@ -55,21 +56,26 @@ function text(v: unknown): string {
return typeof v === "string" ? v : ""
}
function data(request: PermissionV2Request): Dict {
const meta = dict(request.metadata)
return {
...meta,
...dict(meta.input),
function data(request: MiniPermissionRequest): { input: Dict; metadata: Dict } {
const state = request.tool?.state
const metadata = {
...(state && state.status !== "streaming" ? dict(state.structured) : {}),
...dict(request.metadata),
}
if (!state || state.status === "streaming") return { input: {}, metadata }
return { input: dict(state.input), metadata }
}
function patterns(request: PermissionV2Request): string[] {
function patterns(request: MiniPermissionRequest): string[] {
return request.resources.filter((item): item is string => typeof item === "string")
}
export function createPermissionBodyState(requestID: string): PermissionBodyState {
export function createPermissionBodyState(
request: Pick<MiniPermissionRequest, "id" | "sessionID">,
): PermissionBodyState {
return {
requestID,
requestID: request.id,
sessionID: request.sessionID,
stage: "permission",
selected: "once",
message: "",
@ -89,10 +95,10 @@ export function permissionOptions(stage: PermissionStage): PermissionOption[] {
return []
}
export function permissionInfo(request: PermissionV2Request): PermissionInfo {
export function permissionInfo(request: MiniPermissionRequest, directory?: string): PermissionInfo {
const pats = patterns(request)
const input = data(request)
const info = toolPermissionInfo(request.action, input, dict(request.metadata), pats)
const source = data(request)
const info = toolPermissionInfo(request.action, source.input, source.metadata, pats, directory)
if (info) {
return info
}
@ -103,7 +109,7 @@ export function permissionInfo(request: PermissionV2Request): PermissionInfo {
const dir = raw.includes("*") ? raw.slice(0, raw.indexOf("*")).replace(/[\\/]+$/, "") : raw
return {
icon: "←",
title: `Access external directory ${toolPath(dir, { home: true })}`,
title: `Access external directory ${toolPath(dir, { home: true, directory })}`,
lines: pats.map((item) => `- ${item}`),
}
}
@ -123,16 +129,13 @@ export function permissionInfo(request: PermissionV2Request): PermissionInfo {
}
}
export function permissionAlwaysLines(request: PermissionV2Request): string[] {
export function permissionAlwaysLines(request: MiniPermissionRequest): string[] {
const save = request.save ?? []
if (save.length === 1 && save[0] === "*") {
return [`This will allow ${request.action} until OpenCode is restarted.`]
}
return [
"This will allow the following patterns until OpenCode is restarted.",
...save.map((item) => `- ${item}`),
]
return ["This will allow the following patterns until OpenCode is restarted.", ...save.map((item) => `- ${item}`)]
}
export function permissionLabel(option: PermissionOption): string {
@ -143,8 +146,14 @@ export function permissionLabel(option: PermissionOption): string {
return "Cancel"
}
export function permissionReply(requestID: string, reply: PermissionReply["reply"], message?: string): PermissionReply {
export function permissionReply(
sessionID: string,
requestID: string,
reply: PermissionReply["reply"],
message?: string,
): PermissionReply {
return {
sessionID,
requestID,
reply,
...(message && message.trim() ? { message: message.trim() } : {}),
@ -203,7 +212,7 @@ export function permissionRun(state: PermissionBodyState, requestID: string, opt
return {
state,
reply: permissionReply(requestID, "once"),
reply: permissionReply(state.sessionID, requestID, "once"),
}
}
@ -223,7 +232,7 @@ export function permissionRun(state: PermissionBodyState, requestID: string, opt
return {
state,
reply: permissionReply(requestID, "always"),
reply: permissionReply(state.sessionID, requestID, "always"),
}
}
@ -232,7 +241,7 @@ export function permissionReject(state: PermissionBodyState, requestID: string):
return undefined
}
return permissionReply(requestID, "reject", state.message)
return permissionReply(state.sessionID, requestID, "reject", state.message)
}
export function permissionCancel(state: PermissionBodyState): PermissionBodyState {