Repository navigation
feat: recent calls, call waiting and hold, welcome window and more - #7
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
🟡 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
Hide recent rows while the keypad is displayed · New Use stable identities for recent-row selection · New Require an outgoing hook for three-way controls · New Bound pending history waits and recover stale exchanges · New Reject swapping when a waiting caller is present · New Prevent invalid End & Accept for held-only calls · New Serialize hold actions to prevent reply misrouting · New Finalize abandoned waiting callers as missed calls · New Reconcile call identities after external swaps · New Register pending history requests before transport writes · New Disable held-call actions when CHLD is unsupported · New Treat discarded history reads as unanswered · New Avoid duplicate history entries after restored link loss · New Clear stale phone lists after refused history reads · New
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
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Recent calls (popover, while the search field is empty)
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.Add to <contacts app>…(vCard handed to the Mac's default vCard app),Copy Number, ⌘C.AT+VGS/AT+VGMare answered (10 s at most); an unanswered command is given up after 8 s (fixes an endless spinner).docs/BLUETOOTH.md).Calls
AT+CHLD=0/1/2, only when the phone announces three-way calling).App
Tests
scripts/verify.shpassed locally, including sanitizers and all 35 UI tests (tree1bb42c8).Hardware test needed (maintainer)
Add to Contacts…with the default vCard app.🤖 Generated with Claude Code