mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user