@@ -173,12 +181,42 @@ function LifecycleItem({
return (
{item.title}
{item.text ? - {item.text} : null}
+
);
}
+
+const fullDateTimeFormat = new Intl.DateTimeFormat(undefined, {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+});
+
+function TranscriptTimestamp({ timestamp }: { timestamp: string }) {
+ const formatted = formatTranscriptTime(timestamp);
+ if (!formatted) return null;
+ const date = new Date(timestamp);
+ const fullDateTime = Number.isNaN(date.getTime())
+ ? timestamp
+ : fullDateTimeFormat.format(date);
+ return (
+
+
+
+ {formatted}
+
+
+ {fullDateTime}
+
+ );
+}
diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.ts b/desktop/src/features/agents/ui/agentSessionTranscript.ts
index 5f65ec474..e10af80ca 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscript.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscript.ts
@@ -147,6 +147,12 @@ export function buildTranscript(events: ObserverEvent[]): TranscriptItem[] {
existing.args = Object.keys(args).length > 0 ? args : existing.args;
if (result) existing.result = result;
existing.isError = isError || existing.isError;
+ if (
+ (status === "completed" || status === "failed") &&
+ existing.completedAt == null
+ ) {
+ existing.completedAt = timestamp;
+ }
return;
}
sealOpenMessages();
@@ -161,6 +167,8 @@ export function buildTranscript(events: ObserverEvent[]): TranscriptItem[] {
result,
isError,
timestamp,
+ startedAt: timestamp,
+ completedAt: null,
};
items.push(item);
itemsById.set(id, item);
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
index 72c3c4a80..74f86e239 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
@@ -4,7 +4,7 @@ import {
isGenericToolTitle,
normalizeToolName,
} from "./agentSessionToolCatalog";
-import { asRecord, asString, shorten, titleCase } from "./agentSessionUtils";
+import { asRecord, asString, titleCase } from "./agentSessionUtils";
export function extractPromptText(payload: Record
): string {
const params = asRecord(payload.params);
@@ -194,18 +194,14 @@ export function describeTurnStarted(payload: unknown): string {
)
: [];
return ids.length > 0
- ? `Triggered by ${ids.map(shorten).join(", ")}.`
- : "Heartbeat or internal turn.";
+ ? `Triggered by ${ids.length === 1 ? "1 event" : `${ids.length} events`}.`
+ : "";
}
export function describeSessionResolved(payload: unknown): string {
const record = asRecord(payload);
- const sessionId = asString(record.sessionId);
const isNewSession = record.isNewSession === true;
- if (!sessionId) {
- return "Using existing ACP session.";
- }
- return `${isNewSession ? "Created" : "Using"} session ${shorten(sessionId)}.`;
+ return isNewSession ? "New session created." : "";
}
export function describeRawEvent(event: ObserverEvent): string {
diff --git a/desktop/src/features/agents/ui/agentSessionTypes.ts b/desktop/src/features/agents/ui/agentSessionTypes.ts
index b4b8c9f2a..678ca3d33 100644
--- a/desktop/src/features/agents/ui/agentSessionTypes.ts
+++ b/desktop/src/features/agents/ui/agentSessionTypes.ts
@@ -61,6 +61,8 @@ export type TranscriptItem =
result: string;
isError: boolean;
timestamp: string;
+ startedAt: string;
+ completedAt: string | null;
};
export type PromptSection = {
diff --git a/desktop/src/features/agents/ui/agentSessionUtils.ts b/desktop/src/features/agents/ui/agentSessionUtils.ts
index 5efa9d001..ae3a147ab 100644
--- a/desktop/src/features/agents/ui/agentSessionUtils.ts
+++ b/desktop/src/features/agents/ui/agentSessionUtils.ts
@@ -79,3 +79,55 @@ export function shortenMiddle(value: string, maxLength: number) {
const edgeLength = Math.max(4, Math.floor((maxLength - 3) / 2));
return `${value.slice(0, edgeLength)}...${value.slice(-edgeLength)}`;
}
+
+const sameDayTimeFormat = new Intl.DateTimeFormat(undefined, {
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+});
+
+const crossDayTimeFormat = new Intl.DateTimeFormat(undefined, {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+});
+
+export function formatTranscriptTime(isoTimestamp: string): string | null {
+ const date = new Date(isoTimestamp);
+ if (Number.isNaN(date.getTime())) return null;
+ const now = new Date();
+ const sameDay =
+ date.getFullYear() === now.getFullYear() &&
+ date.getMonth() === now.getMonth() &&
+ date.getDate() === now.getDate();
+ return sameDay
+ ? sameDayTimeFormat.format(date)
+ : crossDayTimeFormat.format(date);
+}
+
+export function formatDuration(
+ startIso: string,
+ endIso: string,
+): string | null {
+ if (!startIso || !endIso) return null;
+ const start = new Date(startIso).getTime();
+ const end = new Date(endIso).getTime();
+ if (Number.isNaN(start) || Number.isNaN(end)) return null;
+ const ms = end - start;
+ if (ms < 0) return null;
+ const totalSeconds = ms / 1000;
+ if (totalSeconds < 60) {
+ return totalSeconds < 10
+ ? `${totalSeconds.toFixed(1)}s`
+ : `${Math.round(totalSeconds)}s`;
+ }
+ let minutes = Math.floor(totalSeconds / 60);
+ let seconds = Math.round(totalSeconds % 60);
+ if (seconds === 60) {
+ minutes += 1;
+ seconds = 0;
+ }
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
+}