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

@@ -0,0 +1,122 @@
import SwiftUI
/// T-iOS-34 · **** / /
///
/// Dynamic Type`TerminalTheme`
/// `.footnote` /
///
///
/// `ThemeStore.select` + `@Observable`
/// `preferredColorScheme`
struct SettingsScreen: View {
/// `RootView`
let themeStore: ThemeStore
/// **** `preferredColorScheme`
///
@Environment(\.colorScheme) private var effectiveScheme
var body: some View {
List {
themeSection
terminalPreviewSection
}
.navigationTitle(SettingsCopy.title)
}
// MARK: -
private var themeSection: some View {
Section {
ForEach(AppTheme.allCases, id: \.self) { theme in
themeRow(theme)
}
} header: {
Text(SettingsCopy.themeHeader)
} footer: {
Text(SettingsCopy.themeFooter)
}
}
private func themeRow(_ theme: AppTheme) -> some View {
Button {
themeStore.select(theme)
DS.Haptics.selection()
} label: {
HStack(spacing: DS.Space.md12) {
Image(systemName: theme.symbolName)
.foregroundStyle(DS.Palette.accent)
.frame(width: DS.Space.xxl24)
Text(theme.label)
.foregroundStyle(DS.Palette.textPrimary)
Spacer(minLength: DS.Space.sm8)
if themeStore.theme == theme {
Image(systemName: "checkmark")
.foregroundStyle(DS.Palette.accent)
.fontWeight(.semibold)
}
}
.frame(minHeight: DS.Layout.minHitTarget)
}
.accessibilityIdentifier("settings.theme.\(theme.rawValue)")
// VoiceOver
.accessibilityAddTraits(themeStore.theme == theme ? [.isSelected] : [])
}
// MARK: -
///
/// // `TerminalPalette`
private var terminalPreviewSection: some View {
Section {
let colors = TerminalPalette.colors(for: effectiveScheme)
HStack(spacing: DS.Space.xs2) {
Text(verbatim: SettingsCopy.terminalSample)
.font(DS.Typography.mono(.footnote))
.foregroundStyle(Color(uiColor: colors.foreground))
Rectangle()
.fill(Color(uiColor: colors.caret))
.frame(width: DS.Space.sm8, height: DS.Space.lg16)
Spacer(minLength: 0)
}
.padding(DS.Space.md12)
.background(
Color(uiColor: colors.background),
in: RoundedRectangle(cornerRadius: DS.Radius.sm8)
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(SettingsCopy.terminalPreviewA11y)
} header: {
Text(SettingsCopy.terminalHeader)
}
}
}
///
enum SettingsCopy {
static let title = "设置"
static let themeHeader = "外观"
static let themeFooter = "默认深色 —— 与网页端一致。选「跟随系统」时随 iOS 的浅色/深色切换。"
static let terminalHeader = "终端预览"
static let terminalSample = "$ claude --resume"
static let terminalPreviewA11y = "终端配色预览"
}
// MARK: - Previews
#Preview("SettingsScreen") {
NavigationStack {
SettingsScreen(themeStore: ThemeStore(defaults: PreviewThemeDefaults()))
}
}
/// defaults `UserDefaults`
private final class PreviewThemeDefaults: ThemeDefaults {
private var values: [String: String] = [:]
func themeRaw(forKey key: String) -> String? { values[key] }
func setThemeRaw(_ value: String, forKey key: String) {
values = values.merging([key: value]) { _, new in new }
}
}