@saadnvd1/agent-os 0.3.602 → 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/OrchestratorPinRow.tsx +3 -1
- package/components/SessionList/RowShell.tsx +14 -2
- 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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import "@/components/Chat/composer/test-dom";
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { Session } from "@/lib/db/types";
|
|
6
|
+
import { bookmarksUi } from "@/stores/bookmarksUi";
|
|
7
|
+
import { docsUi } from "@/stores/docsUi";
|
|
8
|
+
import { paletteUi } from "@/stores/palette";
|
|
9
|
+
|
|
10
|
+
const data = vi.hoisted(() => ({
|
|
11
|
+
bookmarks: [] as unknown[],
|
|
12
|
+
docs: null as { id: string } | null,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("@/data/bookmarks", () => ({
|
|
15
|
+
useSessionBookmarks: () => ({ data: data.bookmarks }),
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("@/data/lumifyhub/docs", () => ({
|
|
18
|
+
useDocsWorkspace: () => data.docs,
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("@/components/Tasks/MoveMenuItems", () => ({
|
|
21
|
+
MoveMenuItems: () => null,
|
|
22
|
+
}));
|
|
23
|
+
// Radix's focus handling needs a real browser; plain elements show what the
|
|
24
|
+
// menu offers.
|
|
25
|
+
vi.mock("@/components/ui/dropdown-menu", async () => {
|
|
26
|
+
const { createElement: h } = await import("react");
|
|
27
|
+
const part =
|
|
28
|
+
(tag: string) => (props: { children?: unknown; onSelect?: () => void }) =>
|
|
29
|
+
h(tag, { onClick: props.onSelect }, props.children as never);
|
|
30
|
+
return {
|
|
31
|
+
DropdownMenu: part("div"),
|
|
32
|
+
DropdownMenuTrigger: part("div"),
|
|
33
|
+
DropdownMenuContent: part("div"),
|
|
34
|
+
DropdownMenuLabel: part("div"),
|
|
35
|
+
DropdownMenuItem: part("button"),
|
|
36
|
+
DropdownMenuSeparator: part("hr"),
|
|
37
|
+
DropdownMenuShortcut: part("span"),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
import { PaneMenu } from "./PaneMenu";
|
|
42
|
+
|
|
43
|
+
(
|
|
44
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
45
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
46
|
+
|
|
47
|
+
const session = {
|
|
48
|
+
id: "s1",
|
|
49
|
+
name: "Chat",
|
|
50
|
+
project_id: "p1",
|
|
51
|
+
view: "chat",
|
|
52
|
+
} as Session;
|
|
53
|
+
|
|
54
|
+
describe("PaneMenu", () => {
|
|
55
|
+
let host: HTMLElement;
|
|
56
|
+
let root: Root;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
host = document.createElement("div");
|
|
59
|
+
document.body.appendChild(host);
|
|
60
|
+
root = createRoot(host);
|
|
61
|
+
data.bookmarks = [];
|
|
62
|
+
data.docs = null;
|
|
63
|
+
paletteUi.open = false;
|
|
64
|
+
bookmarksUi.chatSessionId = null;
|
|
65
|
+
docsUi.workspaceId = null;
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
act(() => root.unmount());
|
|
69
|
+
host.remove();
|
|
70
|
+
});
|
|
71
|
+
const render = (bookmarksSessionId: string | null = "s1") =>
|
|
72
|
+
act(() => {
|
|
73
|
+
root.render(
|
|
74
|
+
createElement(PaneMenu, {
|
|
75
|
+
session,
|
|
76
|
+
bookmarksSessionId,
|
|
77
|
+
canSplit: true,
|
|
78
|
+
canClose: true,
|
|
79
|
+
hasAttachedTmux: false,
|
|
80
|
+
onSplitHorizontal: () => {},
|
|
81
|
+
onSplitVertical: () => {},
|
|
82
|
+
onClose: () => {},
|
|
83
|
+
onDetach: () => {},
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
const item = (text: string) =>
|
|
88
|
+
[...host.querySelectorAll("button")].find((b) =>
|
|
89
|
+
b.textContent?.startsWith(text)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it("always offers the command palette with its shortcut, and opens it", () => {
|
|
93
|
+
render(null);
|
|
94
|
+
const palette = item("Command palette");
|
|
95
|
+
expect(palette?.textContent).toContain("⌘K");
|
|
96
|
+
act(() => palette?.click());
|
|
97
|
+
expect(paletteUi.open).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("offers Bookmarks with its count for a chat only", () => {
|
|
101
|
+
data.bookmarks = [{}, {}, {}];
|
|
102
|
+
render();
|
|
103
|
+
expect(item("Bookmarks")?.textContent).toContain("3");
|
|
104
|
+
act(() => root.unmount());
|
|
105
|
+
root = createRoot(host);
|
|
106
|
+
render(null);
|
|
107
|
+
expect(item("Bookmarks")).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("offers Docs only when the project's workspace has them", () => {
|
|
111
|
+
render();
|
|
112
|
+
expect(item("Docs")).toBeUndefined();
|
|
113
|
+
data.docs = { id: "w1" };
|
|
114
|
+
render();
|
|
115
|
+
act(() => item("Docs")?.click());
|
|
116
|
+
expect(docsUi.workspaceId).toBe("w1");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
Command,
|
|
5
7
|
Copy,
|
|
6
8
|
Link,
|
|
7
9
|
MoreHorizontal,
|
|
8
10
|
Share2,
|
|
9
11
|
SplitSquareHorizontal,
|
|
10
12
|
SplitSquareVertical,
|
|
13
|
+
Star,
|
|
11
14
|
Unplug,
|
|
12
15
|
X,
|
|
13
16
|
} from "lucide-react";
|
|
@@ -18,15 +21,23 @@ import {
|
|
|
18
21
|
DropdownMenuItem,
|
|
19
22
|
DropdownMenuLabel,
|
|
20
23
|
DropdownMenuSeparator,
|
|
24
|
+
DropdownMenuShortcut,
|
|
21
25
|
DropdownMenuTrigger,
|
|
22
26
|
} from "@/components/ui/dropdown-menu";
|
|
23
27
|
import { copySessionLink, writeClipboard } from "@/hooks/useCopyToClipboard";
|
|
24
28
|
import type { Session } from "@/lib/db";
|
|
25
29
|
import { shareUiActions } from "@/stores/shareUi";
|
|
30
|
+
import { useSessionBookmarks } from "@/data/bookmarks";
|
|
31
|
+
import { useDocsWorkspace } from "@/data/lumifyhub/docs";
|
|
32
|
+
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
33
|
+
import { docsUiActions } from "@/stores/docsUi";
|
|
34
|
+
import { paletteActions } from "@/stores/palette";
|
|
26
35
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
27
36
|
|
|
28
37
|
interface PaneMenuProps {
|
|
29
38
|
session: Session | null | undefined;
|
|
39
|
+
// The chat on show, for its bookmarks; null for a terminal or another view.
|
|
40
|
+
bookmarksSessionId?: string | null;
|
|
30
41
|
canSplit: boolean;
|
|
31
42
|
canClose: boolean;
|
|
32
43
|
hasAttachedTmux: boolean;
|
|
@@ -40,6 +51,7 @@ interface PaneMenuProps {
|
|
|
40
51
|
// the bar.
|
|
41
52
|
export function PaneMenu({
|
|
42
53
|
session,
|
|
54
|
+
bookmarksSessionId,
|
|
43
55
|
canSplit,
|
|
44
56
|
canClose,
|
|
45
57
|
hasAttachedTmux,
|
|
@@ -48,6 +60,8 @@ export function PaneMenu({
|
|
|
48
60
|
onClose,
|
|
49
61
|
onDetach,
|
|
50
62
|
}: PaneMenuProps) {
|
|
63
|
+
const { data: bookmarks = [] } = useSessionBookmarks(bookmarksSessionId);
|
|
64
|
+
const docs = useDocsWorkspace(session?.project_id);
|
|
51
65
|
const copyId = async (id: string) => {
|
|
52
66
|
try {
|
|
53
67
|
await writeClipboard(id);
|
|
@@ -112,6 +126,29 @@ export function PaneMenu({
|
|
|
112
126
|
/>
|
|
113
127
|
</>
|
|
114
128
|
)}
|
|
129
|
+
{bookmarksSessionId && (
|
|
130
|
+
<DropdownMenuItem
|
|
131
|
+
onSelect={() => bookmarksUiActions.openChat(bookmarksSessionId)}
|
|
132
|
+
>
|
|
133
|
+
<Star className="h-4 w-4" />
|
|
134
|
+
Bookmarks
|
|
135
|
+
{bookmarks.length > 0 && (
|
|
136
|
+
<DropdownMenuShortcut>{bookmarks.length}</DropdownMenuShortcut>
|
|
137
|
+
)}
|
|
138
|
+
</DropdownMenuItem>
|
|
139
|
+
)}
|
|
140
|
+
<DropdownMenuItem onSelect={paletteActions.open}>
|
|
141
|
+
<Command className="h-4 w-4" />
|
|
142
|
+
Command palette
|
|
143
|
+
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
|
|
144
|
+
</DropdownMenuItem>
|
|
145
|
+
{docs && (
|
|
146
|
+
<DropdownMenuItem onSelect={() => docsUiActions.open(docs.id)}>
|
|
147
|
+
<BookOpen className="h-4 w-4" />
|
|
148
|
+
Docs
|
|
149
|
+
</DropdownMenuItem>
|
|
150
|
+
)}
|
|
151
|
+
<DropdownMenuSeparator />
|
|
115
152
|
{hasAttachedTmux && (
|
|
116
153
|
<DropdownMenuItem onSelect={onDetach}>
|
|
117
154
|
<Unplug className="h-4 w-4" />
|
|
@@ -14,6 +14,7 @@ import { cn } from "@/lib/utils";
|
|
|
14
14
|
import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
|
|
15
15
|
import { useRowContext } from "./RowContext";
|
|
16
16
|
import { SessionRowMenu } from "./SessionRowMenu";
|
|
17
|
+
import { ActiveTick } from "./RowShell";
|
|
17
18
|
import { RowRename, UnreadDot } from "./RowParts";
|
|
18
19
|
import { QuickChangeRow } from "./QuickChangeRow";
|
|
19
20
|
import { NestRail } from "./RowParts";
|
|
@@ -59,10 +60,11 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
|
|
|
59
60
|
onActivate={() => !renaming && ctx.onSelect(session.id)}
|
|
60
61
|
className={
|
|
61
62
|
active
|
|
62
|
-
? "bg-
|
|
63
|
+
? "bg-foreground/[0.07]"
|
|
63
64
|
: "bg-muted/50 hover:bg-muted has-[[data-chip]:hover]:bg-muted/50"
|
|
64
65
|
}
|
|
65
66
|
>
|
|
67
|
+
{active && <ActiveTick />}
|
|
66
68
|
<Workflow
|
|
67
69
|
aria-hidden
|
|
68
70
|
className={cn(
|
|
@@ -7,6 +7,17 @@ import * as DM from "@/components/ui/dropdown-menu";
|
|
|
7
7
|
import * as CM from "@/components/ui/context-menu";
|
|
8
8
|
import { cn } from "@/lib/utils";
|
|
9
9
|
|
|
10
|
+
// The open row's accent: a thin rounded tick at the left edge, inside the
|
|
11
|
+
// row so the list's overflow can't clip it.
|
|
12
|
+
export function ActiveTick() {
|
|
13
|
+
return (
|
|
14
|
+
<span
|
|
15
|
+
aria-hidden
|
|
16
|
+
className="bg-primary pointer-events-none absolute inset-y-[9px] left-0 w-[3px] rounded-full"
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export type RowMenuKind = "dropdown" | "context";
|
|
11
22
|
|
|
12
23
|
// Every sidebar row's frame: a leading mark, title over subtitle, what's on
|
|
@@ -66,10 +77,11 @@ export function RowShell({
|
|
|
66
77
|
highlight === "selected"
|
|
67
78
|
? "bg-primary/15"
|
|
68
79
|
: highlight === "active"
|
|
69
|
-
? "bg-
|
|
70
|
-
: "hover:bg-foreground/[0.
|
|
80
|
+
? "bg-foreground/[0.07]"
|
|
81
|
+
: "hover:bg-foreground/[0.025]"
|
|
71
82
|
)}
|
|
72
83
|
>
|
|
84
|
+
{highlight === "active" && <ActiveTick />}
|
|
73
85
|
{leading}
|
|
74
86
|
<span className="@container flex min-w-0 flex-1 flex-col leading-tight">
|
|
75
87
|
{children}
|
|
@@ -31,11 +31,10 @@ import { Button } from "@/components/ui/button";
|
|
|
31
31
|
import type { Workspace } from "@/lib/db";
|
|
32
32
|
import { archivedUiActions } from "@/stores/archivedUi";
|
|
33
33
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
34
|
-
import {
|
|
34
|
+
import { inboxUiActions } from "@/stores/inboxUi";
|
|
35
35
|
import { newDraft } from "@/stores/drafts";
|
|
36
36
|
import { schedulesUiActions } from "@/stores/schedulesUi";
|
|
37
37
|
import { recipesUiActions } from "@/stores/recipesUi";
|
|
38
|
-
import { busUiActions } from "@/stores/busUi";
|
|
39
38
|
import { settingsUiActions } from "@/stores/settingsUi";
|
|
40
39
|
import { usageUiActions } from "@/stores/usageUi";
|
|
41
40
|
import { WorkspaceSwitcher } from "./WorkspaceSwitcher";
|
|
@@ -99,7 +98,9 @@ export function SidebarHeader(props: SidebarHeaderProps) {
|
|
|
99
98
|
</Button>
|
|
100
99
|
}
|
|
101
100
|
items={[
|
|
102
|
-
menuItem("Tasks",
|
|
101
|
+
menuItem("Tasks", () => inboxUiActions.open("tasks"), {
|
|
102
|
+
icon: ListTodo,
|
|
103
|
+
}),
|
|
103
104
|
// Opens Settings > Schedules.
|
|
104
105
|
menuItem("Schedules", () => schedulesUiActions.open(workspaceId), {
|
|
105
106
|
icon: Clock,
|
|
@@ -115,7 +116,9 @@ export function SidebarHeader(props: SidebarHeaderProps) {
|
|
|
115
116
|
menuItem("Recipes", () => recipesUiActions.open(workspaceId), {
|
|
116
117
|
icon: ChefHat,
|
|
117
118
|
}),
|
|
118
|
-
menuItem("Messages",
|
|
119
|
+
menuItem("Messages", () => inboxUiActions.open("messages"), {
|
|
120
|
+
icon: MessagesSquare,
|
|
121
|
+
}),
|
|
119
122
|
...(props.onOpenDocs
|
|
120
123
|
? [menuItem("Docs", props.onOpenDocs, { icon: BookOpen })]
|
|
121
124
|
: []),
|
|
@@ -15,7 +15,7 @@ import { isFinished } from "@/lib/tasks/state";
|
|
|
15
15
|
import { useArmTask, useDropTask, useSignOffTask } from "@/data/tasks";
|
|
16
16
|
import { useResurrectSession } from "@/data/done";
|
|
17
17
|
import { tmuxAttachActions } from "@/stores/tmuxAttach";
|
|
18
|
-
import {
|
|
18
|
+
import { inboxUiActions } from "@/stores/inboxUi";
|
|
19
19
|
import { recipesUiActions } from "@/stores/recipesUi";
|
|
20
20
|
import { cn } from "@/lib/utils";
|
|
21
21
|
import { SimulatorIndicator } from "@/components/Simulator";
|
|
@@ -103,7 +103,7 @@ export function TaskRow({ task }: { task: TaskView }) {
|
|
|
103
103
|
onClick={() => {
|
|
104
104
|
if (task.hostError) return;
|
|
105
105
|
tmuxAttachActions.request(task.tmuxName, task.hostId ?? "local");
|
|
106
|
-
|
|
106
|
+
inboxUiActions.setOpen(false);
|
|
107
107
|
}}
|
|
108
108
|
>
|
|
109
109
|
<SquareTerminal className="h-3.5 w-3.5" />
|
|
@@ -191,7 +191,7 @@ export function TaskRow({ task }: { task: TaskView }) {
|
|
|
191
191
|
className="text-muted-foreground h-11 md:h-8"
|
|
192
192
|
onClick={() => {
|
|
193
193
|
recipesUiActions.saveSessionAs(task.id);
|
|
194
|
-
|
|
194
|
+
inboxUiActions.setOpen(false);
|
|
195
195
|
}}
|
|
196
196
|
>
|
|
197
197
|
<ChefHat className="h-3.5 w-3.5" />
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { useTasksQuery } from "@/data/tasks";
|
|
6
|
+
import { isFinished, needsHuman } from "@/lib/tasks/state";
|
|
7
|
+
import { inboxUiActions } from "@/stores/inboxUi";
|
|
8
|
+
import { newDraft } from "@/stores/drafts";
|
|
9
|
+
import { TaskRow } from "./TaskRow";
|
|
10
|
+
import { BoardTodoSection } from "@/components/LumifyHub";
|
|
11
|
+
import { StacksSection } from "@/components/Stacks";
|
|
12
|
+
import { FeaturesSection } from "@/components/Features";
|
|
13
|
+
|
|
14
|
+
export function TasksPanel() {
|
|
15
|
+
const { data: tasks = [], isPending } = useTasksQuery();
|
|
16
|
+
const active = tasks.filter((t) => !isFinished(t.state));
|
|
17
|
+
// Whatever needs a human first.
|
|
18
|
+
active.sort(
|
|
19
|
+
(a, b) => Number(needsHuman(b.state)) - Number(needsHuman(a.state))
|
|
20
|
+
);
|
|
21
|
+
const done = tasks.filter((t) => !active.includes(t)).slice(0, 10);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-3 [&>*]:min-w-0">
|
|
25
|
+
<div className="flex justify-end">
|
|
26
|
+
<Button
|
|
27
|
+
size="sm"
|
|
28
|
+
onClick={() => {
|
|
29
|
+
inboxUiActions.setOpen(false);
|
|
30
|
+
newDraft({ kind: "current", openPr: true });
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<Plus className="h-4 w-4" />
|
|
34
|
+
New task
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
{isPending && (
|
|
38
|
+
<div className="bg-muted/40 h-16 animate-pulse rounded-xl" />
|
|
39
|
+
)}
|
|
40
|
+
{!isPending && tasks.length === 0 && (
|
|
41
|
+
<p className="text-muted-foreground py-6 text-center text-sm">
|
|
42
|
+
No tasks yet. Start one and keep working while it runs.
|
|
43
|
+
</p>
|
|
44
|
+
)}
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
{active.map((t) => (
|
|
47
|
+
<TaskRow key={t.id} task={t} />
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
<FeaturesSection />
|
|
51
|
+
<StacksSection />
|
|
52
|
+
<BoardTodoSection />
|
|
53
|
+
{done.length > 0 && (
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<p className="label-mono text-muted-foreground pt-2">Finished</p>
|
|
56
|
+
{done.map((t) => (
|
|
57
|
+
<TaskRow key={t.id} task={t} />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { TasksPanel } from "./TasksPanel";
|
|
2
2
|
export { useTasksNeedingYou } from "./useTasksNeedingYou";
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { PanelLeft, PanelLeftClose, Plus } from "lucide-react";
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
5
|
import {
|
|
6
6
|
Tooltip,
|
|
7
7
|
TooltipContent,
|
|
8
8
|
TooltipTrigger,
|
|
9
9
|
} from "@/components/ui/tooltip";
|
|
10
|
-
import {
|
|
11
|
-
import { MessagesButton } from "@/components/Bus/MessagesButton";
|
|
12
|
-
import { DocsButton } from "@/components/LumifyHub/Docs/DocsButton";
|
|
13
|
-
import { NotificationsMenu } from "@/components/NotificationsMenu";
|
|
14
|
-
import type { ViewProps } from "./types";
|
|
10
|
+
import { InboxButton } from "@/components/Inbox/InboxButton";
|
|
15
11
|
import { newDraft } from "@/stores/drafts";
|
|
16
|
-
import { paletteActions } from "@/stores/palette";
|
|
17
|
-
import { useClock } from "@/hooks/useMinuteTick";
|
|
18
|
-
import { inBell } from "@/lib/sidebar/shelves";
|
|
19
12
|
import { DRAFT_KEYS } from "@/lib/drafts";
|
|
20
13
|
|
|
21
|
-
// Labels collapse to icons when the pane bar carrying these is narrow
|
|
22
|
-
// (a split), via the bar's container query.
|
|
23
|
-
export const BAR_LABEL = "hidden @5xl/bar:inline";
|
|
24
|
-
|
|
25
14
|
export function SidebarToggle({
|
|
26
15
|
isPinned,
|
|
27
16
|
togglePin,
|
|
@@ -56,71 +45,29 @@ export function SidebarToggle({
|
|
|
56
45
|
);
|
|
57
46
|
}
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
ViewProps,
|
|
61
|
-
| "sessions"
|
|
62
|
-
| "sessionStatuses"
|
|
63
|
-
| "activeSession"
|
|
64
|
-
| "notificationSettings"
|
|
65
|
-
| "attachToSession"
|
|
66
|
-
>;
|
|
67
|
-
|
|
68
|
-
export function DesktopAppActions({
|
|
69
|
-
sessions,
|
|
70
|
-
sessionStatuses,
|
|
71
|
-
activeSession,
|
|
72
|
-
notificationSettings,
|
|
73
|
-
attachToSession,
|
|
74
|
-
}: AppActionsProps) {
|
|
75
|
-
const now = useClock();
|
|
48
|
+
export function DesktopAppActions() {
|
|
76
49
|
return (
|
|
77
50
|
<div
|
|
78
51
|
className="flex shrink-0 items-center gap-0.5"
|
|
79
52
|
onClick={(e) => e.stopPropagation()}
|
|
80
53
|
>
|
|
54
|
+
<InboxButton />
|
|
81
55
|
<Tooltip>
|
|
82
56
|
<TooltipTrigger asChild>
|
|
83
57
|
<Button
|
|
84
|
-
variant="ghost"
|
|
85
58
|
size="icon-sm"
|
|
86
|
-
aria-label="
|
|
87
|
-
className="h-7 w-7"
|
|
88
|
-
onClick={
|
|
59
|
+
aria-label="New session"
|
|
60
|
+
className="ml-1 h-7 w-7"
|
|
61
|
+
onClick={() => newDraft({ kind: "current" })}
|
|
89
62
|
>
|
|
90
|
-
<
|
|
63
|
+
<Plus className="h-4 w-4" />
|
|
91
64
|
</Button>
|
|
92
65
|
</TooltipTrigger>
|
|
93
66
|
<TooltipContent>
|
|
94
|
-
<p>
|
|
95
|
-
<p className="text-muted-foreground text-xs"
|
|
67
|
+
<p>New session</p>
|
|
68
|
+
<p className="text-muted-foreground text-xs">{DRAFT_KEYS.current}</p>
|
|
96
69
|
</TooltipContent>
|
|
97
70
|
</Tooltip>
|
|
98
|
-
<NotificationsMenu
|
|
99
|
-
settings={notificationSettings}
|
|
100
|
-
waitingSessions={sessions
|
|
101
|
-
.filter((s) => inBell(s, sessionStatuses[s.id], now))
|
|
102
|
-
.map((s) => ({ id: s.id, name: s.name }))}
|
|
103
|
-
onSelectSession={(id) => {
|
|
104
|
-
const session = sessions.find((s) => s.id === id);
|
|
105
|
-
if (session) attachToSession(session);
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
<MessagesButton labelClassName={BAR_LABEL} />
|
|
109
|
-
<TasksButton labelClassName={BAR_LABEL} />
|
|
110
|
-
<DocsButton
|
|
111
|
-
projectId={activeSession?.project_id}
|
|
112
|
-
labelClassName={BAR_LABEL}
|
|
113
|
-
/>
|
|
114
|
-
<Button
|
|
115
|
-
size="sm"
|
|
116
|
-
aria-label="New session"
|
|
117
|
-
title={`New session (${DRAFT_KEYS.current})`}
|
|
118
|
-
className="ml-1 h-7 gap-1 px-2"
|
|
119
|
-
onClick={() => newDraft({ kind: "current" })}
|
|
120
|
-
>
|
|
121
|
-
<Plus className="h-4 w-4" />
|
|
122
|
-
<span className="hidden @3xl/bar:inline">New session</span>
|
|
123
|
-
</Button>
|
|
124
71
|
</div>
|
|
125
72
|
);
|
|
126
73
|
}
|
|
@@ -17,8 +17,6 @@ export function DesktopView({
|
|
|
17
17
|
focusedActiveTab,
|
|
18
18
|
showQuickSwitcher,
|
|
19
19
|
setShowQuickSwitcher,
|
|
20
|
-
notificationSettings,
|
|
21
|
-
attachToSession,
|
|
22
20
|
selectSession,
|
|
23
21
|
openSessionInNewTab,
|
|
24
22
|
handleNewSessionInProject,
|
|
@@ -61,15 +59,7 @@ export function DesktopView({
|
|
|
61
59
|
leading={
|
|
62
60
|
<SidebarToggle isPinned={isPinned} togglePin={togglePin} />
|
|
63
61
|
}
|
|
64
|
-
trailing={
|
|
65
|
-
<DesktopAppActions
|
|
66
|
-
sessions={sessions}
|
|
67
|
-
sessionStatuses={sessionStatuses}
|
|
68
|
-
activeSession={activeSession}
|
|
69
|
-
notificationSettings={notificationSettings}
|
|
70
|
-
attachToSession={attachToSession}
|
|
71
|
-
/>
|
|
72
|
-
}
|
|
62
|
+
trailing={<DesktopAppActions />}
|
|
73
63
|
>
|
|
74
64
|
<PaneLayout renderPane={renderPane} />
|
|
75
65
|
</PaneBarSlotsProvider>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { useSnapshot } from "valtio";
|
|
6
|
+
import { useSessionsQuery } from "@/data/sessions/queries";
|
|
7
|
+
import { fetchStatuses } from "@/data/statuses/queries";
|
|
8
|
+
import { statusKeys } from "@/data/sessions/keys";
|
|
9
|
+
import { useOrchestratorsQuery } from "@/data/orchestrators";
|
|
10
|
+
import { useTasksQuery } from "@/data/tasks";
|
|
11
|
+
import { useBusMessages } from "@/data/bus";
|
|
12
|
+
import { useClock } from "@/hooks/useMinuteTick";
|
|
13
|
+
import { inBell } from "@/lib/sidebar/shelves";
|
|
14
|
+
import { needsHuman } from "@/lib/tasks/state";
|
|
15
|
+
import {
|
|
16
|
+
inboxCounts,
|
|
17
|
+
needsYouRows,
|
|
18
|
+
unreadCount,
|
|
19
|
+
type InboxCounts,
|
|
20
|
+
type NeedsRow,
|
|
21
|
+
} from "@/lib/inbox/counts";
|
|
22
|
+
import { inboxUi, inboxUiActions } from "@/stores/inboxUi";
|
|
23
|
+
|
|
24
|
+
// Everything waiting on him, from the queries the old panels already used.
|
|
25
|
+
export function useInbox(): { rows: NeedsRow[]; counts: InboxCounts } {
|
|
26
|
+
const now = useClock();
|
|
27
|
+
const { data: sessions } = useSessionsQuery();
|
|
28
|
+
const { data: statuses } = useQuery({
|
|
29
|
+
queryKey: statusKeys.all,
|
|
30
|
+
queryFn: fetchStatuses,
|
|
31
|
+
staleTime: 2000,
|
|
32
|
+
});
|
|
33
|
+
const { data: overviews } = useOrchestratorsQuery();
|
|
34
|
+
const { data: tasks } = useTasksQuery();
|
|
35
|
+
const { data: messages } = useBusMessages();
|
|
36
|
+
const { seenMessageId } = useSnapshot(inboxUi);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
inboxUiActions.loadSeen();
|
|
40
|
+
}, []);
|
|
41
|
+
// First run: what is already there isn't new.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!messages || inboxUi.seenMessageId !== null) return;
|
|
44
|
+
inboxUiActions.markSeen(messages.reduce((n, m) => Math.max(n, m.id), 0));
|
|
45
|
+
}, [messages, seenMessageId]);
|
|
46
|
+
|
|
47
|
+
const rows = useMemo(
|
|
48
|
+
() =>
|
|
49
|
+
needsYouRows({
|
|
50
|
+
waiting: (sessions?.sessions ?? [])
|
|
51
|
+
.filter((s) => inBell(s, statuses?.statuses[s.id], now))
|
|
52
|
+
.map((s) => ({ id: s.id, name: s.name })),
|
|
53
|
+
overviews: overviews ?? [],
|
|
54
|
+
}),
|
|
55
|
+
[sessions, statuses, overviews, now]
|
|
56
|
+
);
|
|
57
|
+
const counts = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
inboxCounts({
|
|
60
|
+
rows,
|
|
61
|
+
tasksNeedingYou: (tasks ?? [])
|
|
62
|
+
.filter((t) => needsHuman(t.state))
|
|
63
|
+
.map((t) => t.id),
|
|
64
|
+
unread: unreadCount(messages ?? [], seenMessageId),
|
|
65
|
+
}),
|
|
66
|
+
[rows, tasks, messages, seenMessageId]
|
|
67
|
+
);
|
|
68
|
+
return { rows, counts };
|
|
69
|
+
}
|