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.
65 lines
2.2 KiB
Swift
65 lines
2.2 KiB
Swift
import Foundation
|
|
|
|
// Port of web/src/chat/reducerCliOutput.ts — CLI-echo detection and the
|
|
// command-name → local-command-stdout merge.
|
|
|
|
private let cliTagRegex = JSRegex("<(?:local-command-[a-z-]+|command-(?:name|message|args))>", caseInsensitive: true)
|
|
private let cliCommandNameRegex = JSRegex("<command-name>", caseInsensitive: true)
|
|
private let cliCommandStdoutRegex = JSRegex("<local-command-stdout>", caseInsensitive: true)
|
|
|
|
private func getMetaSentFrom(_ meta: JSONValue?) -> String? {
|
|
meta?.objectValue?["sentFrom"]?.stringValue
|
|
}
|
|
|
|
private func hasCliOutputTags(_ text: String) -> Bool {
|
|
cliTagRegex.test(text)
|
|
}
|
|
|
|
private func hasCommandNameTag(_ text: String) -> Bool {
|
|
cliCommandNameRegex.test(text)
|
|
}
|
|
|
|
private func hasLocalCommandStdoutTag(_ text: String) -> Bool {
|
|
cliCommandStdoutRegex.test(text)
|
|
}
|
|
|
|
/// Port of `isCliOutputText`.
|
|
func isCliOutputText(_ text: String, meta: JSONValue?) -> Bool {
|
|
getMetaSentFrom(meta) == "cli" && hasCliOutputTags(text)
|
|
}
|
|
|
|
/// Port of `mergeCliOutputBlocks`: a `<command-name>` block absorbs its
|
|
/// `<local-command-stdout>` follow-up (same source), preferring the command
|
|
/// block's metadata.
|
|
func mergeCliOutputBlocks(_ blocks: [BlockBox]) -> [BlockBox] {
|
|
var merged: [BlockBox] = []
|
|
|
|
for box in blocks {
|
|
guard case .cliOutput(let block) = box.block else {
|
|
merged.append(box)
|
|
continue
|
|
}
|
|
|
|
if let prevBox = merged.last,
|
|
case .cliOutput(let prev) = prevBox.block,
|
|
prev.source == block.source,
|
|
hasCommandNameTag(prev.text),
|
|
!hasLocalCommandStdoutTag(prev.text),
|
|
hasLocalCommandStdoutTag(block.text) {
|
|
let separator = (prev.text.hasSuffix("\n") || block.text.hasPrefix("\n")) ? "" : "\n"
|
|
var next = prev
|
|
next.text = prev.text + separator + block.text
|
|
next.invokedAt = prev.invokedAt ?? block.invokedAt
|
|
next.durationMs = prev.durationMs ?? block.durationMs
|
|
next.usage = prev.usage ?? block.usage
|
|
next.model = prev.model ?? block.model
|
|
prevBox.block = .cliOutput(next)
|
|
continue
|
|
}
|
|
|
|
merged.append(box)
|
|
}
|
|
|
|
return merged
|
|
}
|