Files
orca/src/main/opencode/opencode-structured-account-home.ts
T
Brennan Benson 76d1c68ab5 Resume in New Native Chat for OpenCode (#28072)
* feat(chat): Resume in New Native Chat for OpenCode terminal conversations

An OpenCode conversation started in the terminal now offers "Resume in New Native Chat" in its
Session History row menu and its tab's right-click menu, as Claude, Codex, Grok and OMP do: a new
OpenCode chat takes it over, showing its history and continuing it. OpenCode 2.x rows
(`opencode2`) and 1.x rows both open an OpenCode chat.

Two pieces kept OpenCode off the shared adoption path. Its account is a locator, not a folder, so
the transcript import now works over the account the chat pins, whatever its kind; the folder
agents wrap their lookups in directoryTranscriptImport, unchanged. Its conversations live in a
database, so the journal import reads the found database with the reader the OpenCode transcript
view already uses. OpenCode refuses to reopen a session from another folder, so the workspace rule
covers it too.

* test(chat): Pi is now the agent with no transcript import

* fix(chat): find an OpenCode 2.x terminal tab's Session History row

The pane reports `opencode` while Session History lists the conversation as `opencode2`, so the pane
header's and tab menu's Resume in New Native Chat found no row. Match by the chat agent that takes the
conversation over.

* Fix OpenCode chat adoption environment and pane eligibility

* test(chat): adapt parent adoption fixtures to account identities

* test: keep only the tests that pin user-visible behavior

* refactor(chat): map a Session History row to its chat agent where the chat starts

Both Resume in New Native Chat callers repeated the row-to-chat agent lookup. The launch now
takes the row agent and maps it once, so the Session History actions file stays unchanged.
2026-10-14 00:25:08 -07:00

96 lines
3.3 KiB
TypeScript

import {
isLegacyAgentSessionAccountHome,
type OpenCodeAgentSessionAccountHome
} from '../../shared/agent-session-account-home'
import {
getManagedDataAccountService,
type ManagedDataAccountService
} from '../managed-data-accounts/service'
import type { AcpAccountBinding } from '../acp/acp-account-binding'
type AccountReader = Pick<
ManagedDataAccountService,
'list' | 'restoreOriginalEnvironment' | 'environmentForAccount'
>
function restoredEnvironment(
environment: NodeJS.ProcessEnv,
managedAccounts: Pick<AccountReader, 'restoreOriginalEnvironment'>
): Record<string, string | undefined> {
const copy = { ...environment }
managedAccounts.restoreOriginalEnvironment(copy)
delete copy.ORCA_DATA_ACCOUNT_PROVIDER
delete copy.ORCA_DATA_ACCOUNT_DATA_HOME
delete copy.ORCA_DATA_ACCOUNT_STATE_HOME
delete copy.ORCA_DATA_ACCOUNT_ORIGINAL_ENV
return copy
}
function definedEnvironment(
environment: Record<string, string | undefined>
): Record<string, string> {
const defined: Record<string, string> = {}
for (const [key, value] of Object.entries(environment)) {
if (value !== undefined) {
defined[key] = value
}
}
return defined
}
/** Resolve the current selection on the execution host when creating a chat. */
export function resolveStructuredOpenCodeAccountHome(input: {
managedAccounts: AccountReader
}): OpenCodeAgentSessionAccountHome {
const selected = input.managedAccounts.list('opencode').activeAccountId
if (selected) {
input.managedAccounts.environmentForAccount('opencode', selected)
return { kind: 'opencode', locator: { kind: 'managed', managedProfileId: selected } }
}
return { kind: 'opencode', locator: { kind: 'unmanaged' } }
}
/** Resolve a saved binding against the current host, then apply it after inherited overlays. */
export function environmentForStructuredOpenCodeAccountHome(
binding: OpenCodeAgentSessionAccountHome,
input: {
managedAccounts: Pick<AccountReader, 'environmentForAccount' | 'restoreOriginalEnvironment'>
baseEnvironment: NodeJS.ProcessEnv
}
): Record<string, string> {
const environment = restoredEnvironment(input.baseEnvironment, input.managedAccounts)
const locator = binding.locator
// Unmanaged: the user's own environment reaches OpenCode as-is.
if (locator.kind === 'unmanaged') {
return definedEnvironment(environment)
}
delete environment.XDG_DATA_HOME
delete environment.XDG_STATE_HOME
delete environment.OPENCODE_DB
delete environment.OPENCODE_AUTH_CONTENT
return {
...definedEnvironment(environment),
...input.managedAccounts.environmentForAccount('opencode', locator.managedProfileId)
}
}
/** OpenCode's account over ACP: a managed profile, or the user's own environment. */
export function openCodeAcpAccountBinding(
managedAccounts: () => AccountReader = getManagedDataAccountService
): AcpAccountBinding {
return {
pin: { accountLocatorKind: 'opencode' },
resolve: async () =>
resolveStructuredOpenCodeAccountHome({ managedAccounts: managedAccounts() }),
environment: (home, env) => {
if (isLegacyAgentSessionAccountHome(home)) {
throw new Error('OpenCode chat requires a pinned data account')
}
return environmentForStructuredOpenCodeAccountHome(home, {
managedAccounts: managedAccounts(),
baseEnvironment: env
})
}
}
}