@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,136 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Check, Copy, Download, Loader2, Share2 } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+ import { writeClipboard } from "@/hooks/useCopyToClipboard";
8
+ import { fetchExport, type ExportEstimate } from "@/data/exports";
9
+ import type { ExportRequest } from "@/lib/export/view";
10
+
11
+ type Busy = "copy" | "download" | "share" | null;
12
+
13
+ function save(blob: Blob, name: string) {
14
+ const url = URL.createObjectURL(blob);
15
+ const a = document.createElement("a");
16
+ a.href = url;
17
+ a.download = name;
18
+ a.click();
19
+ URL.revokeObjectURL(url);
20
+ }
21
+
22
+ // Copy for agent, Download and (where the browser can) Share. Never blocked
23
+ // by a sensitive-data warning, only by the size limit.
24
+ export function ExportActions({
25
+ sessionId,
26
+ title,
27
+ req,
28
+ est,
29
+ }: {
30
+ sessionId: string;
31
+ title: string;
32
+ req: ExportRequest;
33
+ est: ExportEstimate | null;
34
+ }) {
35
+ const [busy, setBusy] = useState<Busy>(null);
36
+ const [copied, setCopied] = useState(false);
37
+ // Only rendered on the client (the sheet loads with ssr off).
38
+ const [canShare] = useState(() => {
39
+ try {
40
+ const probe = new File([""], "x.md", { type: "text/markdown" });
41
+ return !!navigator.canShare?.({ files: [probe] });
42
+ } catch {
43
+ return false;
44
+ }
45
+ });
46
+
47
+ const wire = JSON.parse(JSON.stringify(req)) as ExportRequest;
48
+ const file = () => fetchExport(sessionId, wire, title, est?.selected ?? null);
49
+ const disabled = !est || est.tooBig || busy !== null;
50
+
51
+ const run =
52
+ (kind: Exclude<Busy, null>, work: () => Promise<void>) => async () => {
53
+ setBusy(kind);
54
+ try {
55
+ await work();
56
+ } catch (err) {
57
+ if ((err as Error).name !== "AbortError")
58
+ toast.error((err as Error).message || "Export failed");
59
+ } finally {
60
+ setBusy(null);
61
+ }
62
+ };
63
+
64
+ const copy = run("copy", async () => {
65
+ const pending = file();
66
+ // Safari only allows a copy started inside the tap: hand it the promise.
67
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
68
+ await navigator.clipboard.write([
69
+ new ClipboardItem({
70
+ "text/plain": pending.then(
71
+ (f) => new Blob([f.blob], { type: "text/plain" })
72
+ ),
73
+ }),
74
+ ]);
75
+ } else {
76
+ await writeClipboard(await (await pending).blob.text());
77
+ }
78
+ setCopied(true);
79
+ setTimeout(() => setCopied(false), 1500);
80
+ toast.success("Copied. Paste it into your AI chat.");
81
+ });
82
+
83
+ const download = run("download", async () => {
84
+ const f = await file();
85
+ save(f.blob, f.name);
86
+ });
87
+
88
+ const share = run("share", async () => {
89
+ const f = await file();
90
+ const shared = new File([f.blob], f.name, { type: f.blob.type });
91
+ if (!navigator.canShare?.({ files: [shared] })) return save(f.blob, f.name);
92
+ await navigator.share({ files: [shared], title });
93
+ });
94
+
95
+ const Spin = <Loader2 className="h-4 w-4 animate-spin" />;
96
+ return (
97
+ <div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
98
+ <Button
99
+ className="min-h-11 sm:order-last"
100
+ disabled={disabled}
101
+ onClick={copy}
102
+ >
103
+ {busy === "copy" ? (
104
+ Spin
105
+ ) : copied ? (
106
+ <Check className="h-4 w-4" />
107
+ ) : (
108
+ <Copy className="h-4 w-4" />
109
+ )}
110
+ {req.format === "md" ? "Copy for agent" : "Copy"}
111
+ </Button>
112
+ <div className="flex gap-2">
113
+ <Button
114
+ variant="outline"
115
+ className="min-h-11 flex-1 sm:flex-none"
116
+ disabled={disabled}
117
+ onClick={download}
118
+ >
119
+ {busy === "download" ? Spin : <Download className="h-4 w-4" />}
120
+ Download
121
+ </Button>
122
+ {canShare && (
123
+ <Button
124
+ variant="outline"
125
+ className="min-h-11 flex-1 sm:flex-none"
126
+ disabled={disabled}
127
+ onClick={share}
128
+ >
129
+ {busy === "share" ? Spin : <Share2 className="h-4 w-4" />}
130
+ Share…
131
+ </Button>
132
+ )}
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { useSnapshot } from "valtio";
5
+ import { AlertTriangle, Info } from "lucide-react";
6
+ import { Switch } from "@/components/ui/switch";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { useExportEstimate, type ExportEstimate } from "@/data/exports";
14
+ import { exportUi, exportUiActions } from "@/stores/exportUi";
15
+ import {
16
+ COMPACT_RULES,
17
+ EXPORT_FORMATS,
18
+ formatBytes,
19
+ redactionStatus,
20
+ tokenLabel,
21
+ type ExportFormat,
22
+ type ExportMode,
23
+ type ExportRequest,
24
+ } from "@/lib/export/view";
25
+ import { cn } from "@/lib/utils";
26
+ import { ExportActions } from "./ExportActions";
27
+ import { RangePicker } from "./RangePicker";
28
+ import { Seg } from "./Seg";
29
+ import { TurnSelectBar } from "./TurnSelectBar";
30
+
31
+ // Opened from a menu or a message: what to export (format, detail, turns,
32
+ // masking), what it comes to, and where it goes (copy, download, share).
33
+ export function ExportSheet() {
34
+ const ui = useSnapshot(exportUi);
35
+ return (
36
+ <>
37
+ <Dialog
38
+ open={ui.open && !!ui.sessionId}
39
+ onOpenChange={(open) => !open && exportUiActions.close()}
40
+ >
41
+ {ui.sessionId && (
42
+ <SheetBody
43
+ key={ui.sessionId}
44
+ sessionId={ui.sessionId}
45
+ title={ui.title}
46
+ />
47
+ )}
48
+ </Dialog>
49
+ {ui.selecting && <TurnSelectBar />}
50
+ </>
51
+ );
52
+ }
53
+
54
+ function SheetBody({ sessionId, title }: { sessionId: string; title: string }) {
55
+ const { req, fromItemId } = useSnapshot(exportUi);
56
+ const [est, setEst] = useState<ExportEstimate | null>(null);
57
+ const [error, setError] = useState<string | null>(null);
58
+ const estimate = useExportEstimate(sessionId);
59
+ const seq = useRef(0);
60
+ const wire = JSON.stringify(req);
61
+
62
+ useEffect(() => {
63
+ const mine = ++seq.current;
64
+ const timer = setTimeout(async () => {
65
+ try {
66
+ const e = await estimate.mutateAsync(JSON.parse(wire) as ExportRequest);
67
+ if (mine !== seq.current) return;
68
+ setEst(e);
69
+ setError(null);
70
+ exportUi.turns = e.turns;
71
+ const from = exportUi.fromItemId;
72
+ if (from) {
73
+ exportUi.fromItemId = null;
74
+ const turn = e.turns.find((t) => t.id === from);
75
+ if (turn)
76
+ exportUiActions.setRequest({
77
+ range: { from: turn.n, to: e.selected.of },
78
+ });
79
+ }
80
+ } catch (err) {
81
+ if (mine === seq.current) setError((err as Error).message);
82
+ }
83
+ }, 250);
84
+ return () => clearTimeout(timer);
85
+ // The mutation object changes every render; the request is the input.
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, [wire, fromItemId]);
88
+
89
+ const name = est?.title ?? title;
90
+ const set = exportUiActions.setRequest;
91
+ const size = est?.estimate[req.mode];
92
+ const stale = estimate.isPending;
93
+ const token = (m: ExportMode) =>
94
+ est ? tokenLabel(est.estimate[m].tokens).replace("about ", "~") : " ";
95
+
96
+ return (
97
+ <DialogContent className="flex h-[100dvh] w-screen max-w-none flex-col gap-3 rounded-none p-4 sm:h-auto sm:max-h-[90vh] sm:max-w-[560px] sm:rounded-lg">
98
+ <div>
99
+ <DialogTitle>Export for AI</DialogTitle>
100
+ <DialogDescription className="truncate">{name}</DialogDescription>
101
+ </div>
102
+
103
+ <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
104
+ <Section label="Format">
105
+ <Seg<ExportFormat>
106
+ label="Format"
107
+ value={req.format}
108
+ options={EXPORT_FORMATS.map((f) => ({ id: f.id, label: f.label }))}
109
+ onChange={(format) => set({ format })}
110
+ />
111
+ <p className="text-muted-foreground text-xs">
112
+ {EXPORT_FORMATS.find((f) => f.id === req.format)?.hint}
113
+ </p>
114
+ </Section>
115
+
116
+ <Section label="Detail">
117
+ <Seg<ExportMode>
118
+ label="Detail"
119
+ value={req.mode}
120
+ options={[
121
+ { id: "compact", label: "Compact", sub: token("compact") },
122
+ { id: "full", label: "Full", sub: token("full") },
123
+ ]}
124
+ onChange={(mode) => set({ mode })}
125
+ />
126
+ <details className="text-muted-foreground text-xs">
127
+ <summary className="flex min-h-8 cursor-pointer items-center">
128
+ What Compact drops
129
+ </summary>
130
+ <ul className="list-disc space-y-0.5 pl-5">
131
+ {COMPACT_RULES.map((r) => (
132
+ <li key={r}>{r}</li>
133
+ ))}
134
+ </ul>
135
+ </details>
136
+ </Section>
137
+
138
+ <Section label="Range">
139
+ <RangePicker
140
+ range={req.range}
141
+ est={est}
142
+ onChange={(range) => set({ range })}
143
+ onSelectInChat={() => exportUiActions.startSelect()}
144
+ />
145
+ </Section>
146
+
147
+ <Section label="Masking">
148
+ <label className="flex min-h-11 items-center justify-between gap-3 text-sm">
149
+ <span>Mask paths, machine names and emails</span>
150
+ <Switch
151
+ checked={req.redact === "all"}
152
+ onCheckedChange={(on) => set({ redact: on ? "all" : "secrets" })}
153
+ />
154
+ </label>
155
+ <p className="text-muted-foreground text-xs">
156
+ Credentials are always masked.{" "}
157
+ {est ? redactionStatus(est.hits) + "." : ""}
158
+ </p>
159
+ </Section>
160
+
161
+ <div
162
+ className={cn(
163
+ "space-y-2 rounded-lg border p-3 text-sm",
164
+ stale && "opacity-60"
165
+ )}
166
+ aria-live="polite"
167
+ >
168
+ <p className="font-medium">
169
+ {size
170
+ ? `${formatBytes(size.bytes)} · ${tokenLabel(size.tokens)}`
171
+ : (error ?? "Working out the size…")}
172
+ </p>
173
+ {est?.warnings.map((w) => (
174
+ <p
175
+ key={w.code}
176
+ className={cn(
177
+ "flex gap-2 text-xs",
178
+ w.level === "danger" && "text-destructive",
179
+ w.level === "warn" && "text-amber-600 dark:text-amber-400",
180
+ w.level === "info" && "text-muted-foreground"
181
+ )}
182
+ >
183
+ {w.level === "info" ? (
184
+ <Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
185
+ ) : (
186
+ <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
187
+ )}
188
+ <span>{w.text}</span>
189
+ </p>
190
+ ))}
191
+ </div>
192
+ </div>
193
+
194
+ <ExportActions
195
+ sessionId={sessionId}
196
+ title={name}
197
+ req={req as ExportRequest}
198
+ est={est}
199
+ />
200
+ </DialogContent>
201
+ );
202
+ }
203
+
204
+ function Section({
205
+ label,
206
+ children,
207
+ }: {
208
+ label: string;
209
+ children: React.ReactNode;
210
+ }) {
211
+ return (
212
+ <section className="space-y-2">
213
+ <h3 className="text-muted-foreground text-xs font-medium">{label}</h3>
214
+ {children}
215
+ </section>
216
+ );
217
+ }
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import { MousePointerClick } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { cn } from "@/lib/utils";
6
+ import type { ExportEstimate } from "@/data/exports";
7
+ import type { ExportRange } from "@/lib/export/view";
8
+
9
+ type Chip = "all" | "last1" | "last5" | "compaction" | "custom";
10
+
11
+ const CHIPS: { id: Chip; label: string }[] = [
12
+ { id: "all", label: "Whole session" },
13
+ { id: "last1", label: "Last turn" },
14
+ { id: "last5", label: "Last 5" },
15
+ { id: "compaction", label: "Since compaction" },
16
+ { id: "custom", label: "Custom" },
17
+ ];
18
+
19
+ const chipOf = (r: ExportRange | undefined): Chip =>
20
+ !r
21
+ ? "all"
22
+ : "sinceCompaction" in r
23
+ ? "compaction"
24
+ : "last" in r
25
+ ? r.last === 1
26
+ ? "last1"
27
+ : r.last === 5
28
+ ? "last5"
29
+ : "custom"
30
+ : "custom";
31
+
32
+ const when = (at: string) => at.slice(5, 16).replace("T", " ");
33
+ const clock = (at: string) => at.slice(11, 16);
34
+
35
+ const SELECT =
36
+ "bg-background border-input min-h-11 w-full min-w-0 rounded-md border px-2 text-sm";
37
+
38
+ export function RangePicker({
39
+ range,
40
+ est,
41
+ onChange,
42
+ onSelectInChat,
43
+ }: {
44
+ range: ExportRange | undefined;
45
+ est: ExportEstimate | null;
46
+ onChange: (r: ExportRange | undefined) => void;
47
+ onSelectInChat: () => void;
48
+ }) {
49
+ const chip = chipOf(range);
50
+ const turns = (est?.turns ?? []).filter((t) => t.n > 0);
51
+ const sel = est?.selected;
52
+ const pick = (id: Chip) => {
53
+ if (id === "all") onChange(undefined);
54
+ else if (id === "last1") onChange({ last: 1 });
55
+ else if (id === "last5") onChange({ last: 5 });
56
+ else if (id === "compaction") onChange({ sinceCompaction: true });
57
+ else
58
+ onChange({
59
+ from: sel?.from ?? 1,
60
+ to: sel?.to ?? Math.max(1, turns.length),
61
+ });
62
+ };
63
+ const custom = range && "from" in range ? range : null;
64
+ const options = turns.map((t) => (
65
+ <option key={t.n} value={t.n}>
66
+ {`#${t.n} · ${when(t.at)} · ${t.excerpt.slice(0, 40)}`}
67
+ </option>
68
+ ));
69
+
70
+ return (
71
+ <div className="space-y-2">
72
+ <div className="flex flex-wrap gap-1.5" role="group" aria-label="Range">
73
+ {CHIPS.map((c) => (
74
+ <button
75
+ key={c.id}
76
+ type="button"
77
+ aria-pressed={chip === c.id}
78
+ onClick={() => pick(c.id)}
79
+ className={cn(
80
+ "min-h-11 rounded-full border px-3.5 text-sm transition-colors",
81
+ chip === c.id
82
+ ? "border-primary bg-primary/10 text-foreground"
83
+ : "text-muted-foreground hover:bg-foreground/[0.04]"
84
+ )}
85
+ >
86
+ {c.label}
87
+ </button>
88
+ ))}
89
+ </div>
90
+
91
+ {custom && (
92
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-2">
93
+ <label className="text-muted-foreground space-y-1 text-xs">
94
+ From
95
+ <select
96
+ className={SELECT}
97
+ value={custom.from}
98
+ onChange={(e) =>
99
+ onChange({
100
+ from: Number(e.target.value),
101
+ to: Math.max(custom.to, Number(e.target.value)),
102
+ })
103
+ }
104
+ >
105
+ {options}
106
+ </select>
107
+ </label>
108
+ <label className="text-muted-foreground space-y-1 text-xs">
109
+ To
110
+ <select
111
+ className={SELECT}
112
+ value={custom.to}
113
+ onChange={(e) =>
114
+ onChange({
115
+ from: Math.min(custom.from, Number(e.target.value)),
116
+ to: Number(e.target.value),
117
+ })
118
+ }
119
+ >
120
+ {options}
121
+ </select>
122
+ </label>
123
+ </div>
124
+ )}
125
+
126
+ <div className="flex items-center justify-between gap-2">
127
+ <p className="text-muted-foreground min-w-0 text-xs">
128
+ {sel && sel.startedAt && sel.endedAt
129
+ ? `Turns ${sel.from}–${sel.to} · ${sel.turns} ${sel.turns === 1 ? "turn" : "turns"} · ${clock(sel.startedAt)}–${clock(sel.endedAt)}`
130
+ : "Counting turns…"}
131
+ </p>
132
+ <Button
133
+ variant="outline"
134
+ size="sm"
135
+ className="min-h-11 shrink-0"
136
+ onClick={onSelectInChat}
137
+ >
138
+ <MousePointerClick className="h-4 w-4" />
139
+ Select in chat
140
+ </Button>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ // A row of exclusive choices, each at least 44px tall.
6
+ export function Seg<T extends string>({
7
+ label,
8
+ value,
9
+ options,
10
+ onChange,
11
+ }: {
12
+ label: string;
13
+ value: T;
14
+ options: { id: T; label: string; sub?: string }[];
15
+ onChange: (id: T) => void;
16
+ }) {
17
+ return (
18
+ <div className="flex gap-1.5" role="group" aria-label={label}>
19
+ {options.map((o) => (
20
+ <button
21
+ key={o.id}
22
+ type="button"
23
+ aria-pressed={value === o.id}
24
+ onClick={() => onChange(o.id)}
25
+ className={cn(
26
+ "flex min-h-11 min-w-0 flex-1 flex-col items-center justify-center rounded-lg border px-2 text-sm transition-colors",
27
+ value === o.id
28
+ ? "border-primary bg-primary/10 text-foreground"
29
+ : "text-muted-foreground hover:bg-foreground/[0.04]"
30
+ )}
31
+ >
32
+ <span className="font-medium">{o.label}</span>
33
+ {o.sub && <span className="text-[11px] opacity-80">{o.sub}</span>}
34
+ </button>
35
+ ))}
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useSnapshot } from "valtio";
4
+ import { Button } from "@/components/ui/button";
5
+ import { exportUi, exportUiActions, pickedRange } from "@/stores/exportUi";
6
+ import { tokenLabel } from "@/lib/export/view";
7
+
8
+ // The bar under the chat while turns are being picked for an export.
9
+ export function TurnSelectBar() {
10
+ const { pick, turns } = useSnapshot(exportUi);
11
+ const range = pickedRange(pick);
12
+ const picked = range
13
+ ? turns.filter((t) => t.n >= range.from && t.n <= range.to)
14
+ : [];
15
+ const tokens = picked.reduce((sum, t) => sum + t.tokens, 0);
16
+ const count = range ? range.to - range.from + 1 : 0;
17
+ const text = !range
18
+ ? "Tap the first turn to export"
19
+ : pick.to === null
20
+ ? `Turn ${range.from} · tap the last turn, or export just this one`
21
+ : `Turns ${range.from}–${range.to} · ${count} ${count === 1 ? "turn" : "turns"} · ${tokenLabel(tokens)}`;
22
+ return (
23
+ <div
24
+ role="region"
25
+ aria-label="Select turns to export"
26
+ className="bg-background fixed inset-x-0 bottom-0 z-40 flex flex-wrap items-center gap-2 border-t p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
27
+ >
28
+ <p className="min-w-[140px] flex-1 text-sm">{text}</p>
29
+ <Button
30
+ variant="outline"
31
+ className="min-h-11"
32
+ onClick={exportUiActions.cancelSelect}
33
+ >
34
+ Cancel
35
+ </Button>
36
+ <Button
37
+ className="min-h-11"
38
+ disabled={!range}
39
+ onClick={exportUiActions.finishSelect}
40
+ >
41
+ Export…
42
+ </Button>
43
+ </div>
44
+ );
45
+ }
@@ -3,13 +3,14 @@
3
3
  import { useState } from "react";
4
4
  import { ExternalLink, Link2, Loader2, RefreshCw } from "lucide-react";
5
5
  import { Button } from "@/components/ui/button";
6
+ import { Notice } from "@/components/ui/notice";
6
7
  import { LinkForm } from "@/components/Welcome/ConnectBlock";
7
8
  import {
8
9
  useAccountMachines,
9
10
  useRefreshAccount,
10
11
  useStartLink,
11
12
  } from "@/data/peers";
12
- import { CONNECT_SITE } from "@/lib/connect/view";
13
+ import { UPGRADE_URL } from "@/lib/peer/reasons";
13
14
  import type { AccountMachine } from "@/lib/peer/machines";
14
15
  import { cn } from "@/lib/utils";
15
16
  import { WaitingLinks } from "./IncomingLinkSheet";
@@ -57,7 +58,12 @@ function MachineRow({
57
58
  Linked
58
59
  </span>
59
60
  ) : machine.linkState === "suspended" ? (
60
- <span className="text-muted-foreground text-xs">Suspended</span>
61
+ <span
62
+ className="text-xs text-amber-700 dark:text-amber-400"
63
+ title={machine.suspendedReason ?? undefined}
64
+ >
65
+ Suspended
66
+ </span>
61
67
  ) : (
62
68
  <Button
63
69
  variant="outline"
@@ -122,23 +128,31 @@ export function AccountMachines() {
122
128
  />
123
129
  </Button>
124
130
  </div>
131
+ {data.uncheckedSince && (
132
+ <Notice tone="warning">
133
+ Couldn&apos;t check your account since{" "}
134
+ {new Date(data.uncheckedSince).toLocaleString([], {
135
+ dateStyle: "medium",
136
+ timeStyle: "short",
137
+ })}
138
+ . Machines already linked keep working; new links wait until it&apos;s
139
+ back.
140
+ </Notice>
141
+ )}
125
142
  {data.entitlements && !entitled && (
126
- <div className="flex flex-col gap-2 rounded-lg border p-3 sm:flex-row sm:items-center">
127
- <p className="flex-1 text-sm">
128
- Link your machines with Pro: tasks move between them over any
129
- network, end-to-end encrypted.
130
- </p>
131
- <Button asChild variant="outline" className="min-h-11 sm:min-h-9">
132
- <a
133
- href={`${CONNECT_SITE}/settings/billing`}
134
- target="_blank"
135
- rel="noopener noreferrer"
136
- >
137
- Upgrade
138
- <ExternalLink className="h-3.5 w-3.5" />
139
- </a>
140
- </Button>
141
- </div>
143
+ <Notice
144
+ action={
145
+ <Button asChild variant="outline" className="min-h-11 sm:min-h-9">
146
+ <a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
147
+ Upgrade
148
+ <ExternalLink className="h-3.5 w-3.5" />
149
+ </a>
150
+ </Button>
151
+ }
152
+ >
153
+ Link your machines with Pro: tasks move between them over any network,
154
+ end-to-end encrypted.
155
+ </Notice>
142
156
  )}
143
157
  {entitled && <WaitingLinks />}
144
158
  {data.error && (