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:
Yaojia Wang
2026-07-30 15:57:41 +02:00
parent 9114630c3a
commit 284cfd193a
70 changed files with 10271 additions and 358 deletions

View File

@@ -16,18 +16,28 @@ import SwiftUI
/// `StackRootView` `RootView` body **** compact
/// iPhone
/// `DS.*` token //
/// T-iOS-34 · `ThemeStore` `@main`
/// store `preferredColorScheme`
///
struct RootView: View {
@Bindable var coordinator: AppCoordinator
/// `UserDefaults` `@State` 寿
@State private var themeStore = ThemeStore()
var body: some View {
AdaptiveRootView(coordinator: coordinator)
// DS tint Tokens.swift
// gate amber orange `.tint`
.tint(DS.Palette.accent)
// web DEFAULT_SETTINGS.theme = 'dark'
// --bg #100F0D
// accent/status
.preferredColorScheme(.dark)
// web `DEFAULT_SETTINGS.theme='dark'`
// `.preferredColorScheme(.dark)`
// `colorScheme` nil = iOS
// token
// `Tokens.swift` WCAG 3:1
.preferredColorScheme(themeStore.theme.colorScheme)
// store穿
.environment(themeStore)
}
}
@@ -194,10 +204,16 @@ struct CertRenewalWarningBanner: View {
}
}
// MARK: - Projects toolbar item (shared stack + split, DRY)
// MARK: - Root leading toolbar (shared stack + split, DRY)
/// leading stack split disabled
/// `presentProjects` a11y id tint label accent
/// leading stack split disabled
/// a11y id tint label accent
///
/// +T-iOS-34
/// **** `ProjectsToolbarItem``SplitRootView.swift`
/// C4 Owns iPhone(stack)
/// iPad(split)
/// `RootLeadingToolbar` `SplitRootView.swift`
struct ProjectsToolbarItem: ToolbarContent {
@Bindable var coordinator: AppCoordinator
@@ -211,6 +227,41 @@ struct ProjectsToolbarItem: ToolbarContent {
.disabled(coordinator.sessionList.activeHost == nil)
.accessibilityIdentifier("sessions.projectsButton")
}
ToolbarItem(placement: .topBarLeading) {
SettingsToolbarButton()
}
}
}
/// 齿+ sheet `@State`
/// `AppCoordinator` `isSettingsPresented`
///
///
/// `ThemeStore` ****/ store
/// 齿 crash
struct SettingsToolbarButton: View {
@Environment(ThemeStore.self) private var themeStore: ThemeStore?
@State private var isPresented = false
var body: some View {
if let themeStore {
Button {
isPresented = true
} label: {
Label(RootCopy.settings, systemImage: "gearshape")
}
.accessibilityIdentifier("sessions.settingsButton")
.sheet(isPresented: $isPresented) {
NavigationStack {
SettingsScreen(themeStore: themeStore)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(RootCopy.done) { isPresented = false }
}
}
}
}
}
}
}
@@ -218,6 +269,7 @@ struct ProjectsToolbarItem: ToolbarContent {
enum RootCopy {
static let continueLast = "继续上次会话"
static let projects = "项目"
static let settings = "设置"
static let done = "完成"
/// B3 (HIGH) · Shown when the silent device-cert renewal keeps failing.
static let certRenewalFailing = "设备证书自动续期失败,将在下次前台重试"