Files
hapi/ios/Packages/HapiKit/Sources/HapiClient/Endpoints/MachineEndpoints.swift
T
weishu fa91f06662 feat(ios): new session flow (A-M3c)
Port of the tested Android NEW SESSION reference (B-M3d) to iOS:

- HapiProtocol: NewSessionCatalogs (static claude models/efforts + codex
  reasoning-effort fallback, exact Android data) in Catalog/; CodexModelSummary
  + CodexModelsResponse wire types (shared/src/apiTypes.ts).
- HapiClient: machineCodexModels endpoint (GET /api/machines/:id/codex-models,
  rpc_target_missing surfaces as APIError); NewSession/NewSessionForm.swift —
  typed Codable draft (tolerant decode) + NewSessionLogic: exact spawn body
  per SpawnSessionRequestSchema (yolo incl. false for non-grok/non-codex-family,
  permissionMode incl. 'default' for grok+codex-family, sessionType always,
  trimmed-or-absent worktreeName, serviceTier only while fast tier visible,
  collaborationMode only when plan, model only claude/codex), parent-path
  derivation, suggestion filtering, recent-path LRU(8), worktree-name
  validation, codex catalog helpers, draft sanitization.
- App: Features/NewSession (NewSessionModel @Observable orchestration —
  machine preselect last-used, 250 ms debounced list-directory autocomplete
  with per-parent cache, exists probe with worktree-blocking / simple
  two-tap-create, codex catalog fetch + selection reconcile, UserDefaults
  draft/prefs per hub; NewSessionView Form UI with per-flavor option matrix);
  session-list "+" toolbar button on HomeView presents the sheet, success
  dismisses and pushes the chat.
- Tests: spawn-body exactness (4 configs, canonical JSON), parent query,
  suggestions, LRU, worktree validation, fast-tier detection, reasoning-effort
  normalization, draft sanitize + tolerant decode; codex-models endpoint
  request/error construction.
2026-08-18 10:03:05 +08:00

76 lines
2.9 KiB
Swift

import Foundation
import HapiProtocol
/// Machines and spawning (`docs/api/client-contract/rest.md`).
extension APIClient {
/// `GET /api/machines` — online machines in the caller's namespace.
public func listMachines() async throws -> [Machine] {
let response: MachinesResponse = try await request(.get, "/api/machines")
return response.machines
}
/// `PATCH /api/machines/:id` — rename (≤ 64 chars; empty string clears
/// the custom name back to the hostname).
public func renameMachine(id: String, displayName: String) async throws {
struct RenameMachineRequest: Encodable {
let displayName: String
}
try await requestVoid(
.patch,
"/api/machines/\(encodePathComponent(id))",
body: RenameMachineRequest(displayName: displayName)
)
}
/// `POST /api/machines/:id/spawn`. The response is discriminated on
/// `type`, not HTTP status — a failed spawn is still HTTP 200 with
/// `.error`.
public func spawnSession(machineId: String, _ spawn: SpawnRequest) async throws -> SpawnResponse {
try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/spawn",
body: spawn
)
}
/// `POST /api/machines/:id/list-directory` (RPC envelope — check
/// `success`).
public func listMachineDirectory(
machineId: String,
path: String,
includeHidden: Bool = false
) async throws -> MachineListDirectoryResponse {
struct ListDirectoryRequest: Encodable {
let path: String
let includeHidden: Bool
}
return try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/list-directory",
body: ListDirectoryRequest(path: path, includeHidden: includeHidden)
)
}
/// `POST /api/machines/:id/paths/exists` (≤ 1000 paths).
public func machinePathsExist(machineId: String, paths: [String]) async throws -> [String: Bool] {
struct PathsExistsRequest: Encodable {
let paths: [String]
}
let response: MachinePathsExistsResponse = try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/paths/exists",
body: PathsExistsRequest(paths: paths)
)
return response.exists
}
/// `GET /api/machines/:id/codex-models` — pre-spawn codex model catalog
/// (RPC envelope — check `success`). A runner without the machine-scoped
/// RPC answers 503 `rpc_target_missing`, surfaced as an ``APIError``
/// whose `code` the new-session form uses to hide the picker. Added in
/// A-M3c (endpoint documented in `docs/api/client-contract/rest.md`).
public func machineCodexModels(machineId: String) async throws -> CodexModelsResponse {
try await request(.get, "/api/machines/\(encodePathComponent(machineId))/codex-models")
}
}