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 @@ -4,9 +4,9 @@ anchor: function App
created: 2026-10-15T03:38:04Z
norm: '1'
sig: 063d0f00fea02511
body_hash: fe9a115489fd2e3a
raw_hash: 4c1a930753d49cc2
vouched: 2026-10-23T14:00:31Z
body_hash: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
lines: 235-824
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function row
created: 2026-10-19T20:51:09Z
norm: '1'
sig: 3ee93dbf7c44f79a
body_hash: 8365145a4d0f53de
raw_hash: c6795856e4c91aa5
vouched: 2026-10-22T14:47:34Z
body_hash: 2b37c0b054a09374
raw_hash: e19d804ba1eb0f23
vouched: 2026-10-23T14:03:22Z
lines: 120-170
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function App
created: 2026-10-22T20:29:16Z
norm: '1'
sig: 063d0f00fea02511
body_hash: fe9a115489fd2e3a
raw_hash: 4c1a930753d49cc2
vouched: 2026-10-23T14:00:32Z
body_hash: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
lines: 330-1237
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01M5MH5GTCT454K45QT5RJE1H5
anchor: function App
created: 2026-10-23T14:03:19Z
norm: '1'
sig: 063d0f00fea02511
body_hash: d83ccb35867e607a
raw_hash: 90ddf2e883ff2d5c
vouched: 2026-10-23T14:03:54Z
lines: 334-1357
---

only a window that never chose a project jumps to a run that needs you, once; a saved project or channel always wins
15 changes: 14 additions & 1 deletion apps/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function row(
? "thinking"
: "idle",
unreadCount: outcome === "done" || outcome === "message" ? 1 : 0,
needsAttention: outcome === "failed" || outcome === "question",
needsAttention: outcome === "failed" || outcome === "question" ? outcome : undefined,
mentionCount: attention?.mentions || 0,
lastActivityAt: Math.floor((channel.active || channel.created) / 1000),
online,
Expand Down Expand Up @@ -531,6 +531,19 @@ export function App() {
page === "channels" ? channel?.id : undefined,
people,
);
// A window that never chose a project, such as one a sign-in link opened, starts at a run that
// needs you instead of the first project's Dashboard.
const landed = useRef(false);
useEffect(() => {
if (landed.current || project !== undefined || !hello.host) return;
const waiting = channels.find((value) => {
const mark = attention.get(value.id)?.mark;
return mark === "question" || mark === "failed";
});
if (!waiting) return;
landed.current = true;
select(waiting);
});
const online = (channel: Listing) =>
present(presence[channelProject(channel)], people[hello.user], channel.id);
let groups = projectGroups(visible, hello, people, now, collapsed, attention, pins, online);
Expand Down
14 changes: 13 additions & 1 deletion apps/app/src/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,26 @@ function TeammateRow({ teammate, onChannel }: {
);
}

const NEEDS = {
failed: "Needs you: a run you started failed",
question: "Needs you: the agent is waiting for your answer",
};

/** The sidebar row's marks: a raised hand for a run that needs you, mentions, then unread. */
function Marks({ attention }: { attention?: Attention }) {
if (!attention) return null;
const { mark, mentions } = attention;
return (
<span className="flex shrink-0 items-center gap-2">
{(mark === "failed" || mark === "question") && (
<IconHand className="size-4 text-muted-foreground" aria-label="Needs you" role="img" />
<Tooltip>
<TooltipTrigger
render={<span role="img" aria-label={NEEDS[mark]} className="grid" />}
>
<IconHand className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>{NEEDS[mark]}</TooltipContent>
</Tooltip>
)}
{mentions > 0 && (
<span
Expand Down
54 changes: 35 additions & 19 deletions packages/ui/src/components/session-item/session-item-glyph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,34 +132,50 @@ function MentionBadge({ count, label }: { count: number; label: string }) {

function AttentionWave({ label, mentionCount }: { label: string; mentionCount?: number }) {
return (
<span
aria-label={label}
className="grid size-4.5 place-items-center text-muted-foreground transition-opacity duration-150 ease-out motion-reduce:transition-none group-data-[status=active]/row:text-current/80"
>
<Badge
show={Boolean(mentionCount)}
className={cn(
"size-4.5 translate-x-0.5 -translate-y-0.5 place-items-center",
"[&>span:first-child]:grid [&>span:first-child]:size-4.5 [&>span:first-child]:place-items-center",
)}
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
aria-label={label}
className="grid size-4.5 place-items-center text-muted-foreground transition-opacity duration-150 ease-out motion-reduce:transition-none group-data-[status=active]/row:text-current/80"
/>
}
>
<HandWave
aria-hidden
className="size-4.5 overflow-visible origin-[70%_80%] animate-[attention-wave_3s_ease-in-out_infinite] motion-reduce:animate-none"
/>
</Badge>
</span>
<Badge
show={Boolean(mentionCount)}
className={cn(
"size-4.5 translate-x-0.5 -translate-y-0.5 place-items-center",
"[&>span:first-child]:grid [&>span:first-child]:size-4.5 [&>span:first-child]:place-items-center",
)}
>
<HandWave
aria-hidden
className="size-4.5 overflow-visible origin-[70%_80%] animate-[attention-wave_3s_ease-in-out_infinite] motion-reduce:animate-none"
/>
</Badge>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{label}
</TooltipContent>
</Tooltip>
);
}

function mentionText(count: number) {
return `${count} direct ${count === 1 ? "mention" : "mentions"}`;
}

const NEEDS = {
failed: "a run you started failed",
question: "the agent is waiting for your answer",
};

function attentionText(data: SidebarRow, showAttention: boolean) {
if (!showAttention) return mentionText(data.mentionCount);
if (data.mentionCount <= 0) return "Agent needs your attention";
return `${mentionText(data.mentionCount)} and agent needs your attention`;
if (!showAttention || !data.needsAttention) return mentionText(data.mentionCount);
const reason = `Needs you: ${NEEDS[data.needsAttention]}`;
if (data.mentionCount <= 0) return reason;
return `${reason}, and ${mentionText(data.mentionCount)}`;
}

function canShowMention({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export type SidebarRow = {
// Conversation (viewer-relative)
unreadCount: number;
mentionCount: number;
needsAttention?: boolean;
/** Why the row needs the viewer: a run they started failed or waits on their answer. */
needsAttention?: "failed" | "question";
lastSpeaker?: "user" | "agent";
lastActivityAt: UNIX;

Expand Down