Skip to content
Merged
53 changes: 51 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,20 @@ jobs:
# Here it held three of the account's five macOS runners at a time.
name: Swift tests + iOS build
runs-on: macos-latest
timeout-minutes: 30
# About five minutes end to end: package tests ~1.5, xcodegen ~1, the
# simulator build ~2. The cap is a hang guard, not room for the build
# to grow into: the two guard steps below name the slow code instead.
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: No #available branch in a View extension
# The way the Oct 2026 build went from 5 to 25+ minutes: a
# `@ViewBuilder` View extension branching on `#available` doubles the
# caller's view type at every call, and IRGen on the batch holding
# both never finishes. The type-check flag below cannot see it and a
# build killed at the cap prints nothing, so the shape is refused at
# the source: such a shim is a ViewModifier (ios/App/BackDeployCompat.swift).
run: bash scripts/check-ios-view-shims.sh
- name: Swift package tests
run: swift test --package-path ios
- name: Generate the Xcode project
Expand All @@ -521,15 +532,53 @@ jobs:
cd ios
xcodegen generate
- name: Build the simulator app
# One slice. The generic simulator destination builds arm64 AND
# x86_64 (ONLY_ACTIVE_ARCH is a no-op there), and nothing runs the
# x86_64 one: the runner, the UI-test simulators and every current
# Mac are arm64. Two slices doubled the compile and ran six
# swift-frontends side by side on a 3-core, 7 GB runner. The log is
# kept for the type-check step that follows.
run: |
set -o pipefail
cd ios
xcodebuild \
-project OpenMausCompanion.xcodeproj \
-scheme OpenMausCompanion \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 \
CODE_SIGNING_ALLOWED=NO \
build
build | tee "$RUNNER_TEMP/xcodebuild.log"
- name: No Swift expression or body over the type-check limit
# ios/project.yml (Debug) has the compiler warn when it spends more
# than 500 ms type-checking one expression or function body, the
# usual way a SwiftUI build turns from minutes into a timeout. Swift
# has no diagnostic group for those two warnings, so -Werror cannot
# single them out; the log is read instead.
#
# 500 ms is the bar on a developer Mac, where Xcode shows the
# warning at its line. It is not a stable verdict on this runner:
# the compiler measures wall-clock time, and the shared 3-core
# runner swings about 2x from one run to the next (bodies at 216
# and 263 ms on an M5 measured 551 and 600 ms here on a loaded run,
# under 420 ms on a quiet one). The step therefore lists every
# body over the 500 ms warning limit, and fails only on one over
# three times that, which no body in the app comes near and which
# a type-check problem on its way to a timeout runs past.
run: |
fail_over_ms=1500
warnings=$(grep -E 'took [0-9]+ms to type-check \(limit: ' "$RUNNER_TEMP/xcodebuild.log" | sed -E 's#^.*/ios/##' | sort -u || true)
if [ -n "$warnings" ]; then
echo "Over the 500 ms warning limit on this runner (fails at ${fail_over_ms} ms):"
printf '%s\n' "$warnings"
fi
hits=$(printf '%s\n' "$warnings" | awk -v limit="$fail_over_ms" 'match($0, /took [0-9]+ms/) { ms = substr($0, RSTART + 5, RLENGTH - 7) + 0; if (ms > limit) print }')
if [ -n "$hits" ]; then
printf '%s\n' "$hits"
echo "::error::The Swift type checker spent over ${fail_over_ms} ms on the expression(s) above. Split that body into smaller views or functions, and give colours chosen by a ternary an explicit Color type."
exit 1
fi
echo "No expression or function body took the type checker over ${fail_over_ms} ms."

# Selection includes ios/** as well as android/**: :core's fixtures come
# from ios/Tests/CompanionCoreTests/Fixtures.
Expand Down
93 changes: 52 additions & 41 deletions ios/App/ActivityRunChip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,53 +23,64 @@ struct ActivityRunChip: View {
running ? "Running \(items.count) steps" : "Ran \(items.count) steps"
}

var body: some View {
let isDark = colorScheme == .dark
private var isDark: Bool { colorScheme == .dark }
private var chipFill: Color { isDark ? Color.white.opacity(0.08) : Color.black.opacity(0.05) }
private var chipStroke: Color { isDark ? Color.white.opacity(0.08) : Color.black.opacity(0.06) }
private var summaryColor: Color { isDark ? Color(hex: "#E2E8F0") : Color(hex: "#334155") }

var body: some View {
VStack(alignment: .leading, spacing: 6) {
Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.75)) { expanded.toggle() }
Haptics.selection()
} label: {
HStack(spacing: 6) {
Image(systemName: running ? "ellipsis.circle" : "checkmark.seal.fill")
.font(.system(size: 11))
.foregroundColor(running ? Color.secondary : Color(hex: "#22C55E"))
summaryButton
if expanded {
unfoldedSteps
}
}
.padding(.leading, 2)
}

private var summaryButton: some View {
Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.75)) { expanded.toggle() }
Haptics.selection()
} label: {
summaryLabel
}
.buttonStyle(.plain)
.accessibilityLabel(summary)
.accessibilityHint(expanded ? "Hides the steps" : "Shows the steps")
}

Text(summary)
.font(.caption2.weight(.bold))
.foregroundColor(isDark ? Color(hex: "#E2E8F0") : Color(hex: "#334155"))
private var summaryLabel: some View {
HStack(spacing: 6) {
Image(systemName: running ? "ellipsis.circle" : "checkmark.seal.fill")
.font(.system(size: 11))
.foregroundColor(running ? Color.secondary : Color(hex: "#22C55E"))

Image(systemName: "chevron.right")
.font(.system(size: 8, weight: .bold))
.foregroundColor(Color.secondary)
.rotationEffect(.degrees(expanded ? 90 : 0))
}
.padding(.horizontal, 9)
.padding(.vertical, 4.5)
.background(isDark ? Color.white.opacity(0.08) : Color.black.opacity(0.05))
.clipShape(Capsule())
.overlay(
Capsule()
.stroke(isDark ? Color.white.opacity(0.08) : Color.black.opacity(0.06), lineWidth: 0.5)
)
}
.buttonStyle(.plain)
.accessibilityLabel(summary)
.accessibilityHint(expanded ? "Hides the steps" : "Shows the steps")
Text(summary)
.font(.caption2.weight(.bold))
.foregroundColor(summaryColor)

if expanded {
VStack(alignment: .leading, spacing: 4) {
ForEach(items, id: \.id) { item in
ActivityChip(
tool: item.tool, threadRef: item.threadRef, openThread: openThread,
outputIsProse: item.isTeammateReport
)
}
}
.transition(.opacity.combined(with: .move(edge: .top)))
Image(systemName: "chevron.right")
.font(.system(size: 8, weight: .bold))
.foregroundColor(Color.secondary)
.rotationEffect(.degrees(expanded ? 90 : 0))
}
.padding(.horizontal, 9)
.padding(.vertical, 4.5)
.background(chipFill)
.clipShape(Capsule())
.overlay(Capsule().stroke(chipStroke, lineWidth: 0.5))
}

private var unfoldedSteps: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(items, id: \.id) { item in
ActivityChip(
tool: item.tool, threadRef: item.threadRef, openThread: openThread,
outputIsProse: item.isTeammateReport
)
}
}
.padding(.leading, 2)
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
Loading
Loading