mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
feat(native-chat): read ZCode session transcripts through the OpenCode SQLite reader
ZCode's CLI persists an OpenCode-v1-shaped history in its own SQLite store (~/.zcode/cli/db/db.sqlite). The Agent Session History scan already lists those sessions, but opening one in the native chat found nothing: the chat transcript path had no ZCode awareness. Resolve a zcode transcript against the ZCode database (native home, then WSL homes) before any OpenCode install, tag every read (signal, page, subscribe) with the agent, and apply the same hidden-transcript filter the scan uses so ZCode's hidden bookkeeping rows stay out of chat renders.
This commit is contained in:
@@ -25,7 +25,7 @@ export async function handleOpenCodeSqliteRequest(
|
||||
if (request.kind === 'native-page' || request.kind === 'native-signal') {
|
||||
const value =
|
||||
request.kind === 'native-signal'
|
||||
? readOpenCodeTranscriptSignal(request.dbPath, request.sessionId)
|
||||
? readOpenCodeTranscriptSignal(request.dbPath, request.sessionId, request.agent)
|
||||
: readOpenCodeTranscriptPage({ ...request, limit: request.limit ?? 50 })
|
||||
return { id: request.id, ok: true, value }
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export type OpenCodeNativeChatReadRequest = {
|
||||
sessionId: string
|
||||
limit?: number
|
||||
beforeMessageRowId?: number
|
||||
/** When 'zcode', reads apply ZCode's hidden-transcript filter. */
|
||||
agent?: 'zcode'
|
||||
}
|
||||
|
||||
export type OpenCodeNativeChatReadValue = OpenCodeTranscriptPage | OpenCodeTranscriptSignal | null
|
||||
|
||||
@@ -265,6 +265,7 @@ export async function readOpenCodeTranscriptPageViaWorker(
|
||||
sessionId: string
|
||||
limit: number
|
||||
beforeMessageRowId?: number
|
||||
agent?: 'zcode'
|
||||
},
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
@@ -289,6 +290,7 @@ export async function readOpenCodeTranscriptSignalViaWorker(
|
||||
args: {
|
||||
dbPath: string
|
||||
sessionId: string
|
||||
agent?: 'zcode'
|
||||
},
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
|
||||
import { getAiVaultWslHomeDirs } from '../ai-vault/cached-session-list'
|
||||
@@ -13,9 +14,17 @@ import {
|
||||
listOpenCodeDatabasesInDirectory
|
||||
} from '../opencode-usage/opencode-database-discovery'
|
||||
|
||||
// ZCode's CLI keeps its OpenCode-v1-shaped history in its own database, so a
|
||||
// zcode transcript must never be claimed by an unrelated OpenCode install (the
|
||||
// same separation the AI Vault scanner draws in session-scanner-zcode-sources).
|
||||
function zcodeTranscriptDatabasePaths(wslHomeDirs: readonly string[]): string[] {
|
||||
return [homedir(), ...wslHomeDirs].map((home) => join(home, '.zcode', 'cli', 'db', 'db.sqlite'))
|
||||
}
|
||||
|
||||
export async function discoverOpenCodeTranscriptDatabase(
|
||||
sessionId?: string,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
agent?: 'zcode'
|
||||
): Promise<string | null> {
|
||||
signal?.throwIfAborted()
|
||||
const deadline = new AbortController()
|
||||
@@ -47,7 +56,10 @@ export async function discoverOpenCodeTranscriptDatabase(
|
||||
try {
|
||||
if (
|
||||
await waitForPromiseWithSignal(
|
||||
readOpenCodeTranscriptSignalViaWorker({ dbPath, sessionId }, boundedSignal),
|
||||
readOpenCodeTranscriptSignalViaWorker(
|
||||
{ dbPath, sessionId, ...(agent ? { agent } : {}) },
|
||||
boundedSignal
|
||||
),
|
||||
boundedSignal
|
||||
)
|
||||
) {
|
||||
@@ -61,6 +73,32 @@ export async function discoverOpenCodeTranscriptDatabase(
|
||||
return null
|
||||
}
|
||||
try {
|
||||
// A zcode transcript only ever lives in a ZCode database: probe those first
|
||||
// and never fall through to OpenCode installs that cannot hold the session.
|
||||
if (agent === 'zcode') {
|
||||
const homes = await waitForPromiseWithSignal(getAiVaultWslHomeDirs(), boundedSignal)
|
||||
const native = await findSession(zcodeTranscriptDatabasePaths([]))
|
||||
if (native) {
|
||||
return native
|
||||
}
|
||||
const readerRoots = homes
|
||||
if (readerRoots.length > 0) {
|
||||
const readers = await waitForPromiseWithSignal(
|
||||
prepareOpenCodeWslReaders(readerRoots),
|
||||
boundedSignal
|
||||
)
|
||||
boundedSignal.throwIfAborted()
|
||||
configureOpenCodeWslReaders(readers)
|
||||
}
|
||||
const wsl = await findSession(zcodeTranscriptDatabasePaths(homes))
|
||||
if (wsl) {
|
||||
return wsl
|
||||
}
|
||||
if (refusals[0]) {
|
||||
throw refusals[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
const primary = await waitForPromiseWithSignal(
|
||||
listOpenCodeDatabases(onRefusal, undefined, boundedSignal),
|
||||
boundedSignal
|
||||
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
} from './transcript-opencode-part-blocks'
|
||||
// Cursors are opaque provider order: SQLite rowid in v1, session sequence in v2.
|
||||
|
||||
// ZCode stores hidden bookkeeping transcripts in the same message table; the
|
||||
// same predicate the AI Vault scan applies keeps them out of chat renders.
|
||||
const ZCODE_HIDDEN_MESSAGE_FILTER =
|
||||
"AND COALESCE(json_extract(data, '$.semantics.transcriptVisibility'), 'visible') != 'hidden'"
|
||||
|
||||
export type OpenCodeTranscriptItem = {
|
||||
rowid: number
|
||||
fingerprint: string
|
||||
@@ -70,7 +75,8 @@ function rowsWithinBudget<T>(
|
||||
|
||||
export function readOpenCodeTranscriptSignal(
|
||||
dbPath: string,
|
||||
sessionId: string
|
||||
sessionId: string,
|
||||
agent?: 'zcode'
|
||||
): OpenCodeTranscriptSignal | null {
|
||||
const db = openOpenCodeDatabaseReadonly(dbPath)
|
||||
try {
|
||||
@@ -84,14 +90,22 @@ export function readOpenCodeTranscriptSignal(
|
||||
// Aggregates without GROUP BY always yield exactly one row, so [0] is it.
|
||||
const [messageRow] = rowsOf<{ message_count: number; max_message_rowid: number }>(
|
||||
db.prepare(
|
||||
'SELECT COUNT(*) AS message_count, COALESCE(MAX(rowid), 0) AS max_message_rowid FROM message WHERE session_id = ?'
|
||||
`SELECT COUNT(*) AS message_count, COALESCE(MAX(rowid), 0) AS max_message_rowid FROM message WHERE session_id = ? ${
|
||||
agent === 'zcode' ? ZCODE_HIDDEN_MESSAGE_FILTER : ''
|
||||
}`
|
||||
),
|
||||
sessionId
|
||||
)
|
||||
const [partRow] = rowsOf<{ part_count: number; max_part_time_updated: number }>(
|
||||
db.prepare(
|
||||
'SELECT COUNT(*) AS part_count, COALESCE(MAX(time_updated), 0) AS max_part_time_updated FROM part WHERE session_id = ?'
|
||||
`SELECT COUNT(*) AS part_count, COALESCE(MAX(time_updated), 0) AS max_part_time_updated
|
||||
FROM part WHERE session_id = ? AND message_id IN (
|
||||
SELECT id FROM message WHERE session_id = ? ${
|
||||
agent === 'zcode' ? ZCODE_HIDDEN_MESSAGE_FILTER : ''
|
||||
}
|
||||
)`
|
||||
),
|
||||
sessionId,
|
||||
sessionId
|
||||
)
|
||||
return {
|
||||
@@ -110,6 +124,7 @@ export function readOpenCodeTranscriptPage(args: {
|
||||
sessionId: string
|
||||
limit: number
|
||||
beforeMessageRowId?: number
|
||||
agent?: 'zcode'
|
||||
}): OpenCodeTranscriptPage | null {
|
||||
const db = openOpenCodeDatabaseReadonly(args.dbPath)
|
||||
try {
|
||||
@@ -127,7 +142,9 @@ export function readOpenCodeTranscriptPage(args: {
|
||||
const select = db.prepare(
|
||||
`SELECT rowid AS message_rowid, id, time_created, time_updated, CASE WHEN length(CAST(data AS BLOB)) <= ${OPENCODE_TRANSCRIPT_MAX_ROW_BYTES} THEN data ELSE NULL END AS data
|
||||
FROM message
|
||||
WHERE session_id = ? AND rowid < ?
|
||||
WHERE session_id = ? AND rowid < ? ${
|
||||
args.agent === 'zcode' ? ZCODE_HIDDEN_MESSAGE_FILTER : ''
|
||||
}
|
||||
ORDER BY rowid DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type SubscribeNativeChatTranscriptArgs
|
||||
} from './transcript-watch-contract'
|
||||
import type { OpenCodeTranscriptItem } from './transcript-opencode-sqlite-query'
|
||||
import { openCodeTranscriptDefaultDeps, type OpenCodeTranscriptDeps } from './transcript-opencode'
|
||||
import { bindOpenCodeTranscriptDeps, type OpenCodeTranscriptDeps } from './transcript-opencode'
|
||||
import {
|
||||
openCodeTranscriptPageLimit,
|
||||
OPENCODE_TRANSCRIPT_MAX_WINDOW
|
||||
@@ -31,23 +31,13 @@ export const pruneOpenCodeFingerprintCacheForTest = pruneOpenCodeFingerprintCach
|
||||
export function subscribeOpenCodeNativeChatTranscript(
|
||||
args: SubscribeNativeChatTranscriptArgs,
|
||||
setupSignal?: AbortSignal,
|
||||
deps: OpenCodeTranscriptDeps = {}
|
||||
deps: OpenCodeTranscriptDeps = {},
|
||||
// When 'zcode': discovery probes ZCode's database, reads drop hidden rows.
|
||||
agent?: 'zcode'
|
||||
): NativeChatTranscriptSubscription {
|
||||
setupSignal?.throwIfAborted()
|
||||
const controller = new AbortController()
|
||||
const resolveDbPath = () =>
|
||||
(deps.resolveDbPath ?? openCodeTranscriptDefaultDeps.resolveDbPath)(
|
||||
args.sessionId,
|
||||
controller.signal
|
||||
)
|
||||
const readSignal = (dbPath: string, sessionId: string) =>
|
||||
(deps.readSignal ?? openCodeTranscriptDefaultDeps.readSignal)(
|
||||
dbPath,
|
||||
sessionId,
|
||||
controller.signal
|
||||
)
|
||||
const readPage = (page: Parameters<NonNullable<OpenCodeTranscriptDeps['readPage']>>[0]) =>
|
||||
(deps.readPage ?? openCodeTranscriptDefaultDeps.readPage)(page, controller.signal)
|
||||
const bound = bindOpenCodeTranscriptDeps(deps, controller.signal, agent)
|
||||
const pollMs = args.resolvePollIntervalMs ?? OPENCODE_POLL_MS
|
||||
const initialLimit = openCodeTranscriptPageLimit(
|
||||
args.initialLimit && args.initialLimit > 0 ? args.initialLimit : DESKTOP_READ_WINDOW
|
||||
@@ -117,7 +107,7 @@ export function subscribeOpenCodeNativeChatTranscript(
|
||||
return
|
||||
}
|
||||
try {
|
||||
dbPath ??= await resolveDbPath()
|
||||
dbPath ??= await bound.resolveDbPath()
|
||||
if (closed) {
|
||||
return
|
||||
}
|
||||
@@ -126,7 +116,7 @@ export function subscribeOpenCodeNativeChatTranscript(
|
||||
scheduleTick()
|
||||
return
|
||||
}
|
||||
const signal = await readSignal(dbPath, args.sessionId)
|
||||
const signal = await bound.readSignal(dbPath, args.sessionId)
|
||||
if (closed) {
|
||||
return
|
||||
}
|
||||
@@ -141,7 +131,7 @@ export function subscribeOpenCodeNativeChatTranscript(
|
||||
scheduleTick()
|
||||
return
|
||||
}
|
||||
const page = await readPage({
|
||||
const page = await bound.readPage({
|
||||
dbPath,
|
||||
sessionId: args.sessionId,
|
||||
limit: firstSnapshot
|
||||
@@ -193,7 +183,7 @@ export function subscribeOpenCodeNativeChatTranscript(
|
||||
const displaced = [...fingerprints.keys()].filter((rowid) => rowid < oldest)
|
||||
if (displaced.length > 0) {
|
||||
// Verify the displaced fringe: cap eviction and a real deletion look identical in the tail.
|
||||
const older = await readPage({
|
||||
const older = await bound.readPage({
|
||||
dbPath,
|
||||
sessionId: args.sessionId,
|
||||
limit: displaced.length,
|
||||
@@ -267,14 +257,14 @@ export function subscribeOpenCodeNativeChatTranscript(
|
||||
// overlap, so the gap retries instead of dropping rows.
|
||||
async function replaceWithBridgedWindow(
|
||||
db: string,
|
||||
page: Awaited<ReturnType<typeof readPage>>,
|
||||
page: Awaited<ReturnType<typeof bound.readPage>>,
|
||||
shrinking = false
|
||||
): Promise<boolean> {
|
||||
let limit = Math.max(windowLimit, page?.items.length ?? 0)
|
||||
for (;;) {
|
||||
const replacement =
|
||||
page ??
|
||||
(await readPage({
|
||||
(await bound.readPage({
|
||||
dbPath: db,
|
||||
sessionId: args.sessionId,
|
||||
limit
|
||||
|
||||
@@ -20,11 +20,16 @@ import { openCodeTranscriptPageLimit } from '../../shared/opencode-transcript-pa
|
||||
// OpenCode SQLite reads use the same bounded worker as AI Vault.
|
||||
|
||||
export type OpenCodeTranscriptDeps = {
|
||||
resolveDbPath?: (sessionId?: string, signal?: AbortSignal) => Promise<string | null>
|
||||
resolveDbPath?: (
|
||||
sessionId?: string,
|
||||
signal?: AbortSignal,
|
||||
agent?: 'zcode'
|
||||
) => Promise<string | null>
|
||||
readSignal?: (
|
||||
dbPath: string,
|
||||
sessionId: string,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
agent?: 'zcode'
|
||||
) => Promise<OpenCodeTranscriptSignal | null>
|
||||
readPage?: (
|
||||
args: {
|
||||
@@ -32,6 +37,7 @@ export type OpenCodeTranscriptDeps = {
|
||||
sessionId: string
|
||||
limit: number
|
||||
beforeMessageRowId?: number
|
||||
agent?: 'zcode'
|
||||
},
|
||||
signal?: AbortSignal
|
||||
) => Promise<OpenCodeTranscriptPage | null>
|
||||
@@ -43,17 +49,51 @@ export function resolveOpenCodeTranscriptDbPath(sessionId?: string): Promise<str
|
||||
|
||||
export const openCodeTranscriptDefaultDeps: Required<OpenCodeTranscriptDeps> = {
|
||||
resolveDbPath: discoverOpenCodeTranscriptDatabase,
|
||||
readSignal: (dbPath, sessionId, signal) =>
|
||||
readOpenCodeTranscriptSignalViaWorker({ dbPath, sessionId }, signal),
|
||||
readSignal: (dbPath, sessionId, signal, agent) =>
|
||||
readOpenCodeTranscriptSignalViaWorker({ dbPath, sessionId, agent }, signal),
|
||||
readPage: (args, signal) => readOpenCodeTranscriptPageViaWorker(args, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a subscribe's read legs against its own abort signal, with the agent
|
||||
* tag (when any) already applied to every leg. Kept here so the poll loop stays
|
||||
* free of default-vs-override plumbing.
|
||||
*/
|
||||
export function bindOpenCodeTranscriptDeps(
|
||||
deps: OpenCodeTranscriptDeps,
|
||||
signal: AbortSignal,
|
||||
agent?: 'zcode'
|
||||
): {
|
||||
resolveDbPath: () => Promise<string | null>
|
||||
readSignal: (dbPath: string, sessionId: string) => Promise<OpenCodeTranscriptSignal | null>
|
||||
readPage: (
|
||||
page: Parameters<NonNullable<OpenCodeTranscriptDeps['readPage']>>[0]
|
||||
) => Promise<OpenCodeTranscriptPage | null>
|
||||
} {
|
||||
return {
|
||||
resolveDbPath: () =>
|
||||
(deps.resolveDbPath ?? openCodeTranscriptDefaultDeps.resolveDbPath)(undefined, signal, agent),
|
||||
readSignal: (dbPath, sessionId) =>
|
||||
(deps.readSignal ?? openCodeTranscriptDefaultDeps.readSignal)(
|
||||
dbPath,
|
||||
sessionId,
|
||||
signal,
|
||||
agent
|
||||
),
|
||||
readPage: (page) =>
|
||||
(deps.readPage ?? openCodeTranscriptDefaultDeps.readPage)(
|
||||
{ ...page, ...(agent ? { agent } : {}) },
|
||||
signal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenCodeTailResult =
|
||||
| { messages: NativeChatMessage[]; hasMore: boolean; beforeOffset: number }
|
||||
| { error: string; notFound?: true }
|
||||
|
||||
export async function readOpenCodeNativeChatTranscriptTail(
|
||||
args: { sessionId: string; limit: number; beforeOffset?: number },
|
||||
args: { sessionId: string; limit: number; beforeOffset?: number; agent?: 'zcode' },
|
||||
deps: OpenCodeTranscriptDeps = {},
|
||||
signal?: AbortSignal
|
||||
): Promise<OpenCodeTailResult> {
|
||||
@@ -61,7 +101,8 @@ export async function readOpenCodeNativeChatTranscriptTail(
|
||||
try {
|
||||
const dbPath = await (deps.resolveDbPath ?? openCodeTranscriptDefaultDeps.resolveDbPath)(
|
||||
args.sessionId,
|
||||
signal
|
||||
signal,
|
||||
args.agent
|
||||
)
|
||||
if (!dbPath) {
|
||||
return { error: 'Transcript unavailable', notFound: true }
|
||||
@@ -71,7 +112,8 @@ export async function readOpenCodeNativeChatTranscriptTail(
|
||||
dbPath,
|
||||
sessionId: args.sessionId,
|
||||
limit,
|
||||
...(args.beforeOffset !== undefined ? { beforeMessageRowId: args.beforeOffset } : {})
|
||||
...(args.beforeOffset !== undefined ? { beforeMessageRowId: args.beforeOffset } : {}),
|
||||
...(args.agent ? { agent: args.agent } : {})
|
||||
},
|
||||
signal
|
||||
)
|
||||
@@ -91,7 +133,8 @@ export async function readOpenCodeNativeChatTranscriptTail(
|
||||
export async function readOpenCodeNativeChatTranscriptFull(
|
||||
sessionId: string,
|
||||
deps: OpenCodeTranscriptDeps = {},
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
agent?: 'zcode'
|
||||
): Promise<ReadTranscriptResult> {
|
||||
// Pages arrive newest-window first; reverse windows, preserving message order.
|
||||
const pages: NativeChatMessage[][] = []
|
||||
@@ -113,7 +156,8 @@ export async function readOpenCodeNativeChatTranscriptFull(
|
||||
dbPath,
|
||||
sessionId,
|
||||
limit: 500,
|
||||
...(cursor !== undefined ? { beforeMessageRowId: cursor } : {})
|
||||
...(cursor !== undefined ? { beforeMessageRowId: cursor } : {}),
|
||||
...(agent ? { agent } : {})
|
||||
},
|
||||
signal
|
||||
)
|
||||
|
||||
@@ -107,7 +107,12 @@ export async function readNativeChatTranscriptCached(
|
||||
transcriptPath?: string
|
||||
): Promise<ReadTranscriptResult> {
|
||||
if (resolveNativeChatTranscriptAgent(agent) === 'opencode') {
|
||||
return readOpenCodeNativeChatTranscriptFull(sessionId)
|
||||
return readOpenCodeNativeChatTranscriptFull(
|
||||
sessionId,
|
||||
{},
|
||||
undefined,
|
||||
agent === 'zcode' ? 'zcode' : undefined
|
||||
)
|
||||
}
|
||||
const epoch = cacheEpoch
|
||||
let filePath: string | null
|
||||
|
||||
@@ -45,7 +45,12 @@ export async function readNativeChatTranscript(
|
||||
options: ReadTranscriptOptions = {}
|
||||
): Promise<ReadTranscriptResult> {
|
||||
if (resolveNativeChatTranscriptAgent(agent) === 'opencode') {
|
||||
return readOpenCodeNativeChatTranscriptFull(sessionId)
|
||||
return readOpenCodeNativeChatTranscriptFull(
|
||||
sessionId,
|
||||
{},
|
||||
undefined,
|
||||
agent === 'zcode' ? 'zcode' : undefined
|
||||
)
|
||||
}
|
||||
let filePath: string | null
|
||||
try {
|
||||
|
||||
@@ -246,7 +246,11 @@ export async function readNativeChatTranscriptTail(
|
||||
| { error: string; notFound?: true }
|
||||
> {
|
||||
if (resolveNativeChatTranscriptAgent(args.agent) === 'opencode') {
|
||||
return readOpenCodeNativeChatTranscriptTail(args, {}, signal)
|
||||
return readOpenCodeNativeChatTranscriptTail(
|
||||
{ ...args, agent: args.agent === 'zcode' ? 'zcode' : undefined },
|
||||
{},
|
||||
signal
|
||||
)
|
||||
}
|
||||
const decode = nativeChatLineDecoderForAgent(args.agent)
|
||||
const decodeLifecycle = nativeChatTurnLifecycleDecoderForAgent(args.agent)
|
||||
|
||||
@@ -267,7 +267,12 @@ export async function subscribeNativeChatTranscript(
|
||||
): Promise<NativeChatTranscriptSubscription> {
|
||||
setupSignal?.throwIfAborted()
|
||||
if (resolveNativeChatTranscriptAgent(args.agent) === 'opencode') {
|
||||
return subscribeOpenCodeNativeChatTranscript(args, setupSignal)
|
||||
return subscribeOpenCodeNativeChatTranscript(
|
||||
args,
|
||||
setupSignal,
|
||||
{},
|
||||
args.agent === 'zcode' ? 'zcode' : undefined
|
||||
)
|
||||
}
|
||||
const decode = nativeChatLineDecoderForAgent(args.agent)
|
||||
if (!decode) {
|
||||
|
||||
@@ -15,6 +15,9 @@ describe('resolveNativeChatTranscriptAgent', () => {
|
||||
it('passes codex, grok and omp through and rejects everything else', () => {
|
||||
expect(resolveNativeChatTranscriptAgent('opencode')).toBe('opencode')
|
||||
expect(resolveNativeChatTranscriptAgent('opencode2')).toBe('opencode')
|
||||
// ZCode keeps an OpenCode-v1-shaped history in its own SQLite DB, so its
|
||||
// transcripts read through the OpenCode SQLite reader.
|
||||
expect(resolveNativeChatTranscriptAgent('zcode')).toBe('opencode')
|
||||
expect(resolveNativeChatTranscriptAgent('codex')).toBe('codex')
|
||||
expect(resolveNativeChatTranscriptAgent('grok')).toBe('grok')
|
||||
expect(resolveNativeChatTranscriptAgent('omp')).toBe('omp')
|
||||
@@ -28,6 +31,7 @@ describe('isNativeChatSupportedAgent', () => {
|
||||
it('recognizes the parseable agents and rejects unknown / nullish input', () => {
|
||||
expect(isNativeChatSupportedAgent('opencode')).toBe(true)
|
||||
expect(isNativeChatSupportedAgent('opencode2')).toBe(true)
|
||||
expect(isNativeChatSupportedAgent('zcode')).toBe(true)
|
||||
expect(isNativeChatSupportedAgent('claude')).toBe(true)
|
||||
expect(isNativeChatSupportedAgent('openclaude')).toBe(true)
|
||||
expect(isNativeChatSupportedAgent('omp')).toBe(true)
|
||||
@@ -43,6 +47,7 @@ describe('nativeChatRequiresLocalTranscript', () => {
|
||||
// native chat has to find their file on a disk this process can read.
|
||||
expect(nativeChatRequiresLocalTranscript('opencode')).toBe(true)
|
||||
expect(nativeChatRequiresLocalTranscript('opencode2')).toBe(true)
|
||||
expect(nativeChatRequiresLocalTranscript('zcode')).toBe(true)
|
||||
expect(nativeChatRequiresLocalTranscript('grok')).toBe(true)
|
||||
expect(nativeChatRequiresLocalTranscript('omp')).toBe(true)
|
||||
expect(nativeChatRequiresLocalTranscript('claude')).toBe(false)
|
||||
|
||||
@@ -11,7 +11,8 @@ export const NATIVE_CHAT_SUPPORTED_AGENT_LIST: readonly TuiAgent[] = [
|
||||
'grok',
|
||||
'omp',
|
||||
'opencode',
|
||||
'opencode2'
|
||||
'opencode2',
|
||||
'zcode'
|
||||
]
|
||||
|
||||
export const NATIVE_CHAT_SUPPORTED_AGENTS: ReadonlySet<string> = new Set(
|
||||
@@ -48,6 +49,12 @@ export function resolveNativeChatTranscriptAgent(
|
||||
if (agent === 'opencode' || agent === 'opencode2') {
|
||||
return 'opencode'
|
||||
}
|
||||
// Why: ZCode's CLI stores an OpenCode-v1-shaped history in its own SQLite DB
|
||||
// (~/.zcode/cli/db/db.sqlite), so its transcripts read through the same
|
||||
// OpenCode SQLite reader as OpenCode 1.x — the agent identity stays zcode.
|
||||
if (agent === 'zcode') {
|
||||
return 'opencode'
|
||||
}
|
||||
if (agent === 'codex' || agent === 'grok' || agent === 'omp') {
|
||||
return agent
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user