Sideloaded apps can't use APNs (no push entitlement without Apple signing),
so deliver notifications via an iOS BGAppRefreshTask that polls the monitored
repos every ~15 min and fires local notifications (no Apple push needed):
- new PRs that appear in the open set
- CI status changes on existing PRs
Persistence: NotificationState tracks announced PR ids + last-known CI status
so we only fire on actual changes, not every poll. AppDelegate registers the
background task and reschedules the next one. Info.plist gets UIBackgroundModes
fetch + the BGTask identifier. Settings gains a Notifications toggle; also
poll after a foreground inbox load so state seeds and changes are caught
promptly. Bump to 3.6.
Pull-to-refresh cancels the still-in-flight request (URLError.cancelled,
localizedDescription 'cancelled') to run the refresh, which surfaced as an
error dialog. Add an isCancellation helper and skip cancellations in the PR
load and CI-runs paths. Bump to 3.5.
- After a merge, the app returns to the landing page but the inbox kept stale
PRs. Add an inboxNeedsRefresh flag set on merge success; InboxView reloads
on next appearance when it's set.
- The PR content (incl. the Checks/workflow tab) is now pull-to-refreshable
via .refreshable on the content ScrollView, so workflow status can be
updated by pulling down. Bump to 3.4.
- PRDetailView: guard let current = selectedTab ?? .overview fails because
?? already unwraps to non-optional. Use let current = selectedTab ?? .overview.
- MarkdownText: var inCodeBlock inside ViewBuilder is not allowed. Precompute
lines with their code-block state as a computed property on a private struct,
then iterate via ForEach in the builder. Bump to 3.3.
The horizontal paging ScrollView squished the markdown text and the TabView
broke navigation. Revert to the plain vertical ScrollView + switch (always
rendered correctly). Swipe-to-change now lives on the bottom tab bar itself
(simultaneous DragGesture), so tapping tabs still works. Rewrite MarkdownText
line-based: each source line renders on its own line (AttributedString
collapses single newlines into spaces), with heading/bullet/code handling.
Bump to 3.3.
scrollPosition(id:) requires Binding<some Hashable?>, but selectedTab was a
non-optional DetailTab — the build failed with 'generic parameter some
Hashable could not be inferred'. Make selectedTab optional (DetailTab? =
.overview); bottom-bar == comparisons still work via optional promotion.
The TabView with .page style broke NavigationLink inside tabs (the page
gesture swallows taps) and centered content. Replace with a horizontal paging
ScrollView using containerRelativeFrame and scrollTargetBehavior(.paging):
same swipe-to-change behavior, no gesture conflicts, diff view works again.
Bump to 3.2.
- MergeSheet: show a spinner in the toolbar while the merge is confirming/
rejecting, lock Cancel and interactive dismissal during the request, and
prevent double-taps.
- PR detail: replace the ScrollView-switch with a page-style TabView so you
can swipe left/right to change sections (iOS 26 style); bottom bar taps
still select. Make DetailTab Hashable. Bump to 3.1.
On a successful merge, show a brief full-screen green checkmark animation
('Merged'), then automatically pop the PR detail view back to the landing
page. MergeSheet now calls an onSuccess callback; PRDetailView shows the
overlay and dismisses after ~1.2s. Bump to 3.0.
- Approve: rapid taps fired multiple submitReview calls that each appended a
review, stacking duplicate entries. Add a busy guard (skip while one is in
flight) and dedupe reviews per user so the latest replaces earlier ones;
disable the review menu while submitting.
- Bottom bar: replace the full-width bar with a floating frosted-glass pill
inset from the edges (like SideStore), with a highlighted capsule behind
the active tab. Bump to 2.9.
- Empty state ('No open PRs') was a bare ContentUnavailableView with no
pull-to-refresh, so you couldn't reload when the list was empty. Wrap it in
a refreshable ScrollView so pulling down reloads even with no PRs.
- Switch the landing title from .large to .inline so 'Pull Requests' sits on
the same row as the settings gear (better space use). Bump to 2.8.
The built bundle contains sized variants (AppIcon60x60@2x.png etc.), not a
file named AppIcon.png, so the CI cp failed and scp aborted. Commit the
1024x1024 icon.png to the repo root and upload it directly.
Generate a clean, centered git merge/PR icon via ComfyUI (DreamShaper on
wilkes) and bundle it as the app icon. Update the CI workflow to extract the
icon from the built bundle and upload it as icon.png so SideStore shows it.
Bump to 2.7.
When a PR's head branch is behind main (or conflicts), Gitea's merge returns
409. Previously the app just showed an error. Now: on a 409 the app calls
POST /pulls/{index}/update (merges base into head), then retries the merge
once. Add APIError.mergeConflict for a clear message and map 409 in the
client. Bump to 2.6.
- Bottom tab bar: replace the floating capsule (which had space below it and
was too thin) with a full-width frosted-glass bar at the standard iOS tab
bar height, sitting flush above the home indicator — matching Apple Music.
- Landing page title: use .large navigation title so 'Pull Requests' aligns at
the top like Apple Music's 'Inicio', instead of the inline title.
- Bump to 2.5.
- Bottom tab bar: reduce vertical padding (8→4), icon size (20→18), and
bottom inset (8→4) so it matches Apple's standard compact tab bar height
(Apple Music etc.) instead of a tall bar.
- Remove the manual reload button from the loaded PR toolbar — the detail
view auto-loads on appearance, so it was redundant clutter. The reload
button is kept only on the error/retry state. Bump to 2.4.
- Remove the in-list loading wheel overlay; the landing page just shows the
cached list or the 'No open PRs' screen.
- Add a 'Monitored repos' setting (Settings > Inbox) that limits the inbox to
specific repos, cutting the number of API calls dramatically. Empty =
monitor all accessible repos. Bump to 2.3.
.task fires on every appearance, including popping back from a PR detail, so
the landing page re-ran the full network refresh on each back navigation.
Guard the initial load with didInitialLoad so the list loads once and then
only refreshes when the user pulls down. Bump to 2.2.
The inbox was slow because it enriched every PR with CI runs + reviews (2
extra API calls per PR, sequential). Drop that from the list — CI/review
status now shows only in the PR detail screen. Add a disk cache so the landing
screen renders instantly from the last load, then refreshes in the background
and keeps the cached list on network failure. Bump to 2.1.
- Merge button: use the proper SF Symbol 'arrow.triangle.merge' instead of
the download icon 'arrow.down.circle'.
- Add a Settings toggle 'Delete branch after merge'. When on, the merge call
passes delete_branch_after_merge=true and the head branch is deleted after
a successful merge (best-effort).
- PR detail bottom tab bar: floating capsule style (regularMaterial + shadow)
like the latest iOS tab bars, instead of a full-width bar.
- Bump to 2.0.
- Remove the Commits tab (not useful).
- Move the section switcher to a bottom tab bar with icons (Overview /
Conversation / Files / Checks) instead of a top segmented control.
- Move Review (menu) and Merge into the navigation toolbar, removing the
bottom action bar.
- Add an Overview tab that renders the PR body as markdown via SwiftUI's
built-in AttributedString(markdown:) (no third-party dependency).
- Bump to 1.9.
- Checks: only show the Logs button once a job has actually run (terminal
conclusion). Queued/running jobs are grey with no logs, so the button was
useless there.
- Inbox: remove the segmented filter bar (All/Assigned/By me/Review) — not
useful yet. Bump to 1.8.
Commits tab was empty: Gitea PR commits have no 'id' field (only sha), but
PRCommit.id was a non-optional let decoded from 'id', so decoding [PRCommit]
always threw and the tab silently showed nothing. Make PRCommit.id a computed
sha-based identity so the array decodes and renders.
CI: add a Logs button per job that fetches the job's raw log text
(/actions/jobs/{id}/logs) into a viewable, shareable screen. Bump to 1.7.
The diff data was fine, but the render collapsed: a LazyVStack inside a
dual-axis ScrollView([.horizontal, .vertical]) gets an unbounded proposal and
rows collapse to near-zero width — the green addition background rendered but
the monospaced text was laid out at ~0 width and disappeared. Use a plain
VStack inside nested vertical→horizontal ScrollViews (the stable pattern for
diff text) and give each line fixedSize(horizontal:) so the text isn't
clipped. Bump to 1.6.
DiffLine.id was a computed property returning a fresh UUID() on every access.
SwiftUI's ForEach requires stable identities; regenerating each read churned
the main thread, hanging the diff screen and eventually crashing. Store the
UUID once per line. Also add a 30s URLRequest timeout so a hung request can't
leave the 'Loading diff...' spinner forever. Bump to 1.5.
This Gitea instance never returns a 'patch' field from /pulls/{n}/files (the
field is absent for all files, regardless of size), so the diff viewer always
fell back to 'Diff too large to preview'. Now DiffView fetches the PR's .diff
endpoint (plain-text unified diff), extracts the hunks for the selected file,
and renders them colour-coded. Bump to 1.4.
Opening a PR could fail the whole screen when any one detail sub-request
returns 401/403 (e.g. a token scope the repo's Actions endpoint needs).
Refactor load() so the PR fetch is the only fatal call; comments/reviews/
files/commits/runs each fail independently, are logged, and the rest still
render. Also log actions/runs failures for visibility. Bump to 1.3.
LogStore is @MainActor; GiteaClient.request is not main-actor-isolated, so the
5 LogStore.shared.log calls in the request handler need 'await' under Swift 5
strict concurrency. Without them the build fails at compile time.
- LogStore: bounded in-memory + persisted log of API calls and errors,
viewable and shareable from Settings → View logs.
- GiteaClient: log every request/response status (esp. 404s).
- Inbox: a single failing repo no longer wipes the whole inbox — per-repo
errors are caught, logged, and the rest still load.
- Bump to 1.2 so SideStore offers the update.
SideStore only shows an update when the version changes; the previous build
stayed at 1.0 so no update appeared. Bump MARKETING_VERSION to 1.1 and have
the workflow read CFBundleShortVersionString from the built app's Info.plist
instead of a hardcoded VERSION, so the metadata can never drift from the
pbxproj again.
Gitea returns dates like 2026-08-12T11:09:13+02:00. The default JSONDecoder
date strategy expects a Unix timestamp number, so decoding any Date? field
threw and the whole [PullRequest] array failed — the inbox showed no PRs.
Add a shared decoder with a custom ISO8601 strategy (with and without
fractional seconds) and use it for all get/post decoding.
Multi-repo PR inbox, review (approve/request changes/comment), merge with
gating toggle, read-only diff viewer, and Gitea Actions check status with
one-tap re-run. Ships unsigned via the iMac runner to the SideStore source.