@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.
- 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/inbox/counts.ts +140 -0
- package/package.json +1 -1
- 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/scripts/check-bundle.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// The app's first-load JavaScript,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// The app's first-load JavaScript, watched against a budget. Going over it
|
|
2
|
+
// warns rather than fails: growth is sometimes worth it, and the point is to
|
|
3
|
+
// notice a big jump (a syntax highlighter's every grammar, a diagram renderer
|
|
4
|
+
// imported eagerly) before it ships to the phone. On CI the warning becomes an
|
|
5
|
+
// annotation on the PR. A missing or unreadable size still fails, so a broken
|
|
6
|
+
// check can't pass quietly. Run after `next build`.
|
|
7
|
+
import { appendFileSync, readFileSync } from "fs";
|
|
5
8
|
|
|
6
9
|
// Bytes, uncompressed. 1.04 MB when this was set (2026-10-07), down from 2.07.
|
|
7
10
|
const BUDGETS = { "/": 1_200_000 };
|
|
@@ -9,7 +12,20 @@ const BUDGETS = { "/": 1_200_000 };
|
|
|
9
12
|
const statsPath =
|
|
10
13
|
process.argv[2] ?? ".next/diagnostics/route-bundle-stats.json";
|
|
11
14
|
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
15
|
+
const kb = (n) => `${Math.round(n / 1024)} KB`;
|
|
12
16
|
let failed = false;
|
|
17
|
+
|
|
18
|
+
function warn(text) {
|
|
19
|
+
console.warn(`bundle: ${text}`);
|
|
20
|
+
if (process.env.GITHUB_ACTIONS)
|
|
21
|
+
console.log(`::warning title=Bundle size::${text}`);
|
|
22
|
+
if (process.env.GITHUB_STEP_SUMMARY)
|
|
23
|
+
appendFileSync(
|
|
24
|
+
process.env.GITHUB_STEP_SUMMARY,
|
|
25
|
+
`> [!WARNING]\n> ${text}\n`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
for (const [route, budget] of Object.entries(BUDGETS)) {
|
|
14
30
|
const entry = stats.find((r) => r.route === route);
|
|
15
31
|
if (!entry) {
|
|
@@ -18,18 +34,17 @@ for (const [route, budget] of Object.entries(BUDGETS)) {
|
|
|
18
34
|
continue;
|
|
19
35
|
}
|
|
20
36
|
const bytes = entry.firstLoadUncompressedJsBytes;
|
|
21
|
-
const kb = (n) => `${Math.round(n / 1024)} KB`;
|
|
22
37
|
if (typeof bytes !== "number" || !Number.isFinite(bytes)) {
|
|
23
38
|
// A Next.js upgrade that renames the field must not pass by default.
|
|
24
39
|
console.error(`bundle: no firstLoadUncompressedJsBytes for ${route}`);
|
|
25
40
|
failed = true;
|
|
26
41
|
} else if (bytes > budget) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
42
|
+
const over = Math.round(((bytes - budget) / budget) * 100);
|
|
43
|
+
warn(
|
|
44
|
+
`${route} loads ${kb(bytes)} of JS first, ${over}% over its ${kb(budget)} budget. ` +
|
|
45
|
+
"If that's not worth it, load the heavy part with next/dynamic or import(); " +
|
|
46
|
+
"`npx next experimental-analyze` shows what's in it."
|
|
31
47
|
);
|
|
32
|
-
failed = true;
|
|
33
48
|
} else {
|
|
34
49
|
console.log(`bundle: ${route} ${kb(bytes)} of ${kb(budget)}`);
|
|
35
50
|
}
|
|
@@ -21,9 +21,9 @@ describe("the first-load JS budget", () => {
|
|
|
21
21
|
expect(r.code).toBe(0);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it("
|
|
24
|
+
it("warns, without failing, on a build over budget", () => {
|
|
25
25
|
const r = run([{ route: "/", firstLoadUncompressedJsBytes: 2_000_000 }]);
|
|
26
|
-
expect([r.code, r.out]).toEqual([
|
|
26
|
+
expect([r.code, r.out]).toEqual([0, expect.stringContaining("% over its")]);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it("fails closed when the size is missing or not a number", () => {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
import { INBOX_TABS, type InboxTab } from "@/lib/inbox/counts";
|
|
3
|
+
|
|
4
|
+
const TAB_KEY = "agentos:inbox-tab";
|
|
5
|
+
const SEEN_KEY = "agentos:inbox-messages-seen";
|
|
6
|
+
|
|
7
|
+
const read = (key: string) => {
|
|
8
|
+
try {
|
|
9
|
+
return typeof window === "undefined" ? null : localStorage.getItem(key);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const write = (key: string, value: string) => {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(key, value);
|
|
17
|
+
} catch {
|
|
18
|
+
// Private mode: the choice just isn't kept.
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const lastInboxTab = (): InboxTab | null => {
|
|
23
|
+
const saved = read(TAB_KEY);
|
|
24
|
+
return INBOX_TABS.find((t) => t === saved) ?? null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// The Inbox panel, shared by the bar, the sidebar menu and the palette.
|
|
28
|
+
// `request` is an explicit tab to open on; without one the panel picks.
|
|
29
|
+
export const inboxUi = proxy<{
|
|
30
|
+
open: boolean;
|
|
31
|
+
request: InboxTab | null;
|
|
32
|
+
// The newest message he has looked at; null until first seen.
|
|
33
|
+
seenMessageId: number | null;
|
|
34
|
+
}>({
|
|
35
|
+
open: false,
|
|
36
|
+
request: null,
|
|
37
|
+
seenMessageId: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const inboxUiActions = {
|
|
41
|
+
open: (tab?: InboxTab) => {
|
|
42
|
+
inboxUi.request = tab ?? null;
|
|
43
|
+
inboxUi.open = true;
|
|
44
|
+
},
|
|
45
|
+
setOpen: (open: boolean) => {
|
|
46
|
+
inboxUi.open = open;
|
|
47
|
+
},
|
|
48
|
+
rememberTab: (tab: InboxTab) => write(TAB_KEY, tab),
|
|
49
|
+
loadSeen: () => {
|
|
50
|
+
if (inboxUi.seenMessageId !== null) return;
|
|
51
|
+
const saved = read(SEEN_KEY);
|
|
52
|
+
if (saved !== null && Number.isFinite(Number(saved)))
|
|
53
|
+
inboxUi.seenMessageId = Number(saved);
|
|
54
|
+
},
|
|
55
|
+
markSeen: (id: number) => {
|
|
56
|
+
if (inboxUi.seenMessageId !== null && id <= inboxUi.seenMessageId) return;
|
|
57
|
+
inboxUi.seenMessageId = id;
|
|
58
|
+
write(SEEN_KEY, String(id));
|
|
59
|
+
},
|
|
60
|
+
};
|
package/stores/needsYouFocus.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { proxy } from "valtio";
|
|
2
2
|
|
|
3
|
-
// "Show this ask", raised by the Needs-you strip and
|
|
4
|
-
// above the chat, which opens itself and scrolls the card
|
|
3
|
+
// "Show this ask", raised by the Needs-you strip and the Inbox and handled
|
|
4
|
+
// by the asks list above the chat, which opens itself and scrolls the card
|
|
5
|
+
// into view. "Reply to this question" is handled by the orchestrator's chat.
|
|
5
6
|
export const needsYouFocus = proxy<{
|
|
6
7
|
ask: { id: number; at: number } | null;
|
|
7
|
-
|
|
8
|
+
reply: { workspaceId: string; id: number; text: string; at: number } | null;
|
|
9
|
+
}>({ ask: null, reply: null });
|
|
8
10
|
|
|
9
11
|
export const needsYouFocusActions = {
|
|
10
12
|
ask: (id: number) => {
|
|
11
13
|
needsYouFocus.ask = { id, at: Date.now() };
|
|
12
14
|
},
|
|
15
|
+
reply: (workspaceId: string, id: number, text: string) => {
|
|
16
|
+
needsYouFocus.reply = { workspaceId, id, text, at: Date.now() };
|
|
17
|
+
},
|
|
18
|
+
clearReply: () => {
|
|
19
|
+
needsYouFocus.reply = null;
|
|
20
|
+
},
|
|
13
21
|
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { MessagesSquare } from "lucide-react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { busUiActions } from "@/stores/busUi";
|
|
6
|
-
|
|
7
|
-
export function MessagesButton({
|
|
8
|
-
labelClassName,
|
|
9
|
-
}: {
|
|
10
|
-
labelClassName?: string;
|
|
11
|
-
}) {
|
|
12
|
-
return (
|
|
13
|
-
<Button
|
|
14
|
-
size="sm"
|
|
15
|
-
variant="ghost"
|
|
16
|
-
aria-label="Messages"
|
|
17
|
-
title="Messages"
|
|
18
|
-
className="h-7 px-2"
|
|
19
|
-
onClick={busUiActions.open}
|
|
20
|
-
>
|
|
21
|
-
<MessagesSquare className="h-4 w-4" />
|
|
22
|
-
<span className={labelClassName}>Messages</span>
|
|
23
|
-
</Button>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
4
|
-
import { useSnapshot } from "valtio";
|
|
5
|
-
import {
|
|
6
|
-
Dialog,
|
|
7
|
-
DialogContent,
|
|
8
|
-
DialogDescription,
|
|
9
|
-
DialogHeader,
|
|
10
|
-
DialogTitle,
|
|
11
|
-
} from "@/components/ui/dialog";
|
|
12
|
-
import { Button } from "@/components/ui/button";
|
|
13
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
14
|
-
import {
|
|
15
|
-
Select,
|
|
16
|
-
SelectContent,
|
|
17
|
-
SelectItem,
|
|
18
|
-
SelectTrigger,
|
|
19
|
-
SelectValue,
|
|
20
|
-
} from "@/components/ui/select";
|
|
21
|
-
import { useBusMessages, useBusPeers, useSendMessage } from "@/data/bus";
|
|
22
|
-
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
23
|
-
import { busUi, busUiActions } from "@/stores/busUi";
|
|
24
|
-
import { cn } from "@/lib/utils";
|
|
25
|
-
|
|
26
|
-
const ALL = "all";
|
|
27
|
-
|
|
28
|
-
export function MessagesDialog() {
|
|
29
|
-
const { open } = useSnapshot(busUi);
|
|
30
|
-
const { data: messages = [], isPending } = useBusMessages(open);
|
|
31
|
-
const { data: peers = [] } = useBusPeers(open);
|
|
32
|
-
const send = useSendMessage();
|
|
33
|
-
const [filter, setFilter] = useState(ALL);
|
|
34
|
-
const [to, setTo] = useState("");
|
|
35
|
-
const [body, setBody] = useState("");
|
|
36
|
-
|
|
37
|
-
const label = (p: { name: string; projectName: string | null }) =>
|
|
38
|
-
p.projectName ? `${p.projectName} / ${p.name}` : p.name;
|
|
39
|
-
const shown = useMemo(
|
|
40
|
-
() =>
|
|
41
|
-
filter === ALL
|
|
42
|
-
? messages
|
|
43
|
-
: messages.filter((m) => m.fromId === filter || m.toId === filter),
|
|
44
|
-
[messages, filter]
|
|
45
|
-
);
|
|
46
|
-
const recipient = to || (filter !== ALL ? filter : "");
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<Dialog
|
|
50
|
-
open={open}
|
|
51
|
-
onOpenChange={(o) => {
|
|
52
|
-
// A delivery result belongs to that send only.
|
|
53
|
-
if (!o) send.reset();
|
|
54
|
-
busUiActions.setOpen(o);
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
<DialogContent className="flex max-h-[85vh] max-w-2xl flex-col">
|
|
58
|
-
<DialogHeader>
|
|
59
|
-
<DialogTitle>Messages</DialogTitle>
|
|
60
|
-
<DialogDescription>
|
|
61
|
-
What your agent sessions are saying to each other. Agents use the{" "}
|
|
62
|
-
<code className="font-mono text-xs">aos</code> command.
|
|
63
|
-
</DialogDescription>
|
|
64
|
-
</DialogHeader>
|
|
65
|
-
|
|
66
|
-
<Select
|
|
67
|
-
value={filter}
|
|
68
|
-
onValueChange={(v) => {
|
|
69
|
-
send.reset();
|
|
70
|
-
setFilter(v);
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
73
|
-
<SelectTrigger
|
|
74
|
-
aria-label="Filter by session"
|
|
75
|
-
className="w-full sm:w-72"
|
|
76
|
-
>
|
|
77
|
-
<SelectValue />
|
|
78
|
-
</SelectTrigger>
|
|
79
|
-
<SelectContent>
|
|
80
|
-
<SelectItem value={ALL}>All sessions</SelectItem>
|
|
81
|
-
{peers.map((p) => (
|
|
82
|
-
<SelectItem key={p.id} value={p.id}>
|
|
83
|
-
{label(p)}
|
|
84
|
-
</SelectItem>
|
|
85
|
-
))}
|
|
86
|
-
</SelectContent>
|
|
87
|
-
</Select>
|
|
88
|
-
|
|
89
|
-
<div className="min-h-40 flex-1 space-y-3 overflow-y-auto pr-1">
|
|
90
|
-
{isPending && (
|
|
91
|
-
<div className="bg-muted/40 h-16 animate-pulse rounded-xl" />
|
|
92
|
-
)}
|
|
93
|
-
{!isPending && shown.length === 0 && (
|
|
94
|
-
<p className="text-muted-foreground py-8 text-center text-sm">
|
|
95
|
-
No messages yet.
|
|
96
|
-
</p>
|
|
97
|
-
)}
|
|
98
|
-
{shown.map((m) => (
|
|
99
|
-
<div key={m.id} className="space-y-1">
|
|
100
|
-
<div className="flex items-baseline gap-2 text-xs">
|
|
101
|
-
<span
|
|
102
|
-
className={cn("font-medium", !m.fromId && "text-primary")}
|
|
103
|
-
>
|
|
104
|
-
{m.fromName}
|
|
105
|
-
</span>
|
|
106
|
-
<span className="text-muted-foreground/60">→</span>
|
|
107
|
-
<span className="font-medium">{m.toName}</span>
|
|
108
|
-
<span className="text-muted-foreground/60 ml-auto font-mono tabular-nums">
|
|
109
|
-
{compactTimeAgo(fromSqliteTime(m.createdAt))}
|
|
110
|
-
</span>
|
|
111
|
-
</div>
|
|
112
|
-
<p className="bg-foreground/[0.04] rounded-lg px-3 py-2 text-sm whitespace-pre-wrap">
|
|
113
|
-
{m.body}
|
|
114
|
-
</p>
|
|
115
|
-
</div>
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<form
|
|
120
|
-
onSubmit={(e) => {
|
|
121
|
-
e.preventDefault();
|
|
122
|
-
send.mutate(
|
|
123
|
-
{ to: recipient, body },
|
|
124
|
-
{ onSuccess: () => setBody("") }
|
|
125
|
-
);
|
|
126
|
-
}}
|
|
127
|
-
className="space-y-2"
|
|
128
|
-
>
|
|
129
|
-
<Select
|
|
130
|
-
value={recipient}
|
|
131
|
-
onValueChange={(v) => {
|
|
132
|
-
send.reset();
|
|
133
|
-
setTo(v);
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
136
|
-
<SelectTrigger aria-label="Send to">
|
|
137
|
-
<SelectValue placeholder="Send to a session..." />
|
|
138
|
-
</SelectTrigger>
|
|
139
|
-
<SelectContent>
|
|
140
|
-
{peers.map((p) => (
|
|
141
|
-
<SelectItem key={p.id} value={p.id}>
|
|
142
|
-
{label(p)}
|
|
143
|
-
{!p.running && " (not running)"}
|
|
144
|
-
</SelectItem>
|
|
145
|
-
))}
|
|
146
|
-
</SelectContent>
|
|
147
|
-
</Select>
|
|
148
|
-
<div className="flex gap-2">
|
|
149
|
-
<Textarea
|
|
150
|
-
aria-label="Message"
|
|
151
|
-
value={body}
|
|
152
|
-
onChange={(e) => setBody(e.target.value)}
|
|
153
|
-
placeholder="Message as you"
|
|
154
|
-
rows={2}
|
|
155
|
-
className="min-h-11"
|
|
156
|
-
/>
|
|
157
|
-
<Button
|
|
158
|
-
type="submit"
|
|
159
|
-
disabled={!recipient || !body.trim() || send.isPending}
|
|
160
|
-
>
|
|
161
|
-
Send
|
|
162
|
-
</Button>
|
|
163
|
-
</div>
|
|
164
|
-
{send.error && (
|
|
165
|
-
<p className="text-destructive text-xs">{send.error.message}</p>
|
|
166
|
-
)}
|
|
167
|
-
{send.data?.delivery.state === "failed" && (
|
|
168
|
-
<p className="text-destructive text-xs">
|
|
169
|
-
Saved to its inbox, but not delivered: {send.data.delivery.why}
|
|
170
|
-
</p>
|
|
171
|
-
)}
|
|
172
|
-
</form>
|
|
173
|
-
</DialogContent>
|
|
174
|
-
</Dialog>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { BookOpen } from "lucide-react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { useDocsWorkspace } from "@/data/lumifyhub/docs";
|
|
6
|
-
import { docsUiActions } from "@/stores/docsUi";
|
|
7
|
-
|
|
8
|
-
// Only for a project whose workspace is linked to LumifyHub.
|
|
9
|
-
export function DocsButton({
|
|
10
|
-
projectId,
|
|
11
|
-
labelClassName,
|
|
12
|
-
}: {
|
|
13
|
-
projectId?: string | null;
|
|
14
|
-
labelClassName?: string;
|
|
15
|
-
}) {
|
|
16
|
-
const workspace = useDocsWorkspace(projectId);
|
|
17
|
-
if (!workspace) return null;
|
|
18
|
-
return (
|
|
19
|
-
<Button
|
|
20
|
-
size="sm"
|
|
21
|
-
variant="ghost"
|
|
22
|
-
aria-label="Docs"
|
|
23
|
-
title="Docs"
|
|
24
|
-
className="h-7 px-2"
|
|
25
|
-
onClick={() => docsUiActions.open(workspace.id)}
|
|
26
|
-
>
|
|
27
|
-
<BookOpen className="h-4 w-4" />
|
|
28
|
-
<span className={labelClassName}>Docs</span>
|
|
29
|
-
</Button>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { AlertCircle, Bell, Settings } from "lucide-react";
|
|
5
|
-
import { Button } from "./ui/button";
|
|
6
|
-
import {
|
|
7
|
-
DropdownMenu,
|
|
8
|
-
DropdownMenuContent,
|
|
9
|
-
DropdownMenuItem,
|
|
10
|
-
DropdownMenuLabel,
|
|
11
|
-
DropdownMenuSeparator,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
} from "./ui/dropdown-menu";
|
|
14
|
-
import { cn } from "@/lib/utils";
|
|
15
|
-
import type { NotificationSettings } from "@/lib/notifications";
|
|
16
|
-
import { settingsUiActions } from "@/stores/settingsUi";
|
|
17
|
-
|
|
18
|
-
interface WaitingSession {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The bell: sessions waiting for you, and the way to Settings > Notifications.
|
|
24
|
-
export function NotificationsMenu({
|
|
25
|
-
settings,
|
|
26
|
-
waitingSessions = [],
|
|
27
|
-
onSelectSession,
|
|
28
|
-
}: {
|
|
29
|
-
settings: NotificationSettings;
|
|
30
|
-
waitingSessions?: WaitingSession[];
|
|
31
|
-
onSelectSession?: (id: string) => void;
|
|
32
|
-
}) {
|
|
33
|
-
const [open, setOpen] = useState(false);
|
|
34
|
-
const waitingCount = waitingSessions.length;
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
38
|
-
<DropdownMenuTrigger asChild>
|
|
39
|
-
<Button
|
|
40
|
-
variant="ghost"
|
|
41
|
-
size="icon-sm"
|
|
42
|
-
aria-label="Notifications"
|
|
43
|
-
className="relative h-7 w-7"
|
|
44
|
-
>
|
|
45
|
-
<Bell
|
|
46
|
-
className={cn(
|
|
47
|
-
"h-4 w-4",
|
|
48
|
-
!settings.sound && "text-muted-foreground"
|
|
49
|
-
)}
|
|
50
|
-
/>
|
|
51
|
-
{waitingCount > 0 && (
|
|
52
|
-
<span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-yellow-500 text-[10px] font-bold text-yellow-950">
|
|
53
|
-
{waitingCount}
|
|
54
|
-
</span>
|
|
55
|
-
)}
|
|
56
|
-
</Button>
|
|
57
|
-
</DropdownMenuTrigger>
|
|
58
|
-
<DropdownMenuContent align="end" className="w-52">
|
|
59
|
-
{waitingCount > 0 && (
|
|
60
|
-
<>
|
|
61
|
-
<DropdownMenuLabel className="flex items-center gap-2 text-xs text-yellow-500">
|
|
62
|
-
<AlertCircle className="h-3 w-3" />
|
|
63
|
-
Waiting for input
|
|
64
|
-
</DropdownMenuLabel>
|
|
65
|
-
{waitingSessions.map((session) => (
|
|
66
|
-
<DropdownMenuItem
|
|
67
|
-
key={session.id}
|
|
68
|
-
onClick={() => onSelectSession?.(session.id)}
|
|
69
|
-
className="text-sm"
|
|
70
|
-
>
|
|
71
|
-
{session.name}
|
|
72
|
-
</DropdownMenuItem>
|
|
73
|
-
))}
|
|
74
|
-
<DropdownMenuSeparator />
|
|
75
|
-
</>
|
|
76
|
-
)}
|
|
77
|
-
<DropdownMenuItem
|
|
78
|
-
onClick={() => settingsUiActions.open("notifications")}
|
|
79
|
-
>
|
|
80
|
-
<Settings className="h-3 w-3" />
|
|
81
|
-
Notification settings
|
|
82
|
-
</DropdownMenuItem>
|
|
83
|
-
</DropdownMenuContent>
|
|
84
|
-
</DropdownMenu>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { ListTodo } from "lucide-react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { tasksUiActions } from "@/stores/tasksUi";
|
|
6
|
-
import { useTasksNeedingYou } from "./useTasksNeedingYou";
|
|
7
|
-
|
|
8
|
-
export function TasksButton({ labelClassName }: { labelClassName?: string }) {
|
|
9
|
-
const waiting = useTasksNeedingYou();
|
|
10
|
-
return (
|
|
11
|
-
<Button
|
|
12
|
-
size="sm"
|
|
13
|
-
variant="ghost"
|
|
14
|
-
aria-label="Tasks"
|
|
15
|
-
title="Tasks"
|
|
16
|
-
className="h-7 px-2"
|
|
17
|
-
onClick={tasksUiActions.openPanel}
|
|
18
|
-
>
|
|
19
|
-
<ListTodo className="h-4 w-4" />
|
|
20
|
-
<span className={labelClassName}>Tasks</span>
|
|
21
|
-
{waiting > 0 && (
|
|
22
|
-
<span className="bg-primary text-primary-foreground rounded-full px-1.5 font-mono text-[10px] leading-4 tabular-nums">
|
|
23
|
-
{waiting}
|
|
24
|
-
</span>
|
|
25
|
-
)}
|
|
26
|
-
</Button>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useSnapshot } from "valtio";
|
|
4
|
-
import { Plus } from "lucide-react";
|
|
5
|
-
import {
|
|
6
|
-
Dialog,
|
|
7
|
-
DialogContent,
|
|
8
|
-
DialogHeader,
|
|
9
|
-
DialogTitle,
|
|
10
|
-
} from "@/components/ui/dialog";
|
|
11
|
-
import { Button } from "@/components/ui/button";
|
|
12
|
-
import { useTasksQuery } from "@/data/tasks";
|
|
13
|
-
import { isFinished, needsHuman } from "@/lib/tasks/state";
|
|
14
|
-
import { tasksUi, tasksUiActions } from "@/stores/tasksUi";
|
|
15
|
-
import { newDraft } from "@/stores/drafts";
|
|
16
|
-
import { TaskRow } from "./TaskRow";
|
|
17
|
-
import { BoardTodoSection } from "@/components/LumifyHub";
|
|
18
|
-
import { StacksSection } from "@/components/Stacks";
|
|
19
|
-
import { FeaturesSection } from "@/components/Features";
|
|
20
|
-
|
|
21
|
-
export function TasksDialog() {
|
|
22
|
-
const { panelOpen } = useSnapshot(tasksUi);
|
|
23
|
-
const { data: tasks = [], isPending } = useTasksQuery();
|
|
24
|
-
const active = tasks.filter((t) => !isFinished(t.state));
|
|
25
|
-
// Whatever needs a human first.
|
|
26
|
-
active.sort(
|
|
27
|
-
(a, b) => Number(needsHuman(b.state)) - Number(needsHuman(a.state))
|
|
28
|
-
);
|
|
29
|
-
const done = tasks.filter((t) => !active.includes(t)).slice(0, 10);
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Dialog open={panelOpen} onOpenChange={tasksUiActions.setPanelOpen}>
|
|
33
|
-
<DialogContent className="max-h-[85vh] max-w-xl overflow-y-auto [&>*]:min-w-0">
|
|
34
|
-
<DialogHeader className="flex-row items-center justify-between gap-4 space-y-0">
|
|
35
|
-
<DialogTitle>Tasks</DialogTitle>
|
|
36
|
-
<Button
|
|
37
|
-
size="sm"
|
|
38
|
-
className="mr-6"
|
|
39
|
-
onClick={() => {
|
|
40
|
-
tasksUiActions.setPanelOpen(false);
|
|
41
|
-
newDraft({ kind: "current", openPr: true });
|
|
42
|
-
}}
|
|
43
|
-
>
|
|
44
|
-
<Plus className="h-4 w-4" />
|
|
45
|
-
New task
|
|
46
|
-
</Button>
|
|
47
|
-
</DialogHeader>
|
|
48
|
-
{isPending && (
|
|
49
|
-
<div className="bg-muted/40 h-16 animate-pulse rounded-xl" />
|
|
50
|
-
)}
|
|
51
|
-
{!isPending && tasks.length === 0 && (
|
|
52
|
-
<p className="text-muted-foreground py-6 text-center text-sm">
|
|
53
|
-
No tasks yet. Start one and keep working while it runs.
|
|
54
|
-
</p>
|
|
55
|
-
)}
|
|
56
|
-
<div className="space-y-2">
|
|
57
|
-
{active.map((t) => (
|
|
58
|
-
<TaskRow key={t.id} task={t} />
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
<FeaturesSection />
|
|
62
|
-
<StacksSection />
|
|
63
|
-
<BoardTodoSection />
|
|
64
|
-
{done.length > 0 && (
|
|
65
|
-
<div className="space-y-2">
|
|
66
|
-
<p className="label-mono text-muted-foreground pt-2">Finished</p>
|
|
67
|
-
{done.map((t) => (
|
|
68
|
-
<TaskRow key={t.id} task={t} />
|
|
69
|
-
))}
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
</DialogContent>
|
|
73
|
-
</Dialog>
|
|
74
|
-
);
|
|
75
|
-
}
|
package/stores/busUi.ts
DELETED
package/stores/tasksUi.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { proxy } from "valtio";
|
|
2
|
-
|
|
3
|
-
// The Tasks panel's open state, shared by the header and the sidebar menu.
|
|
4
|
-
// A new task is a draft with "Open a PR when done" (stores/drafts).
|
|
5
|
-
export const tasksUi = proxy({ panelOpen: false });
|
|
6
|
-
|
|
7
|
-
export const tasksUiActions = {
|
|
8
|
-
openPanel: () => {
|
|
9
|
-
tasksUi.panelOpen = true;
|
|
10
|
-
},
|
|
11
|
-
setPanelOpen: (open: boolean) => {
|
|
12
|
-
tasksUi.panelOpen = open;
|
|
13
|
-
},
|
|
14
|
-
};
|