@saadnvd1/agent-os 0.3.603 → 0.3.606
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.
- package/app/page.tsx +3 -8
- package/components/Bookmarks/ChatBookmarksButton.tsx +4 -0
- package/components/Bus/MessagesPanel.tsx +155 -0
- package/components/Bus/index.ts +1 -2
- package/components/Chat/ChatPanel.tsx +9 -1
- package/components/CommandPalette/useAppCommands.ts +12 -4
- package/components/Features/FeaturesSection.tsx +2 -2
- package/components/Inbox/InboxButton.tsx +39 -0
- package/components/Inbox/InboxDialog.tsx +139 -0
- package/components/Inbox/NeedsYouList.tsx +120 -0
- package/components/Inbox/index.ts +2 -0
- package/components/LumifyHub/index.ts +0 -1
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/PaneMenu.test.ts +118 -0
- package/components/Pane/PaneMenu.tsx +37 -0
- package/components/SessionList/SidebarHeader.tsx +7 -4
- package/components/Tasks/TaskRow.tsx +3 -3
- package/components/Tasks/TasksPanel.tsx +63 -0
- package/components/Tasks/index.ts +1 -1
- package/components/views/DesktopAppActions.tsx +10 -63
- package/components/views/DesktopView.tsx +1 -11
- package/hooks/useInbox.ts +69 -0
- package/lib/env-setup.ts +27 -12
- package/lib/inbox/counts.ts +140 -0
- package/lib/project-config/agentos.schema.json +14 -0
- package/lib/project-config/schema.ts +10 -0
- package/lib/worktree-deps/caches.ts +250 -0
- package/lib/worktree-deps/clone.ts +94 -9
- package/lib/worktree-deps/fastcopy.ts +89 -0
- package/lib/worktree-deps/index.ts +14 -5
- package/lib/worktree-deps/paths.ts +20 -0
- package/lib/worktree-deps/spare.ts +26 -8
- package/package.json +1 -1
- package/scripts/bench/worktree-setup.ts +101 -0
- package/scripts/check-bundle.mjs +25 -10
- package/scripts/check-bundle.test.ts +2 -2
- package/stores/inboxUi.ts +60 -0
- package/stores/needsYouFocus.ts +11 -3
- package/components/Bus/MessagesButton.tsx +0 -25
- package/components/Bus/MessagesDialog.tsx +0 -176
- package/components/LumifyHub/Docs/DocsButton.tsx +0 -31
- package/components/NotificationsMenu.tsx +0 -86
- package/components/Tasks/TasksButton.tsx +0 -28
- package/components/Tasks/TasksDialog.tsx +0 -75
- package/stores/busUi.ts +0 -12
- 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
|
|
69
|
-
() => import("@/components/
|
|
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
|
-
<
|
|
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
|
+
}
|
package/components/Bus/index.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
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 {
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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}
|