mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
Render Codex review messages
This commit is contained in:
@@ -403,6 +403,102 @@ describe('normalizeDecryptedMessage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes Codex review JSON messages as structured review content', () => {
|
||||
const message = makeMessage({
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'message',
|
||||
message: JSON.stringify({
|
||||
findings: [{
|
||||
title: '[P2] Remove retained sessions when sockets disconnect',
|
||||
body: 'Retained sockets survive disconnects.',
|
||||
confidence_score: 0.82,
|
||||
priority: 2,
|
||||
code_location: {
|
||||
absolute_file_path: '/data/dz/wapair-ts/src/pairing/manager.ts',
|
||||
line_range: { start: 1614, end: 1619 }
|
||||
}
|
||||
}],
|
||||
overall_correctness: 'patch is incorrect',
|
||||
overall_explanation: 'The message-sending feature retains long-lived sockets but does not fully manage their lifecycle.',
|
||||
overall_confidence_score: 0.8
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const normalized = normalizeDecryptedMessage(message)
|
||||
|
||||
expect(normalized).toMatchObject({
|
||||
role: 'agent',
|
||||
content: [{
|
||||
type: 'codex-review',
|
||||
review: {
|
||||
overallCorrectness: 'patch is incorrect',
|
||||
overallExplanation: 'The message-sending feature retains long-lived sockets but does not fully manage their lifecycle.',
|
||||
overallConfidenceScore: 0.8,
|
||||
findings: [{
|
||||
title: '[P2] Remove retained sessions when sockets disconnect',
|
||||
body: 'Retained sockets survive disconnects.',
|
||||
priority: 2,
|
||||
confidenceScore: 0.82,
|
||||
filePath: '/data/dz/wapair-ts/src/pairing/manager.ts',
|
||||
lineStart: 1614,
|
||||
lineEnd: 1619
|
||||
}]
|
||||
}
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps non-review Codex JSON messages as text', () => {
|
||||
const message = makeMessage({
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'message',
|
||||
message: JSON.stringify({ status: 'ok', message: 'plain JSON' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const normalized = normalizeDecryptedMessage(message)
|
||||
|
||||
expect(normalized).toMatchObject({
|
||||
role: 'agent',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: '{"status":"ok","message":"plain JSON"}'
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps malformed Codex review-looking messages as text', () => {
|
||||
const message = makeMessage({
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'message',
|
||||
message: '{"findings": ['
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const normalized = normalizeDecryptedMessage(message)
|
||||
|
||||
expect(normalized).toMatchObject({
|
||||
role: 'agent',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: '{"findings": ['
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes Codex plan updates as completed update_plan snapshots', () => {
|
||||
const message = makeMessage({
|
||||
role: 'agent',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentEvent, NormalizedAgentContent, NormalizedMessage, ToolResultPermission } from '@/chat/types'
|
||||
import type { AgentEvent, CodexReview, CodexReviewFinding, NormalizedAgentContent, NormalizedMessage, ToolResultPermission } from '@/chat/types'
|
||||
import { AGENT_MESSAGE_PAYLOAD_TYPE, asNumber, asString, isObject } from '@hapi/protocol'
|
||||
import { isClaudeChatVisibleMessage } from '@hapi/protocol/messages'
|
||||
|
||||
@@ -142,6 +142,75 @@ function normalizePlanEntries(value: unknown): Array<{ step: string; status: 'pe
|
||||
return plan
|
||||
}
|
||||
|
||||
function normalizeCodexReviewFinding(value: unknown): CodexReviewFinding | null {
|
||||
if (!isObject(value)) return null
|
||||
const title = asString(value.title)
|
||||
const body = asString(value.body)
|
||||
if (!title || !body) return null
|
||||
|
||||
const codeLocation = isObject(value.code_location)
|
||||
? value.code_location
|
||||
: isObject(value.codeLocation)
|
||||
? value.codeLocation
|
||||
: null
|
||||
const lineRange = codeLocation && isObject(codeLocation.line_range)
|
||||
? codeLocation.line_range
|
||||
: codeLocation && isObject(codeLocation.lineRange)
|
||||
? codeLocation.lineRange
|
||||
: null
|
||||
|
||||
return {
|
||||
title,
|
||||
body,
|
||||
priority: asNumber(value.priority),
|
||||
confidenceScore: asNumber(value.confidence_score ?? value.confidenceScore),
|
||||
filePath: codeLocation ? asString(codeLocation.absolute_file_path ?? codeLocation.absoluteFilePath ?? codeLocation.path) : null,
|
||||
lineStart: lineRange ? asNumber(lineRange.start) : null,
|
||||
lineEnd: lineRange ? asNumber(lineRange.end) : null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCodexReviewJson(value: unknown): CodexReview | null {
|
||||
if (!isObject(value)) return null
|
||||
const hasReviewMarker = Array.isArray(value.findings)
|
||||
|| 'overall_correctness' in value
|
||||
|| 'overallCorrectness' in value
|
||||
|| 'overall_explanation' in value
|
||||
|| 'overallExplanation' in value
|
||||
if (!hasReviewMarker) return null
|
||||
|
||||
const findings = Array.isArray(value.findings)
|
||||
? value.findings
|
||||
.map(normalizeCodexReviewFinding)
|
||||
.filter((finding): finding is CodexReviewFinding => finding !== null)
|
||||
: []
|
||||
|
||||
const overallCorrectness = asString(value.overall_correctness ?? value.overallCorrectness)
|
||||
const overallExplanation = asString(value.overall_explanation ?? value.overallExplanation)
|
||||
const overallConfidenceScore = asNumber(value.overall_confidence_score ?? value.overallConfidenceScore)
|
||||
|
||||
if (findings.length === 0 && !overallCorrectness && !overallExplanation && overallConfidenceScore === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
findings,
|
||||
overallCorrectness,
|
||||
overallExplanation,
|
||||
overallConfidenceScore
|
||||
}
|
||||
}
|
||||
|
||||
function parseCodexReviewMessage(message: string): CodexReview | null {
|
||||
const trimmed = message.trim()
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
|
||||
try {
|
||||
return normalizeCodexReviewJson(JSON.parse(trimmed) as unknown)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAssistantOutput(
|
||||
messageId: string,
|
||||
localId: string | null,
|
||||
@@ -503,6 +572,18 @@ export function normalizeAgentRecord(
|
||||
}
|
||||
|
||||
if (data.type === 'message' && typeof data.message === 'string') {
|
||||
const review = parseCodexReviewMessage(data.message)
|
||||
if (review) {
|
||||
return {
|
||||
id: messageId,
|
||||
localId,
|
||||
createdAt,
|
||||
role: 'agent',
|
||||
isSidechain: false,
|
||||
content: [{ type: 'codex-review', review, uuid: messageId, parentUUID: null }],
|
||||
meta
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: messageId,
|
||||
localId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ChatBlock,
|
||||
GeneratedImageBlock,
|
||||
CliOutputBlock,
|
||||
CodexReviewBlock,
|
||||
ToolCallBlock,
|
||||
ToolPermission,
|
||||
UserTextBlock,
|
||||
@@ -145,6 +146,13 @@ function areGeneratedImageBlocksEqual(left: GeneratedImageBlock, right: Generate
|
||||
&& left.meta === right.meta
|
||||
}
|
||||
|
||||
function areCodexReviewBlocksEqual(left: CodexReviewBlock, right: CodexReviewBlock): boolean {
|
||||
return left.review === right.review
|
||||
&& left.localId === right.localId
|
||||
&& left.createdAt === right.createdAt
|
||||
&& left.meta === right.meta
|
||||
}
|
||||
|
||||
function areAgentEventBlocksEqual(left: AgentEventBlock, right: AgentEventBlock): boolean {
|
||||
return left.createdAt === right.createdAt
|
||||
&& left.meta === right.meta
|
||||
@@ -228,6 +236,11 @@ function reconcileBlock(block: ChatBlock, prevById: ChatBlocksById): ChatBlock {
|
||||
return areGeneratedImageBlocksEqual(prevBlock, block) ? prevBlock : block
|
||||
}
|
||||
|
||||
if (block.kind === 'codex-review') {
|
||||
const prevBlock = prev as CodexReviewBlock
|
||||
return areCodexReviewBlocksEqual(prevBlock, block) ? prevBlock : block
|
||||
}
|
||||
|
||||
const prevBlock = prev as AgentEventBlock
|
||||
return areAgentEventBlocksEqual(prevBlock, block) ? prevBlock : block
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentReasoningBlock, AgentTextBlock, ChatBlock, CliOutputBlock, ToolCallBlock, ToolPermission } from '@/chat/types'
|
||||
import type { AgentReasoningBlock, AgentTextBlock, ChatBlock, CliOutputBlock, CodexReviewBlock, ToolCallBlock, ToolPermission } from '@/chat/types'
|
||||
import type { TracedMessage } from '@/chat/tracer'
|
||||
import { createCliOutputBlock, isCliOutputText, mergeCliOutputBlocks } from '@/chat/reducerCliOutput'
|
||||
import { parseMessageAsEvent } from '@/chat/reducerEvents'
|
||||
@@ -437,8 +437,8 @@ export function reduceTimeline(
|
||||
const targetId = msg.content.targetMessageId
|
||||
const durationMs = msg.content.durationMs as number
|
||||
type DurationBearingBlock = AgentTextBlock | AgentReasoningBlock | CliOutputBlock | ToolCallBlock
|
||||
const isDurationTarget = (b: ChatBlock): b is DurationBearingBlock =>
|
||||
b.kind === 'agent-text' || b.kind === 'agent-reasoning' || b.kind === 'cli-output' || b.kind === 'tool-call'
|
||||
const isDurationTarget = (b: ChatBlock): b is DurationBearingBlock | CodexReviewBlock =>
|
||||
b.kind === 'agent-text' || b.kind === 'agent-reasoning' || b.kind === 'codex-review' || b.kind === 'cli-output' || b.kind === 'tool-call'
|
||||
let foundIndex = -1
|
||||
|
||||
if (targetId) {
|
||||
@@ -760,6 +760,21 @@ export function reduceTimeline(
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.type === 'codex-review') {
|
||||
blocks.push({
|
||||
kind: 'codex-review',
|
||||
id: `${msg.id}:${idx}`,
|
||||
localId: msg.localId,
|
||||
createdAt: msg.createdAt,
|
||||
invokedAt: msg.invokedAt,
|
||||
usage: msg.usage,
|
||||
model: msg.model,
|
||||
review: c.review,
|
||||
meta: msg.meta
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (c.type === 'summary') {
|
||||
blocks.push({
|
||||
kind: 'agent-event',
|
||||
|
||||
+37
-1
@@ -65,6 +65,23 @@ export type GeneratedImageContent = {
|
||||
parentUUID: string | null
|
||||
}
|
||||
|
||||
export type CodexReviewFinding = {
|
||||
title: string
|
||||
body: string
|
||||
priority: number | null
|
||||
confidenceScore: number | null
|
||||
filePath: string | null
|
||||
lineStart: number | null
|
||||
lineEnd: number | null
|
||||
}
|
||||
|
||||
export type CodexReview = {
|
||||
findings: CodexReviewFinding[]
|
||||
overallCorrectness: string | null
|
||||
overallExplanation: string | null
|
||||
overallConfidenceScore: number | null
|
||||
}
|
||||
|
||||
export type NormalizedAgentContent =
|
||||
| {
|
||||
type: 'text'
|
||||
@@ -81,6 +98,12 @@ export type NormalizedAgentContent =
|
||||
| ToolUse
|
||||
| ToolResult
|
||||
| GeneratedImageContent
|
||||
| {
|
||||
type: 'codex-review'
|
||||
review: CodexReview
|
||||
uuid: string
|
||||
parentUUID: string | null
|
||||
}
|
||||
| { type: 'summary'; summary: string }
|
||||
| { type: 'sidechain'; uuid: string; parentUUID: string | null; prompt: string }
|
||||
|
||||
@@ -171,6 +194,19 @@ export type AgentReasoningBlock = {
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
export type CodexReviewBlock = {
|
||||
kind: 'codex-review'
|
||||
id: string
|
||||
localId: string | null
|
||||
createdAt: number
|
||||
invokedAt?: number | null
|
||||
durationMs?: number
|
||||
usage?: UsageData
|
||||
model?: string | null
|
||||
review: CodexReview
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
export type CliOutputBlock = {
|
||||
kind: 'cli-output'
|
||||
id: string
|
||||
@@ -221,4 +257,4 @@ export type ToolCallBlock = {
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
export type ChatBlock = UserTextBlock | AgentTextBlock | AgentReasoningBlock | CliOutputBlock | ToolCallBlock | GeneratedImageBlock | AgentEventBlock
|
||||
export type ChatBlock = UserTextBlock | AgentTextBlock | AgentReasoningBlock | CodexReviewBlock | CliOutputBlock | ToolCallBlock | GeneratedImageBlock | AgentEventBlock
|
||||
|
||||
Reference in New Issue
Block a user