@saadnvd1/agent-os 0.3.715 → 0.3.728

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 (242) 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/spawn/route.ts +22 -1
  11. package/app/api/loops/route.ts +17 -0
  12. package/app/api/models/availability/route.ts +9 -0
  13. package/app/api/models/choices/route.ts +9 -0
  14. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  15. package/app/api/sessions/[id]/crew/route.ts +38 -0
  16. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  17. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  18. package/app/api/sessions/[id]/export/route.ts +34 -0
  19. package/app/api/sessions/[id]/model/route.ts +49 -0
  20. package/app/api/sessions/route.ts +16 -0
  21. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  22. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  23. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  24. package/app/api/tasks/[id]/loop/route.ts +72 -0
  25. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  26. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  27. package/app/api/tasks/[id]/mode/route.ts +29 -0
  28. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  29. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  30. package/app/api/tasks/[id]/report/route.ts +39 -0
  31. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  32. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  33. package/app/page.tsx +13 -0
  34. package/components/Analyst/PairChip.tsx +131 -0
  35. package/components/Analyst/PairSwitch.tsx +76 -0
  36. package/components/Analyst/ReviewCard.tsx +272 -0
  37. package/components/Analyst/SignalChips.tsx +39 -0
  38. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  39. package/components/Analyst/cardState.test.ts +109 -0
  40. package/components/Analyst/cardState.ts +95 -0
  41. package/components/Analyst/pair.ts +81 -0
  42. package/components/Chat/Code.tsx +3 -0
  43. package/components/Chat/Messages.tsx +110 -11
  44. package/components/Chat/itemContext.ts +14 -0
  45. package/components/Draft/DraftChips.tsx +16 -1
  46. package/components/Draft/DraftPanel.tsx +58 -2
  47. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  48. package/components/Draft/RoleChips.tsx +110 -0
  49. package/components/Export/ExportActions.tsx +136 -0
  50. package/components/Export/ExportSheet.tsx +217 -0
  51. package/components/Export/RangePicker.tsx +144 -0
  52. package/components/Export/Seg.tsx +38 -0
  53. package/components/Export/TurnSelectBar.tsx +45 -0
  54. package/components/Models/AgentRoleCard.tsx +114 -0
  55. package/components/Models/AgentRolesSection.tsx +132 -0
  56. package/components/Models/ModelBadges.tsx +48 -0
  57. package/components/Models/ModelsSection.tsx +4 -2
  58. package/components/Models/RoleModelSelect.tsx +87 -0
  59. package/components/Pane/DesktopTabBar.tsx +8 -0
  60. package/components/Pane/MobileTabBar.tsx +29 -0
  61. package/components/Pane/MobileViewPicker.tsx +4 -0
  62. package/components/Pane/PaneMenu.test.ts +21 -0
  63. package/components/Pane/PaneMenu.tsx +26 -0
  64. package/components/Pane/PaneViewToggle.tsx +12 -0
  65. package/components/Pane/index.tsx +19 -0
  66. package/components/Report/DraftCard.tsx +89 -0
  67. package/components/Report/EstimateBar.tsx +51 -0
  68. package/components/Report/EvidenceSheet.tsx +98 -0
  69. package/components/Report/ModeChooser.tsx +43 -0
  70. package/components/Report/ReportView.tsx +289 -0
  71. package/components/SessionList/SessionRow.tsx +18 -7
  72. package/components/SessionList/SessionRowMenu.tsx +12 -0
  73. package/components/Sessions/RolePill.tsx +203 -0
  74. package/components/Settings/SettingsDialog.tsx +9 -0
  75. package/components/Share/ShareDialog.tsx +13 -0
  76. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  77. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  78. package/components/Tasks/Loop/LoopView.tsx +350 -0
  79. package/components/Tasks/Loop/labels.ts +17 -0
  80. package/components/Tasks/TaskRow.tsx +2 -0
  81. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  82. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  83. package/components/ui/segmented.tsx +55 -0
  84. package/components/ui/status-pill.tsx +3 -0
  85. package/contexts/PaneContext.tsx +21 -1
  86. package/data/analyst/index.ts +3 -0
  87. package/data/analyst/keys.ts +6 -0
  88. package/data/analyst/mutations.ts +116 -0
  89. package/data/analyst/queries.ts +35 -0
  90. package/data/exports/index.ts +2 -0
  91. package/data/exports/keys.ts +3 -0
  92. package/data/exports/queries.ts +51 -0
  93. package/data/loops/index.ts +3 -0
  94. package/data/loops/keys.ts +4 -0
  95. package/data/loops/mutations.ts +68 -0
  96. package/data/loops/queries.ts +40 -0
  97. package/data/models/index.ts +98 -0
  98. package/data/push/topics.ts +11 -0
  99. package/data/reports/index.ts +115 -0
  100. package/data/sessions/queries.ts +26 -0
  101. package/lib/agents/brief.ts +1 -0
  102. package/lib/analyst/__fixtures__/items.ts +57 -0
  103. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  104. package/lib/analyst/brief.ts +17 -0
  105. package/lib/analyst/buildDigest.ts +204 -0
  106. package/lib/analyst/createLink.ts +98 -0
  107. package/lib/analyst/endLink.ts +31 -0
  108. package/lib/analyst/forward.ts +109 -0
  109. package/lib/analyst/index.ts +16 -0
  110. package/lib/analyst/linkView.ts +38 -0
  111. package/lib/analyst/onTurnEnd.ts +119 -0
  112. package/lib/analyst/parseReview.ts +41 -0
  113. package/lib/analyst/pushDigest.ts +143 -0
  114. package/lib/analyst/resume.ts +35 -0
  115. package/lib/analyst/route.ts +50 -0
  116. package/lib/analyst/signals.ts +121 -0
  117. package/lib/analyst/store.ts +303 -0
  118. package/lib/analyst/types.ts +140 -0
  119. package/lib/bus/condense.ts +5 -1
  120. package/lib/bus/index.ts +5 -0
  121. package/lib/bus/read-scope.ts +12 -0
  122. package/lib/bus/role-alias.ts +70 -0
  123. package/lib/bus/targets.ts +3 -0
  124. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  125. package/lib/chat/drivers/pi-approvals.ts +7 -4
  126. package/lib/chat/drivers/pi-mapper.ts +38 -0
  127. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  128. package/lib/chat/drivers/pi.ts +8 -3
  129. package/lib/chat/drivers/turn-items.ts +36 -11
  130. package/lib/chat/drivers/turn-outcome.ts +108 -0
  131. package/lib/chat/events.ts +8 -0
  132. package/lib/chat/registry.ts +13 -0
  133. package/lib/chat/store.ts +19 -0
  134. package/lib/chat/turns.ts +1 -0
  135. package/lib/chat/worker/extras.ts +4 -2
  136. package/lib/claims/collectLocal.ts +3 -1
  137. package/lib/db/changes.ts +13 -0
  138. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  139. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  140. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  141. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  142. package/lib/db/session-parts/columns.ts +2 -0
  143. package/lib/db/types.ts +3 -0
  144. package/lib/done/archive.ts +2 -0
  145. package/lib/done/index.ts +2 -0
  146. package/lib/drafts.ts +6 -0
  147. package/lib/export/build.ts +470 -0
  148. package/lib/export/compact.ts +156 -0
  149. package/lib/export/estimate.ts +83 -0
  150. package/lib/export/fixtures.ts +45 -0
  151. package/lib/export/html.tsx +195 -0
  152. package/lib/export/json.ts +39 -0
  153. package/lib/export/load.ts +64 -0
  154. package/lib/export/markdown.ts +127 -0
  155. package/lib/export/request.ts +36 -0
  156. package/lib/export/service.ts +142 -0
  157. package/lib/export/summary.ts +86 -0
  158. package/lib/export/turns.ts +63 -0
  159. package/lib/export/view.ts +173 -0
  160. package/lib/loops/analyse.ts +46 -0
  161. package/lib/loops/analyst-prompt.ts +203 -0
  162. package/lib/loops/checkCriteria.ts +114 -0
  163. package/lib/loops/compactEvidence.ts +140 -0
  164. package/lib/loops/controls.ts +317 -0
  165. package/lib/loops/decideStop.ts +113 -0
  166. package/lib/loops/deps.ts +128 -0
  167. package/lib/loops/diffMap.ts +39 -0
  168. package/lib/loops/draftCriteria.ts +71 -0
  169. package/lib/loops/gatherEvidence.ts +265 -0
  170. package/lib/loops/hooks.ts +40 -0
  171. package/lib/loops/index.ts +7 -0
  172. package/lib/loops/route.ts +83 -0
  173. package/lib/loops/runCycle.ts +323 -0
  174. package/lib/loops/screenInstruction.ts +59 -0
  175. package/lib/loops/sendInstruction.ts +83 -0
  176. package/lib/loops/snapshot.ts +38 -0
  177. package/lib/loops/stopLoop.ts +86 -0
  178. package/lib/loops/store.ts +481 -0
  179. package/lib/loops/summary.ts +27 -0
  180. package/lib/loops/tickLoops.ts +141 -0
  181. package/lib/loops/types.ts +223 -0
  182. package/lib/loops/world.test-helpers.ts +223 -0
  183. package/lib/models/agent-binding.ts +102 -0
  184. package/lib/models/agent-roles-view.ts +157 -0
  185. package/lib/models/agent-roles.ts +87 -0
  186. package/lib/models/availability-refs.ts +17 -0
  187. package/lib/models/availability.ts +91 -0
  188. package/lib/models/catalog.ts +156 -0
  189. package/lib/models/config.ts +27 -0
  190. package/lib/models/locality.ts +33 -0
  191. package/lib/models/plan-step.ts +4 -1
  192. package/lib/models/providers.ts +11 -0
  193. package/lib/models/resolve.ts +50 -12
  194. package/lib/models/roles.ts +12 -0
  195. package/lib/needs-you.ts +1 -1
  196. package/lib/orchestrator/digest.ts +2 -1
  197. package/lib/orchestrator/facts.ts +7 -2
  198. package/lib/orchestrator/loop-guard.ts +5 -2
  199. package/lib/orchestrator/pr-wake.ts +4 -0
  200. package/lib/orchestrator/read.ts +5 -1
  201. package/lib/orchestrator/untrusted.ts +5 -0
  202. package/lib/orchestrator/watcher.ts +8 -1
  203. package/lib/panes.ts +52 -0
  204. package/lib/plans/acceptance.ts +23 -0
  205. package/lib/projects.ts +1 -0
  206. package/lib/reports/drafts.ts +87 -0
  207. package/lib/reports/end-hook.ts +17 -0
  208. package/lib/reports/estimate.ts +107 -0
  209. package/lib/reports/evidence/build.ts +193 -0
  210. package/lib/reports/evidence/gather.ts +229 -0
  211. package/lib/reports/fixtures.ts +17 -0
  212. package/lib/reports/generate.ts +159 -0
  213. package/lib/reports/hooks.ts +30 -0
  214. package/lib/reports/mode.ts +65 -0
  215. package/lib/reports/model.ts +115 -0
  216. package/lib/reports/read.ts +26 -0
  217. package/lib/reports/store.ts +180 -0
  218. package/lib/reports/template.ts +117 -0
  219. package/lib/reports/types.ts +135 -0
  220. package/lib/reports/validate.ts +142 -0
  221. package/lib/session-artifacts/agentos-files.ts +94 -0
  222. package/lib/session-artifacts/collect.ts +17 -0
  223. package/lib/sessions/crew.ts +72 -0
  224. package/lib/sessions/launch.ts +49 -9
  225. package/lib/settings-url.ts +1 -0
  226. package/lib/share/markdown.ts +2 -2
  227. package/lib/share/redact.ts +16 -10
  228. package/lib/share/render.tsx +1 -1
  229. package/lib/share/snapshot.ts +2 -2
  230. package/lib/sidebar/row-menu.ts +15 -1
  231. package/lib/tasks/contract/routes.ts +5 -0
  232. package/lib/tasks/contract/types.ts +3 -0
  233. package/lib/tasks/finish.ts +2 -0
  234. package/lib/tasks/index.ts +6 -0
  235. package/lib/tasks/move-carry.ts +1 -0
  236. package/lib/worktree-deps/caches.ts +3 -2
  237. package/lib/worktree-deps/clone.ts +3 -1
  238. package/lib/worktree-deps/paths.ts +26 -0
  239. package/package.json +2 -2
  240. package/server.ts +9 -1
  241. package/stores/analystUi.ts +19 -0
  242. package/stores/exportUi.ts +87 -0
@@ -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
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { AlertTriangle } from "lucide-react";
4
+ import type { AgentRoleView } from "@/lib/models/agent-roles-view";
5
+ import { cn } from "@/lib/utils";
6
+ import { RoleModelSelect } from "./RoleModelSelect";
7
+
8
+ const SOURCE: Record<string, string> = {
9
+ task: "Set for the task",
10
+ project: "From this project",
11
+ workspace: "From the workspace",
12
+ settings: "Set in Settings",
13
+ env: "From the server's environment",
14
+ inherited: "Same as Gate review until set here",
15
+ default: "Default",
16
+ };
17
+
18
+ const initial = (label: string) => label.slice(0, 1);
19
+
20
+ // One role: what it does, the model it runs on, where that came from, and
21
+ // whether the model's server answers. A role whose local model is down says so.
22
+ export function AgentRoleCard({
23
+ role,
24
+ scope,
25
+ pending,
26
+ onChange,
27
+ onFallback,
28
+ }: {
29
+ role: AgentRoleView;
30
+ scope: "settings" | "workspace" | "project";
31
+ pending: boolean;
32
+ onChange: (ref: string | null) => void;
33
+ onFallback: (ref: string | null) => void;
34
+ }) {
35
+ const down = role.choice?.availability.state === "down";
36
+ const label = `Default (${role.default.split("/").slice(1).join("/")})`;
37
+ const inheritLabel =
38
+ scope === "settings"
39
+ ? role.modelRole === "advise"
40
+ ? "Same as Gate review"
41
+ : label
42
+ : "Inherit";
43
+ return (
44
+ <li
45
+ className={cn(
46
+ "bg-muted/40 space-y-2 rounded-xl px-3 py-3",
47
+ down && "ring-destructive/50 ring-1"
48
+ )}
49
+ >
50
+ <label htmlFor={`agent-role-${role.id}`} className="flex gap-2.5">
51
+ <span className="bg-accent text-primary grid h-8 w-8 shrink-0 place-items-center rounded-lg text-sm font-semibold">
52
+ {initial(role.label)}
53
+ </span>
54
+ <span className="min-w-0">
55
+ <span className="block text-sm font-medium">{role.label}</span>
56
+ <span className="text-muted-foreground block text-xs">
57
+ {role.purpose}
58
+ </span>
59
+ </span>
60
+ </label>
61
+ <RoleModelSelect
62
+ id={`agent-role-${role.id}`}
63
+ value={role.value}
64
+ inheritLabel={
65
+ role.value === null && role.binding
66
+ ? `${inheritLabel} · ${role.binding.ref.split("/").slice(1).join("/")}`
67
+ : inheritLabel
68
+ }
69
+ disabled={pending}
70
+ onChange={onChange}
71
+ />
72
+ <p className="text-muted-foreground text-xs">
73
+ {role.error
74
+ ? role.error
75
+ : role.binding
76
+ ? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
77
+ : ""}
78
+ </p>
79
+ {down && (
80
+ <div
81
+ role="alert"
82
+ className="bg-destructive/10 text-foreground ring-destructive/35 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
83
+ >
84
+ <AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
85
+ <span>
86
+ <b className="font-medium">
87
+ This model&apos;s server isn&apos;t answering.
88
+ </b>{" "}
89
+ {role.choice?.availability.error
90
+ ? `${role.choice.availability.error}. `
91
+ : ""}
92
+ New {role.label} sessions can&apos;t start until it is back
93
+ {role.fallback ? ", or you start on the fallback" : ""}.
94
+ </span>
95
+ </div>
96
+ )}
97
+ {scope === "settings" && (
98
+ <div className="flex flex-wrap items-center gap-2">
99
+ <span className="text-muted-foreground text-xs">Fallback</span>
100
+ <div className="min-w-44 flex-1">
101
+ <RoleModelSelect
102
+ id={`agent-role-fallback-${role.id}`}
103
+ value={role.fallback}
104
+ inheritLabel="None (don't fall back)"
105
+ claudeOnly
106
+ disabled={pending}
107
+ onChange={onFallback}
108
+ />
109
+ </div>
110
+ </div>
111
+ )}
112
+ </li>
113
+ );
114
+ }