mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +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.
70 lines
2.3 KiB
Swift
70 lines
2.3 KiB
Swift
import Foundation
|
|
|
|
// Port of web/src/chat/normalizeUser.ts.
|
|
|
|
/// Port of `parseAttachments`: entries are accepted only when `id`,
|
|
/// `filename`, `mimeType` (strings), `size` (number) and `path` (string) are
|
|
/// all present; invalid entries are skipped and an empty result means
|
|
/// "no attachments" (`nil`).
|
|
func parseAttachments(_ raw: JSONValue?) -> [AttachmentMetadata]? {
|
|
guard let items = raw?.arrayValue else { return nil }
|
|
var attachments: [AttachmentMetadata] = []
|
|
for item in items {
|
|
guard let object = item.objectValue,
|
|
let id = object["id"]?.stringValue,
|
|
let filename = object["filename"]?.stringValue,
|
|
let mimeType = object["mimeType"]?.stringValue,
|
|
let size = object["size"]?.numberValue,
|
|
let path = object["path"]?.stringValue,
|
|
// The wire model stores byte sizes as Int; real sizes are
|
|
// always integral (TS would accept a fractional number).
|
|
let intSize = Int(exactly: size)
|
|
else { continue }
|
|
attachments.append(AttachmentMetadata(
|
|
id: id,
|
|
filename: filename,
|
|
mimeType: mimeType,
|
|
size: intSize,
|
|
path: path,
|
|
previewUrl: object["previewUrl"]?.stringValue
|
|
))
|
|
}
|
|
return attachments.isEmpty ? nil : attachments
|
|
}
|
|
|
|
/// Port of `normalizeUserRecord`.
|
|
func normalizeUserRecord(
|
|
messageId: String,
|
|
localId: String?,
|
|
createdAt: Int,
|
|
content: JSONValue,
|
|
meta: JSONValue?
|
|
) -> NormalizedMessage? {
|
|
if let text = content.stringValue {
|
|
return NormalizedMessage(
|
|
id: messageId,
|
|
localId: localId,
|
|
createdAt: createdAt,
|
|
content: .user(text: text, attachments: nil),
|
|
isSidechain: false,
|
|
meta: meta
|
|
)
|
|
}
|
|
|
|
if let object = content.objectValue,
|
|
object["type"] == .string("text"),
|
|
let text = object["text"]?.stringValue {
|
|
let attachments = parseAttachments(object["attachments"])
|
|
return NormalizedMessage(
|
|
id: messageId,
|
|
localId: localId,
|
|
createdAt: createdAt,
|
|
content: .user(text: text, attachments: attachments),
|
|
isSidechain: false,
|
|
meta: meta
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|