Skip to content

feat: rotating composer hints and time-of-day greetings - #1096

Merged
philmerrell merged 2 commits into
developfrom
feature/composer-hints-and-greetings
Sep 14, 2026
Merged

philmerrell merged 2 commits into
developfrom
feature/composer-hints-and-greetings

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

Two small additions to the empty-chat surface: the composer teaches the two shortcuts nothing else advertises, and the greeting above it knows what time it is.

Rotating discovery hints in the composer

@ and / only reveal themselves once you have already typed the character that opens their menu. The empty composer is where a user looks when they do not yet know what to type, so the hint goes there — cycling How can I help you today? → Type @ to hand this turn to one of your agents → Type / to run one of your skills, cross-faded with Angular 21's animate.enter / animate.leave.

Three rules keep it from being the kind of animation people file bugs about:

  • It is decoration, not information. The native placeholder attribute never rotates, so assistive tech reads one stable string; the visible line is an aria-hidden overlay painted over a placeholder that is transparent but still present. A placeholder that re-announced itself every few seconds would be a screen-reader defect, not a feature.
  • It stops. Three passes (~40s), then it comes to rest on the idle line, and the first keystroke settles it on the spot. Nothing auto-updates indefinitely, so WCAG 2.2.2 asks for no pause control we would then have to fit into the composer's chrome. It restarts on a brand-new conversation — opening an existing one deliberately does not, which would turn a hint into a tic.
  • It honours prefers-reduced-motion. Reduce means no rotation at all, not the same rotation with the fade taken off.

Two details worth a reviewer's eye:

  • The rotation comes to rest in the overlay rather than unmounting back to the native placeholder. Unmounting would exit-animate a copy of the idle line straight off the placeholder underneath, which spells the same words — a ghost double-image on the one transition every user sees.
  • Hints are gated on the surface actually having something to offer (showAgentMentions and a non-empty mentionable list; showSkillCommands and at least one enabled skill), so nobody is told to type a character that opens an empty menu.

Time-of-day greetings

Twenty new greetings, bucketed on the viewer's own clock, each with a no-name fallback for signed-out users:

Bucket Hours Examples
morning 05:00–11:59 "Bright and early, {name}." · "Coffee's on. What are we working on, {name}?"
afternoon 12:00–16:59 "Back at it, {name}?" · "What's next on the list, {name}?"
evening 17:00–21:59 "One more thing before you log off, {name}?"
night 22:00–04:59 "Burning the midnight oil, {name}?" · "The quiet hours, {name}. What are we tackling?"

They pool with the existing five rather than replacing them, so a morning visitor draws from morning ∪ any-time: about half of what anyone sees knows what time it is, half is the familiar copy. The clock is the browser's, read once at resolve time, so "Good evening" means evening where the person actually is and the heading does not re-write itself out from under someone mid-conversation.

DEFAULT_GREETING_TEMPLATES / DEFAULT_FALLBACK_GREETINGS are untouched — brand.defaults.golden.spec.ts pins them verbatim as the pre-branding-refactor greetings, and that guard is worth more than tidiness. The new copy lives in its own constants.

Still rebrandable

Greetings are Brand_Config territory, so timeOfDayGreetings / timeOfDayFallbackGreetings are new optional fields on BrandConfig:

  • Omit either object, or any single bucket, and the built-in defaults apply for what you left out — silently, since omitting is the normal case.
  • An explicitly empty bucket (night: []) is honoured as "stay quiet at that hour". This is the one place these differ from the flat lists, where an empty array means "you gave me nothing usable, have the defaults" — without it a rebrand would have no way to turn a bucket off.
  • Each bucket normalizes independently, so one bad hour never costs a rebrand the other three.

Documented in src/branding/README.md with the hours table, and the worked example config now shows a gym that greets by session time and says nothing at 3am.

Caveat for reviewers

Because the fields are optional and pooled, a rebrand that edits only greetingTemplates will still show stock time-of-day lines about half the time. That is called out in the guide and in a comment directly above the field in brand.config.ts, but it is a real behaviour change for a forker reading neither.

Testing

  • Full SPA suite green: 2938 tests / 242 files, including 34 new ones.
    • Composer: multi-pass rotation, rest-in-overlay handoff, stop-on-keystroke, restart on a new conversation, the accessible placeholder never changing, and yielding to the mid-stream steering placeholder.
    • Greetings: every hour of the clock lands in exactly one bucket (night is the wrap-around case a range-based implementation gets wrong), boundary hours, pool composition, the empty-bucket opt-out, per-bucket normalization, and a regression test for component specs that stub BrandingService by hand and predate the field.
  • npx tsc --noEmit clean.
  • Verified live against the dev backend at localhost:4200: the cross-fade measured with both nodes coexisting ~300ms (outgoing 1.00 → 0.43 → gone, incoming 0.00 → 0.45 → 1.00), rotation continuing into pass 2 and 3, and greetings resolved at 07:15 / 14:15 / 19:15 / 23:15 / 02:15.

🤖 Generated with Claude Code

philmerrell and others added 2 commits September 13, 2026 19:35
`@` and `/` are the two shortcuts nothing on the page advertises: each one
only reveals itself once you have already typed the character that opens
its menu. The empty composer is where a user looks when they do not yet
know what to type, so that is where the hint goes.

Three rules keep it from being the kind of animation people file bugs
about:

- It is decoration, not information. The native `placeholder` attribute
  never rotates, so assistive tech reads one stable string; the visible
  line is an `aria-hidden` overlay painted over a placeholder that is
  transparent but still there. A placeholder that re-announced itself
  every few seconds would be a screen-reader defect, not a feature.
- It stops. Three passes and it comes to rest on the idle line, and the
  first keystroke settles it on the spot. Nothing auto-updates
  indefinitely, so WCAG 2.2.2 asks for no pause control we would then
  have to fit into the composer's chrome.
- It honours `prefers-reduced-motion`. Reduce means no rotation at all,
  not the same rotation with the fade taken off.

The rotation comes to rest *in the overlay* rather than unmounting back
to the native placeholder: unmounting would exit-animate a copy of the
idle line straight off the placeholder underneath, which spells the same
words — a ghost double-image on the one transition every user sees.

Hints are offered only for surfaces this composer actually has, so an
environment with Agents switched off, or a user with no skills enabled,
is never told to type a character that opens an empty menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds twenty greetings bucketed into morning (05:00-11:59), afternoon
(12:00-16:59), evening (17:00-21:59) and night (22:00-04:59), each with a
no-name fallback for signed-out users.

They pool *with* the existing five rather than replacing them, so a
morning visitor draws from morning plus the any-time list: about half of
what anyone sees knows what time it is, and half is the familiar copy.
Replacing instead of pooling would leave the app with exactly one thing
to say each morning.

The clock is the viewer's own, read once at resolve time, so "Good
evening" means evening where the person actually is and the heading does
not re-write itself out from under someone mid-conversation.

`DEFAULT_GREETING_TEMPLATES` and `DEFAULT_FALLBACK_GREETINGS` are left
untouched: `brand.defaults.golden.spec.ts` pins them verbatim as the
pre-branding-refactor greetings, and that guard is worth more than
tidiness. The new copy lives in its own constants.

Greetings are Brand_Config territory, so this stays rebrandable.
`timeOfDayGreetings` / `timeOfDayFallbackGreetings` are optional: omit
either, or any single bucket, and the defaults apply. An explicitly empty
bucket is honoured as "stay quiet at that hour" — the one place these
differ from the flat lists, where empty means "have the defaults" — and
each bucket normalizes independently, so one bad hour never costs a
rebrand the other three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 4e97f37 into develop Sep 14, 2026
6 checks passed
@philmerrell
philmerrell deleted the feature/composer-hints-and-greetings branch September 14, 2026 01:55
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.

1 participant