The signals of the day held today's newest single reading against the
7- and 30-day mean of every reading. For glucose, whose level climbs
with each meal, a fasting morning read as low against an all-day mean
and fed that into the briefing and status prompts. A type in
SAME_HOURS_TYPES now compares today's mean so far with the earlier
days cut at the local time of today's latest reading, the basis the
coincident-deviation flag and the metric strip already use. Below a
week of like-for-like days there is no signal. The signal carries
basis "sameHours" and both briefing prompts say what it means. Other
metrics keep the reading-against-window comparison.
The usual-range strip called the newest reading today's on every
metric page, even when it was days old. CoachReadBaseline now carries
latestDate (the reading's day in the account's zone) and latestIsToday
(isFromToday), on the REST response and the get_metric_baseline MCP
output. The strip names the date when the reading is not from today,
in all seven locales.
The usual-range strip on a metric page placed the latest single reading
against a band built from whole-day means. For glucose the day mean climbs
with every meal, so at nine in the morning a fasting reading sat under a
band of whole days and the strip said "below your range" on an ordinary
morning; the MCP baseline read reported the same.
Glucose is now placed by its day mean. A day still in progress, in the
reader's own zone, is held against the earlier days cut at the local time
of today's latest reading, the comparison the health status already makes;
with fewer than seven earlier days that have readings in those hours there
is no verdict. The baseline carries `basis: "sameHours"` on the coach-read
route and the MCP read, and the strip names the range as the one for this
time of day, in all seven locales. Other metrics keep their placement.
Add the shared contract the reasoning release builds on, with no change
to how a turn runs today.
- Schema: operator reasoning controls on app_settings (0376), the
encrypted trail text on coach_messages (0377), and source,
last_used_at and source_message_id on coach_facts (0378).
- Provider contract: CompletionParams.reasoning and onReasoning,
CompletionResult.reasoning and providerState, AiMessage.providerState.
- Reasoning levels, thinking budgets and the background-job table in
src/lib/ai/reasoning/levels.ts; coachPrefsJson.reasoning, absent
meaning medium, salvaged field by field like every other pref.
- Stream: activity, memoryNote and planProposal frames, done.stop and
done.withheldResults, result.interim, a compare chart kind, eight
table refs, three more clarification kinds with an assumed choice,
the change_assumption chip, and assumptions, activity, stop,
memoryNote and planProposal on the provenance. Zod mirror, type
parity and the native-client key rule cover every new frame.
- Chat request: memoryDecision and planDecision, parsed and not yet
acted on.
- Stub contracts for the memory functions and the activity recorder,
inert until their implementations land.
- Backup, restore and rotation carry the new columns; a fact's message
reference is resolved against the restored messages like its
conversation reference.
- Message keys for the trail, memory, plans, assumptions, replies,
interim tables, comparison charts and reasoning settings in all seven
locales, each reachable from a catalog and checked both ways.
The panel groups conversations by the browser's local day, so the e2e
fixture's "an hour ago" landed in yesterday when CI ran just after
midnight. The turn-steps test reads its markup by splitting rather than
with a replace CodeQL reads as an incomplete sanitiser.
Below md the lines under the 80 px dial (the score delta, how long it
has held, what it rests on) were capped at the dial's width. In German
they broke into three to five rows each, and beside a short lead that
column set the height of the read row and left a gap above the facts.
The score column now dissolves into the grid below md: the dial stays
the top-right cell beside the lead, and its meta lines take a
full-width row right after the read row, flush with the dial's edge,
one line each. The facts move one row down. From md up the column is
the same flex column as before (desktop captures are pixel-identical).
The Today overview e2e renders German at 390 and 360 px with a short
and a long lead and asserts the dial size, one row per meta line, the
lines flush with the dial, the meta row right after the read row, and
exactly one row gap above the facts.
Below md the score ring sat centred under the Today facts as a 168 px
dial, where the bottom navigation covered it. The hero is now one
grid with two arrangements: on a phone the ring is a compact 80 px
dial at the top right beside the lead and signal line, flush with the
card's trailing edge, and the Today facts run the full card width
under both; from md up the ring spans both rows of the trailing
column exactly as before (desktop captures are pixel-identical).
Without a lead the facts take the leading cell, so the ring never
sits beside an empty row.
ScoreRing gains a `responsive` size (80 px, md:168 px) whose label is
left out below md; the ring keeps its aria-label and the surrounding
link names it. The meta lines under the phone dial are capped to its
width, so the lead keeps at least 180 px beside it at 360 px.
Each fact now stacks its muted label above its foreground value on
the reading edge at every width, instead of a label-left /
value-right row that broke long values into a ragged right-aligned
block. Rows stay at the 44 px tap floor below md.
The Today overview e2e covers 1440, 390 and 360 px in both themes
for an AI lead, a long AI lead, an AI-off day and a quiet day, and
asserts the dial size, its place beside the lead, symmetric insets,
full-width facts and stacked 44 px rows on a phone.
Inside a chat turn every tool read lands on the turn's shared full-source
snapshot when its window matches. On an account with many synced series
that build passes the prompt budget, and the budget pass collapses blocks
to a bare unit and then to an `{ omitted }` marker. get_metric_series
sliced the marker out and answered `present: true` with it, so the model
was told the readings exist and was given no figure. The same held for
sleep, glucose, compliance and workouts.
- The snapshot result names the blocks the budget pass cut. A tool whose
block was cut in the shared build reads it again from its own source at
the same window.
- A read scoped to one source condenses its own blocks last and never
swaps them for the marker: it keeps a summary (first, last, min, max,
mean, change) taken before anything is cut, the newest daily values,
weekly means folded into monthly means, and names each cut.
- get_metric_series never answers present with a block holding no figure;
that read reports retrieval_failed instead.
- A chat series read carries the readings it counted, so the step shows
them, and on a long or condensed window a note on what it covers. The
tool description and a new tool-mode rule tell the model to call
get_metric_table itself for longer history instead of asking.
- The screen-reader comma between step parts is unselectable, so a copied
row no longer reads "Weight · ,all time".
MCP output is unchanged: the source-snapshot equality suite stays green.
Every text-entry control (Input, Textarea, Select and NativeSelect
triggers, the date and time fields, the calendar's dropdowns, the Coach
composer and the hand-built note fields) now marks focus by turning its
border the new neutral input-focus tone, about 5:1 on the dark
background and 4:1 on the light one, with no ring. A text field matches
:focus-visible on every mouse click, so the ring was on for everyone.
An invalid field keeps its red border and draws its destructive ring
only while focused. Buttons, links, checkboxes and switches keep their
rings.
text-field-focus-guard.test.ts fails when a text-field class string
brings a focus ring or a primary/ring-coloured focus border back.
A standalone PWA on a tablet reserves the status bar in the top bar but
not in the docked panel's header row, which could tuck its title and
icons under the status bar; the new-chat button sat without the home
indicator inset. Both now reserve the same insets as the top bar.
The docked panel was part of the page, below the top bar. It is now a
column of the shell beside the content column, portalled through a new
side-panel slot: it runs from the top of the window to the bottom, flush
right, and the top bar ends at its left edge. Shut, the panel is gone
entirely and the top bar runs to the window's edge. The panel's header row
is the top bar's band, so the two bottom borders draw one line; the panel
takes the sidebar surface and border, like the rail on the left.
The top bar says where the reader is on desktop: "Coach", then the open
conversation's title, truncated. The panel toggle is its last item, 12 px
from its trailing edge, so it stands against the open panel and at the
window's edge when the panel is shut; it carries a tooltip and is 28 px
beside a fine pointer, 44 px with touch.
New chat is a round 48 px primary button with a plus, 24 px in from the
panel's corner (16 px in the phone sheet), with a tooltip. The top bar's
action slot and the new context and side-panel slots share one portal
helper.
The condition moves out of the shell into isFullBleedPage(), a pure
helper with its own test: /coach renders edge to edge; /coach/plans,
/coach/conversations, a trailing slash, look-alike paths and other
routes keep the padded frame, and so does a refusal on /coach (outside a
shared record, module off).
Focus opens the details tooltip, and the click that Enter or Space then
fires toggled it shut again. A click with no pointer-down before it is a
keyboard activation and now always opens; a tap or a mouse click still
toggles, and Escape closes it.
The flag helper sat between the module gate's doc comment and the
component, so the comment read as its own. It is now
switchedOffModule(), which returns the owning module when it is off and
has a doc comment of its own above the gate's; the gate narrows on its
result directly instead of looking the module up twice.
The panel header buttons shrank to 28 px from md and the conversation rows
to 36 px from sm, so a touch tablet wide enough to dock the panel got
pointer-sized targets. They now shrink only under pointer-fine: 28 px
header buttons, a 36 px gear in the drawer, 36 px rows and row menus
beside a mouse or trackpad, the 44 px floor with touch at any width.
A unit test refuses a width breakpoint on a control size in the three
panel files; an e2e case measures the docked panel at 1280 px with touch
and with a fine pointer.
The tokens and model no longer sit as right-aligned text under a Coach
answer. The row is one left-aligned group: copy, read aloud, try again, a
details icon, then the time. The details icon is a focusable button whose
tooltip names the model and the token count; it opens on hover, on
keyboard focus and on a tap, and a second tap, a tap elsewhere or Escape
closes it. Strings in all seven locales; the joined tokens-with-model
string is gone.
The toggle shows the panel-right open glyph while the panel is open and
the close glyph while it is shut, both mirrored so the chevron points the
way the panel moves on a click. Docked and open, the toggle keeps the
panel's width as its right margin, so it sits beside the panel's leading
edge and travels with it (200 ms, none under reduced motion). Labels and
aria-expanded are unchanged.
The Coach page cancelled the shell's container padding with negative
margins, so <main> kept its reserved scrollbar gutter and painted a dead
strip to the right of the panel on classic-scrollbar platforms. The shell
now renders /coach full bleed: no centred container, no padding and no
reserved gutter, and the page fills what <main> leaves below the top bar.
A refusal card (shared record, module off) keeps the padded frame.
The panel header takes one 12 px inset with a gap of 8 between title and
icons, a text-lg title, and 28 px icon buttons beside a pointer (44 px on
touch). Conversation rows take the control radius and a 36 px pointer
floor.
The same-hours comparison cut the earlier days at the current clock time.
At two in the afternoon a day holding only its fasting reading was then
held against mornings plus lunch, and read as below its range again. The
earlier days are now cut at the local time of today's latest reading.
A range read this way is the usual range for this time of day, not the
whole-day band other surfaces show. The deviation carries a basis, the
health-status API publishes it as `basis` (`day` or `sameHours`), and the
Today lead and the health-status card name the range as the one for this
time of day, in all seven locales.
The chart's label map covered eleven measurement types and fell back to
the raw enum for the rest, so a resting-pulse chart with its trend
overlays on showed "RESTING_HEART_RATE" in the trend line, the legend,
the tooltip and the spoken summary. Types outside the chart's short
labels now take their measurement label, which exists for every type in
every locale.
The muted line under the lead repeated what the lead had just said: a lead
about blood pressure sat above "Your latest blood pressure is sitting in
the optimal band · ↓ ~10 mmHg…". The web client only dropped the headline
when the lead quoted it word for word, and other clients had no rule at
all. The digest now carries `signalLine`, built next to the lead: when the
lead already talks about the top signal's metric (matched in every shipped
language) or says its headline, only the delta remains, and with no delta
there is no line. The hero renders the field verbatim.
/coach?settings=data opened the conversations sheet from the first
render. The server and hydration renders do not know the viewport and
take the narrow layout, so on a desktop the sheet was open until the
docked panel replaced it; only the batching of the hydration re-render
kept it off the screen. The deep link now opens the sheet only once
hydrated on a viewport that does not dock the panel; on a docked one the
panel is revealed as before.
The desktop deep-link spec records every mount of the sheet from the
first byte and expects none; a phone deep-link spec covers the sheet
path and serves as the watcher's positive control.
The steady line compared the newest stored score with the number on
screen but not with its band, version or composition, so a score whose
band moved overnight at the same number was still called steady. A run
that reached back past the 120 days read was also told as if it started
there.
The newest stored day now has to match the ring in value, band and,
where the digest carries them, algorithm version and composition. A run
the read cannot see the start of is marked as open (steadyAtLeast on the
digest score) and the hero says "at least" in seven locales.
Jumping to the latest message pinned the thread first, which hides the
button behind aria-hidden, and only then moved focus to the composer.
Focus now leaves the button before its state changes.
The hero reads only digest.lead. A digest the service worker cached
before the update carries no such field, so the hero lost its lead line
until the next fetch. Such a digest now falls back to the reaction line
or the briefing lead it was built to show; a current digest whose lead
is null stays silent.
The provider and model selects in the Coach's settings already lock while
a write is in flight; the reasoning select did not, so a second PATCH
could race the first and one change could be lost.
Settings → Coach pointed at the gear with a muted paragraph. It now says
how many data areas the Coach reads and how far back it looks, from the
saved preferences, next to an Open Coach settings button that lands on
that section.
The facet triggers and the clear control were 32 px beside a 44/40 px
search field, below the phone tap floor. They now share its height and
reach 44 px square on a phone, and the browser's own clear button no
longer sits on the search icon when the field shrinks.
The overview sat 12 px under the lead sentence; it now keeps a section's
distance (16 px, 24 px from md). On a phone each fact row is a link and
now meets the 44 px tap floor.
The model fields carried the form label's colon and the lookback picker
an uppercase section-style label and a fit-content 36 px trigger. All four
fields now share one unadorned label and the full-width 44/40 px control.
The operator's shared Codex is appended to the resolved chain but is never
a chain entry, so the Coach's provider select had no option for it and
showed its first option as the one answering. The answering provider now
appears as a disabled option, with a note that the operator manages the
connection.
The provider-chain summary marked every entry available, so the Coach's
provider select offered entries without a key or sign-in. Choosing one
saved it to the front of the chain, the resolver skipped it, and the
select jumped back without a word.
Availability now comes from the presence probe, which names every chain
type whose credential is present. After a switch the picker reads the
chain back and says so when the server still answers with another
provider.
The vault's filter row gains a procedure facet beside the condition
facet. It lists the record's procedures and surgeries that hold at least
one live document, headed the way the visits page heads them (the
reason, else the practice, else the kind) with the body site and date
beneath, and narrows the list through the existing `encounterId` filter.
Same control, placement and clear behaviour as the condition facet; the
choice rides the URL as `?encounter=`, and an upload dropped into the
filtered view is filed against the procedure.
The usage read publishes the choices as `linkedProcedures` (additive),
decrypting only the label columns, and returns none when a sharing
grant does not cover the visits section.
On a phone an active facet now gives up label width before the search
field does, and the search keeps room for its icon and padding.
The saved Coach window only seeded the snapshot and each tool's default.
A tool could still read back to all time, the availability probe reported
counts and means for history outside the window, and several blocks read
fixed windows of their own (labs 12 months, correlations 180 days, the
coarse tail, the all-time extremes) whatever the setting said.
coachPrefs.defaultWindow is now enforced as a limit through one module,
history-reach.ts: the turn context clamps the client window, the snapshot
clamps its window and gives every fixed-window block either the shorter
window or, when the block's own window cannot fit, leaves it out. Every
retrieval tool clamps its window, an earlier period or a stored table that
reaches past the limit answers outside_reach, and the probe reads only
inside the limit and names older history as outside_reach without any
figure. Follow-up chips that would read past the limit are not offered.
The default (allTime) is no limit and reads as before. MCP clients pass no
limit and keep their own window arguments.
Adds the twelve-month option, rewrites the picker text to state what is
read with the numbers the readers use, shows the same options in the
managed-profile settings, counts cluster members with a singular, and
drops the opacity on switched-off rows that put their text below WCAG AA.
Under an answer: copy, read aloud, try again and the time as text on the
left; tokens and model on the right, on one line down to 390 px. The
thumbs leave the web (the feedback route stays for the native client),
and so does the "What I'm looking at" disclosure: the method line and
the tables the answer read move into the steps list, and an older
message without steps lists the areas it drew on, never a raw key.
Remember becomes a bookmark icon in the question's row and confirms in
place with a toast. Follow-up chips render inside the latest answer's
column at its text size. The answer column takes the full width below
`sm` and a fixed 80 % above, so nothing moves sideways as a reply
streams. A round button fades in when the thread is scrolled away from
the end and brings it back.
The hero led with the first sentence of the cached briefing, which was
often a greeting ("Good morning.") while the sentence with content was
never shown. Without an AI provider it fell back to "Your health score
today is N.", which the hero then removed because the ring already
shows the number, leaving a ring and "Nothing needs your attention".
The daily digest now publishes the lead resolved on the server and a
short list of facts about the day:
- lead: the reaction line or the first briefing sentence that is not a
greeting and does not repeat the score while AI text is available,
otherwise a deterministic sentence about the strongest signal (a vital
outside its personal range, a night an hour off the usual, vitals in
range, last night). A fact the lead was built from leaves the list.
- today: up to five statements in priority order (Rest Mode, doses taken
today, an appointment today or tomorrow that is not already on the
rail, last night's sleep against the usual, vitals against their
personal range, cycle day). Module-gated on the server, never padded,
never asking for anything. A phone shows the first four.
- restMode: passed through while an illness episode is active.
- score.steadyWeeks: how long the score has held, read off the stored
daily scores, shown under the ring in place of a delta.
briefingLead skips greetings and score sentences the same way, so the
morning push improves with it. Visit times are rendered in the profile
timezone and hour-cycle preference; the cycle day comes from the same
verdict the cycle page uses. The ring, the rail and the charts are
unchanged. OpenAPI regenerated; additive fields only.
The Coach page gets a conversations panel on the right of the thread. From
1280 px it docks beside the thread and slides open and shut; the reading
column keeps its width either way and only its centring moves. It is open
on a first visit and remembers the last choice per device. Below 1280 px it
is a sheet from the right. The toggle sits at the trailing edge of the top
bar, through a small page-owned slot the top bar now renders.
The panel lists every conversation grouped by recency, with a row menu to
rename in place (Enter saves, Escape cancels without closing the panel) and
delete with the existing undo toast. The undo window now survives the sheet
closing. New chat is the round button at the panel's bottom right, and
picking or starting a conversation keeps ?c= in the URL.
The gear in the panel header opens quick settings: a popover from md up, a
bottom sheet on a phone. It switches the answering provider (chain reordered
through the existing route), the model and reasoning level where the person
owns them, and shows what the Coach can see. Keys, the Codex sign-in and the
fallback chain stay in Settings > AI. The drawer's gear opens the same
overlay. The composer's + menu is gone.
Settings > Coach links to /coach?settings=data, which opens the overlay on
what the Coach can see instead of a rail that no longer exists.
Both grids carry an aria-label while their data loads, on a plain div,
which axe reports as aria-prohibited-attr: the label has no role to
name. A scan that lands while the scores strip is still loading fails on
it. The loading grids now take role=status, the role a loading region
has and which already implies the polite live region; the loaded vitals
grid carries neither.