Skip to content

feat: recent calls, call waiting and hold, welcome window and more - #7

Merged
macx merged 20 commits into
mainfrom
feat/recent-calls
Oct 30, 2026
Merged

macx merged 20 commits into
mainfrom
feat/recent-calls

Conversation

@macx

@macx macx commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Summary

Recent calls (popover, while the search field is empty)

  • Outgoing, Incoming and Missed tabs, read from the phone's call log over HFP (AT+CPBS/AT+CPBR); a session list of calls seen since launch when the phone does not share it. Nothing is stored on the Mac.
  • Call button per row, redial from the keypad, Add to <contacts app>… (vCard handed to the Mac's default vCard app), Copy Number, ⌘C.
  • Keyboard: Tab order field → tabs → list, ↓ from the field selects the first row, ←/→ and ⌘1–3 switch tabs, Return calls, Esc returns to the field.
  • Skeleton rows while the first read runs (max 10 s, then a message); reads start once IOBluetooth's own AT+VGS/AT+VGM are answered (10 s at most); an unanswered command is given up after 8 s (fixes an endless spinner).
  • Crash fix: call list reads never overlap IOBluetooth's set-up commands (SIGSEGV of 2026-10-29, see docs/BLUETOOTH.md).

Calls

  • Hold/Resume, Swap, end the held call, and for a waiting call Hold & Accept, End & Accept, Decline (AT+CHLD=0/1/2, only when the phone announces three-way calling).
  • Missed-call dot on the menu bar icon (system red) and a Call Back notification action.

App

  • Phone battery: gauge in the popover's status line, percentage in the menu bar, or hidden (Settings).
  • Resizable popover (300–600 pt, remembered), round keypad keys, About panel links (GitHub, davideiken.de).
  • Welcome window at the first launch and after unsigned updates (permissions may be reset).
  • German texts use "du".
  • Accessibility pass on the dialer (contrast, VoiceOver announcements, focus handling).

Tests

  • scripts/verify.sh passed locally, including sanitizers and all 35 UI tests (tree 1bb42c8).
  • New unit tests for call history, three-way calling (incl. a fuzz-found hang-up regression), transport timeouts, vCard escaping, popover width, battery, welcome policy.

Hardware test needed (maintainer)

  • Hold, swap and call waiting with a real second call.
  • Recent calls right after connecting (new start condition for the read).
  • Add to Contacts… with the default vCard app.

🤖 Generated with Claude Code

macx and others added 16 commits October 29, 2026 12:16
Read the phone's dialed, received and missed call lists over HFP
(AT+CPBS/AT+CPBR) and show them as tabs while the search field is
empty, with a call button on every row. Falls back to the calls seen
since launch when the phone does not share its call history. Tab moves
into the list, Esc returns to the field; the keypad's call button
fills in the last dialed number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ht stable

- missed-call titles, row subtitles and the selection outline reach WCAG
  contrast in light and dark mode (tested)
- left/right arrows and Command-1 to 3 switch tabs; VoiceOver announces
  keyboard selection and tab changes; Tab never traps the keyboard
- call buttons are named after the caller and have a larger hit area;
  the keypad button reads Redial when it fills in the last number
- keypad and recent calls share one fixed-height box, so toggling the
  keypad or loading the calls no longer resizes the popover

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-up

A read that started 2 s after the SLC interleaved with IOBluetooth's own
AT+VGS/AT+VGM and crashed IOBluetooth in processResultCode. Reads now
wait 10 s after the SLC and until no command waits for its reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the list has a fixed width, so the legacy scroller shown with a mouse
  no longer widens it and pushes the content sideways
- the keypad button keeps one glass (tint changes) without the press
  bounce, outside a glass container that made the search field shift
- keys fill the content width, so keypad and lists share side margins
- rows fade out at the bottom instead of being cut

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… German

The recent calls tabs are equal segments that always fill the content
width; VoiceOver still sees a segmented picker. Every dialer block now
has the fixed content width, so no child can widen the popover. Keys
are back to 60 pt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Return is handled in the popover's key monitor when a row is selected,
and rows no longer take keyboard focus, so only one outline shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…recent calls

Tab now follows the system order from the search field to the tabs and then the
list, each one focus stop (also without Full Keyboard Access). Down from the
field selects the first row like in the search results, Command-C copies the
selected number, rows get a Call / Copy Number context menu, and typing while
the tabs or the list have focus continues in the search field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The popover's width can be dragged at either side between 300 and 600 pt
  (VoiceOver: adjustable). Search and lists grow, the keypad keeps its size.
  The width is saved in the user defaults.
- Keypad keys use plain glass with an even press effect: the interactive glass
  stretched pressed keys into ovals. More room between search bar and keys.
- The About panel links to the GitHub repository and to davideiken.de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y display

- A missed call puts a system-red dot on the menu bar icon (saved across
  relaunches). Opening the popover shows the Missed tab and clears it.
- Missed-call notifications for a dialable number offer "Call Back"; service
  codes never get the action.
- Settings > Phone battery: gauge and percentage in the popover's status line
  (default), percentage next to the menu bar icon, or hidden. HFP reports the
  charge in 20 % steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recent calls and typed numbers without a contact get "Add to <app>…" in their
context menu. Pickup writes an escaped vCard to a private temporary folder and
opens it with the app that handles vCards (Contacts, or e.g. Cardhop), then
removes the file after five minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine tracks a second call: a held call next to the active one and the
waiting call. With phones that announce three-way calling (+BRSF bit 0, +CHLD
modes 0-2) the call window offers Hold/Resume, Swap, ending the held call, and
for a waiting call Hold & Accept, End & Accept and Decline (AT+CHLD=0/1/2).
Hanging up the active call with one on hold resumes the held call.

Effects apply on the phone's OK or the matching indicator, whichever comes
first; AT+CLCC confirms numbers. A waiting call taken on the phone itself is
recognised too. The fuzz test found that a phone without three-way calling
could block hanging up once it reported a second call; AT+CHUP is used then
(regression test added). Needs a hardware test with a real second call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The About panel's description and links are centred, with room above and
below the links. The menu bar draws handset and percentage as one template
image with a small gap, the popover's gauge sits closer to its number, and the
space before the percent sign is a narrow no-break space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A welcome window at the first launch and, while Pickup has no Developer ID
  signature, after every update (macOS may reset permissions then). It shows
  the phone, Bluetooth, Contacts and notification permissions with their state
  and the button that grants them, and what to allow on the phone. Automatic
  permission prompts are skipped while it is shown. Centred on the screen.
- German texts address the user with "du" instead of "Sie", including the
  privacy usage descriptions.
- The battery percentage in the menu bar is one point smaller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e loading

- A call list command the phone does not answer within 8 s is given up (it
  was never written after a failed write, and the dialer showed a spinner
  forever). This counts as unanswered, not refused, so no misleading hint
  about the phone's permission appears and the next refresh asks again.
- Reads start as soon as IOBluetooth's own AT+VGS/AT+VGM are answered instead
  of always waiting 10 s after the link came up (10 s stays the upper limit).
- While the first read runs, the dialer shows skeleton rows with a sweeping
  highlight (static with Reduce Motion) for at most 10 s, then a message.
  Nothing is stored on the Mac between launches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The tab test expected Right to go to Missed; the order is Outgoing,
  Incoming, Missed.
- The segmented picker that VoiceOver sees for the tabs was only as wide as
  its labels; it now spans the visible tabs (cursor and hit area match).
- Settings may be 405 pt high: the phone battery row added one row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 11:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical defects can apply call actions to the wrong caller, trigger unintended dialing from hidden or replaced recent entries, and leave call-history refreshes permanently blocked.

14 open issues
What changed in this PR

Expands Pickup’s Bluetooth call controls and adds recent-call browsing to the menu bar app, while keeping conversations on the phone.

Changes:

  • Adds phone call-history reads, session fallback, redial, contact export, and missed-call actions.
  • Introduces hold, resume, swap, and call-waiting controls, plus transport timing safeguards.
  • Adds welcome guidance, battery display options, popover resizing, accessibility improvements, localization, and tests.
File Description
Tests/​PickupCoreTests/​Support/​TestDoubles.swift Adds mock history and hold capabilities.
Tests/​PickupCoreTests/​CallHoldTests.swift Tests hold, waiting calls, swaps, and termination.
Tests/​PickupCoreTests/​CallHistoryTests.swift Tests history parsing, fallback, and refresh behavior.
Tests/​PickupBluetoothTests/​LogCanaryTests.swift Checks history data stays out of logs.
Tests/​PickupBluetoothTests/​AppleHFPTransportTests.swift Tests history sequencing, timeouts, and hold support.
Sources/​PickupCore/​HFPTypes.swift Defines phonebook commands and hold actions.
Sources/​PickupCore/​HFPTransport.swift Extends transport contracts for history and hold.
Sources/​PickupCore/​HFPEvent.swift Parses phone call-history entries.
Sources/​PickupCore/​CallHistory.swift Models phone and session call history.
Sources/​PickupCore/​CallEngine.swift Coordinates history refreshes and multiple calls.
Sources/​PickupBluetooth/​HandsFreeLink.swift Exposes capabilities and raw command sending.
Sources/​PickupBluetooth/​AppleHFPTransport.swift Implements history reads and hold commands.
docs/​BLUETOOTH.md Documents command timing and three-way calling behavior.
App/​Tests/​PickupUITests/​Cases/​PopoverTests.swift Tests recent-call keyboard actions and layout stability.
App/​Tests/​PickupUITests/​Cases/​LayoutAccessibilityTests.swift Updates the settings height allowance.
App/​Tests/​PickupUITests/​Cases/​AX.swift Adds keyboard codes for UI tests.
App/​Tests/​PickupAppTests/​WelcomePolicyTests.swift Tests first-launch and update welcome rules.
App/​Tests/​PickupAppTests/​RecentCallRowsTests.swift Tests recent rows, redial, and input safety.
App/​Tests/​PickupAppTests/​PopoverWidthTests.swift Tests resizing calculations and limits.
App/​Tests/​PickupAppTests/​PhoneBatteryTests.swift Tests battery formatting and missed-call classification.
App/​Tests/​PickupAppTests/​ContactCardTests.swift Tests vCard escaping and filenames.
App/​Tests/​PickupAppTests/​ColorContrastTests.swift Adds dialer text and selection contrast checks.
App/​Sources/​Pickup/​WelcomeView.swift Presents setup and permission guidance.
App/​Sources/​Pickup/​Welcome.swift Determines when welcome guidance appears.
App/​Sources/​Pickup/​Testing/​ScriptedPhone.swift Supplies invented call history for tests.
App/​Sources/​Pickup/​StatusItemController.swift Adds badges, battery text, shortcuts, and welcome presentation.
App/​Sources/​Pickup/​SettingsView.swift Adds battery display preferences.
App/​Sources/​Pickup/​PopoverWidth.swift Defines saved width limits and resize calculations.
App/​Sources/​Pickup/​PickupApp.swift Integrates welcome presentation into launch.
App/​Sources/​Pickup/​PhoneBattery.swift Models battery preferences and percentage formatting.
App/​Sources/​Pickup/​MissedCallNotifier.swift Adds notification callback actions.
App/​Sources/​Pickup/​MenuView.swift Adds resize handles and a battery gauge.
App/​Sources/​Pickup/​DialerView.swift Adds recent-call browsing, actions, and accessibility behavior.
App/​Sources/​Pickup/​DialerRows.swift Builds recent rows and supports copy and redial.
App/​Sources/​Pickup/​ContactCard.swift Exports contacts through temporary vCards.
App/​Sources/​Pickup/​CallViewComponents.swift Adds adaptive dialer colors.
App/​Sources/​Pickup/​CallUIModels.swift Extends UI state and actions for multiple calls.
App/​Sources/​Pickup/​CallHUDView.swift Presents hold, swap, and waiting-call controls.
App/​Sources/​Pickup/​AppModel.swift Connects history, battery, missed calls, and hold actions.
App/​Sources/​Pickup/​AppInfo.swift Adds About-panel destinations.
App/​Resources/​Localizable.xcstrings Adds feature translations and informal German wording.
App/​Resources/​InfoPlist.xcstrings Updates German permission descriptions.

Review effort: Balanced

Comment thread App/Sources/Pickup/DialerView.swift Resolved
Comment thread App/Sources/Pickup/DialerView.swift Resolved
Comment thread Sources/PickupCore/CallEngine.swift Resolved
Comment thread Sources/PickupCore/CallEngine.swift Resolved
Comment thread App/Sources/Pickup/CallHUDView.swift Resolved
Comment thread Sources/PickupCore/CallEngine.swift Resolved
Comment thread Sources/PickupCore/CallHistory.swift Resolved
macx and others added 4 commits October 30, 2026 12:34
The scripted phone lists more invented recent calls and, with
-PickupTestCallHold YES, supports three-way calling (off by default, so the
call-waiting UI test still covers phones without it). The capture script waits
for the recent calls and records the waiting-call card and the held call; the
generator uses a colourful synthetic wallpaper and a hero cropped to its
content (popover, banner, call window) without empty space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fresh README images: recent calls, call waiting and hold, Settings with the
  battery row, on the new wallpaper. The README uses the dark images (a capture
  of the popover alone renders its Liquid Glass flat grey in light mode).
- The feature list covers recent calls, hold/swap/call waiting, the missed-call
  dot with Call Back, the battery level, the resizable popover and the welcome
  window.
- HISTORY.md: hand-written notes per version for release texts and the website
  (CHANGELOG.md stays the generated list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling

Dialer
- Recent rows are not selectable or callable while the keypad covers them.
- A refresh that changes the entry under a selected row (row IDs are
  positions) clears the selection and a pending service-code confirmation.

Three-way calling
- One AT+CHLD at a time; an unanswered one is given up after 10 s. The HUD
  disables the hold buttons meanwhile.
- Swap is refused while a third caller waits (CHLD=2 would take that call).
- End & Accept needs an active call (CHLD=1 leaves a held call).
- Swap/End for a held call only with three-way calling support.
- Calls swapped on the phone are followed: the repeated callheld=1 asks for
  the call list, which exchanges the roles and keeps each call's identity.
- A waiting caller who gives up becomes a missed call (history, red dot,
  notification).
- AT+CHLD is only offered with the write hook (OK must be matched).

Recent calls
- The pending list is registered before the transport is asked.
- A refused read drops the old phone list; the session list takes over.
- Discarded or unsent reads count as unanswered, not refused.
- A read waiting for an idle link is given up after quiet period plus one
  command time instead of waiting forever (the gate is never bypassed).
- A call lost with the link is recorded only if it is not resumed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI runs SwiftLint with --strict and failed on file and type length. Three-way
calling, the call history refresh and the audio hand-back move into CallEngine
extensions in their own files (members they share become module-internal);
the recent calls tabs, skeleton and shimmer get their own file; the transport's
AT+CHLD support moves into an extension. The English welcome text is a little
shorter (the line was too long). No behaviour change.

The local gate now always lints with --strict, like CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@macx
macx merged commit 3e90c3c into main Oct 30, 2026
10 checks passed
@macx
macx deleted the feat/recent-calls branch October 30, 2026 12:22
@macx macx mentioned this pull request Oct 30, 2026
4 tasks done
macx added a commit that referenced this pull request Oct 30, 2026
Release **v0.2.1**, prepared by the prepare-release workflow from main
at `3e90c3c`.


### Features

* recent calls, call waiting and hold, welcome window and more
([#7](#7))
([3e90c3c](3e90c3c))

### Before merging (squash merge, keep the title)
- [x] CI is green on this pull request
- [x] Local gate with UI tests on this branch: `scripts/verify.sh`
- [x] Hardware smoke test with the phone if Bluetooth code changed
- [x] No other pull request was merged into main since this one was
prepared (otherwise re-run prepare-release)

Merging starts release.yml: tag v0.2.1, build, GitHub Release
(pre-release).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants