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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function ConversationHomeList(props: {
finishedConversationIds: ReadonlySet<string>;
loading?: boolean;
timeZone: string;
waitingConversationIds: ReadonlySet<string>;
}) {
const [archivedConversation, setArchivedConversation] =
useState<Conversation>();
Expand Down Expand Up @@ -94,6 +95,7 @@ export function ConversationHomeList(props: {
key={section.key}
onArchiveError={handleArchiveError}
onArchived={handleArchived}
waitingConversationIds={props.waitingConversationIds}
section={section}
/>
))}
Expand Down Expand Up @@ -169,6 +171,7 @@ function ConversationCardSection(props: {
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
onArchived(conversation: Conversation): void;
section: ConversationSection;
waitingConversationIds: ReadonlySet<string>;
}) {
return (
<section aria-labelledby={`conversation-section-${props.section.key}`}>
Expand All @@ -188,6 +191,7 @@ function ConversationCardSection(props: {
key={conversation.id}
onArchiveError={props.onArchiveError}
onArchived={props.onArchived}
waitingOnViewer={props.waitingConversationIds.has(conversation.id)}
/>
))}
</div>
Expand All @@ -200,6 +204,7 @@ function ConversationCard(props: {
finishedSinceSeen: boolean;
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
onArchived(conversation: Conversation): void;
waitingOnViewer: boolean;
}) {
const conversation = props.conversation;
const archive = useArchiveConversation(conversation.id, {
Expand Down Expand Up @@ -231,6 +236,7 @@ function ConversationCard(props: {
finishedSinceSeen={props.finishedSinceSeen}
isPrivate={isPrivate}
status={status}
waitingOnViewer={props.waitingOnViewer}
/>
</span>
<h4 className="m-0 truncate font-display text-base font-medium leading-snug text-dashboard-text">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ import { ActiveIndicator } from "../components/ActiveIndicator";
import { cn } from "../styles";
import type { VisualStatus } from "../types";

/** Render a conversation status marker, with new completions taking priority. */
/**
* Render a conversation status marker. "Waiting on you" replaces every other
* marker, then new completions take priority.
*/
export function ConversationListStatusIcon(props: {
finishedSinceSeen: boolean;
isPrivate: boolean;
status: VisualStatus;
waitingOnViewer: boolean;
}) {
if (props.waitingOnViewer) {
return (
<span
aria-label="Waiting on you"
className="size-2 shrink-0 rounded-full bg-violet-300 shadow-[0_0_6px_rgba(196,181,253,0.7)]"
role="img"
/>
);
}

if (props.finishedSinceSeen) {
return (
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {
} from "../components/ParticipantAvatarStack";
import { ChatLayout } from "./ChatLayout";
import { ComposerDock } from "./ComposerDock";
import { ConversationReviewBar } from "../review-desk/ConversationReviewBar";
import { Transcript } from "./TranscriptView";
import { TranscriptLoading } from "./TranscriptLoading";
import type { TranscriptViewMode } from "./transcriptRenderModel";
Expand Down Expand Up @@ -803,6 +804,7 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
request={props.userInputRequest}
/>
) : null}
<ConversationReviewBar conversationId={props.conversationId} />
<PendingMailboxStack
cancelError={cancelError}
cancelPending={cancelPendingMessages.isPending}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function ConversationSidebar(props: {
query: string;
selectedId?: string;
timeZone: string;
waitingConversationIds: ReadonlySet<string>;
/**
* `panel` = split-pane dock with internal scroll.
* `landing` = flow nav under create compose (parent owns scroll).
Expand Down Expand Up @@ -186,6 +187,9 @@ export function ConversationSidebar(props: {
onArchiveError={handleArchiveError}
onArchived={handleArchived}
selected={entry.conversation.id === props.selectedId}
waitingOnViewer={props.waitingConversationIds.has(
entry.conversation.id,
)}
/>
)
}
Expand Down Expand Up @@ -271,6 +275,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
onArchived(conversation: Conversation): void;
selected: boolean;
waitingOnViewer: boolean;
}) {
const archive = useArchiveConversation(props.conversation.id, {
// Read the attempted direction from the mutation call, not the (possibly
Expand Down Expand Up @@ -316,6 +321,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
finishedSinceSeen={props.finishedSinceSeen}
isPrivate={isPrivate}
status={status}
waitingOnViewer={props.waitingOnViewer}
/>
</div>
<div className="col-start-2 row-start-1 min-w-0 truncate font-sans text-sm font-medium leading-snug text-dashboard-text">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { buildConversations, getDashboardTimeZone } from "../format";
import { useDebouncedValue } from "../debouncedValue";
import type { Conversation } from "../types";
import { ConversationPage } from "./ConversationPage";
import { useWaitingConversationIds } from "../review-desk/queries";
import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";

const CONVERSATION_PAGE_SIZE = 20;
Expand Down Expand Up @@ -61,6 +62,7 @@ export function ConversationWorkspace() {
useEffect(() => {
if (page > totalPages) setPage(totalPages);
}, [page, totalPages]);
const waitingConversationIds = useWaitingConversationIds();
const { finishedConversationIds, markRead } =
useConversationFinishedIndicators(
conversations,
Expand Down Expand Up @@ -107,6 +109,7 @@ export function ConversationWorkspace() {
finishedConversationIds={finishedConversationIds}
loading={feed.isPending}
timeZone={getDashboardTimeZone()}
waitingConversationIds={waitingConversationIds}
/>
<PagePagination
className="pt-1"
Expand Down Expand Up @@ -135,6 +138,7 @@ export function ConversationWorkspace() {
query={query}
selectedId={selectedId}
timeZone={getDashboardTimeZone()}
waitingConversationIds={waitingConversationIds}
/>
</div>
<section
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TooltipTrigger,
} from "../components/ui/tooltip";
import { formatMs } from "../format";
import { reviewItemIdFromToolCall } from "../review-desk/reviewItemFormat";
import type { RenderedTranscriptEntry } from "./transcriptRenderModel";
import { cn } from "../styles";
import { useTranscriptSearch } from "./transcriptSearch";
Expand All @@ -29,6 +30,10 @@ export function isCollapsibleActivityEntry(
// Child agent rows stay in the main view, like their result events.
if (entry.kind === "subagent") return false;
if (entry.kind === "message") return false;
// A review card stays visible where the agent added the item.
if (entry.kind === "tool" && reviewItemIdFromToolCall(entry.part)) {
return false;
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from "../format";
import type { TranscriptViewToolCallPart } from "../types";
import { ShimmerText } from "../components/ShimmerText";
import { TranscriptReviewItemCard } from "../review-desk/ReviewItemCard";
import { reviewItemIdFromToolCall } from "../review-desk/reviewItemFormat";
import { cn } from "../styles";
import { ToolFrame } from "./ToolFrame";
import { toolCallPreview } from "./toolCallPreview";
Expand Down Expand Up @@ -64,7 +66,17 @@ export const TranscriptToolView = memo(function TranscriptToolView(props: {
</ToolFrame>
);

return <div className="min-w-0">{frame}</div>;
const reviewItemId = reviewItemIdFromToolCall(props.part);
return (
<div className="min-w-0">
{frame}
{reviewItemId ? (
<div className="mt-2">
<TranscriptReviewItemCard itemId={reviewItemId} />
</div>
) : null}
</div>
);
});

function ToolSignature(props: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function RecentConversationRow(props: { conversation: Conversation }) {
finishedSinceSeen={false}
isPrivate={conversation.visibility === "private"}
status={visualStatusForConversation(conversation)}
waitingOnViewer={false}
/>
</span>
<div className="min-w-0 truncate font-display text-sm text-dashboard-text">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Card } from "../../components/layout/Card";
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
import { buildConversations, getDashboardTimeZone } from "../../format";
import { useDebouncedValue } from "../../debouncedValue";
import { useWaitingConversationIds } from "../../review-desk/queries";
import { CodeActivityChart } from "./CodeActivityChart";
import { CodeChangeRow } from "./CodeChangeRow";
import { repositoryActivity } from "./RepositorySections";
Expand All @@ -32,6 +33,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
const search = useDebouncedValue(query.trim());
const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
const conversations = buildConversations(feed.data?.conversations ?? []);
const waitingConversationIds = useWaitingConversationIds();
useEffect(() => {
setPage(1);
}, [search]);
Expand Down Expand Up @@ -59,6 +61,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
finishedConversationIds={noFinishedConversations}
loading={!feed.data}
timeZone={getDashboardTimeZone()}
waitingConversationIds={waitingConversationIds}
/>
)}
<PagePagination
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ChevronRight } from "lucide-react";
import { useState } from "react";

import { Drawer } from "../components/Drawer";
import { ReviewItemCard } from "./ReviewItemCard";
import { useReviewItemsData } from "./queries";

function itemCountLabel(count: number): string {
return count === 1 ? "1 open review item" : `${count} open review items`;
}

/**
* Show how many open Review items are linked to a Conversation, above its
* message box. The bar opens a side panel where the viewer can act on each.
*/
export function ConversationReviewBar(props: { conversationId: string }) {
const [open, setOpen] = useState(false);
const items =
useReviewItemsData({
archived: false,
conversationId: props.conversationId,
view: "all",
}).data?.items ?? [];
if (items.length === 0) return null;
const label = itemCountLabel(items.length);
return (
<>
<button
className="mb-2 flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] px-3 py-2 text-left font-sans text-sm text-dashboard-text transition-colors hover:bg-violet-300/[0.1]"
onClick={() => setOpen(true)}
type="button"
>
<span className="flex items-center gap-2">
<span
aria-hidden="true"
className="size-2 shrink-0 rounded-full bg-violet-300"
/>
{label}
</span>
<ChevronRight aria-hidden="true" size={16} />
</button>
{open ? (
<Drawer
closeLabel="Close review items"
header={
<h2 className="m-0 font-sans text-base font-semibold">{label}</h2>
}
label="Review items"
onClose={() => setOpen(false)}
openKey={props.conversationId}
width="narrow"
>
<div className="grid gap-3">
{items.map((item) => (
<ReviewItemCard item={item} key={item.id} />
))}
</div>
</Drawer>
) : null}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Link } from "react-router";
import type { ReviewItemPayload } from "@sentry/junior/api/schema";

import { StatusChip } from "../components/StatusChip";
import { ReviewItemActions } from "./ReviewItemActions";
import { useReviewItemData } from "./queries";
import { reviewItemPath } from "./reviewItemFormat";

function statusLabel(item: ReviewItemPayload): string {
if (item.status === "open") {
return item.kind === "action" ? "Needs an answer" : "Notice";
}
return item.status === "done" ? "Done" : "Dismissed";
}

/**
* Show one Review item as a card: its title, status, and the viewer's
* actions, with a link to the review desk.
*/
export function ReviewItemCard(props: { item: ReviewItemPayload }) {
const { item } = props;
return (
<section
aria-label={`Review item: ${item.title}`}
className="grid gap-2 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] px-3 py-2.5 font-sans text-sm text-dashboard-text"
>
<header className="flex min-w-0 items-baseline justify-between gap-3">
<span className="min-w-0 truncate font-medium">{item.title}</span>
<StatusChip
className="shrink-0"
size="compact"
tone={item.status === "open" ? "accent" : "neutral"}
>
{statusLabel(item)}
</StatusChip>
</header>
<ReviewItemActions item={item} />
<Link className="w-fit text-xs" to={reviewItemPath(item.id)}>
Open on the review desk
</Link>
</section>
);
}

/**
* Show the card of the Review item that a tool call added. It shows nothing
* when the viewer cannot see the item.
*/
export function TranscriptReviewItemCard(props: { itemId: string }) {
const item = useReviewItemData(props.itemId).data;
return item ? <ReviewItemCard item={item} /> : null;
}
24 changes: 24 additions & 0 deletions packages/junior-dashboard/src/client/review-desk/queries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import {
keepPreviousData,
useMutation,
Expand All @@ -9,6 +10,7 @@ import {
reviewItemCountSchema,
reviewItemListSchema,
reviewItemSchema,
reviewItemWaitingConversationsSchema,
type ReviewItemActionBody,
} from "@sentry/junior/api/schema";

Expand Down Expand Up @@ -111,3 +113,25 @@ export function useReviewItemAction() {
queryClient.invalidateQueries({ queryKey: REVIEW_ITEMS_QUERY_KEY }),
});
}

const NO_CONVERSATIONS: ReadonlySet<string> = new Set();

/**
* Return the Conversations that wait on the viewer. Today only the review
* desk sets this: an open linked item assigned to the viewer or Everyone.
*/
export function useWaitingConversationIds(): ReadonlySet<string> {
const query = useQuery({
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "waiting-conversations"],
queryFn: ({ signal }) =>
fetchDashboardJson(
reviewItemWaitingConversationsSchema,
"/api/review-items/waiting-conversations",
signal,
),
refetchInterval: COUNT_REFRESH_MS,
retry: false,
});
const ids = query.data?.conversationIds;
return useMemo(() => (ids ? new Set(ids) : NO_CONVERSATIONS), [ids]);
}
Loading
Loading