Meetings: idle detection stops redrawing the app every 3 seconds, and the page knows a download is under way
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 14/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- macos, swift
- Domain
- desktop, performance
Research direction
Start with LocalVoice --check-meetings and the detection loop in MeetingModel.swift (refreshDetection, around :717-718), then MeetingDetection.swift's snapshot and evaluate paths. The speech-readiness part touches MeetingAdmission.swift and MeetingWorkspaceView.swift, with the gallery's meetings-setup shots. Done means the listed acceptance checks pass: 0 publishes over 5 idle polls, a single publish on the 3rd Zoom observation, and the new speech title and caption assertions, plus native confirmation.
Written by the indexing model from the issue text.
Description
Outcome: Meetings: idle detection stops redrawing the app every 3 seconds, and the page knows a download is under way.
Verified by an independent skeptic against the release train head d7cea35 (round four of the 15–16 October pass: energy, leaks, first run, keyboard, concurrency, settings propagation, public docs).
Idle meeting detection stops redrawing Home and the toolbar every 3 seconds, and reads only the apps it can offer
What a person sees: With Detect Meetings & Calls on and no call, every 3-second poll does a lot of UI work although nothing on screen changes. It redraws the whole Home page when Home is open and both floating-toolbar views. It also runs the toolbar host's placement pass, which forces layout and calls orderFrontRegardless. That is about 1,200 times an hour. The poll also blocks the main thread for about 11 ms. Together these are the maintainer's recorded ~1% idle sample. After the change, an idle Mac does no UI work for detection, and the poll costs about 3 ms. Offers, their order, the PID offered, the three-observation rule, cooldowns, snooze and the auto-finish check behave exactly as now.
Evidence (d7cea35): Part A, publishing. MeetingModel is @MainActor (MeetingModel.swift:8-9) and offer is @Published (:11). configureDetection loops every 3 s (:695-707). refreshDetection assigns offer = detector.evaluate() unconditionally (:717), and nil→nil also fires objectWillChange. :718 re-sets error, and with it the @Published problem, on every poll while a metadata error persists. The disabled branch (:713) assigns offer = nil too. Subscribers to every MeetingModel publish: WorkbenchHomePage @ObservedObject var meetings (WorkbenchHome.swift:562); both toolbar views (FloatingToolbar.swift:85, :395); the toolbar host sink (CapturePanel.swift:343-345 → update(model:) :405-458, which calls measureToolbar :434 when on tools, orderFrontRegardless :444 and updateCoach :457); ToolbarModeFollower (ToolbarModeFollower.swift:38); and the offer sink (MeetingWorkspaceView.swift:380). That sink already ignores an equal offer (:387-388) and hides on nil, so skipping equal assignments changes no panel behaviour. evaluate() returns nil while suppressed (MeetingDetection.swift:152), so busy still clears the offer. Existing house rule: MeetingModel.swift:171 if admission != next, and AppCoordinator.swift:811-814 (Timer publishes only on change). Part B, reading. snapshot() reads the PID, bundle ID and two HAL activity properties, and builds an NSRunningApplication, for every Core Audio process object (MeetingDetection.swift:30-47). evaluate() then sorts the whole list twice in availableApps (:117-147), with comparators that call MeetingAppCatalogue.known(), a linear scan whose owns() lowercases and allocates per call (MeetingModels.swift:29-35, :86-88). Only catalogue apps (:160) and the call service (:170) can become offers. A non-catalogue process keys choices by its own bundle ID (:
Change: A (must, MeetingModel.swift):
- In refreshDetection, replace :717-718 with
let next = detector.evaluate(); if next != offer { offer = next }andif let issue = detector.lastError, !isBusy, problem != .unknown(issue) { error = issue }. - In its guard branch (:713) and in configureDetection's disabled branch (:697), write
if offer != nil { offer = nil }. - Change nothing else in the model.
B (MeetingDetection.swift):
- Add
static func considers(_ bundleID: String) -> Bool { MeetingAppCatalogue.known(bundleID) != nil || MeetingAppCatalogue.callServiceBundleIDs.contains(bundleID) }to MeetingDetector. - Add the requirement
func detectionSnapshot() throws -> [MeetingProcessSnapshot]to MeetingProcessSource. Give it an extension defaulttry snapshot().filter { MeetingDetector.considers($0.bundleID) }, so the three fixture conformers compile unchanged. - In MeetingSystemProcessSource, move the loop into one private
snapshot(including: (String) -> Bool). It reads the bundle ID first, checks exclusion andincluding, and only then reads the PID, both activity flags and NSRunningApplication. snapshot() passes{ _ in true }and detectionSnapshot() passesMeetingDetector.considers. Do not keep two copies of the loop. - MeetingDetector.read() and activity(for:) call detectionSnapshot(). enumerateApps() (the picker) keeps snapshot().
- Leave out the optional catalogue re-keying.
- The PR text should say that an activity-read error on an unrelated process no longer fails the poll.
Checks (MeetingChecks.swift, --check-meetings):
- Attach an objectWillChange counter after enabling detection and one warm-up poll. 5 idle polls give 0 publishes. With a Zoom input fixture, the 3rd poll gives exactly 1 publish carrying the offer and the next 2 give 0.
- A persistent fixt
Acceptance: LocalVoice --check-meetings passes when these all hold:
- 0 MeetingModel.objectWillChange over 5 idle refreshDetection() calls, with the counter attached after enabling detection and one warm-up poll.
- Exactly 1 publish when a Zoom input fixture reaches its 3rd observation, and 0 on the next 2 polls.
- A persistent metadata failure sets
problemonce. - Full-snapshot and filtered-snapshot detectors driven through the same streak, cooldown, Not now, snooze, disable and PID-change sequence over a ~40-process fixture return identical offers (id, name, bundleID, including Chrome's main-app PID) and identical activity(for:) results.
- The existing offer-panel lifecycle assertions (MeetingChecks.swift:602-656) and LiveVoiceExperienceChecks activity checks still pass.
- No assertion depends on two live Core Audio reads agreeing.
Owed natively and not needed to merge: a sample of the installed Preview with detection on and Home open no longer shows an NSHostingView transaction or CapturePanelController.update every 3 s.
Value: 3 of 5 · Grammar: Quality · Risk: Low. Part A only stops publishing values that have not changed. The one offer consumer already ignores an equal offer, and default users (detection off) never had the 3 s tick, so nothing can depend on it. Part B is equivalent for offers and activity because the catalogue and call-service keys cannot collide with non-catalogue bundle IDs. Its only behaviour change is that a failed activity read on an unrelated process no longer suppresses detection or shows an error, which is a robustness gain t
Meetings says "Speech setup needed / Open Models before starting" even while the download the person started is running, with no button beside it
What a person sees: A newcomer who starts the Parakeet download from Home and opens Meetings to get ready for a call reads 'Speech setup needed'. A problem card then tells them to 'Open Models before starting speech recognition. Downloading Parakeet · 42%': to set up something that is already setting up, with no button. Before the download, the same sentence appears twice, in the card and as the caption, and the only door is a small Models… link. Dictate handles these states with 'Preparing dictation…' and 'Dictation unavailable'.
Evidence (d7cea35): MeetingModel.swift:153 turns any snapshot that cannot transcribe into .speech(host.recognition.line), including a running download (canTranscribe is false while operationID is set, RecognitionReadiness.swift:17). AppModel.acceptRecognition calls meetings.refreshAdmission() on every progress delivery (AppModel.swift:270). MeetingAdmission.swift:15 prefixes 'Open Models before starting speech recognition. ', and :32 gives the title 'Speech setup needed', which shows at MeetingWorkspaceView.swift:82. statusLine stays 'Start once and follow the words as they happen · Up to 2 hours' (:234-239). The problem card (:159-161) has buttons only for microphone and app-audio problems (:164-180). The caption repeats the same readiness line beside the page's only Models… link (:151-153; engineName is model.modelMessage, WorkbenchHome.swift:208). Dictate: Views.swift:294 'Preparing dictation…' and :296 'Dictation unavailable'. The gallery renders only the idle setup state.
Change: Make Meetings' speech readiness look like Dictate's: one title, the readiness line once, no triangle card while speech is merely unready or preparing.
-
MeetingAdmission.swift
- Add
var speechPreparing = falseto MeetingAdmission. - Title: when captureProblem is
.speech, return "Preparing speech…" if speechPreparing is true, otherwise "Speech setup needed". All other titles stay as they are. .speech(detail)'s message becomes the detail alone, without "Open Models before starting speech recognition."
- Add
-
MeetingModel.refreshAdmission
- Pass
speechPreparing: host.recognition.isPreparinginto the admission it builds. - Mirror the microphone clear at :172: if
problemis.speechand host.recognition.canTranscribe, call setProblem(nil). That removes the stale card a refused call-offer Start leaves behind. - Precedence of
.speechover source and microphone problems is unchanged.
- Pass
-
MeetingWorkspaceView.swift
- statusLine: when idle and
model.admission.recognitionProblem != nil, return engineName (the readiness line, e.g. the progress or the Download offer). - Bottom engine row: while recognitionProblem != nil and idle, show only the Models… link, not the label, as Dictate's dictateEngine does (Views.swift:274-276). The line then appears once.
- Problem card (:159): skip
.speechwhether it comes from model.problem or admission.captureProblem; the title and caption already carry it. - Start recording stays disabled through admission.canStart. Kept-for-later Transcribe stays disabled through recognitionProblem.
- No spinner or Cancel setup is added here; Models owns Cancel.
- Expose the caption decision as a small static helper, so a check can call it the way WorkbenchPageChecks calls ContentView.progressCaption.
- statusLine: when idle and
-
Meet
Acceptance: 1. LocalVoice --check-meetings, run from scripts/test.sh:97, passes with the new assertions:
- A preparing snapshot gives admission.title "Preparing speech…", canStart false, and no "Open Models before" text in any MeetingProblem message.
- An idle unavailable snapshot gives "Speech setup needed".
- The caption helper returns the snapshot's line ("Downloading Parakeet · 42%", or "Download Parakeet to turn speech into text on this Mac.").
- The card predicate excludes
.speech. - A
.speechproblem from a refused start is nil after a localReady snapshot. - Existing admission checks (:1186-1245) still pass.
- The gallery's Meetings recovery pass renders meetings-setup and meetings-setup-preparing in light and dark. Each shows the setup title, the readiness line exactly once (under the title), the Models… link, a disabled Start recording, and no orange-triangle card.
- The existing microphone and source-failure shots are unchanged.
Value: 2 of 5 · Grammar: Quality · Risk: Low.
- It changes copy and layout in MeetingWorkspaceView, which three train PRs touched tonight (#343, #349, #371), and it changes the build under test right before ship. Rebasing is trivial.
.speechstill masks microphone and source problems until speech is ready, as it does today, so a newcomer with a denied microphone still learns of it only after setup.- A call-offer Start refused for speech now explains itself only through the title and caption, with no dismissable card. Detection is
Refs #280
Build-ready record from round four of the overnight lead pass, verified by an independent skeptic per item. Closes only after native acceptance.
- Dominant language
- Swift
- Stars
- 0
- Forks
- 1
- Avg merge
- 8h 5m
- Merged PRs (30d)
- 215
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Ship-Work/workbench
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
documentation good first issue help wanted
Difficulty 2/5 Half a day Newbie friendliness 74/100
Ship-Work/workbench#29 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 55/100
Maintainers usually reply within 1 day
All issues in Ship-Work/workbench
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
openwatersio/slackwater-ios#736 ·
Maintainers usually reply within 1 day
-
agent/sec-check help wanted security stage/010-actionable
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
hivecommons/hotshot#235 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
MustacheCase/zanadir#254 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100