@saadnvd1/agent-os 0.3.700 → 0.3.723
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 +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +34 -2
- 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/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- 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]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -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]/model/route.ts +49 -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 +31 -14
- package/app/page.tsx +10 -0
- package/components/Chat/Messages.tsx +104 -10
- 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/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +90 -3
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- 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/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -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 +19 -7
- package/components/SessionList/SessionRowMenu.tsx +23 -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/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +9 -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 +36 -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/brief.ts +1 -0
- package/lib/agents/spawn.ts +4 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +13 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +123 -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 +12 -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/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +3 -0
- package/lib/db/types.ts +19 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -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 +12 -2
- package/lib/orchestrator/read.ts +5 -1
- 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/untrusted.ts +5 -0
- 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 +6 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -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/crew.ts +72 -0
- package/lib/sessions/launch.ts +54 -11
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +8 -1
- 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 +11 -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-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- 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 +12 -1
- package/stores/claimsUi.ts +22 -0
- package/stores/exportUi.ts +87 -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,48 @@
|
|
|
1
|
+
import type { Availability } from "@/lib/models/availability";
|
|
2
|
+
import type { Locality } from "@/lib/models/locality";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
// Whether a model runs on this machine's own networks or in the cloud.
|
|
6
|
+
export function LocalityBadge({ locality }: { locality: Locality }) {
|
|
7
|
+
return (
|
|
8
|
+
<span
|
|
9
|
+
className={cn(
|
|
10
|
+
"inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-medium",
|
|
11
|
+
locality === "local"
|
|
12
|
+
? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"
|
|
13
|
+
: "bg-muted text-muted-foreground ring-border ring-1 ring-inset"
|
|
14
|
+
)}
|
|
15
|
+
>
|
|
16
|
+
{locality === "local" ? "Local" : "Cloud"}
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DOT = {
|
|
22
|
+
up: "bg-emerald-500",
|
|
23
|
+
down: "bg-destructive",
|
|
24
|
+
unknown: "bg-muted-foreground/50",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
const TITLE = {
|
|
28
|
+
up: "Answering",
|
|
29
|
+
down: "Not answering",
|
|
30
|
+
unknown: "Not checked",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// The last availability check: green answering, red not, grey not checked.
|
|
34
|
+
export function AvailabilityDot({
|
|
35
|
+
availability,
|
|
36
|
+
}: {
|
|
37
|
+
availability: Pick<Availability, "state"> | undefined;
|
|
38
|
+
}) {
|
|
39
|
+
const state = availability?.state ?? "unknown";
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
role="img"
|
|
43
|
+
aria-label={TITLE[state]}
|
|
44
|
+
title={TITLE[state]}
|
|
45
|
+
className={cn("inline-block h-2 w-2 shrink-0 rounded-full", DOT[state])}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ModelProvider,
|
|
12
12
|
} from "@/lib/models/providers";
|
|
13
13
|
import { SubagentModels } from "./SubagentModels";
|
|
14
|
+
import { INTERNAL_MODEL_ROLES } from "@/lib/models/agent-roles";
|
|
14
15
|
import { MODEL_ROLES, ROLE_LABELS } from "@/lib/models/roles";
|
|
15
16
|
import { ModelSelect } from "./ModelSelect";
|
|
16
17
|
import { ProviderCard } from "./ProviderCard";
|
|
@@ -58,11 +59,12 @@ export function ModelsSection() {
|
|
|
58
59
|
<div className="space-y-6">
|
|
59
60
|
<section className="space-y-2">
|
|
60
61
|
<p className="text-muted-foreground text-sm">
|
|
61
|
-
Which model each kind of work runs on.
|
|
62
|
+
Which model each kind of work runs on. Developer, Analyst, Reviewer
|
|
63
|
+
and Supervisor are in Agent roles. A workspace can set its own
|
|
62
64
|
(Settings › Workspaces), and a task its own when it starts or resumes.
|
|
63
65
|
</p>
|
|
64
66
|
<ul className="space-y-2">
|
|
65
|
-
{MODEL_ROLES.map((role) => (
|
|
67
|
+
{INTERNAL_MODEL_ROLES(MODEL_ROLES).map((role) => (
|
|
66
68
|
<li
|
|
67
69
|
key={role}
|
|
68
70
|
className="bg-muted/40 space-y-1.5 rounded-lg px-3 py-2.5"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectGroup,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectLabel,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "@/components/ui/select";
|
|
12
|
+
import { useModelChoices } from "@/data/models";
|
|
13
|
+
import type { ModelChoice } from "@/lib/models/catalog";
|
|
14
|
+
import { AvailabilityDot, LocalityBadge } from "./ModelBadges";
|
|
15
|
+
|
|
16
|
+
const INHERIT = "__inherit";
|
|
17
|
+
|
|
18
|
+
function Row({ choice }: { choice: ModelChoice }) {
|
|
19
|
+
return (
|
|
20
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
21
|
+
<AvailabilityDot availability={choice.availability} />
|
|
22
|
+
<span className="truncate">{choice.label}</span>
|
|
23
|
+
<LocalityBadge locality={choice.locality} />
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A model picker over the server's list: grouped by provider, each model
|
|
29
|
+
// labelled Local or Cloud with whether its server answers. `claudeOnly`
|
|
30
|
+
// leaves out the models that run through Pi.
|
|
31
|
+
export function RoleModelSelect({
|
|
32
|
+
id,
|
|
33
|
+
value,
|
|
34
|
+
inheritLabel,
|
|
35
|
+
claudeOnly,
|
|
36
|
+
gateOnly,
|
|
37
|
+
disabled,
|
|
38
|
+
onChange,
|
|
39
|
+
}: {
|
|
40
|
+
id?: string;
|
|
41
|
+
value: string | null;
|
|
42
|
+
inheritLabel: string;
|
|
43
|
+
claudeOnly?: boolean;
|
|
44
|
+
// Only models that can serve a merge gate are selectable.
|
|
45
|
+
gateOnly?: boolean;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
onChange: (ref: string | null) => void;
|
|
48
|
+
}) {
|
|
49
|
+
const { data } = useModelChoices();
|
|
50
|
+
const choices = (data ?? []).filter((c) => !claudeOnly || c.agent !== "pi");
|
|
51
|
+
const groups = [...new Set(choices.map((c) => c.group))];
|
|
52
|
+
const current = value ? choices.find((c) => c.ref === value) : undefined;
|
|
53
|
+
return (
|
|
54
|
+
<Select
|
|
55
|
+
value={value ?? INHERIT}
|
|
56
|
+
onValueChange={(v) => onChange(v === INHERIT ? null : v)}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
>
|
|
59
|
+
<SelectTrigger id={id} className="h-11 md:h-9">
|
|
60
|
+
<SelectValue>
|
|
61
|
+
{current ? <Row choice={current} /> : (value ?? inheritLabel)}
|
|
62
|
+
</SelectValue>
|
|
63
|
+
</SelectTrigger>
|
|
64
|
+
<SelectContent>
|
|
65
|
+
<SelectItem value={INHERIT}>{inheritLabel}</SelectItem>
|
|
66
|
+
{value && !current && <SelectItem value={value}>{value}</SelectItem>}
|
|
67
|
+
{groups.map((g) => (
|
|
68
|
+
<SelectGroup key={g}>
|
|
69
|
+
<SelectLabel>{g}</SelectLabel>
|
|
70
|
+
{choices
|
|
71
|
+
.filter((c) => c.group === g)
|
|
72
|
+
.map((c) => (
|
|
73
|
+
<SelectItem
|
|
74
|
+
key={c.ref}
|
|
75
|
+
value={c.ref}
|
|
76
|
+
disabled={gateOnly && !c.gateCapable}
|
|
77
|
+
className="min-h-11 md:min-h-0"
|
|
78
|
+
>
|
|
79
|
+
<Row choice={c} />
|
|
80
|
+
</SelectItem>
|
|
81
|
+
))}
|
|
82
|
+
</SelectGroup>
|
|
83
|
+
))}
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { Session } from "@/lib/db";
|
|
|
15
15
|
import { PaneViewToggle } from "./PaneViewToggle";
|
|
16
16
|
import { PaneMenu } from "./PaneMenu";
|
|
17
17
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
18
|
+
import { RolePill } from "@/components/Sessions/RolePill";
|
|
18
19
|
import { usePaneBarSlots } from "./PaneBarSlots";
|
|
19
20
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
20
21
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -161,6 +162,7 @@ export function DesktopTabBar({
|
|
|
161
162
|
</div>
|
|
162
163
|
|
|
163
164
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
165
|
+
<RolePill session={session} paneId={paneId} />
|
|
164
166
|
<HostBadge hostId={session?.host_id} />
|
|
165
167
|
<ContextMeter sessionId={session?.id} />
|
|
166
168
|
<SessionPrStage sessionId={session?.id} />
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
Search,
|
|
25
25
|
Star,
|
|
26
26
|
Share2,
|
|
27
|
+
Download,
|
|
27
28
|
TextSearch,
|
|
28
29
|
} from "lucide-react";
|
|
29
30
|
import { cn } from "@/lib/utils";
|
|
@@ -31,8 +32,10 @@ import { useAwayQuery } from "@/data/away";
|
|
|
31
32
|
import { awayUntil } from "@/components/Away/time";
|
|
32
33
|
import type { Session, Project } from "@/lib/db";
|
|
33
34
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
35
|
+
import { RolePill } from "@/components/Sessions/RolePill";
|
|
34
36
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
35
37
|
import { shareUiActions } from "@/stores/shareUi";
|
|
38
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
36
39
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
37
40
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
38
41
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -219,6 +222,14 @@ export function MobileTabBar({
|
|
|
219
222
|
<Share2 className="h-4 w-4" />
|
|
220
223
|
Share…
|
|
221
224
|
</DropdownMenuItem>
|
|
225
|
+
<DropdownMenuItem
|
|
226
|
+
onSelect={() =>
|
|
227
|
+
exportUiActions.open(session.id, session.name)
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<Download className="h-4 w-4" />
|
|
231
|
+
Export for AI…
|
|
232
|
+
</DropdownMenuItem>
|
|
222
233
|
<DropdownMenuSeparator />
|
|
223
234
|
</>
|
|
224
235
|
)}
|
|
@@ -270,6 +281,7 @@ export function MobileTabBar({
|
|
|
270
281
|
</button>
|
|
271
282
|
</div>
|
|
272
283
|
|
|
284
|
+
<RolePill session={session} compact />
|
|
273
285
|
{bookmarksSessionId && (
|
|
274
286
|
<ChatBookmarksButton
|
|
275
287
|
sessionId={bookmarksSessionId}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Link,
|
|
9
9
|
MoreHorizontal,
|
|
10
10
|
Share2,
|
|
11
|
+
Download,
|
|
11
12
|
SplitSquareHorizontal,
|
|
12
13
|
SplitSquareVertical,
|
|
13
14
|
Star,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
import { copySessionLink, writeClipboard } from "@/hooks/useCopyToClipboard";
|
|
28
29
|
import type { Session } from "@/lib/db";
|
|
29
30
|
import { shareUiActions } from "@/stores/shareUi";
|
|
31
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
30
32
|
import { useSessionBookmarks } from "@/data/bookmarks";
|
|
31
33
|
import { useDocsWorkspace } from "@/data/lumifyhub/docs";
|
|
32
34
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
@@ -117,6 +119,14 @@ export function PaneMenu({
|
|
|
117
119
|
Share…
|
|
118
120
|
</DropdownMenuItem>
|
|
119
121
|
)}
|
|
122
|
+
{session.view === "chat" && (
|
|
123
|
+
<DropdownMenuItem
|
|
124
|
+
onSelect={() => exportUiActions.open(session.id, session.name)}
|
|
125
|
+
>
|
|
126
|
+
<Download className="h-4 w-4" />
|
|
127
|
+
Export for AI…
|
|
128
|
+
</DropdownMenuItem>
|
|
129
|
+
)}
|
|
120
130
|
<DropdownMenuSeparator />
|
|
121
131
|
<MoveMenuItems
|
|
122
132
|
session={session}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ChevronDown, Folder, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import type { GroupShelf } from "@/lib/sidebar/shelves";
|
|
6
|
+
import {
|
|
7
|
+
useDeleteGroup,
|
|
8
|
+
useReorderGroups,
|
|
9
|
+
useUpdateGroup,
|
|
10
|
+
} from "@/data/session-groups";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuSeparator,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
} from "@/components/ui/dropdown-menu";
|
|
19
|
+
import { cn } from "@/lib/utils";
|
|
20
|
+
import { SessionRow } from "./SessionRow";
|
|
21
|
+
|
|
22
|
+
function GroupHeader({
|
|
23
|
+
shelf,
|
|
24
|
+
siblings,
|
|
25
|
+
workspaceName,
|
|
26
|
+
}: {
|
|
27
|
+
shelf: GroupShelf;
|
|
28
|
+
siblings: GroupShelf[];
|
|
29
|
+
workspaceName?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const { group, rows, rollup } = shelf;
|
|
32
|
+
const update = useUpdateGroup();
|
|
33
|
+
const reorder = useReorderGroups();
|
|
34
|
+
const remove = useDeleteGroup();
|
|
35
|
+
const [renaming, setRenaming] = useState(false);
|
|
36
|
+
const [name, setName] = useState(group.name);
|
|
37
|
+
const at = siblings.findIndex((s) => s.group.id === group.id);
|
|
38
|
+
const move = (by: -1 | 1) => {
|
|
39
|
+
const ids = siblings.map((s) => s.group.id);
|
|
40
|
+
ids.splice(at + by, 0, ids.splice(at, 1)[0]);
|
|
41
|
+
reorder.mutate({ workspaceId: group.workspace_id, ids });
|
|
42
|
+
};
|
|
43
|
+
const commit = () => {
|
|
44
|
+
const next = name.trim();
|
|
45
|
+
setRenaming(false);
|
|
46
|
+
if (next && next !== group.name)
|
|
47
|
+
update.mutate({ id: group.id, name: next });
|
|
48
|
+
else setName(group.name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (renaming)
|
|
52
|
+
return (
|
|
53
|
+
<div className="px-2.5 pt-3.5 pb-1.5">
|
|
54
|
+
<Input
|
|
55
|
+
autoFocus
|
|
56
|
+
value={name}
|
|
57
|
+
maxLength={60}
|
|
58
|
+
aria-label="Group name"
|
|
59
|
+
className="h-11 md:h-8"
|
|
60
|
+
onChange={(e) => setName(e.target.value)}
|
|
61
|
+
onBlur={commit}
|
|
62
|
+
onKeyDown={(e) => {
|
|
63
|
+
if (e.key === "Enter") commit();
|
|
64
|
+
if (e.key === "Escape") {
|
|
65
|
+
setName(group.name);
|
|
66
|
+
setRenaming(false);
|
|
67
|
+
}
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="text-muted-foreground/70 group/header flex items-center pr-1 pl-2.5">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() =>
|
|
78
|
+
update.mutate({ id: group.id, collapsed: !group.collapsed })
|
|
79
|
+
}
|
|
80
|
+
aria-expanded={!group.collapsed}
|
|
81
|
+
className="hover:text-foreground flex min-h-11 min-w-0 flex-1 items-center gap-1 pt-2 text-left text-[11px] font-medium tracking-[0.08em] uppercase md:min-h-8"
|
|
82
|
+
>
|
|
83
|
+
<Folder aria-hidden className="h-3 w-3 shrink-0" />
|
|
84
|
+
<span className="truncate">{group.name}</span>
|
|
85
|
+
<span className="shrink-0">· {rows.length}</span>
|
|
86
|
+
{rollup.needsYou > 0 && (
|
|
87
|
+
<span className="shrink-0 text-amber-600 normal-case dark:text-amber-400">
|
|
88
|
+
· {rollup.needsYou} needs you
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
{workspaceName && (
|
|
92
|
+
<span className="bg-muted text-muted-foreground ml-1 truncate rounded px-1.5 py-0.5 text-[10px] normal-case">
|
|
93
|
+
{workspaceName}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
<ChevronDown
|
|
97
|
+
aria-hidden
|
|
98
|
+
className={cn(
|
|
99
|
+
"h-3 w-3 shrink-0 transition-transform",
|
|
100
|
+
group.collapsed && "-rotate-90"
|
|
101
|
+
)}
|
|
102
|
+
/>
|
|
103
|
+
</button>
|
|
104
|
+
<DropdownMenu>
|
|
105
|
+
<DropdownMenuTrigger asChild>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
aria-label={`${group.name} options`}
|
|
109
|
+
className="hover:bg-muted flex h-11 w-11 shrink-0 items-center justify-center rounded-md md:h-8 md:w-8 md:opacity-0 md:group-hover/header:opacity-100 md:focus-visible:opacity-100 md:data-[state=open]:opacity-100 [@media(hover:none)]:md:h-11 [@media(hover:none)]:md:w-11 [@media(hover:none)]:md:opacity-100"
|
|
110
|
+
>
|
|
111
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
112
|
+
</button>
|
|
113
|
+
</DropdownMenuTrigger>
|
|
114
|
+
<DropdownMenuContent align="end" className="w-44">
|
|
115
|
+
<DropdownMenuItem
|
|
116
|
+
className="min-h-11 md:min-h-8"
|
|
117
|
+
onSelect={() => setRenaming(true)}
|
|
118
|
+
>
|
|
119
|
+
Rename
|
|
120
|
+
</DropdownMenuItem>
|
|
121
|
+
<DropdownMenuItem
|
|
122
|
+
className="min-h-11 md:min-h-8"
|
|
123
|
+
disabled={at <= 0}
|
|
124
|
+
onSelect={() => move(-1)}
|
|
125
|
+
>
|
|
126
|
+
Move up
|
|
127
|
+
</DropdownMenuItem>
|
|
128
|
+
<DropdownMenuItem
|
|
129
|
+
className="min-h-11 md:min-h-8"
|
|
130
|
+
disabled={at < 0 || at >= siblings.length - 1}
|
|
131
|
+
onSelect={() => move(1)}
|
|
132
|
+
>
|
|
133
|
+
Move down
|
|
134
|
+
</DropdownMenuItem>
|
|
135
|
+
<DropdownMenuItem
|
|
136
|
+
className="min-h-11 md:min-h-8"
|
|
137
|
+
onSelect={() =>
|
|
138
|
+
update.mutate({ id: group.id, collapsed: !group.collapsed })
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
{group.collapsed ? "Unfold" : "Fold"}
|
|
142
|
+
</DropdownMenuItem>
|
|
143
|
+
<DropdownMenuSeparator />
|
|
144
|
+
<DropdownMenuItem
|
|
145
|
+
variant="destructive"
|
|
146
|
+
className="min-h-11 md:min-h-8"
|
|
147
|
+
onSelect={() => remove.mutate({ id: group.id, name: group.name })}
|
|
148
|
+
>
|
|
149
|
+
Delete group
|
|
150
|
+
</DropdownMenuItem>
|
|
151
|
+
</DropdownMenuContent>
|
|
152
|
+
</DropdownMenu>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The user's groups, in order, between Pinned and the status shelves. A
|
|
158
|
+
// folded group keeps its header and counts; an auto-settled member is muted.
|
|
159
|
+
export function GroupShelves({
|
|
160
|
+
groups,
|
|
161
|
+
workspaceNames,
|
|
162
|
+
}: {
|
|
163
|
+
groups: GroupShelf[];
|
|
164
|
+
// Set when more than one workspace's groups show, to tag each header.
|
|
165
|
+
workspaceNames?: Map<string, string>;
|
|
166
|
+
}) {
|
|
167
|
+
return groups.map((shelf) => {
|
|
168
|
+
const { group } = shelf;
|
|
169
|
+
const siblings = groups.filter(
|
|
170
|
+
(g) => g.group.workspace_id === group.workspace_id
|
|
171
|
+
);
|
|
172
|
+
const empty = !group.collapsed && shelf.rows.length === 0;
|
|
173
|
+
return (
|
|
174
|
+
<section key={group.id} aria-label={group.name}>
|
|
175
|
+
<GroupHeader
|
|
176
|
+
shelf={shelf}
|
|
177
|
+
siblings={siblings}
|
|
178
|
+
workspaceName={
|
|
179
|
+
workspaceNames && group.workspace_id
|
|
180
|
+
? workspaceNames.get(group.workspace_id)
|
|
181
|
+
: undefined
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
{empty && (
|
|
185
|
+
<p className="text-muted-foreground/70 px-2.5 pb-2 text-xs">
|
|
186
|
+
Nothing here yet · move a session in from its ⋯ menu
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
{(group.collapsed ? [] : shelf.rows).map((r) => (
|
|
190
|
+
<SessionRow key={r.session.id} row={r} muted={r.settled === "auto"} />
|
|
191
|
+
))}
|
|
192
|
+
</section>
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -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
|