Merge claude/elastic-haibt-f29115 into dev (iPad remote port: adaptive sidebar+detail shell, Mac-style two-pane diff, structured commit/command transcript cards, keyboard/pointer affordances, camera-free manual pairing)

This commit is contained in:
2026-07-04 00:54:14 -07:00
14 changed files with 776 additions and 70 deletions
@@ -1,6 +1,9 @@
import Foundation
import Security
import NucleicProtocol
#if canImport(UIKit)
import UIKit
#endif
/// What the phone pins about its Mac at pairing (SYNC §4.2): the host's static key (for IK
/// reconnect), a display name, and the transport + connection hint from the QR — LAN
@@ -42,11 +45,22 @@ enum IdentityStore {
static func deviceID() -> String {
let defaults = UserDefaults.standard
if let existing = defaults.string(forKey: deviceIDKey) { return existing }
let id = "iphone-" + UUID().uuidString.prefix(8).lowercased()
let id = deviceIDPrefix + UUID().uuidString.prefix(8).lowercased()
defaults.set(id, forKey: deviceIDKey)
return id
}
/// Idiom-tagged prefix so the host lists a paired device with the right kind/icon
/// (`ipad-…` vs `iphone-…`). Only stamps *freshly generated* ids — an existing install
/// keeps whatever id it already persisted, so upgrading a phone never changes its identity.
private static var deviceIDPrefix: String {
#if canImport(UIKit)
return UIDevice.current.userInterfaceIdiom == .pad ? "ipad-" : "iphone-"
#else
return "iphone-"
#endif
}
static func loadPairedHost() -> PairedHost? {
guard let data = UserDefaults.standard.data(forKey: pairedHostKey) else { return nil }
return try? JSONDecoder().decode(PairedHost.self, from: data)
@@ -295,6 +295,17 @@ final class RemoteStore: ObservableObject {
+ if (!token) throw new AuthError("missing bearer token")
return verify(token)
}
diff --git a/auth/session.ts b/auth/session.ts
new file mode 100644
--- /dev/null
+++ b/auth/session.ts
@@ -0,0 +1,6 @@
+export interface Session {
+ userId: string
+ issuedAt: number
+}
+
+export const SESSION_TTL = 3600
""")
}
@@ -618,6 +629,12 @@ final class RemoteStore: ObservableObject {
AgentEvent(sessionID: sessionID, seq: seq, at: Date(), backend: .claudeCode,
nativeType: nil, kind: kind)
}
// A realistic `git commit` (heredoc message) so the transcript's structured commit card
// is exercisable offline: expand the Bash call to see the subject + Markdown body.
let demoCommitCommand = "git commit -F - <<'EOF'\nfix: harden auth middleware\n\nRequire a Bearer token and reject a missing or blank one.\n\n- extract `requireSession`\n- add a `Bearer` prefix check\nEOF"
// A multi-step, destructive shell pipeline so the transcript's step list (with the delete
// flagged in red) is exercisable offline: expand the Bash call to see the breakdown.
let demoCleanupCommand = "cd ~/code/nucleic && rm -rf .worktrees/auth-old && git worktree prune && git branch -D nucleic/auth-old"
openEvents = [
event(1, .sessionStarted(SessionStarted(
backendSessionID: "demo", model: "claude-opus-4-8[1m]", cwd: "~/code/nucleic", toolNames: []))),
@@ -633,8 +650,14 @@ final class RemoteStore: ObservableObject {
event(11, .toolCallStarted(ToolCall(toolCallID: "t3", name: "Task", input: ["description": "Audit other call sites", "prompt": "Find every caller of the old auth API."]))),
event(12, .toolCallCompleted(ToolCall(toolCallID: "t3", name: "Task", input: ["description": "Audit other call sites"]))),
event(13, .toolResult(ToolResult(toolCallID: "t3", content: "Checked 7 files; 1 stale caller updated.", isError: false))),
event(14, .usage(Usage(inputTokens: 84_300, outputTokens: 2_140, costUSD: 0.0421, contextInputTokens: 84_300))),
event(15, .runFinished(RunFinished(outcome: .completed, finalText: "Done."))),
event(14, .toolCallStarted(ToolCall(toolCallID: "t4", name: "Bash", input: ["command": .string(demoCommitCommand)]))),
event(15, .toolCallCompleted(ToolCall(toolCallID: "t4", name: "Bash", input: ["command": .string(demoCommitCommand)]))),
event(16, .toolResult(ToolResult(toolCallID: "t4", content: "[nucleic/auth-refactor 1a2b3c4] fix: harden auth middleware\n 2 files changed, 312 insertions(+), 40 deletions(-)", isError: false))),
event(17, .toolCallStarted(ToolCall(toolCallID: "t5", name: "Bash", input: ["command": .string(demoCleanupCommand)]))),
event(18, .toolCallCompleted(ToolCall(toolCallID: "t5", name: "Bash", input: ["command": .string(demoCleanupCommand)]))),
event(19, .toolResult(ToolResult(toolCallID: "t5", content: "Removed 1 worktree; deleted branch nucleic/auth-old.", isError: false))),
event(20, .usage(Usage(inputTokens: 84_300, outputTokens: 2_140, costUSD: 0.0421, contextInputTokens: 84_300))),
event(21, .runFinished(RunFinished(outcome: .completed, finalText: "Done."))),
]
// If this session is blocked on a human, surface a real approval card so the
// Allow/Deny loop is exercisable in the demo (the seeded `a1` session).
@@ -653,11 +676,17 @@ final class RemoteStore: ObservableObject {
ApprovalID(rawValue: "demo-appr-\(sessionID.rawValue)")
}
func closeOpen() {
if let id = openSessionID {
send(.unsubscribe(id))
markOpened(id) // everything up to now has been seen
}
/// Close a session's live subscription. `id` names *which* session is closing — the detail
/// view passes its own. On iPad's split view, switching session A→B can mount B (which calls
/// `open(B)`, setting `openSessionID = B`) *before* A's detail disappears; so we always
/// unsubscribe the named session but only tear down the shared open-state when it still
/// belongs to that session — otherwise we'd wipe B's freshly-loaded transcript. Called with
/// no argument it closes whatever is currently open (the iPhone push/pop path, unchanged).
func closeOpen(_ id: SessionID? = nil) {
guard let target = id ?? openSessionID else { return }
send(.unsubscribe(target))
markOpened(target) // everything up to now has been seen
guard openSessionID == target else { return }
openSessionID = nil
openEvents = []
openApprovals = []