@saadnvd1/agent-os 0.3.603 → 0.3.605

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/app/page.tsx +3 -8
  2. package/components/Bookmarks/ChatBookmarksButton.tsx +4 -0
  3. package/components/Bus/MessagesPanel.tsx +155 -0
  4. package/components/Bus/index.ts +1 -2
  5. package/components/Chat/ChatPanel.tsx +9 -1
  6. package/components/CommandPalette/useAppCommands.ts +12 -4
  7. package/components/Features/FeaturesSection.tsx +2 -2
  8. package/components/Inbox/InboxButton.tsx +39 -0
  9. package/components/Inbox/InboxDialog.tsx +139 -0
  10. package/components/Inbox/NeedsYouList.tsx +120 -0
  11. package/components/Inbox/index.ts +2 -0
  12. package/components/LumifyHub/index.ts +0 -1
  13. package/components/Pane/DesktopTabBar.tsx +2 -0
  14. package/components/Pane/PaneMenu.test.ts +118 -0
  15. package/components/Pane/PaneMenu.tsx +37 -0
  16. package/components/SessionList/SidebarHeader.tsx +7 -4
  17. package/components/Tasks/TaskRow.tsx +3 -3
  18. package/components/Tasks/TasksPanel.tsx +63 -0
  19. package/components/Tasks/index.ts +1 -1
  20. package/components/views/DesktopAppActions.tsx +10 -63
  21. package/components/views/DesktopView.tsx +1 -11
  22. package/hooks/useInbox.ts +69 -0
  23. package/lib/inbox/counts.ts +140 -0
  24. package/package.json +1 -1
  25. package/scripts/check-bundle.mjs +25 -10
  26. package/scripts/check-bundle.test.ts +2 -2
  27. package/stores/inboxUi.ts +60 -0
  28. package/stores/needsYouFocus.ts +11 -3
  29. package/components/Bus/MessagesButton.tsx +0 -25
  30. package/components/Bus/MessagesDialog.tsx +0 -176
  31. package/components/LumifyHub/Docs/DocsButton.tsx +0 -31
  32. package/components/NotificationsMenu.tsx +0 -86
  33. package/components/Tasks/TasksButton.tsx +0 -28
  34. package/components/Tasks/TasksDialog.tsx +0 -75
  35. package/stores/busUi.ts +0 -12
  36. package/stores/tasksUi.ts +0 -14
package/app/page.tsx CHANGED
@@ -65,8 +65,8 @@ import { approvalsModeQuery } from "@/data/approvals";
65
65
 
66
66
  // The app's dialogs open from their stores, so none is needed for the first
67
67
  // paint: their code loads right after it.
68
- const TasksDialog = dynamic(
69
- () => import("@/components/Tasks/TasksDialog").then((m) => m.TasksDialog),
68
+ const InboxDialog = dynamic(
69
+ () => import("@/components/Inbox/InboxDialog").then((m) => m.InboxDialog),
70
70
  { ssr: false }
71
71
  );
72
72
  const AddProjectDialog = dynamic(
@@ -88,10 +88,6 @@ const WelcomeDialog = dynamic(
88
88
  import("@/components/Welcome/WelcomeDialog").then((m) => m.WelcomeDialog),
89
89
  { ssr: false }
90
90
  );
91
- const MessagesDialog = dynamic(
92
- () => import("@/components/Bus/MessagesDialog").then((m) => m.MessagesDialog),
93
- { ssr: false }
94
- );
95
91
  const BookmarksDialog = dynamic(
96
92
  () =>
97
93
  import("@/components/Bookmarks/BookmarksDialog").then(
@@ -153,10 +149,9 @@ const AppDialogs = memo(function AppDialogs({
153
149
  }) {
154
150
  return (
155
151
  <>
156
- <TasksDialog />
152
+ <InboxDialog />
157
153
  <WelcomeDialog notifications={notifications} />
158
154
  <AddProjectDialog />
159
- <MessagesDialog />
160
155
  <ArchivedDialog />
161
156
  <BookmarksDialog />
162
157
  <ChatBookmarksDialog />
@@ -9,11 +9,15 @@ import { bookmarksUiActions } from "@/stores/bookmarksUi";
9
9
  export function ChatBookmarksButton({
10
10
  sessionId,
11
11
  className,
12
+ hideWhenEmpty,
12
13
  }: {
13
14
  sessionId: string;
14
15
  className?: string;
16
+ // The desktop bar shows it only once the chat has some.
17
+ hideWhenEmpty?: boolean;
15
18
  }) {
16
19
  const { data = [] } = useSessionBookmarks(sessionId);
20
+ if (hideWhenEmpty && data.length === 0) return null;
17
21
  return (
18
22
  <button
19
23
  type="button"
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import { useBusMessages, useBusPeers, useSendMessage } from "@/data/bus";
14
+ import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ const ALL = "all";
18
+
19
+ export function MessagesPanel() {
20
+ const { data: messages = [], isPending } = useBusMessages();
21
+ const { data: peers = [] } = useBusPeers();
22
+ const send = useSendMessage();
23
+ const { reset } = send;
24
+ // A delivery result belongs to that send only.
25
+ useEffect(() => reset, [reset]);
26
+ const [filter, setFilter] = useState(ALL);
27
+ const [to, setTo] = useState("");
28
+ const [body, setBody] = useState("");
29
+
30
+ const label = (p: { name: string; projectName: string | null }) =>
31
+ p.projectName ? `${p.projectName} / ${p.name}` : p.name;
32
+ const shown = useMemo(
33
+ () =>
34
+ filter === ALL
35
+ ? messages
36
+ : messages.filter((m) => m.fromId === filter || m.toId === filter),
37
+ [messages, filter]
38
+ );
39
+ const recipient = to || (filter !== ALL ? filter : "");
40
+
41
+ return (
42
+ <div className="flex min-h-0 flex-1 flex-col gap-3">
43
+ <p className="text-muted-foreground text-xs">
44
+ What your agent sessions are saying to each other. Agents use the{" "}
45
+ <code className="font-mono text-xs">aos</code> command.
46
+ </p>
47
+
48
+ <Select
49
+ value={filter}
50
+ onValueChange={(v) => {
51
+ send.reset();
52
+ setFilter(v);
53
+ }}
54
+ >
55
+ <SelectTrigger
56
+ aria-label="Filter by session"
57
+ className="w-full sm:w-72"
58
+ >
59
+ <SelectValue />
60
+ </SelectTrigger>
61
+ <SelectContent>
62
+ <SelectItem value={ALL}>All sessions</SelectItem>
63
+ {peers.map((p) => (
64
+ <SelectItem key={p.id} value={p.id}>
65
+ {label(p)}
66
+ </SelectItem>
67
+ ))}
68
+ </SelectContent>
69
+ </Select>
70
+
71
+ <div className="min-h-40 flex-1 space-y-3 overflow-y-auto pr-1">
72
+ {isPending && (
73
+ <div className="bg-muted/40 h-16 animate-pulse rounded-xl" />
74
+ )}
75
+ {!isPending && shown.length === 0 && (
76
+ <p className="text-muted-foreground py-8 text-center text-sm">
77
+ No messages yet.
78
+ </p>
79
+ )}
80
+ {shown.map((m) => (
81
+ <div key={m.id} className="space-y-1">
82
+ <div className="flex items-baseline gap-2 text-xs">
83
+ <span className={cn("font-medium", !m.fromId && "text-primary")}>
84
+ {m.fromName}
85
+ </span>
86
+ <span className="text-muted-foreground/60">→</span>
87
+ <span className="font-medium">{m.toName}</span>
88
+ <span className="text-muted-foreground/60 ml-auto font-mono tabular-nums">
89
+ {compactTimeAgo(fromSqliteTime(m.createdAt))}
90
+ </span>
91
+ </div>
92
+ <p className="bg-foreground/[0.04] rounded-lg px-3 py-2 text-sm whitespace-pre-wrap">
93
+ {m.body}
94
+ </p>
95
+ </div>
96
+ ))}
97
+ </div>
98
+
99
+ <form
100
+ onSubmit={(e) => {
101
+ e.preventDefault();
102
+ send.mutate(
103
+ { to: recipient, body },
104
+ { onSuccess: () => setBody("") }
105
+ );
106
+ }}
107
+ className="space-y-2"
108
+ >
109
+ <Select
110
+ value={recipient}
111
+ onValueChange={(v) => {
112
+ send.reset();
113
+ setTo(v);
114
+ }}
115
+ >
116
+ <SelectTrigger aria-label="Send to">
117
+ <SelectValue placeholder="Send to a session..." />
118
+ </SelectTrigger>
119
+ <SelectContent>
120
+ {peers.map((p) => (
121
+ <SelectItem key={p.id} value={p.id}>
122
+ {label(p)}
123
+ {!p.running && " (not running)"}
124
+ </SelectItem>
125
+ ))}
126
+ </SelectContent>
127
+ </Select>
128
+ <div className="flex gap-2">
129
+ <Textarea
130
+ aria-label="Message"
131
+ value={body}
132
+ onChange={(e) => setBody(e.target.value)}
133
+ placeholder="Message as you"
134
+ rows={2}
135
+ className="min-h-11"
136
+ />
137
+ <Button
138
+ type="submit"
139
+ disabled={!recipient || !body.trim() || send.isPending}
140
+ >
141
+ Send
142
+ </Button>
143
+ </div>
144
+ {send.error && (
145
+ <p className="text-destructive text-xs">{send.error.message}</p>
146
+ )}
147
+ {send.data?.delivery.state === "failed" && (
148
+ <p className="text-destructive text-xs">
149
+ Saved to its inbox, but not delivered: {send.data.delivery.why}
150
+ </p>
151
+ )}
152
+ </form>
153
+ </div>
154
+ );
155
+ }
@@ -1,2 +1 @@
1
- export { MessagesDialog } from "./MessagesDialog";
2
- export { MessagesButton } from "./MessagesButton";
1
+ export { MessagesPanel } from "./MessagesPanel";
@@ -59,7 +59,7 @@ import { EventGroup } from "./EventTurns";
59
59
  import { useGoToItem } from "./useGoToItem";
60
60
  import { NeedsYouStrip } from "@/components/Orchestrator/NeedsYouStrip";
61
61
  import { useResolveQuestion } from "@/data/orchestrators";
62
- import { needsYouFocusActions } from "@/stores/needsYouFocus";
62
+ import { needsYouFocus, needsYouFocusActions } from "@/stores/needsYouFocus";
63
63
  import { hrefWithSession } from "@/lib/session-url";
64
64
  import { isQuestionTool } from "@/lib/chat/turns";
65
65
  import { useViewport } from "@/hooks/useViewport";
@@ -560,6 +560,14 @@ export function ChatPanel({
560
560
  const [replyTo, setReplyTo] = useState<{ id: number; text: string } | null>(
561
561
  null
562
562
  );
563
+ // "Reply" on a question in the Inbox, once this is that orchestrator's chat.
564
+ const { reply: pendingReply } = useSnapshot(needsYouFocus);
565
+ useEffect(() => {
566
+ if (!pendingReply || pendingReply.workspaceId !== orchestratorOf) return;
567
+ setReplyTo({ id: pendingReply.id, text: pendingReply.text });
568
+ setInsert({ text: quoteMarkdown(pendingReply.text), at: Date.now() });
569
+ needsYouFocusActions.clearReply();
570
+ }, [pendingReply, orchestratorOf]);
563
571
  const find = useFind({
564
572
  foldEvents: !!orchestratorOf,
565
573
  search,
@@ -10,6 +10,7 @@ import {
10
10
  FolderGit2,
11
11
  FolderPlus,
12
12
  Gauge,
13
+ Inbox,
13
14
  LayoutGrid,
14
15
  ListTodo,
15
16
  MessageSquare,
@@ -29,10 +30,9 @@ import { recipesUiActions } from "@/stores/recipesUi";
29
30
  import { usePaletteCommands } from "@/hooks/usePaletteCommands";
30
31
  import { archivedUiActions } from "@/stores/archivedUi";
31
32
  import { bookmarksUiActions } from "@/stores/bookmarksUi";
32
- import { busUiActions } from "@/stores/busUi";
33
+ import { inboxUiActions } from "@/stores/inboxUi";
33
34
  import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
34
35
  import { sidebarUi, sidebarUiActions } from "@/stores/sidebarUi";
35
- import { tasksUiActions } from "@/stores/tasksUi";
36
36
  import { newDraft } from "@/stores/drafts";
37
37
  import { useAddProject } from "@/components/Projects/AddProject/useAddProject";
38
38
  import { useAddExistingProject } from "@/components/Projects/AddProject/useAddExistingProject";
@@ -189,12 +189,20 @@ export function useAppCommands({
189
189
  icon: Scale,
190
190
  run: () => usageUiActions.open("compare"),
191
191
  },
192
+ {
193
+ id: "app.inbox",
194
+ title: "Inbox",
195
+ group: "Go to",
196
+ keywords: ["needs you", "notifications", "asks", "questions", "waiting"],
197
+ icon: Inbox,
198
+ run: () => inboxUiActions.open(),
199
+ },
192
200
  {
193
201
  id: "app.tasks",
194
202
  title: "Tasks",
195
203
  group: "Go to",
196
204
  icon: ListTodo,
197
- run: tasksUiActions.openPanel,
205
+ run: () => inboxUiActions.open("tasks"),
198
206
  },
199
207
  {
200
208
  id: "app.messages",
@@ -202,7 +210,7 @@ export function useAppCommands({
202
210
  group: "Go to",
203
211
  keywords: ["bus", "peers"],
204
212
  icon: MessagesSquare,
205
- run: busUiActions.open,
213
+ run: () => inboxUiActions.open("messages"),
206
214
  },
207
215
  {
208
216
  id: "app.archived",
@@ -6,7 +6,7 @@ import { useFeaturesQuery, useReleasePiece } from "@/data/features";
6
6
  import type { FeatureView } from "@/lib/plans/view";
7
7
  import { duration, money } from "@/components/Recipes/format";
8
8
  import { sessionOpenActions } from "@/stores/sessionOpen";
9
- import { tasksUiActions } from "@/stores/tasksUi";
9
+ import { inboxUiActions } from "@/stores/inboxUi";
10
10
  import { cn } from "@/lib/utils";
11
11
 
12
12
  function FeatureCard({ f }: { f: FeatureView }) {
@@ -66,7 +66,7 @@ function FeatureCard({ f }: { f: FeatureView }) {
66
66
  className="text-primary -mr-2 flex min-h-11 items-center px-2"
67
67
  onClick={() => {
68
68
  sessionOpenActions.request(p.sessionId!);
69
- tasksUiActions.setPanelOpen(false);
69
+ inboxUiActions.setOpen(false);
70
70
  }}
71
71
  >
72
72
  <ArrowUpRight className="h-3.5 w-3.5" />
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { Inbox } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from "@/components/ui/tooltip";
10
+ import { useInbox } from "@/hooks/useInbox";
11
+ import { inboxUiActions } from "@/stores/inboxUi";
12
+
13
+ // One button for everything waiting on you: sessions, tasks, asks and
14
+ // questions, messages.
15
+ export function InboxButton() {
16
+ const { counts } = useInbox();
17
+ const label = counts.total > 0 ? `Inbox (${counts.total})` : "Inbox";
18
+ return (
19
+ <Tooltip>
20
+ <TooltipTrigger asChild>
21
+ <Button
22
+ variant="ghost"
23
+ size="icon-sm"
24
+ aria-label={label}
25
+ className="relative h-7 w-7"
26
+ onClick={() => inboxUiActions.open()}
27
+ >
28
+ <Inbox className="h-4 w-4" />
29
+ {counts.total > 0 && (
30
+ <span className="bg-primary text-primary-foreground ring-background absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1 font-mono text-[10px] leading-none font-bold tabular-nums ring-2">
31
+ {counts.total > 99 ? "99+" : counts.total}
32
+ </span>
33
+ )}
34
+ </Button>
35
+ </TooltipTrigger>
36
+ <TooltipContent>Inbox</TooltipContent>
37
+ </Tooltip>
38
+ );
39
+ }
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Settings } from "lucide-react";
5
+ import { useSnapshot } from "valtio";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+ import { TasksPanel } from "@/components/Tasks";
13
+ import { MessagesPanel } from "@/components/Bus";
14
+ import { useInbox } from "@/hooks/useInbox";
15
+ import { useBusMessages } from "@/data/bus";
16
+ import { pickTab, type InboxTab } from "@/lib/inbox/counts";
17
+ import { cn } from "@/lib/utils";
18
+ import { inboxUi, inboxUiActions, lastInboxTab } from "@/stores/inboxUi";
19
+ import { settingsUiActions } from "@/stores/settingsUi";
20
+ import { NeedsYouList } from "./NeedsYouList";
21
+
22
+ const TABS: { id: InboxTab; label: string }[] = [
23
+ { id: "needs", label: "Needs you" },
24
+ { id: "tasks", label: "Tasks" },
25
+ { id: "messages", label: "Messages" },
26
+ ];
27
+
28
+ // Everything waiting on you, in one panel: under the bar's Inbox button on a
29
+ // wide screen, centred on a phone.
30
+ export function InboxDialog() {
31
+ const { open, request } = useSnapshot(inboxUi);
32
+ const { rows, counts } = useInbox();
33
+ const { data: messages } = useBusMessages();
34
+ const [tab, setTab] = useState<InboxTab>("needs");
35
+ // Chosen as the panel opens, not as its counts change under his hands.
36
+ const [wasOpen, setWasOpen] = useState(false);
37
+ if (open !== wasOpen) {
38
+ setWasOpen(open);
39
+ if (open)
40
+ setTab(
41
+ pickTab({
42
+ explicit: request ?? undefined,
43
+ needs: counts.needs,
44
+ last: lastInboxTab(),
45
+ })
46
+ );
47
+ }
48
+
49
+ const choose = (next: InboxTab) => {
50
+ setTab(next);
51
+ inboxUiActions.rememberTab(next);
52
+ };
53
+
54
+ // Looking at Messages reads them, including ones arriving meanwhile.
55
+ useEffect(() => {
56
+ if (!open || tab !== "messages" || !messages?.length) return;
57
+ inboxUiActions.markSeen(messages.reduce((n, m) => Math.max(n, m.id), 0));
58
+ }, [open, tab, messages]);
59
+
60
+ const count = {
61
+ needs: counts.needs,
62
+ tasks: counts.tasks,
63
+ messages: counts.messages,
64
+ };
65
+
66
+ return (
67
+ <Dialog open={open} onOpenChange={inboxUiActions.setOpen}>
68
+ <DialogContent
69
+ showCloseButton={false}
70
+ onOpenAutoFocus={(e) => e.preventDefault()}
71
+ className={cn(
72
+ "flex max-h-[min(85vh,40rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-lg",
73
+ "md:top-12 md:right-3 md:left-auto md:max-w-md md:translate-x-0 md:translate-y-0"
74
+ )}
75
+ >
76
+ <DialogTitle className="sr-only">Inbox</DialogTitle>
77
+ <DialogDescription className="sr-only">
78
+ Sessions, tasks and messages waiting on you.
79
+ </DialogDescription>
80
+ <div
81
+ role="tablist"
82
+ className="border-foreground/[0.07] flex shrink-0 items-center gap-1 border-b px-2"
83
+ >
84
+ {TABS.map((t) => (
85
+ <button
86
+ key={t.id}
87
+ type="button"
88
+ role="tab"
89
+ aria-selected={tab === t.id}
90
+ onClick={() => choose(t.id)}
91
+ className={cn(
92
+ "-mb-px flex min-h-11 items-center gap-1.5 border-b-2 px-2.5 text-sm transition-colors",
93
+ tab === t.id
94
+ ? "border-primary text-foreground"
95
+ : "text-muted-foreground hover:text-foreground border-transparent"
96
+ )}
97
+ >
98
+ {t.label}
99
+ {count[t.id] > 0 && (
100
+ <span
101
+ className={cn(
102
+ "rounded-full px-1.5 font-mono text-[10px] leading-4 tabular-nums",
103
+ t.id === "needs"
104
+ ? "bg-primary text-primary-foreground"
105
+ : "bg-foreground/10 text-foreground/80"
106
+ )}
107
+ >
108
+ {count[t.id]}
109
+ </span>
110
+ )}
111
+ </button>
112
+ ))}
113
+ <button
114
+ type="button"
115
+ aria-label="Notification settings"
116
+ title="Notification settings"
117
+ onClick={() => {
118
+ inboxUiActions.setOpen(false);
119
+ settingsUiActions.open("notifications");
120
+ }}
121
+ className="text-muted-foreground hover:text-foreground ml-auto flex size-9 items-center justify-center rounded-md"
122
+ >
123
+ <Settings className="h-4 w-4" />
124
+ </button>
125
+ </div>
126
+ <div
127
+ className={cn(
128
+ "min-h-0 flex-1 overflow-y-auto",
129
+ tab !== "needs" && "p-4"
130
+ )}
131
+ >
132
+ {tab === "needs" && <NeedsYouList rows={rows} />}
133
+ {tab === "tasks" && <TasksPanel />}
134
+ {tab === "messages" && <MessagesPanel />}
135
+ </div>
136
+ </DialogContent>
137
+ </Dialog>
138
+ );
139
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { CornerDownLeft, X } from "lucide-react";
4
+ import { toast } from "sonner";
5
+ import { useDismissNeedsYou } from "@/data/orchestrators";
6
+ import type { NeedsRow } from "@/lib/inbox/counts";
7
+ import { cn } from "@/lib/utils";
8
+ import { inboxUiActions } from "@/stores/inboxUi";
9
+ import { needsYouFocusActions } from "@/stores/needsYouFocus";
10
+ import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
11
+ import { sessionOpenActions } from "@/stores/sessionOpen";
12
+
13
+ const PILL = {
14
+ input: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
15
+ ask: "bg-primary/15 text-primary",
16
+ question: "bg-foreground/10 text-foreground/80",
17
+ } as const;
18
+ const PILL_LABEL = {
19
+ input: "Input",
20
+ ask: "Ask",
21
+ question: "Question",
22
+ } as const;
23
+
24
+ function go(row: NeedsRow) {
25
+ inboxUiActions.setOpen(false);
26
+ if (row.kind === "input" && row.sessionId)
27
+ sessionOpenActions.request(row.sessionId);
28
+ else if (row.workspaceId) {
29
+ orchestratorOpenActions.request(row.workspaceId);
30
+ if (row.ask) needsYouFocusActions.ask(row.ask.id);
31
+ }
32
+ }
33
+
34
+ function Row({ row }: { row: NeedsRow }) {
35
+ const dismiss = useDismissNeedsYou(row.workspaceId ?? "");
36
+ const hide = () =>
37
+ dismiss.mutate(
38
+ { key: row.key },
39
+ {
40
+ onSuccess: () =>
41
+ toast("Hidden from Needs you", {
42
+ action: {
43
+ label: "Undo",
44
+ onClick: () => dismiss.mutate({ key: row.key, undo: true }),
45
+ },
46
+ }),
47
+ onError: (e) => toast.error(e.message),
48
+ }
49
+ );
50
+ return (
51
+ <li className="border-foreground/[0.07] flex items-center border-b last:border-0">
52
+ <button
53
+ type="button"
54
+ onClick={() => go(row)}
55
+ className="hover:bg-foreground/[0.04] flex min-h-12 min-w-0 flex-1 items-center gap-3 px-4 py-2 text-left"
56
+ >
57
+ <span
58
+ className={cn(
59
+ "w-[4.25rem] shrink-0 rounded-full px-2 py-0.5 text-center text-[11px] font-medium",
60
+ PILL[row.kind]
61
+ )}
62
+ >
63
+ {PILL_LABEL[row.kind]}
64
+ </span>
65
+ <span className="min-w-0 flex-1">
66
+ <span className="block truncate text-sm">{row.title}</span>
67
+ <span className="text-muted-foreground block truncate text-xs">
68
+ {row.sub}
69
+ </span>
70
+ </span>
71
+ </button>
72
+ {row.question && row.workspaceId && (
73
+ <button
74
+ type="button"
75
+ aria-label="Reply"
76
+ title="Reply"
77
+ onClick={() => {
78
+ inboxUiActions.setOpen(false);
79
+ orchestratorOpenActions.request(row.workspaceId!);
80
+ needsYouFocusActions.reply(
81
+ row.workspaceId!,
82
+ row.question!.id,
83
+ row.question!.text
84
+ );
85
+ }}
86
+ className="text-muted-foreground hover:text-foreground flex size-11 shrink-0 items-center justify-center"
87
+ >
88
+ <CornerDownLeft className="h-3.5 w-3.5" />
89
+ </button>
90
+ )}
91
+ {row.kind !== "input" && (
92
+ <button
93
+ type="button"
94
+ aria-label="Dismiss"
95
+ title="Dismiss"
96
+ onClick={hide}
97
+ className="text-muted-foreground hover:text-foreground flex size-11 shrink-0 items-center justify-center"
98
+ >
99
+ <X className="h-3.5 w-3.5" />
100
+ </button>
101
+ )}
102
+ </li>
103
+ );
104
+ }
105
+
106
+ export function NeedsYouList({ rows }: { rows: NeedsRow[] }) {
107
+ if (!rows.length)
108
+ return (
109
+ <p className="text-muted-foreground py-10 text-center text-sm">
110
+ Nothing needs you.
111
+ </p>
112
+ );
113
+ return (
114
+ <ul>
115
+ {rows.map((row) => (
116
+ <Row key={row.key} row={row} />
117
+ ))}
118
+ </ul>
119
+ );
120
+ }
@@ -0,0 +1,2 @@
1
+ export { InboxButton } from "./InboxButton";
2
+ export { InboxDialog } from "./InboxDialog";
@@ -1,6 +1,5 @@
1
1
  export { LumifyHubDialogs } from "./LumifyHubDialogs";
2
2
  export { BoardTodoSection } from "./BoardTodoSection";
3
- export { DocsButton } from "./Docs/DocsButton";
4
3
  export {
5
4
  WorkspaceLumifyHubItem,
6
5
  ProjectBoardItem,
@@ -167,6 +167,7 @@ export function DesktopTabBar({
167
167
  <ChatBookmarksButton
168
168
  sessionId={bookmarksSessionId}
169
169
  className="h-7 min-w-7"
170
+ hideWhenEmpty
170
171
  />
171
172
  )}
172
173
  {session?.working_directory && (
@@ -187,6 +188,7 @@ export function DesktopTabBar({
187
188
  <div className="flex items-center">
188
189
  <PaneMenu
189
190
  session={session}
191
+ bookmarksSessionId={bookmarksSessionId}
190
192
  canSplit={canSplit}
191
193
  canClose={canClose}
192
194
  hasAttachedTmux={hasAttachedTmux}