Skip to content
Open
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
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,24 @@ scripts build the required E2E bridge before running Playwright.

See [TESTING.md](TESTING.md) for the full multi-agent E2E guide.

### Never assert a DOM element against `null` with `assert.equal`

In the desktop jsdom tests (`desktop/src/**/*.test.mjs`), write

```js
assert.ok(container.querySelector(sel) === null, "no header row");
```

**not** `assert.equal(container.querySelector(sel), null, ...)`. Both pass
identically, but when the `assert.equal` form *fails*, node serializes the
matched element and its entire subtree to build a diff. On a real transcript
that exhausts memory and the runner dies with SIGKILL after ~100s instead of
printing the assertion message — so a genuine regression is unreadable and
looks like a hang or an OOM in unrelated code. The `assert.ok(x === null)` form
fails in milliseconds with the message you wrote.

Comparing `getAttribute(...)` to `null` is fine — attributes are strings.

### PR Screenshots

> **Do NOT use `buzz upload`, the relay media endpoint, or any third-party
Expand Down
2 changes: 2 additions & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export default defineConfig({
"**/thread-reply-anchor-roleplay.spec.ts",
"**/threadpane-ultrawide.spec.ts",
"**/thread-focus-mode.spec.ts",
"**/agent-activity-cover.spec.ts",
"**/agent-activity-cover-screenshots.spec.ts",
"**/animated-avatar.spec.ts",
"**/reminders.spec.ts",
"**/reminder-click-repro.spec.ts",
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
import { useDueReminderBadgeCount } from "@/features/reminders/hooks";
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
import { requestFocusedThreadClose } from "@/features/channels/focusedThreadCloseRequest";
import { requestCoverDrawerClose } from "@/features/channels/coverDrawerCloseRequest";
import { CommunityRail } from "@/features/sidebar/ui/CommunityRail";
import { useChannelMutes } from "@/features/sidebar/lib/useChannelMutes";
import { useChannelStars } from "@/features/sidebar/lib/useChannelStars";
Expand Down Expand Up @@ -846,7 +846,7 @@ export function AppShell() {
addCommunityDialog.onOpenChange
}
onNewMessage={goNewMessage}
onBackgroundClick={requestFocusedThreadClose}
onBackgroundClick={requestCoverDrawerClose}
onCreateChannelOpenChange={setIsCreateChannelOpen}
onOpenAddCommunity={addCommunityDialog.openDialog}
onSendFeedback={() => setIsSendFeedbackOpen(true)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ export function CompactMessageSummary({
const { goChannel } = useAppNavigation();
const { openProfilePanel } = useProfilePanel();
const isCompactPreview = variant === "compactPreview";
const shouldClampBubble = !isCompactPreview;
const isConversation = variant === "conversation";
// Focus mode is a reading view, so sent messages remain fully visible there.
// The default activity surface keeps the compact clamp.
const shouldClampBubble = !isCompactPreview && !isConversation;
const [bubbleRef, hasBubbleOverflow] =
useTranscriptBubbleOverflow(shouldClampBubble);
const canOpenMessage = shouldClampBubble && messageLink !== null;
const canOpenMessage = !isCompactPreview && messageLink !== null;
const mutedTone = compactSummaryTone();
const avatarClassName = cn(
"mr-2 mt-1 shrink-0",
Expand Down Expand Up @@ -135,14 +138,20 @@ export function CompactMessageSummary({
testId="transcript-agent-sent-avatar"
/>
)}
<div className="flex min-w-0 flex-1 flex-col items-start gap-1">
<div
className={cn(
"flex min-w-0 flex-1 flex-col items-start gap-1",
isConversation && "pr-9",
)}
>
<div
className={cn(
"w-full min-w-0 rounded-2xl border px-3 py-2 shadow-sm",
"w-full min-w-0 rounded-2xl border px-3 py-2",
isCompactPreview
? "text-xs leading-4"
: "text-sm leading-relaxed",
shouldClampBubble && "relative max-h-36 overflow-hidden",
isConversation && "relative pr-4",
canOpenMessage &&
"group/bubble cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isCompactPreview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { TranscriptItem } from "../agentSessionTypes";
import { getBuzzToolInfo } from "../agentSessionToolCatalog";
import { buildCompactToolSummary } from "../agentSessionToolSummary";
import type { AgentTranscriptIdentityProps } from "../activityRenderClasses/types";
import { useIsInsideWorkBlockRail } from "../agentSessionTranscriptContext";
import {
formatTranscriptTimestampTitle,
getToolDurationDisplay,
Expand All @@ -34,6 +35,7 @@ export function ToolItem({
profiles?: UserProfileLookup;
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const insideWorkBlockRail = useIsInsideWorkBlockRail();
const hasArgs = Object.keys(item.args).length > 0;
const hasResult = item.result.trim().length > 0;
const canonicalToolName = item.buzzToolName ?? item.toolName;
Expand All @@ -57,7 +59,11 @@ export function ToolItem({
[],
);

if (compactSummary.presentation === "message") {
// Message presentations keep their readable bubble whenever they are standalone.
// The conversation grouping keeps message sends standalone rather than placing
// them on a work block rail; this guard preserves the muted row if another
// transcript composition explicitly embeds one in a rail.
if (compactSummary.presentation === "message" && !insideWorkBlockRail) {
return (
<div
className="not-prose w-full"
Expand Down
Loading
Loading