@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.
Files changed (306) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/groups/route.ts +44 -0
  11. package/app/api/bus/spawn/route.ts +34 -2
  12. package/app/api/claims/[id]/route.ts +20 -0
  13. package/app/api/claims/check/route.ts +23 -0
  14. package/app/api/claims/check.test.ts +113 -0
  15. package/app/api/claims/exchange/route.ts +42 -0
  16. package/app/api/claims/outside.test.ts +90 -0
  17. package/app/api/claims/route.test.ts +193 -0
  18. package/app/api/claims/route.ts +73 -0
  19. package/app/api/hosts/[id]/route.ts +9 -1
  20. package/app/api/models/availability/route.ts +9 -0
  21. package/app/api/models/choices/route.ts +9 -0
  22. package/app/api/peer/unlink/route.ts +17 -0
  23. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  24. package/app/api/peers/[hostId]/route.ts +20 -0
  25. package/app/api/peers/gate.test.ts +64 -1
  26. package/app/api/peers/pending/route.test.ts +37 -0
  27. package/app/api/peers/pending/route.ts +11 -4
  28. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  29. package/app/api/session-groups/[id]/route.ts +25 -0
  30. package/app/api/session-groups/order/route.ts +15 -0
  31. package/app/api/session-groups/route.ts +21 -0
  32. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  33. package/app/api/sessions/[id]/crew/route.ts +38 -0
  34. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  35. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  36. package/app/api/sessions/[id]/export/route.ts +34 -0
  37. package/app/api/sessions/[id]/fork/route.ts +3 -0
  38. package/app/api/sessions/[id]/group/route.ts +18 -0
  39. package/app/api/sessions/[id]/model/route.ts +49 -0
  40. package/app/api/sessions/[id]/route.ts +14 -4
  41. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  42. package/app/api/sessions/claims.test.ts +122 -0
  43. package/app/api/sessions/group/route.ts +17 -0
  44. package/app/api/sessions/route.ts +31 -14
  45. package/app/page.tsx +10 -0
  46. package/components/Chat/Messages.tsx +104 -10
  47. package/components/Claims/ClaimFiles.tsx +38 -0
  48. package/components/Claims/ClaimRow.tsx +82 -0
  49. package/components/Claims/ClaimsSheet.tsx +176 -0
  50. package/components/Claims/OverlapPill.tsx +31 -0
  51. package/components/Claims/StartClaimsNotice.tsx +70 -0
  52. package/components/Claims/claims-ui.test.ts +161 -0
  53. package/components/Claims/format.ts +57 -0
  54. package/components/Claims/index.ts +3 -0
  55. package/components/Draft/DraftChips.tsx +6 -1
  56. package/components/Draft/DraftPanel.tsx +90 -3
  57. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  58. package/components/Draft/RoleChips.tsx +110 -0
  59. package/components/Export/ExportActions.tsx +136 -0
  60. package/components/Export/ExportSheet.tsx +217 -0
  61. package/components/Export/RangePicker.tsx +144 -0
  62. package/components/Export/Seg.tsx +38 -0
  63. package/components/Export/TurnSelectBar.tsx +45 -0
  64. package/components/Hosts/AccountMachines.tsx +32 -18
  65. package/components/Hosts/HostRow.tsx +155 -14
  66. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  67. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  68. package/components/Models/AgentRoleCard.tsx +114 -0
  69. package/components/Models/AgentRolesSection.tsx +132 -0
  70. package/components/Models/ModelBadges.tsx +48 -0
  71. package/components/Models/ModelsSection.tsx +4 -2
  72. package/components/Models/RoleModelSelect.tsx +87 -0
  73. package/components/Pane/DesktopTabBar.tsx +2 -0
  74. package/components/Pane/MobileTabBar.tsx +12 -0
  75. package/components/Pane/PaneMenu.tsx +10 -0
  76. package/components/SessionList/GroupShelves.tsx +195 -0
  77. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  78. package/components/SessionList/RowSubtitle.tsx +7 -1
  79. package/components/SessionList/SelectionToolbar.tsx +52 -1
  80. package/components/SessionList/SessionRow.tsx +19 -7
  81. package/components/SessionList/SessionRowMenu.tsx +23 -0
  82. package/components/SessionList/SessionShelves.tsx +5 -0
  83. package/components/SessionList/SidebarBody.tsx +6 -0
  84. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  85. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  86. package/components/SessionList/SidebarHeader.tsx +5 -0
  87. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  88. package/components/SessionList/index.tsx +18 -14
  89. package/components/SessionList/useSidebarData.ts +10 -0
  90. package/components/Sessions/RolePill.tsx +196 -0
  91. package/components/Settings/SettingsDialog.tsx +9 -0
  92. package/components/Share/ShareDialog.tsx +13 -0
  93. package/components/ui/notice.tsx +46 -0
  94. package/components/ui/status-pill.tsx +39 -0
  95. package/data/analyst/index.ts +3 -0
  96. package/data/analyst/keys.ts +6 -0
  97. package/data/analyst/mutations.ts +116 -0
  98. package/data/analyst/queries.ts +35 -0
  99. package/data/claims/index.ts +2 -0
  100. package/data/claims/keys.ts +5 -0
  101. package/data/claims/queries.ts +47 -0
  102. package/data/exports/index.ts +2 -0
  103. package/data/exports/keys.ts +3 -0
  104. package/data/exports/queries.ts +51 -0
  105. package/data/models/index.ts +98 -0
  106. package/data/peers/keys.ts +1 -0
  107. package/data/peers/queries.ts +31 -0
  108. package/data/push/topics.ts +9 -0
  109. package/data/session-groups/index.ts +156 -0
  110. package/data/sessions/index.ts +0 -1
  111. package/data/sessions/pin.ts +2 -6
  112. package/data/sessions/queries.ts +36 -29
  113. package/data/sessions/shelf.ts +2 -6
  114. package/data/tasks/index.ts +2 -0
  115. package/hooks/useSessions.ts +0 -12
  116. package/lib/__fixtures__/peer-rpc.ts +65 -33
  117. package/lib/agents/brief.ts +1 -0
  118. package/lib/agents/spawn.ts +4 -0
  119. package/lib/analyst/__fixtures__/items.ts +57 -0
  120. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  121. package/lib/analyst/brief.ts +17 -0
  122. package/lib/analyst/buildDigest.ts +204 -0
  123. package/lib/analyst/createLink.ts +98 -0
  124. package/lib/analyst/endLink.ts +31 -0
  125. package/lib/analyst/forward.ts +109 -0
  126. package/lib/analyst/index.ts +16 -0
  127. package/lib/analyst/linkView.ts +38 -0
  128. package/lib/analyst/onTurnEnd.ts +119 -0
  129. package/lib/analyst/parseReview.ts +41 -0
  130. package/lib/analyst/pushDigest.ts +143 -0
  131. package/lib/analyst/resume.ts +35 -0
  132. package/lib/analyst/route.ts +50 -0
  133. package/lib/analyst/signals.ts +121 -0
  134. package/lib/analyst/store.ts +303 -0
  135. package/lib/analyst/types.ts +140 -0
  136. package/lib/bus/condense.ts +5 -1
  137. package/lib/bus/index.ts +13 -0
  138. package/lib/bus/read-scope.ts +12 -0
  139. package/lib/bus/role-alias.ts +70 -0
  140. package/lib/bus/targets.ts +3 -0
  141. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  142. package/lib/chat/drivers/pi-approvals.ts +7 -4
  143. package/lib/chat/drivers/pi-mapper.ts +38 -0
  144. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  145. package/lib/chat/drivers/pi.ts +8 -3
  146. package/lib/chat/drivers/turn-items.ts +36 -11
  147. package/lib/chat/drivers/turn-outcome.ts +108 -0
  148. package/lib/chat/events.ts +8 -0
  149. package/lib/chat/registry.ts +13 -0
  150. package/lib/chat/store.ts +19 -0
  151. package/lib/chat/turns.ts +1 -0
  152. package/lib/chat/worker/extras.ts +4 -2
  153. package/lib/claims/collectLocal.ts +123 -0
  154. package/lib/claims/describe.ts +41 -0
  155. package/lib/claims/exchange.ts +91 -0
  156. package/lib/claims/goal.ts +45 -0
  157. package/lib/claims/index.ts +10 -0
  158. package/lib/claims/loop.ts +51 -0
  159. package/lib/claims/machine.ts +21 -0
  160. package/lib/claims/mine.ts +43 -0
  161. package/lib/claims/notifyOverlaps.ts +101 -0
  162. package/lib/claims/onMerged.ts +85 -0
  163. package/lib/claims/outside.ts +36 -0
  164. package/lib/claims/overlap.ts +87 -0
  165. package/lib/claims/overlapCache.ts +86 -0
  166. package/lib/claims/overlapText.ts +36 -0
  167. package/lib/claims/peers.ts +56 -0
  168. package/lib/claims/startCheck.ts +100 -0
  169. package/lib/claims/startWarning.ts +29 -0
  170. package/lib/claims/store.ts +366 -0
  171. package/lib/claims/types.ts +102 -0
  172. package/lib/claims/view.ts +80 -0
  173. package/lib/claims/yield.ts +24 -0
  174. package/lib/db/changes.ts +12 -0
  175. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  176. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  177. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  178. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  179. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  180. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  181. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  182. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  183. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  184. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  185. package/lib/db/queries.ts +0 -40
  186. package/lib/db/session-parts/columns.ts +3 -0
  187. package/lib/db/types.ts +19 -8
  188. package/lib/doctor/checks.ts +64 -1
  189. package/lib/done/archive.ts +2 -0
  190. package/lib/drafts.ts +4 -0
  191. package/lib/export/build.ts +470 -0
  192. package/lib/export/compact.ts +156 -0
  193. package/lib/export/estimate.ts +83 -0
  194. package/lib/export/fixtures.ts +45 -0
  195. package/lib/export/html.tsx +195 -0
  196. package/lib/export/json.ts +39 -0
  197. package/lib/export/load.ts +64 -0
  198. package/lib/export/markdown.ts +127 -0
  199. package/lib/export/request.ts +36 -0
  200. package/lib/export/service.ts +142 -0
  201. package/lib/export/summary.ts +86 -0
  202. package/lib/export/turns.ts +63 -0
  203. package/lib/export/view.ts +173 -0
  204. package/lib/git-remote.ts +43 -0
  205. package/lib/hosts/index.ts +10 -1
  206. package/lib/models/agent-binding.ts +102 -0
  207. package/lib/models/agent-roles-view.ts +157 -0
  208. package/lib/models/agent-roles.ts +87 -0
  209. package/lib/models/availability-refs.ts +17 -0
  210. package/lib/models/availability.ts +91 -0
  211. package/lib/models/catalog.ts +156 -0
  212. package/lib/models/config.ts +27 -0
  213. package/lib/models/locality.ts +33 -0
  214. package/lib/models/providers.ts +11 -0
  215. package/lib/models/resolve.ts +50 -12
  216. package/lib/models/roles.ts +12 -0
  217. package/lib/needs-you.ts +1 -1
  218. package/lib/orchestration.ts +2 -0
  219. package/lib/orchestrator/act.ts +54 -4
  220. package/lib/orchestrator/brief.ts +2 -1
  221. package/lib/orchestrator/claim-wait.ts +71 -0
  222. package/lib/orchestrator/conditions.ts +28 -1
  223. package/lib/orchestrator/describe.ts +6 -1
  224. package/lib/orchestrator/facts.ts +12 -2
  225. package/lib/orchestrator/read.ts +5 -1
  226. package/lib/orchestrator/serve.ts +37 -2
  227. package/lib/orchestrator/tool-names.ts +1 -0
  228. package/lib/orchestrator/tool-schemas.ts +31 -1
  229. package/lib/orchestrator/tools.ts +3 -1
  230. package/lib/orchestrator/untrusted.ts +5 -0
  231. package/lib/orchestrator/watcher.ts +19 -1
  232. package/lib/peer/account.ts +74 -2
  233. package/lib/peer/allowlist.ts +1 -1
  234. package/lib/peer/client.ts +26 -1
  235. package/lib/peer/link-cli.ts +19 -4
  236. package/lib/peer/links.ts +117 -5
  237. package/lib/peer/machines.ts +14 -2
  238. package/lib/peer/name.ts +32 -0
  239. package/lib/peer/pairing.ts +34 -5
  240. package/lib/peer/pin.ts +10 -3
  241. package/lib/peer/reasons.ts +7 -0
  242. package/lib/peer/revoke.ts +75 -0
  243. package/lib/peer/rpc-server.ts +53 -5
  244. package/lib/peer/unlink.ts +62 -0
  245. package/lib/peer/unlinked-proof.ts +44 -0
  246. package/lib/projects.ts +6 -2
  247. package/lib/security/demo.ts +7 -0
  248. package/lib/session-artifacts/agentos-files.ts +94 -0
  249. package/lib/session-artifacts/collect.ts +17 -0
  250. package/lib/session-groups/assignSessions.ts +81 -0
  251. package/lib/session-groups/cli.ts +61 -0
  252. package/lib/session-groups/createGroup.ts +28 -0
  253. package/lib/session-groups/index.ts +21 -0
  254. package/lib/session-groups/removeGroup.ts +36 -0
  255. package/lib/session-groups/reorderGroups.ts +31 -0
  256. package/lib/session-groups/resolveGroup.ts +19 -0
  257. package/lib/session-groups/route.ts +32 -0
  258. package/lib/session-groups/store.ts +85 -0
  259. package/lib/session-groups/types.ts +25 -0
  260. package/lib/session-groups/updateGroup.ts +35 -0
  261. package/lib/session-groups/workspace.ts +21 -0
  262. package/lib/sessions/crew.ts +72 -0
  263. package/lib/sessions/launch.ts +54 -11
  264. package/lib/settings-url.ts +1 -0
  265. package/lib/share/markdown.ts +2 -2
  266. package/lib/share/redact.ts +16 -10
  267. package/lib/share/render.tsx +1 -1
  268. package/lib/share/snapshot.ts +2 -2
  269. package/lib/sidebar/rollup.ts +1 -0
  270. package/lib/sidebar/row-menu.ts +8 -1
  271. package/lib/sidebar/shelves.ts +90 -15
  272. package/lib/sidebar/visible-order.ts +5 -0
  273. package/lib/tasks/hold-checks.ts +3 -1
  274. package/lib/tasks/holds.ts +3 -0
  275. package/lib/tasks/index.ts +11 -0
  276. package/lib/tasks/queue-rows.ts +8 -1
  277. package/lib/tasks/queue-start.ts +1 -0
  278. package/lib/tasks/queue-types.ts +9 -1
  279. package/lib/tasks/queue.ts +15 -5
  280. package/lib/tasks/schedule-types.ts +1 -0
  281. package/lib/test-temp.ts +19 -3
  282. package/lib/workspaces.ts +11 -3
  283. package/lib/worktree-deps/caches.ts +3 -2
  284. package/lib/worktree-deps/clone.ts +3 -1
  285. package/lib/worktree-deps/paths.ts +26 -0
  286. package/lib/worktree-sweep/testTemp.ts +23 -106
  287. package/package.json +1 -1
  288. package/scripts/agent-os +1 -1
  289. package/scripts/autodeploy +2 -0
  290. package/scripts/autodeploy.test.ts +31 -1
  291. package/scripts/claim-hook.sh +31 -0
  292. package/scripts/claim-hook.test.ts +230 -0
  293. package/scripts/lib/cli-links.sh +36 -0
  294. package/scripts/lib/commands.sh +3 -0
  295. package/scripts/lib/machine.sh +27 -2
  296. package/scripts/lib/machine.test.ts +25 -3
  297. package/scripts/sync-cli-links +7 -0
  298. package/scripts/sync-cli-links.test.ts +69 -0
  299. package/server.ts +12 -1
  300. package/stores/claimsUi.ts +22 -0
  301. package/stores/exportUi.ts +87 -0
  302. package/app/api/groups/[...path]/route.ts +0 -136
  303. package/app/api/groups/route.ts +0 -93
  304. package/data/groups/index.ts +0 -1
  305. package/data/groups/mutations.ts +0 -95
  306. 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. A workspace can set its own
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