mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
Port web/src/chat/** file-for-file into HapiProtocol/Chat/: - ChatTypes: block/tool/permission/usage models; AgentEvent as a record-backed struct (verbatim wire projection) with a typed Kind view and exact-key-set constructors for every synthesized event shape - Normalize/NormalizeUser/NormalizeAgent: the full decode tree (codex / output incl. agy / event families, skip rules, stringify-never-drop) - Tracer: sidechain grouping via parentToolUseId with prompt-match and parentUuid-chain fallbacks (orphan reparenting included) - ReducerEvents (pipe-format limit parsing, dedupe, api-error folding), ReducerTools (BlockBox shared-mutation model, JS-spread-faithful permission merging via presentKeys), ReducerCliOutput, ReducerTimeline (tool pairing, stream coalescing, agent-run cards, title-changed synthesis, sentinel suppression), Reducer (pending-permission synthesis gates, latestUsage, goal filtering) - ToolGroups + codex-exploration family (codexCommandPresentation's grouping predicates); ToolPresentation (minimal title/subtitle, full knownTools catalog deferred to HapiUI) - JSInterop: JS semantics pinned once (nullish coalescing over wire values, truthiness, safeStringify, canonical JSON serializer with integer-preserving number formatting, JS-anchored regex helpers) - FixtureProjection: the normative projection (projection.ts) plus runChatFixturePipeline - ChatFixtureTests: one swift-testing case per shared/fixtures/chat/*.json (48), canonical-JSON byte comparison, per-fixture line-level diff on mismatch, VERSION gate Dictionary-order note: wherever the TS relies on JS object insertion order (agentState requests iteration), the port imposes ascending key order — equivalent to the reference replay because fixture inputs are canonically key-sorted on disk and JSON.parse preserves that order.
81 lines
2.8 KiB
Swift
81 lines
2.8 KiB
Swift
import Foundation
|
|
|
|
// Port of web/src/chat/normalize.ts — total decode of one wire message.
|
|
|
|
/// Port of `normalizeDecryptedMessage`. Returns `nil` only on the two
|
|
/// legitimate drop paths (skippable Claude output, unknown codex content);
|
|
/// every other malformed shape degrades to stringified text.
|
|
public func normalizeDecryptedMessage(_ message: DecryptedMessage) -> NormalizedMessage? {
|
|
guard let record = unwrapRoleWrappedRecordEnvelope(message.content) else {
|
|
return NormalizedMessage(
|
|
id: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: .agent([.text(.init(text: safeStringify(message.content), uuid: message.id, parentUUID: nil))]),
|
|
isSidechain: false
|
|
// No meta / invokedAt on this branch (normalize.ts:10-20).
|
|
)
|
|
}
|
|
|
|
if record.role == "user" {
|
|
if var normalized = normalizeUserRecord(
|
|
messageId: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: record.content,
|
|
meta: record.meta
|
|
) {
|
|
normalized.invokedAt = message.invokedAt
|
|
return normalized
|
|
}
|
|
return NormalizedMessage(
|
|
id: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: .user(text: safeStringify(record.content), attachments: nil),
|
|
isSidechain: false,
|
|
meta: record.meta,
|
|
invokedAt: message.invokedAt
|
|
)
|
|
}
|
|
|
|
if record.role == "agent" {
|
|
if isSkippableAgentContent(record.content) {
|
|
return nil
|
|
}
|
|
let normalized = normalizeAgentRecord(
|
|
messageId: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: record.content,
|
|
meta: record.meta
|
|
)
|
|
if normalized == nil && isCodexContent(record.content) {
|
|
return nil
|
|
}
|
|
if var normalized {
|
|
normalized.invokedAt = message.invokedAt
|
|
return normalized
|
|
}
|
|
return NormalizedMessage(
|
|
id: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: .agent([.text(.init(text: safeStringify(record.content), uuid: message.id, parentUUID: nil))]),
|
|
isSidechain: false,
|
|
meta: record.meta,
|
|
invokedAt: message.invokedAt
|
|
)
|
|
}
|
|
|
|
return NormalizedMessage(
|
|
id: message.id,
|
|
localId: message.localId,
|
|
createdAt: message.createdAt,
|
|
content: .agent([.text(.init(text: safeStringify(record.content), uuid: message.id, parentUUID: nil))]),
|
|
isSidechain: false,
|
|
meta: record.meta,
|
|
invokedAt: message.invokedAt
|
|
)
|
|
}
|