Address Greptile feedback: clear debounce timer, deduplicate util, fix completed-run display

- Clear the lastOutputAt debounce timer on run completion to avoid
  stale writes after the run finishes
- Replace local formatRelativeTime with existing relativeTime util
- Fix completed-run display in LiveRunWidget to show finish time
  instead of unconditionally showing lastOutputAt

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-03-27 00:51:06 -07:00
parent b3bccf3648
commit e58a2330a6
2 changed files with 17 additions and 13 deletions

View file

@ -2502,6 +2502,7 @@ export function heartbeatService(db: Db) {
const currentUserRedactionOptions = await getCurrentUserRedactionOptions();
let lastOutputAtFlushPending = false;
let lastOutputAtLatest: Date | null = null;
let lastOutputAtTimer: ReturnType<typeof setTimeout> | null = null;
const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions);
if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk);
@ -2516,8 +2517,9 @@ export function heartbeatService(db: Db) {
await db.update(heartbeatRuns)
.set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
setTimeout(() => {
lastOutputAtTimer = setTimeout(() => {
lastOutputAtFlushPending = false;
lastOutputAtTimer = null;
if (!lastOutputAtLatest) return;
db.update(heartbeatRuns)
.set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
@ -2649,6 +2651,8 @@ export function heartbeatService(db: Db) {
},
authToken: authToken ?? undefined,
});
// Clear the debounce timer now that the run is complete
if (lastOutputAtTimer) { clearTimeout(lastOutputAtTimer); lastOutputAtTimer = null; }
const adapterManagedRuntimeServices = adapterResult.runtimeServices
? await persistAdapterManagedRuntimeServices({
db,

View file

@ -3,7 +3,7 @@ import { Link } from "@/lib/router";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats";
import { queryKeys } from "../lib/queryKeys";
import { formatDateTime } from "../lib/utils";
import { formatDateTime, relativeTime } from "../lib/utils";
import { AlertTriangle, ExternalLink, Square } from "lucide-react";
import { Identity } from "./Identity";
import { StatusBadge } from "./StatusBadge";
@ -24,14 +24,6 @@ function isRunActive(status: string): boolean {
return status === "queued" || status === "running";
}
function formatRelativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
if (ms < 60_000) return "just now";
const min = Math.floor(ms / 60_000);
if (min < 60) return `${min}m ago`;
const hr = Math.floor(min / 60);
return `${hr}h ${min % 60}m ago`;
}
export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
const queryClient = useQueryClient();
@ -133,12 +125,20 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
Idle
</span>
)}
{run.lastOutputAt && (
{run.lastOutputAt && isRunActive(run.status) && (
<span title={`Last output: ${formatDateTime(run.lastOutputAt)}`}>
Last output {formatRelativeTime(run.lastOutputAt)}
Last output {relativeTime(run.lastOutputAt)}
</span>
)}
{!run.lastOutputAt && <span>{formatDateTime(run.startedAt ?? run.createdAt)}</span>}
{run.finishedAt && !isRunActive(run.status) && (
<span>{formatDateTime(run.finishedAt)}</span>
)}
{!run.lastOutputAt && isRunActive(run.status) && (
<span>{formatDateTime(run.startedAt ?? run.createdAt)}</span>
)}
{!run.finishedAt && !isRunActive(run.status) && !run.lastOutputAt && (
<span>{formatDateTime(run.startedAt ?? run.createdAt)}</span>
)}
</div>
</div>