Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,8 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
const userMessageNavigationByHistoryId = useMemo(() => {
const userHistoryIds: string[] = [];
for (const message of deferredMessages) {
if (message.type === "user") {
// Monitor wake events should not interrupt navigation between human prompts.
if (message.type === "user" && message.bashMonitorWake == null) {
userHistoryIds.push(message.historyId);
}
}
Expand Down
72 changes: 72 additions & 0 deletions src/browser/features/Messages/BashMonitorWakeMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useState, type ReactElement } from "react";
import { ChevronRight, Radar } from "lucide-react";
import { cn } from "@/common/lib/utils";
import type { BashMonitorWakeDisplayRecord, DisplayedMessage } from "@/common/types/message";
import { TranscriptQuoteRoot } from "./TranscriptQuoteBoundary";

interface BashMonitorWakeMessageProps {
message: DisplayedMessage & { type: "user" };
className?: string;
}

function summarizeRecords(records: BashMonitorWakeDisplayRecord[]): string {
if (records.length === 1) {
const record = records[0];
return record.kind === "monitor-lost"
? `${record.displayName} monitor stopped after restart`
: `${record.displayName} monitor matched`;
}

const matchCount = records.filter((record) => record.kind === "match").length;
if (matchCount === records.length) {
return `${records.length} background monitors matched`;
}
if (matchCount === 0) {
return `${records.length} background monitors stopped after restart`;
}
return `${records.length} background monitor updates`;
}

/**
* Monitor wakes are machine-authored events, not user prompts. Keep them visible
* for transcript continuity without giving them a full user bubble, metadata row,
* or duplicate status badge. The model-facing prompt stays available on demand.
*/
export function BashMonitorWakeMessage(props: BashMonitorWakeMessageProps): ReactElement {
const [expanded, setExpanded] = useState(false);
const records = props.message.bashMonitorWake?.records ?? [];
const summary = summarizeRecords(records);

return (
<div
className={cn("my-2 flex min-w-0 flex-col items-center", props.className)}
data-message-block
data-bash-monitor-wake
>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((previous) => !previous)}
className="text-muted hover:bg-muted/10 hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background flex max-w-full cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
>
<Radar aria-hidden="true" className="size-3.5 shrink-0" />
<span className="truncate">{summary}</span>
<ChevronRight
aria-hidden="true"
className={cn(
"size-3 shrink-0 transition-transform duration-200",
expanded && "rotate-90"
)}
/>
<span className="sr-only">{expanded ? "Hide details" : "Show details"}</span>
</button>
{expanded && (
<TranscriptQuoteRoot text={props.message.content} className="mt-1.5 w-full">
<pre className="text-muted bg-muted/5 border-border max-h-[40vh] overflow-y-auto rounded-md border p-2 text-xs leading-relaxed whitespace-pre-wrap">
{props.message.content}
</pre>
</TranscriptQuoteRoot>
)}
</div>
);
}
72 changes: 0 additions & 72 deletions src/browser/features/Messages/BashMonitorWakeMessageContent.tsx

This file was deleted.

17 changes: 12 additions & 5 deletions src/browser/features/Messages/MessageRenderer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,19 @@ const BASH_MONITOR_WAKE_LOST_PROMPT = [
].join("\n");

/**
* Bash monitor wake messages render as compact cards: title + per-monitor
* summary with the raw prompt collapsed behind a "Show details" toggle.
* The play expands the first (match) card so the snapshot covers both the
* expanded prompt and the collapsed monitor-lost card below it.
* Bash monitor wakes render as quiet inline events instead of user bubbles.
* The play expands the first (match) event so the snapshot covers both the
* on-demand raw prompt and the collapsed monitor-lost event below it.
*/
export const BashMonitorWakeMessages: AppStory = {
globals: {
viewport: { value: "mobile1", isRotated: false },
},
parameters: {
pixel: {
matrix: { themes: ["dark", "light"], viewports: ["phone", "laptop"] },
},
},
render: () => (
<AppWithMocks
setup={() => {
Expand Down Expand Up @@ -473,7 +480,7 @@ export const BashMonitorWakeMessages: AppStory = {
() => {
const found = canvas.getAllByRole("button", { name: /show details/i });
if (found.length !== 2) {
throw new Error(`Expected 2 collapsed wake cards, found ${found.length}`);
throw new Error(`Expected 2 collapsed monitor events, found ${found.length}`);
}
return found;
},
Expand Down
26 changes: 17 additions & 9 deletions src/browser/features/Messages/MessageRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,21 +424,25 @@ This is a condition-driven wake-up. Continue from this event.`;
};
}

test("collapses the raw wake prompt behind a details toggle by default", () => {
const { getByText, getByRole, queryByText } = render(
test("renders a quiet inline event with the raw wake prompt collapsed", () => {
const { container, getByText, getByRole, queryByRole, queryByText } = render(
<TooltipProvider>
<MessageRenderer message={createWakeMessage()} />
</TooltipProvider>
);

// Compact summary is visible; the raw prompt body stays hidden until expanded.
expect(getByText("Dev Server · /error|ready/")).toBeDefined();
expect(getByRole("button", { name: /show details/i }).getAttribute("aria-expanded")).toBe(
"false"
);
expect(getByText("Dev Server monitor matched")).toBeDefined();
const toggle = getByRole("button", { name: /show details/i });
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(toggle.className).toContain("focus-visible:ring-2");
expect(queryByText(/failed to load tailwind config/)).toBeNull();
expect(queryByText(/condition-driven wake-up/)).toBeNull();
// The dedicated pill replaces the generic synthetic "auto" pill.

// A machine-authored event should not look or behave like a user prompt.
expect(container.querySelector("[data-bash-monitor-wake]")).not.toBeNull();
expect(container.querySelector("[data-message-meta]")).toBeNull();
expect(queryByRole("button", { name: "Copy" })).toBeNull();
expect(queryByText("monitor wake")).toBeNull();
expect(queryByText("auto")).toBeNull();
});

Expand All @@ -452,7 +456,11 @@ This is a condition-driven wake-up. Continue from this event.`;
const toggle = getByRole("button", { name: /show details/i });
fireEvent.click(toggle);
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(queryByText(/failed to load tailwind config/)).toBeDefined();
const details = queryByText(/failed to load tailwind config/);
expect(details).toBeDefined();
expect(
details?.closest("[data-transcript-quote-root]")?.getAttribute("data-transcript-quote-text")
).toBe(wakePrompt);

fireEvent.click(toggle);
expect(toggle.getAttribute("aria-expanded")).toBe("false");
Expand Down
22 changes: 13 additions & 9 deletions src/browser/features/Messages/MessageRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { TaskReportLinking } from "@/browser/utils/messages/taskReportLinki
import type { ReviewNoteData } from "@/common/types/review";
import type { EditingMessageState } from "@/browser/utils/chatEditing";
import { UserMessage, type UserMessageNavigation } from "./UserMessage";
import { BashMonitorWakeMessage } from "./BashMonitorWakeMessage";
import { AssistantMessage } from "./AssistantMessage";
import { ToolMessage } from "./ToolMessage";
import { ReasoningMessage } from "./ReasoningMessage";
Expand Down Expand Up @@ -87,15 +88,18 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
// Route based on message type
switch (message.type) {
case "user":
renderedMessage = (
<UserMessage
message={message}
className={className}
onEdit={onEditUserMessage}
isCompacting={isCompacting}
navigation={userMessageNavigation}
/>
);
renderedMessage =
message.bashMonitorWake != null ? (
<BashMonitorWakeMessage message={message} className={className} />
) : (
<UserMessage
message={message}
className={className}
onEdit={onEditUserMessage}
isCompacting={isCompacting}
navigation={userMessageNavigation}
/>
);
break;
case "assistant":
renderedMessage = (
Expand Down
14 changes: 0 additions & 14 deletions src/browser/features/Messages/UserMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { ButtonConfig } from "./MessageWindow";
import { MessageWindow } from "./MessageWindow";
import { UserMessageContent } from "./UserMessageContent";
import { GoalSyntheticMessageContent } from "./GoalSyntheticMessageContent";
import { BashMonitorWakeMessageContent } from "./BashMonitorWakeMessageContent";
import {
formatSubagentStructuredOutput,
parseSubagentReportEnvelope,
Expand All @@ -36,7 +35,6 @@ import {
ClipboardCheck,
MessageCircleQuestion,
Pencil,
Radar,
Target,
} from "lucide-react";
import {
Expand Down Expand Up @@ -96,7 +94,6 @@ export const UserMessage: React.FC<UserMessageProps> = ({
const isSynthetic = message.isSynthetic === true;
const isGoalContinuation = message.isGoalContinuation === true;
const isBudgetLimitWrapup = message.isBudgetLimitWrapup === true;
const bashMonitorWake = message.bashMonitorWake;
const content = message.content;
const visibleContent = stripStagedAttachmentNotice(content);
// Only backend-authored synthetic messages may opt into protocol-aware presentation. A user who
Expand Down Expand Up @@ -242,13 +239,6 @@ export const UserMessage: React.FC<UserMessageProps> = ({
goal continuation
</span>
);
} else if (bashMonitorWake) {
label = (
<span className="bg-muted/20 text-muted flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-[10px] font-medium uppercase">
<Radar aria-hidden="true" className="h-3 w-3" />
monitor wake
</span>
);
} else if (subagentReport) {
const isInProgress = subagentReport.status === "in_progress";
label = (
Expand Down Expand Up @@ -292,10 +282,6 @@ export const UserMessage: React.FC<UserMessageProps> = ({
kind={isBudgetLimitWrapup ? "budget-limit" : "continuation"}
/>
);
} else if (bashMonitorWake) {
renderedContent = (
<BashMonitorWakeMessageContent content={content} records={bashMonitorWake.records} />
);
} else if (subagentReport) {
renderedContent = <SubagentReportMessageContent report={subagentReport} />;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function buildUserRow(muxMetadata: MuxMessageMetadata) {
}

describe("buildDisplayedMessagesForMessage bash monitor wake metadata", () => {
test("surfaces well-formed wake records for compact rendering", () => {
test("surfaces well-formed wake records for inline event rendering", () => {
const row = buildUserRow({
type: "bash-monitor-wake",
records: [
Expand Down
Loading