From 277ad5a0d07ed77dc6902da0be0002bdfba89e3d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 15 Sep 2026 13:59:00 +0800 Subject: [PATCH 01/10] fix(ui): give the turn elapsed clock one start authority The running elapsed and the settled duration must read one Turn start fact. The optimistic send timestamp was a second authority: it fed the pre-Turn cue and every transient update re-stamped it to Date.now(), so the running clock restarted mid-turn instead of measuring from the turn's own start. - Drop that client-side start; the pre-Turn cue now waits without a clock until turn.startedAt reaches the client, which is also what the settled duration measures from. - Write a transient message's send time once, so an update no longer moves it. - Drop cursor:pointer from the process disclosure, and cover in the native-cursor convention (the hand stays for links). Generated-by: Maka --- .../transient-message-projection.test.ts | 7 +++++++ .../contracts/transient-message-projection.ts | 4 ++++ .../desktop/src/renderer/styles/native-cursor.css | 10 +++++++--- .../__tests__/chat-view-empty-compaction.test.tsx | 8 +++++--- packages/ui/src/chat-view.tsx | 15 ++++++--------- packages/ui/src/styles.css | 2 -- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/main/__tests__/transient-message-projection.test.ts b/apps/desktop/src/main/__tests__/transient-message-projection.test.ts index 961938d14d..fb5cb24477 100644 --- a/apps/desktop/src/main/__tests__/transient-message-projection.test.ts +++ b/apps/desktop/src/main/__tests__/transient-message-projection.test.ts @@ -205,3 +205,10 @@ test('keeps a Host-bound current Turn when a later IPC result has no Turn identi hostTurnId: 'host-turn', }); }); + +test('keeps a transient message send time when a later update carries a new timestamp', () => { + const first = { ...transient, ts: 2 }; + const later = { ...transient, ts: 9, text: 'edited text' }; + + assert.deepEqual(mergeTransientMessageProjection(first, later), { ...later, ts: 2 }); +}); diff --git a/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts b/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts index 803a6024b6..26bb4ef7fd 100644 --- a/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts +++ b/apps/desktop/src/renderer/application/contracts/transient-message-projection.ts @@ -49,6 +49,10 @@ export function mergeTransientMessageProjection( ): TransientUserMessage { update = { ...update, + // A Message's send time is written once. An update carrying its own `ts` + // (the desktop publisher stamps Date.now() on every publish) must not move + // it, or the optimistic row's time drifts to the latest edit. + ts: current.ts, ...(update.pendingSteering === undefined && current.pendingSteering !== undefined ? { pendingSteering: current.pendingSteering } : {}), ...(!Object.hasOwn(update, 'deliveryStatus') && current.deliveryStatus !== undefined ? { deliveryStatus: current.deliveryStatus } : {}), ...(!Object.hasOwn(update, 'deliveryDetail') && current.deliveryDetail !== undefined ? { deliveryDetail: current.deliveryDetail } : {}), diff --git a/apps/desktop/src/renderer/styles/native-cursor.css b/apps/desktop/src/renderer/styles/native-cursor.css index ed42ad88f3..ecca0601bd 100644 --- a/apps/desktop/src/renderer/styles/native-cursor.css +++ b/apps/desktop/src/renderer/styles/native-cursor.css @@ -54,7 +54,9 @@ * group label (InputGroup: `isGroupLabel`), which names a group via * `aria-labelledby` and forwards no click — the `label` selector misses it, * so it kept StyleX's hand cursor. Match the stable themeProps class to catch - * that span too (real