feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
This commit is contained in:
@@ -9,6 +9,16 @@ import UIKit
|
||||
/// (refined native, Apple HIG), dark-mode-first, amber-gold accent (desktop-matched)
|
||||
/// continuing the web selection color.
|
||||
///
|
||||
/// T-iOS-34 · dark-mode-first no longer means dark-mode-only: the app has a real
|
||||
/// theme setting (`AppTheme` / `ThemeStore`, injected once by `RootView`), so
|
||||
/// EVERY color token must resolve sensibly in both schemes. Adaptive tokens are
|
||||
/// built with the private `adaptive(dark:light:)` helper below and their light
|
||||
/// values are contrast-audited in `AppThemeTests` (WCAG 1.4.11, 3:1 floor for
|
||||
/// non-text UI). Known accepted gap: `accent` at its frozen light value #C9892F
|
||||
/// is 2.88:1 on white — fine as a FILL (dark ink on top is 6.2:1) but marginal
|
||||
/// as bare tinted text; the value is pinned by `DesignSystemTests` and left
|
||||
/// unchanged here (reported to the orchestrator instead of silently re-toned).
|
||||
///
|
||||
/// Vocabulary (all under `DS.`):
|
||||
/// - `Palette` — adaptive `accent` (amber gold, matches desktop) · semantic `status*` colors
|
||||
/// (color is NEVER the only status signal — pair with a symbol,
|
||||
@@ -59,21 +69,36 @@ enum DS {
|
||||
/// ink for contrast — mirrors web --on-accent #1A1305).
|
||||
static let onAccent = rgb(26, 19, 5)
|
||||
/// Faint accent wash (selection/soft highlight) — web --accent-soft.
|
||||
static let accentSoft = Color(red: 0xE3 / 255.0, green: 0xA6 / 255.0, blue: 0x4A / 255.0, opacity: 0.15)
|
||||
/// Adaptive (T-iOS-34): the dark wash is the web value verbatim; on a
|
||||
/// light background a 15% wash of the BRIGHT gold reads as nothing, so
|
||||
/// light uses the deeper `--accent-2` gold at a slightly higher alpha.
|
||||
/// Still a wash in both (alpha < 0.3 — never a solid fill).
|
||||
static let accentSoft = adaptive(
|
||||
dark: UIColor(hex: 0xE3A6_4A, alpha: 0.15),
|
||||
light: UIColor(hex: 0xC989_2F, alpha: 0.18)
|
||||
)
|
||||
|
||||
// ── Semantic status colors (match the desktop/web status palette) ───
|
||||
// These are the ONLY status colors. `StatusStyle` pairs each with a
|
||||
// distinct SF Symbol so status is never conveyed by color alone. Hex
|
||||
// mirrors the web's warm status set (public/style.css:18-20) so iOS and
|
||||
// desktop read the same. `waiting` amber #F5B14C stays distinct from the
|
||||
// gold accent #E3A64A (brighter/less brown), and is only ever a small
|
||||
// badge fill, never a large accent surface.
|
||||
/// working — #46D07F (web --green).
|
||||
static let statusWorking = rgb(70, 208, 127)
|
||||
/// waiting / needs-me — #F5B14C (web --amber).
|
||||
static let statusWaiting = rgb(245, 177, 76)
|
||||
/// stuck — #FF6B6B (web --red).
|
||||
static let statusStuck = rgb(255, 107, 107)
|
||||
// distinct SF Symbol so status is never conveyed by color alone. The
|
||||
// DARK values mirror the web's warm status set (public/style.css:18-20)
|
||||
// byte for byte so iOS and desktop read the same; `waiting` amber
|
||||
// #F5B14C stays distinct from the gold accent #E3A64A (brighter/less
|
||||
// brown), and is only ever a small badge fill, never a large surface.
|
||||
//
|
||||
// T-iOS-34 · the LIGHT values are new. The web chrome is dark-only, so
|
||||
// there was nothing to mirror: each light value is the same hue darkened
|
||||
// until it clears WCAG 1.4.11 (3:1 for non-text UI) against a white
|
||||
// background — the bright dark-mode values sit at 1.96:1 (#46D07F),
|
||||
// 1.60:1 (#F5B14C) and 2.74:1 (#FF6B6B) on white, i.e. unreadable.
|
||||
// `AppThemeTests` pins BOTH the dark bytes and the light contrast floor.
|
||||
/// working — dark #46D07F (web --green) · light #1F8F52 (4.0:1 on white).
|
||||
static let statusWorking = adaptive(dark: 0x46D0_7F, light: 0x1F8F_52)
|
||||
/// waiting / needs-me — dark #F5B14C (web --amber) · light #C25E00
|
||||
/// (4.2:1; burnt orange keeps it distinct from the light gold accent).
|
||||
static let statusWaiting = adaptive(dark: 0xF5B1_4C, light: 0xC25E_00)
|
||||
/// stuck — dark #FF6B6B (web --red) · light #D22B2B (5.1:1).
|
||||
static let statusStuck = adaptive(dark: 0xFF6B_6B, light: 0xD22B_2B)
|
||||
/// idle — quiet secondary gray (distinguished from `unknown` by shape).
|
||||
static let statusIdle = Color.secondary
|
||||
/// unknown / no signal yet — gray.
|
||||
@@ -86,10 +111,12 @@ enum DS {
|
||||
// and `stuck` reuse the status tokens above (same meaning); `done`
|
||||
// reuses `statusWorking` (a completed run). `tool`/`user` get their own
|
||||
// tokens so nothing in the app hardcodes a raw SwiftUI color.
|
||||
/// tool run — indigo, tied to the app accent family (#5E9EFF-ish).
|
||||
static let timelineTool = rgb(94, 158, 255)
|
||||
/// user message — violet (#AF7BFF), distinct from accent & tool.
|
||||
static let timelineUser = rgb(175, 123, 255)
|
||||
/// tool run — dark #5E9EFF · light #2C6ED6 (4.8:1 on white; the bright
|
||||
/// blue is 2.63:1 there).
|
||||
static let timelineTool = adaptive(dark: 0x5E9E_FF, light: 0x2C6E_D6)
|
||||
/// user message — dark #AF7BFF · light #7A3BD6 (6.1:1; bright violet is
|
||||
/// 2.81:1). Distinct from accent & tool in both schemes.
|
||||
static let timelineUser = adaptive(dark: 0xAF7B_FF, light: 0x7A3B_D6)
|
||||
|
||||
// ── Surfaces & text ────────────────────────────────────────────────
|
||||
|
||||
@@ -106,18 +133,48 @@ enum DS {
|
||||
/// Tertiary label color (de-emphasized detail).
|
||||
static let textTertiary = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
// ── Terminal canvas (fixed warm-dark, matches the desktop terminal) ──
|
||||
// A terminal reads as dark regardless of app appearance (like the
|
||||
// desktop). Values mirror the web chrome: --bg #100F0D / --text #ECE9E3.
|
||||
/// Terminal background — warm near-black #100F0D (web --bg).
|
||||
static let terminalBackground = rgb(16, 15, 13)
|
||||
/// Terminal foreground — warm off-white #ECE9E3 (web --text).
|
||||
static let terminalForeground = rgb(236, 233, 227)
|
||||
// ── Terminal canvas (theme-following as of T-iOS-34) ────────────────
|
||||
// Was a FIXED warm-dark canvas ("a terminal reads as dark regardless of
|
||||
// app appearance"). With a real light theme that stops being true — a
|
||||
// full-screen near-black slab is the loudest thing on a light UI. The
|
||||
// two schemes now live in `TerminalPalette` (which also vends the
|
||||
// already-resolved set SwiftTerm needs); these stay as the SwiftUI-side
|
||||
// tokens so existing call sites keep compiling and become adaptive.
|
||||
/// Terminal background — dark #100F0D (web --bg) · light #F6F7F9.
|
||||
static let terminalBackground = Color(uiColor: terminalBackgroundUIColor())
|
||||
/// Terminal foreground — dark #ECE9E3 (web --text) · light #1A1D24.
|
||||
static let terminalForeground = Color(uiColor: terminalForegroundUIColor())
|
||||
|
||||
/// Dynamic terminal background. Exposed as `UIColor` because SwiftTerm's
|
||||
/// `nativeBackgroundColor` takes UIKit colors AND flattens them on set
|
||||
/// (see `TerminalPalette`) — the bridge must not go through SwiftUI.
|
||||
static func terminalBackgroundUIColor() -> UIColor {
|
||||
TerminalPalette.dynamicBackground()
|
||||
}
|
||||
|
||||
/// Dynamic terminal foreground (same rationale as the background).
|
||||
static func terminalForegroundUIColor() -> UIColor {
|
||||
TerminalPalette.dynamicForeground()
|
||||
}
|
||||
|
||||
/// Build an opaque sRGB color from 0–255 components.
|
||||
private static func rgb(_ r: Double, _ g: Double, _ b: Double) -> Color {
|
||||
Color(.sRGB, red: r / 255, green: g / 255, blue: b / 255, opacity: 1)
|
||||
}
|
||||
|
||||
/// One scheme-adaptive color from two `0xRRGGBB` values. THE way to add
|
||||
/// an adaptive token — hand-rolling a `UIColor { trait in }` per token
|
||||
/// is how a scheme branch gets forgotten.
|
||||
private static func adaptive(dark: UInt32, light: UInt32) -> Color {
|
||||
adaptive(dark: UIColor(hex: dark), light: UIColor(hex: light))
|
||||
}
|
||||
|
||||
/// Same, for values that need a non-opaque alpha (washes).
|
||||
private static func adaptive(dark: UIColor, light: UIColor) -> Color {
|
||||
Color(uiColor: UIColor { trait in
|
||||
trait.userInterfaceStyle == .dark ? dark : light
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Space
|
||||
|
||||
Reference in New Issue
Block a user