@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
|
@@ -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" />
|
|
@@ -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
|
+
}
|
package/lib/env-setup.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import { bringDependencies, runCommand } from "./worktree-deps";
|
|
12
|
+
import { inside, realExisting } from "./worktree-deps/paths";
|
|
13
|
+
import { seedBuildCaches } from "./worktree-deps/caches";
|
|
12
14
|
import { loadProjectConfig, projectEnv, withPorts } from "./project-config";
|
|
13
15
|
import { recordPlaced } from "./worktree-placed";
|
|
14
16
|
import { databaseEnv, ensureSessionDatabase } from "./project-config/database";
|
|
@@ -79,18 +81,6 @@ export async function copyEnvFiles(
|
|
|
79
81
|
return copied;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
const inside = (root: string, p: string) =>
|
|
83
|
-
p === root || p.startsWith(root + path.sep);
|
|
84
|
-
|
|
85
|
-
// The real path of `p`'s deepest part that exists.
|
|
86
|
-
async function realExisting(p: string): Promise<string> {
|
|
87
|
-
for (let at = p; ; at = path.dirname(at)) {
|
|
88
|
-
const real = await fs.promises.realpath(at).catch(() => null);
|
|
89
|
-
if (real) return real;
|
|
90
|
-
if (path.dirname(at) === at) return at;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
84
|
/**
|
|
95
85
|
* agentos.json's `copy`: files or folders from the main checkout, each one
|
|
96
86
|
* resolved (symlinks included) to a place inside it, so a project can't
|
|
@@ -272,6 +262,31 @@ export async function setupWorktree(options: {
|
|
|
272
262
|
);
|
|
273
263
|
}
|
|
274
264
|
|
|
265
|
+
// Build caches, so the first typecheck, test or build is warm. An
|
|
266
|
+
// optimisation: it never fails a setup.
|
|
267
|
+
if (config.caches?.length !== 0 && sourcePath !== worktreePath) {
|
|
268
|
+
progress?.onStage?.("deps");
|
|
269
|
+
try {
|
|
270
|
+
const seeded = await seedBuildCaches({
|
|
271
|
+
sourcePath,
|
|
272
|
+
worktreePath,
|
|
273
|
+
paths: config.caches,
|
|
274
|
+
});
|
|
275
|
+
if (seeded.seeded.length || seeded.skipped.length)
|
|
276
|
+
result.steps.push({
|
|
277
|
+
name: "Seed build caches",
|
|
278
|
+
command: "cp (build caches)",
|
|
279
|
+
success: true,
|
|
280
|
+
output: [
|
|
281
|
+
...seeded.seeded.map((c) => `${c.rel} (${c.how})`),
|
|
282
|
+
...seeded.skipped.map((c) => `skipped ${c.rel}: ${c.why}`),
|
|
283
|
+
].join("\n"),
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("Seeding build caches failed:", error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
// 4. The config's setup commands
|
|
276
291
|
if (config.setup && config.setup.length > 0) {
|
|
277
292
|
progress?.onStage?.("script");
|