Three conditions (key rejected, can’t reach the provider, out of credit) and three places they could surface. The Mac app, Tahoe chrome, light/dark follows the system. The title-bar pill also exists as a DEBUG-only native prototype (Diagnostics ▸ Provider problem pill), because toolbar placement and resizing can only be judged native.
The existing out-of-credit pill, generalised. Same envelope, same .status zone; the dot colour and label change with the condition.
Add a new key in Settings.
Try a different network.
Add credit to run new analysis or coding.
The person who tags by hand for months — same project, same dead key:
MessageKind, not a new one. Rejected key is error (xmark.circle.fill, red; .invalid is red everywhere else); credit and unreachable are warning (exclamationmark.triangle.fill, amber). The words say which. This replaces the amber dot the shipped out-of-credit pill uses, a documented decision to reverse.apiServer wording split in two). “Check your connection” is right for none of the last two. If the app can’t tell which, it shows no pill.Kept for the record. Next to the version and the bug. The footer is plain secondary text by design (SidebarFooter.swift: not a capsule, not selectable).
Shipped today for runs: the row says “Run failed”, its popover says why. Three conditions, same row.
Your Claude key was rejected.
Category: Provider key issue
Couldn’t reach Claude. Check your connection.
Category: Network error
Claude is out of credit.
Category: Out of credit
The incidental call has no row to land on. The Signals lens, one card whose refresh was refused:
Out of credit, drawn with the shipped wording. The same pill in four stages, each a title bar with the toolbar's own spacing. The proposed treatments are flagged in each bar.
Add credit to run new analysis or coding.
Same glyph at every stage, and for every condition of the same kind. Only the words drop away:
The traffic-light question, at toolbar scale. The collapsed pill sits where .status puts it (the centre, on macOS 26), with the window controls at the leading edge:
| Idea | HIG | Reads as |
|---|---|---|
| Loud once, quiet after | Feedback: match the significance of the information to the way it’s delivered; “display status information in a passive way so that people can view it when they need it”. | Supports a passive pill. Prominence belongs to the first occurrence and in place, not to a permanent loud chrome. |
| Where the first telling goes | Writing: show an error “as close to the problem as possible”, avoid blame. Alerts: avoid an alert for information that isn’t actionable; Mail’s answer to an unavailable server is “an indicator that people can choose to learn more”. | Stage 1 is the in-place note plus the full-label pill. Not an alert, not a modal. An alert only if a researcher-started action was blocked outright. |
| Popover as the resolve surface | Popovers: “Avoid using a popover to show a warning. People can miss a popover or accidentally close it.” A Close button only “for confirmation and guidance”. | A mismatch with what ships. The out-of-credit popover carries a status and two actions, not a data-loss warning, which is the HIG’s case for an alert. Fine if the first telling is in place (stage 1); not fine if the popover is the only telling. |
| Expanding a condensed view | Popovers: for “condensed and expanded views of the same information”, animate the change so it doesn’t read as a new popover. | Stages 3 → 4 is that case. Animate it, and honour Reduce Motion. |
| A day, as a timer | Accessibility: “Minimize use of time-boxed interface elements… Prefer dismissing views with an explicit action.” Offering help: tips at “once every 24 hours” (a cadence, not a collapse). | A live timer changing the toolbar while someone looks at it is the thing to avoid. Evaluate the 24 hours at launch and on window activation, never mid-glance. Nothing is removed by collapsing, so the content stays one click away. |
| Glyph-only | Buttons: macOS shows a tooltip on hover; tooltips 60–75 characters, “context-sensitive… different text for a control’s different states”. | The collapsed pill needs a state-specific tooltip; its accessibility label stays the full text. The glyph is the kind’s own and doesn’t change on collapse, so a collapsed pill is recognisably yesterday’s. |
| Same colour, different meaning | Color: “Avoid using the same color to mean different things.” | An amber or red disc in the title bar echoes the minimise and close controls, which is why the lamp works in a Settings list and not here. A triangle isn’t a disc. The red error glyph is a disc, but inside a capsule it isn’t the bare window control; check it at real size. |
| Don’t look like a count | Notifications: “Avoid creating a custom image or component that mimics the appearance or behavior of a badge.” | The coloured dot sits on the glyph; no number, ever. |
| Reachable when hidden | Toolbars: “Make every toolbar item available as a command in the menu bar… people can customize or hide” the toolbar. Reduce tinted toolbar controls. | The HIG-conformant answer is a menu twin so the condition can’t be lost to a hidden toolbar or the » overflow. Declined by the owner (micro-interaction on a small flag); the cost is accepted, see the decision below. The stage-1 tint is the one place this runs against the grain. |
| Footer, option B | Sidebars: “Avoid putting critical information or actions at the bottom of a sidebar. People often relocate a window in a way that hides its bottom edge.” | Counts against B directly. |
» overflow can’t lose the condition. That cost is accepted: the Settings dot still says it, and a new refused call or a changed condition brings the pill back. The VoiceOver “Hide” action on the main button stays, so VoiceOver users don’t need to find the ✕. The collapsed glyph is bare, with no capsule, and has no ✕ of its own yet..status zone would also move the glyph and its neighbours. Two layouts are in the native prototype for comparison: slot on the right only (lopsided at rest) and slot balanced on both sides (about 20 pt wider). Two crosses in the error state are accepted (“just unfortunate”: there is no stronger red failure mark and no better-understood close mark). With hover-reveal they are on screen together only while hovering or focused.StatusPill is one Button that toggles the popover, so a nested ✕ needs its own target beside it. Right-click “Dismiss” isn’t available on a toolbar item (macOS swallows it for Customize Toolbar; already a gotcha in desktop/CLAUDE.md). So the accessible dismiss is the Hide button in the popover (fourth frame), with ✕ as the pointer shortcut. A hover-only ✕ would be unreachable by keyboard and VoiceOver.The rule from before: the pill needs a diagnosis, the diagnosis needs measurements, and the sentence’s subject follows the diagnosis. Measured on this Mac on 2 Nov 2026 with a read-only probe (URLSession, Network framework), healthy network only. Everything not marked measured is expected behaviour and untested.
| Signal | How | What it tells apart | Measured here | Limits |
|---|---|---|---|---|
| Network path | NWPathMonitor: status, supportsDNS, interfaces, expensive/constrained. No traffic, always on. | Offline vs “some route”. | satisfied · dns true · en0/wifi | “Satisfied” means a route exists, not that the internet answers (router up, WAN down still reads satisfied). The offline and Wi-Fi-off states are unmeasured. |
| VPN interface | The same path’s interface list (utun, “other”). | Whether a VPN is up; a dropped one. | none present | No VPN to test with. A dropped VPN looks like offline or a block, so it falls through to “can’t tell”. |
| System proxy | CFNetworkCopySystemProxySettings | A managed network (proxy or PAC configured). | empty | Says what the network is likely to do, not what it did. |
| URLError code | The failed call itself. | DNS vs refused vs timeout vs TLS. | −1003 DNS · −1004 refused · −1001 timeout (4.0 s, blackhole address) · −1202 untrusted and expired certificate | A black-holed route and a slow provider are both −1001. Expired and untrusted certificates are the same code. −1009 and −1005 (offline, connection lost) unmeasured. |
| “Reached the provider” signature | An unauthenticated GET to the provider’s models endpoint; no key and no user data is sent. | Reached the provider vs reached something else (a proxy block page). | Anthropic: 401, application/json, headers request-id server cf-ray, body {"type":"error","error":{"type":"authentication_error"…, 140 ms | Proves reach, says nothing about the key (401 either way). Only Anthropic measured; ChatGPT, Gemini and Azure signatures unmeasured. A proxy block page is unmeasured. |
| Control probe | One HTTPS GET to Apple’s captive-portal check, only after a failure. | Internet works, versus captive portal (a login page instead), versus nothing. | captive.apple.com and www.apple.com/library/test/success.html: 200, body “Success”, 38–48 ms, over HTTPS | One request to Apple, carrying an IP address and nothing else. Over HTTPS it can’t see a captive portal: a portal can’t present a valid certificate, so the reply arrives as a TLS error, the same as a wrong clock or an inspecting proxy. Apple’s own check is plain HTTP, which the app’s default transport rules block; the project has no exception for it. |
| DNS answers | Resolve the provider host and the control host, compare. | Sinkholed (null or private address) vs no DNS at all. | not measured | Worth adding; a filtering DNS usually answers with an address that isn’t the provider’s. |
| Provider status and error type | HTTP status and the JSON error.type. | Key rejected, credit, rate limit, outage. | 401 / authentication_error only | 402, 429 and 5xx/529 are unmeasured here. Today a failed run is classified by matching stderr, which turns every network failure into one category. |
| History | When the provider last answered (every LLM call is already logged). | A blip vs a standing block. | not measured | “Worked until 14:20” is also better copy than a bare state. |
| Task metrics (phase times) | URLSessionTaskMetrics | Where a request died (DNS, connect, TLS). | empty for all four failures, even with a wait | Dropped. It would have separated a slow provider from a black-holed route; it can’t. |
| # | Version | Recognised by | Pill? | Line 1 (bold) · line 2 (regular) |
|---|---|---|---|---|
| 1 | Offline | Path unsatisfied | No. In place only, if a call was refused. | No network connection.Try again when you’re back online.line 1 shipped phrase · line 2 proposed |
| 2 | Router up, internet down | Path satisfied; control probe fails (DNS or timeout) | No. In place only. | No internet connection.Check your network.proposed |
| 3 | Captive portal only on evidence | Control probe over HTTP answers, but not with “Success” (login page or redirect). Needs a transport-rules exception for Apple’s host; without it this row can’t be told from row 4 and falls to 13 | No. In place only. | This network needs you to sign in.Open a web page to sign in, then try again.proposed |
| 4 | Wrong date or time on the Mac | Control probe and provider both fail TLS (−1202) | No. In place only. | Your Mac’s date or time looks wrong.Set it automatically in System Settings.proposed; a certificate check fails for everything |
| 5 | Network blocks the provider (filtered DNS, firewall) | Control OK; provider DNS fails or resolves to a null/private address, or connection refused/reset, or no answer | Yes, warning | This network can’t reach Claude.Try a different network.proposed |
| 6 | Network inspects secure traffic | Control OK; provider fails TLS (−1202 or −1200) | Yes, warning | This network can’t make a secure connection to Claude.Try a different network.proposed |
| 7 | A proxy answers instead of the provider | Control OK; HTTP reply that isn’t the provider’s (HTML, 403/407/502, no request-id) | Yes, warning | This network is blocking Claude.Try a different network.proposed |
| 8 | Provider outage | Provider signature present, status 5xx or overloaded | In place; pill only if it persists (your call) | Claude is unavailable.Try again shortly.shipped sentence, split in two |
| 9 | Rate limited | Provider signature, 429 | No. In place only. | Claude rate limit reached.Try again soon.shipped sentence, split in two |
| 10 | No answer, nothing else known | Control OK; timeout (−1001) | No, until it repeats (then 5) | Claude didn’t respond.Try again shortly.proposed; the code can’t tell this from a black-holed route |
| 11 | Key rejected | Provider signature, 401/403, authentication_error on a call that sent a key | Yes, error | Your Claude key was rejected.Add a new key in Settings.line 1 shipped · line 2 proposed |
| 12 | Out of credit | Provider signature, 402 or the credit-balance body | Yes, warning | Claude is out of creditAdd credit to run new analysis or coding.shipped |
| 13 | Can’t tell | Anything not matched above | No. In place only. | Couldn’t reach Claude.Try again in a moment.proposed |
Reading it. The pill is for 5, 6, 7, 11 and 12, plus 8 if you want a persistent outage shown. Rows 5–7 are three ways to detect one condition and say the same words, so they differ only in the details the log keeps. Rows 1–4, 9, 10 and 13 are about the person’s own situation, a transient limit or an unknown, so they are said where it happened and never become a pill. A VPN that dropped can’t be told from rows 2 or 5, so it lands in 13 and says nothing it can’t support.
Remedies assume nothing about who the person is. No line says “ask your administrator”: this could be a café, a plane or a hotel, and for them a different network or a different provider is the only lever. Where another provider is known to work, the pull-down (“Switch to…”) is the action under line 2. For someone who does have an administrator, the popover can still offer a quiet Copy details, as the run-failure popover already does.
Not yet measured, and how to get it. Rows 1, 2 and 3 need the real states: Wi-Fi off, a router with the WAN unplugged, a captive-portal network. Rows 4–7 can be induced locally: a wrong clock (revert it after), a blocking rule in a firewall tool or a null address in the hosts file for the provider, and a local proxy that re-signs TLS or returns a 403 page. Rows 8 and 9 need a stub that answers 529 and 429 with the provider’s JSON. For each, record the URLError code, the HTTP status and headers, the control-probe result and the path state. I can write that as one script.
Two lines say the first layer that fails. Under them, one click down, the layers: what passes, what fails, and what was never checked because something beneath it failed. Each frame says whether it earns a pill. The same message kinds as everywhere else (✓ passes, ⚠ or ✗ fails, — not checked).
Try again when you’re back online.
Check your network.
Open a web page to sign in, then try again.
Try a different network.
Try again shortly.
Add a new key in Settings.
Add credit to run new analysis or coding.
| Rung | What measures it | Measured here | Honest limit | Deliverable? |
|---|---|---|---|---|
| Connected to network | Network path status and interfaces | satisfied · en0/wifi | Says a link exists. Unsatisfied is measured nowhere yet (Wi-Fi off needs a real toggle). | Yes. Core of the API; the offline state itself is untested. |
| Internet (not “Router and internet”) | The control probe answers at all; DNS resolves a neutral host | answers; DNS ok | The router can’t be shown separately. A sandboxed app has no ping or traceroute, so “router fine, ISP down” and “router down” both read as one rung. A gateway check (connect to the router on a port it answers) might split them; unmeasured, and I wouldn’t promise it. | Internet: yes. Router: no. Draw one rung, and don’t claim the router is fine or broken. |
| Claude reachable | The keyless request to the provider returns its JSON error signature | 401 + request-id, 140 ms | Proves a path to the provider, not that the key works. Anthropic only; the others are unmeasured. | Yes for Claude. Each other provider needs its own signature measured; Azure’s host is the user’s own, and Ollama is local. |
| Claude responding | A normal reply in the provider’s own format. A 401, 402 or 429 with its JSON body is a normal reply; a 5xx, an overloaded error or no reply is not. | 401 JSON reply (no key) | 5xx and overloaded replies are unmeasured. Fails only when Claude itself is failing, which is why it is separate from the next rung. | Yes, from the refused call’s own status. No extra probe. |
| Your account | Whether Claude accepted who is asking: key valid, credit left, not rate-limited. | 401 only (no key sent) | 402, 429 and a valid-key call are unmeasured. Credit exhaustion shows only on a billed call, so it comes from the failure and is never probed. Claude answered in all of these; it said no. | Yes, from the refused call. Not checked when a rung beneath it failed. |
The owner’s layout, reading from the right edge: Search, then the actions group, then the status pill(s) on their own. Drawn here only to agree the order; whether it holds is a native question (see the commentary underneath). Nothing in the toolbar code has been changed.
No shared glass on 15: items are plain buttons in a row, so there is no capsule to fuse into and no spacer type to separate them.
| Question | Answer | Source |
|---|---|---|
Where does .status put an item on macOS? | The centre of the toolbar. Informational, “not an action”. It folds into » like anything else. | Apple DocC ToolbarItemPlacement.status; measured 3 Oct 2026 (project notes) |
Where does .primaryAction put one? | Apple’s page says the leading edge on macOS. The app uses it for the trailing actions and that is what is measured on 26. This disagrees with the docs and I have not resolved it. I would not rely on either text for 15; it needs looking at on a Sequoia guest. | DocC primaryAction vs desktop/CLAUDE.md (Toolbar) |
| Can a SwiftUI item say “fold me last”? | No. visibilityPriority exists on NSToolbarItem only; the app sets it through the ToolbarItemPriority probe, re-applied on resize and every rebuild. Search already uses it (.user). | desktop/CLAUDE.md gotcha; QuotesToolbarControls.swift |
| Separating the pill from the actions capsule | On 26, ToolbarSpacer(.fixed) between them (the app already does this before Search, searchSeparator), plus withoutSharedBackground() on the pill so the glass does not draw a second capsule round it. On 15 the spacer type does not exist and the question does not arise. | DocC ToolbarSpacer (26.0+); house helper in StatusPill.swift |
| HIG limits that bear on this | Aim for at most three groups (pill, actions, search is exactly three); trailing edge “remain visible at all window sizes”; the system adds the overflow menu, do not add one; avoid layouts that overflow by default. | HIG Toolbars |
| A pill that hides itself | When there is no problem the item is empty. An empty ToolbarItem can still leave a gap or a stray capsule, so “only there when there is a problem” may need the item to be added and removed from the builder rather than draw nothing. That is a toolbar rebuild, and a rebuild is what loses the priority probe. | Not measured. Reasoned from the rebuild gotcha |
| Does the pill fuse with the actions capsule? | Unknown. Adjacent items share a glass capsule unless split; whether a hidden-background item next to a capsule stays out of it, I have not seen. | Not measured |
| Will the pill survive a narrow window? | Not in the draw above: it folds first-from-the-right, so it would outlast Search and the actions only by sitting left of them, which is the opposite of “important enough to keep”. If it should outlast them, it needs a priority above theirs; if it should go first, it is already right. | Reasoned; my read of the fold rule |
What I have not done. No native measurement: I could not open a window from this shell, and the guests were not up. Nothing here has been seen on a Sequoia or a Tahoe toolbar. Before any build I would want, on the 15.7.3 and 26.6.2 guests and on 27: (1) where .primaryAction and .status land, (2) the order items fold at 940 pt and below, (3) the empty-pill gap, (4) fuse or no fuse beside the actions capsule.
Decision for you. The centre (today) is safe and measured, but it is the place that folds into » and sits under the title. The trailing slot is where you want it and is the more fragile one. A DEBUG switch in Diagnostics that moves only the pill between the two would let you judge it on this machine without changing the shipping toolbar.
Key rejected and out of credit are facts about the account, so they stay until the account changes. “This network can’t reach Claude” is a fact about where you are: it is true in the café and false at home. This section draws that difference. It adds no new control; the pill, popover, ✕ and bare glyph are the ones in A and D.
Try a different network.
No pill, no “resolved” message. The network path changed, so the old finding is not evidence about this one. Nothing is asked of you.
Same: a successful call removes it, whether or not the network changed. The ✕ is not needed to get rid of it, only to hide it sooner.
The key is fine and stays green. A network block says nothing about the key, so it is never written to the key’s saved verdict.
Ollama is on this Mac, so the network is not the cause. It says what it says today (“Ollama isn’t running…”, llmSettings.validation.ollamaUnreachable), in place.
Couldn’t reach Azure OpenAI. Check the endpoint in Settings. Apple’s host answered, and a made-up Azure name fails the same way (measured), so a mistyped endpoint is likelier than a block. In place only.
| Question | Proposal | Why |
|---|---|---|
| What lights it | A refused call, then one HTTPS check against Apple’s connectivity address: it answers, Claude didn’t, and the failed call died in connect, TLS or timeout, or came back from something that wasn’t Claude. | Rows 5–7 of E share these words, so the pill never says which of the three it was; the details keep that. |
| What clears it | A change of network path, or any successful call to that provider. Not a timer. | A timer is a guess about a place; a path change is the Mac saying the place changed. Matches D’s rule against a clock changing the toolbar under a glance. |
| Where it is kept | In memory only. Gone on quit. | It would hold where you were. It also must not become a key verdict: Settings would show a good key as bad, and the saved record would follow you home. |
| Whose refusal | Only the provider you currently use. Switch provider and it goes. | Otherwise the pill names a product you are no longer using. |
| Never for | Ollama; an Azure endpoint that doesn’t resolve; a refusal that carried the provider’s own reply (that is key, credit or rate limit, and says so). | None of those is “this network”. Saying so would be wrong, which is worse than saying nothing. |
| Behind a proxy that answers instead of Claude | Lights this pill, not “key rejected”. | A block page’s 403 reads as a rejected key today (measured on the stub). That is a fix to the classifier, not to the pill. |
| Already decided, reused as is | Warning glyph and flat envelope (A); ✕ on hover, hides it, and a new refused call or a changed condition brings it back (D); the bare glyph after a day; no menu twin. | Owner, 2 Nov 2026. |
| Not drawn | The ladder behind “Connection details”; the sign-in (captive portal) line. | Both deferred in the build plan. F still shows what they would look like. |
Where this could be wrong. A managed Mac with an inspecting proxy whose root certificate is installed passes the Apple check and may pass Claude’s TLS in the system’s store while the app’s own Python connection (which uses its own bundle of roots) fails. Then the pill must come from the failed call’s own TLS error, not from the check, or it will not light at all. The wording is the same; only what triggers it differs. Nothing here has been seen on a real hostile network: every state in the list above is expected, not measured.
Decision for you. Is “clears when the network changes, or a call works” the right persistence for this pill, with key and credit unchanged (they stay until the account is fixed)? And is the in-place Azure line acceptable as proposed wording?
| A · title-bar pill | B · footer out | C · consequence in place | |
|---|---|---|---|
| Hand-tagger, dead key for months | Nags if verdict-triggered; fine if blocked-call-triggered (A′) | Quiet, correct volume | Silent until needed; incidental calls need C′ |
| About to analyse | Seen before they start | Seen if the sidebar is open | Seen only after the run fails |
| Many projects | One fix clears all | One fix clears all | One failure per project |
| Reachable with sidebar hidden | Yes | No | Yes, in place |
| New engineering | Verdict for invalid (small); none for unreachable | New view in a deliberately plain footer | A note on each non-run surface |
| Strings proposed | 2 pill labels, 1 button | 3 status phrases | 1–2 (per surface) |
Decided (owner, 2 Nov 2026): the pill route, as subtle as possible. The footer is out. In-place consequence (C′) stays as the first telling, since a refused call has no row to land on. The pill is the one global surface, and it is for the sticky conditions (key rejected, out of credit) and the network-blocks-the-provider case; everything else is said in place. “Subtle” I read as: the flat envelope with no glass of its own, kind glyph and short label, one tinted moment on first sight, then the ✕ and the glyph-only form. If you meant subtler even than that first sighting, the tint in stage 1 is the thing to take out.
The open call is the persistence rule for the pill that already ships: should out-of-credit also stop showing on a stored verdict alone?
| Text | Status |
|---|---|
| Claude · no credit, Claude is out of credit, Add credit to run new analysis or coding., Add funds…, Switch provider… | shipped (desktop.outOfCredit.*) |
| Your Claude key was rejected., Couldn’t reach Claude. Check your connection., Run failed, Category: … | shipped (pipeline.failure.*, pipeline.diagnostic.*) |
| Claude · invalid key, Claude · unavailable | proposed — built from llmSettings.status.invalid / unavailable |
| Open Settings… | proposed |
| Claude key rejected, Claude unavailable (footer) | proposed |
| Not updated — Claude key rejected | proposed, wording not drafted |
| Couldn’t reach Azure OpenAI. Check the endpoint in Settings. | proposed (section H), in place only |
| Network · can’t reach Claude, This network can’t reach Claude., Try a different network. | proposed (A, E); the pill’s clearing rule is section H |