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
5 changes: 5 additions & 0 deletions apps/desktop/src/components/workpanel/SubagentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useFollowScroll } from "../../hooks/use-follow-scroll";
import { useTranscriptView } from "../../hooks/use-transcript-view";
import { useTranscriptSearchFocus } from "../../hooks/use-transcript-search-focus";
import { IconArrowDown } from "../icons";
import { DisclosureAnchorContext } from "../../lib/disclosure-anchor-context";
import { TooltipButton } from "../ui";
import { SubagentDetail } from "../ChatTranscript";

Expand Down Expand Up @@ -93,6 +94,7 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection
jumpToLatest,
scheduleFollowScroll,
releaseFollow,
disclosureAnchorNotifier,
} = useFollowScroll();

useLayoutEffect(() => {
Expand All @@ -113,6 +115,7 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection
});

return (
<DisclosureAnchorContext.Provider value={disclosureAnchorNotifier}>
<section
id="subagent-panel"
className="subagent-panel"
Expand All @@ -122,6 +125,7 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection
>
<div
ref={scrollRef}
data-scroll-owner="follow"
className="subagent-panel-scroll"
onScroll={handleScroll}
role="log"
Expand Down Expand Up @@ -163,5 +167,6 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection
: t("panel.subagentEmpty")}
</span>
</section>
</DisclosureAnchorContext.Provider>
);
}
2 changes: 2 additions & 0 deletions apps/desktop/src/features/chat/transcript/ActivityGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export const ActivityGroup = memo(function ActivityGroup({
toggle: toggleDisclosure,
collapse: collapseDisclosure,
claim: claimDisclosure,
titleRef,
} = useAutomaticDisclosure(live, revealRequest);
const [now, setNow] = useState(Date.now);
const [finishedAt, setFinishedAt] = useState<number | null>(null);
Expand Down Expand Up @@ -359,6 +360,7 @@ export const ActivityGroup = memo(function ActivityGroup({
}`}
>
<button
ref={titleRef}
className="tool-activity-header"
aria-expanded={open}
aria-controls={detailsId}
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/features/chat/transcript/ChatTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ResponseAnnotationOverlay } from "../../../components/ResponseAnnotatio
import { useTranscriptScroll } from "./hooks/useTranscriptScroll";
import type { TranscriptSearchTarget } from "../../../lib/transcript-reading";
import { TranscriptSearchContext } from "../../../lib/transcript-search-context";
import { DisclosureAnchorContext } from "../../../lib/disclosure-anchor-context";

export const ChatTranscript = memo(function ChatTranscript({
sessionId,
Expand Down Expand Up @@ -109,6 +110,7 @@ export const ChatTranscript = memo(function ChatTranscript({
revealEarlierHistory,
jumpToLatest,
navigateAnnotation,
disclosureAnchorNotifier,
} = useTranscriptScroll({
sessionId,
messages,
Expand Down Expand Up @@ -175,6 +177,7 @@ export const ChatTranscript = memo(function ChatTranscript({

return (
<TranscriptSearchContext.Provider value={searchTarget}>
<DisclosureAnchorContext.Provider value={disclosureAnchorNotifier}>
<div
className="thread-wrap"
ref={wrapRef}
Expand Down Expand Up @@ -206,6 +209,7 @@ export const ChatTranscript = memo(function ChatTranscript({
<div
className="thread-scroll"
ref={scrollRef}
data-scroll-owner="transcript"
onScroll={handleScroll}
role="log"
aria-live="polite"
Expand Down Expand Up @@ -317,6 +321,7 @@ export const ChatTranscript = memo(function ChatTranscript({
</TooltipButton>
) : null}
</div>
</DisclosureAnchorContext.Provider>
</TranscriptSearchContext.Provider>
);
});
12 changes: 11 additions & 1 deletion apps/desktop/src/features/chat/transcript/SubagentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
IconStop,
IconTarget,
} from "../../../components/icons";
import { useDisclosureAnchorNotifier } from "../../../lib/disclosure-anchor-context";
import { CopyButton } from "./shared";
import {
delegateAgentName,
Expand Down Expand Up @@ -198,6 +199,8 @@ export function SubagentDetail({
const taskLabelId = useId();
const taskBodyRef = useRef<HTMLDivElement>(null);
const [taskExpanded, setTaskExpanded] = useState(false);
const taskToggleRef = useRef<HTMLButtonElement | null>(null);
const notifyDisclosureAnchor = useDisclosureAnchorNotifier();
const [taskOverflow, setTaskOverflow] = useState(false);

useLayoutEffect(() => {
Expand Down Expand Up @@ -282,10 +285,17 @@ export function SubagentDetail({
{taskOverflow ? (
<button
type="button"
ref={taskToggleRef}
className="subagent-task-toggle"
aria-expanded={taskExpanded}
aria-controls={taskBodyId}
onClick={() => setTaskExpanded((expanded) => !expanded)}
onClick={() => {
// Expanding the brief changes the height of whichever scroller
// this card sits in (#324), so it holds the same reading
// position the tool and activity titles do.
notifyDisclosureAnchor?.(taskToggleRef.current);
setTaskExpanded((expanded) => !expanded);
}}
>
<span>
{taskExpanded
Expand Down
113 changes: 62 additions & 51 deletions apps/desktop/src/features/chat/transcript/ToolRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
IconStop,
} from "../../../components/icons";
import { TooltipButton } from "../../../components/ui";
import { DisclosureAnchorContext } from "../../../lib/disclosure-anchor-context";
import {
AssistantErrorMessage,
DisclosureCollapseRail,
Expand Down Expand Up @@ -153,6 +154,7 @@ export const ToolRow = memo(function ToolRow({
// through its status icon/label without expanding the payload automatically.
const disclosure = useAutomaticDisclosure(false);
const { open, toggle: toggleDisclosure, collapse: collapseDisclosure } = disclosure;
const titleRef = disclosure.titleRef;
const toggleRow = useCallback(() => {
onUserInteraction?.();
toggleDisclosure();
Expand Down Expand Up @@ -377,6 +379,7 @@ export const ToolRow = memo(function ToolRow({
) : (
<div className={`tool-row-head${runHead ? " is-run" : ""}`}>
<button
ref={titleRef}
className="tool-row-header"
aria-expanded={open}
aria-controls={hasDetails ? detailsId : undefined}
Expand Down Expand Up @@ -588,6 +591,7 @@ function SubagentRunFollow({
handleScroll,
jumpToLatest,
scheduleFollowScroll,
disclosureAnchorNotifier,
} = useFollowScroll();

useLayoutEffect(() => {
Expand All @@ -596,59 +600,66 @@ function SubagentRunFollow({
}, [items, scheduleFollowScroll, scrollable]);

return (
<div className="subagent-run-follow">
{/* The rows scroll inside the run rather than growing the transcript
* (D271). Follow sticks to the latest output while pinned (D302).
* Labelled and focusable so a keyboard reader can reach the scroll
* area the pointer can already use. */}
<div
ref={scrollRef}
className={`subagent-run-rows${scrollable ? "" : " is-panel-flow"}`}
role="group"
tabIndex={scrollable ? 0 : undefined}
aria-labelledby={headingId}
onScroll={scrollable ? handleScroll : undefined}
>
<div ref={contentRef}>
{items.map((item) =>
item.kind === "tool" ? (
<Fragment key={item.message.id}>
<ToolRow message={item.message} />
<ReviewChangeCard message={item.message} />
</Fragment>
) : item.kind === "thinking" ? (
<ThinkingRow
key={`thinking-${item.message.id}`}
message={item.message}
streaming={item.message.status === "streaming"}
/>
) : (
<div className="subagent-answer" data-message-id={item.message.id} key={`answer-${item.message.id}`}>
{item.message.content ? (
<div className="prose-chat">
<Markdown source={item.message.content} />
</div>
) : null}
{item.message.error ? (
<AssistantErrorMessage message={item.message} />
) : null}
</div>
),
)}
<DisclosureAnchorContext.Provider value={disclosureAnchorNotifier}>
<div className="subagent-run-follow">
{/* The rows scroll inside the run rather than growing the transcript
* (D271). Follow sticks to the latest output while pinned (D302).
* Labelled and focusable so a keyboard reader can reach the scroll
* area the pointer can already use. */}
<div
ref={scrollRef}
data-scroll-owner="follow"
className={`subagent-run-rows${scrollable ? "" : " is-panel-flow"}`}
role="group"
tabIndex={scrollable ? 0 : undefined}
aria-labelledby={headingId}
onScroll={scrollable ? handleScroll : undefined}
>
<div ref={contentRef}>
{items.map((item) =>
item.kind === "tool" ? (
<Fragment key={item.message.id}>
<ToolRow message={item.message} />
<ReviewChangeCard message={item.message} />
</Fragment>
) : item.kind === "thinking" ? (
<ThinkingRow
key={`thinking-${item.message.id}`}
message={item.message}
streaming={item.message.status === "streaming"}
/>
) : (
<div
className="subagent-answer"
data-message-id={item.message.id}
key={`answer-${item.message.id}`}
>
{item.message.content ? (
<div className="prose-chat">
<Markdown source={item.message.content} />
</div>
) : null}
{item.message.error ? (
<AssistantErrorMessage message={item.message} />
) : null}
</div>
),
)}
</div>
</div>
{scrollable && showJump ? (
<TooltipButton
type="button"
className="jump-latest-btn"
ariaLabel={t("chat.scrollToBottom")}
tooltip={t("chat.scrollToBottom")}
onClick={jumpToLatest}
>
<IconArrowDown size={14} />
</TooltipButton>
) : null}
</div>
{scrollable && showJump ? (
<TooltipButton
type="button"
className="jump-latest-btn"
ariaLabel={t("chat.scrollToBottom")}
tooltip={t("chat.scrollToBottom")}
onClick={jumpToLatest}
>
<IconArrowDown size={14} />
</TooltipButton>
) : null}
</div>
</DisclosureAnchorContext.Provider>
);
}

Expand Down
Loading