Skip to content

feat(desktop): reveal contextual message timestamps on hover - #3681

Open
Sun-GLiang wants to merge 6 commits into
apache:mainfrom
Sun-GLiang:feat/contextual-message-timestamps
Open

feat(desktop): reveal contextual message timestamps on hover#3681
Sun-GLiang wants to merge 6 commits into
apache:mainfrom
Sun-GLiang:feat/contextual-message-timestamps

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • hide user-message timestamps at rest, reveal them on message hover or focus-within, and preserve their layout space so the transcript does not move
  • present time only for today, month/day/time for another day in the local year, and year/month/day/time across years, using the active locale and local calendar boundaries
  • keep the date-precision policy in a reusable Core module, leave the Astryx base component unchanged, refresh mounted labels after local midnight, and expose one complete absolute timestamp to assistive technology

Closes #3678

Visual evidence

Before — the time is permanently visible:

User-message timestamp permanently visible before the change

After, at rest — the timestamp and actions are visually hidden without moving the message:

User-message timestamp hidden at rest after the change

After, on hover — the same-day shape contains only the time:

Same-day user-message timestamp revealed on hover

Historical messages remain quiet at rest:

Historical user-message timestamps hidden at rest

When revealed, yesterday and five-days-ago messages include month, day, and time:

Historical user-message timestamps showing month, day, and time

These screenshots are captured from the real Desktop Electron renderer; they are not generative images.

Verification

  • npm --workspace @maka/core test — passed
  • npm --workspace @maka/ui test — 233 passed, 0 failed
  • npm --workspace @maka/desktop run e2e -- e2e/message-timestamp-visibility.spec.ts — 1 passed, covering hover, focus, same-day precision, and stable geometry
  • npm run lint — 2,677 files passed
  • npm run format:check — 1,617 files passed
  • npm run build — passed
  • npm run typecheck — passed across all workspaces
  • npx knip --workspace apps/desktop and npx knip --workspace packages/ui — passed
  • git diff --check upstream/main...HEAD — passed

npm test passed every workspace except @maka/runtime; Desktop passed 1,368/1,368. The remaining failure is the existing macOS executable-root assertion canonicalizes macOS Bash cwd and exposes the runtime executable roots, which expects -DEXECUTABLE_ROOT_1=/usr/local under this machine's /usr/local/bin/node. The isolated test reproduces it, and this branch has no diff under packages/runtime.

The new Core, UI, style-contract, and Electron tests were developed red-first and fail without the timestamp presenter, adapter, and reveal rules introduced here.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex helped refine the implementation plan; implemented the reusable timestamp presenter, UI adapter, interaction styling, and red-to-green tests; captured the Electron evidence; ran verification; and performed a preliminary AI code review. That review does not replace the repository's required independent human review. Every affected commit carries a Generated-by: Codex trailer. The human contributor remains responsible for the change, evidence, and submission.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@Sun-GLiang
Sun-GLiang force-pushed the feat/contextual-message-timestamps branch from b7019e3 to 22dacd3 Compare August 24, 2026 08:06
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Could a maintainer please trigger CI for this PR? Thanks!

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 22dacd3cc2. One [P2], one [P3], plus a gate note.

The shape of this is good: the date-precision policy lives in Core as a pure function, the reveal is CSS-only with opacity so the transcript never reflows, :focus-within is there alongside :hover so keyboard users are not excluded, and the visible run is aria-hidden with one complete absolute label exposed to assistive technology instead of letting a screen reader read a bare "23:20". The midnight refresh is real rather than a polling interval.

[P2] The one visible string is assembled by two formatters that resolve different locales

presentConversationMessageTimestamp formats with uiLocaleToIntlLocale(locale) — the app's UI locale, zh-CN or en. It then computes datePrefix by slicing the formatted parts up to (but not including) the hour part, on the assumption that the clock will continue exactly where the prefix stopped.

The clock does not come from that formatter. ConversationMessageTimestamp renders <Timestamp format="time"> from @astryxdesign/core, and formatInstant calls new Intl.DateTimeFormat(undefined, TIME_OPTIONS)undefined resolves to the host locale, not the app's.

So the two halves disagree whenever the UI locale differs from the OS locale, which is precisely the case the UI locale setting exists to serve. With the app set to Chinese on an en-US machine, a message from earlier this year renders as 8月24日 + 11:20 PM. The absoluteLabel read to assistive technology is built from the app locale, so the screen reader and the screen also disagree.

There is a quieter failure in the same seam. prefixBeforeClock returns '' when no hour part is found, and slices strictly before the hour otherwise. In any locale ordering that does not place the date before the clock, the date silently disappears from the visible label — the user sees only a time, with no indication that the message is from another day or another year. The hidden label still carries the full date, so nothing looks broken; the information is simply gone from the screen.

Please format the whole visible string from one formatter. Either render presentation.fallbackText directly instead of composing a prefix with a separately formatted clock, or pass the resolved app locale into the Astryx component if it accepts one. As written, the prefix and the clock are only guaranteed to agree on machines whose OS locale happens to match the app's.

[P3] One timer per message

ConversationMessageTimestamp schedules its own setTimeout to the next local midnight, and the effect depends on now, so every mounted message holds a timer and they all fire together at midnight. On a long transcript that is a few thousand timers to re-render every message at the same instant, for a change that affects only messages crossing a day boundary. A single shared midnight tick — a context or a module-level subscription — would give the same behaviour at one timer.

Gate: no check has run on this head at all. Never-run is not green, so this cannot be approved yet regardless of the findings.

简体中文

已在 22dacd3cc2 上审查。一条 [P2]、一条 [P3],外加一条门禁说明。

整体形状是好的:日期精度策略作为纯函数放在 Core;显隐纯靠 CSS opacity,所以 transcript 不会重排;:focus-within:hover 并列,键盘用户没有被排除在外;可见部分标了 aria-hidden,另外向辅助技术暴露一个完整的绝对时间,而不是让读屏软件念一句光秃秃的"23:20"。跨午夜刷新是真的定时,而不是轮询。

[P2] 同一个可见字符串,是由两个解析出不同 locale 的格式化器拼出来的

presentConversationMessageTimestampuiLocaleToIntlLocale(locale) 格式化——也就是应用的 UI locale,zh-CNen。然后它把格式化后的 parts 切到 hour 之前得到 datePrefix,前提假设是:时钟会恰好从前缀结束的地方接上。

但时钟并不来自那个格式化器。ConversationMessageTimestamp 渲染的是 @astryxdesign/core<Timestamp format="time">,而 formatInstant 调用的是 new Intl.DateTimeFormat(undefined, TIME_OPTIONS)——undefined 解析到的是宿主locale,不是应用的。

于是只要 UI locale 与操作系统 locale 不一致,两半就会打架——而这恰恰正是 UI locale 这个设置存在的意义。应用设为中文、机器是 en-US 时,今年早些时候的一条消息会渲染成 8月24日 + 11:20 PM。而念给辅助技术的 absoluteLabel 用的是应用locale,因此读屏与屏幕显示同样对不上。

同一处接缝还有一个更安静的失败。prefixBeforeClock 在找不到 hour part 时返回 '',否则严格切到 hour 之前。在任何"时钟不在日期之后"的 locale 排序下,日期会从可见标签里悄悄消失——用户只看到一个时间,完全看不出这条消息来自另一天甚至另一年。隐藏标签里仍带着完整日期,所以表面上没有任何异常;信息只是从屏幕上没了。

请让整个可见字符串出自同一个格式化器:要么直接渲染 presentation.fallbackText,不要用"前缀 + 另行格式化的时钟"拼;要么把解析后的应用 locale 传进 Astryx 组件(如果它接受)。按现在的写法,前缀与时钟只有在"操作系统 locale 恰好等于应用 locale"的机器上才保证一致。

[P3] 每条消息一个定时器

ConversationMessageTimestamp 各自 setTimeout 到下一个本地午夜,且该 effect 依赖 now,因此每条已挂载的消息都持有一个定时器,并且它们会在午夜同时触发。长 transcript 上就是几千个定时器在同一瞬间把每条消息重渲染一遍,而真正受影响的只有跨天边界的那几条。用一个共享的午夜 tick(context 或模块级订阅)能以一个定时器得到同样的行为。

门禁:这个 head 上一条 check 都没跑过。「没跑过」不等于绿,所以无论上述发现如何,现在都不具备 approve 条件。

Use one UI-locale formatter for the complete visible timestamp and share one midnight refresh across mounted messages.

Generated-by: Codex
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Addressed the review in d22f7e091.

  • P2 locale consistency: the visual <time> now renders the complete Core-produced visibleText, so date and clock use the active UI locale from one formatter. The Astryx base component remains unchanged.
  • P3 timer fan-out: mounted message timestamps now subscribe to one module-level local-midnight source via useSyncExternalStore; the last subscriber clears the timer and a later mount creates one fresh timer.
  • Added regressions for UI-locale/host-locale mismatch, shared timer lifecycle, mounted reclassification at midnight, and the render-before/subscribe-after-midnight race.

Local verification on this head: Core 657/657, UI 233/233, timestamp Electron E2E 1/1, lint 2,677 files, format 1,617 files, full build, all-workspace typecheck, and git diff --check.

The PR still reports no checks because the fork workflows have not been approved; that gate still requires a maintainer. @Astro-Han, please re-review the updated head when available.

Regenerate the Astryx surface inventory after combining the latest UI surface changes with contextual message timestamps.

Generated-by: Codex
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.

feat(desktop): reveal contextual message timestamps on hover

2 participants