mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): show activity timestamps on demand (#1506)
Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
parent
f35aeb7986
commit
c7a8b0babb
@@ -27,6 +27,7 @@ import {
|
||||
useAgentSessionTranscriptVariant,
|
||||
} from "./agentSessionTranscriptContext";
|
||||
import { useTranscriptAnimationEnabled } from "./transcriptAnimationPreference";
|
||||
import { useTranscriptTimestampsEnabled } from "./transcriptTimestampPreference";
|
||||
import { TranscriptActivityItem } from "./activityRenderClasses/TranscriptActivityItem";
|
||||
import {
|
||||
ActivityRow,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
type TranscriptTurnSegment,
|
||||
} from "./agentSessionTranscriptGrouping";
|
||||
import { buildCompactToolSummary } from "./agentSessionToolSummary";
|
||||
import { shouldShowTranscriptRowTimestamp } from "./agentSessionTranscriptPresentation";
|
||||
import { formatTranscriptTimestampTitle } from "./agentSessionUtils";
|
||||
import { hasFileEditLineDiff } from "./FileEditDiffView";
|
||||
import { UserMessageBubble } from "./activityRenderClasses/UserMessageBubble";
|
||||
@@ -462,44 +464,55 @@ function SameKindSummaryItem({
|
||||
const expandsToToolItems = summary.items.every(
|
||||
(item) => item.type === "tool",
|
||||
);
|
||||
const variant = useAgentSessionTranscriptVariant();
|
||||
const timestampsEnabled = useTranscriptTimestampsEnabled();
|
||||
const showTimestamp = timestampsEnabled && variant !== "compactPreview";
|
||||
|
||||
return (
|
||||
<ActivityRow
|
||||
className="flex flex-col gap-0.5"
|
||||
openToneScope="summary"
|
||||
testId="transcript-same-kind-summary"
|
||||
title={formatTranscriptTimestampTitle(summary.timestamp)}
|
||||
>
|
||||
<ToolRunSummaryLabel label={summary.label} stats={groupedFileEditStats} />
|
||||
<ActivityRowContent
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
expandsToToolItems ? "gap-0.5" : "gap-1 pl-5",
|
||||
)}
|
||||
<>
|
||||
<ActivityRow
|
||||
className="flex flex-col gap-0.5"
|
||||
openToneScope="summary"
|
||||
testId="transcript-same-kind-summary"
|
||||
title={formatTranscriptTimestampTitle(summary.timestamp)}
|
||||
>
|
||||
{expandsToToolItems
|
||||
? summary.items.map((item) => (
|
||||
<TranscriptItemView
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
agentName={agentName}
|
||||
agentPubkey={agentPubkey}
|
||||
item={item}
|
||||
key={item.id}
|
||||
profiles={profiles}
|
||||
/>
|
||||
))
|
||||
: summary.items.map((item) => (
|
||||
<p
|
||||
className="truncate text-xs text-muted-foreground"
|
||||
key={item.id}
|
||||
>
|
||||
{item.type === "tool"
|
||||
? item.descriptor.preview || item.descriptor.label
|
||||
: item.title}
|
||||
</p>
|
||||
))}
|
||||
</ActivityRowContent>
|
||||
</ActivityRow>
|
||||
<ToolRunSummaryLabel
|
||||
label={summary.label}
|
||||
stats={groupedFileEditStats}
|
||||
/>
|
||||
<ActivityRowContent
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
expandsToToolItems ? "gap-0.5" : "gap-1 pl-5",
|
||||
)}
|
||||
>
|
||||
{expandsToToolItems
|
||||
? summary.items.map((item) => (
|
||||
<TranscriptItemView
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
agentName={agentName}
|
||||
agentPubkey={agentPubkey}
|
||||
item={item}
|
||||
key={item.id}
|
||||
profiles={profiles}
|
||||
/>
|
||||
))
|
||||
: summary.items.map((item) => (
|
||||
<p
|
||||
className="truncate text-xs text-muted-foreground"
|
||||
key={item.id}
|
||||
>
|
||||
{item.type === "tool"
|
||||
? item.descriptor.preview || item.descriptor.label
|
||||
: item.title}
|
||||
</p>
|
||||
))}
|
||||
</ActivityRowContent>
|
||||
</ActivityRow>
|
||||
{showTimestamp ? (
|
||||
<TranscriptRowTimestamp timestamp={summary.timestamp} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -760,6 +773,13 @@ function TranscriptItemRow({
|
||||
item: TranscriptItem;
|
||||
profiles?: UserProfileLookup;
|
||||
}) {
|
||||
const variant = useAgentSessionTranscriptVariant();
|
||||
const timestampsEnabled = useTranscriptTimestampsEnabled();
|
||||
const showTimestamp = shouldShowTranscriptRowTimestamp(item, {
|
||||
enabled: timestampsEnabled,
|
||||
variant,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{SHOW_TRANSCRIPT_ACP_SOURCE && item.acpSource ? (
|
||||
@@ -772,6 +792,35 @@ function TranscriptItemRow({
|
||||
item={item}
|
||||
profiles={profiles}
|
||||
/>
|
||||
{showTimestamp ? (
|
||||
<TranscriptRowTimestamp
|
||||
messageLink={
|
||||
item.type === "message" ? getTranscriptMessageLink(item) : null
|
||||
}
|
||||
timestamp={item.timestamp}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt-in per-row timestamp, anchored bottom-left under the row content and
|
||||
* styled to match the chat/transcript timestamps.
|
||||
*/
|
||||
function TranscriptRowTimestamp({
|
||||
messageLink = null,
|
||||
timestamp,
|
||||
}: {
|
||||
messageLink?: { channelId: string; messageId: string } | null;
|
||||
timestamp: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="mt-0.5 flex justify-start"
|
||||
data-testid="transcript-row-timestamp"
|
||||
>
|
||||
<TranscriptTimestamp messageLink={messageLink} timestamp={timestamp} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { ObserverEvent } from "./agentSessionTypes";
|
||||
import { describeRawEvent } from "./agentSessionTranscript";
|
||||
import { TranscriptTimestamp } from "./activityRenderClasses/TranscriptTimestamp";
|
||||
import { useTranscriptTimestampsEnabled } from "./transcriptTimestampPreference";
|
||||
|
||||
export function RawEventRail({ events }: { events: ObserverEvent[] }) {
|
||||
const showTimestamps = useTranscriptTimestampsEnabled();
|
||||
|
||||
return (
|
||||
<section className="flex min-h-0 w-full flex-col text-foreground">
|
||||
<div className="min-h-0 flex-1">
|
||||
@@ -21,6 +25,14 @@ export function RawEventRail({ events }: { events: ObserverEvent[] }) {
|
||||
#{event.seq}
|
||||
</span>{" "}
|
||||
{describeRawEvent(event)}
|
||||
{showTimestamps ? (
|
||||
<span
|
||||
className="mt-1 flex justify-start"
|
||||
data-testid="raw-event-timestamp"
|
||||
>
|
||||
<TranscriptTimestamp timestamp={event.timestamp} />
|
||||
</span>
|
||||
) : null}
|
||||
</summary>
|
||||
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/40 bg-background/45 p-2 font-mono text-xs leading-5 text-muted-foreground">
|
||||
{JSON.stringify(event.payload, null, 2)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getActivityHeadline,
|
||||
isMeaningfulItem,
|
||||
isSpineItem,
|
||||
shouldShowTranscriptRowTimestamp,
|
||||
} from "./agentSessionTranscriptPresentation.ts";
|
||||
|
||||
const baseTimestamp = "2026-06-14T19:00:00.000Z";
|
||||
@@ -255,3 +256,59 @@ test("two-tier headline: metadata headlines when it is the only activity (sessio
|
||||
// activityRenderClasses/RawRailActivity.render.test.mjs — they use
|
||||
// renderToStaticMarkup and would fail if the isRawPayload branch in
|
||||
// RawRailActivity were removed.
|
||||
|
||||
test("shouldShowTranscriptRowTimestamp: off by default (enabled=false)", () => {
|
||||
assert.equal(
|
||||
shouldShowTranscriptRowTimestamp(makeTool(), {
|
||||
enabled: false,
|
||||
variant: "default",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldShowTranscriptRowTimestamp: enabled shows for tools, thoughts, assistant messages", () => {
|
||||
const options = { enabled: true, variant: "default" };
|
||||
assert.equal(shouldShowTranscriptRowTimestamp(makeTool(), options), true);
|
||||
assert.equal(
|
||||
shouldShowTranscriptRowTimestamp(
|
||||
makeMessage({ role: "assistant" }),
|
||||
options,
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldShowTranscriptRowTimestamp(
|
||||
{
|
||||
id: "thought:1",
|
||||
type: "thought",
|
||||
title: "Thinking",
|
||||
text: "hmm",
|
||||
timestamp: baseTimestamp,
|
||||
},
|
||||
options,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldShowTranscriptRowTimestamp: user message bubbles keep their own footer (excluded)", () => {
|
||||
assert.equal(
|
||||
shouldShowTranscriptRowTimestamp(
|
||||
makeMessage({ role: "user", title: "tho" }),
|
||||
{ enabled: true, variant: "default" },
|
||||
),
|
||||
false,
|
||||
"user bubbles already render a timestamp footer",
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldShowTranscriptRowTimestamp: compact preview stays dense", () => {
|
||||
assert.equal(
|
||||
shouldShowTranscriptRowTimestamp(makeTool(), {
|
||||
enabled: true,
|
||||
variant: "compactPreview",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import type { TranscriptItem } from "./agentSessionTypes";
|
||||
import { buildCompactToolSummary } from "./agentSessionToolSummary";
|
||||
|
||||
/**
|
||||
* Whether a polished activity row should render the opt-in timestamp footer.
|
||||
* User message bubbles already render their own timestamp footer, so they are
|
||||
* excluded to avoid doubling up. Compact previews stay dense regardless of
|
||||
* the preference.
|
||||
*/
|
||||
export function shouldShowTranscriptRowTimestamp(
|
||||
item: TranscriptItem,
|
||||
options: { enabled: boolean; variant: string },
|
||||
): boolean {
|
||||
if (!options.enabled || options.variant === "compactPreview") {
|
||||
return false;
|
||||
}
|
||||
if (item.type === "message" && item.role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const LIFECYCLE_NOISE = new Set([
|
||||
"turn started",
|
||||
"session ready",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* User preference for showing a timestamp under each activity row — polished
|
||||
* transcript rows and raw JSON-RPC cards alike. Persisted in localStorage and
|
||||
* shared across every transcript surface, same as the animation preference.
|
||||
* This is a device-level UI preference, not workspace-scoped data, so it is
|
||||
* intentionally not reset on workspace switch. Defaults off to keep the feed
|
||||
* compact.
|
||||
*/
|
||||
const STORAGE_KEY = "buzz:show-transcript-timestamps";
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let timestampsEnabled = readStoredPreference();
|
||||
|
||||
function readStoredPreference(): boolean {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(STORAGE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot(): boolean {
|
||||
return timestampsEnabled;
|
||||
}
|
||||
|
||||
function getServerSnapshot(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Update the preference and notify all subscribed components. */
|
||||
export function setTranscriptTimestampsEnabled(enabled: boolean): void {
|
||||
timestampsEnabled = enabled;
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0");
|
||||
} catch {
|
||||
// Persistence is best-effort; the in-memory value still applies.
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether polished activity rows should show a timestamp footer. */
|
||||
export function useTranscriptTimestampsEnabled(): boolean {
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { Octagon, Settings, Sparkles, TerminalSquare } from "lucide-react";
|
||||
import {
|
||||
Clock3,
|
||||
Octagon,
|
||||
Settings,
|
||||
Sparkles,
|
||||
TerminalSquare,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
|
||||
@@ -41,6 +47,10 @@ import {
|
||||
setTranscriptAnimationEnabled,
|
||||
useTranscriptAnimationEnabled,
|
||||
} from "@/features/agents/ui/transcriptAnimationPreference";
|
||||
import {
|
||||
setTranscriptTimestampsEnabled,
|
||||
useTranscriptTimestampsEnabled,
|
||||
} from "@/features/agents/ui/transcriptTimestampPreference";
|
||||
import type { ChannelAgentSessionAgent } from "./useChannelAgentSessions";
|
||||
|
||||
type AgentSessionThreadPanelProps = {
|
||||
@@ -117,6 +127,7 @@ export function AgentSessionThreadPanel({
|
||||
[rawFeedScopeKey],
|
||||
);
|
||||
const animateActivity = useTranscriptAnimationEnabled();
|
||||
const showTimestamps = useTranscriptTimestampsEnabled();
|
||||
async function handleInterruptTurn() {
|
||||
if (!channel) {
|
||||
return;
|
||||
@@ -217,9 +228,6 @@ export function AgentSessionThreadPanel({
|
||||
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
||||
Show Animations
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-muted-foreground">
|
||||
Slide new activity rows in as they arrive.
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
aria-hidden="true"
|
||||
@@ -228,6 +236,32 @@ export function AgentSessionThreadPanel({
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="items-start gap-3"
|
||||
data-testid="agent-session-toggle-show-timestamps"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
setTranscriptTimestampsEnabled(!showTimestamps);
|
||||
}}
|
||||
title={
|
||||
showTimestamps
|
||||
? "Hide per-row activity timestamps."
|
||||
: "Show a timestamp under each activity row."
|
||||
}
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium">
|
||||
<Clock3 className="h-4 w-4 text-muted-foreground" />
|
||||
Show Timestamps
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
aria-hidden="true"
|
||||
checked={showTimestamps}
|
||||
className="pointer-events-none mt-0.5"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="items-start gap-3"
|
||||
|
||||
Reference in New Issue
Block a user