import APIClient import Foundation import Observation import WireProtocol /// T-iOS-26 · 项目详情状态(`GET /projects/detail?path=` → phase 状态机, /// 与 DiffViewModel/TimelineViewModel 同一纪律)。 /// /// fetch 闭包注入 —— 生产由 `forHost` 包 `APIClient.projectDetail(path:)` /// (builder 百分号编码、400/404/500 `{error}` → 类型化错误均在 T-iOS-38 /// 完成并已测);测试注入 fake。本 VM 只归约用户可见的三种结局: /// - 成功 → `.loaded(ProjectDetail)`(sessions/worktrees/hasClaudeMd 透传); /// - 400 → `.failed(.pathInvalid)`、404 → `.failed(.notFound)`、 /// 500/解码/传输 → `.failed(.unavailable)` —— 全部可经 `load()` 重试。 @MainActor @Observable final class ProjectDetailViewModel { /// 用户可见的失败三分类(文案映射在 ProjectDetailScreen)。 enum Failure: Equatable { case pathInvalid case notFound case unavailable } enum Phase: Equatable { case loading case loaded(ProjectDetail) case failed(Failure) } private(set) var phase: Phase = .loading /// 详情/diff 的目标项目路径(来自列表行 —— 服务器数据;只透传给 /// builder,绝不本地拼 URL)。 let path: String @ObservationIgnored private let fetch: @Sendable () async throws -> ProjectDetail init(path: String, fetch: @escaping @Sendable () async throws -> ProjectDetail) { self.path = path self.fetch = fetch } /// 生产装配缝(ProjectsViewModel.makeDetailViewModel 经此构造)。 static func forHost( endpoint: HostEndpoint, http: any HTTPTransport, path: String ) -> ProjectDetailViewModel { let client = APIClient(endpoint: endpoint, http: http) return ProjectDetailViewModel(path: path, fetch: { try await client.projectDetail(path: path) }) } /// Fetch 并呈现。也是「重试」路径:`.failed` 后可再次调用。 func load() async { phase = .loading do { phase = .loaded(try await fetch()) } catch let error as APIClientError { phase = .failed(Self.failure(for: error)) } catch { phase = .failed(.unavailable) // 传输层等其余错误:可重试兜底 } } private static func failure(for error: APIClientError) -> Failure { switch error { case .projectPathInvalid, .invalidRequest: return .pathInvalid case .projectNotFound: return .notFound default: return .unavailable } } }