Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 133 additions & 1 deletion src/api/normalizers/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
Turn,
UserInput,
} from '../appServerDtos'
import type { CommandExecutionData, UiFileAttachment, UiMessage, UiProjectGroup, UiThread } from '../../types/codex'
import type { CommandExecutionData, UiFileAttachment, UiFileChangeData, UiMessage, UiProjectGroup, UiThread } from '../../types/codex'

function toIso(seconds: number): string {
return new Date(seconds * 1000).toISOString()
Expand Down Expand Up @@ -160,6 +160,43 @@ function toUiMessages(item: ThreadItem): UiMessage[] {
]
}

if (item.type === 'fileChange') {
const raw = item as Record<string, unknown>
const status = normalizeFileChangeStatus(raw.status)
const changes = Array.isArray(raw.changes) ? raw.changes : []
const messages: UiMessage[] = []

for (const [index, change] of changes.entries()) {
if (!change || typeof change !== 'object' || Array.isArray(change)) continue
const row = change as Record<string, unknown>
const path = typeof row.path === 'string' ? row.path.trim() : ''
if (!path) continue

const diff = typeof row.diff === 'string' ? row.diff : ''
const { kind, movePath } = readFileChangeKind(row.kind)
const { linesAdded, linesRemoved, openLine } = readFileChangeDiffStats(diff)

messages.push({
id: `${item.id}:change:${index}`,
role: 'system',
text: path,
messageType: 'fileChange',
fileChange: {
path,
kind,
status,
diff,
movePath,
linesAdded,
linesRemoved,
openLine,
},
})
}

return messages
}

return []
}

Expand All @@ -169,6 +206,97 @@ function normalizeCommandStatus(value: unknown): CommandExecutionData['status']
return 'completed'
}

function normalizeFileChangeStatus(value: unknown): UiFileChangeData['status'] {
if (value === 'completed' || value === 'failed' || value === 'declined') {
return value
}
if (value === 'inProgress' || value === 'in_progress') return 'inProgress'
return 'completed'
}

function readFileChangeKind(value: unknown): Pick<UiFileChangeData, 'kind' | 'movePath'> {
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const record = value as Record<string, unknown>
const type = record.type
if (type === 'add' || type === 'delete' || type === 'update') {
const movePath =
type === 'update' && typeof record.move_path === 'string' && record.move_path.trim().length > 0
? record.move_path.trim()
: null
return { kind: type, movePath }
}
}

return { kind: 'update', movePath: null }
}

function readFileChangeDiffStats(diff: string): Pick<UiFileChangeData, 'linesAdded' | 'linesRemoved' | 'openLine'> {
if (!diff.trim()) {
return {
linesAdded: 0,
linesRemoved: 0,
openLine: null,
}
}

let linesAdded = 0
let linesRemoved = 0
let openLine: number | null = null
let nextOldLine: number | null = null
let nextNewLine: number | null = null

for (const line of diff.split(/\r?\n/u)) {
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/u)
if (hunkMatch) {
nextOldLine = Number(hunkMatch[1])
nextNewLine = Number(hunkMatch[2])
if (openLine === null) {
openLine = nextNewLine > 0 ? nextNewLine : nextOldLine
}
continue
}

if (
line.startsWith('diff --git ') ||
line.startsWith('index ') ||
line.startsWith('+++ ') ||
line.startsWith('--- ') ||
line.startsWith('Binary files ')
) {
continue
}

if (line.startsWith('+')) {
linesAdded += 1
if (openLine === null && nextNewLine !== null) {
openLine = nextNewLine
}
if (nextNewLine !== null) nextNewLine += 1
continue
}

if (line.startsWith('-')) {
linesRemoved += 1
if (openLine === null && nextOldLine !== null) {
openLine = nextOldLine
}
if (nextOldLine !== null) nextOldLine += 1
continue
}

if (line.startsWith(' ')) {
if (nextOldLine !== null) nextOldLine += 1
if (nextNewLine !== null) nextNewLine += 1
}
}

return {
linesAdded,
linesRemoved,
openLine,
}
}

function pickThreadName(summary: Thread): string {
const direct = [summary.preview]
for (const candidate of direct) {
Expand Down Expand Up @@ -265,6 +393,10 @@ export function normalizeThreadMessagesV2(payload: ThreadReadResponse): UiMessag
return messages
}

export function normalizeThreadItemV2(item: ThreadItem): UiMessage[] {
return toUiMessages(item)
}

export function readThreadInProgressFromResponse(payload: ThreadReadResponse): boolean {
const turns = Array.isArray(payload.thread.turns) ? payload.thread.turns : []
return isTurnInProgress(turns.at(-1))
Expand Down
Loading