Fix maximum-clear bubble rendering

This commit is contained in:
gd-ops committed 2026-07-16 11:23:23 +08:00
1 parent 6ed4647143
commit 92e901f844
6 files changed
+77 -60

No files matched your search

@@ -305,7 +305,7 @@ private struct ConversationBubble: View {
cornerRadius: OverlayGeometry.bubbleCornerRadius,
style: .continuous
),
interactive: false
interactive: true
)
.contentShape(RoundedRectangle(cornerRadius: OverlayGeometry.bubbleCornerRadius, style: .continuous))
}
@@ -335,7 +335,7 @@ private struct SessionBubbleRow: View {
if !session.statusText.isEmpty {
Text(session.statusText)
.font(.system(size: 9.5, weight: .semibold))
.foregroundStyle(statusColor)
.foregroundStyle(BubbleForegroundStyle.text)
.apcBubbleTextContrast()
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
@@ -343,11 +343,11 @@ private struct SessionBubbleRow: View {
.padding(.vertical, 1)
.background(
Capsule()
.fill(statusColor.opacity(0.10))
.fill(statusColor.opacity(0.24))
)
.overlay {
Capsule()
.stroke(statusColor.opacity(0.28), lineWidth: 0.75)
.stroke(statusColor.opacity(0.62), lineWidth: 0.75)
.allowsHitTesting(false)
}
}
@@ -128,21 +128,16 @@ private struct APCLiquidGlassModifier<S: Shape>: ViewModifier {
enum APCBubbleGlassStyle {
/// Clear Liquid Glass is the most transparent public system treatment on
/// macOS 26. Keep the normal path free of supplemental fills and borders
/// so the system can sample the desktop directly.
/// macOS 26. Keep the normal path free of supplemental fills, borders, and
/// post-compositing opacity so the system can sample the desktop directly.
static let backdropOpacity = 0.0
static let borderOpacity = 0.0
/// Keep a visible trace of the native Clear lens while allowing the
/// desktop beneath a large conversation bubble to remain recognizable.
/// The glass is rendered on its own background layer, so this opacity
/// never attenuates labels, icons, or controls.
static let opticalOpacity = 0.24
static let legacyBackdropOpacity = 0.10
static let legacyBackdropOpacity = 0.18
static let legacyBorderOpacity = 0.18
static let increasedContrastBackdropOpacity = 0.30
static let increasedContrastBorderOpacity = 0.34
static let increasedContrastBackdropOpacity = 0.42
static let increasedContrastBorderOpacity = 0.52
static let reducedTransparencyBackdropOpacity = 0.86
static let reducedTransparencyBorderOpacity = 0.42
static let reducedTransparencyBorderOpacity = 0.62
static func resolvedBackdropOpacity(
reduceTransparency: Bool,
@@ -177,27 +172,23 @@ enum APCBubbleForegroundStyle {
/// Transparency belongs to the glass surface, not to labels or icons.
static let contentOpacity = 1.0
static let secondaryContentOpacity = 0.86
static let lightHaloOpacity = 0.28
static let darkHaloOpacity = 0.96
static let lightHaloOpacity = 0.16
static let darkHaloOpacity = 0.82
}
/// The native Clear glass is intentionally kept on a background-only layer.
/// `BubbleOverlayRootView` doesn't wrap these surfaces in a
/// `GlassEffectContainer`: that container elevates descendant glass views and
/// can place the optical layer above SwiftUI foreground content in a
/// The native Clear glass is applied directly to the content hierarchy. This
/// is the public maximum-clear treatment and keeps SwiftUI foreground content
/// in the glass view's guaranteed content layer. `BubbleOverlayRootView` does
/// not add a `GlassEffectContainer`: that container elevates descendant glass
/// views and can place the optical layer above foreground content in a
/// transparent `NSPanel`.
private struct APCTransparentBubbleGlassModifier<S: Shape>: ViewModifier {
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.colorScheme) private var colorScheme
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
let shape: S
let interactive: Bool
private var backdropColor: Color {
colorScheme == .dark ? .black : .white
}
private var backdropOpacity: Double {
APCBubbleGlassStyle.resolvedBackdropOpacity(
reduceTransparency: reduceTransparency,
@@ -209,23 +200,18 @@ private struct APCTransparentBubbleGlassModifier<S: Shape>: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
if reduceTransparency {
accessibleFallback(content, supportsLiquidGlass: true)
accessibilityFallback(content, supportsLiquidGlass: true)
} else {
content
.background {
Color.clear
.glassEffect(
interactive ? .clear.interactive() : .clear,
in: shape
)
.opacity(APCBubbleGlassStyle.opticalOpacity)
.allowsHitTesting(false)
}
.background {
if colorSchemeContrast == .increased {
shape.fill(backdropColor.opacity(backdropOpacity))
shape.fill(Color.black.opacity(backdropOpacity))
}
}
.glassEffect(
interactive ? .clear.interactive() : .clear,
in: shape
)
.overlay {
if colorSchemeContrast == .increased {
bubbleBorder(supportsLiquidGlass: true)
@@ -233,24 +219,37 @@ private struct APCTransparentBubbleGlassModifier<S: Shape>: ViewModifier {
}
}
} else {
accessibleFallback(content, supportsLiquidGlass: false)
legacyFallback(content)
}
}
private func accessibleFallback(
private func accessibilityFallback(
_ content: Content,
supportsLiquidGlass: Bool
) -> some View {
let opacity = if reduceTransparency || colorSchemeContrast == .increased {
return content
.background(Color.black.opacity(backdropOpacity), in: shape)
.overlay {
bubbleBorder(supportsLiquidGlass: supportsLiquidGlass)
}
}
private func legacyFallback(_ content: Content) -> some View {
let supplementalOpacity = if reduceTransparency || colorSchemeContrast == .increased {
backdropOpacity
} else {
APCBubbleGlassStyle.legacyBackdropOpacity
}
return content
.background(backdropColor.opacity(opacity), in: shape)
.background {
ZStack {
shape.fill(.ultraThinMaterial)
shape.fill(Color.black.opacity(supplementalOpacity))
}
}
.overlay {
bubbleBorder(supportsLiquidGlass: supportsLiquidGlass)
bubbleBorder(supportsLiquidGlass: false)
}
}
@@ -279,13 +278,13 @@ private struct APCBubbleTextContrastModifier: ViewModifier {
.opacity(APCBubbleForegroundStyle.contentOpacity)
.shadow(
color: .white.opacity(APCBubbleForegroundStyle.lightHaloOpacity),
radius: 0.45,
y: 0.1
radius: 0.3,
y: 0.05
)
.shadow(
color: .black.opacity(APCBubbleForegroundStyle.darkHaloOpacity),
radius: 1.35,
y: 0.4
radius: 0.85,
y: 0.25
)
}
}
@@ -7,8 +7,7 @@ struct BubbleGlassRegressionTests {
func maximumClearSurfaceAddsNoSolidBackdrop() {
#expect(APCBubbleGlassStyle.backdropOpacity == 0)
#expect(APCBubbleGlassStyle.borderOpacity == 0)
#expect(APCBubbleGlassStyle.opticalOpacity > 0)
#expect(APCBubbleGlassStyle.opticalOpacity <= 0.25)
#expect(APCBubbleGlassStyle.legacyBackdropOpacity > 0)
}
@Test
@@ -17,5 +16,22 @@ struct BubbleGlassRegressionTests {
#expect(APCBubbleForegroundStyle.secondaryContentOpacity >= 0.85)
#expect(APCBubbleForegroundStyle.lightHaloOpacity > 0)
#expect(APCBubbleForegroundStyle.darkHaloOpacity > 0)
#expect(APCBubbleForegroundStyle.darkHaloOpacity > APCBubbleForegroundStyle.lightHaloOpacity)
}
@Test
func accessibilityFallbacksRemainDarkerThanLegacyMaterial() {
#expect(
APCBubbleGlassStyle.increasedContrastBackdropOpacity
> APCBubbleGlassStyle.legacyBackdropOpacity
)
#expect(
APCBubbleGlassStyle.reducedTransparencyBackdropOpacity
> APCBubbleGlassStyle.increasedContrastBackdropOpacity
)
#expect(
APCBubbleGlassStyle.reducedTransparencyBorderOpacity
> APCBubbleGlassStyle.legacyBorderOpacity
)
}
}
+5 -4
View File
@@ -36,7 +36,7 @@ App、PetCore、CLI 和连接器不再只比较 RPC 版本。每个 bundle 携
| 功能域 | 当前状态 |
|---|---|
| 三入口导航、Studio 双页签、本地宠物库 | 已实现 |
| 原生 UI | 控制中心、状态栏、桌宠控制、右击菜单和消息气泡使用 macOS 原生 clear Liquid Glass;macOS 14–15 使用系统 `ultraThinMaterial` 回退。 |
| 原生 UI | 控制中心、状态栏、桌宠控制、右击菜单和消息气泡使用 macOS 原生 clear Liquid Glass。消息气泡在 macOS 26 使用无 tint、无填充、无边框、无附加 opacity 的最高透明 clear interactive glass,并把前景直接置于玻璃内容层;macOS 14–15 使用系统 `ultraThinMaterial` 回退。 |
| 桌宠渲染 | Metal-backed;12/20 FPS;高/超清 eager cache、原画 ring cache;左键拖动与宠物右侧缩放;缩放手柄与气泡开关同列且命中区独立;左键单击无额外响应、仅右击显示原生菜单;不显示拖动标签或脚下玻璃底座。 |
| 会话气泡 | 每个 Agent 一个气泡;同 Agent 多会话分行;标题、最多两行的当前公开活动/回复、状态和唯一「打开」操作;两行渲染与 Panel 高度测量一致;默认 15 分钟普通会话收起。 |
| 状态内容 | Running 优先显示公开思考摘要/计划或本地化工具活动;已完成工具不再冒充当前活动,公开 revision 停滞但任务时钟推进时清除旧摘要;Ready 显示最新 Agent 回复;Needs input/Blocked 显示当前交互或错误。 |
@@ -92,7 +92,7 @@ V1 仍不包含公共素材库、分享/社区、Petdex 导入、Codex 内置宠
| 验证 | 结果 |
|---|---:|
| `script/test_all.sh` 默认 deterministic/simulated/security/bounded-stress 门禁 | PASS |
| Swift tests | PASS,95 tests / 10 suites(含旧 `apc.runtime-manifest.v1` 新增字段重建回归) |
| Swift tests | PASS,96 tests / 10 suites(含玻璃辅助功能回退与旧 `apc.runtime-manifest.v1` 新增字段重建回归) |
| Rust workspace tests、fmt、clippy `-D warnings` | PASS |
| App lifecycle contract | PASS,8/8 |
| Swift overlay/UI offline contract | PASS,10/10 |
@@ -106,6 +106,7 @@ V1 仍不包含公共素材库、分享/社区、Petdex 导入、Codex 内置宠
| 桌宠紧凑布局回归 | PASS:Computer Use 优先完成新版启动与旧实例退出;气泡按宠物内容带重新锚定,气泡开关下移并与缩放手柄保持同列。两个控件各自由 36/38 pt 透明原生控制 panel 承载,不再受宠物主体鼠标穿透影响;系统命中检查、收起→展开、缩放增量→恢复、Overlay 10/10 离线契约与当前 Swift 套件均通过。 |
| 桌宠点击与菜单语义 | PASS:左键策略单测确认不打开菜单,AppKit 左键事件仅保留拖动生命周期;Computer Use 实机读取到“桌宠”辅助元素及“按住左键拖动,右击打开快捷菜单”说明,系统 AX 只为该元素公开 `AXShowMenu` 菜单动作;悬浮“拖动”标签已移除。 |
| Codex 气泡活动回归 | PASS:Rust 单元/transport 覆盖工具分类、完成项、稳定状态记录和 reasoning revision;实机验证新思考摘要、完成会话无残留活动、收起后再次展开。当前机器 Hook Trust 仍为用户待确认,故精确 PreToolUse 实机项不伪报通过。 |
| 最大透明气泡回归 | PASS:Computer Use 完成旧 UI Host 退出与新 bundle 启动,运行进程晚于新构建且 build ID 为 `0.1.0.1.20260716031517.53137`;静默窗口像素检查确认标题、当前活动和状态均可见,气泡后方浅色界面文字/分隔线仍可辨认。正常态无 tint、填充、边框或玻璃 opacity;Reduce Transparency/Increase Contrast 使用深色可读性回退。 |
| `.petpack` producer schemas | PASS:manifest/source/brief/source-event/validation 五组 Draft 2020-12 schema 正反 fixtures;当前 Studio 元数据逐文件/逐事件 schema 验证。 |
| Portable Skill 真实前向测试 | PASS:真实图像工具生成 `Lunari` 月兔,7 状态 × 2 张透明 192×208 PNG、14 帧均有状态内变化,384×416 cover/14 帧 WebP,最终 CLI validation `ok=true`、0 warning。 |
| Portable Skill 真实修改测试 | PASS:对月兔仅重绘 `tool` 两帧为星锤动作;其余 6 状态、12 帧与基线 byte-identical,完整包通过 PetCore validation,14 frames、0 warning。 |
@@ -117,8 +118,8 @@ V1 仍不包含公共素材库、分享/社区、Petdex 导入、Codex 内置宠
- `dist/AgentPetCompanion.app`:构建产物;若仓库位于 File Provider 管理目录,Finder 元数据可能随后改变裸 bundle 的严格签名判断。
- `dist/AgentPetCompanion-develop.zip`:当前支持的非正式 develop 交付物。App 在非 File Provider 临时目录完成组装、签名和归档;ZIP 排除资源 fork/xattr,并在另一临时目录解压后再次做严格签名校验。
- 当前 develop build:`0.1.0.1.20260715184943.78233`。
- ZIP SHA-256:`4219f188cd2cbb4fff969145d793fa8fef2f61ae7cc1af79adfd3c0690ad086b`。
- 当前 develop build:`0.1.0.1.20260716031517.53137`。
- ZIP SHA-256:`0d67cb080debd39e1725916cc1b687ba30e3a645a56c51c65dff4b0a3fb46739`。
该包不是 Developer ID 分发包,不承诺 Gatekeeper、公证或正式升级渠道。
@@ -195,14 +195,14 @@ Agent 连接
| 拖动 | AppKit 处理拖拽 |
| 缩放 | 宠物右侧 resize handle,与气泡开关纵向排列在同一控制列 |
| 透明背景 | premultiplied alpha texture |
| 消息气泡材质 | macOS 26 `Glass.clear.interactive()` + `GlassEffectContainer`;macOS 14–15 原生 `ultraThinMaterial` 回退 |
| 消息气泡材质 | macOS 26 将无 tint、无附加 opacity 的 `Glass.clear.interactive()` 直接施加到气泡内容;macOS 14–15 原生 `ultraThinMaterial` 回退 |
| 桌宠控制材质 | 缩放手柄、缩放值和气泡开关共享 clear glass;不显示拖动标签、不绘制脚下底座,宠物帧位于无遮挡内容层;视觉控件紧凑但命中区独立保留 |
| 右击菜单 | 原生 `NSMenu` + SF Symbols,不自绘菜单背景 |
| 动画播放 | Metal texture swap |
| 鼠标穿透 | 行为页开关控制 |
| 收起 | 菜单栏或悬浮层按钮 |
宠物本体与消息气泡分别由透明、无边框、non-activating `NSPanel` 承载;气泡开关和缩放手柄进一步各自使用与 36/38 pt 命中区等大的透明控制 panel,避免宠物主体的鼠标穿透状态让首个点击或拖拽落到下层 App。所有 panel 都属于同一个 macOS App 进程;Rust PetCore LaunchAgent 只负责状态、事件与数据,不绘制桌宠。气泡 panel 保持 `isOpaque=false` 和透明背景,SwiftUI 内容不再铺不透明白底:macOS 26 使用 clear Liquid Glass,附近气泡通过同一 `GlassEffectContainer` 采样;旧系统只使用系统超薄材质,保证原生可访问性与 Reduce Transparency 行为。
宠物本体与消息气泡分别由透明、无边框、non-activating `NSPanel` 承载;气泡开关和缩放手柄进一步各自使用与 36/38 pt 命中区等大的透明控制 panel,避免宠物主体的鼠标穿透状态让首个点击或拖拽落到下层 App。所有 panel 都属于同一个 macOS App 进程;Rust PetCore LaunchAgent 只负责状态、事件与数据,不绘制桌宠。气泡 panel 保持 `isOpaque=false` 和透明背景,SwiftUI 内容不再铺不透明白底:macOS 26 把最高透明度的 clear interactive glass 直接施加到包含前景的气泡内容,不加 tint、填充、边框或事后 opacity;透明 Panel 中不再包裹会提升后代玻璃层级的 `GlassEffectContainer`,避免光学层覆盖文字。旧系统使用系统超薄材质;Reduce Transparency 与 Increase Contrast 改用深色高对比回退,保证白色前景可读。
宠物输入由最小 `NSViewRepresentable` 桥接:左键按下/拖动/抬起只维护移动生命周期,未发生拖动的左键单击不执行额外动作;只有 `rightMouseDown` 在开关允许时构造原生 `NSMenu`。菜单不再同时挂在 SwiftUI 根视图,避免左右键语义重复或一次右击出现两个入口。
@@ -831,11 +831,12 @@ V1 固定遵守:
3. 开启事件会触发桌宠动作。
4. 关闭事件不会触发动作,但事件仍入库。
5. 每个 Agent 使用一个独立气泡;同一 Agent 的多个会话在气泡内按会话标题、右对齐状态和 Agent 回复正文分行展示,正文最多两行且布局测量使用同一限制,运行中尚无回复时显示「思考中」。连接器未提供标题时,PetCore 按事件时间和持久化序号读取该会话第一条 user 消息,折叠为空格并截为最多 80 个字符作为稳定标题;后续 user 消息只更新当前消息,不改写降级标题。若官方标题随后出现,始终覆盖该降级标题。
6. 普通会话默认在最近用户激活 15 分钟后收起且能配置为 1–1440 分钟;新用户消息使其重新出现。仍 active 的 Needs input 与 Blocked 不因普通超时消失,已关闭 Waiting 不长期占位。
7. 宠物动作能区分 Running、Needs input、Ready、Blocked,并按 Needs input > Blocked > Ready > Running 仲裁唯一主状态;气泡同时显示最多 8 个会话。
8. Codex 气泡能显示会话标题及受控的最新 Agent 回复;UserPromptSubmit 提供标题回退,Stop 提供最终回复,缺失时可按明确 session_id 通过 App Server 只读补全;ChatGPT 桌面任务即使 hooks 暂未触发也能由有界近期活动同步在 1 秒轮询周期内出现;已完成工具不得继续显示为当前活动,持久化层省略实时 item 时不得复活旧摘要;PermissionRequest/`activeFlags` 显示明确交互提示且不泄露工具参数。精确实时工具类别的验收以用户已信任的 `PreToolUse` 为前提。
9. 点击或右击任一会话行都路由到该行的 source/session,按钮文案仅为「打开」,关闭热区与会话热区不重叠;Warp 优先精确聚焦原 pane,其他 CLI 至少激活原终端 App;Codex 只对确认属于 ChatGPT 桌面 surface 的任务使用 `codex://threads/<session_id>`,未知/CLI surface 不盲用深链;明确关闭的会话不再提供打开操作。
10. 新 ChatGPT 桌面 App 内嵌 Codex CLI 优先于 PATH 旧 CLI;PetCore RPC 协议或构建 ID 不匹配时 App 强制替换旧守护进程。
6. macOS 26 气泡使用最高透明度 `Glass.clear.interactive()` 且文字、图标、状态和操作必须位于玻璃内容层;明亮、暗色和混合背景上前景均可辨认,正常模式仍能识别气泡后的桌面内容。Reduce Transparency 与 Increase Contrast 下使用更强的系统/深色回退,不能出现白字白底。
7. 普通会话默认在最近用户激活 15 分钟后收起且能配置为 1–1440 分钟;新用户消息使其重新出现。仍 active 的 Needs input 与 Blocked 不因普通超时消失,已关闭 Waiting 不长期占位。
8. 宠物动作能区分 Running、Needs input、Ready、Blocked,并按 Needs input > Blocked > Ready > Running 仲裁唯一主状态;气泡同时显示最多 8 个会话。
9. Codex 气泡能显示会话标题及受控的最新 Agent 回复;UserPromptSubmit 提供标题回退,Stop 提供最终回复,缺失时可按明确 session_id 通过 App Server 只读补全;ChatGPT 桌面任务即使 hooks 暂未触发也能由有界近期活动同步在 1 秒轮询周期内出现;已完成工具不得继续显示为当前活动,持久化层省略实时 item 时不得复活旧摘要;PermissionRequest/`activeFlags` 显示明确交互提示且不泄露工具参数。精确实时工具类别的验收以用户已信任的 `PreToolUse` 为前提。
10. 点击或右击任一会话行都路由到该行的 source/session,按钮文案仅为「打开」,关闭热区与会话热区不重叠;Warp 优先精确聚焦原 pane,其他 CLI 至少激活原终端 App;Codex 只对确认属于 ChatGPT 桌面 surface 的任务使用 `codex://threads/<session_id>`,未知/CLI surface 不盲用深链;明确关闭的会话不再提供打开操作。
11. 新 ChatGPT 桌面 App 内嵌 Codex CLI 优先于 PATH 旧 CLI;PetCore RPC 协议或构建 ID 不匹配时 App 强制替换旧守护进程。
### 14.4 性能
@@ -175,7 +175,7 @@ Agent 连接页、行为页和桌宠消息气泡必须共用同一品牌图标
- 左键单击宠物不触发额外 UI;按住左键拖动仍用于移动位置,仅右击宠物打开快捷菜单。
- Agent 事件触发对应动作。
- 每个 Agent 使用独立气泡;同一 Agent 的多个会话放在同一个气泡中,并以分隔行区分。
- 消息气泡整体使用 macOS 系统原生 Liquid Glass;macOS 26 采用最高透明度的 clear interactive glass,多气泡共享同一玻璃容器。旧系统使用原生 ultra-thin material 回退,不绘制不透明白色卡片。
- 消息气泡整体使用 macOS 系统原生 Liquid Glass;macOS 26 采用最高透明度的 clear interactive glass,不加 tint、不透明填充、边框或玻璃层 opacity。气泡内容直接位于玻璃内容层;透明 `NSPanel` 不再额外使用会提升后代玻璃层级的共享容器,避免光学层遮住文字。旧系统使用原生 ultra-thin material 回退,不绘制不透明白色卡片。
- 缩放手柄、缩放值、会话收起/展开按钮和悬浮操作统一使用原生透明玻璃;不额外显示“拖动”悬浮标签,宠物帧素材保持无遮挡,脚下不绘制玻璃底座或装饰承托层。控制视觉尺寸保持紧凑,但缩放与收起/展开按钮保留独立的可访问命中区。右击操作固定使用系统 `NSMenu`,不自绘仿系统菜单。
- 每个会话行左侧显示会话标题,右侧显示运行状态,下方显示当前信息;正文最多显示两行并在末尾截断,气泡测量高度与两行限制保持一致。运行中优先显示上游公开的实时活动摘要(例如 Codex 的 reasoning summary、计划、搜索、文件修改或工具类别);没有新实时信号时保留最近公开信息,再退回 Agent 回复或「思考中」。已完成的工具项不得继续冒充当前活动,Ready/完成态显示最终 Agent 回复,Needs input/Blocked 显示交互或错误状态提示,不能用旧回复掩盖当前状态。Codex 精确的实时 Shell/读取/搜索切换来自用户已信任的官方 hooks;未信任时 App Server 有损快照只做标题、消息、公开摘要和有限状态兜底,不伪造被省略的具体工具。标题不可用时固定退回该会话第一条用户消息的单行摘要;若首条消息也不可用,再退回项目标识和 Agent 会话占位名。后续用户消息不得改写已形成的降级标题。
- 活跃会话没有新事件时继续保留当前状态与消息,不退回“等待 Agent 事件”。普通会话从最近一次可观测活动起超过配置时长后收起,默认 15 分钟;用户发送新消息或 Agent 产生新活动后重新展示。ChatGPT 桌面 Codex 在 hooks 暂未触发时由官方 App Server 的有界近期任务查询兜底,不要求用户先重启任务。