@saadnvd1/agent-os 0.3.700 → 0.3.715
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/README.md +1 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +12 -1
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +15 -14
- package/app/page.tsx +5 -0
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftPanel.tsx +40 -2
- package/components/Hosts/AccountMachines.tsx +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +1 -0
- package/components/SessionList/SessionRowMenu.tsx +17 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +4 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +10 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/spawn.ts +4 -0
- package/lib/bus/index.ts +8 -0
- package/lib/claims/collectLocal.ts +121 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +1 -0
- package/lib/db/types.ts +16 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +6 -1
- package/lib/orchestrator/facts.ts +5 -0
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/launch.ts +5 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +5 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +1 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +3 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +3 -0
- package/stores/claimsUi.ts +22 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Check, Folder, Plus, X } from "lucide-react";
|
|
5
|
+
import { useSessionsQuery } from "@/data/sessions";
|
|
6
|
+
import { useAssignGroup, useCreateGroup } from "@/data/session-groups";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { Expander, MenuItem, icon, type MenuKind } from "./RowMenuParts";
|
|
9
|
+
|
|
10
|
+
// "Move to group": the groups of the session's workspace with the current
|
|
11
|
+
// one ticked, a way out, and an inline field that makes a new group and
|
|
12
|
+
// files the session into it.
|
|
13
|
+
export function MoveToGroupMenu({
|
|
14
|
+
kind,
|
|
15
|
+
sessionId,
|
|
16
|
+
groupId,
|
|
17
|
+
workspaceId,
|
|
18
|
+
}: {
|
|
19
|
+
kind: MenuKind;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
groupId: string | null | undefined;
|
|
22
|
+
workspaceId: string | null;
|
|
23
|
+
}) {
|
|
24
|
+
const { data } = useSessionsQuery();
|
|
25
|
+
const assign = useAssignGroup();
|
|
26
|
+
const create = useCreateGroup();
|
|
27
|
+
const [naming, setNaming] = useState(false);
|
|
28
|
+
const [name, setName] = useState("");
|
|
29
|
+
const groups = (data?.sessionGroups ?? [])
|
|
30
|
+
.filter((g) => g.workspace_id === workspaceId)
|
|
31
|
+
.sort((a, b) => a.sort_order - b.sort_order);
|
|
32
|
+
|
|
33
|
+
const make = async () => {
|
|
34
|
+
const next = name.trim();
|
|
35
|
+
if (!next) return;
|
|
36
|
+
const { sessionGroup } = await create.mutateAsync({
|
|
37
|
+
name: next,
|
|
38
|
+
workspaceId,
|
|
39
|
+
});
|
|
40
|
+
assign.mutate({ ids: [sessionId], groupId: sessionGroup.id });
|
|
41
|
+
setNaming(false);
|
|
42
|
+
setName("");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Expander kind={kind} icon={Folder} label="Move to group">
|
|
47
|
+
{groups.map((g) => (
|
|
48
|
+
<MenuItem
|
|
49
|
+
key={g.id}
|
|
50
|
+
kind={kind}
|
|
51
|
+
onClick={() =>
|
|
52
|
+
assign.mutate({
|
|
53
|
+
ids: [sessionId],
|
|
54
|
+
groupId: g.id === groupId ? null : g.id,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<Check className={`${icon} ${g.id === groupId ? "" : "invisible"}`} />
|
|
59
|
+
<span className="truncate">{g.name}</span>
|
|
60
|
+
</MenuItem>
|
|
61
|
+
))}
|
|
62
|
+
{groupId && (
|
|
63
|
+
<MenuItem
|
|
64
|
+
kind={kind}
|
|
65
|
+
onClick={() => assign.mutate({ ids: [sessionId], groupId: null })}
|
|
66
|
+
>
|
|
67
|
+
<X className={icon} />
|
|
68
|
+
Remove from group
|
|
69
|
+
</MenuItem>
|
|
70
|
+
)}
|
|
71
|
+
{naming ? (
|
|
72
|
+
<div className="px-1.5 py-1">
|
|
73
|
+
<Input
|
|
74
|
+
autoFocus
|
|
75
|
+
value={name}
|
|
76
|
+
maxLength={60}
|
|
77
|
+
placeholder="Group name"
|
|
78
|
+
aria-label="New group name"
|
|
79
|
+
className="h-11 md:h-8"
|
|
80
|
+
onChange={(e) => setName(e.target.value)}
|
|
81
|
+
onKeyDown={(e) => {
|
|
82
|
+
// Keys here are typing, not the menu's typeahead.
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
if (e.key === "Enter") void make();
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
<p className="text-muted-foreground/70 px-1 pt-1 text-[11px]">
|
|
88
|
+
↵ create
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
) : (
|
|
92
|
+
<MenuItem
|
|
93
|
+
kind={kind}
|
|
94
|
+
onSelect={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setNaming(true);
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Plus className={icon} />
|
|
100
|
+
New group…
|
|
101
|
+
</MenuItem>
|
|
102
|
+
)}
|
|
103
|
+
</Expander>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -9,6 +9,7 @@ import { headerParts } from "@/lib/orchestrator/header-line";
|
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
import { usageLabel, useSessionUsage } from "@/data/load";
|
|
11
11
|
import { useRowContext } from "./RowContext";
|
|
12
|
+
import { OverlapPill } from "@/components/Claims/OverlapPill";
|
|
12
13
|
import { pauseSubtitle } from "@/lib/sidebar/paused";
|
|
13
14
|
|
|
14
15
|
const ATTENTION = (part: string) => part === "paused" || /\basks?$/.test(part);
|
|
@@ -22,11 +23,14 @@ export function RowSubtitle({
|
|
|
22
23
|
detail,
|
|
23
24
|
live,
|
|
24
25
|
hideHost = false,
|
|
26
|
+
groupName,
|
|
25
27
|
}: {
|
|
26
28
|
session: Session;
|
|
27
29
|
detail?: string | null;
|
|
28
30
|
live?: LiveInput;
|
|
29
31
|
hideHost?: boolean;
|
|
32
|
+
// Set on a finished session that keeps its group: Done shows where it goes back to.
|
|
33
|
+
groupName?: string;
|
|
30
34
|
}) {
|
|
31
35
|
const ctx = useRowContext();
|
|
32
36
|
// What its processes use, on a busy session: "3.1 cores · 4.2 GB".
|
|
@@ -63,6 +67,7 @@ export function RowSubtitle({
|
|
|
63
67
|
return (
|
|
64
68
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
65
69
|
<LiveLine {...live} />
|
|
70
|
+
{session.overlap && <OverlapPill overlap={session.overlap} />}
|
|
66
71
|
{pause && (
|
|
67
72
|
<span className="flex min-w-0 items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
|
68
73
|
<Pause aria-hidden className="h-3 w-3 shrink-0" />
|
|
@@ -102,13 +107,14 @@ export function RowSubtitle({
|
|
|
102
107
|
</span>
|
|
103
108
|
</span>
|
|
104
109
|
)}
|
|
105
|
-
{(project || detail || usage || session.placed_note) && (
|
|
110
|
+
{(project || detail || usage || session.placed_note || groupName) && (
|
|
106
111
|
<span className="text-muted-foreground/70 min-w-0 truncate text-xs">
|
|
107
112
|
{[
|
|
108
113
|
usage && usageLabel(usage),
|
|
109
114
|
detail?.slice(0, 200),
|
|
110
115
|
session.placed_note,
|
|
111
116
|
project,
|
|
117
|
+
groupName,
|
|
112
118
|
]
|
|
113
119
|
.filter(Boolean)
|
|
114
120
|
.join(" · ")}
|
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
import { useEffect, useCallback, useState } from "react";
|
|
4
4
|
import { useSnapshot } from "valtio";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Trash2, X } from "lucide-react";
|
|
6
|
+
import { Check, Folder, Trash2, X } from "lucide-react";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu";
|
|
14
|
+
import { useSessionsQuery } from "@/data/sessions";
|
|
15
|
+
import { useAssignGroup } from "@/data/session-groups";
|
|
16
|
+
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
7
17
|
import { selectionStore, selectionActions } from "@/stores/sessionSelection";
|
|
8
18
|
import {
|
|
9
19
|
Dialog,
|
|
@@ -32,6 +42,19 @@ export function SelectionToolbar({
|
|
|
32
42
|
const selectedCount = selectedIds.size;
|
|
33
43
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
34
44
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
45
|
+
const { workspace } = useSelectedWorkspace();
|
|
46
|
+
const { data } = useSessionsQuery();
|
|
47
|
+
const assign = useAssignGroup();
|
|
48
|
+
const groups = (data?.sessionGroups ?? []).filter(
|
|
49
|
+
(g) => !workspace || g.workspace_id === workspace.id
|
|
50
|
+
);
|
|
51
|
+
const moveTo = (groupId: string | null) => {
|
|
52
|
+
const ids = selectionActions.getSelectedIds();
|
|
53
|
+
assign.mutate(
|
|
54
|
+
{ ids, groupId },
|
|
55
|
+
{ onSuccess: () => selectionActions.clear() }
|
|
56
|
+
);
|
|
57
|
+
};
|
|
35
58
|
|
|
36
59
|
const handleSelectAll = useCallback(() => {
|
|
37
60
|
selectionActions.selectAll(allSessionIds);
|
|
@@ -101,6 +124,34 @@ export function SelectionToolbar({
|
|
|
101
124
|
Select all
|
|
102
125
|
</Button>
|
|
103
126
|
)}
|
|
127
|
+
<DropdownMenu>
|
|
128
|
+
<DropdownMenuTrigger asChild>
|
|
129
|
+
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs">
|
|
130
|
+
<Folder className="h-3 w-3" />
|
|
131
|
+
Move to group
|
|
132
|
+
</Button>
|
|
133
|
+
</DropdownMenuTrigger>
|
|
134
|
+
<DropdownMenuContent align="end" className="w-52">
|
|
135
|
+
{groups.map((g) => (
|
|
136
|
+
<DropdownMenuItem
|
|
137
|
+
key={g.id}
|
|
138
|
+
className="min-h-11 md:min-h-8"
|
|
139
|
+
onSelect={() => moveTo(g.id)}
|
|
140
|
+
>
|
|
141
|
+
<Check className="invisible mr-2 h-3.5 w-3.5" />
|
|
142
|
+
<span className="truncate">{g.name}</span>
|
|
143
|
+
</DropdownMenuItem>
|
|
144
|
+
))}
|
|
145
|
+
{groups.length > 0 && <DropdownMenuSeparator />}
|
|
146
|
+
<DropdownMenuItem
|
|
147
|
+
className="min-h-11 md:min-h-8"
|
|
148
|
+
onSelect={() => moveTo(null)}
|
|
149
|
+
>
|
|
150
|
+
<X className="mr-2 h-3.5 w-3.5" />
|
|
151
|
+
Remove from group
|
|
152
|
+
</DropdownMenuItem>
|
|
153
|
+
</DropdownMenuContent>
|
|
154
|
+
</DropdownMenu>
|
|
104
155
|
<Tooltip>
|
|
105
156
|
<TooltipTrigger asChild>
|
|
106
157
|
<Button
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
type RowMenuItem,
|
|
62
62
|
} from "@/lib/sidebar/row-menu";
|
|
63
63
|
import { Expander, MenuItem, PARTS, icon, type MenuKind } from "./RowMenuParts";
|
|
64
|
+
import { MoveToGroupMenu } from "./MoveToGroupMenu";
|
|
64
65
|
|
|
65
66
|
const GROUP_ICON: Record<RowMenuGroup, typeof Copy> = {
|
|
66
67
|
open: ExternalLink,
|
|
@@ -105,6 +106,12 @@ export function SessionRowMenu({
|
|
|
105
106
|
ctx.projects.find((p) => p.id === session.project_id)?.workspace_id ??
|
|
106
107
|
null;
|
|
107
108
|
|
|
109
|
+
// The server files by the project's workspace first.
|
|
110
|
+
const groupWorkspaceId =
|
|
111
|
+
ctx.projects.find((p) => p.id === session.project_id)?.workspace_id ??
|
|
112
|
+
session.workspace_id ??
|
|
113
|
+
null;
|
|
114
|
+
|
|
108
115
|
const plan = rowMenu({
|
|
109
116
|
session,
|
|
110
117
|
row,
|
|
@@ -222,6 +229,16 @@ export function SessionRowMenu({
|
|
|
222
229
|
{projects}
|
|
223
230
|
</Expander>
|
|
224
231
|
);
|
|
232
|
+
case "move-group":
|
|
233
|
+
return (
|
|
234
|
+
<MoveToGroupMenu
|
|
235
|
+
key={it}
|
|
236
|
+
kind={kind}
|
|
237
|
+
sessionId={session.id}
|
|
238
|
+
groupId={session.session_group_id}
|
|
239
|
+
workspaceId={groupWorkspaceId}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
225
242
|
case "check-ins":
|
|
226
243
|
return item("Schedule check-ins", Clock, () =>
|
|
227
244
|
schedulesUiActions.openDraft(workspaceId!, {
|
|
@@ -14,6 +14,7 @@ import { sidebarUiActions } from "@/stores/sidebarUi";
|
|
|
14
14
|
import { cn } from "@/lib/utils";
|
|
15
15
|
import { usePendingCardSessions } from "@/data/learning-cards";
|
|
16
16
|
import { SessionRow } from "./SessionRow";
|
|
17
|
+
import { GroupShelves } from "./GroupShelves";
|
|
17
18
|
import { OrchestratorPinRow, OrchestratorStartRow } from "./OrchestratorPinRow";
|
|
18
19
|
import { QueuedRow } from "./QueuedRow";
|
|
19
20
|
import type { QueuedRow as QueuedRowData } from "@/lib/sidebar/queued";
|
|
@@ -117,6 +118,7 @@ export function SessionShelves({
|
|
|
117
118
|
doneCollapsed,
|
|
118
119
|
snoozedCollapsed,
|
|
119
120
|
donePages,
|
|
121
|
+
workspaceNames,
|
|
120
122
|
}: {
|
|
121
123
|
shelves: Shelves;
|
|
122
124
|
queued: QueuedRowData[];
|
|
@@ -124,6 +126,7 @@ export function SessionShelves({
|
|
|
124
126
|
doneCollapsed: boolean;
|
|
125
127
|
snoozedCollapsed: boolean;
|
|
126
128
|
donePages: number;
|
|
129
|
+
workspaceNames?: Map<string, string>;
|
|
127
130
|
}) {
|
|
128
131
|
const folds = { doneCollapsed, snoozedCollapsed, donePages };
|
|
129
132
|
const shown = shelves.done.slice(0, doneLimit(donePages));
|
|
@@ -142,6 +145,7 @@ export function SessionShelves({
|
|
|
142
145
|
shelves.orchestrators.length +
|
|
143
146
|
toStart.length +
|
|
144
147
|
shelves.pinned.length +
|
|
148
|
+
shelves.groups.length +
|
|
145
149
|
shelves.needsYou.length +
|
|
146
150
|
shelves.working.length +
|
|
147
151
|
shelves.inReview.length +
|
|
@@ -167,6 +171,7 @@ export function SessionShelves({
|
|
|
167
171
|
))}
|
|
168
172
|
</Shelf>
|
|
169
173
|
)}
|
|
174
|
+
<GroupShelves groups={shelves.groups} workspaceNames={workspaceNames} />
|
|
170
175
|
{shelves.needsYou.length > 0 && (
|
|
171
176
|
<Shelf label="Needs you" count={shelves.needsYou.length} accent>
|
|
172
177
|
{shelves.needsYou.map((r) => (
|
|
@@ -63,6 +63,7 @@ export function SidebarBody({
|
|
|
63
63
|
const count =
|
|
64
64
|
shelves.orchestrators.length +
|
|
65
65
|
shelves.pinned.length +
|
|
66
|
+
shelves.groups.length +
|
|
66
67
|
shelves.needsYou.length +
|
|
67
68
|
shelves.working.length +
|
|
68
69
|
shelves.inReview.length +
|
|
@@ -125,6 +126,11 @@ export function SidebarBody({
|
|
|
125
126
|
doneCollapsed={ui.doneCollapsed}
|
|
126
127
|
snoozedCollapsed={ui.snoozedCollapsed}
|
|
127
128
|
donePages={ui.donePages}
|
|
129
|
+
workspaceNames={
|
|
130
|
+
data.workspace
|
|
131
|
+
? undefined
|
|
132
|
+
: new Map(data.workspaces.map((w) => [w.id, w.name]))
|
|
133
|
+
}
|
|
128
134
|
/>
|
|
129
135
|
<MachineShelves groups={data.machines} />
|
|
130
136
|
<SidebarExtras
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
|
|
6
|
+
const mutate = vi.fn();
|
|
7
|
+
vi.mock("@/data/session-groups", () => ({
|
|
8
|
+
useCreateGroup: () => ({ mutate }),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// Radix menus need a real browser; render the item inline.
|
|
12
|
+
vi.mock("@/components/ui/context-menu", async () => {
|
|
13
|
+
const { createElement: h } = await import("react");
|
|
14
|
+
const part = (p: { children?: unknown }) =>
|
|
15
|
+
h("div", null, p.children as never);
|
|
16
|
+
return {
|
|
17
|
+
ContextMenu: part,
|
|
18
|
+
ContextMenuTrigger: part,
|
|
19
|
+
ContextMenuContent: part,
|
|
20
|
+
ContextMenuItem: (p: { children?: unknown; onSelect?: () => void }) =>
|
|
21
|
+
h("button", { onClick: p.onSelect }, p.children as never),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock("@/components/ui/dialog", async () => {
|
|
26
|
+
const { createElement: h } = await import("react");
|
|
27
|
+
const part = (p: { children?: unknown; open?: boolean }) =>
|
|
28
|
+
p.open === false ? null : h("div", null, p.children as never);
|
|
29
|
+
return {
|
|
30
|
+
Dialog: part,
|
|
31
|
+
DialogContent: part,
|
|
32
|
+
DialogHeader: part,
|
|
33
|
+
DialogTitle: part,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
import { SidebarContextMenu, isEditableTarget } from "./SidebarContextMenu";
|
|
38
|
+
|
|
39
|
+
(
|
|
40
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
41
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
42
|
+
|
|
43
|
+
let root: Root;
|
|
44
|
+
let host: HTMLElement;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
mutate.mockClear();
|
|
47
|
+
host = document.createElement("div");
|
|
48
|
+
document.body.append(host);
|
|
49
|
+
root = createRoot(host);
|
|
50
|
+
act(() =>
|
|
51
|
+
root.render(createElement(SidebarContextMenu, { workspaceId: "w1" }, "x"))
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
act(() => root.unmount());
|
|
56
|
+
host.remove();
|
|
57
|
+
document.body.innerHTML = "";
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const openDialog = () =>
|
|
61
|
+
act(() => {
|
|
62
|
+
host.querySelector("button")!.click();
|
|
63
|
+
});
|
|
64
|
+
const type = (value: string) => {
|
|
65
|
+
const input = document.body.querySelector("input")!;
|
|
66
|
+
act(() => {
|
|
67
|
+
Object.getOwnPropertyDescriptor(
|
|
68
|
+
HTMLInputElement.prototype,
|
|
69
|
+
"value"
|
|
70
|
+
)!.set!.call(input, value);
|
|
71
|
+
input.dispatchEvent(new window.Event("input", { bubbles: true }));
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const submit = () =>
|
|
75
|
+
act(() => {
|
|
76
|
+
document.body
|
|
77
|
+
.querySelector("form")!
|
|
78
|
+
.dispatchEvent(
|
|
79
|
+
new window.Event("submit", { bubbles: true, cancelable: true })
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("SidebarContextMenu", () => {
|
|
84
|
+
it("creates a group in the workspace from the typed name", () => {
|
|
85
|
+
openDialog();
|
|
86
|
+
type(" Docs ");
|
|
87
|
+
submit();
|
|
88
|
+
expect(mutate).toHaveBeenCalledWith({ name: "Docs", workspaceId: "w1" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does nothing for an empty name", () => {
|
|
92
|
+
openDialog();
|
|
93
|
+
type(" ");
|
|
94
|
+
submit();
|
|
95
|
+
expect(mutate).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("leaves text fields to the browser's menu", () => {
|
|
99
|
+
const input = document.createElement("input");
|
|
100
|
+
const div = document.createElement("div");
|
|
101
|
+
expect(isEditableTarget(input)).toBe(true);
|
|
102
|
+
expect(isEditableTarget(div)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type MouseEvent, type ReactNode } from "react";
|
|
4
|
+
import { FolderPlus } from "lucide-react";
|
|
5
|
+
import * as CM from "@/components/ui/context-menu";
|
|
6
|
+
import { WorkspaceNameDialog } from "@/components/Workspaces";
|
|
7
|
+
import { useCreateGroup } from "@/data/session-groups";
|
|
8
|
+
|
|
9
|
+
export function isEditableTarget(target: EventTarget | null): boolean {
|
|
10
|
+
return (
|
|
11
|
+
target instanceof HTMLElement &&
|
|
12
|
+
!!target.closest("input, textarea, select, [contenteditable='true']")
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Right-click anywhere in the session list (empty space or a group header)
|
|
17
|
+
// offers "New group". Rows keep their own menu, which wins because it has
|
|
18
|
+
// already handled the event; text fields keep the browser's.
|
|
19
|
+
export function SidebarContextMenu({
|
|
20
|
+
workspaceId,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
workspaceId: string | null;
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const createGroup = useCreateGroup();
|
|
27
|
+
const [naming, setNaming] = useState(false);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<CM.ContextMenu>
|
|
32
|
+
<CM.ContextMenuTrigger
|
|
33
|
+
asChild
|
|
34
|
+
onContextMenuCapture={(e: MouseEvent) => {
|
|
35
|
+
if (isEditableTarget(e.target)) e.stopPropagation();
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</CM.ContextMenuTrigger>
|
|
40
|
+
<CM.ContextMenuContent className="w-44">
|
|
41
|
+
<CM.ContextMenuItem
|
|
42
|
+
className="min-h-11 md:min-h-8"
|
|
43
|
+
onSelect={() => setNaming(true)}
|
|
44
|
+
>
|
|
45
|
+
<FolderPlus className="mr-2 h-3.5 w-3.5" />
|
|
46
|
+
New group
|
|
47
|
+
</CM.ContextMenuItem>
|
|
48
|
+
</CM.ContextMenuContent>
|
|
49
|
+
</CM.ContextMenu>
|
|
50
|
+
<WorkspaceNameDialog
|
|
51
|
+
open={naming}
|
|
52
|
+
title="New group"
|
|
53
|
+
submitLabel="Create"
|
|
54
|
+
onSubmit={(name) => createGroup.mutate({ name, workspaceId })}
|
|
55
|
+
onClose={() => setNaming(false)}
|
|
56
|
+
/>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MoreHorizontal,
|
|
16
16
|
Pin,
|
|
17
17
|
PinOff,
|
|
18
|
+
Network,
|
|
18
19
|
Plus,
|
|
19
20
|
Server,
|
|
20
21
|
Settings,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
import { Button } from "@/components/ui/button";
|
|
31
32
|
import type { Workspace } from "@/lib/db";
|
|
32
33
|
import { archivedUiActions } from "@/stores/archivedUi";
|
|
34
|
+
import { claimsUiActions } from "@/stores/claimsUi";
|
|
33
35
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
34
36
|
import { inboxUiActions } from "@/stores/inboxUi";
|
|
35
37
|
import { newDraft } from "@/stores/drafts";
|
|
@@ -134,6 +136,9 @@ export function SidebarHeader(props: SidebarHeaderProps) {
|
|
|
134
136
|
}),
|
|
135
137
|
separator(),
|
|
136
138
|
menuItem("Machines", props.onManageHosts, { icon: Server }),
|
|
139
|
+
menuItem("Work across machines", () => claimsUiActions.open(), {
|
|
140
|
+
icon: Network,
|
|
141
|
+
}),
|
|
137
142
|
menuItem("Settings", () => settingsUiActions.open(), {
|
|
138
143
|
icon: Settings,
|
|
139
144
|
}),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ChevronDown,
|
|
9
9
|
CircleCheck,
|
|
10
10
|
Clock,
|
|
11
|
+
Folder,
|
|
11
12
|
FolderPlus,
|
|
12
13
|
Gauge,
|
|
13
14
|
LayoutGrid,
|
|
@@ -32,6 +33,7 @@ import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
|
|
|
32
33
|
import { sidebarUiActions } from "@/stores/sidebarUi";
|
|
33
34
|
import { schedulesUiActions } from "@/stores/schedulesUi";
|
|
34
35
|
import { recipesUiActions } from "@/stores/recipesUi";
|
|
36
|
+
import { useCreateGroup } from "@/data/session-groups";
|
|
35
37
|
import { useAddExistingProject } from "@/components/Projects/AddProject/useAddExistingProject";
|
|
36
38
|
|
|
37
39
|
const icon = "mr-2 h-3.5 w-3.5";
|
|
@@ -49,7 +51,8 @@ export function WorkspaceSwitcher({
|
|
|
49
51
|
const update = useUpdateWorkspace();
|
|
50
52
|
const remove = useDeleteWorkspace();
|
|
51
53
|
const addExisting = useAddExistingProject();
|
|
52
|
-
const
|
|
54
|
+
const createGroup = useCreateGroup();
|
|
55
|
+
const [dialog, setDialog] = useState<"new" | "rename" | "group" | null>(null);
|
|
53
56
|
const name = current?.name ?? "All workspaces";
|
|
54
57
|
|
|
55
58
|
return (
|
|
@@ -89,6 +92,12 @@ export function WorkspaceSwitcher({
|
|
|
89
92
|
<Plus className={icon} />
|
|
90
93
|
New workspace
|
|
91
94
|
</DM.DropdownMenuItem>
|
|
95
|
+
{current && (
|
|
96
|
+
<DM.DropdownMenuItem onClick={() => setDialog("group")}>
|
|
97
|
+
<Folder className={icon} />
|
|
98
|
+
New group
|
|
99
|
+
</DM.DropdownMenuItem>
|
|
100
|
+
)}
|
|
92
101
|
{!current && (
|
|
93
102
|
<>
|
|
94
103
|
<DM.DropdownMenuItem onClick={() => recipesUiActions.open(null)}>
|
|
@@ -175,14 +184,22 @@ export function WorkspaceSwitcher({
|
|
|
175
184
|
</DM.DropdownMenuContent>
|
|
176
185
|
</DM.DropdownMenu>
|
|
177
186
|
<WorkspaceNameDialog
|
|
178
|
-
open={dialog
|
|
179
|
-
title={
|
|
187
|
+
open={dialog !== null}
|
|
188
|
+
title={
|
|
189
|
+
dialog === "rename"
|
|
190
|
+
? "Rename workspace"
|
|
191
|
+
: dialog === "group"
|
|
192
|
+
? "New group"
|
|
193
|
+
: "New workspace"
|
|
194
|
+
}
|
|
180
195
|
initialName={dialog === "rename" ? current?.name : ""}
|
|
181
196
|
submitLabel={dialog === "rename" ? "Rename" : "Create"}
|
|
182
197
|
onSubmit={(name) =>
|
|
183
|
-
dialog === "
|
|
184
|
-
?
|
|
185
|
-
:
|
|
198
|
+
dialog === "group"
|
|
199
|
+
? createGroup.mutate({ name, workspaceId: current?.id ?? null })
|
|
200
|
+
: dialog === "rename" && current
|
|
201
|
+
? update.mutate({ id: current.id, name })
|
|
202
|
+
: create.mutate(name)
|
|
186
203
|
}
|
|
187
204
|
onClose={() => setDialog(null)}
|
|
188
205
|
/>
|
|
@@ -21,6 +21,7 @@ import { SidebarSearch } from "./SidebarSearch";
|
|
|
21
21
|
import { ProjectFilter } from "./ProjectFilter";
|
|
22
22
|
import { SelectionToolbar } from "./SelectionToolbar";
|
|
23
23
|
import { SidebarBody } from "./SidebarBody";
|
|
24
|
+
import { SidebarContextMenu } from "./SidebarContextMenu";
|
|
24
25
|
import { SidebarDialogs, type SidebarDialog } from "./SidebarDialogs";
|
|
25
26
|
import type { SessionListProps } from "./SessionList.types";
|
|
26
27
|
|
|
@@ -193,20 +194,23 @@ export function SessionList({
|
|
|
193
194
|
)}
|
|
194
195
|
|
|
195
196
|
<ScrollArea className="w-full flex-1">
|
|
196
|
-
<
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
<SidebarContextMenu workspaceId={data.workspace?.id ?? null}>
|
|
198
|
+
<div className="min-h-[60vh] max-w-full px-1.5 pb-6">
|
|
199
|
+
<RowProvider value={rowContext}>
|
|
200
|
+
<SidebarBody
|
|
201
|
+
data={data}
|
|
202
|
+
onNewProject={addProject}
|
|
203
|
+
devServerHandlers={{
|
|
204
|
+
onStop: mutations.handleStopDevServer,
|
|
205
|
+
onRestart: mutations.handleRestartDevServer,
|
|
206
|
+
onRemove: mutations.handleRemoveDevServer,
|
|
207
|
+
onViewLogs: (serverId) =>
|
|
208
|
+
setDialog({ kind: "logs", serverId }),
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</RowProvider>
|
|
212
|
+
</div>
|
|
213
|
+
</SidebarContextMenu>
|
|
210
214
|
</ScrollArea>
|
|
211
215
|
|
|
212
216
|
<SidebarDialogs
|