Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0009ffe
feat(profile): embed live activity feed
Jun 30, 2026
d34af94
feat(profile): scope live activity by channel
Jun 30, 2026
d6120f3
fix(profile): harden live activity embed
Jun 30, 2026
f5d039a
fix(profile): use declared owner helper for activity gates
Jul 1, 2026
0f96a9c
fix(profile): render live activity for owned relay agents
tellaho Jul 1, 2026
ed3eee9
feat(profile): persist live activity embed after turn completes
tellaho Jul 1, 2026
1fb9306
feat(profile): make activity preview an ingress
tellaho Jul 1, 2026
ef7b3bc
fix(profile): polish activity preview layout
tellaho Jul 1, 2026
1a5fd82
fix(profile): tune activity preview fade
tellaho Jul 1, 2026
f3349d9
refactor(profile): inherit markdown typography in activity preview
tellaho Jul 1, 2026
14be9c5
refactor(agents): simplify activity feed message typography
tellaho Jul 1, 2026
fb35e4a
feat(agents): polish runtime activity message cards
tellaho Jul 1, 2026
7cc2e95
fix(profile): expand compact activity preview
tellaho Jul 1, 2026
4aa31ca
fix(profile): clarify activity preview overlay
tellaho Jul 1, 2026
f02cfa4
fix(profile): refine activity preview label
tellaho Jul 1, 2026
cbfaafe
feat(profile): add in-frame activity carousel with lazy channel slides
tellaho Jul 1, 2026
b9f6f28
fix(profile): refine compact activity preview readability
tellaho Jul 1, 2026
fe427c3
fix(profile): hide activity carousel dots for single-channel embeds
tellaho Jul 1, 2026
50784ab
fix(agents): left-align grouped activity summaries
tellaho Jul 1, 2026
20da096
fix(profile): show pending activity state in embed
tellaho Jul 1, 2026
e1d9de9
fix(profile): place embed padding on transcript content
tellaho Jul 1, 2026
f8b746d
fix(profile): remove working-channel pill from profile panel
tellaho Jul 1, 2026
c005c2c
feat(profile): show activity last-updated timestamp
tellaho Jul 1, 2026
a7c3909
fix(profile): stabilize live activity embed
Jul 1, 2026
d55c6ad
fix(profile): refine activity preview chrome
tellaho Jul 2, 2026
07a196a
fix(agents): hide assistant transcript author chrome
tellaho Jul 2, 2026
0108e4a
fix(agents): remove delayed transcript loading copy
tellaho Jul 2, 2026
66e685d
feat(desktop): render read-file tool output in shared file content panel
tellaho Jul 2, 2026
1825406
refactor(desktop): formalize file-read and image render classes
tellaho Jul 2, 2026
00f9d96
refactor(desktop): consolidate tool summary content and label matching
tellaho Jul 2, 2026
5b7a1f3
fix(agents): scroll-fade both shell command panels and clamp heights
tellaho Jul 2, 2026
0c6c5fa
fix(desktop): prevent file-read footer from overlapping content
tellaho Jul 2, 2026
a1558c7
feat(desktop): render load_skill output in shared file content panel
tellaho Jul 2, 2026
816fc56
feat(desktop): add fuzzy Buzz logo turn-liveness indicator
tellaho Jul 2, 2026
7a214ee
fix(desktop): align tool row height with grouped summary rows
tellaho Jul 2, 2026
f9ea8a8
feat(desktop): add rest window between turn indicator animation loops
tellaho Jul 2, 2026
178abd0
fix(desktop): teach node test loader emoji-mart and json semantics
Jul 2, 2026
88ce8b3
feat(desktop): animate transcript activity rows with Show Animations …
tellaho Jul 2, 2026
4deab73
feat(desktop): shrink staggered turn-liveness marks to 1.25rem
tellaho Jul 2, 2026
c0fcf06
fix(desktop): animate streaming turn segments and correct scroll-driv…
tellaho Jul 2, 2026
6959ad1
fix(e2e): align smoke specs with live-activity profile embed
tellaho Jul 2, 2026
3e4416c
test(e2e): widen channel-history prepend settle budget for CI subpixe…
Jul 2, 2026
f5666c5
fix(desktop): center transcript empty-state loading indicator in its …
tellaho Jul 2, 2026
4337262
fix(desktop): give carousel viewport full height so slide content can…
tellaho Jul 2, 2026
830f22f
feat(desktop): add "Open in chat" hover cue to linked transcript mess…
tellaho Jul 2, 2026
af23272
fix(desktop): gate prompt context behind Checks-icon dialog ingress
tellaho Jul 2, 2026
4f17550
feat(desktop): highlight transcript activity rows to foreground on hover
tellaho Jul 2, 2026
9f04d9c
refactor(desktop): flatten the "Open in chat" hover cue pill
tellaho Jul 2, 2026
7756b34
fix(messages): align prevMessagesRef type with rebased anchored-scrol…
tellaho Jul 2, 2026
b5503ef
fix(profile): preserve activity carousel selection
wesbillman Jul 3, 2026
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
1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@tiptap/starter-kit": "^3.22.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"emoji-mart": "^5.6.0",
"jdenticon": "^3.3.0",
"lucide-react": "^1.0.0",
Expand Down
13 changes: 13 additions & 0 deletions desktop/src/features/agents/observerRelayStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,19 @@ export function injectObserverEventsForE2E(
notifyListeners();
}

/**
* Synchronize the observer store with a sorted buffer of events for one agent.
* Used by test harnesses and replay bridges that already hold decoded frames.
*/
export function syncAgentObserverEvents(
agentPubkey: string,
events: ObserverEvent[],
) {
for (const event of events) {
appendAgentEvent(agentPubkey, event);
}
}

export function resetAgentObserverStore() {
generation += 1;
const unsubscribe = unsubscribeRelay;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import * as React from "react";
import { CheckCheck } from "lucide-react";

import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { cn } from "@/shared/lib/cn";
import { useProfilePanel } from "@/shared/context/ProfilePanelContext";
import { Markdown } from "@/shared/ui/markdown";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
import { MessageLinkHoverCue } from "../activityRenderClasses/MessageLinkHoverCue";
import { TranscriptTimestamp } from "../activityRenderClasses/TranscriptTimestamp";
import { useTranscriptBubbleOverflow } from "../activityRenderClasses/useTranscriptBubbleOverflow";
import { compactSummaryTone } from "./CompactToolSummaryRow";
import type { SentMessageLink } from "./messageLinks";
import { SentMessageContextDialog } from "./SentMessageContextDialog";
Expand All @@ -20,6 +26,7 @@ export function CompactMessageSummary({
label,
messageLink,
preview,
pubkey,
result,
timestamp,
}: {
Expand All @@ -34,34 +41,139 @@ export function CompactMessageSummary({
label: string;
messageLink: SentMessageLink | null;
preview: string | null;
pubkey: string;
result: string;
timestamp: string;
}) {
const [detailsOpen, setDetailsOpen] = React.useState(false);
const variant = useAgentSessionTranscriptVariant();
const { goChannel } = useAppNavigation();
const { openProfilePanel } = useProfilePanel();
const isCompactPreview = variant === "compactPreview";
const shouldClampBubble = !isCompactPreview;
const [bubbleRef, hasBubbleOverflow] =
useTranscriptBubbleOverflow(shouldClampBubble);
const canOpenMessage = shouldClampBubble && messageLink !== null;
const mutedTone = compactSummaryTone();
const avatarClassName = cn(
"mr-2 mt-1 shrink-0",
isCompactPreview ? "size-5" : "size-7",
);
const handleBubbleClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (!messageLink || isNestedInteractiveTarget(event)) return;
event.preventDefault();
event.stopPropagation();
void goChannel(messageLink.channelId, {
messageId: messageLink.messageId,
});
},
[goChannel, messageLink],
);
const handleBubbleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (
!messageLink ||
isNestedInteractiveTarget(event) ||
(event.key !== "Enter" && event.key !== " ")
) {
return;
}

event.preventDefault();
event.stopPropagation();
void goChannel(messageLink.channelId, {
messageId: messageLink.messageId,
});
},
[goChannel, messageLink],
);
const bubbleLinkProps = canOpenMessage
? {
onClick: handleBubbleClick,
onKeyDown: handleBubbleKeyDown,
role: "link" as const,
tabIndex: 0,
}
: {};
return (
<>
<div className="flex max-w-full flex-row items-start justify-start">
<UserAvatar
avatarUrl={avatarUrl}
className="mr-2 mt-1 shrink-0"
displayName={displayName}
size="xs"
testId="transcript-agent-sent-avatar"
/>
<div className="flex max-w-[85%] min-w-0 flex-col items-start gap-1">
{openProfilePanel && !isCompactPreview ? (
<button
aria-label={`Open ${displayName} profile`}
className={cn(
avatarClassName,
"pointer-events-auto rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
openProfilePanel(pubkey);
}}
type="button"
>
<UserAvatar
avatarUrl={avatarUrl}
className="size-full text-xs"
displayName={displayName}
size="sm"
testId="transcript-agent-sent-avatar"
/>
</button>
) : (
<UserAvatar
avatarUrl={avatarUrl}
className={cn(
avatarClassName,
isCompactPreview ? "text-3xs" : "text-xs",
)}
displayName={displayName}
size="sm"
testId="transcript-agent-sent-avatar"
/>
)}
<div className="flex min-w-0 flex-1 flex-col items-start gap-1">
<div
className={cn(
"min-w-0 rounded-2xl border px-3 py-2 text-sm leading-relaxed shadow-sm",
isError
? "border-destructive/25 bg-destructive/10 text-destructive"
: "border-primary/15 bg-primary/6 text-foreground",
"w-full min-w-0 rounded-2xl border px-3 py-2 shadow-sm",
isCompactPreview
? "text-xs leading-4"
: "text-sm leading-relaxed",
shouldClampBubble && "relative max-h-36 overflow-hidden",
canOpenMessage &&
"group/bubble cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isCompactPreview
? isError
? "border-destructive/25 bg-destructive/10 text-destructive"
: "border-transparent bg-muted text-foreground"
: isError
? "border-destructive/25 bg-destructive/10 text-destructive"
: "border-transparent bg-muted text-foreground",
canOpenMessage &&
(isError ? "hover:bg-destructive/15" : "hover:bg-muted/90"),
)}
data-testid="transcript-tool-message-preview"
ref={bubbleRef}
{...bubbleLinkProps}
>
<p className="whitespace-pre-wrap wrap-break-word">
{preview || "Message content unavailable."}
</p>
<Markdown
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
content={preview || "Message content unavailable."}
/>
{hasBubbleOverflow ? (
<span
className={cn(
"pointer-events-none absolute inset-x-0 bottom-0 h-8 rounded-b-2xl bg-linear-to-b from-transparent",
isError
? "to-destructive/10"
: isCompactPreview
? "to-muted"
: "to-muted",
)}
/>
) : null}
{canOpenMessage ? <MessageLinkHoverCue /> : null}
</div>
<div className="inline-flex max-w-full items-center gap-1.5 px-1">
<TranscriptTimestamp
Expand Down Expand Up @@ -100,3 +212,16 @@ export function CompactMessageSummary({
</>
);
}

function isNestedInteractiveTarget(
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
) {
const target =
event.target instanceof Element
? event.target.closest(
"a,button,input,select,textarea,summary,[role='button'],[role='link']",
)
: null;

return target !== null && target !== event.currentTarget;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,51 @@ import * as React from "react";
import { ChevronDown } from "lucide-react";

import { cn } from "@/shared/lib/cn";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
import type { AgentActivityAction } from "../agentSessionTypes";
import type { CompactFileEditSummary } from "../agentSessionToolSummary";
import { isInlineImageData } from "../agentSessionUtils";
import type {
CompactFileEditSummary,
CompactToolKind,
} from "../agentSessionToolSummary";
import { resolveToolImageSrc } from "../agentSessionUtils";
import {
ActivityRowLabel,
splitActivityRowLabel,
type ActivityRowLabelParts,
} from "../activityRenderClasses/ActivityRow";

export function compactSummaryTone() {
return "text-muted-foreground/60 group-open:text-foreground";
return "text-muted-foreground/60 transition-colors group-hover/row:text-foreground group-open:text-foreground";
}

export function CompactToolSummaryRow({
action,
duration,
fileEditSummary,
kind,
label,
preview,
thumbnailSrc,
}: {
action: AgentActivityAction | null;
duration: string | null;
fileEditSummary: CompactFileEditSummary | null;
kind: CompactToolKind;
label: string;
preview: string | null;
thumbnailSrc: string | null;
}) {
const [thumbnailFailed, setThumbnailFailed] = React.useState(false);
const variant = useAgentSessionTranscriptVariant();
const isCompactPreview = variant === "compactPreview";
const mutedTone = compactSummaryTone();
const resolvedThumbnail = React.useMemo(() => {
if (!thumbnailSrc || thumbnailFailed) return null;
return resolveImageSrc(thumbnailSrc);
return resolveToolImageSrc(thumbnailSrc);
}, [thumbnailFailed, thumbnailSrc]);
const actionLabel = fileEditSummary
? null
: getCompactToolActionLabel(action, label, preview);
: getCompactToolActionLabel(action, kind, label, preview);

return (
<>
Expand All @@ -53,7 +60,13 @@ export function CompactToolSummaryRow({
verb={actionLabel.verb}
/>
) : (
<span className={cn("shrink-0 text-sm font-semibold", mutedTone)}>
<span
className={cn(
"shrink-0 font-semibold",
isCompactPreview ? "text-xs" : "text-sm",
mutedTone,
)}
>
{label}
</span>
)}
Expand All @@ -69,7 +82,11 @@ export function CompactToolSummaryRow({
/>
) : !fileEditSummary && !actionLabel && preview ? (
<span
className={cn("min-w-0 max-w-48 truncate text-sm", mutedTone)}
className={cn(
"min-w-0 max-w-48 truncate",
isCompactPreview ? "text-xs" : "text-sm",
mutedTone,
)}
title={preview}
>
{preview}
Expand All @@ -90,6 +107,7 @@ export function CompactToolSummaryRow({

function getCompactToolActionLabel(
action: AgentActivityAction | null,
kind: CompactToolKind,
label: string,
preview: string | null,
): (ActivityRowLabelParts & { title?: string }) | null {
Expand All @@ -108,10 +126,11 @@ function getCompactToolActionLabel(
if (!preview) return parts;

if (
label === "Ran command" ||
label === "Read file" ||
label === "Updated todos" ||
label === "Viewed image"
kind === "shell" ||
kind === "file-read" ||
kind === "skill-read" ||
kind === "plan" ||
kind === "image"
) {
return { verb: parts.verb, object: preview, title: preview };
}
Expand All @@ -138,7 +157,3 @@ function CompactFileEditSummaryView({
/>
);
}

function resolveImageSrc(source: string): string {
return isInlineImageData(source) ? source : rewriteRelayUrl(source);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Terminal } from "lucide-react";

import { ScrollFadeMonoPanel } from "../FileContentBlock";
import { parseShellToolOutput } from "../agentSessionUtils";

export function ShellCommandBlock({
Expand All @@ -14,17 +15,28 @@ export function ShellCommandBlock({

return (
<div
className="rounded-lg bg-muted/40 px-3 py-2 font-mono text-xs leading-5"
className="overflow-hidden rounded-lg bg-muted font-mono text-xs leading-5"
data-testid="transcript-shell-command"
>
<p className="whitespace-pre-wrap wrap-break-word text-muted-foreground/70">
<Terminal className="mr-2 inline h-3.5 w-3.5 align-[-0.1875rem] text-accent-foreground" />
{command}
</p>
<ScrollFadeMonoPanel
fadeFromClassName="from-muted"
maxHeightClassName="max-h-36"
>
<p className="whitespace-pre-wrap wrap-break-word text-muted-foreground/70">
<Terminal className="mr-2 inline h-3.5 w-3.5 align-[-0.1875rem] text-primary" />
{command}
</p>
</ScrollFadeMonoPanel>
{stdout ? (
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap wrap-break-word text-foreground">
{stdout}
</pre>
<ScrollFadeMonoPanel
className="mt-2"
fadeFromClassName="from-muted"
maxHeightClassName="max-h-36"
>
<pre className="whitespace-pre-wrap wrap-break-word text-foreground">
{stdout}
</pre>
</ScrollFadeMonoPanel>
) : null}
</div>
);
Expand Down
Loading
Loading