fix: accurate elapsed timing and UX polish for activity panel

- Fix useElapsedTimer to use completedAt instead of Date.now() for
  finished runs, preventing wildly inaccurate durations (e.g. 9802m)
  when reopening old sessions
- Extract formatElapsedMs as a shared pure formatter; reuse in
  SubagentActivityCard
- Standardize icon column alignment across all activity row types
  using consistent w-4 + h-[18px] icon containers
- Redesign collapsed header: status dot with pulse animation, bolder
  status label, tabular-nums on elapsed time, cleaner hierarchy
- Polish expanded stream: timeline spine connecting activity items,
  more prominent intent dividers with pill-style background,
  tightened vertical rhythm
- Add unit tests for formatElapsedMs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-15 09:11:38 +02:00
co-authored by Copilot
parent 9243ec10a2
commit 05b3771544
5 changed files with 137 additions and 54 deletions
+31 -14
View File
@@ -1,39 +1,56 @@
import { useEffect, useState } from 'react';
function formatElapsed(startMs: number): string {
const seconds = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
/** Format a millisecond duration as a human-readable elapsed string. */
export function formatElapsedMs(durationMs: number): string {
const seconds = Math.max(0, Math.floor(durationMs / 1000));
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainder = seconds % 60;
return `${minutes}m ${remainder}s`;
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
}
/**
* Returns a live-ticking elapsed-time string while `active` is true.
* Freezes the display when `active` becomes false.
*
* When the timer is no longer active and `completedAt` is provided, the
* duration is computed as `completedAt startedAt` — giving an accurate
* frozen value even when the session is reopened much later. Without
* `completedAt` the hook falls back to `Date.now() startedAt` at the
* moment `active` becomes false.
*/
export function useElapsedTimer(startedAt: string | undefined, active: boolean): string | undefined {
export function useElapsedTimer(
startedAt: string | undefined,
active: boolean,
completedAt?: string,
): string | undefined {
const startMs = startedAt ? new Date(startedAt).getTime() : undefined;
const endMs = completedAt ? new Date(completedAt).getTime() : undefined;
const [elapsed, setElapsed] = useState<string | undefined>(() => {
if (!startMs) return undefined;
return formatElapsed(startMs);
});
const computeElapsed = (): string | undefined => {
if (!startMs || Number.isNaN(startMs)) return undefined;
if (!active && endMs && !Number.isNaN(endMs)) {
return formatElapsedMs(endMs - startMs);
}
return formatElapsedMs(Date.now() - startMs);
};
const [elapsed, setElapsed] = useState<string | undefined>(computeElapsed);
useEffect(() => {
if (!startMs) {
if (!startMs || Number.isNaN(startMs)) {
setElapsed(undefined);
return;
}
// Always sync to current value immediately
setElapsed(formatElapsed(startMs));
// Sync immediately
setElapsed(computeElapsed());
if (!active) return;
const id = setInterval(() => setElapsed(formatElapsed(startMs)), 1000);
const id = setInterval(() => setElapsed(formatElapsedMs(Date.now() - startMs)), 1000);
return () => clearInterval(id);
}, [startMs, active]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startMs, endMs, active]);
return elapsed;
}