sync: project quota, provider status, todo triage, and full diffs to the phone
Four Mac surfaces the iOS remote couldn't render now ride the wire, forward-compatibly (decodeIfPresent defaults; unknown HostMsg tags already decode to .unknown): - DashboardSnapshot gains `usage` (WireSubscriptionUsage — the Mac's 5-hour/weekly quota gauges) and `statusFeeds` (WireStatusFeed — active provider incidents); the host re-pushes the dashboard when a poll changes either. - WireTodo gains `triage` (raw TriageLevel name); the phone renders the Mac's TriageBadge honoring the encouraging/classic label setting. - ClientMsg.fetchDiff / HostMsg.sessionDiff deliver the full worktree patch on demand (capped at 512 KB with a truncated flag), gated on the new WireCapabilities.canFetchDiff so a new phone never sends it to an old host. iOS renders a file list + colored unified patch, falling back to the diffstat summary against older hosts. Round-trip and legacy-decode tests cover the new fields; the full suite passes apart from the pre-existing fake-backend fixture gaps and the flaky nvrsion lock-domain test (same failure rate on the base commit). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -49,6 +49,10 @@ final class RemoteStore: ObservableObject {
|
||||
@Published private(set) var openSessionID: SessionID?
|
||||
@Published private(set) var openEvents: [AgentEvent] = []
|
||||
@Published private(set) var openApprovals: [ApprovalRequest] = []
|
||||
/// The open session's full diff (the Mac Diff tab's patch), fetched on demand when the
|
||||
/// user opens the Diff tab and the host advertises `canFetchDiff`. Nil until it arrives.
|
||||
@Published private(set) var openDiff: WireSessionDiff?
|
||||
@Published private(set) var diffLoading = false
|
||||
|
||||
/// A transient host-reported error (the mobile echo of the Mac's last-error bubble):
|
||||
/// shown as a red bubble at the bottom of the screen, auto-dismissed after a few seconds.
|
||||
@@ -123,7 +127,8 @@ final class RemoteStore: ObservableObject {
|
||||
connectivity = .connected
|
||||
hostName = "Andrew's Mac"
|
||||
grantedScope = .control
|
||||
capabilities = WireCapabilities(canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName])
|
||||
capabilities = WireCapabilities(
|
||||
canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName], canFetchDiff: true)
|
||||
modelCatalog = WireModelCatalog(
|
||||
groups: [
|
||||
[WireModelCatalog.Model(sku: "claude-opus-4-8[1m]", displayName: "Opus 4.8", backend: .claudeCode,
|
||||
@@ -179,14 +184,52 @@ final class RemoteStore: ObservableObject {
|
||||
],
|
||||
todos: [
|
||||
WireTodo(id: TodoID(rawValue: "t1"), text: "Add dark mode to settings", summary: "Dark mode in settings",
|
||||
projectID: p2, projectName: "website", status: .open, dispatchedSessionID: nil, updatedAt: Date()),
|
||||
projectID: p2, projectName: "website", status: .open, dispatchedSessionID: nil,
|
||||
triage: "high", updatedAt: Date()),
|
||||
WireTodo(id: TodoID(rawValue: "t2"), text: "Investigate the memory leak in the sync server", summary: "Sync server memory leak",
|
||||
projectID: p1, projectName: "nucleic", status: .open, dispatchedSessionID: nil, updatedAt: Date()),
|
||||
projectID: p1, projectName: "nucleic", status: .open, dispatchedSessionID: nil,
|
||||
triage: "critical", updatedAt: Date()),
|
||||
WireTodo(id: TodoID(rawValue: "t3"), text: "Write release notes", summary: nil,
|
||||
projectID: nil, projectName: nil, status: .open, dispatchedSessionID: nil, updatedAt: Date()),
|
||||
projectID: nil, projectName: nil, status: .open, dispatchedSessionID: nil,
|
||||
triage: "low", updatedAt: Date()),
|
||||
],
|
||||
usage: WireSubscriptionUsage(
|
||||
fiveHour: WireUsageWindow(utilization: 42, resetsAt: Date().addingTimeInterval(3 * 3600)),
|
||||
sevenDay: WireUsageWindow(utilization: 78, resetsAt: Date().addingTimeInterval(2.4 * 86_400))),
|
||||
statusFeeds: [
|
||||
WireStatusFeed(provider: "claude", providerName: "Claude", incidents: []),
|
||||
WireStatusFeed(provider: "openai", providerName: "OpenAI", incidents: [
|
||||
WireStatusIncident(
|
||||
id: "i1", title: "Elevated errors on Codex", url: URL(string: "https://status.openai.com"),
|
||||
updatedAt: Date(), state: "Monitoring", isResolved: false, components: ["Codex"]),
|
||||
]),
|
||||
WireStatusFeed(provider: "xai", providerName: "xAI", incidents: []),
|
||||
])
|
||||
}
|
||||
|
||||
/// Offline diff fixture (NUCLEIC_DEMO) so the full-patch Diff tab renders without a host.
|
||||
private func demoDiff(_ sessionID: SessionID) -> WireSessionDiff {
|
||||
WireSessionDiff(
|
||||
sessionID: sessionID,
|
||||
stat: DiffStat(filesChanged: 2, added: 312, removed: 40),
|
||||
files: [
|
||||
WireFileDiff(path: "auth/middleware.ts", oldPath: nil, status: "modified", added: 290, removed: 38),
|
||||
WireFileDiff(path: "auth/session.ts", oldPath: nil, status: "added", added: 22, removed: 2),
|
||||
],
|
||||
patch: """
|
||||
diff --git a/auth/middleware.ts b/auth/middleware.ts
|
||||
--- a/auth/middleware.ts
|
||||
+++ b/auth/middleware.ts
|
||||
@@ -10,7 +10,9 @@ export function requireSession(req: Request) {
|
||||
- const token = req.headers.get("x-auth")
|
||||
+ const header = req.headers.get("authorization") ?? ""
|
||||
+ const token = header.replace(/^Bearer /, "")
|
||||
+ if (!token) throw new AuthError("missing bearer token")
|
||||
return verify(token)
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
/// Pair from a scanned QR (SYNC §4.2): connect (LAN hint first, else Bonjour), run XXpsk0,
|
||||
/// and on success pin the host key for future IK reconnects.
|
||||
func pair(with payload: PairingPayload) {
|
||||
@@ -240,12 +283,23 @@ final class RemoteStore: ObservableObject {
|
||||
openSessionID = sessionID
|
||||
openEvents = []
|
||||
openApprovals = []
|
||||
openDiff = nil
|
||||
diffLoading = false
|
||||
seenSeq.removeAll()
|
||||
markOpened(sessionID)
|
||||
if demoMode { seedDemoTranscript(sessionID); return }
|
||||
send(.subscribe(Subscribe(sessionID: sessionID, sinceSeq: nil, verbosity: .full)))
|
||||
}
|
||||
|
||||
/// Ask the host for the open session's full patch (Diff tab). No-op when the host
|
||||
/// doesn't advertise the capability — the view falls back to the diffstat summary.
|
||||
func fetchDiff(_ sessionID: SessionID) {
|
||||
if demoMode { openDiff = demoDiff(sessionID); return }
|
||||
guard capabilities.canFetchDiff else { return }
|
||||
diffLoading = openDiff == nil
|
||||
send(.fetchDiff(sessionID))
|
||||
}
|
||||
|
||||
/// Record that the user looked at this session now (clears its unseen-completion wash).
|
||||
func markOpened(_ sessionID: SessionID) {
|
||||
lastOpenedAt[sessionID] = Date()
|
||||
@@ -287,6 +341,8 @@ final class RemoteStore: ObservableObject {
|
||||
openSessionID = nil
|
||||
openEvents = []
|
||||
openApprovals = []
|
||||
openDiff = nil
|
||||
diffLoading = false
|
||||
}
|
||||
|
||||
func respond(_ approval: ApprovalRequest, _ decision: Decision) {
|
||||
@@ -418,6 +474,10 @@ final class RemoteStore: ObservableObject {
|
||||
}
|
||||
case .approvalResolved(let resolved):
|
||||
openApprovals.removeAll { $0.id == resolved.id }
|
||||
case .sessionDiff(let diff):
|
||||
guard diff.sessionID == openSessionID else { break }
|
||||
openDiff = diff
|
||||
diffLoading = false
|
||||
case .wireError(let error):
|
||||
// Not fatal — surface as a transient bubble (the Mac's last-error overlay).
|
||||
// Losing an approval race isn't an error worth interrupting for; the card
|
||||
|
||||
Reference in New Issue
Block a user