Files
orca/src/shared/native-chat-agent-support.test.ts
T
9e4e6ddae5 feat(native-chat): render omp transcripts (#11523)
* feat(native-chat): render omp transcripts

omp already ships as a first-class launchable agent with session_id resume, but
its transcripts had no decoder, so native chat could not render it — the agent
runs and the conversation stays a raw terminal. This adds the decoder and wires
it through the same path Claude, Codex and Grok use.

omp writes one envelope per line, `{ type, id, parentId, timestamp, … }`, where
conversation turns are `type: 'message'` and the rest is session bookkeeping.
Reasoning arrives as a `thinking` content block inside the assistant turn, so
the mapping follows Claude rather than Codex: thinking becomes a text block on
an assistant message, where Codex and Grok emit a separate reasoning role only
because their transcripts carry dedicated reasoning records.

  - toolCall -> tool-call, arguments passed through as the object omp writes
  - toolResult -> tool role, isError preserved
  - developer -> system, matching the Codex non-user/non-assistant fallback
  - blob-handle images drop, as the Claude mapper drops an image record with
    neither path nor url
  - bookkeeping and unrecognized types skip rather than throw

Session files are `<ISO timestamp>_<session id>.jsonl` under a per-cwd directory,
so the resolver matches the id as a base-name suffix the way Codex rollout files
are matched, and honors OMP_CODING_AGENT_DIR through normalizeAgentSessionsDir
so it stays consistent with the AI Vault scanner.

omp records no interruption or abort event, so unlike Claude and Codex there is
no NATIVE_CHAT_INTERRUPTED_STATUS_TEXT path.

Verified against 94,603 lines of real omp transcripts across four sessions:
50,546 records decoded, zero malformed, zero thrown.

* fix(native-chat): complete omp record coverage and gate remote transcripts

Review fixes on the omp transcript decoder.

omp writes several record types with no `content` field, so they decoded
to zero blocks and disappeared from the chat view entirely:

- `bashExecution` / `pythonExecution`: TUI `!command` runs, now a tool turn
- `fileMention`: `@path` attachments, listed by path (never `files[].content`,
  which is an auto-read dump)
- `custom_message` and legacy `custom` / `hookMessage` rows, gated on
  `display` the way omp's own renderer gates them

Also:

- `stopReason: 'aborted'` turns now surface as the interrupted row, matching
  the Claude and Codex decoders. An abort carrying partial content keeps it.
- A cancelled command cell now reads as errored. Every omp cancel path emits
  `exitCode: undefined`, which JSON drops, so an `exitCode !== 0` check read a
  cancelled run as a clean success.
- omp joins Grok in requiring a locally readable transcript. Its hook reports
  no transcript path, so under Model-A SSH the chat view opened against a disk
  this process cannot read and never loaded. Applies on mobile too, which
  shares the same allowlist.
- The session-file walk prunes omp's per-session subagent artifact
  directories, matching the AI Vault scanner. It was returning a subagent
  transcript instead of the parent session, and cost a full recursive readdir
  on every resolve.

* style(native-chat): apply oxfmt to the omp review fixes

Mobile CI gates `oxfmt --check`; the two root files were unformatted too,
just ungated there. Line wrapping only, no behavior change.

---------

Co-authored-by: plotarmordev <299844489+plotarmordev@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-07 00:58:29 -07:00

68 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isNativeChatSupportedAgent,
nativeChatRequiresLocalTranscript,
resolveNativeChatTranscriptAgent,
shouldStepNativeChatAskAnswer
} from './native-chat-agent-support'
describe('resolveNativeChatTranscriptAgent', () => {
it('maps OpenClaude onto the Claude transcript format', () => {
expect(resolveNativeChatTranscriptAgent('openclaude')).toBe('claude')
expect(resolveNativeChatTranscriptAgent('claude')).toBe('claude')
})
it('passes codex, grok and omp through and rejects everything else', () => {
expect(resolveNativeChatTranscriptAgent('codex')).toBe('codex')
expect(resolveNativeChatTranscriptAgent('grok')).toBe('grok')
expect(resolveNativeChatTranscriptAgent('omp')).toBe('omp')
expect(resolveNativeChatTranscriptAgent('cursor')).toBeNull()
expect(resolveNativeChatTranscriptAgent(null)).toBeNull()
expect(resolveNativeChatTranscriptAgent(undefined)).toBeNull()
})
})
describe('isNativeChatSupportedAgent', () => {
it('recognizes the parseable agents and rejects unknown / nullish input', () => {
expect(isNativeChatSupportedAgent('claude')).toBe(true)
expect(isNativeChatSupportedAgent('openclaude')).toBe(true)
expect(isNativeChatSupportedAgent('omp')).toBe(true)
expect(isNativeChatSupportedAgent('cursor')).toBe(false)
expect(isNativeChatSupportedAgent(null)).toBe(false)
expect(isNativeChatSupportedAgent(undefined)).toBe(false)
})
})
describe('nativeChatRequiresLocalTranscript', () => {
it('covers the agents whose hook discloses no transcript path', () => {
// Claude/Codex report `transcript_path`; Grok and omp report only an id, so
// native chat has to find their file on a disk this process can read.
expect(nativeChatRequiresLocalTranscript('grok')).toBe(true)
expect(nativeChatRequiresLocalTranscript('omp')).toBe(true)
expect(nativeChatRequiresLocalTranscript('claude')).toBe(false)
expect(nativeChatRequiresLocalTranscript('openclaude')).toBe(false)
expect(nativeChatRequiresLocalTranscript('codex')).toBe(false)
expect(nativeChatRequiresLocalTranscript('cursor')).toBe(false)
expect(nativeChatRequiresLocalTranscript(null)).toBe(false)
expect(nativeChatRequiresLocalTranscript(undefined)).toBe(false)
})
})
describe('shouldStepNativeChatAskAnswer', () => {
it('steps the digit-commit selector agents (Claude, OpenClaude, Codex)', () => {
expect(shouldStepNativeChatAskAnswer('claude')).toBe(true)
expect(shouldStepNativeChatAskAnswer('openclaude')).toBe(true)
// Codex 0.145's request_user_input card ignores typed labels and commits on
// the highlighted row, so pasted answers misdeliver like STA-1860.
expect(shouldStepNativeChatAskAnswer('codex')).toBe(true)
})
it('does not step other or unknown agents', () => {
expect(shouldStepNativeChatAskAnswer('grok')).toBe(false)
expect(shouldStepNativeChatAskAnswer('omp')).toBe(false)
expect(shouldStepNativeChatAskAnswer('cursor')).toBe(false)
expect(shouldStepNativeChatAskAnswer(null)).toBe(false)
expect(shouldStepNativeChatAskAnswer(undefined)).toBe(false)
})
})