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:
@@ -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 = "设备证书自动续期失败,将在下次前台重试"
|
||||
|
||||
Reference in New Issue
Block a user