mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
The hub's fork, rewind and export routes had no iOS UI. The chat's menu now offers: - Fork Conversation: when the agent advertises forkCurrent, the session is live and no Codex turn is running (the web gate). The new session opens in place of this chat. - Rewind or Fork…: a sheet listing loaded user messages that have a native point in metadata.conversationHistoryPoints. Each row offers a confirmed Fork from Here or Rewind to Here, gated like web: forks need an idle turn except on Codex, and rewinds always need one. The actions live in a sheet, not on the transcript bubbles, because those keep text selection and cached row heights. After a rewind the tail resyncs with a history reset. An ambiguous native boundary (409 ambiguous_native_boundary_fork_safe) offers a fork from the same point instead. - Export Conversation…: saves the hub's lossless JSON payload through the Files save panel, using the web download's filename. A large export asks before retrying with force; a 413 explains the limit. Markdown export stays web-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
1.6 KiB
Swift
42 lines
1.6 KiB
Swift
import Foundation
|
|
import HapiProtocol
|
|
|
|
/// Hub-provided reopen failure text (the Android `formatReopenError` port):
|
|
/// a `{error, missing?}` body becomes `"<error> (missing: a, b)"`. Nil when
|
|
/// the failure carries no hub message, so the caller shows its own
|
|
/// localized fallback.
|
|
public func formatReopenError(_ error: any Error) -> String? {
|
|
guard let message = hubErrorMessage(error),
|
|
let body = (error as? APIError)?.body,
|
|
let value = try? HapiJSON.decoder.decode(JSONValue.self, from: Data(body.utf8)),
|
|
case .object(let object) = value else {
|
|
return nil
|
|
}
|
|
var missing: [String] = []
|
|
if case .array(let items)? = object["missing"] {
|
|
missing = items.compactMap { item in
|
|
if case .string(let field) = item { return field }
|
|
return nil
|
|
}
|
|
}
|
|
return missing.isEmpty ? message : "\(message) (missing: \(missing.joined(separator: ", ")))"
|
|
}
|
|
|
|
/// The hub's `{error}` text, or nil when the failure carries none.
|
|
public func hubErrorMessage(_ error: any Error) -> String? {
|
|
guard let body = (error as? APIError)?.body,
|
|
let value = try? HapiJSON.decoder.decode(JSONValue.self, from: Data(body.utf8)),
|
|
case .object(let object) = value,
|
|
case .string(let message)? = object["error"],
|
|
!message.isEmpty else {
|
|
return nil
|
|
}
|
|
return message
|
|
}
|
|
|
|
/// `DELETE /sessions/:id` answers 409 while the session is active (or
|
|
/// became active again) — the caller shows "archive first" wording.
|
|
public func isActiveSessionConflict(_ error: any Error) -> Bool {
|
|
(error as? APIError)?.status == 409
|
|
}
|