mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
refactor(cli): inline acp one-off helpers
This commit is contained in:
@@ -32,7 +32,10 @@ export function buildConfigOptions(input: {
|
||||
modes?: readonly ConfigOptionMode[]
|
||||
currentModeId?: string
|
||||
}): SessionConfigOption[] {
|
||||
const variants = variantsForModel(input.providers, input.currentModel)
|
||||
const variants =
|
||||
input.providers
|
||||
.find((provider) => provider.id === input.currentModel.providerID)
|
||||
?.models.find((model) => model.id === input.currentModel.modelID)?.variants ?? []
|
||||
const effort =
|
||||
variants.length > 0 ? buildEffortSelectOption({ variants, currentVariant: input.currentVariant }) : undefined
|
||||
return [
|
||||
@@ -121,13 +124,6 @@ export function formatVariantName(variant: string) {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function variantsForModel(providers: readonly ConfigOptionProvider[], model: ModelSelection["model"]) {
|
||||
return (
|
||||
providers.find((provider) => provider.id === model.providerID)?.models.find((item) => item.id === model.modelID)
|
||||
?.variants ?? []
|
||||
)
|
||||
}
|
||||
|
||||
function selectVariant(variant: string | undefined, variants: readonly string[]) {
|
||||
if (variant && variants.includes(variant)) return variant
|
||||
if (variants.includes(DEFAULT_VARIANT_VALUE)) return DEFAULT_VARIANT_VALUE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk"
|
||||
import type { ContentBlock, ContentChunk, ResourceLink } from "@agentclientprotocol/sdk"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
|
||||
@@ -14,8 +14,16 @@ export function promptContentToParts(content: readonly ContentBlock[]): PromptPa
|
||||
|
||||
export function contentBlockToParts(block: ContentBlock): PromptPart[] {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return [{ type: "text", text: block.text, ...audienceFlags(block.annotations?.audience ?? undefined) }]
|
||||
case "text": {
|
||||
const audience = block.annotations?.audience
|
||||
if (audience?.length === 1 && audience[0] === "assistant") {
|
||||
return [{ type: "text", text: block.text, synthetic: true }]
|
||||
}
|
||||
if (audience?.length === 1 && audience[0] === "user") {
|
||||
return [{ type: "text", text: block.text, ignored: true }]
|
||||
}
|
||||
return [{ type: "text", text: block.text }]
|
||||
}
|
||||
case "image":
|
||||
if (block.data) {
|
||||
return [
|
||||
@@ -84,7 +92,8 @@ export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk
|
||||
content: {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
...replayAudience(part),
|
||||
...(part.synthetic ? { annotations: { audience: ["assistant" as const] } } : {}),
|
||||
...(!part.synthetic && part.ignored ? { annotations: { audience: ["user" as const] } } : {}),
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -105,15 +114,17 @@ export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk
|
||||
]
|
||||
}
|
||||
if (!part.url.startsWith("data:")) return []
|
||||
const data = decodeDataUrl(part.url)
|
||||
if (!data) return []
|
||||
if (data.mime.startsWith("image/")) {
|
||||
const match = /^data:([^;]+);base64,(.*)$/.exec(part.url)
|
||||
if (!match?.[1] || match[2] === undefined) return []
|
||||
const mime = match[1]
|
||||
const data = match[2]
|
||||
if (mime.startsWith("image/")) {
|
||||
return [
|
||||
{
|
||||
content: {
|
||||
type: "image",
|
||||
mimeType: data.mime,
|
||||
data: data.base64,
|
||||
mimeType: mime,
|
||||
data,
|
||||
uri: pathToFileURL(part.filename ?? "image").href,
|
||||
},
|
||||
},
|
||||
@@ -124,16 +135,16 @@ export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk
|
||||
content: {
|
||||
type: "resource",
|
||||
resource:
|
||||
data.mime.startsWith("text/") || data.mime === "application/json"
|
||||
mime.startsWith("text/") || mime === "application/json"
|
||||
? {
|
||||
uri: pathToFileURL(part.filename ?? "file").href,
|
||||
mimeType: data.mime,
|
||||
text: Buffer.from(data.base64, "base64").toString("utf8"),
|
||||
mimeType: mime,
|
||||
text: Buffer.from(data, "base64").toString("utf8"),
|
||||
}
|
||||
: {
|
||||
uri: pathToFileURL(part.filename ?? "file").href,
|
||||
mimeType: data.mime,
|
||||
blob: data.base64,
|
||||
mimeType: mime,
|
||||
blob: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -163,24 +174,6 @@ function resourceLinkToPart(link: ResourceLink): PromptPart {
|
||||
return { type: "text", text: link.uri }
|
||||
}
|
||||
|
||||
function decodeDataUrl(url: string): { readonly mime: string; readonly base64: string } | undefined {
|
||||
const match = /^data:([^;]+);base64,(.*)$/.exec(url)
|
||||
if (!match?.[1] || match[2] === undefined) return undefined
|
||||
return { mime: match[1], base64: match[2] }
|
||||
}
|
||||
|
||||
function audienceFlags(audience: readonly Role[] | null | undefined) {
|
||||
if (audience?.length === 1 && audience[0] === "assistant") return { synthetic: true as const }
|
||||
if (audience?.length === 1 && audience[0] === "user") return { ignored: true as const }
|
||||
return {}
|
||||
}
|
||||
|
||||
function replayAudience(part: Extract<PromptPart, { type: "text" }>) {
|
||||
if (part.synthetic) return { annotations: { audience: ["assistant" as const] } }
|
||||
if (part.ignored) return { annotations: { audience: ["user" as const] } }
|
||||
return {}
|
||||
}
|
||||
|
||||
function filenameFromUri(uri: string | undefined): string | undefined {
|
||||
if (!uri || uri.startsWith("data:")) return undefined
|
||||
if (URL.canParse(uri)) return path.basename(new URL(uri).pathname) || undefined
|
||||
|
||||
@@ -311,7 +311,12 @@ async function replayMessage(
|
||||
sessionId: sessionID,
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
...pendingToolCall({ toolCallId: part.id, toolName: part.name, state: { input: toolInput(part) }, cwd }),
|
||||
...pendingToolCall({
|
||||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
state: { input: part.state.status === "streaming" ? {} : part.state.input },
|
||||
cwd,
|
||||
}),
|
||||
},
|
||||
})
|
||||
switch (part.state.status) {
|
||||
@@ -376,10 +381,6 @@ function matchesStart(event: EventSubscribeOutput, start: TurnStart) {
|
||||
return event.type === "session.skill.activated" && event.id === start.id.replace(/^msg_/, "evt_")
|
||||
}
|
||||
|
||||
function toolInput(tool: Extract<SessionMessageAssistant["content"][number], { type: "tool" }>) {
|
||||
return tool.state.status === "streaming" ? {} : tool.state.input
|
||||
}
|
||||
|
||||
function response(
|
||||
assistant: SessionMessageAssistant | undefined,
|
||||
executionError: { readonly type: string; readonly message: string } | undefined,
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import type {
|
||||
AgentSideConnection,
|
||||
PermissionOption,
|
||||
RequestPermissionResponse,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import type { AgentSideConnection, PermissionOption, ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk"
|
||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import { Patch } from "@opencode-ai/core/patch"
|
||||
import { isAbsolute, resolve } from "node:path"
|
||||
@@ -47,10 +41,12 @@ export async function replyPermission(input: {
|
||||
options,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
const selected = result?.outcome.outcome === "selected" ? result.outcome.optionId : undefined
|
||||
const reply = selected === "once" || selected === "always" ? selected : "reject"
|
||||
await input.client.permission.reply({
|
||||
sessionID: input.sessionID,
|
||||
requestID: input.event.data.id,
|
||||
reply: selectedReply(result),
|
||||
reply,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +59,15 @@ export async function syncEditedFiles(input: {
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
}) {
|
||||
if (!input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return
|
||||
const paths = editedPaths(input.toolInput, input.structured)
|
||||
const files = Array.isArray(input.structured.files)
|
||||
? input.structured.files.flatMap((file): string[] => {
|
||||
if (!file || typeof file !== "object") return []
|
||||
const path = Reflect.get(file, "file")
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
: []
|
||||
const path = filePath(input.toolInput)
|
||||
const paths = [...new Set([...files, ...(path ? [path] : [])])]
|
||||
await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const target = resolvePath(path, input.cwd)
|
||||
@@ -161,18 +165,6 @@ function readText(path: string, cwd: string) {
|
||||
.catch(() => "")
|
||||
}
|
||||
|
||||
function editedPaths(input: ToolInput, structured: Readonly<Record<string, unknown>>) {
|
||||
const files = Array.isArray(structured.files)
|
||||
? structured.files.flatMap((file): string[] => {
|
||||
if (!file || typeof file !== "object") return []
|
||||
const path = Reflect.get(file, "file")
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
: []
|
||||
const path = filePath(input)
|
||||
return [...new Set([...files, ...(path ? [path] : [])])]
|
||||
}
|
||||
|
||||
function filePath(input: ToolInput) {
|
||||
return stringValue(input.path) ?? stringValue(input.filePath) ?? stringValue(input.filepath)
|
||||
}
|
||||
@@ -181,12 +173,6 @@ function resolvePath(path: string, cwd: string) {
|
||||
return isAbsolute(path) ? path : resolve(cwd, path)
|
||||
}
|
||||
|
||||
function selectedReply(result: RequestPermissionResponse | undefined): "once" | "always" | "reject" {
|
||||
if (result?.outcome.outcome !== "selected") return "reject"
|
||||
if (result.outcome.optionId === "once" || result.outcome.optionId === "always") return result.outcome.optionId
|
||||
return "reject"
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
@@ -127,7 +127,18 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||
}
|
||||
sessions.set(session.id, state)
|
||||
await registerMcpServers(input.client, registeredMcp, state, mcpServers)
|
||||
await sendAvailableCommands(input.connection, state)
|
||||
await input.connection.sessionUpdate({
|
||||
sessionId: state.id,
|
||||
update: {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands: [
|
||||
...state.catalog.commands,
|
||||
...state.catalog.skills.filter(
|
||||
(skill) => !state.catalog.commands.some((command) => command.name === skill.name),
|
||||
),
|
||||
].map((command) => ({ name: command.name, description: command.description ?? "" })),
|
||||
},
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -491,21 +502,6 @@ function stableStringify(value: unknown): string {
|
||||
.join(",")}}`
|
||||
}
|
||||
|
||||
async function sendAvailableCommands(connection: Connection, session: Attached) {
|
||||
await connection.sessionUpdate({
|
||||
sessionId: session.id,
|
||||
update: {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands: [
|
||||
...session.catalog.commands,
|
||||
...session.catalog.skills.filter(
|
||||
(skill) => !session.catalog.commands.some((command) => command.name === skill.name),
|
||||
),
|
||||
].map((command) => ({ name: command.name, description: command.description ?? "" })),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function sendUsageUpdate(client: OpenCodeClient, connection: Connection, session: Attached) {
|
||||
const info = await client.session.get({ sessionID: session.id })
|
||||
const model = session.catalog.models.find(
|
||||
|
||||
@@ -103,31 +103,35 @@ export function completedToolUpdate(input: {
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
readonly result?: unknown
|
||||
}): ToolCallUpdate {
|
||||
return {
|
||||
toolCallId: input.toolCallId,
|
||||
status: "completed",
|
||||
content: completedToolContent(input.toolName, input.input, input.content, input.structured),
|
||||
rawOutput: {
|
||||
structured: input.structured,
|
||||
...(input.result === undefined ? {} : { result: input.result }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function completedToolContent(
|
||||
toolName: string,
|
||||
input: ToolInput,
|
||||
content: ToolContent,
|
||||
structured: Readonly<Record<string, unknown>>,
|
||||
): ToolCallContent[] {
|
||||
const normalized = toolContent(content)
|
||||
const read = toolName.toLocaleLowerCase() === "read" ? readDisplayText(structured) : undefined
|
||||
const normalized = toolContent(input.content)
|
||||
const read = input.toolName.toLocaleLowerCase() === "read" ? readDisplayText(input.structured) : undefined
|
||||
const images = normalized.filter((part) => part.type === "content" && part.content.type === "image")
|
||||
const primary =
|
||||
read === undefined
|
||||
? normalized.filter((part) => !images.includes(part))
|
||||
: [{ type: "content" as const, content: { type: "text" as const, text: read } }]
|
||||
return [...primary, ...diffContent(input), ...images]
|
||||
const oldText = stringValue(input.input.oldString)
|
||||
const newText = stringValue(input.input.newString)
|
||||
const diff: ToolCallContent[] =
|
||||
oldText === undefined || newText === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: "diff",
|
||||
path: stringValue(input.input.path) ?? stringValue(input.input.filePath) ?? "",
|
||||
oldText,
|
||||
newText,
|
||||
},
|
||||
]
|
||||
return {
|
||||
toolCallId: input.toolCallId,
|
||||
status: "completed",
|
||||
content: [...primary, ...diff, ...images],
|
||||
rawOutput: {
|
||||
structured: input.structured,
|
||||
...(input.result === undefined ? {} : { result: input.result }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function errorToolUpdate(input: {
|
||||
@@ -160,20 +164,6 @@ function toolContent(content: ToolContent): ToolCallContent[] {
|
||||
})
|
||||
}
|
||||
|
||||
function diffContent(input: ToolInput): ToolCallContent[] {
|
||||
const oldText = stringValue(input.oldString)
|
||||
const newText = stringValue(input.newString)
|
||||
if (oldText === undefined || newText === undefined) return []
|
||||
return [
|
||||
{
|
||||
type: "diff",
|
||||
path: stringValue(input.path) ?? stringValue(input.filePath) ?? "",
|
||||
oldText,
|
||||
newText,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function readDisplayText(structured: Readonly<Record<string, unknown>>) {
|
||||
if (typeof structured.content === "string") {
|
||||
if (structured.type === "text-page" || structured.encoding === "utf8") return structured.content
|
||||
|
||||
Reference in New Issue
Block a user