diff --git a/CodegiOS/DesignSystem/GlassComponents.swift b/CodegiOS/DesignSystem/GlassComponents.swift index f330ec1..1f7d764 100644 --- a/CodegiOS/DesignSystem/GlassComponents.swift +++ b/CodegiOS/DesignSystem/GlassComponents.swift @@ -1,5 +1,68 @@ import SwiftUI +/// Keeps Liquid Glass call sites buildable on the app's iOS 18 deployment +/// target. iOS 26 uses the native effect; earlier systems receive a flat, +/// tinted surface that preserves contrast and shape. +extension View { + @ViewBuilder + func codegGlassEffect(tint: Color? = nil, in shape: S) -> some View { + if #available(iOS 26.0, *) { + if let tint { + self.glassEffect(.regular.tint(tint), in: shape) + } else { + self.glassEffect(.regular, in: shape) + } + } else { + self.background { + ZStack { + shape.fill(Theme.bgElevated) + + if let tint { + shape.fill(tint) + } + } + .allowsHitTesting(false) + } + } + } + + /// Uses native glass button styles on iOS 26 and the closest system button + /// styles on iOS 18–25. + @ViewBuilder + func codegGlassButtonStyle(prominent: Bool = false) -> some View { + if #available(iOS 26.0, *) { + if prominent { + self.buttonStyle(.glassProminent) + } else { + self.buttonStyle(.glass) + } + } else if prominent { + self.buttonStyle(.borderedProminent) + } else { + self.buttonStyle(.bordered) + } + } +} + +/// On iOS 26 this groups neighboring glass shapes so they can blend together. +/// Earlier systems do not have an equivalent container, so content is rendered +/// unchanged. +struct CodegGlassEffectContainer: View { + var spacing: CGFloat? + @ViewBuilder var content: () -> Content + + @ViewBuilder + var body: some View { + if #available(iOS 26.0, *) { + GlassEffectContainer(spacing: spacing) { + content() + } + } else { + content() + } + } +} + /// A Liquid Glass surface for cards and rows, with a faint hairline for /// definition on the dark backdrop. struct GlassCard: View { @@ -10,7 +73,7 @@ struct GlassCard: View { var body: some View { content() .padding(padding) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) .hairlineBorder(cornerRadius) } } @@ -93,8 +156,8 @@ struct GlassRow: View { content() .padding(.horizontal, 14) .padding(.vertical, 12) - .glassEffect( - isSelected ? .regular.tint(Theme.accent.opacity(0.22)) : .regular, + .codegGlassEffect( + tint: isSelected ? Theme.accent.opacity(0.22) : nil, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) ) .hairlineBorder(cornerRadius, color: isSelected ? Theme.accent.opacity(0.45) : Theme.surfaceStroke) @@ -247,7 +310,7 @@ struct PrimaryGlassButton: View { .frame(maxWidth: .infinity) .padding(.vertical, 4) } - .buttonStyle(.glassProminent) + .codegGlassButtonStyle(prominent: true) .tint(tint) .disabled(isLoading) } diff --git a/CodegiOS/DesignSystem/StateViews.swift b/CodegiOS/DesignSystem/StateViews.swift index fc2c352..20f2dee 100644 --- a/CodegiOS/DesignSystem/StateViews.swift +++ b/CodegiOS/DesignSystem/StateViews.swift @@ -26,7 +26,7 @@ struct EmptyStateView: View { } if let actionTitle, let action { Button(actionTitle, action: action) - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .padding(.top, 4) } @@ -83,8 +83,8 @@ struct RefreshErrorBanner: View { } .padding(.horizontal, 14) .padding(.vertical, 11) - .glassEffect( - .regular.tint(Theme.danger.opacity(0.16)), + .codegGlassEffect( + tint: Theme.danger.opacity(0.16), in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous) ) .hairlineBorder(Theme.Radius.md, color: Theme.danger.opacity(0.35)) @@ -110,7 +110,7 @@ struct InlineErrorView: View { .multilineTextAlignment(.center) if let retry { Button("Try Again", action: retry) - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .padding(.top, 4) } diff --git a/CodegiOS/Features/Onboarding/OnboardingView.swift b/CodegiOS/Features/Onboarding/OnboardingView.swift index 5bd839d..1f70d68 100644 --- a/CodegiOS/Features/Onboarding/OnboardingView.swift +++ b/CodegiOS/Features/Onboarding/OnboardingView.swift @@ -20,7 +20,7 @@ struct OnboardingView: View { .font(.system(size: 34, weight: .semibold)) .foregroundStyle(Theme.accent) .frame(width: 84, height: 84) - .glassEffect(.regular.tint(Theme.accentDim), in: RoundedRectangle(cornerRadius: Theme.Radius.xl, style: .continuous)) + .codegGlassEffect(tint: Theme.accentDim, in: RoundedRectangle(cornerRadius: Theme.Radius.xl, style: .continuous)) .hairlineBorder(Theme.Radius.xl, color: Theme.accent.opacity(0.3)) .padding(.bottom, 24) diff --git a/CodegiOS/Features/Projects/CloneRepoView.swift b/CodegiOS/Features/Projects/CloneRepoView.swift index f599b73..4a77056 100644 --- a/CodegiOS/Features/Projects/CloneRepoView.swift +++ b/CodegiOS/Features/Projects/CloneRepoView.swift @@ -101,7 +101,7 @@ struct CloneRepoView: View { } label: { Image(systemName: "folder") } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .disabled(cloning) .accessibilityLabel("Browse") diff --git a/CodegiOS/Features/Projects/DirectoryBrowserView.swift b/CodegiOS/Features/Projects/DirectoryBrowserView.swift index 916ef7b..310c032 100644 --- a/CodegiOS/Features/Projects/DirectoryBrowserView.swift +++ b/CodegiOS/Features/Projects/DirectoryBrowserView.swift @@ -60,7 +60,7 @@ struct DirectoryBrowserView: View { Image(systemName: "chevron.up") .font(.body.weight(.semibold)) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .disabled(isRoot || isLoading) .accessibilityLabel("Up") diff --git a/CodegiOS/Features/Servers/ServerEditorSheet.swift b/CodegiOS/Features/Servers/ServerEditorSheet.swift index 5799fc5..2236b40 100644 --- a/CodegiOS/Features/Servers/ServerEditorSheet.swift +++ b/CodegiOS/Features/Servers/ServerEditorSheet.swift @@ -170,7 +170,7 @@ struct ServerEditorSheet: View { .frame(maxWidth: .infinity) .padding(.vertical, 4) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .disabled(model.isTesting || !model.canTest) diff --git a/CodegiOS/Features/SessionDetail/AgentOptionsButton.swift b/CodegiOS/Features/SessionDetail/AgentOptionsButton.swift index ce8c665..02beed9 100644 --- a/CodegiOS/Features/SessionDetail/AgentOptionsButton.swift +++ b/CodegiOS/Features/SessionDetail/AgentOptionsButton.swift @@ -376,7 +376,7 @@ private struct AgentOptionsSheet: View { } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) .hairlineBorder(Theme.Radius.md) } @@ -387,7 +387,7 @@ private struct AgentOptionsSheet: View { .foregroundStyle(Theme.danger) .fixedSize(horizontal: false, vertical: true) Button("Try Again") { options.load() } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) } .padding(14) @@ -511,7 +511,7 @@ struct OptionSection: View { .foregroundStyle(Theme.textTertiary) .padding(.horizontal, 4) VStack(spacing: 0) { content() } - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) .hairlineBorder(Theme.Radius.md) } } diff --git a/CodegiOS/Features/SessionDetail/BranchPickerView.swift b/CodegiOS/Features/SessionDetail/BranchPickerView.swift index 2c62c49..14f8f47 100644 --- a/CodegiOS/Features/SessionDetail/BranchPickerView.swift +++ b/CodegiOS/Features/SessionDetail/BranchPickerView.swift @@ -158,7 +158,7 @@ struct BranchPickerView: View { } } .padding(14) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) .hairlineBorder(Theme.Radius.md) } else { Button { showNewBranch = true } label: { @@ -175,7 +175,7 @@ struct BranchPickerView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) .hairlineBorder(Theme.Radius.md) } } diff --git a/CodegiOS/Features/SessionDetail/ComposeBar.swift b/CodegiOS/Features/SessionDetail/ComposeBar.swift index 1f1faa0..1555b4c 100644 --- a/CodegiOS/Features/SessionDetail/ComposeBar.swift +++ b/CodegiOS/Features/SessionDetail/ComposeBar.swift @@ -56,7 +56,7 @@ struct ComposeBar: View { .transition(.opacity.combined(with: .move(edge: .bottom))) } - GlassEffectContainer(spacing: 8) { + CodegGlassEffectContainer(spacing: 8) { HStack(alignment: .bottom, spacing: 8) { addButton TextField("Message", text: $text, axis: .vertical) @@ -74,7 +74,7 @@ struct ComposeBar: View { // `xl` radius clamps to a capsule while the field is one // line (rhyming with the round +/send buttons) and relaxes // to a rounded rect as it grows — no hard switch needed. - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.xl, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.xl, style: .continuous)) .hairlineBorder(Theme.Radius.xl) actionButton @@ -161,7 +161,7 @@ struct ComposeBar: View { .font(.system(size: 16, weight: .semibold)) .frame(width: 26, height: 26) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .clipShape(Circle()) .tint(Theme.textSecondary) .accessibilityLabel("Add or insert") @@ -175,7 +175,7 @@ struct ComposeBar: View { .font(.system(size: 16, weight: .bold)) .frame(width: 26, height: 26) } - .buttonStyle(.glassProminent) + .codegGlassButtonStyle(prominent: true) .tint(Theme.danger) .clipShape(Circle()) .transition(.scale.combined(with: .opacity)) @@ -186,7 +186,7 @@ struct ComposeBar: View { .font(.system(size: 16, weight: .bold)) .frame(width: 26, height: 26) } - .buttonStyle(.glassProminent) + .codegGlassButtonStyle(prominent: true) .tint(Theme.accent) .clipShape(Circle()) .disabled(!canSend) @@ -276,7 +276,7 @@ private struct NoticeBanner: View { } .padding(.horizontal, 12) .padding(.vertical, 9) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) + .codegGlassEffect(in: RoundedRectangle(cornerRadius: Theme.Radius.md, style: .continuous)) .hairlineBorder(Theme.Radius.md, color: Theme.accent.opacity(0.35)) .transition(.opacity.combined(with: .move(edge: .bottom))) } diff --git a/CodegiOS/Features/SessionDetail/ComposeInsertSheet.swift b/CodegiOS/Features/SessionDetail/ComposeInsertSheet.swift index c49b0dd..a0d3d0c 100644 --- a/CodegiOS/Features/SessionDetail/ComposeInsertSheet.swift +++ b/CodegiOS/Features/SessionDetail/ComposeInsertSheet.swift @@ -174,7 +174,7 @@ struct ComposeInsertSheet: View { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) Button("Try Again") { model.load(source) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) } .padding(.horizontal, 32) diff --git a/CodegiOS/Features/SessionDetail/SessionDetailView.swift b/CodegiOS/Features/SessionDetail/SessionDetailView.swift index c0b3d29..7aa83c7 100644 --- a/CodegiOS/Features/SessionDetail/SessionDetailView.swift +++ b/CodegiOS/Features/SessionDetail/SessionDetailView.swift @@ -295,7 +295,7 @@ private struct JumpToLatestButton: View { .font(.system(size: 15, weight: .bold)) .foregroundStyle(Theme.accent) .frame(width: 40, height: 40) - .glassEffect(.regular, in: Circle()) + .codegGlassEffect(in: Circle()) .hairlineBorder(20) // An explicit hit shape so the whole disc is tappable (and the // tap can't slip past its edge into the transcript underneath). diff --git a/CodegiOS/Features/Settings/Agents/AgentConfigKimiView.swift b/CodegiOS/Features/Settings/Agents/AgentConfigKimiView.swift index e7890c3..2b64888 100644 --- a/CodegiOS/Features/Settings/Agents/AgentConfigKimiView.swift +++ b/CodegiOS/Features/Settings/Agents/AgentConfigKimiView.swift @@ -146,7 +146,7 @@ struct KimiConfigSection: View { else { Label("Fetch", systemImage: "arrow.clockwise").labelStyle(.titleAndIcon) } } .font(.subheadline.weight(.medium)) - .buttonStyle(.glass).tint(Theme.accent) + .codegGlassButtonStyle().tint(Theme.accent) .disabled(saving || fetchingModels) } } @@ -168,7 +168,7 @@ struct KimiConfigSection: View { Image(systemName: "list.bullet") .font(.body.weight(.medium)).frame(width: 34, height: 30) } - .buttonStyle(.glass).tint(Theme.textSecondary) + .codegGlassButtonStyle().tint(Theme.textSecondary) .accessibilityLabel("Choose a fetched model") } @@ -246,9 +246,9 @@ struct KimiConfigSection: View { HStack { Spacer(minLength: 0) if prominent { - saveButton(title, action).buttonStyle(.glassProminent).tint(Theme.accent).disabled(saving) + saveButton(title, action).codegGlassButtonStyle(prominent: true).tint(Theme.accent).disabled(saving) } else { - saveButton(title, action).buttonStyle(.glass).tint(Theme.accent).disabled(saving) + saveButton(title, action).codegGlassButtonStyle().tint(Theme.accent).disabled(saving) } } .padding(.horizontal, 16).padding(.vertical, 12) diff --git a/CodegiOS/Features/Settings/Agents/AgentConfigPiView.swift b/CodegiOS/Features/Settings/Agents/AgentConfigPiView.swift index fc26940..6386baa 100644 --- a/CodegiOS/Features/Settings/Agents/AgentConfigPiView.swift +++ b/CodegiOS/Features/Settings/Agents/AgentConfigPiView.swift @@ -159,7 +159,7 @@ struct PiConfigSection: View { Button { Task { await detectPiBinary() } } label: { Image(systemName: "arrow.clockwise").font(.body.weight(.medium)) } - .buttonStyle(.glass).tint(Theme.textSecondary) + .codegGlassButtonStyle().tint(Theme.textSecondary) .disabled(checkingPi || piOp != nil) .accessibilityLabel("Recheck") if !checkingPi { @@ -184,7 +184,7 @@ struct PiConfigSection: View { if validating { ProgressView().controlSize(.small) } else { Label("Validate", systemImage: "terminal").labelStyle(.titleAndIcon) } } - .font(.subheadline.weight(.medium)).buttonStyle(.glass).tint(Theme.accent) + .font(.subheadline.weight(.medium)).codegGlassButtonStyle().tint(Theme.accent) .disabled(validating || command.trimmingCharacters(in: .whitespaces).isEmpty) } } @@ -313,9 +313,9 @@ struct PiConfigSection: View { .padding(.horizontal, 14).padding(.vertical, 5) } if prominent { - label.buttonStyle(.glassProminent).tint(Theme.accent).disabled(disabled) + label.codegGlassButtonStyle(prominent: true).tint(Theme.accent).disabled(disabled) } else { - label.buttonStyle(.glass).tint(Theme.accent).disabled(disabled) + label.codegGlassButtonStyle().tint(Theme.accent).disabled(disabled) } } diff --git a/CodegiOS/Features/Settings/Agents/AgentDetailView.swift b/CodegiOS/Features/Settings/Agents/AgentDetailView.swift index 82718fd..d9b406b 100644 --- a/CodegiOS/Features/Settings/Agents/AgentDetailView.swift +++ b/CodegiOS/Features/Settings/Agents/AgentDetailView.swift @@ -308,7 +308,7 @@ struct AgentDetailView: View { Text(primary.label).fontWeight(.semibold) .padding(.horizontal, 18).padding(.vertical, 5) } - .buttonStyle(.glassProminent).tint(Theme.accent) + .codegGlassButtonStyle(prominent: true).tint(Theme.accent) .disabled(isInstalling || primary.disabled) Spacer(minLength: 0) if !secondary.isEmpty { versionMenu(secondary, iconOnly: true) } @@ -334,7 +334,7 @@ struct AgentDetailView: View { .font(.subheadline.weight(.medium)).padding(.horizontal, 16).padding(.vertical, 7) } } - .buttonStyle(.glass).tint(iconOnly ? Theme.textSecondary : Theme.accent) + .codegGlassButtonStyle().tint(iconOnly ? Theme.textSecondary : Theme.accent) .accessibilityLabel(iconOnly ? "More version actions" : "Manage version") .disabled(isInstalling) } @@ -499,7 +499,7 @@ struct AgentDetailView: View { } label: { Label("Clear Binary Cache", systemImage: "trash").frame(maxWidth: .infinity).padding(.vertical, 4) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.danger) .disabled(isInstalling) } @@ -643,7 +643,7 @@ private struct FlowFixButtons: View { Button { onTap(fix) } label: { Text(fix.label).font(.caption.weight(.medium)).padding(.horizontal, 10).padding(.vertical, 5) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) .disabled(disabled) } diff --git a/CodegiOS/Features/Settings/Agents/AgentsSettingsView.swift b/CodegiOS/Features/Settings/Agents/AgentsSettingsView.swift index a770a46..e2b87bb 100644 --- a/CodegiOS/Features/Settings/Agents/AgentsSettingsView.swift +++ b/CodegiOS/Features/Settings/Agents/AgentsSettingsView.swift @@ -88,7 +88,7 @@ struct AgentsSettingsView: View { .multilineTextAlignment(.center) .padding(.horizontal, 16) .padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24) .padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) diff --git a/CodegiOS/Features/Settings/ChatChannels/ChatChannelDetailView.swift b/CodegiOS/Features/Settings/ChatChannels/ChatChannelDetailView.swift index 800026b..9dc1c15 100644 --- a/CodegiOS/Features/Settings/ChatChannels/ChatChannelDetailView.swift +++ b/CodegiOS/Features/Settings/ChatChannels/ChatChannelDetailView.swift @@ -148,7 +148,7 @@ struct ChatChannelDetailView: View { .frame(maxWidth: .infinity) .padding(.vertical, 4) } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(role == .destructive ? Theme.danger : Theme.accent) .disabled(model.busy) } @@ -215,7 +215,7 @@ struct ChatChannelDetailView: View { .multilineTextAlignment(.center) .padding(.horizontal, 16) .padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24) .padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) diff --git a/CodegiOS/Features/Settings/ChatChannels/ChatChannelsSettingsView.swift b/CodegiOS/Features/Settings/ChatChannels/ChatChannelsSettingsView.swift index e1bef11..8ba8747 100644 --- a/CodegiOS/Features/Settings/ChatChannels/ChatChannelsSettingsView.swift +++ b/CodegiOS/Features/Settings/ChatChannels/ChatChannelsSettingsView.swift @@ -179,7 +179,7 @@ struct ChatChannelsSettingsView: View { .multilineTextAlignment(.center) .padding(.horizontal, 16) .padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24) .padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) diff --git a/CodegiOS/Features/Settings/ChatChannels/WeixinQRView.swift b/CodegiOS/Features/Settings/ChatChannels/WeixinQRView.swift index 4bba29c..16d20b7 100644 --- a/CodegiOS/Features/Settings/ChatChannels/WeixinQRView.swift +++ b/CodegiOS/Features/Settings/ChatChannels/WeixinQRView.swift @@ -87,7 +87,7 @@ struct WeixinQRView: View { Button { attempt += 1 } label: { Label("Refresh QR Code", systemImage: "arrow.clockwise") } - .buttonStyle(.glass) + .codegGlassButtonStyle() .tint(Theme.accent) } diff --git a/CodegiOS/Features/Settings/ModelProviders/ModelProvidersSettingsView.swift b/CodegiOS/Features/Settings/ModelProviders/ModelProvidersSettingsView.swift index b0bc362..7f3ec37 100644 --- a/CodegiOS/Features/Settings/ModelProviders/ModelProvidersSettingsView.swift +++ b/CodegiOS/Features/Settings/ModelProviders/ModelProvidersSettingsView.swift @@ -125,7 +125,7 @@ struct ModelProvidersSettingsView: View { .multilineTextAlignment(.center) .padding(.horizontal, 16) .padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24) .padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) diff --git a/CodegiOS/Features/Settings/System/SystemSettingsView.swift b/CodegiOS/Features/Settings/System/SystemSettingsView.swift index 42cff51..065576f 100644 --- a/CodegiOS/Features/Settings/System/SystemSettingsView.swift +++ b/CodegiOS/Features/Settings/System/SystemSettingsView.swift @@ -143,7 +143,7 @@ struct SystemSettingsView: View { Text("Test") } } - .buttonStyle(.glass).tint(Theme.accent) + .codegGlassButtonStyle().tint(Theme.accent) .disabled(model.probing || model.customShellPath.trimmingCharacters(in: .whitespaces).isEmpty) if let probe = model.probeResult { Label(probe ? "Executable found" : "Not found", systemImage: probe ? "checkmark.circle" : "xmark.circle") @@ -223,7 +223,7 @@ struct SystemSettingsView: View { .foregroundStyle(Theme.textPrimary) .multilineTextAlignment(.center) .padding(.horizontal, 16).padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24).padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) .task(id: toast) { diff --git a/CodegiOS/Features/Settings/VersionControl/VersionControlSettingsView.swift b/CodegiOS/Features/Settings/VersionControl/VersionControlSettingsView.swift index a4baedc..8c32dd4 100644 --- a/CodegiOS/Features/Settings/VersionControl/VersionControlSettingsView.swift +++ b/CodegiOS/Features/Settings/VersionControl/VersionControlSettingsView.swift @@ -131,10 +131,10 @@ struct VersionControlSettingsView: View { Text("Test") } } - .buttonStyle(.glass).tint(Theme.accent) + .codegGlassButtonStyle().tint(Theme.accent) .disabled(model.testing || model.customPath.trimmingCharacters(in: .whitespaces).isEmpty) Button("Save Path") { Task { await model.saveCustomPath() } } - .buttonStyle(.glassProminent).tint(Theme.accent) + .codegGlassButtonStyle(prominent: true).tint(Theme.accent) Spacer(minLength: 0) } .padding(.horizontal, 16).padding(.vertical, 11) @@ -182,7 +182,7 @@ struct VersionControlSettingsView: View { .foregroundStyle(Theme.textPrimary) .multilineTextAlignment(.center) .padding(.horizontal, 16).padding(.vertical, 11) - .glassEffect(.regular.tint(Theme.accent.opacity(0.18)), in: Capsule()) + .codegGlassEffect(tint: Theme.accent.opacity(0.18), in: Capsule()) .padding(.horizontal, 24).padding(.bottom, 18) .transition(.move(edge: .bottom).combined(with: .opacity)) .task(id: toast) { diff --git a/README.md b/README.md index 6b7a7cf..c139113 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ A native, universal (iPhone + iPad) SwiftUI client for the [codeg](https://github.com/xintaofei/codeg) multi-agent coding server. Manage your codeg servers, browse their sessions, read the full transcript, and reply to the agent with the response streaming back in -real time. Built for iOS 26 with Apple's **Liquid Glass** design language and a -dark, developer-focused aesthetic. +real time. Supports iOS 18 and later, adopting Apple's **Liquid Glass** design +language on iOS 26 with system-style fallbacks on earlier releases. The app is a **pure API client** — all agent and conversation logic lives on the codeg backend; the app only calls its HTTP + WebSocket API. @@ -23,6 +23,7 @@ codeg backend; the app only calls its HTTP + WebSocket API. ## Requirements - Xcode 26 / iOS 26 SDK +- iOS 18 or later - [XcodeGen](https://github.com/yonyz/XcodeGen) (`brew install xcodegen`) — the Xcode project is generated from `project.yml` diff --git a/docs/live-activity-analysis.md b/docs/live-activity-analysis.md index ec4bf57..c4eba77 100644 --- a/docs/live-activity-analysis.md +++ b/docs/live-activity-analysis.md @@ -16,7 +16,7 @@ 2. **最高价值的「等待审批」态,在 app 级数据层根本看不见**。它不是 `ConversationStatus` 的一个 case;只活在**已打开会话**的 `SessionDetailViewModel` 里。app 级的轮询/列表层区分不了"在跑"和"卡着等你批"。要暴露它,得先有 M4 `SessionHub`(app 级持久连接)或服务端新增字段。 3. **「可操作」审批同样受 server 可达性约束**:即便推送把"等审批"送到锁屏,**按下 Approve 真正生效仍需手机能连回 server**。局域网 server + 人在外面 = 看得到、点不动。 -**好消息**:地基比想象的近 —— 传输层 `EventStream` 已经会说 attach/snapshot/replay 协议,`LiveSessionSnapshot` 天然就是一份 ContentState,token 在 Keychain 且后台可读,部署目标 iOS 26 让 ActivityKit 全量可用,`codeg://` 深链已就位。 +**好消息**:地基比想象的近 —— 传输层 `EventStream` 已经会说 attach/snapshot/replay 协议,`LiveSessionSnapshot` 天然就是一份 ContentState,token 在 Keychain 且后台可读,部署目标 iOS 18 已覆盖所规划的 ActivityKit 能力,`codeg://` 深链已就位。 **建议**:Live Activity 不该现在单独上,它是 M4(持久连接)/ M5(推送)的**下游**。分三阶段(§8):阶段 0 零服务端改动、只做前台可见的骨架用来验证 UI;真正的锁屏审批价值必须等推送基础设施。 @@ -147,7 +147,7 @@ Live Activity 要在后台/锁屏发光,需要一个"app 不在前台也能拿 1. **传输层已会说持久协议** —— `EventStream` 就是 `/ws/events` firehose,已实现 attach/snapshot/replay:`WSClientMessage.attach(subscriptionId, connectionId, sinceSeq)`(`EventStream.swift:8-35`)、`WSServerMessage.snapshot/replay/event/detached`(`:40-69`)、`Frame` 枚举(`:78-86`)。M4 `SessionHub` 是"把它包成一条 app 级持久连接 + 加 `scenePhase` 生命周期",**不是从零造协议**。 2. **ContentState 源现成** —— `LiveSessionSnapshot`(§2.2)+ `AcpEvent` 流已归约出 status / 活动工具 / 待审批标志 / token 用量。 3. **token 后台可读** —— `Keychain.swift:13,39`:service `com.codeg.ios.server-token`,`kSecAttrAccessibleAfterFirstUnlock`(首次解锁后后台可读,正是扩展所需)。**但**:keychain sharing(access group)当前**未配置**(无 entitlements)——扩展要读 token,需加 App Group + keychain 共享组。 -4. **平台就绪** —— 部署目标 iOS 26(`project.yml:5,37`),ActivityKit 全量可用。 +4. **平台就绪** —— 部署目标 iOS 18(`project.yml:5,40`),所规划的 ActivityKit 能力均可用。 5. **深链已就位** —— `codeg://`(`CFBundleURLTypes`)+ `Route` 枚举,Live Activity 点击回 app 落到 `Route.conversation(id)` 是现成的。 ### 需要新增的工程要件(落地清单) diff --git a/docs/live-activity-implementation.md b/docs/live-activity-implementation.md index 355d33a..8100537 100644 --- a/docs/live-activity-implementation.md +++ b/docs/live-activity-implementation.md @@ -81,7 +81,7 @@ targets: CodegActivityKit: type: framework platform: iOS - deploymentTarget: "26.0" + deploymentTarget: "18.0" sources: [CodegActivityKit] settings: base: @@ -104,7 +104,7 @@ targets: CodegWidgets: type: app-extension platform: iOS - deploymentTarget: "26.0" + deploymentTarget: "18.0" sources: [CodegWidgets] dependencies: - target: CodegActivityKit diff --git a/project.yml b/project.yml index 7a63c42..1328299 100644 --- a/project.yml +++ b/project.yml @@ -2,7 +2,7 @@ name: CodegiOS options: bundleIdPrefix: com.codeg deploymentTarget: - iOS: "26.0" + iOS: "18.0" createIntermediateGroups: true groupSortPosition: top developmentLanguage: en @@ -37,7 +37,7 @@ targets: CodegiOS: type: application platform: iOS - deploymentTarget: "26.0" + deploymentTarget: "18.0" sources: - path: CodegiOS dependencies: