Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/components/pro/running-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RiArrowDownSLine, RiStopFill } from "@remixicon/react";
import { Dropdown, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { StatusRow } from "@/components/pro/composer-status-bar";
import type { RunningStatus } from "@/components/pro/running-phase";
import { ElapsedTimer } from "@/components/session-ui/background-status-pill";
import { ElapsedTimer } from "@/components/session-ui/elapsed-timer";

export function RunningFooter({
status,
Expand Down
28 changes: 2 additions & 26 deletions frontend/components/session-ui/background-status-pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,12 @@
// this port is the standalone slim pill. The label is a plain prop (upstream
// derives it from backgroundLiveness + fleet liveCount), the elapsed timer is
// added from their MessagesTimeline WorkingTimer pattern (self-ticking text
// node, no React commits - that component is private to working-indicator.tsx),
// node, no React commits - that component now lives in elapsed-timer.tsx),
// and Stop accepts the EXISTING cancel handler as a prop (session-view
// handleStop posts the durable /cancel; no new API call here). Their
// animate-status-pulse -> our ai-loading-pixel; tokens are our semantic tokens.

import { useEffect, useRef } from "react";
import { formatWorkingTimerNow } from "./work-entry";

/** Self-ticking elapsed label; updates its own text node outside React commits. */
export function ElapsedTimer({ startedAt }: { startedAt: string }) {
const textRef = useRef<HTMLSpanElement>(null);
const initialText = formatWorkingTimerNow(startedAt);

useEffect(() => {
const updateText = () => {
if (textRef.current) {
textRef.current.textContent = formatWorkingTimerNow(startedAt);
}
};
updateText();
const id = setInterval(updateText, 1000);
return () => clearInterval(id);
}, [startedAt]);

return (
<span ref={textRef} className="tabular-nums">
{initialText}
</span>
);
}
import { ElapsedTimer } from "./elapsed-timer";

/**
* Slim persistent status pill for a thread with live work: pulsing dot + status
Expand Down
9 changes: 9 additions & 0 deletions frontend/components/session-ui/elapsed-timer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { ElapsedTimer } from "./elapsed-timer";

test("the timer renders the current elapsed text in tabular figures", () => {
const html = renderToStaticMarkup(<ElapsedTimer startedAt={new Date().toISOString()} />);
expect(html).toContain("tabular-nums");
expect(html).toContain("0s");
});
32 changes: 32 additions & 0 deletions frontend/components/session-ui/elapsed-timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

// The thread's one self-ticking elapsed label (shared by the working line and
// the background status pill): renders the current elapsed text, then updates
// its own text node every second outside React commits, from the
// MessagesTimeline WorkingTimer pattern vendored with working-indicator.tsx.

import { useEffect, useRef } from "react";
import { formatWorkingTimerNow } from "./work-entry";

/** Self-ticking elapsed label; updates its own text node outside React commits. */
export function ElapsedTimer({ startedAt }: { startedAt: string }) {
const textRef = useRef<HTMLSpanElement>(null);
const initialText = formatWorkingTimerNow(startedAt);

useEffect(() => {
const updateText = () => {
if (textRef.current) {
textRef.current.textContent = formatWorkingTimerNow(startedAt);
}
};
updateText();
const id = setInterval(updateText, 1000);
return () => clearInterval(id);
}, [startedAt]);

return (
<span ref={textRef} className="tabular-nums">
{initialText}
</span>
);
}
31 changes: 4 additions & 27 deletions frontend/components/session-ui/working-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,13 @@
// Source: apps/web/src/components/chat/MessagesTimeline.tsx (WorkingTimelineRow +
// WorkingTimer): three staggered pulse dots, a SELF-TICKING "Working for Xs" label
// (writes its own text node every second so elapsed time never causes a React
// commit while streaming), and an optional current-step suffix.
// commit while streaming), and an optional current-step suffix. The timer itself
// now lives in elapsed-timer.tsx, shared with the background status pill.
//
// Port notes: their `animate-status-pulse` utility -> our `ai-loading-pixel`
// stagger (globals.css, reduced-motion aware); tokens are our semantic tokens.

import { useEffect, useRef } from "react";
import { formatWorkingTimerNow } from "./work-entry";

/** Live "Working for Xs" label; updates its own text node outside React commits. */
function WorkingTimer({ createdAt }: { createdAt: string }) {
const textRef = useRef<HTMLSpanElement>(null);
const initialText = formatWorkingTimerNow(createdAt);

useEffect(() => {
const updateText = () => {
if (textRef.current) {
textRef.current.textContent = formatWorkingTimerNow(createdAt);
}
};
updateText();
const id = setInterval(updateText, 1000);
return () => clearInterval(id);
}, [createdAt]);

return (
<span ref={textRef} className="tabular-nums">
{initialText}
</span>
);
}
import { ElapsedTimer } from "./elapsed-timer";

export function WorkingIndicator({
createdAt,
Expand All @@ -59,7 +36,7 @@ export function WorkingIndicator({
<span className="shrink-0">
{createdAt ? (
<>
Working for <WorkingTimer createdAt={createdAt} />
Working for <ElapsedTimer startedAt={createdAt} />
</>
) : (
"Working…"
Expand Down
Loading