diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index a21b5f9920..67f7bb75b1 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -84,6 +84,7 @@ const Part = memo(function Part({ quotes={part.quotes} steerId={part.steerId} createdAt={part.createdAt} + isSubmitting={isSubmitting} /> ); } diff --git a/client/src/components/Chat/Messages/Content/Parts/SteerPart.tsx b/client/src/components/Chat/Messages/Content/Parts/SteerPart.tsx index 5c5446fd3a..ca472fed1c 100644 --- a/client/src/components/Chat/Messages/Content/Parts/SteerPart.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/SteerPart.tsx @@ -29,6 +29,7 @@ const SteerPart = memo(function SteerPart({ quotes, steerId, createdAt, + isSubmitting = false, }: { steer: string; files?: TMessage['files']; @@ -38,6 +39,10 @@ const SteerPart = memo(function SteerPart({ /** Anchors the part for the message-nav rail (`#steer-` rib target). */ steerId?: string; createdAt?: number; + /** The owning response is still generating: the receipt keeps its amber + * steering identity while it is the live thing at the end, then settles + * to timestamp gray (always settled on reload, share, and search). */ + isSubmitting?: boolean; }) { const localize = useLocalize(); /** Read the atom rather than the auth context: AuthContextProvider mirrors the @@ -151,7 +156,7 @@ const SteerPart = memo(function SteerPart({
- +
diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/SteerPart.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/SteerPart.test.tsx index 6fd7d30462..6b4fdc1a06 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/SteerPart.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/SteerPart.test.tsx @@ -260,3 +260,25 @@ describe('SteerPart live receipt draw-in', () => { expect(screen.getByTestId('live-ids').textContent).toBe(''); }); }); + +describe('SteerPart receipt settling', () => { + const checks = () => screen.getByLabelText('com_ui_steer_applied_info').querySelector('svg'); + + it('keeps the amber identity while the owning response is still generating', () => { + render( + set(store.user, SEEDED_USER as never)}> + + , + ); + expect(checks()).toHaveClass('dark:text-amber-500'); + expect(checks()).not.toHaveClass('text-text-secondary'); + }); + + it('settles to timestamp gray once the response is done, and on reload/share', () => { + // The default (no isSubmitting) is the settled, reload, share, and search + // rendering: still a double check, no longer lit. + renderPart(); + expect(checks()).toHaveClass('text-text-secondary'); + expect(checks()).not.toHaveClass('dark:text-amber-500'); + }); +}); diff --git a/client/src/components/Chat/Steering/Receipt.tsx b/client/src/components/Chat/Steering/Receipt.tsx index 163ded1f66..da2f1b5b1f 100644 --- a/client/src/components/Chat/Steering/Receipt.tsx +++ b/client/src/components/Chat/Steering/Receipt.tsx @@ -22,20 +22,27 @@ const AMBER_DOT = 'bg-amber-600 dark:bg-amber-500'; * - `interrupting`: asked to seal generation at the next safe boundary — the * amber label and pulsing dot say "working, not stuck". Until the arm is * server-confirmed (`confirmed` false) the label shows without its check. - * - `applied`: the running response durably absorbed the words (amber double - * check, persistent — it renders identically live, on reload, and in - * shared/search views). + * - `applied`: the running response durably absorbed the words (double check, + * persistent). It stays lit amber only while the owning response is still + * generating — the window where "did it land" is the question — then + * settles to the same muted gray as the timestamp, so a conversation full + * of historical steers doesn't glow forever. * * The hover explanations render through the shared `InfoHoverCard` with the * receipt as the trigger, so the info text doubles as the accessible name. */ const SteerReceipt = memo(function SteerReceipt({ state, + live = false, confirmed = true, animateIn = false, className, }: { state: SteerReceiptState; + /** Applied only: the owning response is still generating, so the receipt is + * the newest thing on screen and keeps the amber steering identity. Off + * (settled, reload, share, search) it dims to the timestamp gray. */ + live?: boolean; /** Server confirmation of the underlying enqueue/arm; an interrupt awaiting * its ACK shows the label and pulse without a check. */ confirmed?: boolean; @@ -62,15 +69,15 @@ const SteerReceipt = memo(function SteerReceipt({