feat: show runtime checklists and support remote Continue
Display native OpenCode and Continue checklists alongside scheduled tasks, preserving request ownership, explicit clears and saved progress. Bundle both remote runtimes and support managed Continue installation, model routing, cancellation and restart continuation. Validation: real local and Linux remote model checks, full Electron remote Continue flows, typecheck and lint passed. Full suite: 4707 passed, 67 skipped, one OpenCode compaction failure passed targeted rerun. Release note: Display native execution checklists above conversations and support managed remote Continue projects with saved progress and cancellation recovery.
This commit is contained in:
+20
-8
@@ -45,6 +45,7 @@ import {
|
||||
type VerifiedRuntimeBundle
|
||||
} from './runtime-bundle-verifier'
|
||||
import { createProductionRuntimeProtocol } from './runtime-composition'
|
||||
import { runContinueAcpHelper } from './continue-acp-helper'
|
||||
import {
|
||||
runOpenCodeModelBridgeHelper,
|
||||
type ModelBridgeProtocol
|
||||
@@ -86,7 +87,7 @@ export type AgentCliDependencies = {
|
||||
loadRegisteredInstallation?: typeof loadRegisteredAgentBundle
|
||||
runtimePaths?: (
|
||||
installationId: string,
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: 'opencode' | 'continue',
|
||||
bundleDigest: string
|
||||
) => ManagedRuntimePaths
|
||||
runtimeReleaseKeyRegistry?: AgentReleaseKeyRegistry
|
||||
@@ -187,6 +188,8 @@ async function runModelBridgeHelper(
|
||||
'supports-image-input',
|
||||
'work-mode',
|
||||
'shared-sessions',
|
||||
'runtime-id',
|
||||
'continue-entrypoint',
|
||||
'opencode-entrypoint'
|
||||
])
|
||||
requireOptions(options, [
|
||||
@@ -194,8 +197,7 @@ async function runModelBridgeHelper(
|
||||
'protocol',
|
||||
'model',
|
||||
'supports-image-input',
|
||||
'work-mode',
|
||||
'opencode-entrypoint'
|
||||
'work-mode'
|
||||
])
|
||||
const protocol = options.protocol
|
||||
if (
|
||||
@@ -216,6 +218,16 @@ async function runModelBridgeHelper(
|
||||
if (options['shared-sessions'] !== undefined && options['shared-sessions'] !== 'true') {
|
||||
throw new Error('Invalid model bridge shared-sessions option')
|
||||
}
|
||||
if (options['runtime-id'] === 'continue') {
|
||||
requireOptions(options, ['continue-entrypoint'])
|
||||
return runContinueAcpHelper({
|
||||
socketPath: options['socket-path']!, protocol, model: options.model!,
|
||||
supportsImageInput: imageInput === 'true', workMode,
|
||||
sharedSessions: options['shared-sessions'] === 'true', entrypoint: options['continue-entrypoint']!
|
||||
})
|
||||
}
|
||||
if (options['runtime-id'] !== undefined && options['runtime-id'] !== 'opencode') throw new Error('Invalid Runtime helper identity')
|
||||
requireOptions(options, ['opencode-entrypoint'])
|
||||
return await (
|
||||
dependencies.runModelBridgeHelper ??
|
||||
runOpenCodeModelBridgeHelper
|
||||
@@ -943,7 +955,7 @@ function resolveInstallationPaths(
|
||||
|
||||
function resolveRuntimePaths(
|
||||
installationId: string,
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: 'opencode' | 'continue',
|
||||
bundleDigest: string,
|
||||
dependencies: AgentCliDependencies
|
||||
): ManagedRuntimePaths {
|
||||
@@ -997,8 +1009,8 @@ function validateInstallationId(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
function validateRuntimeId(value: string): 'opencode' {
|
||||
if (value !== 'opencode') {
|
||||
function validateRuntimeId(value: string): 'opencode' | 'continue' {
|
||||
if (value !== 'opencode' && value !== 'continue') {
|
||||
throw new Error('Invalid Runtime ID')
|
||||
}
|
||||
return value
|
||||
@@ -1022,13 +1034,13 @@ function validateRuntimeArchitecture(
|
||||
|
||||
function assertActivatedRuntimeIdentity(
|
||||
verified: VerifiedRuntimeBundle,
|
||||
runtimeId: 'opencode',
|
||||
runtimeId: 'opencode' | 'continue',
|
||||
bundleDigest: string,
|
||||
architecture: AgentArchitecture
|
||||
): void {
|
||||
if (
|
||||
verified.manifest.runtimeId !== runtimeId ||
|
||||
verified.manifest.provider !== 'opencode' ||
|
||||
verified.manifest.provider !== runtimeId ||
|
||||
verified.manifest.bundleDigest !== bundleDigest ||
|
||||
verified.manifest.architecture !== architecture
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { randomBytes, randomUUID } from 'node:crypto'
|
||||
import { createServer } from 'node:http'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Readable, Writable } from 'node:stream'
|
||||
import { AgentSideConnection, ndJsonStream, PROTOCOL_VERSION, type Agent, type McpServer } from '@agentclientprotocol/sdk'
|
||||
import { ContinueHostAdapter } from '../main/agent/continue-host-adapter'
|
||||
import { promptWithUntrustedConversationHistory } from '../main/agent/runtime-conversation-history'
|
||||
import type { AgentExecutionRequest, AgentImage } from '../main/agent/runtime'
|
||||
import { createUnixModelBridgeExchange } from './model-bridge-broker'
|
||||
import { ModelBridgeLoopbackProxy, MODEL_BRIDGE_SDK_AUTH_SENTINEL, openCodeModelBridgeModelId, type ModelBridgeProtocol } from './model-bridge-helper'
|
||||
|
||||
export async function runContinueAcpHelper(options: {
|
||||
socketPath: string
|
||||
protocol: ModelBridgeProtocol
|
||||
model: string
|
||||
supportsImageInput: boolean
|
||||
workMode: 'ask' | 'execute'
|
||||
sharedSessions?: boolean
|
||||
entrypoint: string
|
||||
}): Promise<number> {
|
||||
const proxy = new ModelBridgeLoopbackProxy({
|
||||
exchange: createUnixModelBridgeExchange({ socketPath: options.socketPath }),
|
||||
sharedSessions: options.sharedSessions
|
||||
})
|
||||
const origin = await proxy.listen()
|
||||
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-continue-'))
|
||||
const questions = new Map<string, { sessionId: string; adapter: ContinueHostAdapter }>()
|
||||
const token = randomBytes(32).toString('base64url')
|
||||
const replies = createServer((request, response) => { void (async () => {
|
||||
if (request.method !== 'POST' || request.url !== `/${token}`) { response.writeHead(404).end(); return }
|
||||
let body = ''
|
||||
for await (const chunk of request) { body += chunk; if (body.length > 64 * 1024) throw new Error('Question reply too large') }
|
||||
const value = JSON.parse(body) as { questionId: string; sessionId: string; answers: string[][] }
|
||||
const pending = questions.get(value.questionId)
|
||||
if (!pending || pending.sessionId !== value.sessionId) { response.writeHead(409).end(); return }
|
||||
await pending.adapter.respondToQuestion(value.questionId, value.answers.length ? value.answers : undefined)
|
||||
questions.delete(value.questionId)
|
||||
response.writeHead(200).end('{}')
|
||||
})().catch(() => response.writeHead(400).end()) })
|
||||
await new Promise<void>((resolve, reject) => { replies.once('error', reject); replies.listen(0, '127.0.0.1', resolve) })
|
||||
const questionEndpoint = `http://127.0.0.1:${(replies.address() as import('node:net').AddressInfo).port}/${token}`
|
||||
const sessions = new Map<string, {
|
||||
cwd: string
|
||||
mcpServers: McpServer[]
|
||||
history: NonNullable<AgentExecutionRequest['history']>
|
||||
active?: { abort: AbortController; adapter: ContinueHostAdapter; finished: Promise<void> }
|
||||
}>()
|
||||
const modelId = openCodeModelBridgeModelId(options.protocol, options.model)
|
||||
const configOptions = [{ id: 'model', name: 'Model', type: 'select' as const, currentValue: modelId,
|
||||
options: [{ value: modelId, name: options.model }] }]
|
||||
const agent: Agent = {
|
||||
initialize: async () => ({ protocolVersion: PROTOCOL_VERSION, agentCapabilities: {
|
||||
loadSession: true, mcpCapabilities: { http: true }, promptCapabilities: { image: true },
|
||||
sessionCapabilities: { resume: {}, close: {} }
|
||||
}, agentInfo: { name: 'GoodBuddy Continue', version: '1.5.47' } }),
|
||||
authenticate: async () => ({}),
|
||||
newSession: async ({ cwd, mcpServers }) => {
|
||||
const sessionId = randomUUID()
|
||||
sessions.set(sessionId, { cwd, mcpServers, history: [] })
|
||||
return { sessionId, configOptions }
|
||||
},
|
||||
loadSession: async ({ sessionId, cwd, mcpServers }) => {
|
||||
const session = sessions.get(sessionId)
|
||||
if (!session || session.cwd !== cwd) throw new Error('Continue Session is unavailable')
|
||||
session.mcpServers = mcpServers
|
||||
return { configOptions }
|
||||
},
|
||||
resumeSession: async ({ sessionId, cwd, mcpServers }) => {
|
||||
const session = sessions.get(sessionId)
|
||||
if (!session || session.cwd !== cwd) throw new Error('Continue Session is unavailable')
|
||||
if (mcpServers) session.mcpServers = mcpServers
|
||||
return { configOptions }
|
||||
},
|
||||
setSessionConfigOption: async ({ sessionId, configId, value }) => {
|
||||
if (!sessions.has(sessionId) || configId !== 'model' || value !== modelId) throw new Error('Continue model selection does not match the prepared model')
|
||||
return { configOptions }
|
||||
},
|
||||
prompt: async ({ sessionId, prompt }) => {
|
||||
const session = sessions.get(sessionId)
|
||||
if (!session || session.active) throw new Error('Continue Session is unavailable or busy')
|
||||
const route = options.sharedSessions ? await fetch(`${origin}/session?sessionId=${encodeURIComponent(sessionId)}`, {
|
||||
signal: AbortSignal.timeout(10_000)
|
||||
}).then(async response => {
|
||||
if (!response.ok) throw new Error('Continue model route is unavailable')
|
||||
return await response.json() as { operationId: string; workMode: 'ask' | 'execute' }
|
||||
}) : undefined
|
||||
const workMode = route?.workMode ?? options.workMode
|
||||
const configPath = join(root, `${sessionId}.json`)
|
||||
await writeFile(configPath, JSON.stringify({ name: 'GoodBuddy', version: '1.0.0', schema: 'v1',
|
||||
mcpServers: workMode === 'execute' ? session.mcpServers.map(server => {
|
||||
if (!('type' in server) || server.type !== 'http') throw new Error('Continue remote MCP requires HTTP')
|
||||
return { name: server.name, type: 'streamable-http', url: server.url,
|
||||
requestOptions: { headers: Object.fromEntries(server.headers.map(header => [header.name, header.value])) } }
|
||||
}) : [] }), { mode: 0o600 })
|
||||
const adapter = new ContinueHostAdapter({
|
||||
binaryPath: options.entrypoint, configPath, workspace: session.cwd, cacheRoot: root, mode: 'chat',
|
||||
modelProfile: { id: 'agent-bridge', name: 'GoodBuddy', modelName: options.model,
|
||||
protocol: options.protocol, authentication: 'api-key', apiKey: MODEL_BRIDGE_SDK_AUTH_SENTINEL,
|
||||
baseUrl: `${origin}/v1`, supportsImageInput: options.supportsImageInput,
|
||||
...(route ? { requestHeaders: { 'x-goodbuddy-session': sessionId, 'x-goodbuddy-operation': route.operationId } } : {}) },
|
||||
launchHost: (entry, args, launch) => spawn(process.execPath, [entry, ...args], { ...launch, stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
})
|
||||
const abort = new AbortController()
|
||||
let finish!: () => void
|
||||
const text = prompt.filter(part => part.type === 'text').map(part => part.text).join('\n')
|
||||
const images = prompt.filter(part => part.type === 'image').map<AgentImage>((part, index) => {
|
||||
if (part.mimeType !== 'image/png' && part.mimeType !== 'image/jpeg') throw new Error('Unsupported Continue image type')
|
||||
return { name: `image-${index}`, mediaType: part.mimeType, data: part.data }
|
||||
})
|
||||
session.active = { abort, adapter, finished: new Promise(resolve => { finish = resolve }) }
|
||||
try {
|
||||
const result = await adapter.run(promptWithUntrustedConversationHistory({ prompt: text, history: session.history }, true), abort.signal,
|
||||
async () => workMode === 'execute' ? 'once' : 'deny', {
|
||||
workMode, images,
|
||||
onEvent: async event => {
|
||||
if (event.type === 'text') await connection.sessionUpdate({ sessionId, update: {
|
||||
sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: event.delta }
|
||||
} })
|
||||
if (event.type === 'checklist') await connection.sessionUpdate({ sessionId, update: {
|
||||
sessionUpdate: 'tool_call_update', toolCallId: 'goodbuddy-native-checklist',
|
||||
_meta: { goodbuddyChecklist: event.checklist }
|
||||
} })
|
||||
if (event.type === 'tool') await connection.sessionUpdate({ sessionId, update: {
|
||||
sessionUpdate: 'tool_call_update', toolCallId: event.tool.callId, title: event.tool.name,
|
||||
status: event.tool.state === 'running' ? 'in_progress' : event.tool.state,
|
||||
rawInput: event.tool.input,
|
||||
content: event.tool.output || event.tool.error ? [{ type: 'content', content: {
|
||||
type: 'text', text: event.tool.error ?? event.tool.output!
|
||||
} }] : []
|
||||
} })
|
||||
if (event.type === 'question') {
|
||||
questions.set(event.questionId, { sessionId, adapter })
|
||||
await connection.sessionUpdate({ sessionId, update: {
|
||||
sessionUpdate: 'tool_call_update', toolCallId: event.questionId,
|
||||
_meta: { goodbuddyQuestion: { endpoint: questionEndpoint,
|
||||
question: { id: event.questionId, sessionID: sessionId, questions: event.questions } } }
|
||||
} })
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!result.streamedText && result.text) await connection.sessionUpdate({ sessionId, update: {
|
||||
sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: result.text }
|
||||
} })
|
||||
session.history.push({ role: 'user', content: text }, { role: 'assistant', content: result.text })
|
||||
return { stopReason: 'end_turn', ...(result.usage ? { usage: {
|
||||
inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens,
|
||||
totalTokens: result.usage.inputTokens + result.usage.outputTokens,
|
||||
cachedReadTokens: result.usage.cacheReadTokens, cachedWriteTokens: result.usage.cacheWriteTokens
|
||||
} } : {}) }
|
||||
} catch (error) {
|
||||
if (abort.signal.aborted) return { stopReason: 'cancelled' }
|
||||
throw error
|
||||
} finally {
|
||||
await adapter.dispose()
|
||||
for (const [id, pending] of questions) if (pending.adapter === adapter) questions.delete(id)
|
||||
await rm(configPath, { force: true })
|
||||
session.active = undefined
|
||||
finish()
|
||||
}
|
||||
},
|
||||
cancel: async ({ sessionId }) => {
|
||||
const active = sessions.get(sessionId)?.active
|
||||
active?.abort.abort(new DOMException('Cancelled', 'AbortError'))
|
||||
await active?.finished
|
||||
},
|
||||
closeSession: async ({ sessionId }) => {
|
||||
await agent.cancel({ sessionId })
|
||||
sessions.delete(sessionId)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const connection = new AgentSideConnection(() => agent, ndJsonStream(
|
||||
Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)
|
||||
))
|
||||
try {
|
||||
if (options.sharedSessions) await connection.extNotification('goodbuddy/modelBridgeReady', { origin })
|
||||
await connection.closed
|
||||
return 0
|
||||
} finally {
|
||||
for (const session of sessions.values()) session.active?.abort.abort()
|
||||
await Promise.all([...sessions.values()].map(session => session.active?.finished))
|
||||
await proxy.close()
|
||||
replies.closeAllConnections()
|
||||
await new Promise<void>(resolve => replies.close(() => resolve()))
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,34 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('OpenCode direct launch profile', () => {
|
||||
it.each(['ask', 'execute'] as const)('launches Continue through the managed HTTP facade in %s', (workMode) => {
|
||||
const fixture = createFixture()
|
||||
const manifest = remoteRuntimeBundleManifestSchema.parse({
|
||||
...fixture.manifest, runtimeId: 'continue', provider: 'continue', runtimeVersion: '1.5.47',
|
||||
files: fixture.manifest.files.map(file => file.path === 'bin/opencode' ? { ...file, path: 'lib/continue/dist/cn.js' } : file),
|
||||
entrypoint: { ...fixture.manifest.entrypoint, identity: 'continue-acp', path: 'lib/continue/dist/cn.js', argvPrefix: [] }
|
||||
})
|
||||
expect(() => createOpenCodeLaunchProfile({ ...fixture, manifest, workMode })).toThrow(/model bridge/)
|
||||
const profile = createOpenCodeLaunchProfile({ ...fixture, manifest, workMode,
|
||||
modelBridge: {
|
||||
agentExecutablePath: resolve(fixture.bundleDirectory, 'agent', 'goodbuddy-agent'),
|
||||
bridgeDirectory: resolve(fixture.bundleDirectory, 'bridge'),
|
||||
socketPath: resolve(fixture.bundleDirectory, 'bridge', 'model.sock'),
|
||||
sharedSessions: true,
|
||||
policy: { protocol: 'openai-responses', model: 'selected-model',
|
||||
modelProfileDigest: `sha256:${'9'.repeat(64)}`, supportsImageInput: true }
|
||||
}
|
||||
})
|
||||
expect(profile.args).toEqual(expect.arrayContaining([
|
||||
'model-bridge-helper', '--runtime-id', 'continue', '--continue-entrypoint',
|
||||
resolve(fixture.bundleDirectory, 'lib/continue/dist/cn.js'), '--work-mode', workMode,
|
||||
'--shared-sessions', 'true'
|
||||
]))
|
||||
expect(profile.args).not.toContain('acp')
|
||||
expect(profile.cwd).toBe(fixture.workspaceDirectory)
|
||||
expect(profile.env.OPENCODE_CONFIG_CONTENT === process.env.OPENCODE_CONFIG_CONTENT).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['ask', 'execute'] as const)(
|
||||
'disables unused automatic Git snapshots for %s',
|
||||
(workMode) => {
|
||||
|
||||
@@ -45,15 +45,20 @@ export function createOpenCodeLaunchProfile(input: {
|
||||
};
|
||||
}): OpenCodeLaunchProfile {
|
||||
const manifest = remoteRuntimeBundleManifestSchema.parse(input.manifest);
|
||||
const isContinue = manifest.runtimeId === "continue" && manifest.provider === "continue";
|
||||
if (
|
||||
manifest.runtimeId !== "opencode" ||
|
||||
!isContinue && (manifest.runtimeId !== "opencode" ||
|
||||
manifest.provider !== "opencode" ||
|
||||
manifest.entrypoint.identity !== "opencode-acp" ||
|
||||
manifest.entrypoint.argvPrefix.length !== 1 ||
|
||||
manifest.entrypoint.argvPrefix[0] !== "acp"
|
||||
manifest.entrypoint.argvPrefix[0] !== "acp")
|
||||
) {
|
||||
throw new Error("Runtime manifest is not the fixed OpenCode ACP profile");
|
||||
}
|
||||
if (isContinue && (manifest.entrypoint.identity !== "continue-acp" ||
|
||||
manifest.entrypoint.path !== "lib/continue/dist/cn.js" || manifest.entrypoint.argvPrefix.length !== 0 || !input.modelBridge)) {
|
||||
throw new Error("Continue requires its managed ACP facade and model bridge");
|
||||
}
|
||||
if (
|
||||
[...manifest.allowedEnvironmentNames].sort().join("\0") !==
|
||||
[...OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES].sort().join("\0")
|
||||
@@ -135,25 +140,26 @@ export function createOpenCodeLaunchProfile(input: {
|
||||
"--work-mode",
|
||||
input.workMode,
|
||||
...(modelBridge.sharedSessions ? ["--shared-sessions", "true"] : []),
|
||||
"--opencode-entrypoint",
|
||||
...(isContinue ? ["--runtime-id", "continue"] : []),
|
||||
isContinue ? "--continue-entrypoint" : "--opencode-entrypoint",
|
||||
executablePath,
|
||||
],
|
||||
};
|
||||
|
||||
const environmentBase = {
|
||||
...executeEnvironment(process.env, workspaceDirectory),
|
||||
...OPEN_CODE_RUNTIME_ENVIRONMENT,
|
||||
OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode"),
|
||||
...(isContinue ? {} : { ...OPEN_CODE_RUNTIME_ENVIRONMENT,
|
||||
OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode") }),
|
||||
};
|
||||
const permission = input.workMode === "ask" ? "ask" : "allow";
|
||||
const environment = {
|
||||
...environmentBase,
|
||||
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
||||
...(isContinue ? {} : { OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
||||
// Native snapshot summaries are not consumed by GoodBuddy.
|
||||
snapshot: false,
|
||||
permission,
|
||||
agent: { build: { permission } },
|
||||
}),
|
||||
}) }),
|
||||
};
|
||||
return {
|
||||
...runtimeCommand,
|
||||
|
||||
@@ -5,6 +5,31 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { openCodeSubagentPluginSource } from './opencode-subagent-plugin'
|
||||
|
||||
describe('OpenCode ACP child event plugin', () => {
|
||||
it('forwards native todos intact under their own Session and stops after disposal', async () => {
|
||||
const write = vi.fn()
|
||||
const factory = runInNewContext(
|
||||
`(${openCodeSubagentPluginSource().replace(/^import .*\n/gmu, '').replace('export default ', '')})`,
|
||||
{ process: { stdout: { write } }, createServer, randomBytes }
|
||||
)
|
||||
const hooks = await factory()
|
||||
const items = [{ content: 'long'.repeat(2000), status: 'cancelled', priority: 'low' }]
|
||||
for (const sessionID of ['root', 'child']) {
|
||||
for (const todos of [items, []]) {
|
||||
const event = { type: 'todo.updated', properties: { sessionID, todos } }
|
||||
await hooks.event({ event })
|
||||
expect(JSON.parse(write.mock.calls.at(-1)![0]).params).toEqual({
|
||||
sessionId: sessionID, update: {
|
||||
sessionUpdate: 'tool_call_update', toolCallId: 'goodbuddy-native-todos',
|
||||
_meta: { goodbuddyTodoEvent: event }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
await hooks.dispose()
|
||||
await hooks.event({ event: { type: 'todo.updated', properties: { sessionID: 'root', todos: [] } } })
|
||||
expect(write).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('hides foreign image tools and removes the current image tool in Ask on a reused Session', async () => {
|
||||
let mode = 'execute'
|
||||
const imageToolName = `goodbuddy_image_${'a'.repeat(24)}`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* OpenCode's ACP adapter omits native questions and child sessions. Native
|
||||
* OpenCode's ACP adapter omits native todos, questions and child sessions. Native
|
||||
* events use ACP metadata; question replies use a process-local capability.
|
||||
*/
|
||||
export function openCodeSubagentPluginSource(modelBridgeOrigin?: string): string {
|
||||
@@ -155,6 +155,7 @@ async function plugin(input?: {
|
||||
requestID?: string
|
||||
status?: { type?: string }
|
||||
sessionID?: string
|
||||
todos?: unknown
|
||||
part?: {
|
||||
sessionID?: string
|
||||
type: string
|
||||
@@ -168,6 +169,15 @@ async function plugin(input?: {
|
||||
} }) => {
|
||||
const properties = event.properties
|
||||
if (closed) return
|
||||
if (event.type === 'todo.updated' && properties.sessionID) {
|
||||
// Keep the native Session identity: child todos must not become root todos.
|
||||
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', method: 'session/update', params: {
|
||||
sessionId: properties.sessionID,
|
||||
update: { sessionUpdate: 'tool_call_update', toolCallId: 'goodbuddy-native-todos',
|
||||
_meta: { goodbuddyTodoEvent: event } }
|
||||
} }) + '\n')
|
||||
return
|
||||
}
|
||||
if (event.type === 'session.status' && properties.status?.type === 'idle' && properties.sessionID) {
|
||||
for (const [key, route] of modelMessages) {
|
||||
if (route.sessionId === properties.sessionID) modelMessages.delete(key)
|
||||
|
||||
@@ -2638,7 +2638,7 @@ export class RuntimeAcpBackend {
|
||||
): void {
|
||||
const manifest = verified.manifest
|
||||
if (
|
||||
manifest.provider !== 'opencode' ||
|
||||
(manifest.provider !== 'opencode' && manifest.provider !== 'continue') ||
|
||||
manifest.runtimeId !== preparation.runtimeId ||
|
||||
manifest.bundleDigest !== preparation.runtimeBundleDigest ||
|
||||
manifest.adapterDigest !== preparation.runtimeAdapterDigest ||
|
||||
|
||||
Reference in New Issue
Block a user